React Fast Refresh: Next-Gen Hot Reloading 설명
James Reed
Infrastructure Engineer · Leapcell

서문
먼저 **라이브 리로딩(Live Reloading)**과 **핫 리로딩(Hot Reloading)**의 차이점을 소개합니다.
- 라이브 리로딩: 파일이 수정되면 Webpack이 다시 컴파일하고 브라우저를 강제로 새로 고칩니다. 이는
window.location.reload()
와 동일한 전체 새로 고침을 초래합니다. - 핫 리로딩: 파일이 수정되면 Webpack이 해당 모듈을 다시 컴파일하지만, 새로 고침은 애플리케이션의 상태를 유지하여 부분 새로 고침이 가능합니다.
소개
Fast Refresh는 React에서 React Native(v0.6.1)를 위해 도입한 공식 HMR(Hot Module Replacement) 솔루션입니다. 핵심 구현이 플랫폼 독립적이므로 Fast Refresh는 웹에도 적용할 수 있습니다.
새로 고침 전략
- React 컴포넌트만 내보내는 모듈 파일을 편집하는 경우 Fast Refresh는 모듈의 코드만 업데이트하고 컴포넌트를 다시 렌더링합니다. 스타일, 렌더링 로직, 이벤트 핸들러 또는 효과를 포함하여 파일 내에서 무엇이든 편집할 수 있습니다.
- 모듈이 React 컴포넌트를 내보내지 않는 경우 Fast Refresh는 모듈과 해당 모듈을 가져오는 모든 모듈을 다시 실행합니다. 예를 들어
Button.js
와Modal.js
가 모두Theme.js
를 가져오는 경우Theme.js
를 편집하면Button.js
와Modal.js
둘 다 업데이트됩니다. - 마지막으로, 편집하는 파일이 React 렌더링 트리 외부의 모듈에서만 가져오는 경우 Fast Refresh는 전체 새로 고침으로 대체됩니다. 이는 파일이 React 컴포넌트를 렌더링하고 비 React 모듈에서 사용하는 값을 내보내는 경우에 발생할 수 있습니다. 예를 들어 React 컴포넌트 모듈이 상수를 내보내고 비 React 모듈이 이를 가져오는 경우 Fast Refresh는 해당 파일을 격리하여 업데이트할 수 없습니다. 이러한 경우 내보낸 값을 별도의 파일로 이동하여 두 모듈로 가져오는 것을 고려하십시오. 이렇게 하면 Fast Refresh가 올바르게 작동할 수 있습니다.
오류 처리
- Fast Refresh 중에 구문 오류가 발생하면 오류를 수정하고 파일을 다시 저장할 수 있습니다. 빨간색 오류 화면이 사라집니다. 결함이 있는 모듈은 실행이 차단되므로 앱을 다시 로드할 필요가 없습니다.
- 모듈 초기화 중에 런타임 오류가 발생하는 경우(예: 실수로
StyleSheet.create
대신Style.create
를 작성) 오류를 수정하면 Fast Refresh 세션을 계속할 수 있습니다. 오류 화면이 사라지고 모듈이 업데이트됩니다. - 컴포넌트 내부에서 런타임 오류가 발생하는 경우 오류를 수정하면 Fast Refresh 세션도 재개됩니다. 이 경우 React는 업데이트된 코드를 사용하여 컴포넌트를 다시 마운트합니다.
- 충돌하는 컴포넌트가 오류 경계 내부에 있는 경우 오류를 수정하면 Fast Refresh는 오류 경계 내의 모든 노드를 다시 렌더링합니다.
제한 사항
파일을 편집할 때 Fast Refresh는 안전한 경우 컴포넌트 상태를 유지합니다. 그러나 다음 경우에는 파일 편집 후 상태가 재설정됩니다.
- 클래스 컴포넌트의 로컬 상태는 유지되지 않습니다(기능 컴포넌트와 Hooks의 상태만 유지됨).
- 편집하는 모듈이 React 컴포넌트 이외의 다른 것을 내보내는 경우.
- 때때로 모듈은
createNavigationContainer(MyScreen)
와 같은 **고차 컴포넌트(HOC)**를 내보냅니다. 반환된 컴포넌트가 클래스 컴포넌트인 경우 상태가 재설정됩니다.
함수형 컴포넌트와 Hooks가 점점 더 보편화됨에 따라 Fast Refresh를 사용한 편집 경험은 계속 개선될 것입니다.
팁
- 기본적으로 Fast Refresh는 함수형 컴포넌트와 Hooks에서 상태를 유지합니다.
- 마운트 시에만 실행되는 애니메이션을 디버깅하는 경우 편집할 때마다 전체 다시 마운트를 강제로 수행할 수 있습니다. 이렇게 하려면 파일의 아무 곳에나
// @refresh reset
을 추가하십시오. 이 지시어는 해당 파일에 정의된 컴포넌트를 편집할 때마다 Fast Refresh가 다시 마운트하도록 강제합니다.
Fast Refresh의 Hooks 동작
Fast Refresh는 편집 중에 컴포넌트 상태를 유지하려고 시도합니다. 특히 useState
와 useRef
는 다음 조건이 충족되는 한 이전 값을 유지합니다.
- 매개변수를 변경하지 않는 경우.
- Hook 호출 순서를 변경하지 않는 경우.
종속성이 있는 Hooks(예: useEffect
, useMemo
및 useCallback
)는 Fast Refresh 중에 종속성 목록을 무시하고 항상 다시 실행됩니다.
예를 들어 다음을 변경하는 경우:
useMemo(() => x * 2, [x]);
다음으로 변경합니다.
useMemo(() => x * 10, [x]);
x
가 변경되지 않은 상태로 유지되더라도 팩토리 함수가 다시 실행됩니다. 이러한 동작이 없으면 변경 사항이 UI에 반영되지 않습니다.
이 메커니즘은 때때로 예상치 못한 동작을 생성합니다. 예를 들어 useEffect
종속성 배열이 비어 있더라도 Fast Refresh는 여전히 효과를 한 번 다시 실행합니다. 그러나 새로운 종속성을 나중에 도입하기 쉽도록 간헐적인 재실행을 처리할 수 있는 useEffect
hooks를 작성하는 것이 일반적으로 좋은 방법입니다.
구현 세부 정보
HMR(모듈 수준) 및 **React Hot Loader(제한된 컴포넌트 수준)**보다 더 세분화된 업데이트를 달성하려면 컴포넌트 수준 및 Hooks 수준의 안정적인 업데이트를 지원해야 합니다. 이를 위해서는 런타임 패치 또는 컴파일 시간 변환과 같은 외부 메커니즘만으로는 불충분하므로 React와의 심층적인 통합이 필요합니다.
Fast Refresh는 React에서 완전히 지원하는 "핫 리로딩"의 재구현입니다.
이는 이전에는 피할 수 없었던 문제(예: Hooks 처리)를 이제 React의 협력으로 해결할 수 있음을 의미합니다.
핵심적으로 Fast Refresh는 여전히 HMR에 의존하며 다음과 같은 계층이 있습니다.
- HMR 메커니즘(예: Webpack HMR)
- 컴파일 시간 변환(
react-refresh/babel
) - 런타임 개선(
react-refresh/runtime
) - React의 내장 지원(
React DOM 16.9+
또는react-reconciler 0.21.0+
)
프록시 컴포넌트를 사용하는 React Hot Loader와 달리 Fast Refresh는 React가 이제 함수형 컴포넌트와 Hooks에 대한 핫 교체를 기본적으로 지원하므로 이 계층을 제거합니다.
Fast Refresh는 react-refresh
패키지 내에서 관리되는 두 부분으로 구성됩니다.
- Babel 플러그인(
react-refresh/babel
) - 런타임(
react-refresh/runtime
)
서로 다른 진입점을 통해 이러한 기능을 노출합니다.
Fast Refresh의 구현을 4가지 주요 측면으로 나눌 수 있습니다.
- Babel 플러그인은 컴파일 시간에 무엇을 합니까?
- 런타임은 실행 시간에 어떻게 작동합니까?
- React는 이를 위해 어떤 특정 지원을 제공합니까?
- 이 메커니즘은 HMR과 어떻게 통합됩니까?
1. Babel 플러그인은 컴파일 시간에 무엇을 합니까?
높은 수준에서 Fast Refresh의 Babel 플러그인은 코드에서 모든 컴포넌트와 사용자 지정 Hooks를 감지하고 컴포넌트를 등록하고 Hook 서명을 수집하기 위해 함수 호출을 삽입합니다.
변환 전
function useFancyState() { const [foo, setFoo] = React.useState(0); useFancyEffect(); return foo; } const useFancyEffect = () => { React.useEffect(() => {}); }; export default function App() { const bar = useFancyState(); return <h1>{bar}</h1>; }
변환 후
var _s = $RefreshSig$(), _s2 = $RefreshSig$(), _s3 = $RefreshSig$(); function useFancyState() { _s(); const [foo, setFoo] = React.useState(0); useFancyEffect(); return foo; } _s(useFancyState, 'useState{ct{}', false, function () { return [useFancyEffect]; }); const useFancyEffect = () => { _s2(); React.useEffect(() => {}); }; _s2(useFancyEffect, 'useEffect{}'); export default function App() { _s3(); const bar = useFancyState(); return <h1>{bar}</h1>; } _s3(App, 'useFancyState{bar}', false, function () { return [useFancyState]; }); _c = App; var _c; $RefreshReg$(_c, 'App');
_s
및 _s2
함수는 Hook 서명을 수집하고 $RefreshReg$
는 Fast Refresh에 대해 컴포넌트를 등록합니다.
2. 런타임은 실행 시간에 어떻게 작동합니까?
변환된 코드에서 Babel 플러그인에서 삽입한 정의되지 않은 두 가지 함수를 확인할 수 있습니다.
$RefreshSig$
: 사용자 지정 Hook 서명을 수집합니다.$RefreshReg$
: 컴포넌트를 등록합니다.
이러한 함수는 **react-refresh/runtime
**에서 제공됩니다. 일반적인 설정은 다음과 같습니다.
var RefreshRuntime = require('react-refresh/runtime'); window.$RefreshReg$ = (type, id) => { // 참고: `module.id`는 Webpack 특정입니다. 다른 번들러는 다른 식별자를 사용할 수 있습니다. const fullId = module.id + ' ' + id; RefreshRuntime.register(type, fullId); }; window.$RefreshSig$ = RefreshRuntime.collectCustomHooksForSignature;
다음은 React Refresh Runtime API에 매핑되는 방식입니다.
createSignatureFunctionForTransform
: Hook 서명 정보를 추적합니다.register
: **참조(type
)**를 **고유 ID(id
)**에 매핑하여 컴포넌트를 등록합니다.
createSignatureFunctionForTransform
작동 방식
createSignatureFunctionForTransform
함수는 세 단계로 Hook 사용량을 추적합니다.
- 초기 단계: 함수 서명을 해당 컴포넌트와 연결합니다.
- Hook 수집 단계: 컴포넌트에서 사용되는 사용자 지정 Hooks에 대한 정보를 수집합니다.
- 해결된 단계: 세 번째 호출 후에는 불필요한 오버헤드를 방지하기 위해 더 이상의 변경 사항 기록을 중지합니다.
export function createSignatureFunctionForTransform() { let savedType; let hasCustomHooks; let didCollectHooks = false; return function <T>( type: T, key: string, forceReset?: boolean, getCustomHooks?: () => Array<Function> ): T | void { if (typeof key === 'string') { if (!savedType) { savedType = type; hasCustomHooks = typeof getCustomHooks === 'function'; } if (type != null && (typeof type === 'function' || typeof type === 'object')) { setSignature(type, key, forceReset, getCustomHooks); } return type; } else { if (!didCollectHooks && hasCustomHooks) { didCollectHooks = true; collectCustomHooksForSignature(savedType); } } }; }
register
작동 방식
register
함수는 컴포넌트 업데이트를 추적합니다.
export function register(type: any, id: string): void { let family = allFamiliesByID.get(id); if (family === undefined) { family = { current: type }; allFamiliesByID.set(id, family); } else { pendingUpdates.push([family, type]); } allFamiliesByType.set(type, family); }
다음은 발생하는 상황입니다.
- 컴포넌트가 아직 등록되지 않은 경우 전역 컴포넌트 레지스트리(
allFamiliesByID
)에 추가됩니다. - 컴포넌트가 이미 존재하는 경우 보류 중인 업데이트 큐(
pendingUpdates
)에 추가됩니다. - 보류 중인 업데이트는 Fast Refresh가 실행될 때 나중에 처리됩니다.
업데이트가 적용되면 performReactRefresh
는 보류 중인 업데이트를 **활성 업데이트 테이블(updatedFamiliesByType
)**로 이동하여 React가 함수와 컴포넌트의 최신 버전을 조회할 수 있도록 합니다.
function resolveFamily(type) { return updatedFamiliesByType.get(type); }
3. React는 Fast Refresh에 어떤 지원을 제공합니까?
React 런타임은 Fast Refresh 통합을 위한 여러 함수를 제공합니다.
import type { Family, RefreshUpdate, ScheduleRefresh, ScheduleRoot, FindHostInstancesForRefresh, SetRefreshHandler, } from 'react-reconciler/src/ReactFiberHotReloading';
핵심 기능 중 하나는 setRefreshHandler
이며, 이는 Fast Refresh를 React의 조정 프로세스에 연결합니다.
export const setRefreshHandler = (handler: RefreshHandler | null): void => { if (__DEV__) { resolveFamily = handler; } };
Fast Refresh가 React 업데이트를 트리거하는 방법
Fast Refresh가 업데이트를 감지하면 다음을 수행합니다.
- 업데이트 테이블(
updatedFamilies
)을 React에 전달합니다. scheduleRefresh
및scheduleRoot
를 사용하여 React 업데이트를 트리거합니다.
export function performReactRefresh(): RefreshUpdate | null { const update: RefreshUpdate = { updatedFamilies, // 상태를 유지하면서 다시 렌더링될 컴포넌트 staleFamilies, // 다시 마운트해야 하는 컴포넌트 }; helpersByRendererID.forEach((helpers) => { helpers.setRefreshHandler(resolveFamily); }); failedRootsSnapshot.forEach((root) => { const helpers = helpersByRootSnapshot.get(root); const element = rootElements.get(root); helpers.scheduleRoot(root, element); }); mountedRootsSnapshot.forEach((root) => { const helpers = helpersByRootSnapshot.get(root); helpers.scheduleRefresh(root, update); }); }
React가 업데이트된 컴포넌트를 사용하는 방법
React는 resolveFamily
를 사용하여 컴포넌트 또는 Hooks의 최신 버전을 가져옵니다.
export function resolveFunctionForHotReloading(type: any): any { const family = resolveFamily(type); if (family === undefined) { return type; } return family.current; }
렌더링하는 동안 React는 이전 컴포넌트 참조를 새 컴포넌트 참조로 바꿉니다.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { switch (workInProgress.tag) { case IndeterminateComponent: case FunctionComponent: case SimpleMemoComponent: workInProgress.type = resolveFunctionForHotReloading(current.type); break; case ClassComponent: workInProgress.type = resolveClassForHotReloading(current.type); break; case ForwardRef: workInProgress.type = resolveForwardRefForHotReloading(current.type); break; default: break; } }
4. Fast Refresh는 HMR과 어떻게 통합됩니까?
지금까지의 모든 내용은 컴포넌트 수준 업데이트를 활성화하지만 Fast Refresh가 작동하려면 HMR(Hot Module Replacement)과의 통합이 여전히 필요합니다.
HMR 워크플로
-
React를 로드하기 전에 런타임을 애플리케이션에 삽입합니다.
const runtime = require('react-refresh/runtime'); runtime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type;
-
각 모듈을 Fast Refresh 등록 로직으로 래핑합니다.
window.$RefreshReg$ = (type, id) => { const fullId = module.id + ' ' + id; RefreshRuntime.register(type, fullId); }; window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; try { // !!! 실제 모듈 소스 코드 !!! } finally { window.$RefreshReg$ = prevRefreshReg; window.$RefreshSig$ = prevRefreshSig; }
-
모든 모듈을 처리한 후 HMR API에 연결합니다.
const myExports = module.exports; if (isReactRefreshBoundary(myExports)) { module.hot.accept(); // 번들러에 따라 다름 const runtime = require('react-refresh/runtime'); let enqueueUpdate = debounce(runtime.performReactRefresh, 30); enqueueUpdate(); }
isReactRefreshBoundary
는 모듈이 핫 리로딩을 지원하는지 아니면 전체 라이브 리로드가 필요한지를 결정합니다.
웹 환경에서의 사용
Fast Refresh는 원래 React Native용으로 구축되었지만 핵심 구현은 플랫폼 독립적이므로****웹 애플리케이션에서도 사용할 수 있습니다.
원래는 React Native용으로 제공되지만 대부분의 구현은 플랫폼 독립적입니다.
웹 애플리케이션에서 Fast Refresh를 사용하려면 Metro(React Native의 번들러)를 Webpack으로 바꾸고 위에 설명된 통합 단계를 따르십시오.
우리는 Node.js 프로젝트 호스팅을 위한 최고의 선택인 Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하십시오. 요청이나 요금이 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 높은 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없으므로 빌드에만 집중하십시오.
설명서에서 자세히 알아보십시오!
X에서 팔로우하세요: @LeapcellHQ