CPU 아키텍처는 CPU가 실행하는 명령어 집합(Instruction Set Architecture, ISA)의 종류다. 서버와 PC에서는 x86-64와 ARM64 두 계열이 주로 쓰이고, 한 계열용으로 컴파일된 기계어는 다른 계열 CPU에서 실행되지 않는다. Node.js 프로젝트에서는 이 차이가 네이티브 모듈에서 드러난다.
두 계열
| 계열 | 다른 이름 | 흔한 곳 |
|---|---|---|
| x86-64 | x64, amd64 | 대부분의 데스크톱·서버, 인텔 맥 |
| ARM64 | aarch64, arm64 | 스마트폰, 애플 실리콘 맥, ARM 기반 클라우드 서버 |
같은 "덧셈"이라도 두 계열의 기계어 인코딩이 다르다. C·C++·Rust 코드를 컴파일하면 대상 아키텍처를 정해서 그 계열의 기계어로 된 네이티브 바이너리(.node·.so·.dll)가 나온다. 태어날 때부터 특정 아키텍처에 묶인다.
JS는 왜 상관없나
JS 파일은 미리 기계어로 굳혀 둔 것이 아니다. 실행하는 Node.js의 V8 엔진이 그 자리에서 바이트코드로 해석하고, 자주 도는 코드는 JIT(Just-In-Time) 컴파일한다. 아키텍처에 묶이는 것은 각 서버에 따로 설치된 Node.js 런타임이지 JS 파일이 아니다. 그래서 같은 JS 번들이 x86-64 서버에서도 ARM64 서버에서도 돈다.
문제는 npm 패키지 중 일부가 내부에 네이티브 바이너리를 품고 있다는 것이다. 이미지 처리 라이브러리 sharp가 대표적이다. sharp는 C 라이브러리 libvips를 감싼 얇은 JS 래퍼라서, 실제 무게 대부분이 플랫폼별로 컴파일된 libvips다.
플랫폼별 패키지로 나눠 배포하기
sharp는 미리 컴파일한 바이너리를 @img/sharp-linux-arm64, @img/sharp-linuxmusl-x64, @img/sharp-darwin-arm64처럼 플랫폼별 패키지로 나누고, 이를 모두 optionalDependencies로 건다. 각 패키지의 package.json에 os·cpu·libc 필드가 있어서, 패키지 매니저는 설치하는 머신에 맞는 것만 받고 나머지는 건너뛴다(2026 기준).
- 아키텍처뿐 아니라 C 표준 라이브러리도 맞아야 한다. 같은 Linux x64라도 glibc(Debian·Ubuntu)와 musl(Alpine)용 바이너리가 따로 있다
- 다른 플랫폼용을 함께 받아 두려면 npm v10+는
--os·--cpu·--libc플래그, pnpm v8+·yarn v3+는 설정 파일의supportedArchitectures를 쓴다(npm과 pnpm) optionalDependencies설치를 끄는 설정이면 바이너리가 아예 없어 실행 때 실패한다
빌드 머신과 실행 머신이 다르면
설치 시점의 플랫폼이 결과물에 새겨진다. 그래서 x64 CI 러너에서 node_modules를 만들어 ARM64 서버로 옮기면, JS는 돌지만 네이티브 모듈을 불러오는 순간 로드에 실패한다. 이미지 최적화처럼 특정 요청에서만 불리는 모듈이면 배포 직후가 아니라 런타임에 늦게 터진다.
빌드 머신 arch ──결정──▶ 결과물 속 바이너리 arch ──일치해야 함──▶ 실행 머신 arch해결은 셋 중 하나다.
- 실행 아키텍처에서 설치·빌드한다. ARM 러너를 쓰거나 Docker에서
--platform을 지정한다.docker buildx build --platform linux/arm64는 다른 아키텍처면 QEMU 에뮬레이션으로 돌아서 느리다. 멀티 스테이지에서FROM --platform=$BUILDPLATFORM인 스테이지가 설치를 하면 빌더 아키텍처용 바이너리가 들어가니 주의한다(Dockerfile: 레이어 캐시와 멀티 스테이지) - 대상 플랫폼 바이너리를 명시해 설치한다. 위의
--cpu·supportedArchitectures - 쓰지 않는 네이티브 모듈을 결과물에서 뺀다. 결과물이 순수 JS가 되면 빌드와 실행 아키텍처가 서로 독립한다
Next.js와 sharp
Next.js 16은 sharp를 optionalDependencies로 갖고 있고, next/image의 이미지 최적화에 쓴다(Next.js 16.3 기준). 이미지 최적화를 쓰지 않는다면 images.unoptimized: true로 끄고, standalone 출력의 outputFileTracingExcludes로 sharp와 @img/*를 트레이스에서 빼는 방법을 쓸 수 있다. 문서가 직접 안내하는 조합은 아니므로, 뺀 뒤 이미지가 나오는 화면을 실제로 확인한다. libvips는 이미 컴파일된 바이너리라 gzip으로 잘 줄지 않아서, 압축한 배포 아티팩트에서도 꽤 큰 비중을 차지하곤 한다. 반대로 최적화를 쓴다면 실행 플랫폼용 sharp가 결과물에 들어가도록 설치 플랫폼을 맞춰야 한다. glibc Linux에서는 메모리 사용이 커질 수 있다는 sharp 쪽 안내도 있다.
출처: sharp - Installation: Cross-platform · Next.js - Self-Hosting: Image Optimization · Docker - Multi-platform builds