크로스 브라우징 개념 이해하기 👀

돌멩이·2024년 7월 8일
2
post-thumbnail

크로스 브라우징(Cross Browsing)이란? 🤔

웹 페이지가 다양한 브라우저에서 동일하게 보여지도록 개발 하는 것을 의미한다.
브라우저마다 기본 CSS 스타일이 다르기 때문에 크로스 브라우징을 적용하지 않으면 웹 사이트의 화면이 브라우저마다 다르게 보인다. 이는 사용자 경험을 저하시킬 수 있다.

동일한 웹 페이지가 다르게 보이는 예시 (Chrome / Safari)


이러한 문제를 해결하기 위해, 개발자는 크로스 브라우징 기술을 사용하여 가능한 많은 브라우저에서 일관된 사용자 경험을 제공해야 한다!




CSS 크로스 브라우징

브라우저마다 다르게 보이는 기본 CSS 스타일을 어떻게 통일시킬까?

1. CSS Reset

말 그대로 CSS를 리셋함을써 모든 브라우저에서 기본으로 적용되는 스타일을 초기화한다.
https://meyerweb.com/eric/tools/css/reset/
https://html5doctor.com/html-5-reset-stylesheet/
위의 사이트에 접속하여 CSS Reset 코드를 복사할 수 있다.

html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
    margin:0;
    padding:0;
    border:0;
    outline:0;
    font-size:100%;
    vertical-align:baseline;
    background:transparent;
}

reset CSS의 일부 코드로 다양한 태그들의 CSS 속성을 변경하는 것을 확인할 수 있다.
body 태그는 default로 8px의 margin을 가진다고 한다. 이렇게 브라우저가 기본적으로 정해놓은 CSS는 개발 중에 예상치 못한 디자인 오류를 발생시킬 수 있다.
또한 브라우저마다 default 값이 다를 수 있으므로 그 과정에서 생기는 차이를 없애고자 reset CSS를 적용한다. 일반적으로 CSS 파일의 최상단에 reset CSS 코드를 작성한다.


2. Vendor Prefix

CSS 코드 중에 -webkit-transform: rotate(45deg); 처럼 앞에 특이한 접두사가 붙은 것이 Vendor Prefix이다.
Vendor Prefix는 간단하게 말하자면 표준이 아닌 CSS 스타일을 특정 브라우저에서 동작하게 만든다.
최신 CSS 기능을 표준화 이전의 상태로 구현한 구형 브라우저에서도 해당 기능이 정상적으로 작동하도록 보장한다. 예시를 보자.

위는 Can I Use 라는 웹 기술들의 브라우저 호환성을 알려주는 사이트에서 backdrop-filter 기술을 검색한 예시이다.

Safari 9 버전부터 17.4 버전까지는 해당 기술이 -webkit- prefix를 통해 지원된다.


Safari 18의 Technology Preview 버전에서는 prefix 없이 사용할 수 있다.


Vendor Prefix 종류

각 브라우저에 대한 Vendor Prefix의 종류는 다음과 같다.
-webkit- Chrome, Safari, 신 버전의 Opera와 Edge, 대부분의 iOS 브라우저에 쓰인다. 기본적으로 모든 WebKit 또는 Chromium 기반 브라우저를 위해 사용한다.
-moz- Firefox 브라우저
-o- Opera의 옛버전 브라우저
-ms- Internet Explorer와 Chromium 이전의 Microsoft Edge 브라우저

사용 예시

.button {
    border: 10px solid; /* 기본 보더를 설정 */
    border-image: -webkit-linear-gradient(red, yellow) 1; /* 크롬과 사파리 */
    border-image: -moz-linear-gradient(red, yellow) 1; /* 파이어폭스 */
    border-image: -ms-linear-gradient(red, yellow) 1; /* 익스플로러 */
    border-image: -o-linear-gradient(red, yellow) 1; /* 오페라 */
    border-image: linear-gradient(red, yellow) 1; /* CSS 표준 문법 코드 */
}

Vendor Prefix를 CSS에 일일이 적용해야 할까?

Autoprefixer를 이용하여 자동으로 Vendor Prefix를 붙일 수 있다. 😄

공식 사이트에서 바꾸고자 하는 css 코드를 왼쪽에 붙여넣으면 자동으로 변환된다.
또는 패키지 매니저를 통해 autoprefixer를 프로젝트에 적용할 수 있다.




JavaScript 크로스 브라우징

Js에서도 크로스 브라우징이 발생해? 😮

모든 브라우저가 동일한 Js 기능을 동일한 방식으로 지원하지 않는다. 특히, 구형 브라우저에서는 최신 JavaScript 기능을 지원하지 않을 수 있다.

예시
모든 브라우저가 동일한 Js 기능을 동일한 방식으로 지원하지 않는다.
➡️ 각 브라우저는 고유한 렌더링 엔진을 사용하기에, 이로 인해 DOM 조작이나 이벤트 처리 방식에 차이가 발생할 수 있다. IE의 attachEvent vs. 다른 브라우저의 addEventListener

구형 브라우저에서는 최신 JavaScript 기능을 지원하지 않을 수 있다.
➡️ fetch API는 최신 브라우저에서 지원되지만, 구형 브라우저에서는 지원 ❌
➡️ let과 const 키워드는 ES6 이후에 도입되어 구형 브라우저에서는 지원 ❌

Modernizr를 이용해 브라우저가 특정 HTML5 및 CSS3 기능을 지원하는지 확인할 수 있다. 또는 Babel, Polyfill과 같은 트랜스파일러를 사용해 코드를 자동으로 변환함으로써 해결한다. 해당 글에서는 후자만 서술한다.



Babel

Babel은 최신 JavaScript 코드를 구형 브라우저에서도 호환되도록 ES5 또는 그 이하 버전의 코드로 변환해주는 트랜스파일러이다.

// 최신 JavaScript 코드 (ES6+)
const greet = () => {
  console.log("Hello, world!");
};

// Babel을 통해 트랜스파일된 코드 (ES5)
var greet = function() {
  console.log("Hello, world!");
};

Arrow function과 같이 구형 브라우저에서 지원하지 않는 자바스크립트 문법을 자동으로 변환해준다.
➡️ 최신 문법으로 작성한 코드도 구형 브라우저에서 문제 없이 동작한다.


Polyfill

최신 JavaScript API와 기능을 구형 브라우저에서도 사용할 수 있도록 추가한다.
Promise, fetch API 등은 최신 브라우저에서만 지원할 수 있다.

➡️ 최신 브라우저 뿐만 구형 브라우저에서도 기능을 사용할 수 있다.


Babel과 Polyfill 적용해보기

Babel 적용

실습을 위해 간단한 바닐라Js 프로젝트를 만들었다.

시작 폴더 구조

📦vanilla-js-project
 ┣ 📂src
 ┃ ┗ 📜script.js
 ┣ 📜index.html

아래와 같이 간단한 script.js 파일을 만들었다.

const greet = (name) => {
  return `Hello, ${name}!`;
};

const name = "World";
console.log(greet(name));

패키지 매니저를 통해 Babel package를 프로젝트에 설치한다.

(선택) 1. package.json 생성

npm init -y

2. Babel 설치

npm install --save-dev @babel/core @babel/cli @babel/preset-env

@babel/core - Babel 플러그인과 프리셋을 사용하여 JavaScript 코드를 변환한다.

@babel/cli - Babel을 터미널에서 상호작용할 수 있게 하는 CLI(Command Line Interface) 도구.

@babel/preset-env - Babel 프리셋. 플러그인과 프리셋을 통해 변환될 js 문법을 정한다.

Babel 프리셋(Preset)과 플러그인(Plugin), 무슨 차이일까?

플러그인은 특정 JavaScript 문법을 변환한다.
예를 들어 Arrow function을 일반 함수로 변환하고 싶다면, .babelrc 파일의 plugins@babel/plugin-transform-arrow-functions 플러그인을 적으면 된다.

또는 letconstvar 로 변환하고 싶다면, @babel/plugin-transform-block-scoping 플러그인을 사용하면 된다.


프리셋은 Babel 플러그인을 모아둔 것이다.
필요한 여러 플러그인을 한 번에 적용할 수 있도록 해준다. 가장 많이 사용되는 프리셋 중 하나는 @babel/preset-env 이다. 타켓 환경을 작성하면, 필요한 Babel 플러그인들을 자동으로 구성해준다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        }
      }
    ]
  ]
}

targets를 통해 변환된 코드가 지원해야 하는 브라우저와 버전을 지정함으로써, 불필요한 폴리필이나 트랜스파일링을 줄일 수 있다. @babel/preset-env에 대한 자세한 내용은 여기를 참고하자.


3. 루트 폴더에 .babelrc 생성 후 프리셋 지정

{
  "presets": ["@babel/preset-env"]
}

(선택) 4. package.json 파일에 scripts 추가

"build": "babel src --out-dir dist" 를 추가한다.
터미널에 npm run build 를 입력하면 해당 스크립트가 실행된다. babel cli를 이용하여 src 폴더 내의 JavaScript 파일들을 변환하여 dist 폴더에 출력한다.


5. 터미널에 npm run build 입력

터미널에 Successfully compiled 1 file with Babel (189ms). 와 같은 결과가 출력되면 변환이 끝난 것이다.


6. 변환 결과 확인

변환 전 (src 폴더의 script.js)

const greet = (name) => {
  return `Hello, ${name}!`;
};

const name = "World";
console.log(greet(name));

변환 후 (dist 폴더의 script.js)

"use strict";

var greet = function greet(name) {
  return "Hello, ".concat(name, "!");
};
var name = "World";
console.log(greet(name));

dist 폴더의 script.js 파일을 확인하면 최신 js 문법이 구형 브라우저에서 동작하도록 변환된 것을 확인할 수 있다.

상단에 "use strict"; 는 무슨 의미일까?
js 코드에서 엄격 모드(strict mode)를 활성화하는 지시어이다. 일반 모드에서는 문법 오류가 발생하지 않는 코드도, 해당 모드에서는 오류로 취급하여 명시적으로 오류가 발생했음을 알려준다.
선언되지 않은 변수 사용, 함수 매개변수에서 중복된 이름 사용 등 오류를 쉽게 찾을 수 있도록 도와준다.



Polyfill 적용

babel은 완료했으니 다음으로 Polyfill를 적용해보자.
폴리필은 앞서 설명했던 것처럼 최신의 자바스크립트 API와 기능이 구형 브라우저에서도 동작하도록 하는 도구이다.

1. src 폴더의 script.js 파일에 Promise API 추가

const greet = (name) => {
  return `Hello, ${name}!`;
};

const name = "World";
console.log(greet(name));

// Promise 예제
const asyncOperation = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;
      if (success) {
        resolve("Operation succeeded");
      } else {
        reject("Operation failed");
      }
    }, 1000);
  });
};

asyncOperation()
  .then((message) => {
    console.log(message);
  })
  .catch((error) => {
    console.error(error);
  });


2. core-js 다운로드

core-js를 이용하여 폴리필을 할 수 있다.
npm install core-js 으로 프로젝트에 core-js 를 다운받는다.


3. .babelrc 업데이트

core-js만 별개로 사용하여 폴리필을 진행할 수 있지만 그런 경우 변환하고자 하는 기능(e.g. Promise)에 대해 필요한 폴리필을 직접 임포트해야 한다.
core-js와 babel을 함께 이용하면 손쉬운 폴리필이 가능하므로 .babelrc 을 업데이트하여 폴리필을 진행한다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

@babel/preset-env 이 폴리필을 처리하도록 변경한다.
"useBuiltIns" - 3가지 옵션(entry, usage, false)이 있다.

  • entry : 코드의 진입점(변환하고자 있는 코드가 있는 파일)에 core-js/stableregenerator-runtime/runtime을 직접 임포트하여 폴리필 전체를 추가한다.

  • usage : 폴리필을 직접 추가할 필요가 없다. 실제 사용한 폴리필이 변환 과정에서 자동으로 삽입된다. 필요한 폴리필만 추가하여 번들의 크기가 작다는 장점이 있다.

  • false : 폴리필을 자동으로 추가하지 않는다. 이 경우 import "core-js/features/promise";처럼 필요한 폴리필을 직접 추가해야 한다.

"corejs" - Babel이 사용할 core-js의 특정 버전을 지정한다. version 3이 메이저 버전이다.


useBuiltIns 의 3가지 옵션 중 usage 옵션이 좋아보이는데 entry 옵션을 사용해야 하는 경우는 언제일까❓
동적 임포트가 필요한 경우가 있을 수 있겠다. usage 옵션은 정적 분석을 통해 코드에서 사용된 기능을 감지하여 폴리필을 추가한다. 동적 임포트는 런타임에 모듈이 로드되기 때문에 정적 분석으로는 감지할 수 없다.

추가로, ECMAScript의 기능이 아닌 Fetch 함수와 같은 Web API를 폴리필하고 싶다면 whatwg-fetch와 같은 폴리필 라이브러리를 추가로 설치해야 한다.


4. 터미널에 npm run build 입력

앞서 "build": "babel src --out-dir dist" 스크립트를 추가했다.


5. 결과 확인

변환 전 (src 폴더의 script.js)

const greet = (name) => {
  return `Hello, ${name}!`;
};

const name = "World";
console.log(greet(name));

// Promise 예제
const asyncOperation = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;
      if (success) {
        resolve("Operation succeeded");
      } else {
        reject("Operation failed");
      }
    }, 1000);
  });
};

asyncOperation()
  .then((message) => {
    console.log(message);
  })
  .catch((error) => {
    console.error(error);
  });

변환 후 (dist 폴더의 script.js)

"use strict";

require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/web.timers.js");
var greet = function greet(name) {
  return "Hello, ".concat(name, "!");
};
var name = "World";
console.log(greet(name));

// Promise 예제
var asyncOperation = function asyncOperation() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      var success = true;
      if (success) {
        resolve("Operation succeeded");
      } else {
        reject("Operation failed");
      }
    }, 1000);
  });
};
asyncOperation().then(function (message) {
  console.log(message);
})["catch"](function (error) {
  console.error(error);
});

상단에 require("core-js/modules/es.promise.js"); 같이 폴리필이 추가된 것이 보인다. 이제 Promise 기능이 없는 브라우저에서는 core-js 에서 제공하는 폴리필을 통해 정상적으로 기능이 동작할 것이다.


최종 폴더 구조

📦vanilla-js-project
 ┣ 📂dist
 ┃ ┗ 📜script.js
 ┣ 📂src
 ┃ ┗ 📜script.js
 ┣ 📂node_modules
 ┣ 📜.babelrc
 ┣ 📜index.html
 ┣ 📜package-lock.json
 ┗ 📜package.json



결론

프레임워크를 사용하면 간단히 해결 👍

프레임워크의 CRA(Create React App)와 같은 CLI 툴을 이용해 프로젝트를 생성한 경우, Polyfill과 Transpiler이 자동으로 적용된다. 덕분에 개발자는 자유롭게 최신 기술과 문법을 사용하여 코드를 작성할 수 있다

하지만 전역 오염과 같은 문제가 발생했을 때 폴리필과 트랜스파일링 관련 개념이 없다면 이슈를 해결하는 데 어려움을 겪을 수 있으니 관련 내용을 학습해두면 좋을 것 같다.


profile
하나를 배웠을 때 하나를 알면 잘하는 것이다. 💡

0개의 댓글