왜 아직도 IE/Edge 레거시와 Safari를 신경 써야 할까
IE 11과 Edge Legacy(Chromium 이전 EdgeHTML 기반 Edge)의 공식 지원은 종료되었지만, 여전히 레거시 환경을 사용하는 기업 인트라넷, 정부 시스템, 제조 현장의 전용 단말기는 많습니다. 더불어 iOS Safari와 macOS Safari는 최신 표준 반영 속도, 일부 독자적 구현, 그리고 iOS의 터치/뷰포트 특성 때문에 “잘 만든” 사이트도 예상치 못한 버그에 부딪히기 쉽습니다.
이 글은 다음을 목표로 합니다:
- IE/Edge 레거시를 안전하게 지원하는 전략과 실전 패턴
- Safari(특히 iOS) 특정 버그의 원인과 회피/대응 방법
- 빌드 설정, 폴리필, CSS/JS 코딩 방식, 테스트/배포 체크리스트
핵심은 “모든 브라우저를 동일하게 보이게” 만드는 것이 아니라, “필수 기능은 모든 브라우저에서 동작하고, 부가 기능은 점진적으로 향상되는” 경험을 설계하는 것입니다.
전략의 뼈대: 지원 범위 정의와 점진적 향상
1) 지원 매트릭스부터 확정하자
- 필수 지원: IE 11, Edge Legacy(12–15: -ms-grid, 16+: modern grid), iOS Safari 12+, macOS Safari 12+
- 권장: 최신 Chromium/Firefox/Edge(Chromium) 최신 2 버전
- 데이터 근거: 애널리틱스(UA, OS), 고객사 요구, SLA. 지원 범위를 문서화하고 릴리즈마다 재검토합니다.
2) 점진적 향상(Progressive Enhancement) vs 우아한 강등(Graceful Degradation)
- 점진적 향상: 기본 기능은 HTML/CSS/ES5로 보장하고, 고급 기능은 지원 브라우저만 활성화합니다.
- 우아한 강등: 최신 기능을 먼저 만들고, 핵심 경로에서만 레거시 대안을 제공합니다.
- 권장: 핵심 비즈니스 여정은 반드시 점진적 향상으로 설계합니다.
3) UA 스니핑 대신 기능 탐지
- CSS: @supports 또는 CSS.supports
- JS: ‘in’ 연산자, typeof, try/catch 기반 탐지
- Modernizr나 수동 기능 탐지 유틸을 사용해 브라우저별 분기 최소화
빌드 파이프라인: Browserslist, Babel/TypeScript, PostCSS
Browserslist로 타깃 일원화
프로젝트 루트에 명시합니다. 도구(Babel, Autoprefixer, PostCSS)가 같은 대상 정의를 공유하도록 합니다.
# .browserslistrc
[production]
ie 11
edge 15
last 2 chrome versions
last 2 firefox versions
ios_saf >= 12
safari >= 12
[development]
last 1 chrome version
last 1 firefox version
- caniuse-lite를 정기 업데이트하세요: npx update-browserslist-db@latest
Babel 설정: ES5 타깃과 폴리필 자동 주입
/* babel.config.js */
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 실제 사용된 기능에 한해 폴리필 주입
corejs: { version: 3, proposals: false },
bugfixes: true,
modules: false
}]
],
plugins: [
['@babel/plugin-transform-runtime', { helpers: true, regenerator: true }]
]
};
- IE 11을 위해 async/await → regenerator, Promise 등 폴리필 필요
- 선택적 체이닝(?.), nullish 병합(??) 등은 트랜스파일됨
TypeScript 타깃/라이브러리
/* tsconfig.json */
{
"compilerOptions": {
"target": "ES5",
"lib": ["ES5", "ES2015", "DOM"],
"downlevelIteration": true,
"module": "ESNext",
"moduleResolution": "Node",
"isolatedModules": true,
"noEmitOnError": true,
"skipLibCheck": true
}
}
- DOM 라이브러리 포함, 다운레벨 이터레이션 활성화(IE에서 for..of 등 변환 안정화)
PostCSS + Autoprefixer
/* postcss.config.js */
module.exports = {
plugins: [
require('postcss-preset-env')({
stage: 3,
features: {
'custom-properties': false // IE는 CSS 변수 미지원, 빌드타임 대체 권장
},
autoprefixer: { grid: true } // -ms-grid 변환 활성화
})
]
};
- grid: true로 IE/Edge Legacy용 -ms-grid 자동 변환
- CSS 변수는 런타임 미지원(IE), 빌드타임 변환 또는 폴리필 사용
HTML/CSS 모범 사례: IE/Edge 레거시 대응
표준 모드와 문서 헤더
- 최상단에
- IE 호환성 보기 방지 메타
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
- iOS 뷰포트
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
벤더 프리픽스와 기능 쿼리
- Autoprefixer로 프리픽스 문제를 자동화
- 기능 감지(@supports)로 고급 기능을 조건부 적용
/* CSS 변수 미지원 브라우저 대응 */
:root { --brand: #0a84ff; }
.button { background: #0a84ff; /* fallback */
background: var(--brand); }
/* 스티키 지원 브라우저만 적용 */
@supports (position: sticky) {
.sticky { position: sticky; top: 0; }
}
Flexbox/그리드 호환 패턴
- IE 11 Flexbox 버그: flex: 1가 오버플로우를 만들거나 축소되지 않을 수 있음
- 해결: flex 항목에 min-width: 0; min-height: 0 지정
- IE 11의 auto/min-content 계산 문제와 overflow 사이드 이펙트
- 해결: 필요한 컨테이너에 overflow: hidden 또는 min-height: 0
.flex-container { display: flex; }
.flex-item {
flex: 1 1 auto;
min-width: 0; /* IE 11 내용 넘침 방지 */
min-height: 0;
}
- CSS Grid: IE/Edge Legacy(<16)는 -ms-grid(초기 사양)만 지원
- Autoprefixer grid 변환을 켜고, 간단한 레이아웃은 -ms-grid로 함께 유지
/* 현대 그리드 */
.grid {
display: grid;
grid-template-columns: 1fr 2fr;
grid-gap: 16px;
}
/* Autoprefixer가 아래와 유사하게 변환(-ms-grid) */
.grid {
display: -ms-grid;
-ms-grid-columns: 1fr 16px 2fr; /* gap 보완 */
}
- position: sticky는 IE/Edge Legacy에서 미지원 → Stickyfill 같은 폴리필 사용
/* JS에서 조건 적용 */
if (!CSS || !CSS.supports || !CSS.supports('position', 'sticky')) {
// Stickyfill.init(document.querySelectorAll('.sticky'));
}
이미지/폰트 포맷
- IE 11: WOFF2 미지원 → WOFF 폴백 제공
- Safari 구버전/IE: WebP 미지원 → picture로 폴백
<picture>
<source srcset="img.avif" type="image/avif">
<source srcset="img.webp" type="image/webp">
<img src="img.jpg" alt="">
</picture>
object-fit, filter 등 미지원 기능
- IE: object-fit 미지원 → object-fit-images 폴리필 또는 background-image 활용
- filter/backdrop-filter: Safari는 성능/지원 이슈 존재 → blur 등은 성능 프로파일링, 폴백 색상/투명도 마련
JavaScript 모범 사례: 안전한 API 표면 만들기
필수 폴리필 체크리스트(IE/Edge 레거시)
- ES 기능: Promise, Symbol, Map/Set, WeakMap, Array.from/includes/find, String.startsWith/endsWith, Object.assign, URL, URLSearchParams
- DOM/웹 API: fetch(또는 XHR 대체), CustomEvent, classList(SVG), Element.closest, NodeList.forEach, IntersectionObserver, requestAnimationFrame(IE 9–)
Babel + core-js ‘usage’ 설정으로 대부분 자동 주입되지만, fetch/IntersectionObserver 등 일부는 수동/별도 폴리필이 필요할 수 있습니다.
fetch 안전 래퍼(IE는 XHR 폴백)
function httpGet(url, options = {}) {
if (window.fetch) {
return fetch(url, options);
}
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
resolve({ ok: xhr.status >= 200 && xhr.status < 300, status: xhr.status, text: () => Promise.resolve(xhr.responseText) });
};
xhr.onerror = reject;
xhr.send();
});
}
CustomEvent, addEventListener 옵션 호환
- IE 11: new CustomEvent 미지원 → 폴리필
- addEventListener 옵션 객체(passive, capture 등) 미지원 브라우저 처리
/* CustomEvent 폴리필 */
(function () {
if (typeof window.CustomEvent === "function") return;
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: null };
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
})();
/* passive 지원 탐지 */
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() { supportsPassive = true; }
});
window.addEventListener('test', null, opts);
} catch (e) {}
const wheelOpts = supportsPassive ? { passive: true } : false;
window.addEventListener('wheel', onWheel, wheelOpts);
NodeList.forEach, classList(SVG), closest
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = Array.prototype.forEach;
}
if (!('closest' in Element.prototype)) {
// closest 폴리필 로드
}
모듈/노모듈 번들 전략
최신 브라우저는 type="module", 레거시는 nomodule 번들을 받게 구성합니다. 번들 크기/폴리필 범위를 최적화할 수 있습니다.
<script type="module" src="/assets/app.modern.js"></script>
<script nomodule src="/assets/app.legacy.js"></script>
- modern.js: 최소 폴리필, ES2017+
- legacy.js: Babel ES5 변환 + core-js + regenerator
Safari 특정 버그와 해결책
Safari는 표준 지원은 빠르지만 iOS 터치/뷰포트와 렌더링에서 고유한 제약이 있습니다. 다음은 자주 만나는 이슈와 대응입니다.
1) iOS 100vh/주소창 수축 문제
문제: iOS Safari에서 100vh가 실제 가시 영역과 다릅니다. 풀스크린 레이아웃에 빈 공간이 생기거나 스크롤이 튑니다.
해결:
- height: 100dvh(iOS 16+) 또는 -webkit-fill-available 사용
- CSS 환경 변수를 활용해 안전 영역 고려
- JS로 실제 뷰포트 높이 계산해 CSS 변수에 주입
/* CSS */
.full {
min-height: 100dvh; /* 최신 */
min-height: -webkit-fill-available; /* 구버전 대안 */
padding-bottom: env(safe-area-inset-bottom);
}
/* JS fallback */
function setVh() {
document.documentElement.style.setProperty('--vh', window.innerHeight * 0.01 + 'px');
}
window.addEventListener('resize', setVh);
setVh();
/* CSS: height: calc(var(--vh, 1vh) * 100); */
2) position: sticky가 동작 안 함
원인:
- Safari에서 부모에 overflow: hidden/auto/scroll이 있으면 sticky가 무력화
- transform, perspective 등이 조상에 선언되어도 문제
해결:
- overflow를 제거하거나 sticky 컨테이너를 더 바깥으로 이동
- transform을 분리하거나 sticky 요소에 will-change: transform 대신 기본 흐름 유지
3) iOS에서 background-attachment: fixed 미지원
패럴랙스 효과가 깨집니다.
해결:
- JS 스크롤 리스너 + translateY로 패럴랙스 구현
- 또는 단순히 fixed 대신 transform 기반 스크롤 효과
4) 인풋 포커스 시 확대(줌) 이슈
iOS Safari는 font-size < 16px 인 입력 필드를 탭하면 자동 확대합니다.
해결:
- 입력 폰트를 16px 이상으로 지정
- 디자인상 작은 텍스트 필요 시 transform: scale로 축소된 느낌만 제공
input, select, textarea {
font-size: 16px;
}
5) 스크롤 컨테이너와 position: fixed
- -webkit-overflow-scrolling: touch가 적용된 내부 스크롤 영역에서 fixed 요소가 튀거나 겹침
해결:
- fixed 요소를 스크롤 컨테이너 밖(루트)에 위치
- 필요 시 포탈/레이어 시스템으로 루트에 렌더링(모달/툴팁)
6) Flexbox 최소 크기 계산 버그
Safari에서 flex 아이템이 축소되지 않거나 overflow 발생
해결:
- 부모/자식에 min-height: 0, min-width: 0 명시
- flex-basis: 0 사용으로 균등 분배 강제
.flex {
display: flex;
}
.flex > * {
min-width: 0;
min-height: 0;
}
7) Blob 다운로드/파일 저장
iOS Safari는 a[download]와 Blob URL 다운로드가 제한적입니다.
해결:
- 서버 측 Content-Disposition: attachment 헤더로 직접 다운로드 유도
- 모바일에서는 새 탭으로 Blob 뷰어 열기 또는 서버 변환 링크 제공
8) localStorage 예외(프라이빗 모드)
구버전 iOS Safari 프라이빗 모드에서 localStorage 접근 시 예외가 발생
해결:
function safeStorage() {
try {
const testKey = '__t__';
localStorage.setItem(testKey, '1');
localStorage.removeItem(testKey);
return localStorage;
} catch (e) {
// 메모리 기반 대체
const mem = {};
return {
getItem: k => mem[k] || null,
setItem: (k, v) => mem[k] = String(v),
removeItem: k => delete mem[k]
};
}
}
const storage = safeStorage();
9) passive 이벤트 리스너와 스크롤 성능
Safari는 passive 기본 동작이 브라우저별 상이했던 시기가 있습니다. 위의 supportsPassive 탐지 로직을 사용해 성능을 챙기세요.
10) backdrop-filter 성능
blur, saturation 필터는 Safari에서 가능하지만 성능 비용 큼(모바일에서 프레임 드랍). 폴백 배경색과 투명도를 제공하고, 모달/네비처럼 제한된 면적에만 사용하세요.
IE/Edge 레거시 전용 트러블슈팅
문서 모드/호환성 보기
- 인트라넷 환경은 “호환성 보기” 강제 정책이 있을 수 있음
- 반드시 X-UA-Compatible, 표준 doctype 명시, Enterprise Mode가 필요한 경우 IT와 협업해 최적 모드 지정
SVG와 classList
- IE 11은 SVGElement.classList가 완전하지 않음 → className.baseVal 사용 또는 폴리필
function svgAddClass(el, cls) {
if ('classList' in el) return el.classList.add(cls);
el.setAttribute('class', (el.getAttribute('class') || '') + ' ' + cls);
}
이벤트/포인터
- IE 10–11: MSPointer 이벤트 과거 명칭(-ms-)
- Edge Legacy/현대 브라우저: Pointer Events
- 크로스 브라우저 입력 처리를 위한 전략:
- pointerdown/PointerEvent 지원 탐지 후 fallback으로 mousedown/touchstart
const onDown = 'onpointerdown' in window ? 'pointerdown' : ('ontouchstart' in window ? 'touchstart' : 'mousedown');
window.addEventListener(onDown, handleDown);
폼/placeholder 스타일
- IE에서 placeholder 스타일링은 ::-ms-input-placeholder 사용 필요(Autoprefixer 커버 범위 확인)
CSS calc 및 서브픽셀 이슈
- IE 11의 calc() + flex 조합에서 반올림 문제
- 해결: 픽셀 스냅(정수 px), 레이아웃 단순화, 가능한 곳에서 gap 대신 마진
Polyfill.io/선택적 폴리필 로딩
- 사용자 UA에 맞춰 필요한 폴리필만 제공
<script src="https://polyfill.io/v3/polyfill.min.js?features=default,fetch,URL,URLSearchParams"></script>
실전 예시: 레거시 그리드 폴백 제공하기
목표: 2열 레이아웃을 최신 브라우저에서는 CSS Grid, IE/Edge Legacy에서는 -ms-grid로.
.container {
display: grid;
grid-template-columns: 1fr 320px;
gap: 24px;
}
@supports (-ms-ime-align:auto) {
/* IE/Edge Legacy 전용 감지(해킹) */
.container {
display: -ms-grid;
-ms-grid-columns: 1fr 24px 320px;
}
.main { -ms-grid-column: 1; }
.aside { -ms-grid-column: 3; }
}
- 권장: 위와 같은 해킹 대신 Autoprefixer grid 변환 사용
- grid-auto-flow/areas 등 복잡한 기능은 -ms-grid로 자동 변환이 어려울 수 있으므로 단순화
이미지/미디어: 포맷과 성능 고려
- AVIF/WebP: 최신 브라우저 우선, Safari/IE 폴백 제공
- 비디오: H.264 컨테이너(mp4) 제공, 자동재생(iOS) 조건(무음 playsinline) 충족
- 아이콘: SVG inline 사용(IE 외부
접근성/상호작용: 브라우저별 포커스/터치
- :focus-visible은 IE/Safari 구버전 미지원 → focus-visible 폴리필 또는 focus 스타일 일관화
- 터치 하이라이트 제거(iOS)
a, button { -webkit-tap-highlight-color: rgba(0,0,0,0); }
- 터치 제스처 충돌 방지
/* IE 10+, Edge Legacy */
.element { -ms-touch-action: pan-y; touch-action: pan-y; }
/* 최신 브라우저는 touch-action만으로 충분 */
테스트/디버깅/배포
테스트 툴체인
- 클라우드 디바이스 농장: BrowserStack, Sauce Labs
- 자동화: Playwright, WebDriver, Cypress(IE 미지원이므로 E2E는 Edge Legacy/IE는 수동 또는 WebDriver + Selenium Grid)
- 유닛 테스트: Jest + jsdom(렌더링 차이는 실제 브라우저로 회귀 테스트 필요)
시각 회귀 테스트
- Percy, Chromatic, Loki 등을 활용해 주요 UI 스냅샷 비교
- Safari(iOS), IE/Edge Legacy에서 핵심 페이지 템플릿만이라도 스냅샷 유지
성능/오류 모니터링
- RUM: 브라우저/OS, Core Web Vitals, JS 에러 스택(소스맵 업로드)
- 오류 수집 시 브라우저별 분포를 대시보드화 → 호환성 이슈 빠른 감지
배포 체크리스트
- 빌드 산출물: modern/legacy 번들 분리(module/nomodule)
- 폴리필: Polyfill.io 또는 core-js 포함 여부 확인
- CSS: Autoprefixer grid 옵션, CSS 변수/최신 기능의 빌드타임 변환
- 폰트/이미지 폴백(woff, jpg)
- 서비스워커/오프라인: IE/Edge Legacy 미지원 대비(기능 감지로 옵트아웃)
실용 레시피 모음
1) CSS 변수 폴백 자동화(PostCSS)
- postcss-custom-properties로 런타임 변수를 정적 값으로 치환
- 테마가 필요한 경우: IE 전용 CSS 번들을 별도로 생성해 테마 스위칭을 제한적으로 지원
2) IntersectionObserver 폴리필 로딩
async function ensureIO() {
if (!('IntersectionObserver' in window)) {
await import('intersection-observer'); // 또는 정적 스크립트 로드
}
}
ensureIO().then(initLazyLoad);
3) 이미지 지연 로딩 Safari/IE
- loading="lazy"는 IE/Edge Legacy 미지원
- fallback: IntersectionObserver 기반 또는 scroll/resize 스로틀
4) 애니메이션/트랜지션
- will-change 남발 금지(모바일 Safari에서 메모리/합성기 과부하)
- transform/opacity 위주로, filter/box-shadow 과도 사용 제한
흔한 함정과 피하기
- UA 스니핑으로 Safari/IE를 구분해 분기 → 유지보수 악몽. 기능 탐지로 대체.
- 공통 CSS를 복잡한 해킹으로 뒤덮기 → 빌드 단계에서 처리(PostCSS/Autoprefixer).
- 한 번에 모든 기능을 “같게” 만들려는 집착 → 핵심 사용자 여정부터 안정화하고, 부가 기능은 “있으면 좋음” 수준으로 단계적 개선.
- 폴리필 과적재 → module/nomodule, Polyfill.io의 UA 기반 서빙으로 최소화.
- 인프라 무시 → 인트라넷 프록시/캐시/구형 OS(Windows 7 IE 11)에서의 인증/보안 설정 차이 고려.
간단 체크리스트
- 지원 매트릭스 문서화했는가? 애널리틱스 근거가 있는가?
- Browserslist/Babel/PostCSS 설정이 일관적인가?
- module/nomodule로 현대/레거시 번들을 분리했는가?
- 핵심 기능의 폴리필이 포함되어 있는가? (Promise, fetch/XHR, URL, CustomEvent 등)
- CSS Grid/Flexbox에 대해 IE/Edge Legacy 폴백을 제공했는가?
- iOS Safari의 100vh, sticky, fixed, input zoom 이슈를 검증했는가?
- 이미지/폰트 포맷 폴백을 제공했는가?
- 브라우저별 자동/수동 테스트 루틴이 있는가? (BrowserStack 등)
- 성능/오류 모니터링으로 호환성 이슈를 조기에 포착하는가?
마무리: “가능하면 최신, 필요하면 레거시”
IE/Edge 레거시 지원은 비용이 듭니다. 하지만 올바른 전략(점진적 향상), 자동화된 도구(Browserslist, Babel, PostCSS), 신중한 기능 탐지, 현명한 폴리필 로딩을 결합하면 유지보수 가능한 수준으로 관리할 수 있습니다. Safari 특유의 버그는 원인을 이해하고 레이아웃 구조를 조정하거나 적절한 폴백을 두는 것으로 대부분 해결됩니다.
핵심 요점:
- 표준을 기본값으로, 레거시에는 폴백을 “필요 최소한”으로
- 테스트와 관찰(모니터링)로 실제 사용자의 문제를 빠르게 확인
- 복잡도는 빌드 타임으로 밀어내고, 런타임 로직은 단순하게
이 가이드의 베스트 프랙티스를 적용하면, 브라우저 호환성 문제로 인한 일정 지연과 장애를 크게 줄이고, 사용자에게 안정된 경험을 제공할 수 있습니다.