Skip to main content

Command Palette

Search for a command to run...

React useRef 깊은 이해: DOM 접근부터 Fiber 내부 동작까지

Published
•4 min read•View as Markdown

React를 사용하다 보면 useRef가 “DOM 접근용”으로만 알려져 있지만, 실제로는 렌더 사이에서 값을 유지하거나, 불필요한 렌더를 방지하는 중요한 메커니즘을 담당합니다.
이번 글에서는 useRef가 어떻게 동작하는지, 그리고 그 기반이 되는 React Fiber 아키텍처까지 함께 깊이 있게 살펴보겠습니다.


1. useRef란 무엇인가?

useRef는 다음 값을 반환합니다:

{ current: initialValue }

이 객체는 변경 가능한(mutable) 객체이며, React는 이 객체를 Fiber 노드의 hook 구조 내부에 저장합니다.

즉, useRef는 단순한 객체처럼 보이지만 실제로는 React의 내부 렌더링 시스템과 연결된 중요한 Hook입니다.


2. useRef는 왜 값이 유지되는가?

React는 컴포넌트마다 Fiber Node라는 내부 객체를 유지합니다.
이 Fiber Node 안에 컴포넌트에 관련된 모든 상태가 저장됩니다:

  • state

  • props

  • memoized 값

  • effect

  • ref

useRef에서 반환되는 { current: ... } 객체는 이 Fiber Node의 memoizedState 내부의 Hook 리스트에 저장됩니다.

렌더링이 다시 일어나도 Fiber Node가 유지되기 때문에
→ 같은 Hook 위치에 있는 useRef 객체도 재사용됩니다.
→ 따라서 ref.current 값이 렌더 사이에서도 계속 유지됩니다.


3. useRef 변경은 왜 리렌더링을 발생시키지 않는가?

useState는 값이 바뀌면 React의 업데이트 큐에 등록되고 렌더가 다시 발생합니다.
하지만 useRef는 아래처럼 단순한 mutable 값 변경일 뿐입니다:

ref.current = newValue

React는 ref.current 변경을 감지하지 않으며,
렌더 트리(Fiber Tree)에 영향을 준다고 판단하지 않습니다.

따라서:

✔ 렌더 발생하지 않음
✔ 단순한 JavaScript 객체 변경
✔ React의 의존성 추적(dependency tracking) 시스템 밖에서 움직임

이 점 덕분에 useRef는 렌더링 성능을 유지하면서 값을 저장하는 데 매우 적합합니다.


4. useRef와 DOM 접근

useRef는 렌더 단계에서는 DOM을 알 수 없고,
commit 단계에서 React가 실제 DOM 요소를 준비한 뒤 다음과 같이 값이 할당됩니다:

ref.current = 해당 DOM 노드

언마운트 시에는:

ref.current = null

이 작업이 commit 단계에서 이뤄지므로 렌더 직후에는 종종 ref가 null일 수 있습니다.


5. useRef의 실용적인 활용 패턴

① DOM 접근

input 포커스, 스크롤 위치 제어 등

② 렌더 사이 값 유지

이전 값 저장, 함수 호출 횟수 등

③ 타이머 또는 interval ID 보관

debounce / throttle 구현에 필수

④ 외부 라이브러리 인스턴스 보관

Map, WebGL 객체, Video.js 등

⑤ stale closure 문제 해결

state가 갱신되기 전에 최신 값을 유지해야 하는 경우 매우 유용


6. Fiber란 무엇인가?

useRef를 깊게 이해하려면 Fiber도 이해해야 합니다.

Fiber는 React 컴포넌트 하나를 표현하는 내부 객체이자, React 16에서 도입된 “작업 단위(Work Unit)” 구조입니다.

즉, Fiber는 React가:

  • state

  • props

  • hook 목록

  • 렌더링 작업 상태

  • 자식/형제 컴포넌트 관계

  • 이전 렌더의 정보

등을 관리하기 위해 사용하는 핵심 단위입니다.


7. 왜 Fiber가 필요했을까?

React 15까지는 렌더링이 중단 불가능했습니다.

  • 렌더 중에는 브라우저 UI가 멈춤

  • 큰 컴포넌트 트리에서는 프리즈 발생

  • 사용자 경험 악화

React 16의 Fiber 아키텍처는 이를 해결했습니다.

Fiber로 인해 가능해진 것들

✔ 렌더 중단 + 재개

작업을 잘게 나누고 우선순위에 따라 처리

✔ Concurrent Rendering

사용자 입력 같은 높은 우선순위 작업을 먼저 처리

✔ Suspense & Error Boundary

Fiber Tree 기반으로 특정 컴포넌트 단위 제어 가능

✔ Hook 관리

useState/useEffect/useRef 모두 Fiber의 hook 구조로 구성


8. Fiber Node의 구조 예시

실제로는 매우 복잡하지만 핵심 필드를 단순화하면 다음과 같습니다:

FiberNode {
  type,
  pendingProps,
  memoizedProps,
  memoizedState,  // useState, useRef, useEffect 등이 저장됨
  updateQueue,
  child,
  sibling,
  return,
  alternate,      // 이전 렌더의 Fiber
}

여기서 memoizedState 내부에 hook들이 단일 연결 리스트(linked list)로 저장됩니다.

➡ 그래서 Hook은 "호출 순서가 중요"한 것
➡ 그래서 조건문 안에서 Hook을 호출하면 안 됨


9. Fiber와 Virtual DOM의 관계

개념역할
Virtual DOMrender 결과물 (UI 구조)
Fiberrender 작업 단위 + 상태 + 우선순위 + hook 저장

Virtual DOM은 UI의 스냅샷이지만
Fiber는 작업 단위이자 React의 모든 동작의 기반입니다.


10. 전체 요약

  • useRef는 DOM 접근뿐 아니라 렌더 간 값 유지에 사용하는 mutable 객체.

  • React는 useRef 객체를 Fiber의 hook 구조에 저장해 유지한다.

  • useRef 변경은 리렌더를 발생시키지 않음 → React 의존성 추적 대상이 아님.

  • DOM ref는 commit 단계에서 설정된다.

  • Fiber는 React가 렌더링을 중단·재개·우선순위 처리하기 위해 도입한 핵심 구조.

  • Fiber 안에 hook 정보(useRef, useState 등)가 저장되므로 렌더 사이 값이 유지됨.


마무리

React의 렌더링 최적화와 hook 시스템은 Fiber 아키텍처 위에서 돌아갑니다.
useRef는 단순한 DOM 접근 도구가 아니라, Fiber 구조 내부에서 렌더와 독립적으로 움직이는 mutable 캐시입니다.

이 개념을 이해하면:

  • 불필요한 렌더 방지

  • stale closure 문제 해결

  • 고급 상태 관리 패턴 설계

  • 성능 최적화

등에 큰 도움이 됩니다.


🧾 작성 참고

이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.

More from this blog

LangGraph의 astream_events: 에이전트 실행을 실시간으로 들여다보기

💡 한 줄 요약: astream_events()는 LangGraph 그래프 실행 중 발생하는 모든 이벤트(LLM 토큰, 툴 호출, 노드 시작/종료 등)를 실시간 스트림으로 받을 수 있는 비동기 API입니다. 들어가며 AI 에이전트가 응답을 생성하는 데 25초가 걸린다고 상상해보세요. 사용자는 평균 8초 후에 페이지를 이탈합니다. 하지만 스트리밍을 도입

Jun 20, 20266 min read

LLM은 어떻게 글을 읽고 쓰는가 — 입력부터 출력까지

대화형 AI에게 질문을 던지면, 마치 사람처럼 문장을 이해하고 답을 써 내려가는 것처럼 보인다.하지만 그 안에서 벌어지는 일은 생각보다 단순하고, 또 생각보다 기계적이다.이 글에서는 두 가지 질문에 답해 본다.LLM은 어떻게 글을 만들어내는가,그리고 우리가 입력한 프롬프트는 모델에 어떤 모습으로 들어가는가. 1부. LLM은 어떻게 글을 만들어내는가 핵심은

Jun 10, 20264 min read

useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가

React를 어느 정도 사용하다 보면 한 번쯤은“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다. useEffect가 계속 늘어나고 의존성 배열은 점점 길어지고 왜 실행되는지 이해하기 어려워지며 eslint-disable-next-line 이 늘어나는 상태 하지만 많은 사람들이 오해한다.useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다. 이 글에서는 ‘useEffect 지옥’의 ...

Jan 12, 20263 min read

Zod란 무엇인가: TypeScript에서 런타임 검증과 타입 안정성을 동시에 해결하는 방법

TypeScript 프로젝트를 하다 보면 반드시 마주치는 문제가 있다.바로 “타입은 있는데, 데이터는 믿을 수 없다”는 것이다. TypeScript는 컴파일 타임에는 강력하지만,런타임에 들어오는 데이터에 대해서는 아무런 보장을 해주지 않는다. API 요청 데이터 사용자 입력(Form) 환경 변수 (process.env) 외부 JSON / 설정 파일 이 모든 것은 타입 시스템 바깥에서 들어온다. 이 문제를 해결하기 위해 등장한 라이브러...

Jan 10, 20263 min read

Flatpak을 이해하기 위한 배경 지식

배포판, 라이브러리 충돌, 그리고 OSTree까지 리눅스 데스크톱에서 Flatpak이 표준처럼 자리 잡은 데에는 분명한 이유가 있다.그 이유를 이해하려면 단순히 “Flatpak은 패키징 시스템이다”를 넘어서,리눅스 배포판 구조, 라이브러리 버전 충돌, OSTree라는 기술까지 함께 이해해야 한다. 이 글에서는 다음 질문에 답해본다. 리눅스에서 말하는 배포판이란 무엇인가? 라이브러리 버전 충돌은 왜 발생하는가? Flatpak은 이 문제를 어...

Jan 8, 20265 min read

Dev note

32 posts