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>
);
}
일반적으로 dynamic import는 위와 같은 상황에 사용하게된다.
실제로 이를 dev tool에서 확인해보면 Render 버튼을 눌렀을때에 DynamicComponent를 lazy하게 요청하여 렌더시키는 것을 확인할 수 있다.
하지만 이 때에 주의해야 할 점이 있다.
자바스크립트 개발자들이 많이 쓰는 export 방식중 하나인데
대충 이러한 구조로 export를 해주는 방식이다. 이는 실제로 코드의 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
의 가능성이 있다고 판단하여 트리 쉐이킹
을 발생시키지 않고 묶어서 번들링을 진행하게되고 NormalComponent
를 호출하였을 때 DynamicComponent
도 같이 가져와 lazy loading
이 발생하지 않는것이다.
물론 대부분의 경우에는 SideEffect
옵션을 false로 하여 모든 파일에 대해 트리 쉐이킹
을 하도록 해도 정상적으로 동작하도록 개발을 하겠지만 혹시모를 위험성을 방지하는 역할을 하기에 이러한SideEffect
옵션이 존재하게 되는 것이다.
여기서 SideEffect
의 룰들을 Package.json 파일에서 핸들링 할 수 있는데
"sideEffects": false
와 같이 선언하여 모든 파일에 대해 SideEffect
체크를 하지 않도록 설정해 준다던가
"sideEffects": [ "dist/store.js", "dist/polyfill.js" ]
와 같은 구문을 추가하여 어떠한 디렉터리만 SideEffect
를 검사하지 않도록 적용시키는 등의 다양한 옵션 설정이 가능하며
함수의 앞에
/*#__PURE__*/ noSideEffects();
와 같이 지정하여 순수한 함수임을 번들러에게 알려주는 방법도 존재하며 찾아보면 더욱 많은 방법이 존재하는것을 확인할 수 있을것이다.
SideEffect
옵션은 단순하게 사용하자면 그럴수도 있지만 어떠한 경우에는 트리 쉐이킹
한 패키지임에도 불구하고 결국 모든 것들이 연결되어 있어 의미없는 트리 쉐이킹
을 진행하거나 이로 인해 해결하기 어려운 에러가 발생하는 등 사용하기전에 한 번쯤은 생각해보고 사용하는것을 권장한다.
SideEffect옵션을 False로 하여 프로덕션 환경에서 정상적으로 Code Splitting이 적용된 모습