개발새발 로그

객체 구조 분해 할당과 객체 전개 연산자가 번들링 크기를 크게 만든다고?? 본문

React

객체 구조 분해 할당과 객체 전개 연산자가 번들링 크기를 크게 만든다고??

이즈흐 2024. 4. 3. 20:54

우리는 리액트로 프로젝트를 개발할 때 객체 구조 분해 할당과 전개 연산자를 정말 자주 사용한다.

 

근데 이 객체 구조 분해 할당과 전개 연산자가 번들링 크기를 크게 만들어서 사용시 고려해야한다고 한다..!

 

나는 이 부분에 대해서 "왜 번들링 크기가 커지는 것이지?" 에 대해서 생각하게 됐고,

"그럼 객체 구조 분해 할당이나 객체 전개 연산자는 사용하면 안되는거야?" 에 대해서 찾아보게 됐다.

 

 

발전해가는 Javascript

다른 언어와 마찬가지로 Javascript는 새로운 버전과 함께 새로운 기능이 나오고 있다.

이러한 Javascript 표준을 ECAMScript라고 한다.

 

그래서 작성하고자하는 자바스크립트 문법이 어떤 ECMAScript 버전에서 만들어졌는지 확인해야한다.

왜냐하면 모든 브라우저와 자바스크립트 런타임이 항상 새로운 자바스크립트 문법을 지원하는 것이 아니기 때문이다.

 

예를 들어 인터넷 익스플로어 11은 ECMAScript 5(ES5) 까지만 지원하기 때문에 최신 자바스크립트 문법을 사용할 수 없다.

만약 개발하고 있는 서비스가 이를 고려해야한다면 코드에서 최신 자바스크립트 문법을 사용할 수 없다는 점을 고려해야된다.

그리고 크롬,사파리,파이어폭스 등 다양한 브라우저가 있기 때문에 이 브라우저들에서의 문법 지원 또한 염두에 두어야한다.

 

Babel의 등장

이렇게 다양한 브라우저의 환경, 최신 문법을 사용하려는 개발자의 요구를 해결하기 위해 탄생한 것이 Babel이다.

Babel은 자바스크립트의 최신 문법을 다양한 브라우저에서도 일관적으로 지원할 수 있도록 코드를 트랜스파일한다.

 

인터넷 익스플로어 11의 지원은 종료 됐지만 아직 사용 하는 곳도 있고,

엣지 환경에서도 호환모드를 사용해 익스플로어 환경에 접근가능하다.

뿐만 아니라 셋톱박스와 같은 구형 기기에서도 ES5만 동작하는 경우가 있기 때문에 ES5 기준으로 트랜스파일된 코드를 파악하는 것이 중요하다.

 

트랜스파일러 / 컴파일러?

바벨 공식 문서에서 가장 먼저 보이는 문구는 "바벨은 자바스크립트 컴파일러입니다."이다.

나는 "Bable이 트랜스파일을 하는거니까 트랜스 파일러 아니야?"라고 생각했다.

 

컴파일러는 고급 프로그래밍언어로 작성된 소스코드를 컴퓨터가 이해할 수 있는 기계어나 바이트코드 같은 저급 언어로 변환하는 프로그램이다.

대략적으로 렉싱 or 토크나이징 -> 파싱 -> 의미 분석 -> 최적화 -> 코드 생성 단계다.

 

트랜스 파일러는 고급 프로그래밍 언어로 작성된 코드를 다른 고급 프로그래밍 언어로 변환하는 도구다.

컴파일러와 유사하지만 컴파일러는 고급언어를 기계어로 변환하는 것과 대조적으로 트랜스 파일러는 고급 언어끼리의 변환을 담당한다.

 

그런데 왜 공식 문서에서는 컴파일러라고 명시하는 것일까?

트랜스파일러는 source-to-source compiler라고 불리며 컴파일러의 일종이라고 한다.

어떻게 보면 트랜스파일러라고 하지만 더 큰 범주에서는 컴파일러인 것이다.

그리고 바벨은 코드의 변환 뿐만 아니라 최적화, 에러체크, 폴리필 추가 등 다양한 작업을 포함한다.

따라서 바벨은 단순한 트랜스 파일러를 넘어서는 기능을 제공하므로 "컴파일러"라고 칭하는 것이라고 한다.

 

 

 

객체 구조 분해 할당

배열의 구조 분해할당은 ES2015에 처음 등장했고, 객체의 구조 분해 할당은 ECMA 2018에 등장했다.

 

객체 구조 분해 할당은 말 그대로 객체에서 값을 꺼내온 뒤 할당하는 것을 의미한다.

배열 구조 분해 할당과 달리 객체는 객체 내부 이름으로 꺼내온다는 차이가 있다.

let options = {
  title: "Menu",
  width: 100,
  height: 200
};

let {title, width, height} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200

 

객체 구조 분해할당 사용법은 생략하고, 내가 처음에 가지고 있던 궁금증에 대해서 찾아보려고 한다.

 

번들링 크기가 커지는 이유

객체 구조 분해 할당 코드가 바벨에서 어떻게 트랜스파일 되는지 살펴보자.

https://babeljs.io/repl/#?browsers=&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=MYewdgzgLgBCBGArApsWBeGBvAUDGAhgFwwCMANHjPCRVcLeTFQCaNXLsC-OOoksLASbwmAOgkAnZNC4xMCFGiA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env%2Cstage-0%2Cstage-1%2Cstage-2&prettier=true&targets=&version=7.24.3&externalPlugins=&assumptions=%7B%7D

 

Babel · Babel

The compiler for next generation JavaScript

babeljs.io

const object = {
  a: 1,
  b: 1,
  c: 1, 
  d: 1,
  e: 1,
}

const {a, b, ...rest} = object

위 코드를 바벨로 트랜스파일하면 아래와 같은 결과가 나온다.

function _objectWithoutProperties(source, excluded) {
  if (source == null) return {};
  var target = _objectWithoutPropertiesLoose(source, excluded);
  var key, i;
  if (Object.getOwnPropertySymbols) {
    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
    for (i = 0; i < sourceSymbolKeys.length; i++) {
      key = sourceSymbolKeys[i];
      if (excluded.indexOf(key) >= 0) continue;
      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
      target[key] = source[key];
    }
  }
  return target;
}
function _objectWithoutPropertiesLoose(source, excluded) {
  if (source == null) return {};
  var target = {};
  var sourceKeys = Object.keys(source);
  var key, i;
  for (i = 0; i < sourceKeys.length; i++) {
    key = sourceKeys[i];
    if (excluded.indexOf(key) >= 0) continue;
    target[key] = source[key];
  }
  return target;
}
var object = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1
};
var a = object.a,
  b = object.b,
  rest = _objectWithoutProperties(object, ["a", "b"]);

 

배열 구조 분해 할당는 간단하게 구성되지만 객체 구조 분해 할당은 위처럼 복잡한 것을 볼 수있다.

이를 통해서 객체 구조 분해를 할 경우 이러한 트랜스 파일을 거치게 될 것이고, 번들링 크기가 상대적으로 커진다는 것이었다.

 

그래서 객체 구조 분해 할당을 자주 쓰지 않는다면 꼭 써야하는지 검토하고, 트랜스파일이 부담스럽지만 객체 구조 분해 할당을 통한 ...rest와 같은 함수가 필요하다면 외부 라이브러리를 권장한다고 한다.

 

여기서 번들링 크기가 왜 커지는지 알 수 있었다.

 

그렇지만 이 같은 이유로 쓰지 않기에는 객체 구조 분해 할당의 장점은 많았다.

 

객체 구조 분해 할당의 장점

객체 구조 분해 할당을 사용함으로써 얻을 수 있는 장점이다.

1. 코드 가독성

2. 중복 코드 감서

3. 변수 선언 간소화

4. 가져오가자 하는 속성명 변경

5. 기본 값 설정 가능

 

이 장점들이 있는데 굳이 쓰지 않을 이유가 있을까? 판단했다.

 

그리고 의문점🤔

확실히 번들링 했을 때 코드의 길이가 길어지는 것은 사실이지만 저 코드의 길이로 인해 성능이 저하되지는 않는다고 생각됐다.

그래서 만약 객체 구조 분해 할당이 많아지면 거기에 비례해서 커지는 것인가? 생각했다.

const object = {
  a: 1,
  b: 1,
  c: 1, 
  d: 1,
  e: 1,
}

const {a, b, ...rest} = object

const object2 = {
  f: 1,
  g: 1,
  h: 1, 
  i: 1,
  j: 1,
}

const {f, g, ...rest2} = object2
function _objectWithoutProperties(source, excluded) {
  if (source == null) return {};
  var target = _objectWithoutPropertiesLoose(source, excluded);
  var key, i;
  if (Object.getOwnPropertySymbols) {
    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
    for (i = 0; i < sourceSymbolKeys.length; i++) {
      key = sourceSymbolKeys[i];
      if (excluded.indexOf(key) >= 0) continue;
      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
      target[key] = source[key];
    }
  }
  return target;
}
function _objectWithoutPropertiesLoose(source, excluded) {
  if (source == null) return {};
  var target = {};
  var sourceKeys = Object.keys(source);
  var key, i;
  for (i = 0; i < sourceKeys.length; i++) {
    key = sourceKeys[i];
    if (excluded.indexOf(key) >= 0) continue;
    target[key] = source[key];
  }
  return target;
}
var object = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1
};
var a = object.a,
  b = object.b,
  rest = _objectWithoutProperties(object, ["a", "b"]);
var object2 = {
  f: 1,
  g: 1,
  h: 1,
  i: 1,
  j: 1
};
var f = object2.f,
  g = object2.g,
  rest2 = _objectWithoutProperties(object2, ["f", "g"]);

하지만 바벨에서도 구조 분해 할당을 변환할 때 필요한 보조 함수를 처음 사용하는 지점에만 삽입하고, 이후에는 같은 보조 함수를 재사용할 수 있다고 한다.

따라서, 구조 분해 할당을 여러 번 사용한다고 해서 번들링 크기가 그 횟수에 비례해서 크게 증가하지는 않는 것이다.

 

객체 전개 구문도?

객체 전개 구문도 살펴보자

아래 코드를 바벨로 트랜스파일해보자.

const object = {
  a: 1,
  b: 1,
  c: 1, 
  d: 1,
  e: 1,
}

const newObj = {...object};
function _typeof(o) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
          }
        : function (o) {
            return o &&
              "function" == typeof Symbol &&
              o.constructor === Symbol &&
              o !== Symbol.prototype
              ? "symbol"
              : typeof o;
          }),
    _typeof(o)
  );
}
function ownKeys(e, r) {
  var t = Object.keys(e);
  if (Object.getOwnPropertySymbols) {
    var o = Object.getOwnPropertySymbols(e);
    r &&
      (o = o.filter(function (r) {
        return Object.getOwnPropertyDescriptor(e, r).enumerable;
      })),
      t.push.apply(t, o);
  }
  return t;
}
function _objectSpread(e) {
  for (var r = 1; r < arguments.length; r++) {
    var t = null != arguments[r] ? arguments[r] : {};
    r % 2
      ? ownKeys(Object(t), !0).forEach(function (r) {
          _defineProperty(e, r, t[r]);
        })
      : Object.getOwnPropertyDescriptors
      ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t))
      : ownKeys(Object(t)).forEach(function (r) {
          Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
        });
  }
  return e;
}
function _defineProperty(obj, key, value) {
  key = _toPropertyKey(key);
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == _typeof(i) ? i : i + "";
}
function _toPrimitive(t, r) {
  if ("object" != _typeof(t) || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != _typeof(i)) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}
var object = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1
};
var newObj = _objectSpread({}, object);

확실이 단순한 코드가 트랜스파일 되었을 때 상대적으로 번들링 크기가 커질 것으로 예상된다.

 

하지만 이 또한 위에서 말했던 것처럼 이러한 변환으로 인한 번들 크기의 증가는 대부분의 경우 상대적으로 작다고 판단된다.

현대 웹 개발에서는 이미지, 외부 라이브러리, 프레임워크 등이 차지하는 크기가 훨씬 크므로,

이러한 문법의 사용으로 인한 크기 증가는 전체 번들 크기에서 큰 비중을 차지하지 않는다고 생각됐다.

현대의 번들러

 현대의 번들러들은 트리 쉐이킹(tree shaking)과 같은 최적화 기법을 적용하여 불필요한 코드를 제거하고, 최종적으로 생성되는 번들의 크기를 최소화 한다고 한다.

이러한 과정에서 사용되지 않는 코드나 중복된 코드가 제거되므로, 구조 분해 할당과 같은 문법을 사용하더라도 최적화를 통해 번들 크기를 효율적으로 관리할 수 있다.

 

따라서, 코드의 가독성과 유지보수성을 개선하는 데 도움이 되는 구조 분해 할당과 같은 문법을 적극적으로 활용하는 것이 좋다고 판단했다.

 

 

트리쉐이킹?

트리 쉐이킹(Tree Shaking)은 불필요한 코드를 제거하는 과정을 말한다.

웹 애플리케이션을 빌드할 때, 개발자는 다양한 라이브러리와 모듈을 사용한다.

하지만 실제로 애플리케이션에서 사용되는 것보다 훨씬 많은 코드를 포함하고 있을 수 있다.

트리 쉐이킹은 이러한 불필요한 코드를 식별하고 제거함으로써 최종 번들의 크기를 줄이는 최적화 기법입니다.

 

 

트리 쉐이킹을 구현하는 대표적인 도구로는 Webpack, Rollup, Parcel 등이 있습니다.

 

Webpack에서 트리쉐이킹 방법

필요한 모듈만 import 하기

import 구문을 이용해서 라이브러리를 불러와서 사용할 때, 라이브러리 전체가 아닌 필요한 모듈만 불러온다.
이렇게 하면 번들링 과정에서 사용하는 부분의 코드만 포함시키기 때문에 트리쉐이킹을 할 수 있다.

import React from 'react'; // ❌

import { useState, useEffect } from 'react; // ⭕️

 

Babelrc 파일 설정하기

Babel
JavaScript 문법이 구 버전 브라우저에서도 호환이 가능하도록 ES5 문법으로 변환해주는 라이브러리 (컴파일러)

 

이때, ES5 문법은 import를 지원하지 않기 때문에, commonJS 문법의 require로 변경시킨다.
require는 라이브러리의 모든 모듈을 불러오기 때문에 1번에서 작성한 것처럼 필요한 모듈만 불러오는 코드를 작성해도 소용이 없어진다.

 

➡️ 이를 방지하기 위해 Babelrc 파일에 아래처럼 코드를 작성해주면 ES5 문법으로 변환하는 것을 막을 수 있다.

// .babelrc
{
  “presets”: [ 
    [
      “@babel/preset-env”,
      {
	    "modules": false // modules 값을 false로 설정하면 ES5 문법으로 변환하는 것을 막고,
                         // 반대로 true로 설정하면 항상 ES5 문법으로 변환한다.
      }
    ]
 ]
}

 

sideEffects 설정하기

Webpack은 사이드 이펙트를 일으킬 수 있는 코드의 경우, 사용하지 않는 코드라도 트리쉐이킹에서 제외시킨다.

// 사이트 이펙트를 일으키는 코드 예시
// addCrew() 함수는 함수 외부에 있는 배열 `crews`를 변경시킨다.
const crews = ['kim', 'park'];

const addCrew = (name) => {
  crews.push(name);
}

Webpack은 순수 함수가 아닌, 사이드 이펙트를 일으키는 함수를 트리쉐이킹으로 제외하는 경우, 문제가 생길 수도 있다고 판단하여 번들링할 때 해당 코드를 제외시키지 않는다. (즉, 사용하지 않아도 번들링시킨다..)

 

➡️ 이때, package.json 파일에서 sideEffects를 설정하여, 애플리케이션 전체에서 사이드 이펙트가 발생하지 않을 것이니 트리쉐이킹을 해도 된다고 Webpack에게 알려줄 수 있다.

// package.json
{
  "name": "tree-shaking",
  "version": "1.0.0",
  "sideEffects": false
}

 

혹은 아래처럼 작성하여 특정 파일에서는 사이드 이펙트가 발생하지 않을 것이라고 알려줄 수 있다.

// package.json
{
  "name": "tree-shaking",
  "version": "1.0.0",
  "sideEffects": ["./src/components/NoSideEffect.js"]
}

ES6 문법을 사용하는 모듈 사용하기

보통 3번까지 작성하면 트리쉐이킹이 잘 작동한다.
그럼에도 트리쉐이킹이 적용되지 않는 라이브러리가 있다면, 해당 라이브러리가 ES5 문법을 사용하고 있진 않은지 확인해야 한다.
이 경우에는 해당 모듈을 대체할 수 있는, ES6 문법을 지원하는 다른 모듈을 사용하는 것이 좋다.

 

 

 

 

728x90
반응형
LIST