[인프런 워밍업 클럽 Full Stack 3기] 1주차 TODO
Next.js 기본 개념 정리 (Part 1)
Next.js란?
“개인이 풀스택 개발을 하기에 최적화된 웹 프레임워크”
서버사이드 렌더링(SSR) 지원 → SEO(검색 최적화) 강점
별도 서버 구축 없이 API까지 개발 가능
Next.js 프로젝트 생성
npx create-next-app@latest [프로젝트명]
폴더 구조 및 주요 파일
폴더 구조 = Route(URL) 구조와 동일
app/movies→/moviesapp/movies/[id]→/movies/1(Dynamic Route)
주요 파일
page.tsx(js): 해당 경로의 페이지 컴포넌트layout.tsx(js): 페이지 레이아웃 관리route.ts(js): API 서버 역할 (GET, POST, PUT, DELETE 등 구현 가능)
Link 사용
a태그 대신Link컴포넌트 사용 권장 (클라이언트 사이드 라우팅 최적화)
import Link from 'next/link';
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>;
}
Next.js Part 2 - Metadata & Server Action
Server Action이란?
Next.js에서 백엔드 API 없이 서버에서 직접 데이터 처리 가능
기존
fetch+REST API방식보다 간단하고 빠름
Server Action 사용 예시
유저 검색 기능을 구현할 때 두 가지 방식 비교
기존 방식 → fetch + REST API
개선 방식 → Server Action 활용
Metadata (SEO 및 공유성 강화)
Next.js는 SEO 및 링크 미리보기를 위해 메타데이터 API 제공
Static Metadata (정적 메타데이터 설정)
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '페이지 제목',
description: '페이지 설명',
}
export default function Page() {}
Dynamic Metadata (동적 메타데이터 설정)
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const id = params.id
const product = await fetch(`https://.../${id}`).then((res) => res.json())
const previousImages = (await parent).openGraph?.images || []
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}
export default function Page({ params, searchParams }: Props) {}
Next.js Part 3 - Supabase 프로젝트 생성
1. Supabase 가입 및 프로젝트 설정
Supabase 회원가입 후 대시보드 접속
새 프로젝트 생성 → 프로젝트명:
inflearn-supabase-projects테이블 생성 (
todo테이블)title (text)completed (boolean)created_at (timestampz)updated_at (timestampz)
2. .env 파일 설정
NEXT_PUBLIC_SUPABASE_URL=https://[project_id].supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=[anon_key]
NEXT_SUPABASE_SERVICE_ROLE=[service_role_key]
NEXT_SUPABASE_DB_PASSWORD=[db_password]
3. package.json 수정 (타입 자동 생성 스크립트 추가)
"scripts": {
"generate-types": "npx supabase gen types typescript --project-id [project_id] --schema public > types_db.ts"
}
4. Supabase CLI 설치 및 로그인
npx supabase login
Next.js Part 4 - 필수 라이브러리 설정 (React Query, Supabase)
1. 필수 라이브러리 설치
npm i @supabase/ssr @tanstack/react-query
2. React Query 설정
config/ReactQueryClientProvider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export const queryClient = new QueryClient({});
export default function ReactQueryClientProvider({
children,
}: React.PropsWithChildren) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
app/layout.tsx
import ReactQueryClientProvider from "config/ReactQueryClientProvider";
export default function RootLayout({ children }) {
return (
<ReactQueryClientProvider>
...
</ReactQueryClientProvider>
);
}
3. Supabase 설정
환경 변수 설정 (.env)
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_SUPABASE_SERVICE_ROLE=
NEXT_SUPABASE_DB_PASSWORD=
utils/supabase/client.ts (브라우저용 Supabase 클라이언트)
"use client";
import { createBrowserClient } from "@supabase/ssr";
export const createBrowserSupabaseClient = () =>
createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
utils/supabase/server.ts (서버용 Supabase 클라이언트)
"use server";
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "types_db";
export const createServerSupabaseClient = async (
cookieStore: ReturnType<typeof cookies> = cookies(),
admin: boolean = false
) => {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
admin
? process.env.NEXT_SUPABASE_SERVICE_ROLE!
: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// 서버 컴포넌트에서 set 호출 시 무시
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: "", ...options });
} catch (error) {
// 서버 컴포넌트에서 delete 호출 시 무시
}
},
},
}
);
};
export const createServerSupabaseAdminClient = async (
cookieStore: ReturnType<typeof cookies> = cookies()
) => {
return createServerSupabaseClient(cookieStore, true);
};
4. Middleware 설정
app/middleware.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
export const applyMiddlewareSupabaseClient = async (request: NextRequest) => {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({ name, value, ...options });
response = NextResponse.next({ request: { headers: request.headers } });
response.cookies.set({ name, value, ...options });
},
remove(name: string, options: CookieOptions) {
request.cookies.set({ name, value: "", ...options });
response = NextResponse.next({ request: { headers: request.headers } });
response.cookies.set({ name, value: "", ...options });
},
},
}
);
await supabase.auth.getUser();
return response;
};
export async function middleware(request: NextRequest) {
return await applyMiddlewareSupabaseClient(request);
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
Part 5. 할일 CRUD 기능 구현 (feat. Server Action)
1. CRUD Server Action 만들기
server/actions/todo.ts
"use server";
import { Database } from "types_db";
import { createServerSupabaseClient } from "utils/supabase/server";
export type TodoRow = Database["public"]["Tables"]["todo"]["Row"];
export type TodoRowInsert = Database["public"]["Tables"]["todo"]["Insert"];
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 = "" }): 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;
}
export async function createTodo(todo: TodoRowInsert) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase.from("todo").insert({
...todo,
created_at: new Date().toISOString(),
});
if (error) {
handleError(error);
}
return data;
}
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(),
})
.eq("id", todo.id);
if (error) {
handleError(error);
}
return data;
}
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;
}
Part 6. 추가 과제 - 할 일의 생성 및 업데이트 시간 표시
기능 추가: 할 일 목록에 created_at(생성 시간)과 updated_at(업데이트 시간) 표시
{setCreated_at ? (
<p className="flex-1">created_at: {formatDate(created_at)}</p>
) : (
<p className="flex-1">created_at: {formatDate(created_at)}</p>
)}
<p className="flex-1">updated_at: {formatDate(todo.updated_at)}</p>
업데이트 내용
created_at: 할 일이 처음 생성된 시간 표시updated_at: 할 일이 수정된 최신 시간 표시
댓글을 작성해보세요.