들어가면서
React 기반 컴포넌트를 개발하고, Vite를 이용한 번들링하는 환경에서 별도의 패키지를 만들었다. 이 글에서는 TodoList 컴포넌트 패키지를 예시로 들어 설명한다.
하나의 패키지에서 개발된 컴포넌트처럼 해당 패키지가 다음과 같이 사용할 수 있도록 구성하는 것이 목표였다.
import { Todolist } from "@test/todolist"
그러나 기대했던 바와 달리, 해당 컴포넌트를 import한 부분에서 CSS가 제대로 적용되지 않았다.
dist 폴더를 확인해보았을 때, index.css가 정상적으로 빌드되어 있었고
패키지를 별도로 실행했을 때, 네트워크 탭에서 index.css가 로드되고 제대로 CSS가 적용되는 것도 확인할 수 있었다.
이를 해결하기 위한 실험해 본 여러가지 방법들을 기술하고자 한다.
1. 번들링 시 CSS 주입 방식: vite plugin을 곁들인
첫 번째 시도는 vite 플러그인을 사용하여 번들링된 index.js 파일에 CSS 임포트 구문을 자동으로 주입하는 방식이었다. 이 방법은 CSS 파일을 번들링된 Javascript 파일에 자동으로 포함시켜 패키지를 import 하는 곳에서 CSS 파일도 불러와야하는 번거러움을 피할 수 있다.
vite-plugin-libcss: https://github.com/wxsms/vite-plugin-libcss
vite-plugin-css-injected-by-js: https://github.com/marco-prontera/vite-plugin-css-injected-by-js
아래 코드에서 보이는 것처럼 여러 개발자들이 만든 vite 플러그인들을 사용하여 간편하게 번들링된 JS에 CSS를 주입할 수 있다.
// vite.config.ts
import react from "@vitejs/plugin-react";
import { type PluginOption, defineConfig } from "vite";
import dts from "vite-plugin-dts";
import libCss from "vite-plugin-libcss";
export default defineConfig({
plugins: [
react(),
dts({
include: ["src"],
rollupTypes: true,
}),
libCss(), // vite-plugin-libcss를 사용해서 dist/index.js에 index.css를 주입하였다.
] as PluginOption[],
build: {
lib: {
entry: "src/index.ts",
fileName: "index",
formats: ["es"],
},
rollupOptions: {
output: {
entryFileNames: "index.js",
chunkFileNames: "chunk-[name].js",
assetFileNames: "[name].[ext]",
},
},
},
});
하지만, 빌드 과정에만 적용되고 로컬 개발 시 CSS 변경사항을 확인하려면 매번 패키지를 다시 빌드해야한다는 문제점이 있다. 이 패키지를 사용할 때는 빌드된 결과물을 통해 CSS가 자동으로 적용되지만, 로컬 개발에서는 HMR(Hot Module Replacement)로 제대로 확인할 수 없기 때문에 즉각 확인이 어렵다.
그리고 코드는 간결했지만, 얼렁뚱땅 넘어간듯한 임시방편에 불과한 방법이다. 플러그인이 아닌 다른 방식을 찾아보자.
2. 번들링 시 CSS 주입 방식 : rollupOptions.output.intro를 곁들인
두 번째 시도는 Vite의 rollup 기능을 활용하여 rollupOptions.output.intro를 사용하여 빌드된 JS 파일에 CSS import 구문을 자동으로 주입하는 방식을 적용했다. Vite의 production 빌드는 rollup을 사용하므로, 해당 옵션을 활용하면 플러그인과 동일한 동작을 기대할 수 있다.
rollup의 output 옵션: https://rollupjs.org/configuration-options/#output-intro-output-outro
vite의 이슈로 올라온 css 주입 관련 이슈들: https://github.com/vitejs/vite/issues/1579
// vite.config.ts
rollupOptions: {
output: {
intro: (chunk) => {
if (chunk.fileName === "index.js") {
return `import "./index.css";`; // 빌드된 JS에 CSS import 구문 주입
}
return "";
},
// 다른 설정들...
}
}
외부 플러그인을 제거하고 vite 기능만으로 CSS 주입이 가능하지만, 여전히 로컬 개발 서버에서 CSS 변경 사항을 확인하기 위해서는 빌드를 해야하는 문제점이 남아있다.
3. package.json "exports" 필드 내 CSS 파일 경로 명시
세 번째 시도는 빌드된 JS 파일에 CSS import 하여 주입하는 방식이 아닌, Node.js 패키지 시스템 기능을 활용하는 것이다. 이 방법의 핵심은 package.json의 exports 필드를 통해서 CSS 파일의 경로를 명시적으로 노출하는 것이다.
위 방식을 vite 공식 문서에서도 권장하고 있다. (공식문서를 먼저 꼼꼼히 보자..!)
https://vite.dev/guide/build.html#css-support
1) 기본 pacakge.json 필드 이해
먼저, exports를 설정하기 전에 package.json의 주요 필드가 어떤 역할을 하고 있는지 이해해야 한다.
- type 필드: 모듈 시스템 지정 ("commonjs" 또는 "module")
- main 필드: CommonJS 환경의 진입점 (require() 호출 시 사용)
- module 필드: ESM 환경의 진입점 (import 문 사용 시 우선 참조)
- types 필드: Typescript를 사용하는 환경에서 번들링 된 라이브러리의 타입을 지원하기 위한 d.ts 경로
2) "exports" 필드의 역할과 구성
다음으로, Node.js v12.7.0부터 도입된 exports 필드는 공식 문서의 정의는 다음과 같다. https://nodejs.org/api/packages.html#exports
The "exports" field allows defining the entry points of a package when imported by name loaded either via a node_modules lookup or a self-reference to its own name.
"exports" 필드는 패키지가 이름으로 import 될 때 또는 node_modules에서 로드될 때 그 패키지의 진입점을 정의하는 것을 허용한다.
즉, epxorts 필드에 패키지에서 외부로 노출할 파일과 경로를 명시적으로 정의할 수 있다. 이를 통해 CSS 파일을 포함한 다양한 리소스를 명시적으로 노출할 수 있다. 좀 더 자세한 설명은 해당 아티클(CommonJS와 ESM에 모두 대응하는 라이브러리 개발하기: exports field)을 참고해보면 좋다.
"exports": {
".": {
"development": "./src/index.ts", // 개발 환경용 진입점
"import": "./dist/index.js", // ESM 환경용 진입점
"require": "./dist/index.cjs", // CommonJS 환경용 진입점
"types": "./dist/index.d.ts" // Typescript 타입 정의
},
"./index.css": "./dist/index.css" // CSS 파일 경로 명시적 노출
},
3) 실제 구현 방법
패키지의 CSS를 반영하는 방법은 다음과 같다.
파일 구성
- src/index.ts: CSS import 구문을 컴포넌트 진입점에 포함
- vite.config.ts: 기본 빌드 설정 (필자는 추가적으로 rollupOptions을 통해 output 파일명 지정하였다.)
- package.json: exports 필드를 통해 CSS 파일 경로 명시적 노출
// src/index.ts
import "@test/todolist/index.css";
import TodoList from "./TodoList.tsx";
export { TodoList };
// package.json
{
"name": "@test/todolist",
"version": "1.0.0",
"type": "module",
"files": ["dist"],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"development": "./src/index.ts", // 개발 환경용 진입점
"import": "./dist/index.js", // ESM 환경용 진입점
"require": "./dist/index.cjs", // CommonJS 환경용 진입점
"types": "./dist/index.d.ts" // Typescript 타입 정의
},
"./index.css": "./dist/index.css" // CSS 파일 경로 명시적 노출
},
// 기타 설정...
}
// vite.config.ts
import react from "@vitejs/plugin-react";
import { type PluginOption, defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
plugins: [
react(),
dts({
include: ["src"],
rollupTypes: true,
}),
] as PluginOption[],
build: {
lib: {
entry: "src/index.ts",
fileName: "index",
formats: ["es"],
},
rollupOptions: {
output: {
entryFileNames: "index.js",
chunkFileNames: "chunk-[name].js",
assetFileNames: "index.[ext]",
},
},
},
});
다른 프로젝트에서 패키지 import사용 시 사용 방법
// 다른 프로젝트에서 사용 시
import { TodoList } from "@test/todolist";
// CSS는 패키지 내부에서 자동으로 import됨
이렇게 설정하면, 위에서 해결되지 못 했던 개발환경에서 이 패키지를 작업할 때 CSS 변경 사항이 즉시 반영되어 확인 가능하다.
4. CSS 변경사항 즉시 반영을 가능하게 하는 exports 필드 설정의 작동 과정
이 설정 방식의 가장 큰 장점은 이전 접근법들에서 해결하지 못했던 개발 환경에서의 실시간 CSS 변경 반영이 가능해진다는 점이다. 패키지 작업 중에 CSS를 수정할 때마다(CSS뿐만아니라개발내역모두) 즉시 그 변화를 확인할 수 있어 비교적 개발 속도가 빨라진다.
HMR(Hot Module Replacement)이 정상 작동하는 두 가지 핵심
1) 개발 환경에서의 소스 파일 직접 참조
"development": "./src/index.ts" 설정은 개발 환경에서 빌드된 파일 대신 소스 파일을 직접 참조하도록 지정한다. 빌드 과정을 거치지 않고 소스 파일을 직접 사용하므로, 수정 후 다시 빌드할 필요 없이 변경사항이 즉시 적용된다.
2) exports 필드를 통한 정확한 CSS 파일 참조 경로 제공
import "@test/todolist/index.css" 구문은 패키지 이름을 통해 CSS 파일을 참조한다.
Node.js의 모듈 해석 알고리즘은 import "@test/todolist/index.css"와 같은 import 문을 만나면 먼저 package.json의 exports 필드를 확인한다.
exports 필드에서 "./index.css"에 매핑된 경로인 "./dist/index.css"를 찾아낸다.
이 상대 경로는 패키지 루트 디텍토리(즉, node_modules/@test/todolist)를 기준으로 하므로, 실제로는 node_modules/@test/todolist/dist/index.css 파일을 최종적으로 참조하게 된다.
따라서, exports 필드에 정의된 경로를 따라 node_modules/@test/todolist/dist/index.css 파일이 올바르게 해석되어 적용된다.
이렇게 CSS 파일 경로가 exports 필드에 명시적으로 노출됨으로써 개발 환경과 프로덕션에서 일관된 방식으로 CSS를 참조할 수 있고, CSS 번들링에 관련된 문제를 패키지 시스템의 표준 메커니즘을 활용하여 깔끔하게 해결할 수 있다.
5. 번외) 위 vite.config.ts 설정 짚고 넘어가자면
vite를 사용하여 라이브러리를 빌드하기 위한 설정으로, 간단한 컴포넌트를 빌드하는 경우를 가정한 구성이다.
// vite.config.ts
import react from "@vitejs/plugin-react";
import { type PluginOption, defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
plugins: [
react(), // react 코드를 처리하기 위한 플러그인 (vite의 공식 플러그인이다.)
dts({ // TypeScript 선언 파일(.d.ts)을 생성하는 플러그인
include: ["src"],
rollupTypes: true, // 여러 타입 정의 파일을 하나로 번들링 (모든 타입을 단일 파일로 통합)
}),
] as PluginOption[],
build: {
lib: {
entry: "src/index.ts", // 라이브러리의 진입점 파일 경로
fileName: "index", // 출력 파일의 기본 이름 (formats와 함께 사용)
formats: ["es"], // ES 모듈 형식으로만 빌드 (다른 옵션: 'umd', 'cjs', 'iife')
},
// vite 내부적으로 production 빌드에서 rollup을 사용하기 때문에 이 옵션을 통해 직접 제어 가능
rollupOptions: {
output: { // 출력 파일 구성
entryFileNames: "index.js", // 메인 진입점 파일의 이름
chunkFileNames: "chunk-[name].js", // 코드 분할 시 청크 파일 이름 패턴
assetFileNames: "index.[ext]", // CSS와 같은 에셋 파일의 이름 패턴
},
},
},
});
마무리
React 컴포넌트 패키지에서 CSS를 올바르게 번들링하는 여러 방법을 살펴보았다. 다양한 접근 방식 중에서 package.json의 exports 필드를 활용한 방법이 가장 깔끔한 방법이었다. 이 방식의 장점은 다음과 같다.
1. 개발 환경에서의 실시간 CSS 변경 반영으로 HMR로 빠른 개발 가능하다.
2. 표준 Node.js 패키지 시스템 활용함으로써 외부 플러그인에 의존하지 않을 수 있다.
3. 개발 환경과 프로덕션 환경에서 동일한 방식으로 작동하기 때문에 통일성 있다.
CSS 번들링을 위한 다양한 방법들을 찾아보면서, 개인적으로 package.json과 vite.config.ts 설정들과 조금 가까워진 시간이었다. 앞으로 패키지 개발 외에도 다양한 케이스에 대한 vite rollupOptions을 더 알아보거나, 다른 오픈소스 프로젝트에서 production 번들링을 어떻게 하는지 조금 더 찾아봐야할 것 같다.
*이상한 내용이 있다면 첨언 부탁드립니다.!
'Development > Web' 카테고리의 다른 글
[Vite] Vite는 어떤 역할을 하고 있는가 (0) | 2024.10.27 |
---|---|
PUB/SUB 구조와 Redis, Kafka를 이해하기 위한 과정 (4) | 2023.10.16 |
[Browser] LocalStorage와 SessionStorage의 차이 (2) | 2023.07.10 |
웹 통신의 큰 흐름(브라우저, 프로토콜 스택, LAN 어댑터, 허브, 스위치, 라우터, 방화벽, 캐시 서버, 웹 서버) (0) | 2023.01.09 |