라이브러리의 동작 원리를 깊게 이해하고 싶다면, 직접 소스 코드를 뜯어보며 로컬 환경에서 실행해보는 것이 가장 확실한 방법 중 하나입니다.
이 글에서는 최신 리액트 소스 코드를 여러분의 로컬 환경에 준비하는 방법과, 간단한 빌드 명령어를 통해 여러분의 프로젝트에서 수정된 리액트를 사용할 수 있도록 설정하는 방법을 단계별로 알려드릴게요! 목표는 리액트 소스코드를 수정하면 자동으로 프로젝트에 반영되도록 하는 것입니다.
리액트 공식 문서에도 빌드 방법에 대한 가이드가 있습니다 (How to Contribute). 하지만 최신 버전의 리액트와 약간 달라서 그대로 따라 하면 동작하지 않을 수 있는데요, 제가 트러블슈팅한 내용을 함께 공유해볼게요.
프로젝트 구조는 대략 이렇습니다. 아주 심플하죠?
react-study/
├── react # 여기에 리액트 소스 코드가 담깁니다.
└── my-vite-app # 수정된 리액트를 사용할 여러분의 로컬 개발 앱입니다.
react-study
폴더 아래에 리액트 공식 소스 코드를 클론받고, 그 옆에 그 소스 코드를 사용해볼 간단한 리액트 앱 프로젝트를 만드는 것이 목표입니다.
우선 리액트 소스 코드를 여러분의 컴퓨터로 가져오고 개발에 필요한 빌드 환경을 설정해봅시다!
먼저 터미널을 열고, react-study
같은 적절한 작업 폴더로 이동한 다음 아래 명령어를 실행해 리액트 소스 코드를 클론받습니다:
git clone https://github.com/facebook/react
cd react
리액트 소스 코드는 빠르게 바뀝니다. 특정 시점의 안정적인 코드를 살펴보고 싶다면 특정 릴리스 버전에 해당하는 태그(tag) 를 이용해 해당 시점의 코드로 이동하는 것이 좋습니다. 아래 명령어로 태그 목록을 최신순으로 확인해보세요:
git tag --sort=-v:refname
이후 원하는 태그로 체크아웃합니다. 예를 들어 v19.1.0 릴리스 시점의 코드를 보고 싶다면 아래 명령어를 실행합니다:
git checkout tags/v19.1.0
이제 프로젝트 의존성을 설치해줍시다:
yarn install
리액트는 개발자가 소스 코드를 수정하면서 실시간으로 변경 사항을 확인할 수 있도록 빌드 스크립트를 제공합니다. 리액트 프로젝트 폴더에서 아래 명령어를 실행해보세요:
yarn build react/index,react/jsx,react-dom/index,react-dom/client,scheduler --type=NODE_DEV --watch
빌드 관련된 공식 문서 내용입니다:
yarn build react/index,react-dom/index --type=UMD
를 실행하고 fixtures/packaging/babel-standalone/dev.html을 실행하는 게 변경을 시도해보는 가장 쉬운 방법입니다. 이 파일은 build 폴더의 react.development.js를 이미 사용하고 있으므로 변경 사항을 확인할 수 있습니다.
최신 리액트에서는 UMD 빌드가 제거되었습니다. 따라서 NODE_DEV
(--type=NODE_DEV
) 타입을 사용하여 CommonJS(CJS) 형태로 빌드해야 합니다.
그렇담 이번엔 노드 빌드를 위한 문서 내용을 봅시다:
npm을 통해 React를 사용하고 있다면, 의존성에서 react와 react-dom을 삭제하고 yarn link를 사용해서 로컬 build 폴더를 가리키게 해주세요. 빌드할 때 --type=UMD 대신 --type=NODE을 전달해야 한다는 점을 주의해주세요. 또한 scheduler 패키지도 아래처럼 빌드해야 합니다. (중략)
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE
공식 문서의 빌드 명령어에는 react-dom/client
패키지가 없지만 이 패키지를 명시적으로 추가해야 정상적으로 빌드가 동작합니다. 리액트 17까지는 react-dom
과 react-dom/server
로 나뉘어져있었는데 18부터는 react-dom
, react-dom/client
, react-dom/server
로 나뉘게 된 영향 아닐까요?
위 yarn build
명령이 성공적으로 실행되면, 빌드된 파일들은 리액트 폴더 안의 react/build/node_modules
경로에 생성됩니다.
이렇게 빌드된 로컬 리액트 코드를 여러분의 로컬 개발 프로젝트에서 실제로 사용하려면 추가적인 설정이 필요합니다. 여기서는 vite 기준으로 설명드릴게요.
우선 vite 리액트 앱을 생성합니다:
yarn create vite my-vite-app --template react
자동 생성된 package.json
에서 React와 ReactDOM을 삭제해줍시다:
{
"name": "my-vite-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {...},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {...}
}
나머지 필요한 패키지를 설치해줍니다:
yarn install
이제 빌드된 리액트 패키지를 사용하도록 vite.config.js
를 수정해봅시다. 아래 명령어로 vite 서버 재시작용 플러그인을 설치합니다:
yarn add -D vite-plugin-restart
필요한 설정이 포함된 vite.config.js
코드입니다. 주석으로 설명을 적어놨어요.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import VitePluginRestart from "vite-plugin-restart";
// 로컬 빌드된 리액트 패키지들이 위치한 경로를 설정합니다.
const nodeModulesPath = path.resolve(__dirname, "../react/build/node_modules");
export default defineConfig({
plugins: [
// 일반적인 React 플러그인
react(),
// 빌드된 리액트 파일 변경 시 Vite 서버를 재시작합니다.
VitePluginRestart({
restart: ["../react/build/node_modules/**/*.js"],
}),
// Vite는 기본적으로 node_modules 내부 파일 변경을 감지하지 않기에
// Vite의 watcher 무시 목록에서 빌드된 리액트 경로를 제외시킵니다.
// https://github.com/vitejs/vite/issues/8619#issuecomment-1170762244
{
name: "watch-node-modules",
configureServer: (server) => {
server.watcher.options = {
...server.watcher.options,
ignored: [/node_modules\/(?!react|react-dom).*/, "**/.git/**"],
};
}
}
],
// 모듈 이름(예: 'react')이 실제 파일 경로에 매핑되도록 설정합니다.
// 이렇게 하면 'react'를 import 할 때 node_modules 대신 로컬 빌드된 파일을 참조하게 됩니다.
resolve: {
alias: {
"react/jsx-dev-runtime": path.resolve(
nodeModulesPath,
"react/cjs/react-jsx-dev-runtime.development.js"
),
react: path.resolve(nodeModulesPath, "react/cjs/react.development.js"),
"react-dom/client": path.resolve(
nodeModulesPath,
"react-dom/cjs/react-dom-client.development.js"
),
"react-dom": path.resolve(
nodeModulesPath,
"react-dom/cjs/react-dom.development.js"
),
scheduler: path.resolve(
nodeModulesPath,
"scheduler/cjs/scheduler.development.js"
),
},
},
});
vite-plugin-react의 소스코드를 봅시다.
const jsxImportSource = opts.jsxImportSource ?? 'react'
const jsxImportRuntime = `${jsxImportSource}/jsx-runtime`
const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime`
// ...
const dependencies = [
'react',
'react-dom',
jsxImportDevRuntime,
jsxImportRuntime,
]
의존성에 jsx-dev-runtime이 포함됨을 확인할 수 있습니다. JSX 코드를 지원하기 위함인데 자세한건 Introducing the New JSX Transform를 참고하세요.
scheduler의 경우 react-dom-client.development.js
파일에서 아래와 같은 코드를 확인할 수 있습니다.
var Scheduler = require("scheduler"),
React = require("react"),
ReactDOM = require("react-dom"),
소스코드를 확인해보면 scheduler를 아래와 같이 설명하네요:
This is a package for cooperative scheduling in a browser environment. It is currently used internally by React, but we plan to make it more generic.
이제 react
폴더에서 리액트 소스 코드를 수정하고 저장하면, 빌드 와쳐가 이를 감지하여 build/node_modules
의 파일들을 업데이트하고, VitePluginRestart
플러그인이 이를 감지하여 Vite 개발 서버를 재시작할 것입니다. 그리고 브라우저에서는 수정된 리액트 코드로 실행되는 여러분의 테스트 앱을 확인할 수 있게 됩니다.
리액트 소스코드의 핵심 함수 중 하나인 workLoopSync
를 수정해봅시다. react/packages/react-reconciler/src/ReactFiberWorkLoop.js
경로의 파일을 찾고 그 안에 있는 workLoopSync
에 console.log
를 추가해보세요. 리액트가 렌더링할 때 로그가 잘 찍히나요?
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
while (workInProgress !== null) {
console.log('Hello, react!');
performUnitOfWork(workInProgress);
}
}
workLoopSync
는 리액트가 동기적으로 컴포넌트 트리를 렌더링할 때 사용하는 핵심 루프로, 남아 있는 모든 작업(Fiber 노드)을 처리할 때까지 중단 없이 실행됩니다. 이를 통해 상태 변화에 따른 UI 업데이트가 DOM에 반영됩니다.
이렇게 리액트 소스코드 공부를 위한 준비를 마쳤습니다. 기회가 된다면 소스코드를 공부하며 인상깊었던 부분도 공유할게요 🙌
로그인 중...