Next.js의 App Router, TypeScript, Tailwind CSS, PostgreSQL을 활용한 웹 애플리케이션 개발은 현대적인 풀스택 개발 경험을 제공합니다. 하지만 이러한 기술 스택을 효과적으로 활용하기 위해서는 체계적인 개발 접근법이 필요합니다. 이 포스트에서는 빠르고 효율적인 웹 애플리케이션 개발을 위한 단계별 접근 방법을 알아보겠습니다.
효과적인 개발 순서
1. 프로젝트 초기화
모든 개발은 견고한 기초에서 시작합니다. Next.js의 공식 CLI를 사용하면 TypeScript, Tailwind CSS, App Router를 단 한 번의 명령어로 설정할 수 있습니다:
npx create-next-app@latest my-app --typescript --tailwind --app
이 명령어는 필요한 모든 의존성을 설치하고 기본 설정을 자동으로 구성해줍니다. 소스 코드를 루트 디렉토리에 직접 배치하는 방식(src 폴더 없이)을 선호한다면, 프로젝트 생성 중 관련 질문에 ‘No’로 답하면 됩니다.
2. 기본 폴더 구조 설정
App Router는 파일 시스템 기반 라우팅을 사용하므로, 초기에 폴더 구조를 잘 설계하는 것이 중요합니다. 일반적인 구조는 다음과 같습니다:
app/
– 페이지 및 라우트app/components/
– 재사용 가능한 UI 컴포넌트app/lib/
– 유틸리티 함수 및 헬퍼app/api/
– API 라우트 핸들러public/
– 정적 파일
폴더 구조를 초기에 체계적으로 설정하면 프로젝트가 성장함에 따라 코드 관리가 훨씬 쉬워집니다.
3. 데이터베이스 연결
데이터 흐름은 애플리케이션의 핵심이므로, 데이터베이스 연결을 일찍 설정하는 것이 좋습니다. Prisma는 TypeScript와 완벽하게 통합되는 ORM으로, PostgreSQL과 함께 사용하기 적합합니다:
npm install prisma @prisma/client
npx prisma init
.env
파일에 PostgreSQL 연결 문자열을 설정하고, prisma/schema.prisma
파일에 기본 데이터 모델을 정의합니다:
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
createdAt DateTime @default(now())
}
스키마를 정의한 후에는 다음 명령어로 데이터베이스를 마이그레이션합니다:
npx prisma migrate dev --name init
4. 인증 시스템 구현
데이터베이스 연결을 설정한 후, 인증 시스템을 구축하는 것이 중요합니다. Next.js에서는 NextAuth.js(현재는 Auth.js로 이름 변경됨)를 사용하여 쉽게 인증을 구현할 수 있습니다:
npm install next-auth @auth/prisma-adapter
먼저 Prisma 스키마에 인증 관련 모델을 추가합니다:
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
password String?
accounts Account[]
sessions Session[]
posts Post[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
마이그레이션을 실행합니다:
npx prisma migrate dev --name add-auth-models
Next.js 13 App Router에서 Auth.js를 설정합니다:
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import GithubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { prisma } from "@/lib/prisma";
import { compare } from "bcrypt";
export const authOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email }
});
if (!user || !user.password) {
return null;
}
const isPasswordValid = await compare(credentials.password, user.password);
if (!isPasswordValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
};
}
})
],
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
},
callbacks: {
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.sub;
}
return session;
},
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
로그인 컴포넌트를 애플리케이션에 통합하고, 세션 관리를 위한 Provider를 추가합니다:
// app/providers.tsx
'use client';
import { SessionProvider } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
5. 핵심 UI 컴포넌트 구현
레이아웃, 네비게이션 바, 푸터 등 공통 UI 요소를 먼저 개발하면 일관된 디자인 시스템을 구축할 수 있습니다. 예를 들어, app/components/Navbar.tsx
, app/components/Footer.tsx
등을 생성하고, 이를 app/layout.tsx
에 통합합니다.
// app/components/Navbar.tsx
'use client';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
import UserButton from './UserButton';
export default function Navbar() {
const { data: session } = useSession();
return (
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<Link href="/" className="flex-shrink-0 flex items-center">
<span className="text-xl font-bold text-indigo-600">MyApp</span>
</Link>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<Link href="/" className="border-indigo-500 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
홈
</Link>
<Link href="/posts" className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
블로그
</Link>
{session && (
<Link href="/dashboard" className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
대시보드
</Link>
)}
</div>
</div>
<div className="flex items-center">
<div className="ml-4 flex items-center md:ml-6">
<UserButton />
</div>
</div>
</div>
</div>
</nav>
);
}
6. 주요 기능 페이지 개발
메인 페이지부터 시작하여 핵심 기능에 필요한 페이지를 순차적으로 개발합니다. App Router에서는 각 라우트가 자체 폴더를 가지며, page.tsx
파일이 해당 경로의 UI를 담당합니다:
// app/page.tsx - 홈페이지
export default function Home() {
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold">환영합니다!</h1>
{/* 추가 콘텐츠 */}
</main>
);
}
// app/posts/page.tsx - 블로그 포스트 목록
export default function Posts() {
// 포스트 목록 페이지 구현
}
7. API 및 데이터 연동
Next.js 13 이상에서는 Server Actions 또는 Route Handlers를 사용하여 데이터베이스와 상호 작용할 수 있습니다. Server Actions를 사용하면 별도의 API 엔드포인트 없이도 서버 사이드 기능을 구현할 수 있습니다:
// app/actions.ts
'use server';
import { prisma } from '@/lib/prisma';
export async function createPost(data: FormData) {
const title = data.get('title') as string;
const content = data.get('content') as string;
return prisma.post.create({
data: {
title,
content,
},
});
}
클라이언트에서 Server Actions를 사용하는 예:
// app/posts/create/page.tsx
'use client';
import { createPost } from '@/app/actions';
import { useRouter } from 'next/navigation';
export default function CreatePost() {
const router = useRouter();
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
await createPost(formData);
router.push('/posts');
router.refresh();
}
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-2xl mx-auto p-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">제목</label>
<input
type="text"
name="title"
id="title"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required
/>
</div>
<div>
<label htmlFor="content" className="block text-sm font-medium text-gray-700">내용</label>
<textarea
name="content"
id="content"
rows={5}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
></textarea>
</div>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
저장하기
</button>
</form>
);
}
8. 스타일링 및 UI 개선
기본 기능이 작동하는 것을 확인한 후, Tailwind CSS를 활용하여 디자인을 개선합니다. 반응형 디자인을 적용하고 사용자 경험을 향상시킵니다:
<article className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow duration-300">
<h2 className="text-2xl font-semibold text-gray-800">{post.title}</h2>
<p className="mt-2 text-gray-600">{post.content}</p>
<div className="mt-4 flex justify-between items-center text-sm text-gray-500">
<span>{new Date(post.createdAt).toLocaleDateString()}</span>
{post.published ? (
<span className="px-2 py-1 bg-green-100 text-green-800 rounded-full">발행됨</span>
) : (
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded-full">초안</span>
)}
</div>
</article>
9. 기본 배포
Vercel과 같은 플랫폼에 애플리케이션을 배포하여 실제 환경에서 테스트합니다. Next.js는 Vercel과 완벽하게 통합되어 있어서 배포 과정이 매우 간단합니다:
# Vercel CLI 설치
npm i -g vercel
# 로그인 및 배포
vercel login
vercel
이 접근 방식을 추천하는 이유
이러한 개발 순서를 추천하는 데는 몇 가지 중요한 이유가 있습니다:
- 빠른 기초 설정: 공식 도구를 사용한 초기화로 설정 오류와 불일치를 방지합니다.
- 체계적인 구조: 초기에 폴더 구조를 잘 설계하면 프로젝트 확장 시 코드 관리가 용이합니다.
- 데이터 중심 접근: 데이터 모델을 먼저 설정함으로써 애플리케이션의 핵심 구조를 명확히 합니다.
- 일찍 인증 구현: 초기에 인증 시스템을 구축하면 사용자 관련 기능을 개발할 때 인증 상태를 고려하여 설계할 수 있습니다.
- 컴포넌트 재사용: 공통 UI 요소를 먼저 개발하여 일관성을 유지하고 개발 속도를 높입니다.
- 핵심 기능 우선: 가장 중요한 기능을 먼저 개발함으로써 MVP를 빠르게 구현하고 검증할 수 있습니다.
- 효율적인 데이터 처리: Server Actions를 활용하면 API 라우트 없이도 서버 기능을 구현할 수 있어 코드가 간결해집니다.
- 점진적 디자인 개선: 기능이 작동한 후 디자인을 개선하는 접근 방식으로 개발 과정이 명확해집니다.
- 지속적인 배포: 초기부터 자주 배포하여 실제 환경에서의 문제를 조기에 발견하고 해결할 수 있습니다.
결론
효과적인 Next.js 애플리케이션 개발은 체계적인 접근과 현대적인 도구의 활용에 달려 있습니다. App Router, TypeScript, Tailwind CSS, PostgreSQL의 조합은 강력한 개발 경험을 제공하지만, 이를 최대한 활용하기 위해서는 체계적인 개발 프로세스가 필요합니다.
위에서 설명한 단계별 접근 방법을 따르면 최소한의 설정으로 빠르게 작동하는 애플리케이션을 개발할 수 있으며, 이후에 필요에 따라 기능을 확장해 나갈 수 있습니다. “작동하는 최소 기능 제품(MVP)”을 빠르게 개발하고, 점진적으로 개선해 나가는 방식은 개발 과정에서의 시행착오를 줄이고 효율성을 극대화하는 데 도움이 됩니다.