DOM 연결 외 Refs 활용법

장효정·2025년 12월 2일

Udemy React 강의 137번 정리

이 강의에서는 ref의 두 번째 핵심 용도를 다룬다. DOM 요소 말고도 UI에 직접 영향 없는 값을 저장할 때 ref를 쓸 수 있다는 점.
특히 타이머 예제를 통해 왜 변수나 useState로는 안 되고, 왜 ref가 정답인지 설명한다.

1. 문제 : 타이머를 정지해야 하는데,,,

setTimeout이 반환하는 타이머는 아래와 같이 사용할 수 있다.

const timer = setTimeout(() => {
  setTimerExpired(true)
}, targetTime * 1000)

clearTimeout(timer)

즉, 타이머를 멈추려면 타이머 ID(pointer)가 필요함.

그래서 처음엔 이렇게 생각할 수 있다.

let timer // 타이머 ID 저장

function handleStart() {
  timer = setTimeout(...)
}
                     
function handleStop() {
  clearTimeout(timer)
}

하지만 이렇게 하면 제대로 작동하지 않는다.

2. 왜 단순 변수(let)로는 안 될까?

문제를 일으키는 핵심 이유 2가지 :

1) 컴포넌트 렌더링마다 변수 재생성

React는 state가 바뀌면 컴포넌트를 재실행(re-run)한다.
그리고 컴포넌트 안의 let timer는 렌더링이 다시 일어날 때마다 새로 선언되고, 이전 값은 사라진다.
즉, handleStart에 저장한 타이머 ID가 handleStop 실행 시점에는 사라져 버린 상태가 된다.
그래서 clearTimeout()이 제대로 호출되지 않음.

2) 여러 컴포넌트 인스턴스 사이에 공유됨

이 문제는 더 심각하다.
강의 예제처럼 5초 타이머 / 1초 타이머 두 개가 따로 렌더링된다면 어떻게 될까?

let timer

이 변수가 컴포넌트 함수 밖에 선언된다면, 두 컴포넌트 인스턴스가 같은 timer 변수를 공유하게 된다.

결과는,
1. 5초 타이머 시작 → timer = 5초 타이머 ID
2. 1초 타이머 시작 → timer = 1초 타이머 ID로 덮어씀
3. 5초 타이머 stop 누르면? → 이미 ID가 덮었음 → 5초 타이머는 안 멈춰짐
4. 결국 You lost! 뜸

즉, 컴포넌트 별로 독립된 타이머가 필요한데, 변수는 전부 공유된다.

3. 그렇다면 state로 저장하면 되지 않을까?

state를 사용하면, 또 다른 문제가 발생한다.

state로 타이머 ID를 저장하면,

const [timerId, setTimerId] = useState()

문제는,

  • state 변경 → 리렌더 발생
  • 리렌더 → setTimeout 사이드 이펙트 재실행 위험
  • 리렌더는 타이머 관리 목적과 맞지 않음
  • 타이머 ID 자체는 UI에 전혀 영향 없음

즉, UI를 바꿀 필요 없는 값 때문에 리렌더를 유발하면 불필요한 성능 소모가 발생한다.

4. 그래서 ref가 정답이다

ref는 다음과 같은 특징을 가지기 때문.

1) 리렌더 사이에서도 값이 유지됨 (state처럼 유지됨)

ref.current는 컴포넌트가 여러 번 렌더링되어도 값이 보존됨.
즉, handleStart에서 저장한 ID → handleStop에서도 그대로 살아 있음.

2) 컴포넌트마다 ref가 독립됨

각 TimerChallenge 컴포넌트마다 샌드박스처럼 독립된 ref 생성됨.
즉, 5초/1초 타이머가 서로 타이머 ID를 덮어쓰지 않음.

3) ref 변경은 리렌더를 유발하지 않음

state와 반대로 ref.current 변경은 리렌더를 유발하지 않고, UI에 영향을 주지 않는 값 관리에 최적함.

코드로 보자.

const timer = useRef(); 

function handleStart() {
  timer.current = setTimeout(() => {
    setTimerExpired(true);
  }, targetTime * 1000);
}

function handleStop() {
  clearTimeout(timer.current);
}
  • 컴포넌트가 리렌더 되어도 timer.current 값 유지
  • 여러 컴포넌트가 있어도 ID 충돌 없음
  • UI 영향 받지 않음 (불필요한 리렌더 없음)

5. 최종 요약

변수(let)

  • 렌더마다 초기화됨 → 값 보존 불가
  • 여러 컴포넌트 인스턴스에서 공유됨 → 충돌 발생

state

  • 값 유지됨
  • 하지만 변경 시 리렌더 → UI에 필요 없는 렌더 발생

ref

  • 값 유지됨
  • 컴포넌트마다 독립됨
  • 변경해도 리렌더 없음 (UI 영향 없는 값에 최적)

그래서 타이머 ID 같은 값은 ref로 관리하는 것이 정답이다.


0개의 댓글