브라우저에서 JSON import vs fetch: 언제 무엇을 써야 하는가?

okorion·2025년 11월 14일

// 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()를 대체하는가?
→ 결론은 아니다. 특정 상황에서만 제한적으로 사용해야 한다.
이 글은 그 이유를 에러 처리 · 캐싱 구조 · 메모리 관리 · 번들링 영향까지 포함하여 가장 세밀하게 정리한 기술 분석이다.


1. Static JSON Import와 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 importfetch()
네트워크 실패 시모듈 그래프 전체 실패try/catch로 세밀한 핸들링 가능
파싱 실패 시모듈 자체가 실패response.text()로 원본 접근 가능
캐싱 방식세션 동안 영구 모듈 캐싱GC 가능. 재요청 가능
메모리 점유한 번 로드되면 해제 불가참조 해제 시 GC 가능
API 특성모듈 시스템의 일부HTTP 요청·스트리밍·상태 코드 기반 처리
사용 권장빌드 타임 번들 리소스사용자 입력 기반 동적 요청

정리하면:
JSON import는 "코드처럼 취급되는 정적 자원", fetch는 "실행 중 발생하는 동적 데이터"에 적합하다.


2. 에러 처리: import는 너무 단단하고 fetch는 세밀하다

2-1. static import는 실패하면 전체 모듈 그래프가 붕괴된다

import data from './3rd-party.json' with { type: 'json' };

만약 CDN이나 외부 JSON이 잠시 다운되면?

해당 모듈을 사용하는 모든 스크립트가 실행되지 않는다.
→ React 앱 자체가 비정상 시작
→ 화면 전체가 빈 페이지

네트워크 불안정한 환경에서는 절대 사용 불가.

2-2. dynamic import는 훨씬 안전

try {
  const { default: data } = await import(url, { with: { type: 'json' } });
} catch (err) {
  // fallback 처리 가능
}

하지만 여전히 fetch와 비교하면 진단 능력 부족:

  • response.status 없음
  • response.headers 없음
  • response.text()로 디버깅 불가
  • CORS, preflight 실패 사유 접근 불가

2-3. 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");
}

따라서:

  • “404? 500? CORS?” 명확히 파악 가능
  • JSON 파싱 실패 시 원본 text 접근 가능

3. JSON import의 치명적 문제: 메모리 누수

3-1. import()는 세션 생명 주기 전체에 걸쳐 모듈 캐싱이 영구적

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 가능

3-2. 큰 JSON을 일부만 쓰고 버려도 import는 해제를 못한다

// 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 대상

4. JSON import가 유효한 유일한 상황

4-1. “완전히 정적이며 빌드 타임에 포함될 JSON”

예:

  • 로컬 설정 파일
  • 정적 mock 데이터
  • 에셋 manifest
  • Vite/Rollup이 최적화할 수 있는 JSON

이 경우 import는 다음 장점이 있다:

  • bundler가 그래프에 합침
  • dead code elimination 가능
  • minify 가능
  • 타입 추론 가능

"코드와 함께 묶여야 하는 JSON"에만 import가 유효하다.


5. Bundler의 트리쉐이킹 차이

기본 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 불가.


6. 결론: JSON import는 좋지만 fetch를 절대 대체하지 못한다

JSON import를 써야 하는 경우

  • 빌드 타임에 포함될 정적 JSON
  • 네트워크 요청 필요 없음
  • 모듈 그래프에 넣어도 안전
  • bundle-time plugin이 최적화할 수 있는 경우

JSON import를 쓰면 안 되는 경우

  • 사용자 입력 기반 요청 (검색, 필터링, 동적 페이지네이션)
  • 대규모 JSON 데이터 (메모리 누수 발생)
  • 외부 API나 불안정한 네트워크
  • fallback이 필요한 모든 상황
  • JSON 파싱 실패 원본이 필요한 경우
  • 캐싱 전략이 필요할 때

JSON import를 절대 사용하면 안 되는 최악의 사례

await import(`https://api.example.com/user/${id}.json`);

→ 메모리 누수 + fallback 불가 + 디버깅 불가 + 의도치 않은 캐싱


7. 최종 요약

상황import JSONfetch JSON
정적 JSON최적사용 가능
대규모 JSON비추천 (GC 안 됨)안전
동적 URL절대 비추천최적
에러 핸들링약함강함
상태 코드 접근불가가능
원본 JSON 확인불가가능
캐싱 제어불가능캐시 옵션 가능
메모리 관리영구 캐싱GC 가능
번들러 최적화가능불가

정적 JSON에만 import를 쓰고, 동적 데이터는 무조건 fetch를 사용하라.
JSON import는 fetch의 대체제가 아니라 정적 리소스를 위한 모듈 시스템 확장에 가깝다.


profile
okorion's Tech Study Blog.

0개의 댓글