회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
해외여행을 가면 종종 전압이 달라 맞는 어댑터를 구비하곤 합니다. 110V 콘센트에 220V 코드를 꽂을 수 없는 것처럼 호환성은 중요한 문제입니다. 이를 웹 개발에 비유하면 크롬, 엣지, 파이어폭스 등의 브라우저는 모두 다른 콘센트를 가지고 있습니다. 프론트엔드 개발자는 이러한 브라우저의 차이를 인지하고, 웹페이지가 원활하게 기능을 제공할 수 있도록 준비해야 합니다. 현재 전 세계적으로 많은 사용자들이 크롬을 이용하고 있지만, 유럽에서는 파이어폭스의 점유율이 10%에 달하기도 합니다.
웹 호환성은 어떤 기기나 브라우저에서든 원활한 서비스를 제공하겠다는 원칙과도 같습니다. 각각의 브라우저는 CSS 속성, 자바스크립트 문법, 브라우저 API에 따라 조금씩 차이를 보이는데요. 이번 글에서는 구버전 브라우저에서도 실행할 수 있는 자바스크립트 웹 호환성 지원 방법을 살펴보겠습니다.
인터넷 익스플로러는 왜 최신 스크립트 문법을 지원하지 않을까요? 같은 자바스크립트 코드도 왜 크롬에서는 빠른 반면에 사파리에서는 다소 늦는 걸까요? 그 이유는 인터넷 익스플로러, 크롬, 사파리가 서로 다른 소프트웨어고, 서로 다른 소스 코드 위에서 동작하기 때문입니다. 예를 들어, 인터넷 익스플로러의 자바스크립트 문법 해석 코드에는 화살표 함수 해석 로직이 없습니다. HTML의 id를 사용해 엘리먼트를 찾는 document.getElementById 함수는 크롬과 사파리에서 같은 결과를 보여주지만, 소스 코드는 분명 다릅니다.
사용자에게 접근 권한을 요구하는 Permission API는 크롬 브라우저에서 다양한 인터페이스를 제공하는 반면, 파이어폭스와 사파리에서는 미지원 API라고 생각해도 될 만큼 부실한 인터페이스를 제공합니다. 크롬, 사파리처럼 기업이 만들어 사용자의 편의와 기업의 이익을 모두 충족하는 기능의 브라우저도 있고, 전 세계 개발자들이 오픈소스 프로젝트로 만든 브라우저도 있습니다.
브라우저 역시 사람이 만드는 소프트웨어라서 사용자의 니즈(needs), 개인정보 접근에 대한 관점 차이 등 만드는 사람과 단체의 의도를 반영하게 됩니다. 순수하게 기술로 보면 버그, 알고리즘의 차이로 호환성 문제가 발생하기도 합니다.
ES6, ESnext, ECMAScript는 프론트엔드 개발자라면, 알고 싶지 않아도 자연스럽게 알게 되는 용어입니다. 특히 오래된 자바스크립트 코드를 최신 문법으로 리팩토링해 보신 분들이라면 더 잘 알고 있을 것입니다.
ECMAScript는 스크립트 언어가 준수해야 하는 규칙, 세부 사항, 지침을 말합니다. 그리고 자바스크립트는 ECMAScript를 준수하는 언어입니다. 2015년 이후로 ECMAScript는 매년 개발자들의 니즈를 반영해 직관적인 문법, 기능들을 새로운 버전에 포함해 발표하고 있습니다. 그에 맞춰 자바스크립트를 해석하고 실행하는 브라우저는 새 버전의 ECMAScript 지원 업데이트를 반영합니다. 따라서 구버전 브라우저 사용자가 최신 ECMAScript 스펙으로 작성한 웹페이지에 접속하면 전체 또는 일부 기능에 제한을 받습니다.
개발자라면 팬시(fancy)하고 효율적인 최신 문법, 기능을 굳이 멀리하지는 않을 것입니다. 세줄짜리 코드를 한 줄로 줄일 수도 있으니까요. 하지만 그로 인해 구버전 브라우저 사용자가 고장 난 웹페이지에 접속하는 것을 무시할 순 없습니다. 웹 호환성에서 감초 역할을 하는 바벨(babel)이 등장한 것도 바로 이 때문입니다.
바벨은 ES6 이상의 ECMAScript 스펙을 구버전 브라우저에서도 해석할 수 있도록 변환하는 도구입니다. webpack 설정 파일을 작성해 봤다면 이미 낯이 익을 것입니다.
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
그리고 위 설정을 적용하기 위해 아래와 같이 이름부터 낯선 패키지도 설치했을 겁니다.
> npm install -D @babel/core @babel/preset-env babel-loader
@babel/core는 구문 변환의 핵심 로직을, @babel/preset-env는 바벨 설정 중 유용한 것들을 모아 저장해 둔 프리셋입니다. 세 번째 패키지 babel-loader는 webpack이 바벨을 사용할 수 있도록 도와주는 도구이므로 따로 설명하지 않겠습니다. 그럼 @babel/core와 @babel/preset-env를 활용해 직접 최신 ECMAScript 구문을 변환해보겠습니다. 이번 글에서는 필요한 패키지를 직접 설치 후, CLI 환경에서 변환하겠지만 babeljs.io/repl에 접속하여 간단히 테스트해봐도 좋습니다.
# 프로젝트 디렉터리 생성
> mkdir babel-practice && cd babel-practice
# 패키지 설치
> npm install -D @babel/core @babel/preset-env @babel/cli
먼저 프로젝트 디렉터리를 만든 후 패키지를 설치합니다.
{
"presets": ["@babel/preset-env"]
}
프로젝트 루트 경로에 바벨 설정 파일 babel.config.json을 생성합니다. 생성한 파일에 위와 같이 설정을 정의합니다.
// package.json
{
"name": ...,
...
"browserslist": "defaults"
}
그리고 package.json 을 열어 브라우저리스트 옵션을 추가합니다.
브라우저리스트 옵션은 어떤 브라우저를 어느 버전까지 거를지 결정하는 쿼리입니다. 예를 들어, 쿼리가 ‘last 2 chrome versions’이면 최신 2개의 크롬 버전을 뜻합니다. 이와 관련해 깃허브에 쿼리 작성 방법이 자세하게 소개되어 있으니 참고해 보시기 바랍니다.
이제 바벨 설치와 설정을 마쳤으니 변환할 자바스크립트 코드를 작성해보겠습니다. 이번에 테스트할 코드는 ES2022에 새로 추가된 클래스 private 속성 구문입니다. 이전까지는 클래스 private 속성을 흉내 내기 위해 다양한 방법을 시도했지만, ES2022에 private 속성 구문을 정식 지원하면서 간편하게 사용할 수 있게 됐습니다.
class Person {
name; // public 속성
#age; // private 속성
}
test.js 파일을 생성한 뒤 클래스 private 속성 구문을 포함하는 코드를 작성합니다.
> npx 바벨test.js -o test.result.js
작성한 코드를 바벨로 변환합니다.
// test.result.js
function _classPrivateFieldInitSpec...
function _checkPrivateRedeclaration...
...
var _age = /*#__PURE__*/new WeakMap();
class Person {
constructor() {
_defineProperty(this, "name", void 0);
_classPrivateFieldInitSpec(this, _age, {
writable: true,
value: void 0
});
}
}
변환 결과 파일 test.result.js 를 열어봅니다.
단 4줄에 불과했던 코드가 20줄 남짓의 코드로 바뀌었습니다. 역으로 생각하면 구버전 브라우저에서도 동작하는 클래스 private 속성을 지원하기 위해, 20줄의 수고를 들여야 하는데 이를 덜어준 것입니다. webpack과 babel-loade로 번들링한 파일 역시 같은 과정을 거쳐, 구버전 브라우저에서 동작할 수 있는 것입니다.
여기서 한 가지 재밌는 테스트를 추가해보겠습니다. 올해 6월 마이크로소프트에서 더 이상 IE를 지원하지 않는다는 소식을 전했는데요. 그럼에도 각기 다른 사정으로 IE를 지원해야 하는 회사도 아직 있을 것 같습니다. 이런 경우 바벨이 든든한 지원군이 되어줍니다.
만약 클래스 private 속성 구문이 IE 최신 버전에서도 동작하려면 설정 하나를 수정해야 합니다. @babel/preset-env는 내부에서 브라우저리스트를 사용하기 때문에, 변환할 코드가 어느 브라우저까지 지원할 것인지 쉽게 지정할 수 있습니다.
{
"name": ...,
...
"browserslist": "defaults, IE 11"
}
package.json을 열고 위와 같이 수정하겠습니다.
> npx 바벨test.js -o test.result_ie11.js
새롭게 설정한 브라우저리스트 옵션으로 다시 코드를 변환하겠습니다. 이전 결과와 차이를 살펴보기 위해 결과 파일 이름은 test.result_ie11.js 로 지정합니다.
// test.result_ie11.js
function _typeof...
function _defineProperties...
...
var _age = /*#__PURE__*/new WeakMap();
var Person = /*#__PURE__*/_createClass(function Person() {
_classCallCheck(this, Person);
_defineProperty(this, "name", void 0);
_classPrivateFieldInitSpec(this, _age, {
writable: true,
value: void 0
});
});
test.result_ie11.js를 열어보면 이전과 다른 결과를 보여줍니다. 심지어 이번에는 class
구문조차 function
으로 변환됐습니다.
> ls -lh
-rw-r--r-- 1 ... staff 1.3K Dec 18 15:09 test.result.js
-rw-r--r-- 1 ... staff 2.4K Dec 18 15:08 test.result_ie11.js
이때 두 파일의 크기는 거의 두 배 가까이 차이 납니다.
더 오래된 브라우저를 지원할수록 결과 파일이 커질 수 있습니다. 4줄짜리 코드가 2.4K로 변환됐다면, 프로젝트 규모의 코드는 그보다 더 급진적으로 커질 것입니다. 마이크로소프트가 IE를 더 이상 지원하지 않는 현재, 합리적인 브라우저 지원 범위는 과연 어디까지일까요?
browserslist.dev에 접속하면 브라우저리스트 쿼리로 어떤 브라우저를, 어느 버전까지 거를 수 있는지 알 수 있습니다. 예를 들어, 브라우저 사용자 중 10% 이상이 이용 중인 것만 지원한다고 가정하고 ‘>= 10%’를 입력하면 오직 크롬만이 해당합니다. 크롬이 가장 큰 점유율을 차지하는 시점에서 5% 이상은 웹 호환성과 거리가 멀어 보입니다. 개인적인 의견으로는 0.25% 이상이 적당할 것 같습니다. 0.25% 은 아직 IE 11을 포함하지만, 앞으로 IE 사용자가 줄어들 것을 감안하면 나쁘지 않아 보입니다.
구버전 브라우저를 지원하는 데 있어 바벨이 만병통치약은 아닙니다. 특히 블루투스, 화상 카메라 등 모던 브라우저에 추가된 다채로운 API를 구버전 브라우저에 지원하는 것은 구문 변환과는 무관한 부분입니다. 브라우저 내장 API는 구버전 지원도 문제지만, 브라우저 간 스펙 차이도 있습니다. 예를 들어, 앞서 소개한 Permission API는 크롬에서 다채로운 옵션을 제공하는 것과 달리 다른 브라우저에서는 빈약합니다.
Array.prototype.find = function (finder) {
for (let i=0; i < this.length; i++) {
if (finder(this[i])) {
return this[i];
}
}
}
이때 폴리필은 브라우저, 버전 간 API의 차이를 없애거나 줄일 수 있는 방법입니다. 특정 브라우저에 내장되지 않은 API를 직접 작성해 몽키패칭 하거나, 폴리필 패키지를 다운로드할 수 있습니다. 예를 들어, 배열에서 조건을 만족하는 요소를 찾는 메서드 find
의 폴리필을 작성한다면 위와 같을 것입니다.
중복 없이 값을 저장하는 Set
자료구조는 아직도 IE에서 일부 메서드를 사용할 수 없습니다. IE 점유율을 무시할 수 없던 몇 년 전까지 프론트엔드 개발자들은 Set
의 온전한 사용을 보장할 수 있도록 폴리필을 추가해야만 했습니다.
const element = document.getElementById('...');
element.scrollIntoView();
이번 절에서는 화면의 특정 엘리먼트로 스크롤을 옮기는 scrollIntoView
함수와 그 옵션 smooth
를 예시로 폴리필을 적용해보겠습니다. scrollIntoView
는 목표 엘리먼트가 보일 수 있도록 페이지 스크롤을 이동하는 함수입니다. 아래와 같이 엘리먼트의 인스턴스 메서드로서 실행하는 간단한 함수입니다.
element.scrollIntoView({ behavior: 'smooth' });
scrollIntoView
는 인자 없이 실행할 수도 있지만, 스크롤 이동 방법에 관한 옵션을 인자로 전달할 수도 있습니다. 가령 위와 같이 { behavior: 'smooth' }
인자를 전달하면 목표 엘리먼트까지 부드럽게 이동합니다.
화면을 급격하게 전환하지 않아 이동 시간만 길지 않다면 사용자 경험에 좋은 옵션입니다. 그러나 사파리 브라우저는 이 옵션을 지원하지 않습니다. 그러니 어느 브라우저에서든 같은 사용자 경험을 주고 싶다면 폴리필을 사용해야 합니다.
<script src="<https://unpkg.com/smoothscroll-polyfill@0.4.4/dist/smoothscroll.min.js>"></script>
폴리필은 직접 작성해도 좋지만, 조금만 검색하면 여러 예외 처리까지 고려한 훌륭한 코드를 찾을 수 있습니다. 이번엔 깃허브를 활용해 보겠습니다. 대부분의 폴리필 패키지는 사용법이 함께 있으니 쉽게 적용할 수 있습니다. smoothscroll는 패키지를 직접 설치하거나, 빌드 파일을 직접 가져다 쓰도록 안내하고 있습니다. 방법은 폴리필마다 다르므로 안내 문서를 따라 하면 됩니다. 간단하게 <script>
태그를 적용해 보겠습니다.
그럼 위와 같이 폴리필이 적용된 모습을 확인할 수 있습니다.
이처럼 웹 호환성은 프론트엔드 개발자에게 잊지 말아야 할 마지막 보루와도 같습니다. 웹서비스를 만드는 기획, 디자인, 개발 과정 중에 놓쳤던 호환성 이슈는 없는지 항상 점검하고, 웹을 이용하는 다수의 사용자 뿐만 아니라 소수의 사용자도 함께 만족시킬 수 있도록 노력해야 할 것입니다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.