6th 코드 로그 · React Hooks: useRef

허정석·2025년 7월 20일

TIL

목록 보기
6/19
post-thumbnail

useRef

🤖 Gemini 설명문

useRef의 가장 중요한 임무는

"컴포넌트가 몇 번을 리렌더링하든, 절대 변하지 않는 자신만의 보관함(객체)을 갖는 것"

입니다.


함수 컴포넌트를 "매일 출근하는 직원"이라고 상상해 보세요.

  • 첫 출근 날 (첫 렌더링): 회사는 직원에게 개인 사물함을 하나 줍니다. 이 사물함은 이 직원이 퇴사하기 전까지는
    절대 바뀌지 않는, 오직 이 직원만의 사물함입니다.
  • 그 이후 매일 출근할 때 (리렌더링): 직원은 매일 어제 썼던 바로 그 사물함을 그대로 사용합니다. 회사가 매일 새로운 사물함을 주지 않죠.

여기서 useRef가 하는 일이 바로 이 "개인 사물함"을 만들어주는 것입니다.


코드와 비유 연결하기


import { useState } from "react";

export function useRef<T>(initialValue: T): { current: T } {
  const [ref] = useState(() => ({ current: initialValue }));
  return ref;
}
  • 첫 렌더링 (첫 출근 날)

    컴포넌트가 처음 실행되면 useState({ current: initialValue }) 코드가 호출됩니다.

    1. React는 "아, 이 컴포넌트가 처음이네. 개인 사물함(useState의 저장 공간)을 만들어 줘야겠다." 라고 생각합니다.
    2. { current: initialValue } 라는 객체(사물함 내용물)를 만듭니다.
    3. React는 이 객체를 방금 만든 개인 사물함에 넣고 잠급니다. 이 작업은 오직 맨 처음 한 번만 일어납니다.
    4. useState는 사물함에 넣어둔 그 객체를 refObject라는 변수에 담아줍니다.
    5. useRef는 이 refObject(객체)를 반환합니다.
  • 리렌더링 (다음 출근 날)

    컴포넌트의 상태가 바뀌어서 리렌더링이 발생하면, useRef 함수 안의 코드는 다시 실행됩니다.

    1. useState({ current: initialValue }) 코드가 또 호출됩니다.
    2. 하지만 React는 "어? 이 컴포넌트는 이미 개인 사물함이 있잖아?" 라는 것을 기억하고 있습니다.
    3. 그래서 useState의 인자로 들어온 { current: initialValue }를 완전히 무시합니다. (새로운 사물함을 주지 않습니다!)
    4. 대신, 첫날 만들어서 잠가 두었던 바로 그 사물함을 열어서 내용물(객체)을 꺼내 refObject 변수에 다시 담아줍니다.
    5. useRef는 결국 첫날 받았던 것과 완전히 똑같은 그 객체를 또 반환합니다.

📍결론

useState는 보통 [값, 값을 바꾸는 함수] 형태로 상태를 바꿀 때 쓰지만, 여기서는 그 기능을 쓰는 게 아닙니다.

useState의 "초기값은 맨 처음 딱 한 번만 사용되고, 그 이후에는 무시된다" 는 숨겨진 특징을 이용해서, 리렌더링 되어도 절대 변하지않는 "고정된 값(여기서는 객체)"을 만들어내는 "저장소" 역할로 활용한 것입니다.

이것이 바로 useRef가 useState로 구현될 수 있는 이유입니다.

how to remember

React는 컴포넌트와 훅의 호출 순서를 이용해 기억합니다.

React가 이 모든 것을 기억할 수 있는 이유는, 훅이 호출되는 순서가 항상 같을 것이라고 믿기 때문입니다.

React 내부에는 각 컴포넌트마다 훅의 데이터를 저장하는 순서가 있는 리스트(배열)가 있습니다.
e.g) * MyComponent의 메모리: [ 훅1_데이터, 훅2_데이터, 훅3_데이터, ... ]

리렌더링될 때, React는 컴포넌트 코드를 다시 실행하면서 훅을 순서대로 만납니다.

  • 첫 번째 useState를 만나면 -> 메모리 리스트의 첫 번째 칸에서 데이터를 꺼내옵니다.
  • 두 번째 useRef를 만나면 -> 메모리 리스트의 두 번째 칸에서 데이터를 꺼내옵니다.
  • ... 이런 식으로 계속됩니다.

"훅은 조건문이나 반복문 안에서 호출하면 안 된다"는 규칙이 있는 이유이며,

   1 if (someCondition) {
   2   useState(0); // 어떨 때는 호출되고, 어떨 때는 안 됨
   3 }
   4 useRef("cup");

React는 순서가 엉키면 어떤 데이터를 어디서 가져와야 할지 모르기 때문에 에러를 발생시키는 것 입니다.

📍 React는 컴포넌트별로 숨겨진 메모리 공간을 가지고 있고, 그 공간에 훅이 호출되는 순서대로 상태 값을 저장합니다.
리렌더링 시에는 그 순서에 맞춰 저장된 값을 다시 꺼내주는 방식으로 상태를 "기억"합니다.


구현 코드

초기 코드

import { useState } from "react";

export function useRef<T>(initialValue: T): { current: T } {
  /* 최초 랜더링 시에 딱 한 번만 무언가를 만들고 그 참조를 계속 유지 */
  // 지연 초기화
  const [value] = useState(() => createExpensiveObject({ current: initialValue }));
  return value;
}
/**
 * 제네릭 타입 명시
 * 1. createExpensiveObject 함수의 타입 지정
 * 2. useRef<T> 와 useState 내부 값 타입이 정확히 매칭
 * */
function createExpensiveObject<T>(value: { current: T }): { current: T } {
  return value;
}
  1. 지연 초기화(Lazy initialization)

리팩토링

import { useState } from "react";

export function useRef<T>(initialValue: T): { current: T } {
  const [ref] = useState(() => ({ current: initialValue }));
  return ref;
}
  • 함수 (createExpensiveObject) 제거
    → 단순히 값을 반환하기만 하므로 불필요
  • 타입 선언도 생략
    → 추론에 맡김

8개의 댓글

comment-user-thumbnail
2025년 7월 21일

정돌쓰 퀄리티 무슨 일,, 내용 재밌고 깔끔함 👍

1개의 답글
comment-user-thumbnail
2025년 7월 21일

글 잘 봤습니다! 그런데 지연 초기화가 꼭 필요할까요?

1개의 답글