정의
옛 브라우저에서 앱을 돌리고 싶을 때 해야 하는 일은 둘이며, 서로 다르다. 하나는 새 문법(예: a?.b, a ?? b)을 옛 브라우저도 읽는 옛 문법으로 기계가 바꿔주는 트랜스파일(transpile). 다른 하나는 옛 브라우저에 아예 없는 함수/API(예: structuredClone(), Array.prototype.at)를 JavaScript로 재구현해 채워 넣는 폴리필(polyfill).
핵심은 이것이다: 트랜스파일러는 문법만 바꾼다. 없는 런타임 함수는 만들어 주지 않는다. 그래서 문법을 다 낮췄어도, 없는 함수를 호출하는 순간 TypeError: ... is not a function이 난다.
왜 필요한가
옛 브라우저에서 앱이 "이유 없이 통째로 죽는" 버그의 흔한 뿌리가 이 둘을 혼동하는 것이다. 팀이 @babel/preset-env로 문법만 낮추고 "이제 옛 브라우저 지원 끝났다"고 여겼는데, 정작 코드가 structuredClone()을 부르는 순간 죽는 식이다. 문법은 완벽히 통과했지만 그 함수 자체가 옛 브라우저엔 없기 때문이다.
두 문제를 구분해 두면 진단이 빨라진다.
- 문법(syntax) 문제: 옛 파서가 새 문법을 못 읽어 파일 전체가
SyntaxError로 죽는다. 첫 오류 이후 그 스크립트는 한 줄도 실행되지 않는다. - 런타임 API 문제: 문법은 통과하지만 없는 함수를 호출하는 시점에
TypeError가 난다.
동작 원리
- 지원 하한(예: 특정 옛 버전)을 한 곳에 선언한다 — 보통
browserslist. 트랜스파일 타깃과 폴리필 타깃이 이 값을 공유해야 어긋나지 않는다. - 문법 층:
@babel/preset-env에 타깃을 주면 그 브라우저가 못 읽는 문법만 골라 다운레벨한다. 서드파티(node_modules)까지 변환 경로에 태워야 라이브러리의?.도 낮춰진다. - API 층:
core-js-compat이 타깃 목록을 입력받아 "그 브라우저에 부족한 표준 API 목록"을 계산한다. 그 목록으로 폴리필 번들을 만든다(제안 단계esnext.*API는 보통 제외). - 회귀 방지: 레포가 실제로 쓰는 API가 폴리필 엔트리에 포함됐는지 테스트로 강제한다. 손으로 관리하면 새로 쓴 API가 빠진다.
// 문법: babel이 옛 문법으로 낮춤 (아래는 예시)
const name = user?.profile?.name ?? "guest";
// 런타임 API: babel은 이걸 만들어 주지 않는다 → 폴리필 필요
const copy = structuredClone(data);
const last = list.at(-1);
const upper = text.replaceAll("a", "b");
실무 적용
- 콘솔 첫 에러가
SyntaxError면 트랜스파일 설정(타깃·node_modules 포함)을 본다. TypeError: X is not a function이면 그 API가 폴리필 번들에 있는지 본다.- 폴리필 소스는 사람이 아니라
core-js-compat이 타깃에서 도출하게 한다. - 지원 하한을 바꿀 때
browserslist와 폴리필 빌드 타깃을 함께 수정한다.
트레이드오프
| 선택 | 장점 | 비용 |
|---|---|---|
| 트랜스파일만 | 설정 단순 | 없는 API 미해결 → 런타임 사망 |
| 트랜스파일 + 폴리필 | 문법·API 모두 커버 | 번들 크기·빌드 복잡도 증가 |
| 폴리필 손수 관리 | 도구 불필요 | 목록 드리프트, 새 API 누락 |
사용하면 안 되는 경우
- 이미 최신만 지원하기로 한 프로젝트에 무거운 폴리필을 상시 포함하는 경우 — 불필요한 비용.
- node_modules를 트랜스파일 대상에서 무조건 전부 포함시켜 빌드가 느려지는 경우 — 문제되는 패키지만 태운다.
흔한 실수
- "babel 넣었으니 옛 브라우저 지원 끝"이라 여기고 폴리필을 빠뜨리기.
- 앱 코드만 다운레벨하고 서드파티 라이브러리의 새 문법을 남겨 두기.
- 트랜스파일 타깃과 폴리필 타깃을 각각 다른 값으로 두어 커버 범위가 어긋나기.
관련 개념
- differential-polyfill-loading — 폴리필을 옛 브라우저에만 조건부로 로드하기
- hydration-failure-dead-handlers — 번들이 죽으면 하이드레이션이 불발돼 핸들러가 통째로 죽는다
- browser-compat-interaction-smoke — 문법·API 회귀를 실제로 잡는 스모크 테스트