코타키나발루 투아란 선셋

1. 개선 방향

bundle size 분석을 위하여 webpack-bundle-analyzer를 사용하였습니다. [부록 4.1]

성능 개선을 위하여 번들파일 분석을 진행하였고, 우선적으로 가장 사이즈가 큰 vender 파일을 중점으로 개선방향을 모색하였습니다.

현재 벤더파일의 문제점으로 용량이 큰 라이브러리(lodash, moment)들이 트리쉐이킹 되지지 않은 채 통째로 번들링되어 벤더파일로 적재되어 있는것을 발견하였고 이를 경량화 하는것을 이번 태스크의 목표로 잡았습니다.S


2. 개선 과정

단순 타겟 라이브러리의 개선 결과가 아닌 과정을 기록한 이유는 향후 다른 라이브러리를 도입하게 될 때 이번 트리쉐이킹에서 배제된 기술들이 더 용이한 경우도 있어 참고용으로 기록하였습니다.

2.1 lodash

현재 프로젝트에서 lodash를 사용하는 구문은 아래와 같습니다.

import { orderby } from 'lodash'; //71.1k

현재 개발환경에서 named import 를 사용하는건 탁월한 선택입니다. webpack에서 자동으로 tree shaking을 진행하여 사용되지 않은 메소드들은 번들파일에 포함되지 않기 때문입니다. 하지만 예외 사항으로 위의 lodash 처럼 commonJS 모듈을 사용하는 경우는 webpack은 트리쉐이킹을 진행하지 않습니다.

이를 해결하기 위하여 lodash를 트리쉐이킹하는 보편적인 3가지 방법을 소개하겠습니다

2.1.2 default import

import orderBy from "lodash/orderBy"; //20.8k

const arr = [1, 2, 3, 4, 5];

orderBy(arr, (num) => {
  console.log(num, "hello");
});

장점: lodash는 default import가 가능하며, 지정한 메서드만 사용하기 때문에 이미 트리쉐이킹 한 효과를 줄 수 있습니다.

단점: lodash의 메서드를 사용하기 때문에 lodash-es보다 사이즈가 크며 lodash의 여러가지 기능을 사용 시 부수적인 작업이 필요합니다.

ex) 여러개의 메소드드 가져올 시 webpack 세팅 필요

// 개별 import
import orderBy from "lodash/orderBy"; 
import debounce from "lodash/debounce"; 

//여러개 import 시 webpack 세팅 필요
import {orderBy, debounce} from "???"

2.1.3 lodash-es

import { orderBy } from "lodash-es"; //16.4k
const arr = [1, 2, 3, 4, 5];

orderBy(arr, (num) => {
  console.log(num, "hello");
});

장점: es6 구문으로만 쓰여저 가장 크기가 작으며, commonJS 모듈을 사용하지 않아 웹팩에서 tree shaking을 지원해줍니다. 즉 import 시 조금만 신경을 쓸 시 webpack 세팅이나 babel-plugin-lodash 등을 사용하지 않아도 자유롭게 사용 가능합니다.

단점: lodash uninstall 후 lodash-es 설치, import 변경 ‘lodash’ ⇒ ‘lodash-es’

2.1.4 개선 결과

2.1.3 lodash-es를 채택하여 현 프로젝트에 적용 시켜 보았습니다.

부수적인 코드 작성 없이 성능개선을 이루어낼 수 있었습니다


2.2 moment

현 프로젝트에서 moment의 사용은 아래와 같이 이루어져 있습니다.

import moment from 'moment';
formatDate(date) {
    return date ? moment(date.slice(0, 8)).format('MM월 DD일') : '';
}

현재 moment의 일부 기능만 사용하지만 모든 moment 기능이 트리쉐이킹 되지 않고 남아있는것을 볼 수 있습니다. 이를 개선하기위한 3가지 방법을 소개하겠습니다.

2.2.1 IgnorePlugin

const webpack = require('webpack')
module.exports = {
  plugins: [
    new webpack.IgnorePlugin(/^\\.\\/locale$/, /moment$/),
  ],
}

웹팩 내장 메서드를 사용하여 moment의 locale을 전부 무시할 수 있습니다. locale을 사용하지 않고 순수 date조작 기능만 사용하는 경우 유용한 방법입니다.

2.2.2 ContextReplacementPlugin 사용

const webpack = require('webpack')
module.exports = {
  plugins: [
    new webpack.ContextReplacementPlugin(/moment[/\\\\]locale$/, /ko/),
  ],
}

웹팩 내장 메서드를 사용하여 moment의 특정 locale만 추가하여 가져올 수 있는 방법입니다. date조작 및 특정 언어의 지원이 필요할 시 유용한 방법입니다.

2.2.3 moment → dayjs

moment를 뜯어내고 dayjs를 도입하는 방법입니다.

moment 단점

  • 내부적으로 정규표현식을 사용하여 타 date 라이브러리보다 느림
  • 용량이 무겁고 locale을 트리쉐이킹 하더라도 타 data 라이브러리 보다 무거움
  • Moment 라이브러리의 개발 중단(업데이트 x 트리쉐이킹 개선 x)

dayjs 장점

  • Immutable
  • 2KB의 경량 사이즈
  • moment의 대부분의 기능 대체 가능 및 사용법이 비슷해 러닝커브가 짧음
  • 가장많이 쓰는 date 라이브러리

2.2.4 개선 결과

2.2.2의 contextReplacementPlugin을 사용하여 해결하였습니다.

webpack에 간단한 플러그인을 추가하여 성능 개선을 이루어 낼 수 있었습니다.

2.2.1 미채택 - 현 프로젝트에서 moment.local(’ko’) 의 사용

2.2.3 미채택 - 팀원들의 협의가 필요


3. 개선 결과

번들사이즈 전/후

빌드 속도 전/후

빌드 속도 측정을 위하여 [부록 4.2]의 speed-measure-webpack-plugin 를 사용하였습니다.

청크 사이즈 축소 435kb → 350 kb

번들 사이즈 측면에서 10%이상의 개선효과를 볼 수 있었으며, 빌드 속도 측면에서는 40%이상의 개선 효과가 있었습니다.

4. 부록

4.1 webpack-bundle-analyzer

https://github.com/webpack-contrib/webpack-bundle-analyzer

4.1.1 소개

웹팩 번들 분석 플러그인 입니다. 웹팩을 이용하여 번들링되는 모듈들을 분석해서 시각적으로 나타냅니다.

자세한 사용법은 사이트 참조

4.1.2 사용법

# install
npm install --save-dev webpack-bundle-analyzer
//usage (as a plugin)
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    ...
  plugins: [
    new BundleAnalyzerPlugin() //webpack 플러그인에 추가
  ]
    ...
}

실행 방법

  1. npm 설치
  2. 플러그인 추가
  3. 컴파일 시 자동실행

4.2 speed-measure-webpack-plugin

https://github.com/stephencookdev/speed-measure-webpack-plugin

4.1.1 소개

웹팩 번들 분석 플러그인 입니다. 웹팩을 이용하여 번들링되는 모듈들을 분석해서 시각적으로 나타냅니다.

자세한 사용법은 사이트 참조

4.1.2 사용법

# install
npm install --save-dev speed-measure-webpack-plugin
//usage (as a plugin)
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [new MyPlugin(), new MyOtherPlugin()],
})

//smp.wrap으로 webpack 설정 감싸주기
//vue cli 기준 configureWebpack: smp:wrap({}) 자세한 이유는 vue cli 참조

실행 방법

  1. npm 설치
  2. 플러그인 추가
  3. 컴파일 시 자동 실행

머리말

자바스크립트는 싱글 스레드에서 작동한다. 즉 한 번에 하나의 작업만 처리할 수 있는데, 현대의 모던 웹 애플리케이션에서는 네트워크 요청이 발생하는 순간에도 다른 작업을 처리할 수 있다. 이는 어떻게 된 일일까?

이를 이해하려면 비동기 작업이 어떻게 처리 되는지 이해하고 비동기 처리를 도와주는 이벤트 루프를 비롯한 다양한 개념을 알고 있어야한다. 이번 포스트에서는 자바스크립트가 어떻게 여러가지 요청을 동시에 처리하고 있는지, 요청받은 테스크에 대한 우선순위는 무엇인지 파악해보자

KeyPoint

스택, 큐, 이벤트루프, 태스크 큐, 마이크로 태스크큐

런타임 환경과 함께 내용을 알아보자


Stack

함수의 호출들은 ‘스택’을 형성합니다 아래 예시코드를 들어보겠습니다

function foo(b) {
  const a = 10;
  return a + b + 11;
}

function bar(x) {
  const y = 3;
  return foo(x * y);
}

const baz = bar(7);
  1. bar(7)을 호출할 때 인수와 지역변수를 포함하는 첫 번째 프레임이 생성됩니다.
  2. bar가 foo를 호출할 때 위와같은 방식으로 두 번째 프레임이 생성됩니다.
  3. 이제 foo가 반환되면, 맨 위의 프레임 요소를 스택 밖으로 꺼냅니다.
  4. 마지막으로 남아있던 bar가 반환되며, 스택이 비어있게 됩니다.

이때 콜스택이 가득차면, 흔히보던 Uncaught RangeError: Maxium call stack size exceeded 오류를 만나볼 수 있습니다.


큐(Queue)

자바스크립트의 런타임은 메세지 큐, 즉 처리할 메세지의 대기열을 사용하는데. 각각의 메세지에는 메세지를 처리하기 위한 함수가 연결되어 있습니다.

이벤트 루프가 시작되는 시점에, 런타임은 대기열에서 가장 오래된 메세지부터 큐에서 꺼내 처리하기 시작합니다. 이를 위해 런타임은 꺼낸 메세지를 매개 변수로, 메세지에 연결된 함수를 호출합니다. 이 때 함수를 호출하면 해당 함수가 사용할 새로운 스택 프레임이 생성됩니다.

(setTimeout이 시작되는 시점에 해당 함수에 관된 내용이 스택에 쌓임)

여기서 이벤트 루프가 시작되는 시점이 대체 언제냐? 이벤트 루프를 통해 알아봅시다.


이벤트 루프

이벤트루프는 이벤트 루프만의 단일스레드 내에서 두 가지 역할을 합니다.

  1. 호출 스택이 비어있는지 확인
  2. 호출 스택 내부에 수행해야 할 작업이 있는지 확인하고, 수행해야 할 코드가 있으면 자바스크립트 엔진을 이용해 실행

코드를 통해 알아봅시다.

function foo(){
  setTimeout(() => console.log('bar'));
  console.log('foo');
}
foo()
  1. foo가 호출스택에 들어감
  2. setTimeout이 호출스택에 들어감
  3. setTimeout이 타이머 이벤트에 의해 큐로 들어가고 스택에서 제거됨.
  4. console.log(‘foo’)가 스택에 들어간 후 실행되고 스택에서 빠짐.
  5. 이벤트 루프가 호출 스택이 비워져있다는 것을 확인
  6. 이벤트 루프가 큐를 확인하여 수행해야 할 작업이 있나 확인 후 있다면 스택에 넣음
  7. 스택 실행 console.log(‘bar’) 실행

 

여기서 궁금할 수 있는 점으로는 setTimeout과 같이 n초를 기다리거나 fetch를 기반으로 실행되는 네트워크 요청은 누가 보내고 응답 받는것일까? 입니다.

 이러한 작업들은 전부 자바스크립트 메인 스레드가 아닌 태스크 큐가 할당되는 별도의 스레드에서 실행됩니다. 바로 브라우저나 Node.js에서 말입니다!(Web API등 외부에서 실행되고 큐로 들어감)

자 이제 우리는 이벤트루프를 통해 스택과 큐가 어떻게 작동 하는지 알게 되었습니다. 이제 마지막 챕터인 마이크로 태스크 큐로 넘어가 봅시다.


큐는 사실 FIFO가 아닌 set형태를 띄고 있다. 선택된 중 ‘실행 가능한’ 가장 오래된 태스크를 가져와야하기 때문이다.

 이때까지 큐라고 불렀던 공간은 흔히 태스크 큐(task queue) 또는 매크로 태스크 큐(macrotask que)라고들 합니다. 이번에는 이 태스크 큐보다 우선순위가 높은 마이크로 태스크 큐에대해 알아봅시다.

 

마이크로 태스크 큐는 대표적으로 Promise가 있다. 즉 setTImeout, 클릭 이벤트 등은 Promise보다 늦게 실행이 됩니다.

명세에 따르면 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미루어집니다.

  • 태스크 큐: 이벤트 핸들러, setTImeout, setInterval, promise를 사용하지않은 AJAX 요청
  • 마이크로 태스크 큐: Promise, queueMicrotask 함수, MutationObserver

이때 중요한 점으로 브라우저 렌더링 작업은 주로 태스크 큐에 속하고 있으며, Dom 변경에 대한 콜백이 Promise의 then() 또는 MutationObserver와 같은 API를 통해 등록된 경우가 아닌 이상 마이크로 태스크 큐태스크 큐 사이에 렌더링이 일어납니다.

 


 끝까지 읽어주셔서 감사합니다.! 이번 글을 통해 조금이라도 도움이 되셨다면 좋겠습니다. 마무리삼아 퀴즈를 하나 내드리겠습니다.

아래 코드는 어떤 순서대로 실행이 될까요??

const quiz = () => {
  console.log('1');

  Promise.resolve().then(() => {
    console.log(2);
  });

  setTimeout(() => console.log(3));

  window.requestAnimationFrame(() => console.log(4));
};

quiz();

'개발' 카테고리의 다른 글

[nextjs 톺아보기] <head>  (0) 2025.04.08
Vue Ref 톺아보기  (0) 2025.03.26
빌드시스템은 왜 필요할까?  (0) 2025.03.26
번들사이즈 최적화  (0) 2024.09.30

+ Recent posts

  1. 2.1 lodash
  2. 2.1.2 default import
  3. 2.1.3 lodash-es
  4. 2.1.4 개선 결과
  5. 2.2 moment
  6. 2.2.1 IgnorePlugin
  7. 2.2.2 ContextReplacementPlugin 사용
  8. 2.2.3 moment → dayjs
  9. 2.2.4 개선 결과
  10. 4.1 webpack-bundle-analyzer
  11. 4.1.1 소개
  12. 4.1.2 사용법
  13. 4.2 speed-measure-webpack-plugin
  14. 4.1.1 소개
  15. 4.1.2 사용법