1. 브라우저 호환성이 중요한 이유
프론트엔드 코드는 사용자의 브라우저에서 실행된다.
그리고 모든 사용자의 브라우저 종류와 버전, 업데이트 상태는 각기 다르다.
그렇다보니 개발자의 의도와 다르게,사용자 브라우저 환경에 따라 서비스가 작동하지 않을 수 있다.
예를 들어 다음 코드를 살펴보자:
const lastUserName = users.at(-1)?.name ?? "Unknown";
최신 브라우저에서는 자연스러운 코드지만, 지원 범위를 넓혀 보면 두 가지 문제가 있다.
?.와??는 브라우저가 문법 자체를 이해하지 못할 수 있다.Array.prototype.at()은 문법적으로 해석되더라도 브라우저에 해당 메서드가 없을 수 있다.
전자는 코드가 실행되기 전에 문제를 일으키고, 후자는 실행 중에 오류를 일으킨다.
어느 쪽이든 서비스 진입이 어렵거나 특정 기능을 사용할 수 없게 된다.
즉 브라우저 호환성은 사용자가 서비스를 안정적으로 이용할 수 있는지에 관한 문제다.
1.1. 브라우저마다 지원 범위가 다른 이유
브라우저별 JavaScript 지원 범위가 다른 이유 중 하나는
브라우저가 사용하는 JavaScript 엔진과 엔진 버전이 다르기 때문이다.
| 브라우저 | JavaScript 엔진 |
|---|---|
| Chrome, Chromium 기반 Edge | V8 |
| Safari | JavaScriptCore |
| Firefox | SpiderMonkey |
새로운 JavaScript 기능이 표준으로 채택되더라도 모든 엔진이 동일 시점에 지원하는 건 아니다.
DOM이나 Web API의 지원 범위는 JavaScript 엔진뿐 아니라 브라우저와 운영체제의 구현 차이에도 영향을 받는다.
특히 오래된 단말기나 WebView는 브라우저 업데이트가 늦거나 중단될 수 있다.
1.2. 호환성 오류가 발생하는 두 시점
호환성 문제는 같은 시점에 발생하는 게 아니다.
브라우저는 JavaScript를 먼저 파싱한 뒤 실행하므로, 호환성 오류도 두 시점으로 나뉜다.
JavaScript 코드 로드
↓
문법 파싱
↓
코드 실행
(1) 파싱 단계에서의 오류
파싱은 브라우저가 코드의 문법 구조를 분석하는 과정이다.
user?.name;
Optional Chaining을 지원하지 않는 브라우저는 ?. 문법 자체를 이해하지 못한다.
그래서 실행 단계에 도달하기도 전에 SyntaxError 문법 오류가 발생한다.
SyntaxError: Unexpected token .
(2) 실행 단계에서의 오류
앞선 예시와 달리 다음 코드는 메서드 호출 형태라, 브라우저가 문법적으로 해석할 수 있다.
items.at(-1);
하지만 파서(parser)는 at 메서드의 실제 존재 여부까지 확인하지 않는다.
따라서 실행 시 브라우저에 Array.prototype.at이 없다면 items.at은 undefined로 평가되고,
이를 함수처럼 호출하는 순간 오류가 발생한다.
TypeError: items.at is not a function
두 오류를 정리하면 다음과 같다.
| 코드 | 문제 | 실패 시점 | 필요한 대응 |
|---|---|---|---|
user?.name |
브라우저가 문법을 이해하지 못함 | 파싱 단계 | 지원하는 문법으로 변환 |
items.at(-1) |
브라우저에 메서드가 없음 | 실행 단계 | Polyfill 추가 또는 다른 구현으로 대체 |
이 구분이 중요한 이유는 실패 시점에 따라 해결 방법이 달라지기 때문이다.
- 브라우저가 문법을 이해하지 못하면 -> 코드 형태를 미리 변환해야 하고,
- 문법은 이해하지만 API가 없다면 -> 그 기능을 추가하거나 다른 방식으로 대체해야 한다.
1.3. 브라우저 호환성의 세 가지 유형
파싱/실행 단계를 기준으로 JavaScript 호환성은 문법 호환성과 API 호환성으로 구분할 수 있다.
추가로 bundle과 chunk를 불러오는 방식도 고려해야 하기에, 브라우저 호환성은 세 가지로 나눌 수 있다.
- 문법 호환성
- API 호환성
- 모듈 및 로딩 호환성
(1) 문법 호환성
문법 호환성은 브라우저가 JavaScript 코드의 표현 방식을 이해할 수 있는지에 관한 문제다.
화살표 함수, Optional Chaining, Nullish Coalescing 등이 여기에 해당한다.
const name = user?.name;
지원 브라우저가 이 문법을 이해하지 못한다면 Babel과 같은 도구로 이해가능한 문법으로 변환해야 한다.
변환 결과는 다음과 같다. (실제 산출물은 Babel 버전과 설정에 따라 달라질 수 있음)
var name = user === null || user === undefined
? undefined
: user.name;
(2) API 호환성
API 호환성은 브라우저가 코드에서 호출하는 함수, 객체, 메서드를 실제로 제공하는지에 관한 문제다.
items.at(-1);
Promise.resolve();
Object.entries(data);
이 코드들은 문법적으로 해석되더라도, 해당 API를 지원하지 않는 브라우저에서는 실행 중 오류가 발생한다.
이 경우 Polyfill을 추가해 누락된 기능을 보완할 수 있다.
// 동작 원리를 단순화한 의사 코드이며,
// 실제 ECMAScript 명세를 완전히 구현한 Polyfill은 아님
if (!Array.prototype.at) {
Array.prototype.at = function (index) {
var target = index < 0 ? this.length + index : index;
return this[target];
};
}
| 구분 | 문법 변환 | Polyfill |
|---|---|---|
| 해결 대상 | 브라우저가 읽지 못하는 문법 | 브라우저에 없는 API |
| 예 | ?., ??, 화살표 함수 |
Promise, Object.entries, Array.prototype.at |
| 처리 방식 | 코드의 표현 형태를 변경 | 필요한 함수·객체·메서드를 추가 |
(3) 모듈 및 로딩 호환성
애플리케이션은 일반적으로 여러 모듈로 구성된다.
import { getUsers } from "./api.js";
그래서 번들러는 모듈의 의존 관계를 분석해 번들과 청크를 만들고, 이를 불러오기 위한 런타임 코드도 생성한다.
모듈 관계 분석
↓
의존성 그래프 생성
↓
번들과 청크 생성
↓
런타임에서 필요한 파일 로드
따라서 애플리케이션 코드의 문법을 변환했다고 호환성이 보장되는 것은 아니다.
번들러가 생성한 런타임 코드의 문법과 동적 청크 로딩 방식도 모두 지원 브라우저에서 동작해야 한다.
결국 하나만 해결해서는 서비스 전체의 호환성을 보장할 수 없다.
1.4. 그럼 어떻게 서비스의 호환성을 지킬 수 있을까?
앞서 브라우저 호환성을 문법, API, 모듈 및 로딩의 세 가지 유형으로 나누어 살펴봤다.
각 문제는 파싱, 실행, 파일 로딩처럼 서로 다른 단계에서 발생하므로,
한 가지 문제를 해결했다고 최종 산출물의 호환성이 보장되는 것은 아니다.
빌드 성공도 마찬가지다.
빌드가 성공했다는 것은 빌드 도구가 원본 코드로 산출물을 생성했다는 의미일 뿐,
모든 지원 브라우저가 해당 산출물을 파싱하고 실행할 수 있다는 의미는 아니다.
따라서 브라우저 호환성 관리의 목표는 단순히 빌드를 통과하는 코드를 만드는 것이 아니다.
개발자는 최신 JavaScript 문법과 API를 사용하되,
최종 빌드 산출물은 서비스가 지원하는 브라우저에서 정상적으로 실행될 수 있어야 한다!
이 목표를 달성하려면 먼저 지원 범위를 명확하게 정의하고,
각 도구별 역할을 인식해야한다.
Browserslist → 지원할 브라우저 범위 정의
ESLint → 원본 코드 검사
Babel → 지원하지 않는 JavaScript 문법 변환
Polyfill → 브라우저에 없는 API 보완
webpack → 모듈 분석과 bundle·chunk 생성
CI 및 브라우저 테스트 → 설정과 최종 산출물 검증
Babel, Polyfill Provider, webpack target은 Browserslist를 공통 지원 기준으로 사용할 수 있지만,
각 도구가 맡는 책임은 다르다.
Browserslist는 지원 대상을 정의할 뿐 코드를 직접 변환하지 않으며,Babel로 문법을 변환해도 브라우저에 없는 API까지 모두 보완되는 것은 아니다.webpack역시 모듈을 결합하고 산출물을 생성하지만, 그 자체만으로 사용자 코드의 모든 호환성 문제를 해결하지는 않는다.ESLint로 브라우저 호환성까지 검사하려면 별도 규칙이나 플러그인이 필요하다.
따라서 지원 범위 정의, 원본 코드 검사, 문법 변환, API 보완, 모듈 로딩, 최종 검증을 하나의 관리 흐름으로 구성해야 한다.
다음 문단에서 webpack 프로젝트를 기준으로 Browserslist, ESLint, Babel, Polyfill이 각각 어떤 역할을 담당하며,
webpack 빌드 과정에서 어떻게 연결되는지 살펴보자!
2. 브라우저 호환성을 관리하는 도구(with. webpack)
webpack 프로젝트에서 브라우저 호환성을 관리하려면 각 도구의 역할을 구분해야 한다. (코드 예시)
webpack이 모든 호환성 작업을 직접 처리하는 것은 아니다.
webpack은 모듈을 탐색하고 전체 빌드 흐름을 관리하며,
코드의 문법 변환은 babel-loader를 통해 Babel에 위임한다.
브라우저에 없는 JavaScript 표준 API는 Babel Polyfill Provider와 core-js를 통해 보완한다.
2.1. 도구별 역할
| 도구·설정 | 역할 |
|---|---|
| Browserslist | 지원할 브라우저 범위 정의 |
| ESLint | 원본 코드의 오류와 규칙 위반 검사 |
| webpack | 모듈 탐색과 전체 빌드 흐름 관리 |
| babel-loader | webpack이 발견한 JavaScript 파일을 Babel에 전달 |
| Babel | 지원 브라우저에 맞게 최신 JavaScript 문법 변환 |
| Babel Polyfill Provider | 필요한 Polyfill을 판단하고 import 추가 |
| core-js | JavaScript 표준 객체와 메서드 구현 제공 |
webpack target |
특정 환경에 맞는 webpack 런타임 코드를 생성하도록 설정 (단, 사용자 코드 문법은 변환하지 않음) |
Browserslist는 Babel과 webpack 등이 참고하는 지원 환경 기준이다.
ESLint는 일반적으로 webpack 빌드와 별도로 실행되는 정적 검사 단계다.
webpack 빌드는 다음과 같은 흐름으로 이해할 수 있다.
webpack이 Entry에서 모듈 탐색 시작
↓
babel-loader가 JavaScript 파일을 Babel에 전달
↓
Babel이 문법을 변환
↓
Polyfill 플러그인이 필요한 core-js import 추가
↓
webpack이 코드와 Polyfill을 번들링
↓
target에 맞는 webpack 런타임 생성
(1) Browserslist: 지원 환경 정의
Browserslist는 최종 산출물이 지원해야 하는 브라우저 범위를 정의한다.
[production]
> 0.5%
not dead
[development]
last 1 Chrome version
Browserslist는 위와 같은 쿼리를 실제 브라우저와 버전 목록으로 변환한다.
계산된 결과는 여러 도구가 공통 기준으로 사용할 수 있다.
Browserslist
├─ Babel
│ └─ 어떤 문법을 변환할지 판단
│
├─ Polyfill Provider
│ └─ 어떤 표준 API를 보완할지 판단
│
└─ webpack target
└─ 런타임에서 사용할 수 있는 기능 판단
Browserslist는 지원 환경을 정의할 뿐, 코드를 직접 검사하거나 변환하지 않는다.
따라서 Babel, Polyfill Provider, webpack에 서로 다른 브라우저 범위를 중복으로 설정하기보다,Browserslist를 공통 기준으로 사용해야 한다.
그래야 문법 변환, API 보완, webpack 런타임이 동일한 지원 정책을 기준으로 동작한다.
Browserslist
→ 지원 환경 정의
Babel·Polyfill Provider·webpack
→ 동일한 지원 환경을 참고하여 실제 처리
각 도구에서 별도의 targets를 지정하지 않고 동일한 Browserslist 환경을 참조하면,Browserslist 설정을 변경해 지원 범위를 함께 조정할 수 있다.
(2) ESLint: 원본 코드 검사
ESLint는 브라우저에서 실행할 산출물을 만드는 도구가 아니라,
개발자가 작성한 원본 코드에서 문제를 미리 찾는 정적 분석 도구다.
원본 코드
↓
문법 파싱 및 AST 생성
↓
ESLint 규칙 실행
↓
오류와 경고 출력
최신 문법을 Babel로 변환할 예정이라면 ESLint도 해당 원본 문법을 해석할 수 있어야 한다.
export default [
{
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
},
];
ecmaVersion: "latest"는 ESLint가 Optional Chaining, Nullish Coalescing 등의 최신 문법을 파싱할 수 있도록 한다.
다만 이 설정은 ESLint가 원본 코드를 읽을 수 있게 하는 것일 뿐, 해당 문법을 지원 브라우저가 직접 실행할 수 있다는 의미는 아니다.
ESLint ecmaVersion
→ ESLint가 해석할 원본 코드의 문법 범위
Browserslist
→ 최종 산출물이 지원해야 하는 브라우저 범위
ESLint는 기본적으로 코드 품질과 규칙 위반을 검사한다.
지원 브라우저에 없는 API 사용까지 확인하려면, Browserslist 설정을 참조하는 별도의 호환성 규칙이나
플러그인을 추가해야 한다.
(3) babel-loader와 Babel: 최신 문법 변환
webpack의 module.rules는 test, include, exclude 조건으로 어떤 모듈에 Loader를 적용할지 정한다.babel-loader는 이 조건과 일치한 모듈에서 Babel을 실행해 webpack과 Babel을 연결한다.
webpack module.rules
→ 어떤 모듈에 어떤 Loader를 적용할지 결정
babel-loader
→ webpack에서 Babel 변환 실행
Babel
→ 실제 JavaScript 문법 변환
Babel의 @babel/preset-env는 Browserslist에서 계산한 지원 대상을 참고해 필요한 변환 플러그인을 선택한다.
지원 브라우저가 이해하는 문법은 유지하고, 지원하지 않는 문법만 호환 가능한 형태로 변환한다.
const name = user?.name ?? "Unknown";
지원 대상이 Optional Chaining과 Nullish Coalescing을 지원하지 않는다면, 다음과 같은 형태로 변환된다.
var _user = user;
var _name = _user == null ? undefined : _user.name;
var name = _name == null ? "Unknown" : _name;
이 과정은 단순한 문자열 치환이 아니다.Babel은 원본 코드를 AST로 변환한 뒤, 지원 브라우저에 필요한 문법 변환을 적용하고 다시 JavaScript 코드를 생성한다.
원본 JavaScript
↓
AST 생성
↓
Browserslist 지원 대상 확인
↓
필요한 문법 변환
↓
JavaScript 코드 재생성
↓
변환 결과를 webpack에 반환
webpack은 Babel이 반환한 코드를 다른 모듈과 함께 최종 bundle에 포함한다.
(4) Babel Polyfill Provider와 core-js: 표준 API 보완
Babel은 JavaScript 문법을 변환하지만, 브라우저에 존재하지 않는 표준 객체나 메서드까지 직접 구현하지는 않는다.
const name = user?.name;
const lastUser = users.at(-1);
?.는 최신 JavaScript 문법이므로 Babel이 기존 문법으로 변환할 수 있다.
반면 users.at(-1)은 기존의 객체.메서드() 문법을 사용한다.
브라우저가 코드 구조를 이해하더라도 Array.prototype.at이 존재하지 않으면 실행 중 오류가 발생한다.
TypeError: users.at is not a function
이처럼 브라우저에 없는 JavaScript 표준 객체나 메서드를 보완하는 코드를 Polyfill이라고 한다.
Polyfill을 직접 구현할 수도 있지만, 일반적으로는 표준 기능별 구현을 제공하는 라이브러리를 사용한다.
그중 core-js는 ECMAScript 표준 라이브러리와 일부 웹 표준 기능에 대한 다양한 Polyfill을 제공한다.
필요한 Polyfill 판단·import 추가 → Babel Polyfill Provider
실제 표준 API 구현 제공 → core-js
추가된 Polyfill 모듈을 bundle에 포함 → webpack
(5) webpack target: webpack 런타임의 실행 환경
webpack은 애플리케이션 모듈을 결합하면서 bundle 실행에 필요한 자체 런타임 코드도 생성한다.
이 런타임은 모듈 실행과 캐시, 동적 import(), 비동기 chunk 로딩 등의 작업을 담당한다.target은 webpack이 생성하는 런타임 코드와 chunk 로딩 방식을 어떤 실행 환경에 맞출 것인지 지정하는 설정이다.
module.exports = {
target: "browserslist",
};
target: "browserslist"를 설정하면 webpack은 Browserslist 설정을 참고해
런타임 코드에서 사용할 수 있는 ECMAScript 기능과 실행 환경을 판단한다. (webpack)
개발자가 작성한 JavaScript → Babel이 Browserslist를 기준으로 변환
webpack이 생성한 런타임 코드 → webpack target이 Browserslist를 기준으로 생성
target은 사용자 코드를 변환하지 않는다.
따라서 Optional Chaining과 같은 최신 문법은 Babel을 통해 변환해야 한다! (webpack)
만약 Browserslist가 없고, target만 설정했다면?
module.exports = {
target: ["web", "es2020"],
};
이 설정은 webpack이 브라우저에서 ES2020 기능을 사용할 수 있다고 가정한다.
그러나 개발자가 작성한 사용자 코드까지 ES2020 기준으로 변환하는 것은 아니다.
babel-loader는 webpack의 target 정보를 Babel에 전달하지만,@babel/preset-env가 이를 자동으로 자신의 targets로 사용하지는 않는다.
webpack target → webpack 런타임과 로딩 코드의 생성 기준
Babel preset-env targets → 사용자 코드의 문법 변환 기준
따라서 Browserslist가 없다면, Babel의 targets도 별도로 설정해야 두 코드의 지원 범위를 맞출 수 있다.
만약 Browserslist와 target이 다르다면?
Babel은 Browserslist를 기준으로 사용자 코드를 변환하더라도,
webpack의 target이 다른 환경을 가리키면 webpack 런타임은 다른 기준으로 생성된다.
module.exports = {
target: ["web", "es2020"],
};
이 설정은 브라우저 환경에서 ES2020 기능을 사용할 수 있다고 webpack에 알린다.
Browserslist → 오래된 브라우저까지 지원
webpack target → web 플랫폼의 ES2020 환경을 가정
이처럼 target > Browserslist 환경 이라면, 사용자 코드는 정상적으로 변환되었더라도
webpack 런타임에 오래된 브라우저가 지원하지 않는 기능이 포함될 수 있다.
반대로 target< Browserslist 환경이라면,
런타임 코드가 필요 이상으로 보수적으로 생성될 수 있다.
따라서 애플리케이션에서는 Babel과 webpack 런타임이 동일한 지원 범위를 사용하도록target: "browserslist"로 맞추는 것이 안전하다.
Browserslist
→ 사용자 코드와 Polyfill의 지원 기준
webpack target: "browserslist"
→ webpack 런타임도 같은 기준 적용
2.2. 도구별 역할 정리
앞에서 살펴본 도구의 책임을 질문 형태로 정리하면 다음과 같다.
| 도구·설정 | 해결하는 질문 |
|---|---|
| Browserslist | 어떤 브라우저를 지원할 것인가? |
| ESLint | 원본 코드에 문제가 없는가? |
| webpack | 어떤 모듈이 필요하며, 이를 어떻게 묶을 것인가? |
| babel-loader | webpack에서 Babel 변환을 어떻게 실행할 것인가? |
| Babel | 지원 브라우저를 위해 어떤 최신 문법을 변환할 것인가? |
| Babel Polyfill Provider | 어떤 Polyfill import가 필요한가? |
| core-js | 브라우저에 없는 ECMAScript 표준 API를 어떻게 보완할 것인가? |
webpack target |
webpack 런타임을 어떤 환경에 맞출 것인가? |
관계를 개념적으로 표현하면 다음과 같다.

위 그림은 도구별 역할을 보여 주기 위해 단순화한 것이다.
실제 webpack은 Entry에서 시작해 발견한 의존 모듈을 반복해 처리하며 의존성 그래프를 만든다.
Browserslist는 코드를 직접 처리하는 단계가 아니다.
- Babel, Polyfill Provider, webpack
target이 호환성 수준을 판단할 때 참고할 수 있는 공통 지원 기준이다. - ESLint 역시 일반적인 webpack 빌드 흐름 안에서 코드를 변환하는 도구가 아니라, 원본 코드를 별도로 검사하는 도구다.
webpack은 babel-loader를 통해 Babel 변환을 빌드 과정에 포함한다.
- Babel 내부의 Polyfill Provider가 필요한
core-jsimport를 추가하면, - webpack은 이를 다른 모듈과 함께 의존성 그래프에 포함해 최종 bundle과 chunk를 생성한다.
3. 마치며...
브라우저 호환성은 단순히 Babel을 설치하거나 webpack의 target을 변경하는 것으로 완성되지 않는다.
- 먼저 Browserslist로 지원 환경을 명확하게 정의해야 한다.
- 이후 Babel이 해당 환경에 맞게 사용자 코드의 문법을 변환하고,
- Babel Polyfill Provider와
core-js가 누락된 JavaScript 표준 API를 보완한다.
webpack은 babel-loader를 통해 Babel 변환을 빌드에 포함하고,- Babel이 추가한 Polyfill import를 다른 모듈과 함께 최종 bundle과 chunk로 조립한다.
- webpack
target은 webpack이 자체적으로 생성하는 런타임 코드를 지원 환경에 맞춘다.

각 도구는 서로 연결되어 있지만 책임은 다르다.
이 책임을 구분하면 호환성 문제가 발생했을 때 원인도 명확하게 추적할 수 있다.

- 브라우저가 코드를 파싱하지 못한다면 Babel 변환 범위를 확인한다.
- 특정 함수가 없다는 실행 오류가 발생한다면 Polyfill 범위를 확인한다.
- 외부 모듈이나 chunk를 불러오지 못한다면 webpack 번들 및 런타임 설정을 확인한다.
- 특정 Web API만 동작하지 않는다면 core-js가 아닌 별도 Polyfill이 필요한지 확인한다.
결국 브라우저 호환성을 지킨다는 것은 모든 코드를 가장 오래된 문법으로 바꾸는 일이 아니다.
지원할 환경을 먼저 정의하고,
그 환경에 필요한 문법 변환과 API 보완만 적용한 뒤,
실제 브라우저에서 최종 산출물을 검증하는 과정이다.
'개발 기술 > 개발 이야기' 카테고리의 다른 글
| 컴포넌트 프리뷰는 어떻게 실행될까? iframe으로 만들어본 프리뷰 런타임 (0) | 2026.05.05 |
|---|---|
| SVG 아이콘 시스템 설계: Runtime에서 Build Time으로 전환하기 (2) | 2026.03.31 |
| [TS × 클린 아키텍처] 2편 — 타입스크립트 한계와 Mapper: AST로 타입 검증하기 (0) | 2025.10.12 |
| [TS × 클린 아키텍처] 1편 — 타입스크립트 한계와 Mapper: 스키마로 런타임 검증하기 (0) | 2025.09.28 |
| Tailwind 없이, PostCSS+PurgeCSS로 유틸리티 클래스 구축하기 (0) | 2025.05.26 |
댓글