컴포넌트가 어떤 정보를 "기억"하도록 하고 싶지만 그 정보로 새로운 렌더링을 발생시키고 싶지는 않다면 ref를 사용하세요.
이 페이지에서는
- 컴포넌트에 ref를 어떻게 추가하는지
- ref의 값을 어떻게 업데이트하는지
- 상태와 ref가 어떻게 다른지
- 어떻게 안전히 ref를 사용하는지
를 알아볼 거예요.
Adding a ref to your component | 컴포넌트에 ref 추가하기
리액트에서 useRef
훅을 불러와서 컴포넌트에 ref를 추가할 수 있어요.
import { useRef } from 'react';
컴포넌트에서 useRef
훅을 호출하고 첫 번째 인자로 참조하고 싶은 초기값을 전달하세요. 예를 들어 아래 예시는 값이 0
인 ref에요.
const ref = useRef(0);
useRef
는 아래와 같은 객체를 반환해요.
{
current: 0 // useRef로 전달한 값
}

Illustrated by Rachel Lee Nabors (출처: react.dev)
ref.current
속성을 통해서 ref의 현재 값에 접근할 수 있어요. 이 값은 바꿀 수 있어요 즉, 이 값은 읽고 쓸 수 있어요. 리액트가 추적하지 않는 컴포넌트의 비밀 주머니라고 생각하세요. (리액트의 단방향 데이터 흐름에서 "해치를 피하도록" 만들어주는 부분이에요. 아래에서 더 알아볼게요!)
이 예시의 버튼은 클릭을 할 때마다 ref.current
를 증가시켜요.
ref는 숫자 타입을 가리키지만 상태처럼 아무 타입이나 가리킬 수 있어요. 문자열, 객체, 함수 등등 모두 가능해요. 상태와는 다르게 ref는 읽고 쓸 수 있는 current
속성을 가지는 일반적인 자바스크립트 객체에요.
컴포넌트는 매 증가마다 리렌더링되지 않는다는 사실을 기억하세요. 상태와 같이 ref는 리렌더링 중 리액트에 의해 유지돼요. 그러나 상태를 설정하면 컴포넌트를 리렌더링해요. ref를 변경하면 리렌더링되지 않아요!
Example: building a stopwatch | 예시: 스톱워치 만들기
하나의 컴포넌트에서 ref와 상태를 합칠 수 있어요. 사용자가 버튼을 눌러서 시작하거나 멈출 수 있는 스톱워치를 만들어볼게요. 사용자가 "Start" 를 누른 후에 시간이 얼마나 지났는지를 보여주기 위해서는 Start 버튼을 눌렀을 때와 현재 시각이 언제인지를 추적해야해요. 이 정보는 렌더링할 떄 사용되기 때문에 상태에서 갖고 있어야해요.
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
사용자가 "Start" 버튼을 누르면 매 10ms마다 업데이트를 시키기 위해 setInterval
을 사용해요.
"Stop" 버튼을 누르면 현재 유지되고 있는 인터벌을 취소하여 상태 변수 now
를 업데이트하지 않아야 해요. clearInterval
을 호출하면 되지만 "Start" 버튼을 눌렀을 때 호출한 setInterval
에서 반환된 인터벌 ID를 줘야해요. 어딘가에는 인터벌 ID를 보관해야겠죠. 인터벌 ID가 렌더링에 사용되지 않기 떄문에 ref 안에서 가지고 있으면 돼요.
정보의 일부가 렌더링에 사용될 때는 상태에서 갖고 있으면 돼요. 정보의 일부가 이벤트 핸들러에서만 필요하고 해당 정보를 변경하는 것이 리렌더링에 필요하지 않다면 ref를 사용하는 것이 더 효율적이에요.
Differences between refs and state | ref와 상태의 차이
아마 ref가 상태보다 덜 "엄격"한 것처럼 보일 수도 있어요. 예를 들어 상태를 설정하는 함수를 사용하는 대신 직접 변경할 수 있어요. 그러나 많으 경우에서는 상태를 사용할 거예요. ref는 여러분이 자주 필요하지는 않을 "해치 탈출 방법"이에요. 상태와 ref를 비교해볼게요.
ref | state |
useRef(initialValue)
는 { current: initialValue }
를 반환해요.useState(initialValue)
는 상태 변수의 현재 값과 상태 설정 함수([value, setValue]
)를 반환해요.
ref를 바꾸면 리렌더링이 발생하지 않아요.
상태를 바꾸면 리렌더링이 발생해요.
변경 가능해요. 렌더링 과정의 밖에 있는 current
의 값을 수정하고 업데이트할 수 있어요
변경하면 안돼요. 상태 변수를 수정하여 리렌더링을 대기열에 넣으려면 상태 설정 함수를 사용해야해요.
current
값을 렌더링하는 동안 읽을 수 없어요. (쓸 수도 없어요.)
언제나 상태를 읽을 수 있어요. 그런 각 렌더링은 바뀌지 않는 각 상태의 스냅샷을 가져요.
상태로 구현된 카운터 버튼 예시에요.
count
값이 보여지기 때문에 상태를 사용하는 것이 맞아요. 카운터의 값이 setCount()
로 설정될 때, 리액트는 컴포넌트를 리렌더링하고 화면은 새로운 count를 반영하여 업데이트 돼요.
만약 이를 ref를 사용하여 구현한다면 리액트는 컴포넌트를 리렌더링하지 않기 때문에 count가 바뀌지 않을 거예요! 이 버튼을 클릭하면 이 텍스트가 업데이트 되지 않는 것을 확인해보세요.
렌더링 중에 ref.current
를 읽는 것이 신뢰할 수 없는 코드를 만드는 이유가 바로 이거예요. 필요하다면 상태를 사용하세요.
useRef 는 내부에서 어떻게 동작하나요?더 알아보기useState
와useRef
모두 리액트에서 제공되는 훅이지만 원칙적으로useRef
는useState
의 상위에서 구현되어 있어요. 리액트 안에서useRef
는 아래와 같이 구현되어 있다고 생각할 수 있어요.// 리액트 내부 function useRef(initialValue) { const [ref, unused] = useState({ current: initialValue }); return ref; }
첫 번째 렌더링에서
useRef
는{ current: initialValue }
를 반환해요. 이 객체는 리액트에서 저장되기 때문에 다음 렌더링에서도 동일한 객체가 반환돼요. 상태 설정 함수가 이 예시에서 어떻게 사용되지 않는지를 보세요.useRef
는 항상 같은 객체를 반환하기 때문에 불필요해요.useRef
는 실제로 자주 사용되기 때문에 리액트는useRef
의 버전을 제공해요. 그러나 세터 없는 일반적인 상태 변수라고 생각하세요. 만약 객체 지향 프로그래밍에 익숙하다면ref
는 인스턴스 필드를 생각나게 할거에요. 하지만this.something
대신somethingRef.current
로 작성하면 돼요.
When to use refs | ref를 사용해야하는 때
일반적으로 컴포넌트가 리액트 외부에서 동작하는 단계가 필요하고 컴포넌트의 시각적인 부분에는 영향을 미치지 않는 브라우저 API와 같은 외부 API와 소통해야할 때는 ref를 사용하세요. 아래와 같은 드문 경우도 있어요.
- timeout ID 보관하기
- 다음 페이지를 덮는 DOM 엘리먼트 보관하고 조작하기
- JSX를 계산하는데 필요하지 않는 다른 객체 보관하기
만약 컴포넌트가 어떤 값을 저장해야하지만 렌더링 로직에 영향을 주고 싶지 않다면 ref를 선택하세요.
Best practices for refs | ref의 사용 예시
이 원칙들을 따르면 더욱 예측가능한 컴포넌트를 만들 수 있어요.
- ref를 해치 탈출 도구로 생각하세요. ref는 외부 시스템이나 브라우저 API와 작업할 때 유용해요. 만약 여러분의 어플리케이션 로직이나 데이터 플로우의 대부분이 ref에 의존한다면 방법을 다시 생각해보세요.
- 렌더링을 하는 동안
ref.current
를 읽거나 쓰지 마세요. 만약 어떤 정보가 렌더링을 하는 동안 필요하다면 상태를 사용하세요. 리액트는ref.current
가 언제 변했는지 모르기 때문에 렌더링 하는 동안 읽는 행위도 컴포넌트의 행위도 예측하기 어렵게 만들어요. (최초 렌더링에서만 ref를 설정하는if (!ref.current) ref.current = new Thing()
와 같은 코드가 유일한 예외예요.)
리액트 상태의 제약조건은 ref에 적용되지 않아요. 예를 들어 상태는 매 렌더링마다 스냅샷처럼 작동하고 동기적으로 업데이트 되지 않아요. 그러나 ref는 현재 값을 바꾸면 즉시 변경돼요.
ref.current = 5;
console.log(ref.current); // 5
왜냐하면 ref 자체가 일반적인 자바스크립트 객체이기 때문에 이렇게 동작해요.
ref로 작업을 할 때 변이를 피하는 것은 고민할 필요가 없어요. 바꾸는 객체가 렌더링에 사용되지 않는 한 리액트는 ref나 ref의 콘텐츠로 무엇을 하는지는 신경쓰지 않아요.
Refs and the DOM | ref와 DOM
어떤 값이든 ref로 가리킬 수 있어요. 그러나 ref를 사용한 가장 흔한 케이스는 DOM 엘리먼트에 접근하는 거예요. 예를 들어 만약 프로그래밍적으로 입력창을 포커싱하고 싶다면 ref를 사용하면 편리해요. <div ref={myRef}>
처럼 JSX의 ref
속성에 ref를 전달할 때, 리액트는 해당하는 DOM 엘리먼트에 myRef.current
를 추가해요. 엘리먼트가 DOM에서 제거되면 리액트는 myRef.current
를 null
로 업데이트해요. ref로 DOM을 조작하기 문서에서 이에 대해 더 알아보세요.
Recap | 요약
- ref는 렌더링에 사용되지 않는 값을 갖고 있어서 해치를 탈출할 수 있어요. 자주 사용할 일은 없어요.
- ref는
current
라고 불리는 단일 속성을 가진 일반적인 자바스크립트 객체이고 읽거나 설정할 수 있어요. - 리액트가
useRef
훅을 호출하여 ref를 달라고 리액트에 요청할 수 있어요. - 상태처럼 ref는 컴포넌트의 리렌더링 사이에 정보를 유지하도록 만들어줘요.
- 상태와는 달리 ref의
current
값을 설정해도 리렌더링을 발생시키지 않아요. - 렌더링을 하는 동안
ref.current
를 읽거나 쓰지 마세요. 컴포넌트를 예측하기 어렵게 만들거예요.
Challenges | 도전 과제
1. 고장난 채팅 입력창 고치기
메시지를 입력하고 "Send"를 누르세요. "Send" alert를 보기 전에 3초 간의 지연이 있다는 사실을 알아차릴 수 있을 거예요. 지연시간 동안, "Undo" 버튼을 볼 수 있어요. 눌러보세요. 이 버튼으로 "Sent!" 메시지가 보이지 않을 것이라고 예상할 거예요. handleSend
동안 저장된 timeout ID로 clearTimeout
를 호출하여 이 작업을 진행할 수 있어요. 그러나 "Undo"가 클릭된 이후에도 "Sent!" 메시지는 여전히 떠요. 이유를 찾고 고쳐보세요.
HINT
`let timeoutID`와 같은 일반적인 변수는 리렌더링 동안 "살아남지" 않아요. 모든 렌더링은 컴포넌트를 스크래치에서 실행시키기 때문이에요.(그리고 변수를 초기화시켜요.) timeout ID를 어디에선가 갖고 있을 수 있을까요?
SOLUTION
(상태를 설정할 떄와 같이) 컴포넌트가 리렌더링 될 때마다 모든 지역 변수는 스크래치에서 초기화돼요. 그래서 timeout ID를 `timeoutID`와 같은 지역 변수에 넣어서 저장할 수 없고 미래에 다른 이벤트 핸들러가 그것을 "본다"고 기대할 수 없어요. 대신, ref에 저장하면 리액트는 렌더링 사이에 이를 보존해요.
2. 리렌더링에 실패한 컴포넌트 고치기
이 버튼은 "On"과 "Off" 상태를 변경해줄 것처럼 보여요. 그러나 항상 "Off"만을 보여줘요. 이 코드에 무슨 문제가 있는 걸까요? 해결해보세요.
SOLUTION
이 예시에서 ref의 현재 값은 렌더링 결과인 {isOnRef.current ? 'On' : 'Off'}
를 계산하는데 사용해요. 그렇기 때문에 이 정보는 ref 안에 있으면 안되고 상태에 넣어야만해요. 이를 고치기 위해서는 ref를 제거하고 상태를 사용하면 돼요.
3. 디바운싱 고치기
이 예시에서 모든 버튼 클릭 핸들러는 "디바운싱"돼요. 무슨 의미인지를 확인하려면 버튼 중 하나를 눌러보세요. 1초 후에 메시지가 어떻게 보이는지를 보세요. 만약 메시지를 기다리를 동안 버튼을 누른다면 타이머는 초기화돼요. 그래서 만약 같은 버튼을 빠르게 여러번 누른다면 메시지는 클릭을 멈춘 이후에 몇 초동안 보이지 않을 거예요. 디바운싱은 사용자가 "무엇을 하는 것을 멈출 때"까지 행동을 지연시키는 행동을 의미해요.
이 예시는 작동하지만 의도한 대로 작동하는 것은 아니에요. 버튼은 독립적이지 않아요. 문제를 보기 위해서는 버튼 중 하나를 클릭하고 즉시 다른 버튼을 클릭하세요. 지연 이후에 두 버튼의 메시지를 볼 것이라고 예상하지만 마지막 버튼의 메시지만 나올 거예요. 첫 번쨰 버튼의 메시지는 사라져요.
왜 이 버튼은 서로를 방해하는 걸까요? 이유를 찾고 문제를 해결하세요.
HINT
마지막 timeout ID 값은 모든 `DebouncedButton` 컴포넌트에서 공유돼요. 그래서 버튼을 클릭하는 것은 다른 버튼의 timeout을 초기화해요. 각 버튼의 timeout ID를 분리해서 저장할 수 있나요?
SOLUTION
`timeoutID`와 같은 변수는 모든 컴포넌트에서 공유돼요. 그래서 두 번쨰 버튼을 누르면 첫 번째 버튼의 지연된 timeout이 초기화돼요. 이를 해결하려면 ref에서 timeout을 갖고 있으세요. 각 버튼은 각각의 ref를 가지게 되어 서로 충돌이 발생하지 않아요. 두 버튼을 빠르게 누르면 두 메시지를 모두 보여줄 거예요.
4. 최근 상태 읽기
이 예시에서 "Send" 버튼을 누르면 메시지가 보여지기 전까지 작은 지연이 발생해요. "hello"를 입력하고 "Send"를 누른 후 빠르게 입력창을 다시 수정하세요. 여러분이 수정 했음에도 불구하고 alert는 여전히 (버튼을 클릭한 때의 상태 값인) "hello"를 보여줄 거예요.
보통 이 행동은 app안에서 실행하고 싶은 행동이에요. 하지만 어떤 상태의 최근 상태를 읽기 위해 비동기 코드가 필요한 드문 케이스가 있어요. 클릭할 때의 상태가 아니라 현재 입력 텍스트를 alert가 보여주도록 하는 방법을 생각할 수 있나요?
SOLUTION
상태는 [스냅샷처럼 ]() 동작하기 떄문에 timeout과 같은 비동기 연산에서 최근 상태를 읽어올 수 없어요. 그러나 ref에서 최근 입력창의 텍스트를 보관할 수 있어요. ref는 변경이 가능하기 떄문에 `current` 속성을 언제든 읽을 수 있어요. 이 예시에서 현재 텍스트가 렌더링에 사용되기 때문에 (렌더링을 위한) 상태 변수와 (timeout 안에서 읽는) ref 모두에서 필요해요. 현재 ref 값을 직접 업데이트 해줘야해요.