Snowpack, 더 빠른 웹개발을 위한 빌드 시스템

0

개요

Webpack과 같은 기존의 번들러는 커스터마이징의 자유도가 낮은 것도 있지만, 앱의 덩치가 커지면 커질수록 빌드 속도가 느려진다는 문제점이 있습니다. 그 이유는 기존의 번들러는 파일을 하나만 변경해도 번들 파일을 새로 만들기 때문인데, 그래서 더 빠른 웹 개발에 대한 수요가 생겨나게 된 것이죠.

Snowpack은 이런 필요에 의해 개발 된 빌드 시스템으로, Snowpack은 Webpack처럼 복잡하고 무거운 빌드 시스템의 대안으로, 자바스크립트의 네이티브 모듈 시스템을 사용해서 무거운 번들링 작업을 제거하고 프로젝트의 크기와 상관없이 빠른 속도를 제공하도록 만들어진 것이 특징입니다.

즉, Snowpack은 더 빠른 웹 개발을 위한 최신 프론트엔드 빌드 도구라고 할 수 있는데, 기존의 Webpack, Rollup, Parcel과 같은 무겁고 복잡한 번들러 보다 번들에 걸리는 시간을 획기적으로 절약할 수 있습니다. Pika의 벤치마크에 따르면 각 번들러의 번들 소요 시간은 다음과 같습니다.

Benchmarks by Pika

Snowpack은 단순히 더 빠른 번들 시간을 제공하는 것에 그치지 않고, 웹 빌드 시스템에 대한 새로운 접근 방식을 제공하는데, JavaScript의 ESM으로 최초 빌드 시스템을 생성한 이후에는 같은 파일을 다시 빌드하지 않고 변경사항을 브라우저에 즉시 적용할 수 있습니다. 다음은 Snowpack의 주요 특징입니다.

  • 개발서버는 처음에만 의존 모듈 설치 시간을 필요로 하며, 그 다음부터는 50ms 안에 실행된다.
  • 소스가 변경되어도 브라우저 새로고침이 필요하지 않고, 변경 사항이 브라우저에 즉시 반영된다.
  • 코드가 수정되어도 컴포넌트의 상태를 유지하는데, 예를 들어 React에서 useState 등으로 설정된 값을 초기화하지 않음으로서 더 빠른 새로고침이 가능하다.
  • 브라우저 안에서 자바스크립트 네이티브 모듈(ESM)을 사용하여 같은 파일을 두번 빌드하지 않는다.
  • React, Preact, Svelte에서 HMR+Fast refresh를 지원한다.
  • JSX, 타입스크립트, React, Preact, CSS 모듈을 기본 지원하며, 최적화된 빌드와 선호하는 번들러(Webpack, Rollup 등)와 통합할 수 있도록 지원한다.
  • TypeScript, JSX, CSS 모듈 및 Babel, Sass, MDX 등의 기능 확장을 위한 빌드 플러그인을 지원한다.
  • React, Vue, Svelte 같은 다양한 프레임워크(라이브러리)를 지원하고 템플릿을 제공한다.

번들 없는 개발 방식

번들 없는 개발은 개별 파일을 브라우저에 전달하는 아이디어로 Babel, TypeScript, Sass 처럼 자주 사용하는 라이브러리로 파일을 빌드하고, ESM의 importexport 구문으로 브라우저에서 개별적으로 로드하는 개념으로, 이 개념을 적용한 Snowpack은 파일이 변경되면 변경된 단일 파일만 다시 빌드할 수 있습니다.

기존의 Webpack, Rollup과 같은 번들러들은 번들 개발의 개념으로 구현되었는데, 이 방식으로 개발된 번들러로 애플리케이션을 실행하면, 코드를 저장할 때마다 모든 변경사항을 나머지 애플리케이션과 함께 다시 번들하는 과정을 거쳐야 변경사항이 브라우저에 반영되기 때문에, 개발에 있어서는 추가적인 작업과 복잡성이 발생할 수 밖에 없었습니다.

또 기존의 번들러들은 ESM이 지원되지 않았기 때문에 이런 방식으로 구현할 수 밖에 없었는데, 현재는 ESM이 표준으로 지원되고 있기 때문에 번들 개발 방식으로 개발 할 필요가 없어졌다고 볼 수 있습니다. 번들 없는 개발 방식은 기존의 방식에 비해 다음과 같은 장점이 있습니다.

  • 속도가 빠름
  • 예측한 대로 동작함
  • 디버깅이 더 쉬움
  • 개별 파일 캐시가 더 좋음
  • 프로젝트 크기가 개발 속도에 영향을 주지 않음

Snowpack에서 모든 파일은 개별적으로 빌드되고 지속해서 캐시되는데, 파일에 변경사항이 없으면 파일을 다시 빌드하지 않고 브라우저에서 다시 다운로드하지 않는데, 이것이 바로 번들 없는 개발의 핵심이라고 할 수 있습니다.

Snowpack은 50ms 미만의 속도로 즉시 시작할 수 있고, 속도의 저하 없이 무한히 큰 프로젝트로 확장할 수 있는데, 참고로 기존 번들러로 큰 규모의 앱을 빌드하는 경우라면 처음 개발 시작 시간이 30초 이상 걸리는 것이 일반적입니다.

의존성 모듈 처리

NPM 패키지는 대부분 웹에서 직접 실행할 수 없는 CommonJS를 사용하고 있기 때문에, 브라우저에서 직접 실행할 수 있는 ESM의 importexport 구문으로 애플리케이션을 작성해도 NPM 패키지를 가져올 때는 번들러가 동작하게 됩니다.

하지만 Snowpack은 의존성 모듈을 처리할 때 조금 다른 접근 방식을 취하는데, 전체 애플리케이션을 번들로 묶는 대신 의존성을 별도로 처리합니다.

  • Snowpack이 애플리케이션을 스캔함
  • node_modules 디렉토리에 설치된 의존성 모듈을 확인
  • 모든 의존성 모듈을 개별 JavaScript 파일로 변환(react => react.js)
  • 변환된 각 파일은 브라우저에서 직접 실행하고 ESM import 구문을 통해 가져옴
  • 의존성 모듈은 따로 변경하지 않는 이상 변경사항이 없기 때문에 다시 빌드하지 않음
의존성 모듈 Javascript 변환
node_modules/react  =>  http://localhost:8080/web_modules/react.js
node_modules/vue    =>  http://localhost:8080/web_modules/vue.js
의존성 모듈 import
<body>
  <script type="module">
    import React from 'react'
    import Vue from 'vue'

    console.log(React)
    console.log(Vue)
  </script>
</body>

의존성 모듈 테스트

Snowpack은 Skypack이란 사이트를 통해서 ESM을 지원하는 패키지를 쉽게 검색하고 테스트할 수 있습니다.

기본 구조

Vue.js 템플릿(@snowpack/app-template-vue)을 기준으로 Snowpack의 기본 구조를 알아보겠습니다. 우선 다음의 두 가지 중 하나의 명령어로 Snowpack을 설치해 보세요.

npm으로 설치하기
npm install --save-dev snowpack
yarn으로 설치하기
yarn add --dev snowpack

Snowpack를 설치했다면 다음의 명령어로 Vue 템플릿을 설치할 수 있습니다.

vue 템플릿 설치하기
npx create-snowpack-app {folder-name} --template @snowpack/app-template-vue

Vue 템플릿을 잘 설치했다면 다음과 같은 폴더 구조를 볼 수 있습니다.

snowpack.config.js

snowpack.config.js는 Snowpack의 설정파일입니다. 이 파일에서는 여러 옵션을 사용해서 프로젝트를 설정할 수 있는데, 사용할 수 있는 모든 옵션에 대한 자세한 내용은 CONFIG에서 확인할 수 있고, mount 연결 옵션에 대해서는 CONFIG.MOUNT에서 확인할 수 있습니다.

mount
Snowpack 빌드에 사용할 ‘현재 경로’와 빌드 결과로 연결할 ‘빌드 경로’를 지정
plugins
Snowpack에서 사용할 플러그인 목록을 지정

mount의 사용법은 다음과 같습니다.

mount: {
  현재_경로: 빌드_경로
}

생성된 프로젝트에서 public 폴더를 루트(/)로 사용하고, src 폴더를 /_dist_ 경로로 사용하고 싶다면 다음 처럼 설정하면 됩니다.

snowpack.config.js 설정 예제
module.exports = {
  mount: {
    public: '/',
    src: '/_dist_'
  },
  plugins: [
    '@snowpack/plugin-vue'
  ]
};

package.json

package.json 파일은 배포한 모듈 정보를 담기위해 만들어진 파일입니다. 기본적으로 CommonJS의 명세를 충실히 따르는 JSON 형식으로 직접 작성할 수도 있고 npm init 명령을 통해 자동으로 생성할 수도 있습니다.

노드로 작성하는 애플리케이션도 package.json 파일로 관리할 수 있는데, 꼭 확장 모듈 형태로 배포하기 위한 것이 아니더라도 애플리케이션을 개발할 때 package.json 파일을 이용하면 사용하는 확장 모듈에 대한 의존성 관리가 가능하기 때문에 편리합니다.

Snowpack의 Vue 템플릿을 설치하면 기본적으로 snowpack dev 명령으로 개발 서버를 준비하고, snowpack build 명령으로 제품을 빌드 할 수 있도록 scripts 명령어가 설정되어 있는데, 이 속성을 yarn dev, yarn serve와 같이 원하는 명령어로 변경해서 사용할 수도 있습니다.

다음 코드에는 프로젝트에서 Vue를 사용하는 경우에 개발에서만 필요한 개발 의존성인 snowpack과 Vue 구성을 위한 @snowpack/plugin-vue를 추가하고, 일반 의존성으로 실제로 사용할 애플리케이션인 Vue를 추가한 예제입니다.

package.json 설정 예제
{
  "scripts": {
    "dev": "snowpack dev",
    "build": "snowpack build"
  },
  "devDependencies": {
    "@snowpack/plugin-vue": "^2.2.4",
    "snowpack": "^2.15.1"
  },
  "dependencies": {
    "vue": "^3.0.2"
  }
}

public/index.html

Snowpack에서 제공하는 Vue 템플릿의 기본 진입점은 src/index.js입니다. 이 경로는 snowpack.config.js에 지정되어 있는데, public/index.htmlindex.js 파일이 추가되어 있는 것을 확인할 수 있습니다.

public/index.html
<body>
  <div id="root"></div>
  <script type="module" src="/_dist_/index.js"></script>
</body>

/build

snowpack build

# or

npm run build

프로젝트를 빌드하면 build/ 디렉토리가 생성되는데, build/ 디렉토리의 구조와 각 파일, 디렉토리의 역할은 다음과 같습니다.

  • _snowpack: Snowpack의 메타 데이터(HMR, ENV 등)가 출력되는 디렉토리로 buildOptions.metaUrlPath 옵션으로 이름 수정 가능
  • dist: src/에 지정된 빌드 후 파일이 출력되는 디렉토리
  • pkg: 프로젝트에서 사용할 웹 모듈(라이브러리) 디렉토리

빌드와 관련된 옵션과 자세한 내용은 build-pipeline 페이지에서 확인할 수 있습니다.

주요 구성옵션

snowpack.config.js 파일의 주요 구성옵션은 다음과 같습니다.

옵션 설명 타입
mount
빌드에 포함될 디렉토리와 연결될 URL을 지정
object
plugins
기능을 확장할 플러그인을 연결
string[] | [string, object][]
alias
앱에서 사용할 경로 별칭 지정
object
devOptions
개발 서버의 작동 방식을 지정
object
buildOptions
최종 빌드를 처리하는 방법을 지정
object
installOptions
외부 모듈을 처리하는 방법을 지정
object
testOptions
테스트 환경의 작동 방식을 지정
object
proxy
개발 서버의 프록시 동작을 지정
object
exclude
지정된 파일을 빌드에서 제외
string[]
install
자동으로 감지되지 않는 설치할 모듈을 지정
string[]
extends
지정된 별도의 기본 구성 옵션을 상속해 병합
string

devOptions

devOptionssnowpack dev를 통한 개발 서버의 작동 방식을 지정하는 옵션입니다.

snowpack.config.js
module.exports = {
  devOptions: {
    port: 8080, // 개발 서버를 실행할 포트 번호
    fallback: 'index.html', // SPA인 경우 제공할 모든 사용자 경로
    open: 'default', // 새 브라우저 탭에 개발 서버를 열기, "default" | "none" | "BROWSER_NAME"
    output: 'dashboard', // 콘솔의 출력 모드를 지정, "stream" | "dashbaord"
    hostname: 'localhost', // 브라우저가 열리는 호스트 이름
    hmr: true, // Hot Module Replacement(HMR, 수정사항을 즉시 반영) 활성화
    hmrErrorOverlay: true, // HMR 활성화시 자바스크립트 오류 표시 여부
    secure: false, // HTTP2 활성화 상태에서 HTTPS 사용 여부
    // out: 'build' // Deprecated
  }
}

buildOptions

buildOptionssnowpack build를 통해서 최종 빌드를 처리하는 방법을 지정하는 옵션입니다.

snowpack.config.js
module.exports = {
  buildOptions: {
    out: 'build', // 최종 빌드를 출력하는 로컬 디렉토리 이름
    baseUrl: '/', // 제품 모드의 기본 URL 지정, 현재 앱이 하위 디렉토리로 배포되는 경우 유용
    clean: false, // 빌드 전 기존 데이터 제거
    metaUrlPath: '_snowpack', // HMR 및 ENV 등의 정보를 출력할 디렉토리 이름
    sourceMap: false // 소스 맵 사용 여부
  }
}

installOptions

installOptions은 설치하는 외부 모듈을 처리하는 방법을 지정하는 옵션입니다.

snowpack.config.js
module.exports = {
  installOptions: {
    dest: 'web_modules', // 사용하는 웹 모듈이 저장될 디렉토리 이름
    sourceMap: false, // 설치된 모듈의 소스맵 사용
    env: {}, // 환경 변수 지정
    treeshake: false, // 의존성 모듈의 트리쉐이킹 여부(제품 모드만 동작)
    installType: false, // 모듈 설치 시 타입 정의를 함께 설치(타입스크립트)
    namedExports: [], // `Uncaught SyntaxError: The requested module '/web_modules/XX.js' does not provide an export named 'YY'` 에러가 발생하는 경우, `namedExports: ['XX']`
    externalPackage: [], // 무시할 외부 모듈의 목록, E.g. 'fs'
    packageLookupFields: [], // package.json의 "내보내기" 매핑에 대한 사용자 정의 조회 필드를 설정
    rollup: { // Snowpack이 내부적으로 사용하는 Rollup을 더욱 세부적으로 제어하기 위한 옵션
      plugins: [], // Rollup 플러그인을 지정
      dedupe: [], // 중복 번들을 방지하기 위한 외부 모듈 이름을 지정(rollup-plugin-node-resolve)
      context: [] // 최상위 `this`를 지정, 최상위 변수를 참조하는 레거시 CJS 모듈의 오류 해결에 유용
    },
  }
}

Setting Example

snowpack.config.js
/** @type {import("snowpack").SnowpackUserConfig } */
const path = require("path");

module.exports = {
  mount: {
    public: "/",
    src: "/_dist_",
  },
  plugins: [
    "@snowpack/plugin-react-refresh",
    "@snowpack/plugin-dotenv",
    "@snowpack/plugin-typescript",
    "@snowpack/plugin-webpack",
  ],
  installOptions: {
    polyfillNode: true,
  },
  devOptions: {
    port: 3000,
  },
  buildOptions: {
    sourceMaps: true,
  },
  proxy: {
    /* ... */
  },
  alias: {
    __mocks__: path.join(__dirname, "src/__mocks__"),
    component: path.join(__dirname, "src/component"),
    constants: path.join(__dirname, "src/constants"),
    hook: path.join(__dirname, "src/hook"),
    page: path.join(__dirname, "src/page"),
    store: path.join(__dirname, "src/store"),
    style: path.join(__dirname, "src/style"),
    types: path.join(__dirname, "src/types"),
    styles: path.join(__dirname, "src/styles"),
    utils: path.join(__dirname, "src/utils"),
    vendor: path.join(__dirname, "src/vendor"),
  },
  testOptions: {
    files: <(spec|test).*">"**/*.,
  },
};

plugins

  • @snowpack/plugin-react-refresh: HMR 사용을 위한 플러그인
  • @snowpack/plugin-dotenv: .env 파일에 있는 환경 변수 파일을 import.meta.env 객체를 통해 접근할 수 있는 플러그인
  • @snowpack/plugin-typescript: // 타입스트립트 소스를 자바스크립트로 변환해주는 플러그인
  • @snowpack/plugin-webpack: // Snowpack은 번들링 기능을 포함하고 있지 않기 때문에 서비스 배포를 위해선 소스 압축을 위해 webpack의 기능을 활용

polyfillNode

polyfillNode는 소스에서 path 같은 Node.js 모듈을 사용할 경우에 필요한 속성입니다.

proxy

proxy는 개발 서버의 프록시를 설정하는 속성입니다. 예를 들어 /api로 접근하면 https://www.youdad.kr/api/v2/로 연결시켜주는 등의 기능을 제공합니다.

alias

alias는 다음처럼 소스 파일을 절대 경로로 접근하려고 할 때 설정하는 속성입니다.

절대 경로 설정하기
import useTypedSelector from "hook/useTypedSelector";

testOptions

testOptions는 테스트 파일의 위치를 지정하는 속성으로, 테스트 파일 관련 모듈은 개발 서버 실행 시에만 설치되고, 빌드 작업에는 포함되지 않습니다.

템플릿

공식 템플릿

Snowpack은 주요 프레임워크의 공식 템플릿을 지원하고 있어서 쉽게 시작할 수 있고, Snowpack을 이해하는 데도 많은 도움이 될 수 있습니다. 현재 지원하고 있는 주요 템플릿은 Snowpack의 공식 github 페이지에서 확인 할 수 있습니다. 다음은 Snowpack의 대표적인 공식 템플릿입니다.

설치 방법은 다음과 같습니다.

공식 템플릿 설치하기
npx create-snowpack-app {new-dir} --template @snowpack/{app-template-NAME} [--use-yarn | --use-pnpm | --no-install | --no-git]

공식 템플릿으로 프로젝트 만들기

Svelte를 기준으로 공식 템플릿을 이용한 프로젝트를 만드는 과정은 다음과 같습니다.

  • 프로젝트 생성
  • 관련 package 설치
  • package.json에 script 추가
  • snowpack.config.js 파일 생성
  • index.html에 스크립트 파일 경로 생성
  • App.svelte 파일 생성
  • main.js 파일 생성
프로젝트 생성
npm init -y

프로젝트를 생성한 후에는 다음과 같은 패키지들을 설치해 주면 됩니다.

  • snowpack svelte @snowpack/plugin-svelte(템플릿 구성용)
  • @snowpack/plugin-optimize(html, js 난독화 및 최적화)
  • @snowpack/plugin-babel(자바스크립트 전처리기)
  • babel-plugin-transform-remove-console(배포시 console을 모두 지운 상태로 배포)
  • @snowpack/plugin-sass(css 전처리기)
관련 package 설치
npm i -D snowpack svelte @snowpack/plugin-svelte
npm i -D @snowpack/plugin-optimize
npm i -D @snowpack/plugin-babel
npm i -D babel-plugin-transform-remove-console
npm i -D @snowpack/plugin-sass
package.json에 script 추가
"scripts": {
  "dev": "snowpack dev",
  "build": "snowpack build"
}
snowpack.config.js 파일 생성
module.exports = {
  mount: {
    public: '/',  // root 폴더
    src: '/dist'  // build된 결과 파일 위치
  },
  plugins: [
    '@snowpack/plugin-svelte',
    '@snowpack/plugin-optimize',
    '@snowpack/plugin-babel',    // 바벨
    '@snowpack/plugin-sass'      // Sass
  ],
  alias: {
    '~':'./src'   // 경로 별칭
  },
  devOptions: {
    //port: 8080,
    open: 'none'  // 서버 새로 시작 시 브라우저 열기기능 막음
  }
}
index.html 파일에 스크립트 파일 경로 생성
<script type="module" src="/dist/main.js"></script>
App.svelte 파일 생성
<script>
    let name = 'hello Svelte!'
</script>

<h1>{name}</h1>

<style lang="scss">
  $color: red;
    h1 {
      color: $color;
    }
</style>
main.js 파일 생성
import App from './App.svelte'

const app = new App({
  target: document.body
})

export default app

Svelte Custom Template

공식템플릿은 아니지만 개발자가 직접 만들어 배포한 Snowpack 기반의 Svelte 템플릿도 있습니다. 개발을 위해 구성한 템플릿이기 때문에 별도의 구성 없이 바로 프로젝트를 시작할 수 있어서 편리하게 사용할 수 있는데, 이 커스텀 템플릿은 크게 다음과 같은 것들을 지원합니다.

  • Svelte
  • TypeScript
  • SCSS
  • Autoprefixer/PostCSS
  • Web test runner
  • Chai
  • Reset.css

cmd에서 다음의 명령어로 설치할 수 있습니다.

svelte custom template 사용하기
# Install template
npx degit ParkYoungWoong/svelte-snowpack-template {DIR_NAME}

# Change directory
cd {DIR_NAME}

# Install dependencies
npm i

# Start dev server
npm run dev

타입스크립트를 사용하는 경우에는 다음과 같이 작성합니다.

타입스크립트 사용하기
<script lang="ts">
  let count: number = 0
</script>

SCSS를 사용하는 경우에는 다음과 같이 작성합니다.

SCSS 사용하기
<style lang="scss">
  $color--primary: royalblue;
  h1 {
    color: $color--primary;
  }
</style>

답글 남기기