Skip to content

필사 모드: TypeScript & Next.js 실전 가이드 — 타입 안전한 풀스택 개발

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

목차

1. [TypeScript 타입 시스템 심화](#1-typescript-타입-시스템-심화)

2. [유틸리티 타입 완전 정복](#2-유틸리티-타입-완전-정복)

3. [타입 가드와 타입 좁히기](#3-타입-가드와-타입-좁히기)

4. [Next.js 15 App Router](#4-nextjs-15-app-router)

5. [Server Components vs Client Components](#5-server-components-vs-client-components)

6. [Server Actions](#6-server-actions)

7. [데이터 페칭 전략](#7-데이터-페칭-전략)

8. [미들웨어](#8-미들웨어)

9. [배포 전략](#9-배포-전략)

10. [성능 최적화](#10-성능-최적화)

1. TypeScript 타입 시스템 심화

TypeScript는 단순히 JavaScript에 타입을 추가한 것을 넘어, 강력한 타입 레벨 프로그래밍 언어입니다. 이 장에서는 실무에서 자주 쓰이는 고급 타입 패턴을 다룹니다.

1.1 Union 타입과 Intersection 타입

Union 타입은 여러 타입 중 하나를 나타내며, Intersection 타입은 여러 타입을 모두 만족하는 타입입니다.

// Union 타입: 여러 타입 중 하나

type Status = 'idle' | 'loading' | 'success' | 'error'

type ApiResponse =

| { status: 'success'; data: unknown }

| { status: 'error'; message: string }

// Intersection 타입: 여러 타입을 결합

type Timestamped = { createdAt: Date; updatedAt: Date }

type SoftDeletable = { deletedAt: Date | null }

type BaseEntity = Timestamped & SoftDeletable

interface User extends BaseEntity {

id: string

name: string

email: string

}

Discriminated Union은 공통 필드(판별자)를 이용해 타입을 구분하는 패턴으로, 복잡한 상태 관리에 매우 유용합니다.

// Discriminated Union 패턴

type Action =

| { type: 'SET_USER'; payload: User }

| { type: 'SET_ERROR'; payload: string }

| { type: 'RESET' }

function reducer(state: State, action: Action): State {

switch (action.type) {

case 'SET_USER':

// action.payload는 자동으로 User 타입

return { ...state, user: action.payload, error: null }

case 'SET_ERROR':

// action.payload는 자동으로 string 타입

return { ...state, error: action.payload }

case 'RESET':

return initialState

}

}

1.2 제네릭 (Generics)

제네릭은 타입을 매개변수화하여 재사용 가능한 컴포넌트를 만드는 핵심 도구입니다.

// 기본 제네릭 함수

function identity<T>(value: T): T {

return value

}

// 제네릭 제약 조건

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {

return obj[key]

}

// 제네릭 인터페이스

interface Repository<T extends { id: string }> {

findById(id: string): Promise<T | null>

findAll(): Promise<T[]>

create(data: Omit<T, 'id'>): Promise<T>

update(id: string, data: Partial<T>): Promise<T>

delete(id: string): Promise<void>

}

// 제네릭 클래스

class ApiClient<TResponse> {

constructor(private baseUrl: string) {}

async get(path: string): Promise<TResponse> {

const response = await fetch(`${this.baseUrl}${path}`)

return response.json() as Promise<TResponse>

}

}

1.3 조건부 타입 (Conditional Types)

조건부 타입은 입력 타입에 따라 출력 타입이 결정되는 강력한 패턴입니다.

// 기본 조건부 타입

type IsString<T> = T extends string ? true : false

type A = IsString<string> // true

type B = IsString<number> // false

// infer 키워드로 타입 추출

type UnpackPromise<T> = T extends Promise<infer U> ? U : T

type Resolved = UnpackPromise<Promise<string>> // string

// 배열 요소 타입 추출

type ElementOf<T> = T extends (infer E)[] ? E : never

type Item = ElementOf<string[]> // string

// 함수 반환 타입 추출

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

1.4 매핑된 타입 (Mapped Types)

기존 타입을 변환하여 새로운 타입을 만듭니다.

// 모든 속성을 읽기 전용으로

type Readonly<T> = {

readonly [P in keyof T]: T[P]

}

// 모든 속성을 선택적으로

type MyPartial<T> = {

[P in keyof T]?: T[P]

}

// 특정 키만 선택

type MyPick<T, K extends keyof T> = {

[P in K]: T[P]

}

// 실전 예제: Form 상태 타입 자동 생성

type FormFields<T> = {

[K in keyof T]: {

value: T[K]

error: string | null

touched: boolean

}

}

interface LoginData {

email: string

password: string

}

type LoginForm = FormFields<LoginData>

// 결과:

// {

// email: { value: string; error: string | null; touched: boolean }

// password: { value: string; error: string | null; touched: boolean }

// }

1.5 템플릿 리터럴 타입 (Template Literal Types)

문자열 리터럴 타입을 조합하여 새로운 문자열 타입을 생성합니다.

// 기본 템플릿 리터럴 타입

type EventName = `on${Capitalize<string>}`

// 구체적인 조합 생성

type Color = 'red' | 'blue' | 'green'

type Size = 'sm' | 'md' | 'lg'

type ClassName = `${Color}-${Size}`

// 'red-sm' | 'red-md' | 'red-lg' | 'blue-sm' | ...

// CSS 속성 자동 생성

type CSSProperty = 'margin' | 'padding'

type Direction = 'top' | 'right' | 'bottom' | 'left'

type SpacingProp = `${CSSProperty}-${Direction}`

// 'margin-top' | 'margin-right' | ... | 'padding-left'

// API 경로 타입 안전성

type ApiRoutes = `/api/${'users' | 'posts' | 'comments'}`

type ApiRouteWithId = `${ApiRoutes}/${string}`

2. 유틸리티 타입 완전 정복

TypeScript는 자주 쓰이는 타입 변환을 위한 내장 유틸리티 타입을 제공합니다.

2.1 기본 유틸리티 타입

interface User {

id: string

name: string

email: string

role: 'admin' | 'user'

createdAt: Date

}

// Partial: 모든 속성을 선택적으로

type UpdateUserDto = Partial<User>

// Required: 모든 속성을 필수로

type StrictUser = Required<User>

// Pick: 특정 속성만 선택

type UserPreview = Pick<User, 'id' | 'name'>

// Omit: 특정 속성 제외

type CreateUserDto = Omit<User, 'id' | 'createdAt'>

// Record: 키-값 쌍의 타입

type UserMap = Record<string, User>

type RolePermissions = Record<User['role'], string[]>

2.2 고급 유틸리티 타입

// ReturnType: 함수 반환 타입 추출

function createUser(name: string, email: string) {

return { id: crypto.randomUUID(), name, email, createdAt: new Date() }

}

type CreatedUser = ReturnType<typeof createUser>

// Parameters: 함수 매개변수 타입 추출

type CreateUserParams = Parameters<typeof createUser>

// [name: string, email: string]

// Awaited: Promise 언래핑

async function fetchUser(id: string): Promise<User> {

const res = await fetch(`/api/users/${id}`)

return res.json()

}

type FetchedUser = Awaited<ReturnType<typeof fetchUser>>

// User

// Extract & Exclude

type Status = 'active' | 'inactive' | 'pending' | 'banned'

type ActiveStatus = Extract<Status, 'active' | 'pending'>

// 'active' | 'pending'

type InactiveStatus = Exclude<Status, 'active' | 'pending'>

// 'inactive' | 'banned'

// NonNullable

type MaybeUser = User | null | undefined

type DefiniteUser = NonNullable<MaybeUser>

// User

2.3 커스텀 유틸리티 타입 만들기

// DeepPartial: 중첩 객체까지 모두 선택적으로

type DeepPartial<T> = {

[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]

}

// DeepReadonly: 중첩 객체까지 모두 읽기 전용

type DeepReadonly<T> = {

readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]

}

// RequiredKeys: 특정 키만 필수로

type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

type UserWithRequiredEmail = RequiredKeys<Partial<User>, 'email'>

// Prettify: 타입을 펼쳐서 읽기 쉽게

type Prettify<T> = {

[K in keyof T]: T[K]

} & {}

3. 타입 가드와 타입 좁히기

런타임에서 타입을 안전하게 좁히는 방법입니다.

3.1 typeof 가드

function formatValue(value: string | number | boolean): string {

if (typeof value === 'string') {

return value.toUpperCase()

}

if (typeof value === 'number') {

return value.toFixed(2)

}

return value ? 'Yes' : 'No'

}

3.2 instanceof 가드

class ApiError extends Error {

constructor(

message: string,

public statusCode: number

) {

super(message)

}

}

class ValidationError extends Error {

constructor(

message: string,

public fields: Record<string, string>

) {

super(message)

}

}

function handleError(error: unknown): string {

if (error instanceof ApiError) {

return `API Error ${error.statusCode}: ${error.message}`

}

if (error instanceof ValidationError) {

return `Validation Error: ${Object.values(error.fields).join(', ')}`

}

if (error instanceof Error) {

return error.message

}

return 'Unknown error'

}

3.3 in 연산자 가드

interface Dog {

bark(): void

breed: string

}

interface Cat {

meow(): void

color: string

}

function makeSound(animal: Dog | Cat): void {

if ('bark' in animal) {

animal.bark()

} else {

animal.meow()

}

}

3.4 사용자 정의 타입 가드 (is 키워드)

// 커스텀 타입 가드

function isUser(value: unknown): value is User {

return (

typeof value === 'object' &&

value !== null &&

'id' in value &&

'name' in value &&

'email' in value

)

}

// 배열 필터링에서 타입 가드 활용

const items: (User | null)[] = [user1, null, user2, null]

const users: User[] = items.filter((item): item is User => item !== null)

3.5 satisfies 연산자

TypeScript 4.9에서 도입된 satisfies는 타입 체크와 타입 추론을 동시에 지원합니다.

type Route = {

path: string

element: React.ReactNode

}

// satisfies를 사용하면 타입 체크를 하면서도 리터럴 타입을 유지

const routes = {

home: { path: '/', element: '<Home />' },

about: { path: '/about', element: '<About />' },

blog: { path: '/blog', element: '<Blog />' },

} satisfies Record<string, Route>

// routes.home.path의 타입은 '/' (리터럴)

// 만약 `as const`를 쓰면 readonly가 되어 수정이 불가

type ColorConfig = Record<string, string | string[]>

const palette = {

primary: '#007bff',

secondary: ['#6c757d', '#adb5bd'],

danger: '#dc3545',

} satisfies ColorConfig

// palette.primary는 string 타입

// palette.secondary는 string[] 타입 (배열 메서드 사용 가능)

4. Next.js 15 App Router

Next.js 15의 App Router는 React Server Components를 기반으로 한 새로운 라우팅 시스템입니다.

4.1 app 디렉토리 구조

App Router는 파일 시스템 기반 라우팅을 사용합니다. 각 폴더는 URL 세그먼트에 대응하며, 특수한 파일 이름이 각각의 역할을 담당합니다.

app/

layout.tsx # 루트 레이아웃 (필수)

page.tsx # 홈페이지 (/)

loading.tsx # 로딩 UI

error.tsx # 에러 UI

not-found.tsx # 404 UI

global-error.tsx # 전역 에러 UI

blog/

page.tsx # /blog

[slug]/

page.tsx # /blog/some-post

loading.tsx # 이 라우트의 로딩

dashboard/

layout.tsx # 대시보드 레이아웃

page.tsx # /dashboard

settings/

page.tsx # /dashboard/settings

(marketing)/ # 라우트 그룹 (URL에 영향 없음)

about/

page.tsx # /about

contact/

page.tsx # /contact

api/

users/

route.ts # API 라우트

4.2 Layout과 Page

// app/layout.tsx - 루트 레이아웃

export const metadata: Metadata = {

title: {

default: 'My App',

template: '%s | My App',

},

description: 'A modern full-stack application',

}

export default function RootLayout({

children,

}: {

children: React.ReactNode

}) {

return (

)

}

// app/blog/[slug]/page.tsx - 동적 라우트

interface PageProps {

params: Promise<{ slug: string }>

}

export async function generateMetadata({ params }: PageProps) {

const { slug } = await params

const post = await getPost(slug)

if (!post) return {}

return {

title: post.title,

description: post.summary,

}

}

export default async function BlogPost({ params }: PageProps) {

const { slug } = await params

const post = await getPost(slug)

if (!post) {

notFound()

}

return (

)

}

// 정적 생성할 경로 지정

export async function generateStaticParams() {

const posts = await getAllPosts()

return posts.map((post) => ({

slug: post.slug,

}))

}

4.3 Loading, Error, Not Found

// app/blog/loading.tsx

export default function Loading() {

return (

)

}

// app/blog/error.tsx

'use client' // Error 컴포넌트는 반드시 클라이언트 컴포넌트

export default function Error({

error,

reset,

}: {

error: Error & { digest?: string }

reset: () => void

}) {

useEffect(() => {

console.error(error)

}, [error])

return (

)

}

// app/not-found.tsx

export default function NotFound() {

return (

)

}

5. Server Components vs Client Components

Next.js 15에서 가장 중요한 개념 중 하나는 서버 컴포넌트와 클라이언트 컴포넌트의 구분입니다.

5.1 Server Components (기본값)

App Router에서 모든 컴포넌트는 기본적으로 서버 컴포넌트입니다. 서버에서만 실행되므로 번들 크기에 포함되지 않습니다.

// 서버 컴포넌트 - 기본값

// async 함수로 직접 데이터를 페칭할 수 있음

export default async function UserList() {

// 직접 DB에 접근 가능 (서버에서만 실행)

const users = await db.user.findMany({

orderBy: { createdAt: 'desc' },

take: 10,

})

return (

{users.map((user) => (

))}

)

}

5.2 Client Components

상호작용이 필요한 경우 파일 상단에 `'use client'`를 선언합니다.

'use client'

interface SearchProps {

onSearch: (query: string) => Promise<void>

}

export default function SearchBar({ onSearch }: SearchProps) {

const [query, setQuery] = useState('')

const [isPending, startTransition] = useTransition()

const handleSubmit = (e: React.FormEvent) => {

e.preventDefault()

startTransition(() => {

onSearch(query)

})

}

return (

type="text"

value={query}

onChange={(e) => setQuery(e.target.value)}

placeholder="검색어를 입력하세요..."

disabled={isPending}

/>

{isPending ? '검색 중...' : '검색'}

)

}

5.3 경계 설정 패턴

서버 컴포넌트와 클라이언트 컴포넌트의 조합 패턴입니다.

// 서버 컴포넌트가 클라이언트 컴포넌트를 감싸는 패턴

// app/dashboard/page.tsx (서버 컴포넌트)

export default async function DashboardPage() {

const data = await db.analytics.getOverview()

return <DashboardClient initialData={data} />

}

// app/dashboard/DashboardClient.tsx (클라이언트 컴포넌트)

'use client'

interface Props {

initialData: AnalyticsOverview

}

export default function DashboardClient({ initialData }: Props) {

const [data, setData] = useState(initialData)

const [timeRange, setTimeRange] = useState('7d')

return (

value={timeRange}

onChange={(e) => setTimeRange(e.target.value)}

>

)

}

서버 컴포넌트와 클라이언트 컴포넌트를 선택하는 기준은 다음과 같습니다.

**서버 컴포넌트를 사용하는 경우:**

- 데이터를 페칭할 때

- 백엔드 리소스에 직접 접근할 때

- 민감한 정보(API 키, 토큰)를 사용할 때

- 큰 의존성을 서버에 유지하고 싶을 때

**클라이언트 컴포넌트를 사용하는 경우:**

- 상호작용(이벤트 리스너)이 필요할 때

- useState, useEffect 등 훅을 사용할 때

- 브라우저 전용 API를 사용할 때

- 커스텀 훅에 상태가 포함될 때

6. Server Actions

Server Actions는 서버에서 실행되는 비동기 함수로, 폼 제출과 데이터 변경을 처리합니다.

6.1 기본 Server Action

// app/actions/user.ts

'use server'

const CreateUserSchema = z.object({

name: z.string().min(2, '이름은 2자 이상이어야 합니다'),

email: z.string().email('유효한 이메일을 입력하세요'),

role: z.enum(['admin', 'user']),

})

export async function createUser(formData: FormData) {

const rawData = {

name: formData.get('name'),

email: formData.get('email'),

role: formData.get('role'),

}

const validatedData = CreateUserSchema.safeParse(rawData)

if (!validatedData.success) {

return {

errors: validatedData.error.flatten().fieldErrors,

}

}

await db.user.create({ data: validatedData.data })

revalidatePath('/users')

redirect('/users')

}

6.2 폼과 함께 사용하기

// app/users/new/page.tsx

export default function NewUserPage() {

return (

)

}

6.3 클라이언트 컴포넌트에서 Server Action 사용

'use client'

export default function UserForm() {

const [state, formAction, isPending] = useActionState(createUser, null)

return (

{state?.errors?.name && (

)}

{state?.errors?.email && (

)}

{isPending ? '처리 중...' : '생성'}

)

}

6.4 보안 고려사항

Server Actions 사용 시 반드시 지켜야 할 보안 사항입니다.

'use server'

export async function deletePost(postId: string) {

// 1. 인증 확인

const session = await auth()

if (!session?.user) {

throw new Error('인증이 필요합니다')

}

// 2. 권한 확인

const post = await db.post.findUnique({ where: { id: postId } })

if (post?.authorId !== session.user.id) {

throw new Error('권한이 없습니다')

}

// 3. Rate limiting

const limiter = await rateLimit(session.user.id)

if (!limiter.success) {

throw new Error('요청이 너무 많습니다. 잠시 후 다시 시도하세요.')

}

// 4. 입력 검증

const validatedId = z.string().uuid().parse(postId)

// 5. 실제 작업 수행

await db.post.delete({ where: { id: validatedId } })

revalidatePath('/posts')

}

7. 데이터 페칭 전략

Next.js 15에서의 데이터 페칭은 서버 컴포넌트와 함께 더욱 강력해졌습니다.

7.1 서버 컴포넌트에서 fetch

// 기본 fetch - 기본적으로 캐시됨 (Next.js 14)

// Next.js 15부터는 기본이 no-store

async function getPost(slug: string) {

const res = await fetch(

`https://api.example.com/posts/${slug}`,

{

next: {

revalidate: 3600, // 1시간마다 재검증

tags: ['posts'], // 캐시 태그

},

}

)

if (!res.ok) throw new Error('Failed to fetch post')

return res.json()

}

// 캐시하지 않는 요청

async function getCurrentUser() {

const res = await fetch('https://api.example.com/me', {

cache: 'no-store',

headers: {

Authorization: `Bearer ${getToken()}`,

},

})

return res.json()

}

7.2 unstable_cache (서버 전용 캐시)

// DB 쿼리 결과를 캐시

const getCachedPosts = unstable_cache(

async (category: string) => {

return db.post.findMany({

where: { category, published: true },

orderBy: { createdAt: 'desc' },

take: 20,

})

},

['posts-by-category'], // 캐시 키

{

revalidate: 600, // 10분마다 재검증

tags: ['posts'], // 수동 재검증을 위한 태그

}

)

// 사용

export default async function BlogPage() {

const posts = await getCachedPosts('technology')

return <PostList posts={posts} />

}

7.3 캐시 무효화

'use server'

// 특정 경로의 캐시 무효화

export async function publishPost(postId: string) {

await db.post.update({

where: { id: postId },

data: { published: true },

})

// 특정 경로 재검증

revalidatePath('/blog')

revalidatePath(`/blog/${postId}`)

// 또는 태그 기반 재검증

revalidateTag('posts')

}

7.4 병렬 데이터 페칭

// 순차적 (느림)

export default async function Dashboard() {

const user = await getUser()

const posts = await getPosts() // user 완료 후 시작

const analytics = await getAnalytics() // posts 완료 후 시작

return <DashboardView user={user} posts={posts} analytics={analytics} />

}

// 병렬 (빠름)

export default async function Dashboard() {

const [user, posts, analytics] = await Promise.all([

getUser(),

getPosts(),

getAnalytics(),

])

return <DashboardView user={user} posts={posts} analytics={analytics} />

}

// Suspense를 활용한 스트리밍

export default async function Dashboard() {

const user = await getUser() // 필수 데이터

return (

)

}

8. 미들웨어

미들웨어는 요청이 완료되기 전에 코드를 실행하여 인증, 리다이렉트, 국제화 등을 처리합니다.

8.1 기본 미들웨어

// middleware.ts (프로젝트 루트)

export function middleware(request: NextRequest) {

// 인증 토큰 확인

const token = request.cookies.get('auth-token')?.value

// 보호된 경로 확인

if (request.nextUrl.pathname.startsWith('/dashboard')) {

if (!token) {

const loginUrl = new URL('/login', request.url)

loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname)

return NextResponse.redirect(loginUrl)

}

}

// 응답 헤더 추가

const response = NextResponse.next()

response.headers.set('x-request-id', crypto.randomUUID())

return response

}

export const config = {

matcher: [

// 정적 파일과 API 라우트 제외

'/((?!api|_next/static|_next/image|favicon.ico).*)',

],

}

8.2 국제화 미들웨어

const locales = ['ko', 'en', 'ja']

const defaultLocale = 'ko'

function getLocale(request: NextRequest): string {

const negotiatorHeaders: Record<string, string> = {}

request.headers.forEach((value, key) => {

negotiatorHeaders[key] = value

})

const languages = new Negotiator({

headers: negotiatorHeaders,

}).languages()

return match(languages, locales, defaultLocale)

}

export function middleware(request: NextRequest) {

const pathname = request.nextUrl.pathname

// 이미 로케일이 있는지 확인

const pathnameHasLocale = locales.some(

(locale) =>

pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`

)

if (pathnameHasLocale) return

// 적절한 로케일로 리다이렉트

const locale = getLocale(request)

request.nextUrl.pathname = `/${locale}${pathname}`

return NextResponse.redirect(request.nextUrl)

}

8.3 Rate Limiting 미들웨어

const rateLimit = new Map<string, { count: number; resetTime: number }>()

export function middleware(request: NextRequest) {

if (request.nextUrl.pathname.startsWith('/api')) {

const ip = request.headers.get('x-forwarded-for') ?? 'unknown'

const now = Date.now()

const windowMs = 60 * 1000 // 1분

const maxRequests = 60

const current = rateLimit.get(ip)

if (!current || now > current.resetTime) {

rateLimit.set(ip, { count: 1, resetTime: now + windowMs })

} else if (current.count >= maxRequests) {

return NextResponse.json(

{ error: 'Too Many Requests' },

{ status: 429 }

)

} else {

current.count++

}

}

return NextResponse.next()

}

9. 배포 전략

9.1 Vercel 배포

Vercel은 Next.js의 제작사이며 가장 최적화된 배포 환경을 제공합니다.

Vercel CLI 설치 및 배포

npm i -g vercel

vercel

프로덕션 배포

vercel --prod

환경 변수 설정

vercel env add DATABASE_URL production

환경 변수 관리를 위한 설정 파일 예시입니다.

// next.config.ts

const nextConfig: NextConfig = {

// 환경 변수 검증

env: {

CUSTOM_KEY: process.env.CUSTOM_KEY,

},

// 이미지 최적화

images: {

remotePatterns: [

{

protocol: 'https',

hostname: 'images.example.com',

},

],

},

}

export default nextConfig

9.2 Docker 배포

Dockerfile

FROM node:20-alpine AS base

의존성 설치

FROM base AS deps

WORKDIR /app

COPY package.json yarn.lock* ./

RUN yarn install --frozen-lockfile

빌드

FROM base AS builder

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules

COPY . .

RUN yarn build

프로덕션

FROM base AS runner

WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs

RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./

COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000

CMD ["node", "server.js"]

// next.config.ts - standalone 출력 설정

const nextConfig: NextConfig = {

output: 'standalone',

}

9.3 셀프호스팅

PM2를 사용한 프로세스 관리

npm install -g pm2

빌드

npm run build

PM2로 실행

pm2 start npm --name "nextjs-app" -- start

클러스터 모드 (CPU 코어 수만큼)

pm2 start npm --name "nextjs-app" -i max -- start

자동 재시작 설정

pm2 startup

pm2 save

Nginx 리버스 프록시 설정:

/etc/nginx/sites-available/nextjs

server {

listen 80;

server_name example.com;

location / {

proxy_pass http://localhost:3000;

proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;

proxy_set_header Connection 'upgrade';

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_set_header X-Forwarded-Proto $scheme;

proxy_cache_bypass $http_upgrade;

}

정적 파일 캐싱

location /_next/static {

proxy_pass http://localhost:3000;

add_header Cache-Control "public, max-age=31536000, immutable";

}

}

10. 성능 최적화

10.1 Image 최적화

// 기본 이미지 최적화

export function HeroImage() {

return (

src="/hero.jpg"

alt="Hero image"

width={1200}

height={630}

priority // LCP 이미지에 사용

placeholder="blur"

blurDataURL="data:image/jpeg;base64,..."

/>

)

}

// 반응형 이미지

export function ResponsiveImage() {

return (

src="/photo.jpg"

alt="Responsive photo"

fill

sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"

className="object-cover"

/>

)

}

10.2 Font 최적화

// app/layout.tsx

const inter = Inter({

subsets: ['latin'],

display: 'swap',

variable: '--font-inter',

})

const notoSansKR = Noto_Sans_KR({

subsets: ['latin'],

weight: ['400', '500', '700'],

display: 'swap',

variable: '--font-noto-sans-kr',

})

export default function RootLayout({

children,

}: {

children: React.ReactNode

}) {

return (

lang="ko"

className={`${inter.variable} ${notoSansKR.variable}`}

>

)

}

10.3 Bundle 분석

@next/bundle-analyzer 설치

npm install @next/bundle-analyzer

// next.config.ts

const nextConfig: NextConfig = {

// 기타 설정

}

export default withBundleAnalyzer({

enabled: process.env.ANALYZE === 'true',

})(nextConfig)

번들 분석 실행

ANALYZE=true npm run build

10.4 Lazy Loading

// 컴포넌트 지연 로딩

const DynamicChart = dynamic(() => import('@/components/Chart'), {

loading: () => <p>차트 로딩 중...</p>,

ssr: false, // 클라이언트에서만 렌더링

})

// 조건부 로딩

const DynamicEditor = dynamic(

() => import('@/components/RichTextEditor'),

{ ssr: false }

)

export default function PostEditor() {

const [showEditor, setShowEditor] = useState(false)

return (

에디터 열기

{showEditor && <DynamicEditor />}

)

}

10.5 종합 성능 체크리스트

프로덕션 배포 전에 확인해야 할 성능 최적화 항목입니다.

**이미지:**

- next/image 컴포넌트 사용

- LCP 이미지에 priority 속성 추가

- 적절한 sizes 속성 설정

- WebP/AVIF 포맷 자동 변환 활용

**코드 분할:**

- dynamic import로 큰 컴포넌트 지연 로딩

- 라우트별 자동 코드 분할 활용

- 불필요한 의존성 제거

**캐싱:**

- 적절한 revalidate 값 설정

- 캐시 태그를 활용한 세밀한 무효화

- CDN 캐싱 활용

**서버 컴포넌트:**

- 가능한 한 서버 컴포넌트 활용

- 클라이언트 컴포넌트 경계 최소화

- Suspense로 스트리밍 렌더링 구현

**번들 최적화:**

- bundle-analyzer로 번들 크기 분석

- tree-shaking 최적화

- barrel export 패턴 주의 (필요한 것만 import)

마무리

TypeScript와 Next.js는 현대 웹 개발의 표준이 되었습니다. TypeScript의 강력한 타입 시스템은 런타임 에러를 컴파일 타임에 잡아주고, Next.js의 App Router와 Server Components는 최적의 사용자 경험과 개발자 경험을 동시에 제공합니다.

핵심 정리:

- TypeScript의 제네릭, 조건부 타입, 매핑된 타입을 활용하면 타입 안전하면서도 유연한 코드를 작성할 수 있습니다

- Server Components를 기본으로 사용하고, 상호작용이 필요한 부분만 Client Components로 분리하세요

- Server Actions는 폼 처리와 데이터 변경의 새로운 표준입니다 (항상 인증과 입력 검증을 잊지 마세요)

- 데이터 페칭은 병렬화하고, Suspense를 활용해 점진적 렌더링을 구현하세요

- 이미지, 폰트, 번들 최적화로 Core Web Vitals를 개선하세요

이 가이드에서 다룬 내용을 바탕으로 타입 안전하고 성능이 뛰어난 풀스택 애플리케이션을 구축해 보세요.

현재 단락 (1/834)

1. [TypeScript 타입 시스템 심화](#1-typescript-타입-시스템-심화)

작성 글자: 0원문 글자: 19,745작성 단락: 0/834