개발 환경 구축의 배경과 필요성
현대의 웹 브라우저 환경은 매우 다양하며, 각 브라우저마다 최신 자바스크립트 기능의 지원 범위가 상이하다. 크롬, 사파리, 파이어폭스, 엣지와 같은 에버그린 브라우저들은 ES6 사양을 약 98% 정도 지원하는 반면, IE 11의 ES6 지원율은 11%에 불과하다. 게다가 매년 새롭게 등장하는 ES6+ 버전과 아직 제안 단계에 있는 ES.NEXT 사양은 브라우저별로 지원 정도가 제각각이라는 문제가 있다.
대부분의 모던 프로젝트에서는 모듈 시스템을 사용하는데, ES6의 모듈 시스템(ESM)을 사용할 때 몇 가지 중요한 문제점이 존재한다. 첫째로, IE를 포함한 구형 브라우저들은 ESM을 전혀 지원하지 않는다. 둘째로, ESM을 사용하더라도 코드의 트랜스파일링이나 번들링 작업이 여전히 필요하다. 마지막으로, ESM이 아직 지원하지 못하는 기능들이 있고, 현재 존재하는 여러 이슈들이 완전히 해결되지 않은 상태다.
이러한 환경적 제약을 극복하고 최신 ECMAScript 사양을 프로젝트에 적용하기 위해서는, 구형 브라우저에서도 문제없이 동작할 수 있는 개발 환경을 구축해야 한다. 이를 위해 트랜스파일러인 Babel과 모듈 번들러인 Webpack을 활용할 수 있다.
Babel
Babel은 최신 자바스크립트 코드를 하위 버전으로 변환해주는 트랜스파일러다. ES6+나 ES.NEXT로 작성된 코드를 IE와 같은 구형 브라우저에서도 정상적으로 동작할 수 있도록 ES5 사양의 코드로 변환해준다. 예를 들어 다음과 같은 변환이 가능하다.
// 변환 전: ES6의 화살표 함수와 ES7의 지수 연산자 사용
[1, 2, 3].map(n => n ** n);
// 변환 후: ES5 호환 코드
"use strict";
[1, 2, 3].map(function (n) {
return Math.pow(n, n);
});
Babel 설치
Babel을 사용하기 위해서는 몇 가지 핵심 패키지를 설치해야 한다. 우선 프로젝트 디렉토리를 생성하고 초기화한 후, 필요한 패키지들을 설치한다.
mkdir esnext-project && cd esnext-project
npm init -y
npm install --save-dev @babel/core @babel/cli
여기서 @babel/core는 Babel의 핵심 기능을 담고 있으며, @babel/cli는 터미널에서 Babel을 실행할 수 있게 해주는 커맨드라인 도구다. 개발 단계에서만 필요한 도구이므로 --save-dev 옵션을 사용해 devDependencies에 추가한다.
Babel 프리셋 설치와 babel.config.json 설정 파일 작성
Babel을 효과적으로 사용하기 위해서는 @babel/preset-env를 설치해야 한다. 이는 함께 사용되어야 하는 Babel 플러그인들을 모아놓은 프리셋으로, 프로젝트의 지원 환경에 맞춰 필요한 플러그인들을 동적으로 결정해준다. Babel이 제공하는 주요 공식 프리셋은 다음과 같다.
- @babel/preset-env: 범용적인 최신 자바스크립트 변환
- @babel/preset-flow: Flow 문법 지원
- @babel/preset-react: React의 JSX 문법 지원
- @babel/preset-typescript: TypeScript 문법 지원
프리셋을 설치하고 설정하는 과정은 다음과 같다.
npm install --save-dev @babel/preset-env
설치가 완료되면 프로젝트의 루트 디렉토리에 babel.config.json 파일을 생성하고 다음과 같이 설정한다.
{
"presets": ["@babel/preset-env"]
}
@babel/preset-env
@babel/preset-env는 특히 『.browserslistrc』 파일을 통해 지원하고자 하는 브라우저의 범위를 세세하게 지정할 수 있다. 예를 들어 다음과 같이 설정할 수 있다.
//.browserslistrc
1%
last 2 versions
not dead
not IE 11
이러한 설정을 통해 특정 브라우저 버전만을 지원하도록 하여, 불필요한 트랜스파일링을 줄이고 번들 크기를 최적화할 수 있다.
트랜스파일링
트랜스파일링 작업을 효율적으로 수행하기 위해 package.json의 scripts에 Babel CLI 명령어를 등록하는 것이 좋다. 매번 긴 명령어를 입력하는 번거로움을 피할 수 있기 때문이다. package.json을 다음과 같이 수정한다.
{
"name": "esnext-project",
"version": "1.0.0",
"scripts": {
"build": "babel src/js -w -d dist/js"
},
"devDependencies": {
"@babel/cli": "^7.10.3",
"@babel/core": "^7.10.3",
"@babel/preset-env": "^7.10.3"
}
}
여기서 build 스크립트는 src/js 폴더의 모든 자바스크립트 파일을 트랜스파일링하여 결과물을 dist/js 폴더에 저장하도록 설정된다. 사용된 옵션들의 의미는 다음과 같다.
- -w (--watch): 소스 파일의 변경을 감지하여 자동으로 트랜스파일링을 수행한다.
- -d (--out-dir): 트랜스파일링된 결과물이 저장될 디렉토리를 지정한다. 해당 디렉토리가 존재하지 않으면 자동으로 생성된다.
이제 트랜스파일링을 테스트하기 위해 ES6+/ES.NEXT 사양의 소스 코드를 작성해보자. src/js 폴더를 생성하고 그 안에 lib.js와 main.js 파일을 다음과 같이 작성한다.
// src/js/lib.js
export const pi = Math.PI; // ES6 모듈
export function power(x, y) {
return x ** y; // ES7의 지수 연산자
}
// ES6 클래스와 private 필드
export class Foo {
#private = 10;
foo() {
const { a, b, ...x } = { ...{ a: 1, b: 2 }, c: 3, d: 4 };
return { a, b, x };
}
bar() {
return this.#private;
}
}
// src/js/main.js
import { pi, power, Foo } from './lib';
console.log(pi);
console.log(power(pi, pi));
const f = new Foo();
console.log(f.foo());
console.log(f.bar());
Babel 플러그인 설치
현재 제안 단계에 있는 자바스크립트 기능들(예: 클래스 private 필드)을 사용하기 위해서는 추가적인 Babel 플러그인이 필요하다. 이러한 플러그인은 Babel 홈페이지에서 검색하여 찾을 수 있다. 클래스 필드 정의를 위한 플러그인을 설치하려면 다음 명령어를 실행한다.
npm install --save-dev @babel/plugin-proposal-class-properties
설치 후 babel.config.json 파일에 플러그인을 추가한다.
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
브라우저에서 모듈 로딩 테스트
Node.js 환경에서는 CommonJS 방식의 모듈 시스템을 기본적으로 지원하지만, 브라우저는 이를 지원하지 않는다. 트랜스파일링된 코드를 브라우저에서 실행하면 다음과 같은 문제가 발생한다.
<!DOCTYPE html>
<html>
<body>
<script src="dist/js/lib.js"></script>
<script src="dist/js/main.js"></script>
</body>
</html>
이 HTML 파일을 브라우저에서 실행하면 'exports is not defined'나 'require is not defined'와 같은 에러가 발생한다. 이는 브라우저가 Node.js의 모듈 시스템을 이해하지 못하기 때문이다. 이 문제를 해결하기 위해 모듈 번들러인 Webpack을 사용할 수 있다.
Webpack
Webpack은 모던 자바스크립트 애플리케이션을 위한 강력한 모듈 번들러다. 프로젝트 내의 자바스크립트, CSS, 이미지 등 다양한 리소스 간의 의존 관계를 분석하여 이들을 하나 또는 여러 개의 파일로 묶어준다. Webpack을 사용하면 다음과 같은 이점을 얻을 수 있다.
- 모든 의존 모듈이 하나의 파일로 번들링되어 별도의 모듈 로더가 필요 없어진다.
- HTML 파일에서 여러 개의 script 태그로 자바스크립트 파일을 로드해야 하는 번거로움이 해소된다.
- 코드를 수정할 때마다 자동으로 빌드하고 브라우저를 새로고침할 수 있다.
- 소스 맵을 제공하여 디버깅을 용이하게 한다.
Webpack 설치
Webpack을 설치하기 위해서는 webpack과 webpack-cli 두 패키지가 필요하다. webpack-cli는 터미널에서 Webpack 명령어를 실행할 수 있게 해주는 커맨드라인 도구다.
npm install --save-dev webpack webpack-cli
설치가 완료되면 package.json 파일이 다음과 같이 업데이트된다.
{
"name": "esnext-project",
"version": "1.0.0",
"scripts": {
"build": "babel src/js -w -d dist/js"
},
"devDependencies": {
"@babel/cli": "^7.10.3",
"@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/preset-env": "^7.10.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12"
}
}
babel-loader 설치
Webpack이 자바스크립트 파일을 번들링할 때 Babel을 사용하여 ES6+/ES.NEXT 사양의 코드를 ES5로 트랜스파일링하도록 설정하기 위해 babel-loader를 설치해야 한다.
npm install --save-dev babel-loader
그리고 package.json의 scripts를 수정하여 기존의 Babel 대신 Webpack을 실행하도록 변경한다.
{
"scripts": {
"build": "webpack -w"
}
}
webpack.config.js 설정 파일 작성
Webpack의 동작을 세부적으로 제어하기 위해서는 webpack.config.js 설정 파일이 필요하다. 이 파일은 Webpack이 실행될 때 참조하는 설정 파일로, 프로젝트 루트 디렉토리에 생성하며 다음과 같이 작성한다.
const path = require('path');
module.exports = {
// 진입점이 되는 파일 지정
entry: './src/js/main.js',
// 번들링된 결과물의 출력 설정
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/bundle.js'
},
// 모듈 처리 방법 설정
module: {
rules: [
{
test: /\.js$/, // .js 확장자를 가진 파일에 대해
include: [
path.resolve(__dirname, 'src/js')
],
exclude: /node_modules/, // node_modules 디렉토리는 제외
use: {
loader: 'babel-loader', // babel-loader를 사용하여 처리
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-proposal-class-properties']
}
}
}
]
},
// 소스 맵 생성 설정
devtool: 'source-map',
// 개발 모드 설정
mode: 'development'
};
추가로 아래의 두 속성은 다음과 같다.
- devtool: 'source-map' 옵션은 디버깅을 위한 소스맵을 생성한다. 소스맵은 번들링된 코드를 원본 소스 코드와 매핑해주어 브라우저의 개발자 도구에서 원본 코드를 기준으로 디버깅할 수 있게 해준다.
- mode: 'development' 설정은 개발 환경에 맞는 최적화를 제공한다. 'production' 모드로 설정하면 코드 압축, 최적화 등이 자동으로 적용되어 배포에 적합한 결과물이 생성된다.
babel-polyfill 설치
Babel을 사용하여 ES6+ 코드를 ES5로 트랜스파일링해도, 브라우저가 지원하지 않는 일부 기능들이 여전히 남아있을 수 있다. 대표적인 예로 Promise, Object.assign, Array.from 등이 있는데, 이러한 기능들은 ES5 사양으로 대체할 수 있는 기능이 없기 때문에 트랜스파일링되지 않고 그대로 남게 된다.
이러한 문제를 확인하기 위해 다음과 같은 테스트 코드를 작성해볼 수 있다.
// src/js/main.js
import { pi, power, Foo } from './lib';
console.log(pi);
console.log(power(pi, pi));
const f = new Foo();
console.log(f.foo());
console.log(f.bar());
// polyfill이 필요한 코드들
console.log(new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 100);
}));
console.log(Object.assign({}, { x: 1 }, { y: 2 }));
console.log(Array.from([1, 2, 3], v => v + v));
이 코드를 트랜스파일링하면, Promise, Object.assign, Array.from 등의 코드는 변환되지 않고 그대로 남아있게 된다. IE와 같은 구형 브라우저에서는 이러한 기능들을 지원하지 않으므로, 이를 해결하기 위해 @babel/polyfill을 설치해야 한다.
npm install @babel/polyfill
@babel/polyfill은 개발 환경뿐만 아니라 실제 운영 환경에서도 필요한 패키지이므로, --save-dev 옵션을 사용하지 않고 설치한다. 설치 후 package.json은 다음과 같이 업데이트된다.
{
"dependencies": {
"@babel/polyfill": "^7.10.1"
}
}
폴리필을 적용하는 방법에는 두 가지가 있다.
1. ES6의 import를 사용하는 경우: 진입점 파일의 최상단에서 폴리필을 먼저 로드한다.
import "@babel/polyfill";
import { pi, power, Foo } from './lib';
// ... 나머지 코드
2. Webpack을 사용하는 경우: webpack.config.js 파일의 entry 배열에 폴리필을 추가한다.
module.exports = {
entry: ['@babel/polyfill', './src/js/main.js'],
// ... 나머지 설정
};
이렇게 설정하면 브라우저의 지원 여부와 관계없이 Promise, Object.assign, Array.from 등의 기능을 안전하게 사용할 수 있게 된다. 폴리필이 해당 기능들을 구형 브라우저에서도 동작할 수 있도록 구현해주기 때문이다.
최신 Babel과 Polyfill 사용에 대한 참고사항
@babel/polyfill은 사실상 core-js와 regenerator-runtime의 조합이다. 최신 버전의 Babel에서는 @babel/polyfill 대신 core-js를 직접 사용하는 것을 권장한다. 이는 더 세밀한 폴리필 관리가 가능하고, 필요한 기능만 선택적으로 가져올 수 있어 번들 크기를 최적화할 수 있기 때문이다.
core-js를 사용하는 방식은 다음과 같다.
npm install core-js regenerator-runtime
그리고 진입점 파일에서 다음과 같은 내용을 작성합니다.
import "core-js/stable";
import "regenerator-runtime/runtime";
HTML 파일 로딩 최적화
번들된 JavaScript 파일을 HTML에서 로드할 때, script 태그의 위치는 성능에 중요한 영향을 미친다. body 태그의 끝부분에 스크립트를 위치시키면 다음과 같은 이점이 있다.
1. HTML 문서의 파싱이 JavaScript 파일을 다운로드하고 실행하는 동안 차단되지 않는다.
2. DOM이 완전히 구성된 후에 JavaScript가 실행되므로, DOM 조작에 관한 에러를 방지할 수 있다.
3. 페이지 로딩 시간이 체감상 더 빠르게 느껴진다.
다음은 최적화된 스크립트 로딩 방식의 예시다.
<!DOCTYPE html>
<html>
<body>
<!-- 페이지 컨텐츠 -->
<script src="dist/js/bundle.js"></script>
</body>
</html>
또는 아래 코드처럼 script 태그를 head 부분에 위치시키고 싶다면, defer 속성을 사용하여 비슷한 효과를 얻을 수 있다.
<!DOCTYPE html>
<html>
<head>
<script defer src="dist/js/bundle.js"></script>
</head>
<body>
<!-- 페이지 컨텐츠 -->
</body>
</html>
요약
개발 환경 구축의 배경과 필요성
최신 ECMAScript 사양과 브라우저 지원 현황
- 에버그린 브라우저(크롬, 사파리, 파이어폭스, 엣지)의 ES6 지원율은 98% 수준이다.
- 반면 IE 11의 ES6 지원율은 11%에 불과하다.
- ES6+ 버전과 ES.NEXT 사양은 브라우저별로 지원 정도가 상이하다.
모듈 시스템(ESM) 사용 시 문제점
- 구형 브라우저는 ESM을 지원하지 않는다.
- ESM 사용 시에도 트랜스파일링과 번들링이 필요하다.
- ESM이 지원하지 않는 기능들이 있고 해결되지 않은 이슈가 존재한다.
Babel의 역할과 기능
Babel의 핵심 기능
- 최신 자바스크립트 코드를 구형 브라우저에서도 동작하는 ES5 코드로 변환한다.
- ES6+/ES.NEXT 사양의 소스코드를 트랜스파일링한다.
Babel 설치와 구성
- 핵심 패키지 설치 (@babel/core, @babel/cli)
- 프리셋 설치 (@babel/preset-env)
- 설정 파일 (babel.config.json) 작성
- 필요한 플러그인 설치 (예: @babel/plugin-proposal-class-properties)
Babel의 주요 프리셋
- @babel/preset-env: 범용 자바스크립트 변환
- @babel/preset-flow: Flow 문법 지원
- @babel/preset-react: React JSX 지원
- @babel/preset-typescript: TypeScript 지원
Webpack의 역할과 기능
Webpack의 핵심 기능
- 모듈 번들러로서 의존 관계에 있는 리소스들을 하나로 묶는다.
- 자바스크립트, CSS, 이미지 등 다양한 리소스를 처리한다.
- 별도의 모듈 로더 없이 의존성 관리가 가능하다.
Webpack의 주요 이점
- 모듈 의존성 해결과 번들링
- 로딩 스크립트 단순화
- 자동 빌드와 새로고침
- 소스맵 제공으로 디버깅 용이
개발 환경 구축 상세 과정
1. Babel 설정
// babel.config.json
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
2. Webpack 설정
// webpack.config.js
module.exports = {
entry: './src/js/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
}
3. 폴리필 설정과 적용
// 방법 1: 진입점 파일에서 import
import "@babel/polyfill";
// 방법 2: webpack.config.js에서 설정
entry: ['@babel/polyfill', './src/js/main.js']
- @babel/polyfill 설치 필요성
- Promise, Object.assign, Array.from 등 ES5로 대체 불가능한 기능 지원
- 구형 브라우저 호환성 보장
4. 최신 폴리필 전략
import "core-js/stable";
import "regenerator-runtime/runtime";
(core-js와 regenerator-runtime 직접 사용)
- 장점
- 더 세밀한 폴리필 관리 가능
- 필요한 기능만 선택적 사용
- 번들 크기 최적화
개발 환경 최적화 전략
1. 스크립트 로딩 최적화
- body 태그 끝에 스크립트 위치
<body>
<!-- 컨텐츠 -->
<script src="dist/js/bundle.js"></script>
</body>
- 또는 defer 속성 사용
<head>
<script defer src="dist/js/bundle.js"></script>
</head>
2. 버전 관리 전략
- 프로덕션 환경
- 특정 버전 명시 (예: @babel/core@7.10.3)
- 팀 간 일관성 유지
- 안정성 보장
- 개발 환경
- 최신 버전 사용 가능
- 새로운 기능과 개선사항 활용
- 호환성 이슈 고려 필요
주요 고려사항과 모범 사례
- 브라우저 지원 범위 설정
- .browserslistrc 파일을 통한 타겟 브라우저 지정
- 불필요한 트랜스파일링 방지
- 최적화된 번들 크기 유지
- 개발 생산성 향상
- 자동 빌드 및 리로드
- 소스맵을 통한 디버깅
- 모듈 핫 리플레이스먼트(HMR) 활용
- 성능 최적화
- 코드 분할 (Code Splitting)
- 지연 로딩 (Lazy Loading)
- 트리 쉐이킹 (Tree Shaking)
예상문제 [🔥]
https://github.com/junh0328/prepare_frontend_interview?tab=readme-ov-file
자바스크립트의 가비지 컬렉션에 대해 알고 있나요?
자바스크립트의 가비지 컬렉션은 메모리 관리를 자동화하는 중요한 기능입니다. 개발자가 직접 메모리를 할당하고 해제할 필요 없이 자동으로 관리되는데, 주로 두 가지 알고리즘을 사용합니다.
첫 번째는 '참조 카운팅' 방식입니다. 이는 각 객체에 대한 참조 수를 추적하다가, 참조 카운트가 0이 되면 해당 객체를 수거하는 방식입니다. 구현이 간단하고 즉시 수거가 가능하다는 장점이 있지만, 순환 참조 문제를 해결하지 못한다는 단점이 있습니다. 예를 들어, 두 객체가 서로를 참조하고 있으면 실제로는 사용되지 않더라도 메모리에서 해제되지 않는 문제가 발생할 수 있습니다.
두 번째는 '마크 앤 스위프' 방식으로, 현대 자바스크립트 엔진들이 주로 사용하는 방식입니다. 이는 두 단계로 동작하는데, 먼저 GC 루트(전역 객체나 현재 함수의 스코프 체인 등)에서 시작해서 도달 가능한 모든 객체를 마크하고, 그 다음 단계에서 마크되지 않은 객체들을 메모리에서 해제합니다. 이 방식은 순환 참조 문제를 해결할 수 있다는 장점이 있습니다.
최근의 자바스크립트 엔진들은 이러한 기본적인 알고리즘을 더욱 최적화하여 사용합니다. 예를 들어, 객체를 젊은 세대와 오래된 세대로 나누어 관리하는 '세대별 수집' 방식이나, 가비지 컬렉션 작업을 여러 작은 단위로 나누어 수행하는 '증분 수집' 방식 등을 활용합니다. 이를 통해 메모리를 효율적으로 관리하면서도 성능에 미치는 영향을 최소화할 수 있습니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 48장 - 모듈 (0) | 2024.11.30 |
---|---|
[모던 자바스크립트 Deep Dive] 47장 - 에러 처리 (0) | 2024.11.30 |
[모던 자바스크립트 Deep Dive] 46장 - 제너레이터와 async/await (1) | 2024.11.30 |
[모던 자바스크립트 Deep Dive] 45장 - Promise (0) | 2024.11.26 |
[모던 자바스크립트 Deep Dive] 44장 - REST API (2) | 2024.11.24 |