FEInterview Prep

외부 원문 링크

JSON 가져오기(import) vs 페치(fetch)

JSON 모듈 import 가 표준이 됐다고 fetch().json() 을 대체하면 안 된다. 둘은 에러 처리·캐싱·GC 가 다른 별개의 도구다.

2025-11-05·6분 읽기
JavaScript성능브라우저
원문 보기 ↗

핵심 요약

두 방식은 같은 '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 텍스트 를 동시에 보존한다는 점에서 디버깅 가치가 높다.

blast radiusgraceful degradationHTTP statusJSON parse error
자주 하는 오해

정적 import 로 외부 API 같은 불안정한 데이터를 가져오는 것. 실패 시 앱 전체가 부팅에 실패한다.

동적 URL + import = 메모리 누수

import 된 모듈은 환경(페이지/워커) 수명 동안 모듈 캐시에 보관된다. 같은 specifier 의 import 는 같은 모듈을 반환한다 — 이건 보통 장점이지만, 동적 쿼리 URL 에 쓰면 함정이 된다.

// 검색마다 새 specifier — 모듈 그래프에 쌓임
const { default: results } = await import(
  `/api/search?q=${q}`,
  { with: { type: 'json' }}
);

각 검색 결과가 페이지 수명 끝까지 살아남는다. fetch 였다면 변수 참조가 끊기는 즉시 GC 된다. 동적 데이터 = fetch 가 안전한 디폴트다.

module cachegarbage collectionlifetimespecifier
자주 하는 오해

쿼리 파라미터로 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 는 그대로 낭비다.

tree-shakingtop-level keyssubset extractionbuild-time vs runtime
자주 하는 오해

프런트엔드 코드에서 import data from 'package.json' 으로 버전 문자열만 꺼내는 것. 번들 폭발 의 흔한 원인이다.

읽는 순서

  1. 1이론

    Jake Archibald 원문과 MDN의 Import attributes 페이지를 읽고, 차이 표 를 본인 손으로 5칸 이상 채워보세요. 다음 질문에 답할 수 있어야 합니다 — "실패 / 캐싱 / GC / 동적 / 번들".

  2. 2구현

    같은 큰 JSON(예: 2MB) 을 (a) 정적 import, (b) fetch + json() 두 가지로 로드한 뒤 Chrome DevTools Memory 탭에서 Heap Snapshot 을 비교하세요. import 쪽은 변수 null 처리에도 사이즈가 안 줄어드는 걸 직접 확인합니다.

  3. 3실무

    현재 프로젝트의 import 한 JSON 들을 grep 으로 찾아 (정적/동적, 사용 비율, 동적 URL 여부) 세 컬럼 표를 만들고, fetch 로 옮겨야 할 후보를 선별하세요.

  4. 4설명

    팀에 'JSON import 를 언제 쓰지 말아야 하는가' 를 5분 발표. 에러 폭발 / 메모리 누수 / 트리쉐이킹 한계 세 축으로 정리하고 대안을 제시합니다.

면접 연결 질문

mediumJSON 모듈 import 와 `fetch().json()` 을 *언제 각각 써야 하는지* 본인의 기준으로 설명해보세요.
힌트

[감점 답변] '취향 차이'. [좋은 답변] 세 축으로 답한다.

  1. 정적인가 동적인가 — 빌드 시점에 알 수 있는 로컬 리소스면 import, 런타임 URL/쿼리면 fetch
  2. 실패 격리 — 실패가 모듈 그래프 전체를 죽여도 되면 import, 부분 fallback 이 필요하면 fetch
  3. 메모리 수명 — 페이지 끝까지 메모리에 남아도 되면 import, 일회성이면 fetch

번들러의 최상위 키 트리쉐이킹이 동작하는 작은 정적 리소스 가 import 의 진짜 sweet spot 이다.

hard`import data from '/api/search?q=foo' with { type: 'json' }` 같은 코드가 왜 위험한지 설명해보세요.
힌트

[감점 답변] 'JSON 동적이라 안 됨'. [좋은 답변] 두 축. (1) 에러: 네트워크 실패가 현재 모듈 그래프 를 죽여 fallback 이 불가능. (2) 메모리: import 된 모듈은 환경 수명 동안 캐시되므로 검색 쿼리마다 결과가 쌓여 누수. fetch 는 응답 객체가 GC 대상이라 자연스럽게 처리된다.

medium번들러의 'JSON 트리쉐이킹' 이 어디까지 동작하는지, 그리고 한계는 무엇인지 답해보세요.
힌트

[감점 답변] '트리쉐이킹은 자동임'. [좋은 답변] esbuild/Vite/Rollup 은 최상위 키 만 export 로 노출해 트리쉐이킹한다 — import { version } from './package.json'. 깊게 중첩된 데이터는 여전히 통째로 번들된다. 깊은 데이터 일부만 필요하면 빌드 타임 플러그인 으로 변환하는 게 가장 깔끔하다.

자기 점검

정적 import 의 실패 폭발 반경(blast radius)을 동적 import 와 비교해 한 단락으로 설명해보세요.
module graphblast radiusfallbacktry/catch
자주 하는 오해

동적 import() 도 동기 import 와 똑같이 모든 실패에 취약하다는 오해. 동적은 promise rejection 으로 격리 된다.

JSON 모듈 import 의 캐싱 의미를 한 문장으로 정의하세요 — *언제 좋고 언제 누수가 되는지*.
module cachelifetimespecifierGC
자주 하는 오해

'캐싱은 무조건 빠르다' 라는 단순화. 데이터 수명 ≠ 페이지 수명일 때 그 캐싱이 누수가 된다.

프런트엔드 코드에서 `package.json` 을 import 해 버전 한 줄만 쓰는 패턴의 문제점을 설명해보세요.
tree-shaking최상위 키번들 사이즈build-time
자주 하는 오해

번들러가 다 알아서 트리쉐이킹한다는 가정. 깊은 키는 못 자른다 — 빌드 플러그인으로 한 줄만 뽑아내는 게 정답이다.