Native JSON Modules — 번들러 없이 JSON을 import하는 시대
번들러가 항상 필요했던 이유
JavaScript에서 JSON 파일을 쓰려면 오래전부터 번들러가 필요했다. import config from './config.json'이 동작하는 건 webpack이나 Vite가 JSON을 JavaScript 모듈로 변환해줬기 때문이다. 브라우저 네이티브로는 안 됐다.
이제 그게 바뀌었다. 모던 브라우저와 Node.js, Deno, Bun이 네이티브 JSON 모듈을 지원하기 시작했다. 번들러 없이 import만으로 JSON을 다룰 수 있다.
문법: with { type: "json" }
// 정적 import
import config from './config.json' with { type: 'json' }
// 동적 import
const data = await import('./data.json', {
with: { type: 'json' }
})핵심은 with { type: 'json' } 절이다. 런타임에게 "이 파일을 JavaScript 코드가 아니라 데이터로 파싱하라"고 명시적으로 알려주는 계약이다. 비유하면 배송 박스에 '깨지기 쉬운 물품' 스티커를 붙이는 것과 같다. 내용물이 뭔지 처리 방법을 박스 바깥에서 명시하는 거다.
[💡 잠깐! 이 용어는?]
Import Attributes: import 구문에 메타데이터를 추가하는 문법. with { type: '...' }로 모듈 타입을 명시한다. 이전에는 assert 키워드를 썼지만 with으로 표준화됐다.
기존 번들러 방식과 뭐가 다른가
| 항목 | 번들러 방식 | Native JSON Modules |
|---|---|---|
| 처리 시점 | 빌드 타임 | 런타임 |
| 브라우저 ESM 지원 | 불필요 (번들 결과물) | 네이티브 지원 |
| 빌드 스텝 필요 여부 | 필수 | 불필요 |
| 번들에 인라인 | 보통 됨 | 기본적으로 안 됨 |
| 캐싱 | 번들 파일 기준 | HTTP 캐싱 + 모듈 캐싱 |
번들러 방식에서 JSON은 대부분 빌드 결과물에 인라인된다. Native JSON Modules는 별도 파일로 서빙되고, ES 모듈처럼 파싱 한 번 후 캐싱된다. 여러 파일에서 같은 JSON을 import해도 같은 인스턴스를 공유한다.
왜 with { type: 'json' }이 필요한가
확장자만으로는 파일 타입을 신뢰할 수 없다. .json 파일이 실제로는 악의적인 JavaScript를 담을 수 있다. CORS와 보안 정책이 개입하면 확장자 기반 추측은 더 복잡해진다.
with { type: 'json' }은 "이 파일을 코드로 실행하지 말고 데이터로만 처리하라"는 명시적 지시다. 브라우저와 런타임이 MIME 타입이나 Content-Type과 무관하게 일관되게 처리할 수 있다.
포인트: "import attributes는 파일 타입을 추측하는 대신, import하는 코드가 기대하는 것을 선언하는 패턴이다." — JSON 너머 CSS modules 등 미래 모듈 타입으로 확장 가능한 설계다.
실제 사용 사례
import messages from './ko.json' with { type: 'json' }
export function t(key) {
return messages[key] ?? key
}// 동적 로딩: 언어 파일을 lazy하게 가져올 때
async function loadLocale(lang) {
const messages = await import(`./locales/${lang}.json`, {
with: { type: 'json' }
})
return messages.default
}[💡 잠깐! 이 용어는?] ES 모듈 캐싱: 같은 URL의 모듈은 한 번만 파싱하고 이후 요청에서는 캐싱된 결과를 반환하는 메커니즘. 여러 파일에서 동일한 JSON을 import해도 파싱이 중복되지 않는다.
브라우저 지원
Chrome, Firefox, Safari 최신 버전과 Node.js 22+, Deno, Bun이 지원한다. import() 동적 방식과 정적 import 모두 동작한다.
번들러를 완전히 대체하는 건 아니다. 프로덕션에서 번들링이 필요한 이유(트리 쉐이킹, 코드 스플리팅, 폴리필)는 여전히 존재한다. 다만 JSON 파일 하나를 위해 번들러 설정을 추가해야 하는 상황은 줄어든다. 비유하면 대형 공사 현장에서 인부가 여전히 필요하지만, 나사 하나 조이려고 굳이 공사팀 전체를 부르지 않아도 되는 것과 같다.
마무리
Native JSON Modules는 작은 변화처럼 보이지만 의미가 있다. 웹 플랫폼이 JSON을 first-class 모듈로 다루기 시작했다는 신호이기 때문이다. with { type: 'json' } 문법은 앞으로 CSS modules 등으로 확장될 Import Attributes 표준의 첫 번째 실용적 사례다. 번들러 없는 간단한 프로젝트나 엣지 함수에서는 바로 써볼 수 있다.
참고:
관심 있을 만한 포스트
Babel 8 Beta — CJS를 버리고 ESM 전용으로 간다
2년간의 알파를 거쳐 베타에 진입한 Babel 8의 핵심 변경사항과 마이그레이션 전략을 정리한다.
Babel 7.29.0 — 10년 역사의 마지막 마이너, 그리고 8 RC1
2026년 1월 31일, Babel 7의 마지막 마이너 릴리스가 공개됐다. 이 버전이 갖는 역사적 의미와 Babel 8 RC1의 핵심 변화를 정리한다.
Error.isError() — realm을 넘나드는 안전한 에러 검사 API
instanceof Error가 iframe과 worker에서 실패하는 이유, 그리고 이를 근본적으로 해결하는 Error.isError()의 동작 원리를 정리한다.
V8의 Sea of Nodes 탈출기 — 왜 우아한 이론이 실전에서 무너졌는가
V8 팀이 10년간 사용한 Sea of Nodes IR을 포기하고 Turboshaft로 전환한 7가지 이유와 그 교훈을 정리한다.
배포 전 출국 심사 — Publint로 npm 패키지 실수를 원천 차단하는 법
npm 패키지의 exports, entry points, 모듈 포맷을 배포 전에 검증하는 Publint 도구 사용법.
jQuery 4.0 — 10년 만의 메이저 릴리스, 무엇이 바뀌었나
jQuery가 20주년을 맞아 10년 만에 메이저 버전을 출시했다. IE 지원 축소, ES 모듈 전환, Trusted Types 등 핵심 변경 사항을 정리한다.
V8 Mutable Heap Numbers — 숫자 하나 바꿀 때마다 새 객체를 만들던 비효율을 잡다
V8 엔진이 스크립트 컨텍스트의 숫자 변수를 매번 새 HeapNumber로 할당하던 방식을 제자리 수정(mutable)으로 바꿔 최대 2.5배 성능 향상을 달성했다.
V8 Explicit Compile Hints — 주석 한 줄로 JavaScript 시작 속도를 630ms 줄이는 법
Chrome 136에 도입된 V8의 Explicit Compile Hints 기능으로 JavaScript 초기 로딩 성능을 개선하는 원리와 사용법을 분석한다.