
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.ts의 outputFileTracingIncludes에 포함시킬 파일을 명시하는 것이다.
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에서 처음 발견하면 한참 거슬러 올라가야 한다.
댓글
아직 댓글이 없습니다.