home

Next.js에서 런타임에 읽는 파일이 배포에서 사라질 때

fs.readFile로 읽는 파일이 production 배포에서 ENOENT를 내는 원인과 outputFileTracingIncludes로 해결하는 방법을 정리한다.

서버 컴포넌트에서 마크다운 파일을 직접 읽어 렌더하는 패턴을 자주 쓴다. 로컬 pnpm dev에서는 잘 동작하지만, 같은 코드가 production 배포에서 ENOENT를 내는 경우가 있다.

Error: ENOENT: no such file or directory, open '/var/task/privacy/2026-05-25.md'

원인은 Next.js의 빌드 시 파일 추적 메커니즘이 그 파일을 배포 번들에 포함시키지 못한 것이다.

Next.js의 file tracing

Next.js는 빌드 시점에 @vercel/nft(node-file-trace)로 의존성 트리를 정적 분석한다. 결과는 .next/standalone 디렉터리나 Vercel serverless 함수 번들에 모인다. 각 라우트가 런타임에 실제로 필요할 파일만 압축해서 배포하기 위함이다.

추적 대상은 다음과 같다.

  • import / require로 명시한 모듈
  • 그 모듈이 다시 import한 자산
  • 정적 분석으로 추론 가능한 파일 경로

정적 분석이 놓치는 케이스

문제는 nft가 정적 분석만 한다는 점이다. 런타임에 결정되는 파일 경로는 잡히지 않거나 보수적으로 처리되어 누락된다.

// 잡힘: 모듈 그래프에 직접 포함됨
import data from "./data.json"

// 못 잡을 수 있음: 경로가 동적으로 조립됨
const filePath = path.join(process.cwd(), "privacy/2026-05-25.md")
const markdown = await readFile(filePath, "utf-8")

process.cwd()나 변수가 섞이면 nft는 어떤 파일이 실제로 열릴지 단정할 수 없으므로 안전하게 제외하는 경향이 있다. 빌드 자체는 성공하지만, 배포된 serverless 함수가 호출되는 순간 파일이 없어 ENOENT가 발생한다.

outputFileTracingIncludes로 명시

해결 방법은 next.config.tsoutputFileTracingIncludes에 포함시킬 파일을 명시하는 것이다.

import type { NextConfig } from "next"

const nextConfig: NextConfig = {
  outputFileTracingIncludes: {
    "/privacy": ["./privacy/**/*.md"],
  },
}

export default nextConfig

키는 페이지 라우트 경로, 값은 그 라우트에 함께 패키징할 파일 glob 배열이다. 빌드 후 /privacy 라우트의 serverless 함수 번들에 ./privacy/ 아래 모든 .md 파일이 포함된다.

키 형식 몇 가지 예시:

  • 단순 라우트: "/privacy"
  • 동적 라우트: "/posts/[slug]"
  • API 라우트: "/api/feed"

언제 필요한가

이 설정은 정적 분석이 추적하지 못하는 파일 접근이 있을 때만 필요하다.

  • fs.readFile, fs.readdir, fs.readFileSync 등으로 런타임에 파일을 읽을 때
  • import("./content.md")처럼 정적 import 형태면 자동 처리되므로 불필요
  • public/ 폴더 파일은 별도 CDN 경로로 항상 서빙되므로 불필요

운영 관점

이 종류의 버그는 pnpm dev에서 절대 안 나타난다. 개발 서버는 실제 디스크에서 직접 읽기 때문에 file tracing 결과와 무관하게 동작한다. 문제는 production 빌드 후 격리된 serverless 환경에서 처음 호출됐을 때 비로소 드러난다.

그래서 외부 파일을 런타임에 읽는 패턴을 도입할 때는 처음부터 outputFileTracingIncludes를 같이 추가하는 편이 안전하다. PR 머지 후 배포된 production에서 처음 발견하면 한참 거슬러 올라가야 한다.

댓글

아직 댓글이 없습니다.