ํ ๋ฒ ์ง์ ๋ฒ๋ค๋ฌ 5๊ฐ ์ง์ํ๊ธฐ โ unplugin์ผ๋ก ๋ง๋ ์ ๋๋ฒ์ค ํ๋ฌ๊ทธ์ธ
react-code-locator๋ฅผ ๋ง๋ค ๋ ํ์ค์ ์ธ ๊ณ ๋ฏผ์ด ํ๋ ์์๋ค. ์์ฆ React ํ๋ก์ ํธ์ ๋ฒ๋ค๋ฌ๋ ์ ๊ฐ๊ฐ์ด๋ค. Vite, Next.js(Webpack), Rollup, esbuild, Rspackโฆ. ์์ค์ ์์น ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ฌ์ผ๋ ค๋ฉด ๋ฒ๋ค๋ฌ์ ๋น๋ ํ์ดํ๋ผ์ธ์ ๋ผ์ด๋ค์ด์ผ ํ๋๋ฐ, ๋ฒ๋ค๋ฌ๋ง๋ค ํ๋ฌ๊ทธ์ธ API๊ฐ ๋ค๋ฅด๋ค.
์ด๊ฑธ ๋ค์ฏ ๋ฒ๋ก ๊ฐ๊ฐ ๊ตฌํํ๋ฉด? ์ง์ฅ์ด๋ค. ๊ทธ๋์ unplugin ์ ์ผ๋ค. ์ด ๊ธ์ "๋ณํ ๋ก์ง ํ๋๋ก ๋ฒ๋ค๋ฌ ๋ค์ฏ ๊ฐ๋ฅผ ์ปค๋ฒํ" ์ด์ผ๊ธฐ์, ๊ทธ ๊ณผ์ ์์ ๋ง๋ ํจ์ ๋ค์ ์ ๋ฆฌํ๋ค.
๋ฒ๋ค๋ฌ ํ๋ฌ๊ทธ์ธ์ ๊ณตํต์
์ด๋ฆ๊ณผ ํํ๋ ๋ฌ๋ผ๋, ๋ชจ๋ ๋ฒ๋ค๋ฌ ํ๋ฌ๊ทธ์ธ์ด ๊ณตํต์ผ๋ก ์ ๊ณตํ๋ ํ ์ด ์๋ค.
"ํ์ผ ํ๋์ ์์ค ์ฝ๋๋ฅผ ๋ฐ์, ๋ณํํด์ ๋๋ ค์ค๋ค" (
transform)
Vite/Rollup์ transform(code, id), Webpack/Rspack์ loader, esbuild๋
onLoadโฆ ์ด๋ฆ๋ง ๋ค๋ฅผ ๋ฟ ๋ณธ์ง์ ๊ฐ๋ค. unplugin์ ์ด ๊ณตํต์ ์ ํ๋์ ํต์ผ๋
ํ
์ผ๋ก ์ถ์ํํ๊ณ , ๊ฐ ๋ฒ๋ค๋ฌ์ฉ ์ด๋ํฐ๋ฅผ ์๋์ผ๋ก ๋ง๋ค์ด ์ค๋ค.
ํต์ฌ: transform ์ฝ์ด๋ ๋ฑ ํ๋
๋ด๊ฐ ์ง ๊ฑด "์์ค ์ฝ๋๋ฅผ ๋ฐ์ ์์น ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ฝ์
ํด ๋๋ ค์ฃผ๋"
transformSource ํจ์ ํ๋๋ฟ์ด๋ค. unplugin์ผ๋ก ๊ฐ์ธ๋ฉด ๋์ด๋ค.
import { createUnplugin } from "unplugin";
import { transformSource } from "./core/transform";
const _unplugin = createUnplugin((options = {}) => ({
name: "react-code-locator",
enforce: "pre",
// ์ด๋ค ํ์ผ์ ๋ณํํ ์ง ํํฐ (.js/.jsx/.tsx, node_modules ์ ์ธ)
transformInclude(id) {
return shouldTransform(id, include, exclude);
},
// ์ค์ ๋ณํ: ๋ฒ๋ค๋ฌ๊ฐ ๋ญ๋ ์ด ํจ์ ํ๋๋ก ์ฒ๋ฆฌ๋๋ค
transform(code, id) {
const isEnabled = enabled ?? process.env.NODE_ENV === "development";
if (!isEnabled) return null;
return transformSource(code, { filename: id, projectRoot });
},
}));
๊ทธ๋ฆฌ๊ณ ๊ฐ ๋ฒ๋ค๋ฌ์ฉ ํ๋ฌ๊ทธ์ธ์ unplugin์ด ๋ง๋ค์ด ์ค ๊ฑธ ๊ทธ๋๋ก ๋ด๋ณด๋ด๋ฉด ๋๋ค.
export const webpackPlugin = _unplugin.webpack;
export const rollupPlugin = _unplugin.rollup;
export const esbuildPlugin = _unplugin.esbuild;
export const rspackPlugin = _unplugin.rspack;
transformInclude๋ก ๋์ ํ์ผ์ ๋จผ์ ๊ฑฐ๋ฅด๋ ๊ฒ๋ ํฌ์ธํธ๋ค. ๋ณํ ๋์์ด
์๋๋ฉด ์์ transform์ ๋ถ๋ฅด์ง ์์์, node_modules๋ .css ๊ฐ์ ํ์ผ์
์ธ๋ฐ์๋ ํ์ฑ ๋น์ฉ์ ์ ์ด๋ค.
ํจ์ 1: Vite๋ง ์ด๋ํฐ๋ฅผ ์ ์ฐ๊ณ ์ง์ ์งฐ๋ค
์ฌ๊ธฐ์ ์ฒซ ๋ฒ์งธ ๋ฒฝ. Vite๋งํผ์ unplugin ์ด๋ํฐ ๋์ ๋ค์ดํฐ๋ธ Vite ํ๋ฌ๊ทธ์ธ์ผ๋ก ๋ฐ๋ก ๊ตฌํํ๋ค. ์ด์ ๋ ์คํ ์์ ๋๋ฌธ์ด๋ค.
๋ด ํ๋ฌ๊ทธ์ธ์ ๊ฐ๋ฅํ๋ฉด @vitejs/plugin-react๋ณด๋ค ๋จผ์ ๋์์ผ ํ๋ค.
ํ๋ฌ๊ทธ์ธ ๋ฆฌ์กํธ๊ฐ ์ฝ๋๋ฅผ ๊ฑด๋๋ฆฌ๊ธฐ ์ ์ ์๋ณธ ๊ธฐ์ค์ผ๋ก ์์น๋ฅผ ์ฌ์ด์ผ ํ๋๊น.
๊ทธ๋์ enforce: "pre"๋ก ์ ์์๋ฅผ ์์ฒญํ๋๋ฐ, ์ด๊ฒ ์์๋ฅผ ์์ ํ
๋ณด์ฅํด ์ฃผ์ง๋ ์๋๋ค. @vitejs/plugin-react๋ ๊ฐ์ pre ๊ทธ๋ฃน์ด๋ผ, ์ํฉ์
๋ฐ๋ผ ๊ทธ์ชฝ์ด ๋จผ์ ๋ ์ ์๋ค. unplugin ์ด๋ํฐ๋ฅผ ๊ฑฐ์น๋ฉด ์ด๋ฐ ์์ ์ ์ด๊ฐ ๋
์ ๋งคํด์ ธ์, Vite๋ ์์์ ์ฐํ ๋ก์ง์ ์ง์ ์ฅ ์ ์๋ ๋ค์ดํฐ๋ธ ํ๋ฌ๊ทธ์ธ์ผ๋ก
๋ฐ๋ก ์งฐ๋ค.
export function vitePlugin(options): Plugin[] {
const transformPlugin: Plugin = {
name: "react-code-locator",
enforce: "pre", // react()๋ณด๋ค ์ ์์๋ฅผ ์์ฒญ (๋ณด์ฅ์ ์๋)
configResolved(config) {
resolvedEnabled = enabled ?? config.command === "serve";
},
transform(code, id) { /* ... transformSource ... */ },
};
return [transformPlugin, ...clientInjector];
}
๊ทธ๋ฐ๋ฐ ์์๋ฅผ ์์ฒญํด ๋๋๋ผ๋ @vitejs/plugin-react๊ฐ ๋จผ์ ๋์ ํ์ผ ์์
fast-refresh preamble์ ๋ถ์ด๋ ๊ฒฝ์ฐ๊ฐ ์ค์ ๋ก ์๊ธด๋ค. ๊ทธ๋ฌ๋ฉด ๋ชจ๋ ์ค ๋ฒํธ๊ฐ
๋ฐ๋ฆฐ๋ค. ๊ทธ๋์ ๋ค์ดํฐ๋ธ ํ๋ฌ๊ทธ์ธ์์๋ ๋ฐ์ ์ฝ๋๊ฐ ๋์คํฌ์ ์๋ณธ๊ณผ ๋ค๋ฅด๋ฉด
์๋ณธ ํ์ผ์ ์ง์ ์ฝ์ด preamble์ ๋ผ์ด๋ด๊ณ ๋ณํํ ๋ค ๋ค์ ๋ถ์ด๋ ์ฐํ๋ฅผ
๋ฃ์๋ค. ์ด ์ค ๋ฒํธ ๋ฒ๊ทธ์ ์์ธํ ์ด์ผ๊ธฐ๋ ์๋ ๊ธธ์ด์ ๋ฐ๋ก ๋ค๋ฃฌ๋ค.
ํจ์ 2: Webpack/Rspack loader์ ESM/CJS
unplugin์ WebpackยทRspack ์ด๋ํฐ๋ ๋ด๋ถ์ ์ผ๋ก loader ํ์ผ์ ๋ฐ๋ก ์ฐธ์กฐํ๋ค. ์ด loader๋ค์ด ๋ฐฐํฌ ํจํค์ง ์์ ์ ๋๋ก ๋ค์ด๊ฐ๊ณ , CommonJS๋ก๋ ๋ก๋๋๊ฒ ํ๋ ค๋ฉด ๋น๋ ํ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค.
- ๋น๋ ์ unplugin์ loader๋ค์
dist๋ก ๋ณต์ฌํ๊ณ , .js๋ฅผ.cjs๋ก๋ ๋ณต์ฌํ ๋ค,- ๋ฒ๋ค ์์ loader ๊ฒฝ๋ก ๋ฌธ์์ด์
.cjs๋ก ์นํํ๋ ํจ์น ์คํฌ๋ฆฝํธ๋ฅผ ๋๋ฆฐ๋ค.
// scripts/patch-loaders.cjs โ ๋ฒ๋ค ์ loader ๊ฒฝ๋ก๋ฅผ .cjs๋ก ๊ต์ฒด
const patterns = [
["webpack/loaders/transform", "webpack/loaders/transform.cjs"],
["rspack/loaders/transform", "rspack/loaders/transform.cjs"],
// ...
];
for (const [from, to] of patterns) {
content = content.replaceAll(JSON.stringify(from), JSON.stringify(to));
}
package.json์ exports ๋งต์ผ๋ก import/require/browser/types
์ง์
์ ์ ๋๋ ์ฃผ๋ ๊ฒ๊น์ง ํด์ผ "์ด๋ค ๋ฒ๋ค๋ฌ์์ ์ด๋ป๊ฒ ๋ถ๋ฌ๋ ๊นจ์ง์ง ์๋"
ํจํค์ง๊ฐ ๋๋ค.
"exports": {
".": {
"browser": "./dist/runtime.js",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
๋ฐฐ์ด ๊ฒ
-
๋ฒ๋ค๋ฌ๋ง๋ค API๋ ๋ฌ๋ผ๋ "ํ์ผ์ ๋ฐ์ ๋ณํํ๋ค"๋ ๊ณตํต์ ์ด ์๊ณ , unplugin์ ๊ทธ ๊ณตํต์ ์ ์ถ์ํํด ์ค๋ค. ํต์ฌ ๋ก์ง ํ๋๋ง ์ ๋ง๋ค๋ฉด ๋๋ค.
-
๋ค๋ง ์ถ์ํ๊ฐ ๋ชจ๋ ๊ฑธ ๊ฐ์ถฐ์ฃผ์ง ์๋๋ค. ์คํ ์์(
enforce)๋ loader ๊ฒฝ๋ก, ESM/CJS ๊ฐ์ ๋ฒ๋ค๋ฌ๋ณ ํ์ค์ ๊ฒฐ๊ตญ ๋ง์ฃผํด์ผ ํ๋ค. -
์ถ์ํ๋ 90%๋ฅผ ์์ ์ฃผ๊ณ , ๋๋จธ์ง 10%(๊น๋ค๋ก์ด ๊ฒ)๋ฅผ ๋จ๊ธด๋ค. ๊ทธ 10%๋ฅผ ์ดํดํ๋ ๊ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ์์ ๋ชซ์ด๋ผ๋ ๊ฑธ ๋ฐฐ์ ๋ค.
-
์ค์ ๊ตฌํ: react-code-locator (GitHub)
-
๊ด๋ จ ๊ธ: ์ฝ๋๋ ์ด๋ป๊ฒ ํธ๋ฆฌ๊ฐ ๋๋๊ฐ โ AST์ ํ์ฑ