React로 웹 애플리케이션을 만들다 보면 UI 외에도 직접 해결해야 할 문제가 늘어난다.
라우팅, 서버 렌더링, 데이터 조회, SEO, API, 인증, 캐싱, 이미지 최적화, 배포 등이 대표적이다.
Next.js는 이런 웹 애플리케이션 개발에 필요한 기능을 React 위에 통합한 프레임워크다.
따라서 Next.js를 공부할 때 API를 하나씩 외우기보다 먼저 서버와 클라이언트의 경계를 이해하는 것이 중요하다.
Browser -> Next.js -> Server Component / Client Component / Server Action / Route Handler -> Database / External API
Next.js의 핵심은 React에 서버 실행 환경과 웹 애플리케이션에 필요한 기능을 결합한 것이다.
Next.js는 무엇을 해결하는가
React는 기본적으로 UI를 구성하기 위한 라이브러리다.
실제 웹 애플리케이션을 만들려면 UI 외에도 여러 기능이 필요하다.
React -> UI
Next.js에서는 여기에 웹 애플리케이션 개발에 필요한 기능이 추가된다.
Next.js -> UI / Routing / Rendering / Data Fetching / Server Logic / API / Caching / Optimization
특히 현재 Next.js의 App Router에서는 React Server Components를 기반으로 서버와 클라이언트의 역할을 나누는 구조가 중요하다.
App Router
Next.js의 App Router는 app 디렉터리의 파일 구조를 URL 구조와 연결한다.
예를 들어:
app -> page.tsx / layout.tsx / about/page.tsx / users/page.tsx
각 파일은 다음과 같은 Route와 연결된다.
app/page.tsx -> /
app/about/page.tsx -> /about
app/users/page.tsx -> /users
별도의 Router 설정을 반복해서 작성하지 않아도 파일과 폴더 구조를 통해 페이지 구조를 표현할 수 있다.
page와 layout
page.tsx는 해당 Route에서 실제로 보여줄 페이지 UI를 정의한다.
export default function Page() {
return <h1>Home</h1>;
}
layout.tsx는 여러 페이지에서 공유되는 UI 구조를 정의한다.
Root Layout -> Home Page / Users Layout
중첩된 Layout은 특정 Route 하위에서만 공유된다.
Users Layout -> Users Page / User Detail
Header, Navigation, 공통 Provider처럼 여러 페이지에서 유지해야 하는 UI를 Layout에 배치할 수 있다.
Server Component와 Client Component
Next.js를 이해할 때 가장 중요한 개념이다.
App Router에서는 컴포넌트가 기본적으로 Server Component다.
export default async function Page() {
const users = await getUsers();
return <UserList users={users} />;
}
Server Component는 서버에서 실행되므로 서버에서 데이터에 접근하고 UI를 구성할 수 있다.
반면 브라우저에서 상태나 사용자 이벤트를 처리해야 한다면 Client Component가 필요하다.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
두 컴포넌트의 역할을 단순화하면 다음과 같다.
Server Component -> Server Execution / Data Access / UI Generation
Client Component -> Browser Execution / State / Event / Browser API
Next.js에서는 이 경계를 어디에 만들 것인지가 매우 중요하다.
Client Component가 필요한 경우
다음과 같은 기능이 필요하면 Client Component를 고려한다.
useStateuseEffect- 사용자 이벤트
- Browser API
- 브라우저에서 실행되는 라이브러리
예를 들어:
'use client';
export default function Button() {
return (
<button onClick={() => alert('Hello')}>
Click
</button>
);
}
반대로 서버에서 데이터를 가져와 표시하는 것이 목적이라면 Server Component로 충분한 경우가 많다.
Server Data Fetching -> Server Component
User Interaction -> Client Component
Client Component를 무조건 많이 사용하는 것이 아니라 브라우저에서 실행되어야 하는 최소 범위에 Client 경계를 만드는 것이 중요하다.
Server Component와 Client Component의 관계
Server Component 안에서 Client Component를 사용할 수 있다.
export default async function Page() {
const data = await getData();
return (
<div>
<h1>{data.title}</h1>
<Counter />
</div>
);
}
구조적으로 보면:
Server Component -> Server UI / Client Component
서버에서 데이터를 가져온 뒤 사용자 상호작용이 필요한 부분만 Client Component로 분리하는 구조다.
반대로 Client Component에서 서버 전용 기능을 자유롭게 사용할 수 있는 것은 아니다.
따라서 Next.js에서는 서버에서 처리할 로직과 브라우저에서 처리할 로직을 구분하는 설계가 중요하다.
Data Fetching
Server Component에서는 서버에서 데이터를 가져오는 구조를 자연스럽게 만들 수 있다.
export default async function UsersPage() {
const response = await fetch('https://api.example.com/users');
const users = await response.json();
return <UserList users={users} />;
}
일반적인 React SPA에서는 브라우저에서 API를 호출하고 State를 변경하는 방식이 흔하다.
Next.js에서는 서버에서 데이터를 조회한 뒤 UI를 생성할 수 있다.
Browser -> Request -> Next.js Server -> API / Database -> Rendered UI -> Browser
서버에서 처리할 수 있는 데이터 조회를 서버에서 수행하면 브라우저에 불필요한 서버 전용 로직이나 접근 정보를 노출하지 않는 데도 도움이 된다.
Caching
Next.js에서는 데이터 조회와 함께 캐싱을 고려해야 한다.
기본적인 개념은 다음과 같다.
Request -> Cache Hit -> Cached Data
Request -> Cache Miss -> Fetch Data
다만 Next.js의 캐싱 동작은 버전과 사용하는 API에 따라 달라질 수 있다.
따라서 과거 버전의 fetch 캐싱 동작을 암기하는 것보다 현재 프로젝트에서 사용하는 Next.js 버전의 공식 문서와 실제 설정을 기준으로 판단하는 것이 중요하다.
revalidate, no-store 등도 이러한 캐싱과 데이터 신선도 정책을 결정하는 데 사용된다.
Static Rendering과 Dynamic Rendering
Next.js에서는 페이지를 정적인 결과로 생성할 수도 있고 요청 시 동적으로 생성할 수도 있다.
정적인 페이지는 미리 생성할 수 있다.
Build Time -> HTML Generation -> User Request -> HTML Response
요청에 따라 결과가 달라지는 페이지는 서버에서 동적으로 처리할 수 있다.
User Request -> Server -> Data Fetch -> Render -> Response
어떤 렌더링 방식을 사용할지는 데이터의 특성, 캐싱 정책, 사용자별 데이터 여부 등에 따라 결정된다.
Streaming과 Suspense
서버에서 페이지 전체가 준비될 때까지 기다릴 필요가 없는 경우 Streaming을 사용할 수 있다.
예를 들어 Header는 빠르게 준비되지만 사용자 목록은 느리다고 하자.
Request -> Header / Main Content / Slow Data
느린 데이터 때문에 전체 페이지를 기다리는 대신 준비된 UI부터 전달할 수 있다.
React의 Suspense와 함께 사용하면 특정 영역의 Loading UI를 분리할 수 있다.
<Suspense fallback={<Loading />}>
<UserList />
</Suspense>
이 방식은 데이터 조회 시간이 서로 다른 페이지에서 특히 유용하다.
Loading과 Error 처리
App Router에서는 파일 기반으로 Loading과 Error UI를 구성할 수 있다.
Route -> page.tsx / loading.tsx / error.tsx
loading.tsx는 해당 Route의 로딩 상태를 표현하고 error.tsx는 오류 상황을 처리하는 UI를 구성한다.
페이지 내부에서 모든 상태를 직접 관리하는 것보다 Route 구조와 상태 처리를 연결할 수 있다는 장점이 있다.
Route Handler
Next.js에서는 API Endpoint도 직접 만들 수 있다.
예를 들어:
app -> api/users/route.ts
export async function GET() {
const users = await getUsers();
return Response.json(users);
}
그러면 해당 Route에서 HTTP 요청을 처리할 수 있다.
Client -> /api/users -> Route Handler -> Database / External API
간단한 Backend API, Webhook, 외부 API 중계 등의 용도로 사용할 수 있다.
다만 Route Handler가 별도의 Backend를 항상 대체해야 한다는 의미는 아니다.
서비스 규모가 커지고 복잡한 비즈니스 로직이나 독립적인 Backend가 필요하다면 별도의 Backend 서버를 사용하는 것도 충분히 합리적인 구조다.
Server Action
Server Action은 서버에서 실행되는 함수를 UI와 연결할 수 있게 해준다.
예를 들어 Form 제출을 서버 함수와 연결할 수 있다.
<form action={createUser}>
<input name="name" />
<button type="submit">
Create
</button>
</form>
서버에서는:
'use server';
export async function createUser(formData: FormData) {
const name = formData.get('name');
// Database 처리
}
전체 흐름은 다음과 같다.
User -> Form -> Server Action -> Database
간단한 데이터 변경 작업에서는 별도의 API Endpoint를 직접 만드는 것보다 편리할 수 있다.
다만 Server Action 역시 서버에서 실행된다고 해서 자동으로 안전한 것은 아니다.
인증과 권한 검증을 반드시 고려해야 한다.
Dynamic Route
URL의 일부를 데이터로 사용하는 경우 Dynamic Segment를 사용할 수 있다.
app -> users/[id]/page.tsx
이 구조는 다음과 같은 URL을 처리할 수 있다.
/users/1
/users/2
/users/3
페이지에서는 Route Parameter를 받아 사용할 수 있다.
export default async function UserPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <div>User: {id}</div>;
}
Next.js 버전에 따라 params 등의 API 사용 방식이 변경될 수 있으므로 실제 프로젝트에서는 해당 버전의 공식 문서를 기준으로 작성해야 한다.
Metadata와 SEO
Next.js에서는 페이지 Metadata를 코드로 관리할 수 있다.
export const metadata = {
title: 'User',
description: 'User page',
};
페이지별 title, description 등의 정보를 관리할 수 있기 때문에 검색 엔진과 링크 공유에 필요한 Metadata를 구성하기 편하다.
React SPA에서도 구현할 수 있지만 Next.js에서는 프레임워크 구조와 자연스럽게 연결된다.
Image Optimization
Next.js에서는 next/image를 통해 이미지 최적화 기능을 사용할 수 있다.
import Image from 'next/image';
<Image
src="/profile.png"
alt="Profile"
width={200}
height={200}
/>
이미지 크기와 로딩 등의 최적화 기능을 프레임워크 수준에서 사용할 수 있다.
웹 애플리케이션에서는 이미지가 네트워크 전송량과 초기 로딩 성능에 큰 영향을 줄 수 있기 때문에 이미지 처리도 중요한 최적화 영역이다.
Static Assets
정적 파일은 일반적으로 public 디렉터리에 배치할 수 있다.
public -> images / icons / fonts
예를 들어:
public/images/logo.png -> /images/logo.png
Next.js에서는 Font 최적화 기능도 제공한다.
이미지와 Font는 초기 로딩 성능에 직접적인 영향을 줄 수 있기 때문에 필요한 리소스만 적절하게 로드하는 것이 중요하다.
인증과 권한
Next.js에서 인증과 권한은 구분해서 생각해야 한다.
Authentication -> 누구인가?
Authorization -> 무엇을 할 수 있는가?
예를 들어 관리자 페이지 접근을 처리한다면:
Request -> Session 확인 -> User 확인 -> Permission 확인 -> Resource 접근
Client Component에서 관리자 버튼을 숨기는 것만으로 권한 제어가 끝나는 것은 아니다.
실제 데이터 접근이나 Server Action, Route Handler 등 서버에서 실행되는 지점에서도 권한을 검증해야 한다.
환경 변수
Next.js에서는 서버 전용 환경 변수와 클라이언트에 공개되는 환경 변수를 구분해야 한다.
DATABASE_URL -> Server Only
NEXT_PUBLIC_API_URL -> Client Accessible
NEXT_PUBLIC_으로 시작하는 환경 변수는 클라이언트 번들에 포함될 수 있다.
따라서 Database Password, Secret Key 같은 민감한 정보는 공개 환경 변수에 넣어서는 안 된다.
Secret -> Server
Public Config -> Server / Browser
서버와 클라이언트의 경계를 이해해야 하는 대표적인 이유다.
프로젝트 구조
Next.js 프로젝트는 규모와 팀의 설계 방식에 따라 다양한 구조를 사용할 수 있다.
예를 들어:
src -> app / components / features / lib / types
app은 Route와 페이지 구조를 담당하고, components는 공통 UI, features는 기능 단위 코드, lib는 공통 서버나 유틸리티 코드 등을 배치하는 식으로 구성할 수 있다.
중요한 것은 특정 폴더 구조를 암기하는 것이 아니다.
UI, 데이터 접근, 서버 로직, 클라이언트 로직의 책임을 적절하게 분리하는 것이 핵심이다.
React와 Next.js의 차이
React와 Next.js의 관계는 다음처럼 볼 수 있다.
React -> UI Library
Next.js -> React + Routing / Rendering / Server Features / Data Fetching / Optimization
React만으로도 웹 애플리케이션을 만들 수 있다.
Next.js는 React를 기반으로 실제 웹 애플리케이션을 개발하면서 반복적으로 필요한 기능과 서버 실행 환경을 하나의 프레임워크로 통합한다.
따라서 Next.js를 단순히 React의 상위 버전이라고 이해하면 안 된다.
React는 UI를 구성하는 핵심 라이브러리이고, Next.js는 React를 기반으로 웹 애플리케이션 전체를 구성하기 위한 프레임워크다.
Next.js의 핵심 구조
Next.js를 가장 단순하게 표현하면 다음과 같다.
Browser -> Next.js -> Server / Client -> Data -> UI
좀 더 구체적으로 보면:
Request -> App Router -> Layout / Page -> Server Component / Client Component -> UI
서버 데이터가 필요하면:
Server Component -> Database / External API -> Render -> Browser
사용자 상호작용이 필요하면:
Client Component -> Event -> State Update -> Render
데이터 변경이 필요하면:
Client -> Server Action / Route Handler -> Database -> Updated State -> UI
Next.js에서 가장 중요한 경계
Next.js를 공부할 때 가장 먼저 이해해야 하는 것은 Server와 Client의 경계다.
서버에서 처리하기 적합한 것은:
Server -> Database / Secret / Authentication / Data Fetching / Server Logic
브라우저에서 처리해야 하는 것은:
Client -> Interaction / State / Event / Browser API
그리고 UI는 이 둘을 조합해서 구성한다.
Server Component -> Server UI / Client Component
이 경계를 기준으로 생각하면 Server Component, Client Component, Server Action, Route Handler, Caching, Rendering 같은 기능들이 서로 독립적인 API가 아니라 하나의 구조 안에서 연결된다.
정리
Next.js를 공부할 때 가장 먼저 잡아야 하는 것은 API 목록이 아니다.
어디에서 코드가 실행되는가를 먼저 이해해야 한다.
Server -> Data / Database / Authentication / Server Logic
Client -> State / Event / Browser API / Interaction
React의 기본 흐름은:
State -> Render -> UI -> Event -> State Update -> Render
Next.js에서는 여기에 Server와 Client의 실행 환경이 추가된다.
Request -> Route -> Server / Client -> Data -> Render -> Browser
App Router는 URL과 페이지 구조를 연결하고, Server Component는 서버에서 데이터와 UI를 처리하며, Client Component는 브라우저의 상태와 사용자 상호작용을 담당한다.
Route Handler는 HTTP API를 제공하고 Server Action은 서버에서 실행되는 데이터 변경 로직을 UI와 연결한다.
Caching과 Rendering은 데이터를 언제 가져오고 결과를 얼마나 재사용할 것인지 결정하며, Metadata와 Image Optimization은 웹 애플리케이션의 SEO와 성능을 보완한다.
결국 Next.js에서 가장 중요한 설계 판단은 하나로 압축할 수 있다.
이 로직을 서버에서 실행할 것인가, 브라우저에서 실행할 것인가.
이 경계를 제대로 잡으면 Next.js의 나머지 기능들은 그 위에서 자연스럽게 연결된다.