// 2025년 기준 기술 스택에 맞춘 상세 분석
JavaScript는 2025년을 기준으로 JSON 모듈 import 기능을 모든 주요 브라우저에 기본 제공하기 시작했다.
import data from './data.json' with { type: 'json' };
// 혹은 동적 import
const { default: data } = await import('./data.json', {
with: { type: 'json' },
});
자바스크립트 역사에서 중요한 변화지만, 이 기능이 fetch()를 대체하는가?
→ 결론은 아니다. 특정 상황에서만 제한적으로 사용해야 한다.
이 글은 그 이유를 에러 처리 · 캐싱 구조 · 메모리 관리 · 번들링 영향까지 포함하여 가장 세밀하게 정리한 기술 분석이다.
두 코드는 겉보기엔 같은 목적(“JSON 불러오기”)처럼 보인다.
import JSON
import data from './data.json' with { type: 'json' };
fetch(JSON)
const response = await fetch('./data.json');
const data = await response.json();
하지만 둘은 구조적으로 완전히 다른 방식으로 동작한다:
| 비교 항목 | JSON import | fetch() |
|---|---|---|
| 네트워크 실패 시 | 모듈 그래프 전체 실패 | try/catch로 세밀한 핸들링 가능 |
| 파싱 실패 시 | 모듈 자체가 실패 | response.text()로 원본 접근 가능 |
| 캐싱 방식 | 세션 동안 영구 모듈 캐싱 | GC 가능. 재요청 가능 |
| 메모리 점유 | 한 번 로드되면 해제 불가 | 참조 해제 시 GC 가능 |
| API 특성 | 모듈 시스템의 일부 | HTTP 요청·스트리밍·상태 코드 기반 처리 |
| 사용 권장 | 빌드 타임 번들 리소스 | 사용자 입력 기반 동적 요청 |
정리하면:
JSON import는 "코드처럼 취급되는 정적 자원", fetch는 "실행 중 발생하는 동적 데이터"에 적합하다.
import data from './3rd-party.json' with { type: 'json' };
만약 CDN이나 외부 JSON이 잠시 다운되면?
→ 해당 모듈을 사용하는 모든 스크립트가 실행되지 않는다.
→ React 앱 자체가 비정상 시작
→ 화면 전체가 빈 페이지
즉 네트워크 불안정한 환경에서는 절대 사용 불가.
try {
const { default: data } = await import(url, { with: { type: 'json' } });
} catch (err) {
// fallback 처리 가능
}
하지만 여전히 fetch와 비교하면 진단 능력 부족:
try {
const res = await fetch(url);
if (!res.ok) {
console.log(res.status, res.statusText);
}
const data = await res.json();
} catch (err) {
console.error("Network or parsing error");
}
따라서:
const { default: results } = await import(`/api/search?q=${input}`, {
with: { type: 'json' },
});
사용자 검색어가 바뀔 때마다?
→ 새로운 URL
→ 새로운 JSON module
→ 새로운 모듈이 모듈 그래프에 누적됨
→ GC 불가
→ 검색어 500번 입력 시 모듈 500개 영구 보관
→ 심각한 메모리 누수
fetch는 이런 문제가 없다.
let res = await fetch(url);
let data = await res.json();
data = null; // GC 가능
// import 사용
let { default: large } = await import('/large-data.json', { with: { type: 'json' } });
const small = large.slice(0, 10);
large = null; // 의미 없음. 모듈에 남아 있음
fetch에서는 다음이 가능:
let large = await (await fetch(...)).json();
const small = large.slice(0, 10);
large = null; // 나머지 GC 대상
예:
이 경우 import는 다음 장점이 있다:
즉 "코드와 함께 묶여야 하는 JSON"에만 import가 유효하다.
기본 import는 트리쉐이킹 안 된다:
import config from './package.json';
console.log(config.version); // json 전체가 번들에 포함됨
하지만 bundler 확장 문법을 쓰면 top-level key 단위로 tree-shaking 가능:
import { version } from './package.json';
console.log(version);
※ 단, 중첩 구조는 여전히 tree-shake 불가.
await import(`https://api.example.com/user/${id}.json`);
→ 메모리 누수 + fallback 불가 + 디버깅 불가 + 의도치 않은 캐싱
| 상황 | import JSON | fetch JSON |
|---|---|---|
| 정적 JSON | 최적 | 사용 가능 |
| 대규모 JSON | 비추천 (GC 안 됨) | 안전 |
| 동적 URL | 절대 비추천 | 최적 |
| 에러 핸들링 | 약함 | 강함 |
| 상태 코드 접근 | 불가 | 가능 |
| 원본 JSON 확인 | 불가 | 가능 |
| 캐싱 제어 | 불가능 | 캐시 옵션 가능 |
| 메모리 관리 | 영구 캐싱 | GC 가능 |
| 번들러 최적화 | 가능 | 불가 |
정적 JSON에만 import를 쓰고, 동적 데이터는 무조건 fetch를 사용하라.
JSON import는 fetch의 대체제가 아니라 정적 리소스를 위한 모듈 시스템 확장에 가깝다.