외부 원문 링크
JSON 가져오기(import) vs 페치(fetch)
JSON 모듈 import 가 표준이 됐다고 fetch().json() 을 대체하면 안 된다. 둘은 에러 처리·캐싱·GC 가 다른 별개의 도구다.
핵심 요약
두 방식은 같은 'JSON 가져오기' 처럼 보이지만 의미론이 다르다.
// 1) JSON 모듈 import (정적/동적)
import data from './data.json' with { type: 'json' };
const { default: data } = await import('./data.json', { with: { type: 'json' }});
// 2) Fetch 후 JSON 파싱
const res = await fetch('./data.json');
const data = await res.json();
주요 차이를 표로 정리:
| 측면 | import ... with { type: 'json' } | fetch().json() |
|---|---|---|
| 실패 시 | 정적 import 는 모듈 그래프 중단, 동적은 catch 가능 | try/catch + response.status / text() 로 풍부한 진단 |
| 캐싱 | 환경 수명 동안 모듈 캐시에 영구 보관 | 응답 객체 참조가 끊기면 GC 됨 |
| 부분 GC | 전체 객체가 메모리에 남음 | slice 후 원본을 null 하면 큰 부분 GC 가능 |
| 동적 URL | 검색 쿼리마다 모듈이 쌓여 누수 | 자연스럽게 처리 |
| 번들러 | esbuild/Vite/Rollup 은 최상위 키 트리쉐이킹 가능 | 일반적으로 불가 |
결론: 로컬 정적 JSON 은 import, 동적/서드파티 JSON 은 fetch. 트리쉐이킹이 핵심이라면 빌드 타임에 처리하는 Vite/Rollup 플러그인이 더 깔끔하다.
JSON import 는 "모듈 그래프에 영구 편입", fetch().json() 은 "수명이 짧은 일회성 페이로드". 핵심은 메모리 모델이다 — import 한 데이터는 환경 수명 동안 캐시되어 GC 되지 않고, fetch 한 데이터는 참조가 끊기면 GC 된다. 동적 데이터에 import 를 쓰는 순간 메모리 누수의 길에 들어선다.
최근 모든 브라우저 엔진에 import data from './data.json' with { type: 'json' } 가 구현되었지만, "이제 fetch 안 써도 돼" 로 넘어가는 사람이 많다. 이는 다음 세 가지를 놓치는 함정이다.
- 에러 처리: import 실패는 모듈 그래프 전체 를 중단시킨다 — 서드파티 데이터에는 부적합
- 캐싱: import 한 모듈은 페이지 수명 동안 보관 — 동적 URL 로 넣으면 그대로 누수
- 부분 GC: fetch 결과는 슬라이스를 남기고 큰 부분만 GC 가능, import 는 통째로 메모리에 머묾
면접에서 "표준이 됐다고 항상 좋은가" 같은 질문에 언제 안 되는지 까지 답할 수 있어야 깊이가 산다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
에러 폭발 반경(blast radius) 의 차이
정적 import 가 실패하면 모듈 그래프 전체가 중단 된다 — 그 모듈에 의존하는 모든 코드가 함께 죽는다. 서드파티 데이터처럼 때때로 실패하는 리소스에는 치명적이다.
// 정적 — 실패 시 전체 모듈 중단
import data from './third-party.json' with { type: 'json' };
// 동적 — 격리 가능
try {
const { default: data } = await import(url, { with: { type: 'json' }});
} catch (e) { /* fallback */ }
// fetch — 더 풍부한 진단
try {
const r = await fetch(url);
if (!r.ok) handleHttp(r.status);
const text = await r.text(); // 파싱 실패해도 raw 보존
const data = JSON.parse(text);
} catch (e) { /* fallback */ }
fetch 는 HTTP 상태와 raw 텍스트 를 동시에 보존한다는 점에서 디버깅 가치가 높다.
정적 import 로 외부 API 같은 불안정한 데이터를 가져오는 것. 실패 시 앱 전체가 부팅에 실패한다.
동적 URL + import = 메모리 누수
import 된 모듈은 환경(페이지/워커) 수명 동안 모듈 캐시에 보관된다. 같은 specifier 의 import 는 같은 모듈을 반환한다 — 이건 보통 장점이지만, 동적 쿼리 URL 에 쓰면 함정이 된다.
// 검색마다 새 specifier — 모듈 그래프에 쌓임
const { default: results } = await import(
`/api/search?q=${q}`,
{ with: { type: 'json' }}
);
각 검색 결과가 페이지 수명 끝까지 살아남는다. fetch 였다면 변수 참조가 끊기는 즉시 GC 된다. 동적 데이터 = fetch 가 안전한 디폴트다.
쿼리 파라미터로 import 를 쓰면 "브라우저가 알아서 정리하겠지" 라고 가정하는 것. 모듈은 GC 대상이 아니다.
부분 추출 후 GC — fetch 만 가능
큰 JSON에서 작은 부분만 쓰고 나머지를 버리는 패턴은 fetch 만 안전하다.
// fetch — 원본 GC 가능
let data = await (await fetch('/large.json')).json();
const small = data.items.slice(0, 10);
data = null; // 나머지 GC 가능
// import — 통째로 메모리
let { default: data } = await import('/large.json', { with: { type: 'json' }});
const small = data.items.slice(0, 10);
data = null; // 모듈 캐시에 그대로 — GC 안 됨
특히 package.json 같은 큰 파일에서 version 한 줄만 필요한 경우 — 번들러의 최상위 키 트리쉐이킹이 받쳐주지 않으면 import 는 그대로 낭비다.
프런트엔드 코드에서 import data from 'package.json' 으로 버전 문자열만 꺼내는 것. 번들 폭발 의 흔한 원인이다.
읽는 순서
- 1이론
Jake Archibald 원문과 MDN의 Import attributes 페이지를 읽고, 차이 표 를 본인 손으로 5칸 이상 채워보세요. 다음 질문에 답할 수 있어야 합니다 — "실패 / 캐싱 / GC / 동적 / 번들".
- 2구현
같은 큰 JSON(예: 2MB) 을 (a) 정적 import, (b) fetch + json() 두 가지로 로드한 뒤 Chrome DevTools Memory 탭에서 Heap Snapshot 을 비교하세요. import 쪽은 변수
null처리에도 사이즈가 안 줄어드는 걸 직접 확인합니다. - 3실무
현재 프로젝트의 import 한 JSON 들을 grep 으로 찾아 (정적/동적, 사용 비율, 동적 URL 여부) 세 컬럼 표를 만들고, fetch 로 옮겨야 할 후보를 선별하세요.
- 4설명
팀에 'JSON import 를 언제 쓰지 말아야 하는가' 를 5분 발표. 에러 폭발 / 메모리 누수 / 트리쉐이킹 한계 세 축으로 정리하고 대안을 제시합니다.
면접 연결 질문
[감점 답변] '취향 차이'. [좋은 답변] 세 축으로 답한다.
- 정적인가 동적인가 — 빌드 시점에 알 수 있는 로컬 리소스면 import, 런타임 URL/쿼리면 fetch
- 실패 격리 — 실패가 모듈 그래프 전체를 죽여도 되면 import, 부분 fallback 이 필요하면 fetch
- 메모리 수명 — 페이지 끝까지 메모리에 남아도 되면 import, 일회성이면 fetch
번들러의 최상위 키 트리쉐이킹이 동작하는 작은 정적 리소스 가 import 의 진짜 sweet spot 이다.
[감점 답변] 'JSON 동적이라 안 됨'. [좋은 답변] 두 축. (1) 에러: 네트워크 실패가 현재 모듈 그래프 를 죽여 fallback 이 불가능. (2) 메모리: import 된 모듈은 환경 수명 동안 캐시되므로 검색 쿼리마다 결과가 쌓여 누수. fetch 는 응답 객체가 GC 대상이라 자연스럽게 처리된다.
[감점 답변] '트리쉐이킹은 자동임'. [좋은 답변] esbuild/Vite/Rollup 은 최상위 키 만 export 로 노출해 트리쉐이킹한다 — import { version } from './package.json'. 깊게 중첩된 데이터는 여전히 통째로 번들된다. 깊은 데이터 일부만 필요하면 빌드 타임 플러그인 으로 변환하는 게 가장 깔끔하다.
자기 점검
동적 import() 도 동기 import 와 똑같이 모든 실패에 취약하다는 오해. 동적은 promise rejection 으로 격리 된다.
'캐싱은 무조건 빠르다' 라는 단순화. 데이터 수명 ≠ 페이지 수명일 때 그 캐싱이 누수가 된다.
번들러가 다 알아서 트리쉐이킹한다는 가정. 깊은 키는 못 자른다 — 빌드 플러그인으로 한 줄만 뽑아내는 게 정답이다.