javascript

Code splitting시 주의해야할 점(side effect)

하리하링웹 2023. 1. 15. 16:33

javascript에서 Code splitting을 해서 Dynamic하게 import를 진행할 때에 주의해야 할 점이 있다.

 

이 글에서는 Next.JS의 dynamic을 사용하여 예를들어 설명해보겠다.

import dynamic from 'next/dynamic';
import { useState } from 'react';

import { Button } from '@/components/ui';

const DynamicComponent = dynamic(() => import('@/components/test/DynamicComponent'));
export default function TestPage() {
  const [isRender, setIsRender] = useState(false);

  return (
    <div>
      <Button onClick={() => setIsRender((p) => !p)}>BUTTON</Button>
      {isRender && <DynamicComponent />}
    </div>
  );
}

일반적으로 next의 dynamic import는 위와 같은 상황에 사용하게된다.

 

실제로 이를 dev tool에서 확인해보면 버튼을 눌렀을때에 DynamicComponent를 lazy하게 요청하여 렌더시키는 것을 확인할 수 있다.

 

하지만 이 때에 주의해야 할 점이 있다.

 

아래는 자바스크립트 개발자들이 많이 쓰는 export 방식중 하나인데

인덱스 exrpot 사진

대충 이러한 구조로 index 파일을 따로 만들어 export를 해주는 방식이다.

 

이는 실제로 코드의 import 부분을 깔끔하게 만들어주며 여러가지 방법으로 응용하여 사용할 수 있어 많은 사람들이 사용하고 있는 방식이다.

 

하지만 이 파일의 컴포넌트를 dynamic하게 import 하려고 하면 문제가 발생하게된다.

 

import dynamic from 'next/dynamic';
import { useState } from 'react';

import { NormalComponent } from '@/components/test';
import { Button } from '@/components/ui';

const DynamicComponent = dynamic(() => import('@/components/test/DynamicComponent'));
export default function TestPage() {
  const [isRender, setIsRender] = useState(false);

  return (
    <div>
      <Button onClick={() => setIsRender((p) => !p)}>BUTTON</Button>
      <NormalComponent />
      {isRender && <DynamicComponent />}
    </div>
  );
}

만약 위의 코드를 실행한다면 DynamicComponent는 lazy하게 가져와질까?

 

정답은 아니요 이다.

 

일단 매 번 빌드를 하거나 하는것이 아닌이상 대부분의 데브환경에서는 위와 같은 경우 특수한 경우를 제외하고는 dynamic import가 거의 불가능하다고 보는것이 맞을 것 같으며(확실하지는 않음)

 

프로덕션 환경을 기준으로 설명하자면 Webpack과 같은 번들러에서 해당 파일에 SideEffect가 존재한다고 판단하여 트리 쉐이킹 대상에서 제외하여 트리 쉐이킹 이 발생하지 않아 생기는 이슈이다.

 

그렇다면 SideEffect가 뭐기에 이런 일이 발생하는 것이며 번들러는 왜 이 파일을 트리 쉐이킹 대상에서 제외하는것일까?

SideEffect는 CS적인 설명으로는 어떠한 함수에서 결과 값 외에 다른 어떠한 상태를 변경시키는 것 정도로 설명할 수 있을 것 같다.

 

SideEffect는 위의 설명만 보자면 개발자가 의도하지 않은 문제를 발생시킬 수 있는 뭔가 불안한 느낌을 받겠지만 이는 반드시 나쁜것만은 아니며 실제로 개발을 하다보면 많은 경우에 SideEffect를 발생시키는 자신을 확인할 수 있을것이다.

 SideEffect는 개발자가 의도하였을수도 의도하지 않았을수도 있다고 말할 수 있을 것 같다. (아마 대부분의 경우에는 의도하였을것이다.)

 

이러한 SideEffect에 대해 함부로 트리 쉐이킹을 했을때에 몇가지의 문제가 발생할 수 있게된다.

 

SideEffect가 발생하는 파일 혹은 함수에서 어떠한 모듈을 import하여 가져오기만 해도 실행되는 경우도 있으며(polyfill, css 등) 파일 내부에서 개발자가 의도적으로 어떠한 글로벌한 함수를 바꾸거나 하는 경우도 존재할 수 있게된다.

 

이러한 것들을 번들러가 빌드 과정에서 트리 쉐이킹해버린다면 이 파일 혹은 함수가 개발자가 의도하지 않는대로 동작할 가능성이 존재하게 되어버리는 것이다.

 

하지만 번들러는 이러한 것들에 대해 개발자가 의도한 것인지 의도하지 않은것인지에 대해 알 수가 없어 개발자가 따로 파일을 지정해주거나 하지 않는이상은 이렇게 SideEffect가 존재하는 파일 자체를 트리 쉐이킹에서 제외하여 묶어서 번들링을 진행하게되어 결과적으로 트리 쉐이킹이 발생하지 않게 되는것이다.

 

이를 통해 번들러는 번들링 과정에서 개발자가 의도하지 않거나 잘못된 부분에 대한 위험성을 방지할 수 있게 된다.

 

즉 위의 index.ts파일은 NormalComponent만 import하여 가져오더라도 아래에 export되어있는 DynamicComponent가 존재하기에 번들러가 SideEffect의 가능성이 있다고 판단하여 트리 쉐이킹을 발생시키지 않고 묶어서 번들링을 진행하게되기에 lazy loading이 우리가 원하는대로 동작하지 않는다고 말할 수 있다.

 

물론 대부분의 경우에는 트리쉐이킹을 진행하여도 위험성이 존재하지 않도록 개발을 하겠지만 혹시모를 위험성을 방지하는 역할을 하기에 번들러에는 이러한 SideEffect옵션이 존재하게 되는 것이다.

 

 SideEffect의 옵션들은 Package.json 파일에서 핸들링 할 수 있다.  (기본값은 true라고 보면 된다.)

"sideEffects": false

와 같이 선언하여 이 프로젝트는 SideEffect의 위험성이 없는 안전한 프로젝트라고 명시해준다던가

"sideEffects": [ "dist/store.js", "dist/polyfill.js" ]

와 같은 구문을 추가하여 특정 디렉터리만 지정하여 여기에는 사이드 이펙트가 존재하는 파일이라고 명시하여 트리 쉐이킹 대상에서 제외 할 수 있게된다.

/*#__PURE__*/ noSideEffects();

그리고 이런 방식으로 함수의 앞에 직접 지정하여 SideEffect가 없는 Pure한 함수임을 알려주는 방법도 있다.

 

자세한 설정 방법 혹은 동작 방식은 아래의 링크에서 확인할 수 있다.

https://webpack.js.org/guides/tree-shaking/

 

Tree Shaking | webpack

webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

webpack.js.org

 

 

SideEffect옵션은 단순하게 사용하자면 SideEffect 값을 false로 주고 사용할수도 있지만 어떠한 경우에는 트리쉐이킹을 해서 번들링 한 패키지임에도 불구하고 결국 모든 것들이 연결되어 있어 의미없는 트리 쉐이킹을 진행하거나 이로 인해 해결하기 어려운 에러가 발생하는 등의 문제가 발생할 위험성이 있으며 이러한 부분에서 발생한 문제는 경험상 진짜 엄청나게 찾기 어려울 수 있으니 사용하기전에 이러한 위험성에 대해 한 번쯤은 생각해보고 사용하는것을 권장한다.

 

동작 GIF

SideEffect옵션을 False로 하여 프로덕션 환경에서 정상적으로 Code Splitting이 적용된 모습