일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- lazy()
- 모던자바스크립트
- Firebase
- 리액트
- 리액트훅
- express
- CSR
- Database
- reactquery
- NextJs
- useLayoutEffect
- docker
- ErrorBoundary
- msw
- react-hook-form
- next-cookies
- key
- 초기마운트
- SSR
- react
- 클래스
- useEffect
- react-hook
- 리액트 훅
- Today
- Total
한우의 개발일기
useRef() 파해치기 본문
https://github.com/Bookiwi-hub/flow/blob/deep-dive/apps/reader/src/hooks/useAsync.ts
위에 훅을 공부 하던중 비동기 처리를 ref 를 이용하여 위에 훅으로 사용할 수 있는 훅을 만든것을 보았는데 왜 ref를 사용하고 ref 의 동작원리를 잘 모르겠어서 ref에 대해서 공부해 보았습니다.
React의 useRef() 훅 내부 동작 분석
React의 훅 중 하나인 useRef()
의 내부 동작 원리를 깊이 살펴보려고 합니다. React를 사용하다 보면 useRef()
를 DOM 요소에 접근하거나 re-render 사이에 값을 유지하기 위해 자주 사용하게 되는데요, 이 훅이 어떻게 작동하는지 내부 메커니즘을 이해하면 더 효과적으로 활용할 수 있습니다.
useRef()는 무엇인가요?
간단히 말해 useRef()
는 .current
속성을 가진 변경 가능한 객체를 반환하는 훅입니다. 이 객체는 컴포넌트의 전체 생명주기 동안 유지되며, 컴포넌트가 재렌더링되어도 그 값이 보존됩니다.
가장 흔한 사용법은 다음과 같습니다
function MyComponent() {
const myRef = useRef(null);
return <div ref={myRef} />;
}
이 코드에서 두 가지 중요한 부분이 있습니다:
useRef()
를 호출하여 ref 객체를 생성하는 부분- JSX에서
ref={myRef}
와 같이 사용하여 DOM 요소와 연결하는 부분
이 두 가지 작업이 내부적으로 어떻게 이루어지는지 React의 소스 코드를 파헤쳐 봅시다.
useRef()의 내부 구현
React의 소스 코드를 살펴보면, useRef()
훅은 초기 렌더링과 업데이트(리렌더링) 단계에서 각각 다른 함수를 사용합니다.
초기 렌더링 시: mountRef()
function mountRef<T>(initialValue: T): {| current: T |} {
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}
{ current: initialValue }
형태의 객체를 생성합니다- 이 객체를 새로운 훅의
memoizedState
에 저장합니다 - 생성한 객체를 반환합니다
여기서 mountWorkInProgressHook()
은 React의 내부 함수로, 새로운 훅 인스턴스를 생성하고 컴포넌트의 파이버(fiber)에 연결하는 역할을 합니다.
리렌더링 시: updateRef()
function updateRef<T>(initialValue: T): {| current: T |} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
- 현재 처리 중인 훅을 가져옵니다(
updateWorkInProgressHook()
) - 이전에 저장했던 ref 객체를 그대로 반환합니다
이것이 바로 useRef()
가 컴포넌트의 리렌더링 사이에서도 동일한 객체 참조를 유지하는 방식입니다. 그저 기존 객체를 재사용합니다.
실제 DOM 요소와의 연결: ref={myRef}
이제 더 흥미로운 부분입니다. JSX에서 ref={myRef}
와 같이 사용하면 React는 어떻게 실제 DOM 요소를 myRef.current
에 연결할까요?
이 과정은 React의 렌더링 및 커밋 단계에서 복잡한 과정을 통해 이루어집니다.
1. Ref 변경 감지
먼저 React는 ref가 변경되었는지 확인하는 markRef()
함수를 사용합니다:
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
workInProgress.flags |= Ref;
// ...
}
}
이 함수는 다음과 같은 경우에 파이버의 flags
에 Ref
플래그를 설정합니다
- 새로운 ref가 생성되었을 때
- 기존 ref가 다른 ref로 변경되었을 때
이 플래그는 나중에 커밋(Commit Phase) 단계에서 ref를 처리해야 함을 React에게 알려주는 신호역할을 합니다.
2. Ref 연결 (Attaching)
커밋 단계에서는 commitAttachRef()
함수가 호출됩니다
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
// ...
}
if (typeof ref === "function") {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
여기서 중요한 부분을 살펴보면
- DOM 요소(HostComponent)의 경우,
getPublicInstance(instance)
를 통해 실제 DOM 노드를 가져옵니다 - ref가 함수라면 해당 함수를 호출하고(콜백 ref), 객체라면
.current
속성에 DOM 노드를 할당합니다
이 과정이 리액트 내부를 살펴보면 useLayoutEffect
훅과 동일한 단계에서 실행되는데, 이는 ref가 DOM에 연결되는 시점이 useEffect
보다 빠르다는 것입니다, 이 특성은 DOM 요소를 다루는 커스텀 훅을 만들 때 매우 유용합니다.
useEffect: 브라우저가 화면을 그린 후에 비동기적으로 실행됩니다.
useLayoutEffect: 브라우저가 화면을 그리기 전에 동기적으로 실행됩니다. (이 부분에 대해서는 학습후 포스팅을 하겠습니다)
3. Ref 분리 (Detaching)
컴포넌트가 언마운트되거나 ref가 변경될 때는 기존 ref와 DOM 요소의 연결을 해제해야 합니다
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === "function") {
currentRef(null);
} else {
currentRef.current = null;
}
}
}
이 함수는 간단하게
- ref가 함수라면
null
을 인자로 호출합니다 - ref가 객체라면
.current
속성을null
로 설정합니다
실제 활용 예시와 팁
이런 내부 동작 원리를 이해하면 useRef()
를 더 효과적으로 활용할 수 있습니다
1. DOM 측정하기
DOM 요소의 크기나 위치를 측정해야 할 때
function MeasureExample() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
if (divRef.current) {
const { width, height } = divRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<>
<div ref={divRef}>측정할 요소</div>
<p>너비: {dimensions.width}px, 높이: {dimensions.height}px</p>
</>
);
}
여기서 useLayoutEffect
를 사용한 이유는 ref.current
가 DOM 요소에 연결되는 시점이 useLayoutEffect
실행 시점과 동일하기 때문입니다. 따라서 DOM 측정 작업은 useEffect
보다 useLayoutEffect
에서 수행하는 것이 더 안전합니다.
2. 이전 값 기억하기
컴포넌트의 이전 상태를 기억해야 할 때
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 사용 예시
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>현재: {count}, 이전: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
이 패턴은 useRef()
가 리렌더링 사이에 값을 유지하는 특성을 활용한 것입니다.
결론 및 주의사항
React의 useRef()
훅은 겉으로 보기에 단순하지만, 내부적으로는 React의 파이버 아키텍처와 밀접하게 연결되어 동작합니다. 이러한 내부 메커니즘을 이해하면 다음과 같은 이점이 있습니다:
- 성능 최적화 ref를 사용하여 불필요한 리렌더링을 방지할 수 있습니다.
- DOM 조작 시점 제어 DOM 요소에 접근하는 적절한 시점(useLayoutEffect vs useEffect)을 선택할 수 있습니다.
- 고급 패턴 구현 ref 기반의 커스텀 훅을 더 효과적으로 설계할 수 있습니다.
다만, ref를 사용할 때는 다음 사항에 주의해야 합니다
- 직접적인 DOM 조작은 최소화하자 React의 선언적 패러다임을 벗어나는 작업이 될 수 있습니다.
- ref.current 변경은 리렌더링을 트리거하지 않는다 상태 관리에는
useState
나useReducer
를 사용하세요. - 초기 렌더링 시에는 ref.current가 null일 수 있다 항상 null 체크를 해주세요.
useRef()
는 단순해 보이지만, 그 내부에는 복잡하네요
다음 포스팅에서는 useAysnc 훅이 ref를 통해서 어떻게 동작하는지 알아보겠습니다.
'React' 카테고리의 다른 글
useEffect vs useLayoutEffect (0) | 2025.03.21 |
---|---|
useAsync 파해치기 (0) | 2025.03.18 |
리액트의 초기 마운트 (0) | 2025.02.13 |
리액트의 내부개요 (0) | 2025.02.13 |
React-hook-form에서 watch 와 useWatch (0) | 2024.11.11 |