본문으로 건너뛰기
개발 머꼬
개발 노트Node.js
hohyeon.dev64

server-only를 붙인 파일을 시드 스크립트가 불렀더니 바로 죽은 이유 — 조건부 exports

  • #Engineering Note
  • #Next.js
  • #Node.js

문제 발생

DB 클라이언트 모듈이 Client Component 번들에 실수로 들어가지 않도록 import "server-only"를 맨 위에 두었습니다. Next.js 앱에서는 아무 문제가 없었는데, 같은 모듈을 쓰는 시드 스크립트를 node src/seed.ts로 돌리자 첫 줄에서 죽었습니다.

Error: This module cannot be imported from a Client Component module. It should only be used from a Server Component.

Client Component가 아닌데 그렇게 말합니다.

원인 분석

server-only는 조건부 exports 하나로 된 표식 패키지입니다. 설치된 package.json을 열어 보면 전부입니다.

{
  "exports": {
    ".": {
      "react-server": "./empty.js",
      "default": "./index.js"
    }
  }
}

index.js의 내용은 위 오류를 던지는 한 줄이고, empty.js는 비어 있습니다. Next.js가 Server Component를 번들할 때 react-server 조건을 켜므로 그 환경에서만 빈 모듈로 풀립니다. Client Component 번들에는 그 조건이 없으니 throw하고, 그것이 이 패키지가 하려는 일입니다.

평범한 Node 실행에도 react-server 조건이 없습니다. Node 문서에 따르면 exports의 조건은 node, import, require, default 같은 내장 조건과 --conditions 플래그로 더한 사용자 조건으로 풀립니다. 아무것도 더하지 않으면 default가 선택되고, 그것이 throw하는 쪽입니다. 그래서 "Client Component에서 불렀다"는 메시지가 시드 스크립트에서 나옵니다 — 메시지가 가정하는 세상에 Node 스크립트가 없을 뿐입니다.

해결 방안

  1. 가드 없는 모듈과 가드된 진입점을 파일로 나눕니다. pool.ts는 풀을 만들기만 하고 아무 가드도 없습니다. client.tsimport "server-only" 뒤에 pool.ts를 재노출합니다. Next.js 앱은 client.ts만 import하고, 스크립트는 pool.ts를 직접 import합니다.
// pool.ts — 가드 없음
export const db = drizzle(createPool(process.env.DATABASE_URL));

// client.ts — 앱 전용
import "server-only";
export { db } from "./pool.ts";
  1. "안 쓰는 export만 안 쓰면 된다"는 성립하지 않습니다. ES 모듈은 import 시 파일 전체를 실행하므로 가드가 있는 파일을 거치는 순간 throw합니다. 그래서 함수를 나누는 것이 아니라 파일 자체를 나눕니다. 비밀값을 읽는 서버 전용 env 모듈도 같은 이유로 파일을 분리합니다.

  2. --conditions=react-server로 우회하지 않습니다. 동작은 하지만 스크립트가 서버 렌더 환경인 척하는 것이고, 그 조건에 따라 다르게 풀리는 다른 패키지까지 함께 바뀝니다.

  3. 경계를 문서에 적습니다. 어느 파일이 가드 없는 쪽인지, 누가 그것을 직접 import해도 되는지(빌드 밖의 순수 Node 스크립트만)를 규칙으로 남겨야 다음 사람이 pool.ts를 앱에서 import하지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.