![[인프런 워밍업 클럽 3기 풀스택] 1주차 발자국](https://cdn.inflearn.com/public/files/blogs/141ea710-3903-4d2a-8137-742fdaa77b22/download.png)
[인프런 워밍업 클럽 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.js
Next.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 action
export 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.tsx
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 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 강의를 수강하며 구체적인 동작과정을 더 배워야 할 것 같다.
다음 실습도 기대된다. 다음주도 화이팅!
댓글을 작성해보세요.