
풀스택 - 1주차
강의 수강
supabase 장점
오픈소스 프로젝트 (자체 서버구축 가능)
PostgreSQL 기반 (관계형 DB 장점을 살릴 수 있다)
Firebase 대비 저렴
다양한 연동방식 지원
NEXT.JS 특징
“use server” → 서버에서만 동작함. (DB를 연결해도 됨)
fetch + REST API 조합 api/route.ts를 사용하여 api를 만들수 있음.
Server Actions api 구현을 하지않아도 데이터를 구현할 수 있다.
SEO를 위한 Metadata
투두리스트 CRUD 기능 구현
actions/todo-action.ts
"use server"; // Next.js의 서버 액션(Server Action)으로 사용하기 위해 명시해야 함.
import { Database } from "types_db";
import { createServerSupabaseClient } from "utils/supabase/server";
// 📝 todo 테이블에서 조회할 때 반환되는 데이터의 타입
export type TodoRow = Database["public"]["Tables"]["todo"]["Row"];
// 📝 todo 테이블에 새로운 데이터를 추가할 때 필요한 데이터의 타입
export type TodoRowInsert = Database["public"]["Tables"]["todo"]["Insert"];
// 📝 todo 테이블에서 기존 데이터를 수정할 때 사용할 데이터의 타입
export type TodoRowUpdate = Database["public"]["Tables"]["todo"]["Update"];
// 🔹 에러 핸들링 함수
function handleError(error: any) {
console.error(error); // 에러 로그 출력
throw new Error(error.message); // 에러 메시지를 포함한 예외 발생
}
export async function getTodos({ searchInput = "" }) {
const supabase = await createServerSupabaseClient();
// 🔹 Supabase를 통해 todo 테이블에서 데이터 조회
const { data, error } = await supabase // ✅ `await` 추가하여 비동기 데이터 처리
.from("todo") // 📌 todo 테이블에 접근
.select("*") // 📌 모든 컬럼 조회
.like("title", `%${searchInput}%`) // 📌 LIKE 연산자로 title에 searchInput이 포함된 데이터 필터링
.order("created_at", { ascending: true }); // 📌 생성 날짜 기준 오름차순 정렬
if (error) {
handleError(error); // 에러 발생 시 핸들링
}
return data; // 조회된 데이터 반환
}
export async function createTodo(todo: TodoRowInsert) {
const supabase = await createServerSupabaseClient(); // ✅ Supabase 서버 클라이언트 생성
// 🔹 Supabase를 사용하여 todo 테이블에 새로운 데이터 삽입
const { data, error } = await supabase
.from("todo") // 📌 todo 테이블에 접근
.insert({
...todo, // 📌 클라이언트에서 받은 데이터(todo) 삽입
created_at: new Date().toISOString(), // 📌 클라이언트에서 전달된 값이 이상할 수 있으므로, 서버에서 직접 현재 시간을 생성하여 추가
});
if (error) {
handleError(error); // 🔹 에러 발생 시 핸들링
}
return data; // ✅ 삽입된 데이터 반환
}
//todo 업데이트
export async function updateTodo(todo: TodoRowUpdate) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase
.from("todo")
.update({
...todo,
updated_at: new Date().toISOString(),
})
.eq("id", todo.id); // 📌 id가 일치하는 행만 업데이트
if (error) {
handleError(error);
}
return data;
}
//todo 삭제
export async function deleteTodo(id: number) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase.from("todo").delete().eq("id", id);
if (error) {
handleError(error);
}
return data;
}
new Date().toISOString()
이란?현재 시간을 ISO 8601 형식의 문자열로 변환하는 메서드입니다.
1⃣ 시간 표준화 (Time Standardization)
문제: 서버와 클라이언트의 시간대가 다를 수 있음
사용자가 다른 시간대(한국, 미국, 유럽 등)에서 앱을 사용할 수 있음.
예를 들어, 한국(KST)에서 저장한 날짜가 미국(PST)에서 보면 이상하게 보일 수 있음.
해결책:
모든 시간을 UTC 기준으로 저장하여 시간대를 통일하면 문제 해결!
new Date().toISOString()
은 항상 UTC(세계 표준시)로 변환되므로, 서버-클라이언트 간 시간 오차 없이 일관성 유지 가능.
2⃣ 클라이언트와 서버 간 시간 차이 해결
문제: 클라이언트에서
new Date()
를 사용하면 각 기기의 로컬 시간이 저장됨만약 한국(KST)에서
new Date()
를 저장하면2025-03-02T23:30:45
미국(PST)에서
new Date()
를 저장하면2025-03-02T06:30:45
서버에서 보면 시간이 뒤죽박죽이 됨 😵
해결책:
모든 시간을 UTC 기준으로 저장(
new Date().toISOString()
)클라이언트가 받을 때 필요한 시간대로 변환해서 사용
예시:
js 복사편집 const utcTime = new Date().toISOString(); console.log(utcTime); // "2025-03-02T14:30:45.123Z" (UTC)
한국(KST)에서는
2025-03-02 23:30:45
로 변환해서 보여주면 됨.미국(PST)에서는
2025-03-02 06:30:45
로 변환해서 보여주면 됨.
3⃣ 데이터베이스와의 호환성
문제: DB에서
created_at
필드를 올바르게 저장하려면?Supabase(PostgreSQL)에서는
TIMESTAMP
또는TIMESTAMP WITH TIME ZONE
타입을 사용함.클라이언트에서 로컬 시간을 넣으면, DB에서 잘못 해석할 수 있음.
해결책:
ISO 8601
형식(2025-03-02T14:30:45.123Z
)을 사용하면 DB에서 자동 변환 가능!Supabase에서
created_at
이TIMESTAMP
필드라면,new Date().toISOString()
을 넣으면 자동으로 올바른 값이 저장됨.
미션
TODO 항목 옆에 생성시간을 표시하기
테이블에서 생성시간을 뽑아오니 utc기준으로 보여지고 있었다. 한국시간으로 바꾸기위해 day.js 라이브러리를 활용하였다. 🤟🏻 created_at(UTC)을 로컬 시간으로 변환 로컬 시간(Local Time)이란 현재 사용자의 컴퓨터(브라우저) 또는 서버에서 설정된 시간대(Time Zone)에 맞는 시간을 의미**npm install dayjs** <p>{dayjs(todo.created_at).format("YYYY-MM-DD HH:mm:ss")}</p>
completed_at 필드를 추가하여 완료한 시간도 함께 저장하기
supabase에서 compledte_at 컬럼 추가
터미널에서
npm run generate-types
입력하면types_db.ts
에 타입정보가 뜬다. completed_at 타입 추가하면 완성.//todo 업데이트 export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), completed_at: new Date().toISOString(), //추가 }) .eq("id", todo.id); // 📌 id가 일치하는 행만 업데이트 if (error) { handleError(error); } return data; }
검색 입력창 디바운스 적용하여 렌더링 개선.
export default function UI() { const [searchInput, setSearchInput] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(searchInput); // 검색어 입력 시 `debounce` 적용 (100ms 딜레이) useEffect(() => { const handler = setTimeout(() => { setDebouncedSearch(searchInput); }, 100); return () => clearTimeout(handler); }, [searchInput]); const { data: todosQuery, isPending, refetch, } = useQuery({ queryKey: ["todos", debouncedSearch], // 검색어가 변경될 때마다 쿼리 새로 실행 queryFn: () => getTodos({ searchInput: debouncedSearch }), });
회고
퇴근 후 매일 1시간씩 꾸준히 공부한 내 자신을 칭찬하고 싶다. 완강을 목표로 하고 있지만, 한 단계 더 나아가 다양한 기능을 직접 구현하며 부딪혀 보고 익히고 싶다. 이를 통해 한 달 후에는 스스로 미니 프로젝트를 완성해 보는 것이 목표다.
시간이 날 때 조금 더 개선해 보고 싶은 사항:
리액트 훅을
hooks
폴더로 분리 → 비즈니스 로직을 컴포넌트에서 분리하여 코드의 가독성과 재사용성을 높이기리액트 쿼리 키를 별도로 관리 → 쿼리 키를 체계적으로 관리하여 유지보수성과 확장성을 강화하기
댓글을 작성해보세요.