포스트

[Troubleshooting] Next.js 14+ TypeScript 빌드 에러 가이드

[Troubleshooting] Next.js 14+ TypeScript 빌드 에러 가이드

들어가며

Next.js 14 버전이 출시되면서 많은 개발자들이 TypeScript와 함께 사용하는 과정에서 다양한 빌드 에러를 경험하고 있습니다. 이 글에서는 가장 빈번하게 발생하는 5가지 주요 빌드 에러와 그 해결 방법을 상세히 알아보겠습니다.

Next.js 14의 주요 변경사항

Next.js 14는 이전 버전과 비교하여 몇 가지 중요한 변경사항을 도입했습니다:

  1. App Router가 기본 라우팅 시스템으로 채택
  2. 서버 컴포넌트(Server Components)가 기본값으로 설정
  3. 메타데이터 API의 개선
  4. 향상된 타입 안정성

이러한 변경사항들로 인해 TypeScript와 함께 사용할 때 새로운 형태의 빌드 에러들이 발생하고 있습니다.

주요 빌드 에러 및 해결 방법

1. Server Component에서 ‘use client’ 지시어 관련 에러

문제 상황

서버 컴포넌트에서 클라이언트 사이드 기능을 사용하려 할 때 발생하는 가장 일반적인 에러입니다.

1
2
3
4
5
6
7
8
// ❌ 에러 발생 코드
import { useState } from 'react'

export default function ServerComponent() {
  // Error: useState can only be used in Client Components
  const [count, setCount] = useState(0)
  return <div>{count}</div>
}

원인 분석

Next.js 14에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트로 취급됩니다. 서버 컴포넌트에서는 브라우저 API나 React의 상태 관리 훅을 사용할 수 없습니다.

해결 방법

1
2
3
4
5
6
7
8
// ✅ 해결 방법
'use client'
import { useState } from 'react'

export default function ClientComponent() {
  const [count, setCount] = useState(0)
  return <div>{count}</div>
}

모범 사례

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 부모 (서버 컴포넌트)
import ClientCounter from './ClientCounter'

export default function ParentServerComponent() {
  return (
    <div>
      <h1>서버 컴포넌트</h1>
      <ClientCounter />
    </div>
  )
}

// 자식 (클라이언트 컴포넌트)
'use client'
export default function ClientCounter() {
  const [count, setCount] = useState(0)
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  )
}

2. 메타데이터 타입 관련 에러

문제 상황

페이지나 레이아웃에서 메타데이터를 정의할 때 발생하는 타입 에러입니다.

1
2
3
4
5
// ❌ 에러 발생 코드
export const metadata = {
  title: 'My App',
  description: 'Description'  // Type error
}

해결 방법

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 해결 방법
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'Description',
  openGraph: {
    title: 'My App',
    description: 'Description',
    type: 'website'
  }
}

고급 설정 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import type { Metadata } from 'next'

export function generateMetadata({ params }: Props): Metadata {
  return {
    title: `Page ${params.id}`,
    description: `Dynamic description for page ${params.id}`,
    robots: {
      index: true,
      follow: true
    },
    viewport: {
      width: 'device-width',
      initialScale: 1
    }
  }
}

3. 동적 라우트 파라미터 타입 에러

문제 상황

동적 라우팅에서 파라미터 타입이 제대로 정의되지 않았을 때 발생합니다.

해결 방법

1
2
3
4
5
6
7
8
9
10
11
// ✅ 기본적인 해결 방법
interface PageProps {
  params: {
    id: string
  }
  searchParams: { [key: string]: string | string[] | undefined }
}

export default function Page({ params, searchParams }: PageProps) {
  return <div>Post: {params.id}</div>
}

타입 가드 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 타입 가드를 활용한 고급 해결 방법
interface PostParams {
  id: string
}

function isValidPostParams(params: any): params is PostParams {
  return typeof params.id === 'string'
}

export default function Page({ params }: { params: unknown }) {
  if (!isValidPostParams(params)) {
    throw new Error('Invalid parameters')
  }

  return <div>Post: {params.id}</div>
}

4. API 라우트 응답 타입 에러

문제 상황

API 라우트에서 응답을 반환할 때 타입 에러가 발생하는 경우입니다.

해결 방법

1
2
3
4
5
6
7
// ✅ 기본적인 해결 방법
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  return NextResponse.json({ message: 'Hello' })
}

에러 처리를 포함한 고급 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

interface ErrorResponse {
  error: string
  status: number
}

interface SuccessResponse {
  data: any
  status: number
}

type ApiResponse = SuccessResponse | ErrorResponse

export async function GET(
  request: NextRequest
): Promise<NextResponse<ApiResponse>> {
  try {
    const data = await fetchSomeData()
    return NextResponse.json({ 
      data, 
      status: 200 
    })
  } catch (error) {
    return NextResponse.json(
      { error: error.message, status: 500 },
      { status: 500 }
    )
  }
}

5. Image 컴포넌트 속성 타입 에러

문제 상황

Next.js의 Image 컴포넌트 사용 시 필수 속성이 누락되어 발생하는 에러입니다.

기본 해결 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 정적 이미지
import Image from 'next/image'

export default function MyImage() {
  return (
    <Image 
      src="/image.jpg" 
      alt="My Image" 
      width={500} 
      height={300}
      priority
    />
  )
}

동적 이미지 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ✅ 동적 이미지 처리
import Image from 'next/image'

interface ImageProps {
  src: string
  alt: string
}

export default function DynamicImage({ src, alt }: ImageProps) {
  return (
    <div className="relative w-full h-64">
      <Image 
        src={src}
        alt={alt}
        fill
        style=objectFit: cover
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        quality={75}
        loading="lazy"
      />
    </div>
  )
}

프로젝트 설정 최적화

TypeScript 설정 (tsconfig.json)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

ESLint 설정 (.eslintrc.json)

1
2
3
4
5
6
7
8
9
10
11
12
{
  "extends": [
    "next/core-web-vitals",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

디버깅 도구 및 테크닉

1. 타입 체크 명령어

1
2
3
4
5
# 타입 에러 확인
npx tsc --noEmit

# 상세한 빌드 로그 확인
next build --debug

2. VS Code 설정

1
2
3
4
5
6
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true
}

3. 유용한 디버깅 도구

  • TypeScript Error Translator
  • React Developer Tools
  • Next.js DevTools

성능 최적화 팁

  1. 코드 분할
    1
    2
    3
    4
    
    // ✅ 동적 임포트 사용
    const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
      loading: () => <p>Loading...</p>
    })
    
  2. 이미지 최적화
    1
    2
    3
    4
    
    // ✅ 이미지 최적화 설정
    const imageLoader = ({ src, width, quality }) => {
      return `https://example.com/${src}?w=${width}&q=${quality || 75}`
    }
    
  3. 캐싱 전략
    1
    2
    
    // ✅ 페이지 캐싱 설정
    export const revalidate = 3600 // 1시간마다 재검증
    

마치며

Next.js 14와 TypeScript를 함께 사용할 때 발생하는 빌드 에러들은 대부분 새로운 기능과 타입 시스템의 엄격한 검사로 인해 발생합니다. 이러한 에러들은 개발 과정에서 더 안정적인 코드를 작성하도록 도와주는 가이드라인으로 생각할 수 있습니다.

앞으로도 Next.js와 TypeScript는 계속 발전할 것이며, 이에 따라 새로운 형태의 에러들이 발생할 수 있습니다. 하지만 이 글에서 다룬 기본적인 문제 해결 접근 방식을 이해하고 있다면, 새로운 에러들도 충분히 해결할 수 있을 것입니다.

참고 자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.