[인프런 워밍업 클럽 3기 풀스택] 1주차 발자국
[풀스택 완성] Supabase로 웹사이트 3개 클론하기 (Next.js 14) - 로펀 1주차는 Supabase, Next.js, Tailwindcss, Recoil, React Query 소개와 기본 문법, Server Action CRUD 사용법을 배웠다.Supabase 특징오픈 소스 프로젝트 (자체 서버구축 가능)PostgresSQL 기반 (복잡한 요구사항 개발 가능)Firebase 대비 저렴다양한 연동방식 지원 (+SDK, DB Connection, GraphQL, API) Next.jsNext.js의 폴더명이 곧 Route와 같다.page.tsx는 각 폴더의 대표 파일과 같다.app/layout.tsx는 화면 레이아웃을 잡아주는 Next.js 서버 컴포넌트이므로, 클라이언트에서만 동작하는 상태 관리 라이브러리(Zustand, Recoil, React Query 등)를 직접 사용할 수 없다.따라서 React Query의 Client Provider를 별도의 클라이언트 컴포넌트로 만들어 layout.tsx에 주입해야 한다.(서버 컴포넌트는 클라이언트 상태나 브라우저 API에 접근할 수 없음)서버 액션은 API 라우트 없이 서버에서 직접 실행 가능하다. React Query서버에서 가져온 데이터를 캐싱, 동기화, 상태 관리까지 자동으로 해주는 라이브러리useQuery(): 데이터 조회 (GET 요청)const todosQuery = useQuery({ queryKey: ["todos"], // 캐싱 키 queryFn: fetchTodos, // API 요청 함수 }); const { data, isLoading, error } = todosQuery; queryKey: 데이터 캐싱 키queryFn: 데이터 호출 함수data: fetchTodos의 반환 값isLoading: 데이터 로딩 여부 (true면 로딩 중)error: 요청 실패 시 에러 객체 useMutation(): 데이터 변경 (POST, PUT, DELETE)const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { console.log("성공!"); }, onError: (error) => { console.error("에러 발생:", error); }, }); mutationFn: API 호출 함수onSuccess: 성공 시 실행할 함수onError: 실패 시 실행할 함수 TODO LIST 만들기Supabase todo 테이블 정의Supabase Next.js 연결.env 환경변수 설정Supabase Project API Keys를 사용하여 환경변수 추가package.json 수정scripts에서 generate-types 옵션 수정Supabase 로그인 후 타입 생성generate-types 실행하여 데이터베이스 테이블의 타입 정의 가져오기 Next.js에서 Supabase 클라이언트 생성브라우저/서버 환경에서 사용할 Supabase 클라이언트 생성Next.js의 모든 request에서 Supabase 인증 토큰을 갱신하도록 middleware 설정 TODO 조회 - server actionexport async function getTodos({ searchInput = "" }): Promise<TodoRow[]> { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .select("*") .like("title", `%${searchInput}%`) .order("created_at", { ascending: true }); if (error) { handleError(error); } return data; }getTodos 함수는 searchInput을 기본값 ""로 받아 Promise<TodoRow[]> 타입을 반환한다.이 함수는 서버 환경에서 실행되며, Supabase 클라이언트를 사용하여 todo 테이블에서 데이터를 조회한다.title 컬럼에서 searchInput을 포함한 값을 검색하며created_at을 기준으로 오름차순으로 정렬하여 반환한다.검색된 데이터가 없거나 오류가 발생하면 handleError 함수가 호출된다. TODO 조회 - React Query useQuery()const todosQuery = useQuery({ queryKey: ["todos", searchInput], queryFn: () => getTodos({ searchInput }), }); return ( // ... {todosQuery.data && todosQuery.data.map((todo) => <Todo key={todo.id} todo={todo} />)} // ... ) useQuery는 todosQuery라는 데이터를 서버에서 가져오는 비동기 작업을 관리한다.queryKey는 ["todos", searchInput]으로 설정되어, searchInput에 따라 조회된 결과가 달라지도록 한다.queryFn은 서버 액션 getTodos를 호출해, searchInput에 맞는 TODO 데이터를 반환한다.반환된 데이터(todosQuery.data)가 있을 경우, map()을 사용하여 각 TODO 항목을 렌더링한다. 1주차 미션github: https://github.com/thayoon/nextjs-supabase-todolist생성된 TODO의 생성 시간을 저장하고 이를 표시하는 기능을 추가하세요.TODO 항목 옆에 생성 시간을 표시하기component/todo.tsxexport default function Todo({ todo }) { // 한국 시간, YYYY/MM/DD HH:MM 포맷 함수 const getTime = (time: string): string => new Date(time) .toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Asia/Seoul", }) .replace(",", ""); const createdTime = getTime(todo.created_at); return( // ... {isEditing ? ( <input className="flex-1 border-b-black border-b pb-1" value={title} onChange={(e) => setTitle(e.target.value)} /> ) : ( <div className="flex-1"> <p className={`${completed && "line-through"}`}>{title}</p> <span className="text-gray-400 text-xs">⏱️{createdTime}</span> </div> )} // ... ); } getTime 함수는 created_at 값을 한국 시간에 맞게 YYYY/MM/DD HH:MM 포맷으로 변환한다.isEditing이 false일 때, 생성된 TODO 항목의 생성 시간을 createdTime을 통해 표시한다.todo 매개변수는 useQuery로 가져온 데이터 객체이며, created_at 값을 이용해 생성 시간을 표시한다. (선택 사항) completed_at 필드를 추가하여 완료한 시간도 함께 저장Supabase에서 completed_at 필드를 추가하여, TODO 완료 시각을 저장한다.completed_at은 NULL을 허용하는 timestamptz 타입 칼럼이다.actions/todo-actions.ts (server action)updateTodo 함수는 completed 상태에 따라 completed_at 값을 업데이트한다.export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); console.log(todo); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), completed_at: todo.completed ? new Date().toISOString() : null, }) .eq("id", todo.id); if (error) { handleError(error); } return data; } completed가 true일 경우 completed_at에 현재 시각을 저장하고, false일 경우 null을 저장한다.3. components/todo.tsx (React Query useMutation())completed_at 값을 확인하여 완료 시각을 표시한다.export default function Todo({ todo }) { // 한국 시간, YYYY/MM/DD HH:MM 포맷 함수 const getTime = (time: string): string => new Date(time) .toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Asia/Seoul", }) .replace(",", ""); const completedTime = todo.completed_at ? getTime(todo.completed_at) : null; const updateTodoMutation = useMutation({ mutationFn: () => updateTodo({ id: todo.id, title, completed, }), onSuccess: () => { setIsEditing(false); queryClient.invalidateQueries({ queryKey: ["todos"], }); }, }); return( <div className="w-full flex items-center gap-1"> <Checkbox checked={completed} onChange={async (e) => { await setCompleted(e.target.checked); await updateTodoMutation.mutate(); }} /> {isEditing ? ( <input className="flex-1 border-b-black border-b pb-1" value={title} onChange={(e) => setTitle(e.target.value)} /> ) : ( <div className="flex-1"> <p className={`${completed && "line-through"}`}>{title}</p> <span className="text-gray-400 text-xs"> ⏱️{createdTime}{" "}{completedTime && `~ ${completedTime}`} </span> </div> )} // ... </div> ); } completed 상태에 따라 체크박스를 변경하고, 완료된 경우 completed_at 시각을 표시한다.setCompleted로 상태를 갱신하고, updateTodoMutation을 호출하여 서버에서 completed_at 값을 업데이트한다.완료된 TODO 항목에 대해 completed_at 값이 존재하면 완료 시각을 getTime() 함수로 포맷하여 표시한다.정리:TODO 완료 상태 변경:사용자가 체크박스를 클릭하면 setCompleted로 completed 상태를 갱신한다.갱신된 상태는 updateTodoMutation.mutate()를 통해 서버로 전송되어 completed_at 값이 업데이트된다.완료 시각 컴포넌트 업데이트:서버에서 completed_at 값이 갱신되면, 해당 TODO의 완료 시각이 한국 시간으로 포맷되어 표시된다.완료된 경우에는 completed_at을 getTime() 함수를 통해 표시하고, 완료되지 않은 경우에는 null이므로 표시되지 않는다.쿼리 데이터 갱신:updateTodoMutation.onSuccess에서 queryClient.invalidateQueries({ queryKey: ["todos"] })를 호출하여 todos 쿼리의 데이터를 무효화한다.쿼리의 데이터가 무효화되면 React Query는 자동으로 서버에서 최신 데이터를 다시 불러와 화면에 반영한다. 1주차 회고☀사실 Next.js를 아주 짧게 배운 상태로 강의를 듣게 됐는데, 섹션1에서 중요한 부분을 잘 설명해주셔서 큰 어려움없이 수강할 수 있었다. 그리고 실습을 따라하면서 어느정도 흐름은 알게 된 것 같다. 그렇지만 다른 Next.js 강의를 수강하며 구체적인 동작과정을 더 배워야 할 것 같다. 다음 실습도 기대된다. 다음주도 화이팅!