플러그인으로 DX 개선하기 with Webpack

Doeunnkimm·2024년 11월 30일
3

FE log

목록 보기
7/7
post-thumbnail

최근에 플러그인을 통해서 DX를 개선한 경험이 있어 이를 공유해보려고 합니다. 방법이 번뜩 생각이 났고, 비교적 간단한 작업이 후딱 반영했는데요. 이번 글에서 경험을 공유하고, 또 저도 더 자세히 알아보려고 합니다. 🚀


어떤 상황?

.md 파일들을 이관해야 하는 작업이 필요했어요. 문서화를 위해 사용하고 있는 도구는 다행히도 mdmdx로 변환하는 기능이 내장되어 있긴 하더라구요.

그런데, MDX는 Markdown과 JSX(JavaScript XML)를 결합한 포맷으로, Syntax 오류가 발생할 수 있습니다. 예를 들면 JSX 태그가 제대로 닫히지 않았다는 이유로 말이죠.

그래서 아래와 같은 상황이 다릅니다.

// md: ✅ no problem
이 문장은 줄바꿈을 <br> 태그로 표현합니다.
// mdx: 🚨 error
이 문장은 줄바꿈을 <br> 태그로 표현합니다.

이미 작성되어 있는 많은 파일들에서 <br> 혹은 <Br>이 이곳저곳에서 사용되고 있었어요. 그래서 아래와 같은 에러를 마주했습니다.

정리해보자면 상황은 이렇습니다.

  1. md 파일을 이관해옵니다.
  2. 사용하고 있는 도구가 알아서 md → mdx로 바꿔줍니다.
  3. 그런데 md에서 사용하고 있던 문법들이 mdx에 와서 터지고 있습니다.

일차원적인 해결 방법

위 문제를 해결하기 위해서는 아주 일차원적인 방법이 생각나기도 했어요.

  • 손수 수정하기 (ㅋㅋ)
  • 파일을 돌면서 일괄적으로 바꿔주는 script 추가하기
  • 앞으로는 이런 일을 방지하기 위해 mdx 문법을 지키기

위 방법들은 비용이 조금씩 반복적으로 발생할 것 같았어요. 왜나면, 앞으로 문서를 같이 작성하게 될 사람들은 MD에 이미 익숙하기 때문에, 앞으로도 동일하게 문서를 작성할 경우 2번도 반복해야 하며, 3번도 생산성을 저하시키는 요인이 될 것 같았습니다.


저는 이렇게 해결했어요

제가 원하는 건 이렇습니다.

  • MD에 익숙한 사람들도 그대로 사용할 수 있는 환경. 즉, 신경 안 쓸 수 있는 환경

이러한 환경 차이를 중간에서 처리해주는 방식을 사용하면 어떨까 생각이 들었어요. 기존 MD 파일을 별도의 수정 없이 MDX 환경에서 사용할 수 있도록 만들어줄 수 있지 않을까 했습니다.

플러그인 추가

우선 발견한 문제는 <br>이 문제였어서, 이를 transform하는 과정을 추가하면 될 거라고 생각했어요.

그래서 제가 선택한 방법은, 플러그인을 추가하는 것이에요.

사용하고 있는 문서화 도구는 Next.js 기반이기 때문에 next.config.jswebpack config를 쉽게 추가할 수 있었습니다. (Next.js는 기본적으로 Webpack을 통해 번들링)

// lib/replaceBr.js
const replaceBr = async() => {
  const { default: visit } = await import('unist-util-visit');

  return (tree) => {
    visit(tree, 'html', (node) => {
      node.value = node.value.replace(/<br\s*\/?>/gi, '<br />');
    });
  };
}

module.exports = replaceBr;
// next.config.js
const replaceBr = require('./lib/replaceBr');

const nextConfig = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.md$/,
      use: [
        {
          loader: 'remark-loader',
          options: {
            remarkOptions: {
              plugins: [replaceBr],
            },
          },
        },
      ],
    });
    return config;
  },
}
module.exports = nextConfig

MD 파일을 Remark를 사용해 파싱을 하고, 제가 추가한 플러그인이 적용될 수 있도록 설정했어요.

결과적으로 아래와 같이 이전과는 다르게 빌드에 문제없이 성공할 수 있었어요.

최근 회사에서 플러그인을 추가해서 어떤 문제를 해결했다고 들었던 것을 출발로, 번뜩 생각이 났던 것이였어요. 아이디어와 gpt만으로 코드를 작성하여 빠르게 반영할 수 있었지만, 더 깊은 내용을 이해하기 위해 추가적으로 알아보려고 합니다.


Webpack

Webapck은 번들러입니다. 번들러는 모듈과 리소스를 번들로 묶어 효율적으로 관리하고 최적화하는 역할을 해요.

번들링 과정

번들링(bundling)이라는 건, 여러 개의 모듈, 리소스들을 하나 또는 여러 개의 출력 파일로 묶는 과정입니다. 주로 JavaScript, CSS, 이미지와 같은 각종 리소스들을 묶어서 효율적으로 배포할 수 있도록 최적화하는 작업입니다.

🤔 효율적 배포라는 건 뭘까?

배포되어야 하는 파일들의 크기가 클수록 이를 다운로드하고 실행하는 데 더 많은 시간이 소요됩니다. 번들러가 수행하는 효율적인 배포를 위한 작업들에는 즉, 번들링 하는 과정에서 수행하는 작업에는 아래와 같은 것들이 있어요.

  • 번들링: 여러 개의 작은 파일을 하나로 합쳐 네트워크 요청수 절약
  • 파일 크기 최적화: 불필요한 공백, 주석, 줄바꿈 등을 제거하여 파일 크기 최소화 → 압축하여 다운로드 속도 개선 및 네트워크 대역폭 절약
  • 코드 스플리팅: 여러 개의 작은 번들로 분할되어, 필요한 코드만 로드할 수 있도록 → 초기 로딩 시간 단축
  • 이미지 최적화: 이미지 파일을 압축하거나, 포맷을 변경하여 파일 크기 절약 가능. 이미지 최적화 플러그인(ex. image-webpack-loader, url-loader)을 사용하여 압축 및 파일 크기 절약 가능
  • Tree Shaking: 사용되지 않는 코드를 제거 → 모듈 시스템을 분석하여 실제로 사용되지 않는 코드를 제거하여 번들 크기 절약

위와 같은 작업을 거쳐서 번들 파일을 생성하는 것인데, 하나씩 자세히 알아보려고 합니다.

번들링: 적당한 번들링을 위한 전략

🤔 적당히 번들링한다는 건 뭘까?

너무 많이 쪼개거나 너무 많이 묶는 것은 모두 성능에 부정적인 영향을 줄 것 같아요. 추상적이지만.. 적당히 번들링하는 전략이 필요할 것 같았습니다.

보통 적절한 번들링을 위한 전략에는 다음과 같은 것들이 있어요.

⚔️ 코드 스플리팅

https://webpack.js.org/guides/code-splitting/

엔트리 포인트에서부터 모듈을 탐색하여 의존성을 파악하고, 필요한 모듈을 추적해서 번들로 묶습니다. 예를 들어 A.js에서 B.jsC.js를 import해서 사용한다면, 번들에 포함되도록 추가하고 각각의 모듈들을 하나의 번들 파일 또는 여러 개의 파일로 묶어서 최종 결과물을 만들어요.


📦 모듈 공유하기

dependOn이라는 옵션을 통해 청크 간 모듈을 공유할 수도 있다고 합니다. 중복을 줄이기 위해 모듈을 공유하도록 하는 것인데요.

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    index: {
      import: './src/index.js',  // 'index' 엔트리 포인트
      dependOn: 'shared',        // 'shared' 모듈에 의존
    },
    another: {
      import: './src/another-module.js',  // 'another' 엔트리 포인트
      dependOn: 'shared',        // 'shared' 모듈에 의존
    },
    shared: 'lodash',  // 'shared'는 'lodash' 라이브러리로 지정
  },
  output: {
    filename: '[name].bundle.js',  // 번들 파일 이름은 엔트리 포인트 이름을 따름
    path: path.resolve(__dirname, 'dist'),  // 결과물은 'dist' 폴더에 저장
  },
};

위와 같이 sharedlodash를 공유하도록 설정할 수 있어요. index와 another 모두 lodash를 의존하고 있기 때문에 Webpack은 이를 별도의 번들로 추출하고 중복을 피합니다. lodash는 shared.bundle.js라는 별도의 파일로 추출되고 이를 참조하는 형태로 구성되게 됩니다.

라이브러리를 명시한다고 해서 라이브러리 전체 모듈이 번들되는 것은 아니고, 트리 쉐이킹에 의해 사용되지 않는 부분은 제거되고 공통 코드만 포함된다고 합니다. 그럼 lodash 같이 CJS 모듈 시스템을 지원하는 라이브러리 즉 트리 쉐이킹이 어려운 라이브러리는 전체가 번들에 포함되게 되는 것이겠네요.


🌪️ dymamic import

필요한 시점에만 모듈을 로드할 수 있도록 번들을 분리하는 기법을 말합니다. 엔트리 파일에서 사용은 되지만, 초기 렌더링 시에는 실질적으로 사용되지 않는다면, dynamic import 번들로 분리하여 초기 렌더링 속도를 개선할 수 있습니다. 즉, 당장 필요하지 않으니까 나중에 로드할게 같은 것이죠.

주의할 점도 있을 것 같아요. 아무래도 동적 임포트는 모듈을 분리하는 것이므로 네트워크 요청이 늘어납니다. 너무 많은 사용은 오히려 네트워크 응답 시간을 느려지게 하여 사용자가 기다려야 하는 상황이 일어날 수 있음을 주의해야겠습니다.

최적화: 최적화는 어떻게?

https://webpack.js.org/configuration/optimization/

Webpack에서 소개하고 있는 최적화 전략이 정말 많더라구요.

이 중에서 제가 흥미로웠던 최적화 기법들 몇 가지를 소개해보려고 합니다.

🗽 chunkIds

  • 생성되는 청크의 이름을 결정하는 설정
  • 기본적으로는 파일 일므에 해당하는 고유 식별자(숫자, 해시 값, ...)를 가진다
  • named로 설정할 경우, 명시적인 이름을 할당 → 디버깅 시 용이
  • deterministic으로 설정할 경우, 빌드마다 동일한 ID 할당 가능 → 캐시 관리가 중요하거나 디버깅 환경에서 예측 가능하도록 설정하고 싶을 때 사용

👝 minimize, minimizer

  • 코드 압축을 담당하는 설정
  • 자동으로 코드 최소화를 할지 여부를 결정 (기본값 true) → TerserPlugin(기본 플러그인)을 사용해 JavaScript 파일을 최소화
  • minimizer에는 최소화할 때 사용할 플러그인 설정 가능
  • 디버깅 용도로 false로 설정하여 분석 활용

❄️ sideEffects

  • Webpack은 트리 쉐이킹을 통해 사용되지 않는 코드를 제거하는데
  • 일부 모듈은 side effect를 가질 수 있기 때문에, 모듈을 제거하지 않도록 설정 가능
  • true로 설정할 경우, side effect를 가질 수 있는 코드를 트리쉐이킹 하지 않음으로써 보호
  • package.json에서도 sideEffects 필드를 통해 설정 가능

Webpack이 판단하는 side effect가 없는 코드는 즉, 순순히 트리 쉐이킹을 해주는 코드는 순수 함수만 포함된 모듈이나 상태를 변경하지 않는 모듈이라고 해요. (빡빡함) 그래서 판단하에 직접적으로 설정을 해주면 번들 크기 면에서 이점을 얻을 수 있어 보입니다.

🎄 Tree Shaking: 오해와 진실

트리 쉐이킹은 정적 분석을 통해 사용되지 않는 부분을 찾아서 제거한다는 간단한 원리에 대한 코멘트를 남기고..

이번에는 트리 쉐이킹에 대한 오해와 진실에 대해 정리해보려고해요. 최근 회사에서 트리 쉐이킹 관련해서 작업이 있었는데 헷갈리더라구요. 🤔 ㄷ..되는건가?


1️⃣ export ... = Object.assign(...) → ❌

서브 컴포넌트를 제공하기 위해 Object.assingn를 통해 통합해서 제공하기도 하는데요.

export const Foo = Object.assign(FooImpl, {
  A: FooAComponent,
  B: FooBComponent,
  C: FooCComponent,
})

해당 모듈에서 이미 모든 컴포넌트를 import 해서 객체를 만들었다는 것으로, 전체 객체를 하나의 단위로 취급하기 때문에 정적 분석 시 모든 모듈을 번들할 수 밖에 없습니다. 즉, 트리 쉐이킹을 활용하지 못하는 경우라고 할 수 있어요.


2️⃣ export * as Foo from 'Foo' → ⭕️ (최신)

아래와 같은 케이스는 어떨까요?

// Foo.tsx
import { FooA } from 'FooA'
import { FooB } from 'FooB'
import { FooC } from 'FooC'

export { FooA as A }
export { FooB as B }
export { FooC as C }

// index.ts
export * as Foo from 'Foo'

위와 같은 구조는 개별적으로 export하고, index.ts 에서 Foo.tsx에서 export된 모든 내용을 Foo라는 네임스페이스로 다시 export하고 있는 구조에요. 사용처에서는 아래와 같이 쓸 수 있어요.

import { Foo } from 'lib-foo'

<Foo.A />
<Foo.B />
<Foo.C />

여러 모듈을 하나의 네임스페이스로 묶은 방식으로, 각 모듈은 독립적인 모듈로 처리됩니다. 따라서 정적 분석이 가능하여 트리 쉐이킹이 가능합니다.

찾아보니, webpack4 이전에는 불가능했다고 해요. 정확하게 이제는 됩니다!라는 포스팅을 못찾아서..직접 해보았습니다. 결과는 아래와 같아요.

아래는 결과입니다.

트리 쉐이킹이 된 것을 확인할 수 있었습니다 👍🏽


3️⃣ import * as React from 'react' → ⭕️

이 또한 네임스페이스로 불러오는 방식으로, 각 모듈을 독립적인 모듈로 처리가 가능하기 때문에 트리 쉐이킹에 문제가 없습니다. (관련 포스팅)

plugin의 동작 원리

Webpack에서 플러그인은 빌드 프로세스를 확장하거나 커스터마이징하는 데 사용되는 도구에요. 플러그인은 특정 작업을 수행하는데, 주로 빌드 최적화, 번들링, 코드 스플리팅, 파일 시스템 조작 등을 처리할 수 있습니다.

그래서 플러그인이 실행되는 시점을 설정할 수도 있고, 이를 통해 특정 시점에 원하는 작업을 추가할 수도 있습니다.

Webpack이 처리할 수 있도록 로더를 통해 JS 모듈로 변환해서 처리합니다. Webpack은 JS 파일만 처리할 수 있기 때문에 css-loader나 style-loader 와 같은 로더를 통해 변환을 하는 것이죠.


끝으로

우연한 기회로 플러그인을 통해 DX를 개선한 경험이 생겨서 공유하게 되었는데요! 하다보니 Webpack의 전반적인 내용까지 공부하게 되었습니다 😁

이 경험을 통해, 그동안 코드에서 에러가 발생하면 그냥 맞춰서 돌아가게 만드는 것에서 벗어나, 이제는 필요할 때 빌드 프로세스를 활용해 변환을 통해 문제를 해결할 수 있다는 자신감이 조금.. 생긴 것 같았다는 말을 남기고 글을 마무리해보려고 합니다 :hooray: ! 👋

profile
개발자와 사용자 모두의 눈👀을 즐겁게 하는 개발자가 되고 싶어요 :) 👩🏻‍💻

0개의 댓글