Escape Hatches : Refs로 값 참조하기
컴포넌트가 특정 정보를 '기억'하도록 하고 싶지만, 해당 정보가 새 렌더링을 트리거하지 않도록 하려는 경우 ref를 사용할 수 있습니다.
학습 내용은 다음과 같습니다.
- 컴포넌트에 참조를 추가하는 방법
- 참조 값을 업데이트하는 방법
- state와 ref의 차이점
- 참조를 안전하게 사용하는 방법
컴포넌트에 참조 추가하기
React에서 useRef Hook을 가져와서 컴포넌트에 참조를 추가할 수 있습니다:
import { useRef } from 'react';
컴포넌트 내에서 useRef Hook을 호출하고 참조하려는 초기 값을 유일한 인수로 전달합니다. 예를 들어 다음은 값 0에 대한 참조입니다:
const ref = useRef(0);
useRef는 다음과 같은 객체를 반환합니다:
{
current: 0 // useRef에 전달한 값
}
ref.current 속성을 통해 해당 참조의 현재 값에 액세스할 수 있습니다.
이 값은 의도적으로 변경 가능하므로 읽기와 쓰기가 모두 가능합니다. React가 추적하지 않는 컴포넌트의 비밀 주머니와 같습니다.
(이것이 바로 React의 단방향 데이터 흐름에서 "탈출구"가 되는 이유입니다. 아래에서 자세히 설명합니다!)
여기서 버튼은 클릭할 때마다 ref.current를 증가시킵니다:
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
ref는 숫자를 가리키지만 state와 마찬가지로 문자열, 객체, 함수 등 무엇이든 가리킬 수 있습니다.
state와 달리 ref는 현재 속성을 읽고 수정할 수 있는 일반 자바스크립트 객체입니다.
컴포넌트가 변화(increment)할 때마다 다시 렌더링되지 않는다는 점에 유의하세요. state와 마찬가지로 ref는 리렌더링 사이에 React에 의해 유지됩니다. 하지만 state를 설정하면 컴포넌트가 다시 렌더링됩니다. ref를 변경하면 그렇지 않습니다!
예제: 스톱워치 만들기
ref와 state를 단일 컴포넌트로 결합할 수 있습니다. 예를 들어 사용자가 버튼을 눌러 시작하거나 중지할 수 있는 스톱워치를 만들어 봅시다. 사용자가 '시작'을 누른 후 얼마나 시간이 지났는지 표시하려면 시작 버튼을 누른 시점과 현재 시간을 추적해야 합니다. 이 정보는 렌더링에 사용되므로 상태를 유지해야 합니다:
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
사용자가 '시작'을 누르면 10밀리초마다 시간을 업데이트하기 위해 setInterval을 사용합니다:
import { useState } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
function handleStart() {
// Start counting.
setStartTime(Date.now());
setNow(Date.now());
setInterval(() => {
// Update the current time every 10ms.
setNow(Date.now());
}, 10);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
</>
);
}
"Stop" 버튼을 누르면 현재 상태 변수의 업데이트를 중지하도록 기존 interval을 취소해야 합니다. 이 작업은 clearInterval을 호출하여 수행할 수 있지만, 사용자가 시작을 눌렀을 때 이전에 setInterval 호출에서 반환한 interval ID를 제공해야 합니다. 따라서 interval ID를 어딘가에 보관해야 합니다. interval ID는 렌더링에 사용되지 않으므로 ref에 보관할 수 있습니다:
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
렌더링에 정보가 사용되는 경우 해당 정보를 state로 유지하세요.
이벤트 핸들러만을 필요로 하고, 변경하면 다시 렌더링할 필요가 없는 경우에는 ref를 사용하는 것이 더 효율적일 수 있습니다.
ref와 state의 차이점
state setting 함수를 항상 사용할 필요 없이 변경할 수 있기 때문에 ref가 state보다 덜 '엄격'하다고 생각할 수 있습니다. 하지만 대부분의 경우 state를 사용하고 싶을 것입니다. ref는 자주 사용하지 않는 "탈출구"입니다. 상태와 참조를 비교하는 방법은 다음과 같습니다:
refs :
- useRef(initialValue)는 { current: initialValue } 를 반환합니다.
- 변경 시 다시 렌더링이 트리거되지 않습니다.
- 변경 가능 - 렌더링 프로세스 외부에서 현재 값을 수정하고 업데이트할 수 있습니다.
- 렌더링 중에는 현재 값을 읽거나 쓰지 않아야 합니다.
state :
- useState(initialValue)는 상태 변수의 현재 값과 상태 설정자 함수([value, setValue])를 반환합니다.
- 변경 시 렌더링을 다시 트리거합니다.
- "불변" - state setting 기능(setState)을 사용하여 상태 변수를 수정하여 다시 렌더링을 대기열에 추가해야 합니다.
- 언제든지 상태를 읽을 수 있습니다. 그러나 각 렌더링에는 변경되지 않는 자체 상태 스냅샷이 있습니다.
다음은 상태와 함께 구현된 카운터 버튼입니다:
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You clicked {count} times
</button>
);
}
카운트 값이 표시되므로 state값을 사용하는 것이 합리적입니다. 카운터의 값이 setCount()로 설정되면 React는 컴포넌트를 다시 렌더링하고 화면이 새로운 카운트를 반영하도록 업데이트합니다.
만약 이것을 ref로 구현하려고 한다면, React는 컴포넌트를 다시 렌더링하지 않으므로 카운트가 변경되는 것을 볼 수 없을 것입니다!
이 버튼을 클릭해도 텍스트가 업데이트되지 않는것을 확인하세요:
import { useRef } from 'react';
export default function Counter() {
let countRef = useRef(0);
function handleClick() {
// 이것은 컴포넌트를 다시 렌더링하지 않습니다!
countRef.current = countRef.current + 1;
}
return (
<button onClick={handleClick}>
You clicked {countRef.current} times
</button>
);
}
렌더링 중에 ref.current를 읽으면 코드가 불안정해지는 이유입니다. 필요하다면 state를 대신 사용하세요.
Deep Dive - useRef는 내부적으로 어떻게 동작하나요?
useState와 useRef는 모두 React에서 제공하지만, 원칙적으로 useRef는 useState 위에 구현될 수 있습니다.
React 내부에서 useRef는 다음과 같이 구현된다고 상상할 수 있습니다:
// 리액트 내부
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
첫 번째 렌더링 중에 useRef는 { current: initialValue }를 반환합니다. 이 객체는 React에 의해 저장되므로 다음 렌더링 중에 동일한 객체가 반환됩니다. 이 예제에서 state setter가 어떻게 사용되지 않는지 주목하세요. useRef는 항상 동일한 객체를 반환해야 하기 때문에 불필요합니다!
React는 useRef의 기본 버전을 제공하는데, 이는 실제로 충분히 일반적이기 때문입니다. setter가 없는 일반 상태 변수라고 생각하면 됩니다. 객체지향 프로그래밍에 익숙하다면 ref가 인스턴스 필드를 떠올리게 하겠지만, this.something 대신 somethingRef.current를 작성하면 됩니다.
ref를 사용하는 경우
일반적으로 컴포넌트가 React를 "외부로 나가서" 외부 API, 즉 컴포넌트의 모양에 영향을 주지 않는 브라우저 API와 통신해야 할 때 ref를 사용합니다. 다음은 이러한 드문 상황 중 몇 가지입니다:
- 타임아웃 ID를 저장할때
- (다음 페이지에서 다룰) DOM 엘리먼트를 저장 및 조작할때
- JSX를 계산하는 데 필요하지 않은 다른 객체를 저장하는 경우
컴포넌트에 일부 값을 저장해야 하지만, 렌더링 로직에 영향을 미치지 않는 경우 ref를 선택하세요.
ref를 위한 모범사례
다음 원칙을 따르면 컴포넌트를 더 예측 가능하게 만들 수 있습니다:
- ref를 탈출구로 취급하세요.
ref는 외부 시스템이나 브라우저 API로 작업할 때 유용합니다.
애플리케이션 로직과 데이터 흐름의 대부분이 ref에 의존하는 경우 접근 방식을 재고해 보세요. - 렌더링 중에는 ref.current를 읽거나 쓰지 마세요.
렌더링 중에 일부 정보가 필요하다면 state를 대신 사용하세요. React는 ref.current가 언제 변경되는지 모르기 때문에 렌더링 중에 읽어도 컴포넌트의 동작을 예측하기 어렵습니다. (유일한 예외는 첫 번째 렌더링 중에 ref를 한 번만 설정하는 if (!ref.current) ref.current = new Thing()과 같은 코드입니다).
React state의 제한은 ref에는 적용되지 않습니다. 예를 들어 state는 모든 렌더링에 대해 스냅샷처럼 작동하며 동기적으로 업데이트되지 않습니다. 하지만 ref의 현재 값을 변경하면 즉시 변경됩니다:
ref.current = 5;
console.log(ref.current); // 5
이는 ref 자체가 일반 자바스크립트 객체이므로 자바스크립트 객체처럼 동작하기 때문입니다.
또한 ref로 작업할 때 변형을 피하는 것에 대해 걱정할 필요가 없습니다.
변이하려는 객체가 렌더링에 사용되지 않는 한, React는 ref나 그 콘텐츠로 무엇을 하든 상관하지 않습니다.
참조와 DOM
참조는 모든 값을 가리킬 수 있습니다. 그러나 참조의 가장 일반적인 사용 사례는 DOM 요소에 액세스하는 것입니다.
예를 들어 프로그래밍 방식으로 입력에 초점을 맞추고자 할 때 유용합니다.
<div ref={myRef}>와 같이 JSX의 ref 어트리뷰트에 ref를 전달하면 React는 해당 DOM 엘리먼트를 myRef.current에 넣습니다. 이에 대한 자세한 내용은 ref로 DOM 조작하기(다음 장)에서 확인할 수 있습니다.
요약
- ref는 렌더링에 사용되지 않는 값을 유지하기 위한 탈출구입니다. 자주 필요하지 않습니다.
- ref는 현재라는 단일 props를 가진 일반 자바스크립트 객체로, 읽거나 설정할 수 있습니다.
- useRef Hook을 호출하여 React에 ref를 제공하도록 요청할 수 있습니다.
- state와 마찬가지로 ref를 사용하면 컴포넌트의 재렌더링 사이에 정보를 유지할 수 있습니다.
- state와 달리 ref의 현재 값을 설정해도 리렌더링이 트리거되지 않습니다.
- 렌더링 중에는 ref.current를 읽거나 쓰지 마세요. 이렇게 하면 컴포넌트를 예측하기 어렵습니다.
과제 1 / 4: 깨진 채팅 입력 수정하기
메시지를 입력하고 "보내기"를 클릭합니다. "전송되었습니다!" 알림이 표시되기 전에 3초 정도 지연되는 것을 볼 수 있습니다. 이 지연 시간 동안 "실행 취소" 버튼이 표시됩니다. 이 버튼을 클릭합니다. 이 "실행 취소" 버튼은 "전송됨!" 메시지가 표시되지 않도록 하기 위한 것입니다. 이 버튼은 handleSend 중에 저장된 시간 초과 ID에 대해 clearTimeout을 호출하여 이를 수행합니다. 그러나 "실행 취소"를 클릭한 후에도 "전송됨!" 메시지가 계속 표시됩니다. 작동하지 않는 이유를 찾아서 수정하세요.
https://codesandbox.io/s/fix-a-broken-chat-input-xs2dng?file=/App.js
과제 2/4: 컴포넌트가 다시 렌더링되지 않는 문제 해결
이 버튼은 "켜짐"과 "꺼짐" 표시를 전환해야 합니다. 그러나 항상 "꺼짐"으로 표시됩니다. 이 코드에 어떤 문제가 있나요? 수정하세요.
https://codesandbox.io/s/fix-a-component-failing-to-re-render-driqtk?file=/App.js
과제 3/4: 디바운싱 수정
이 예제에서는 모든 버튼 클릭 핸들러가 "디바운스"되어 있습니다. 이것이 무엇을 의미하는지 확인하려면 버튼 중 하나를 눌러 보세요. 메시지가 1초 후에 어떻게 표시되는지 확인하세요. 메시지를 기다리는 동안 버튼을 누르면 타이머가 재설정됩니다. 따라서 같은 버튼을 계속 빠르게 여러 번 클릭하면 클릭을 멈춘 후 1초가 지나야 메시지가 표시됩니다. 디바운싱을 사용하면 사용자가 "작업을 중단"할 때까지 일부 작업을 지연시킬 수 있습니다.
이 예제는 작동하지만 의도한 대로 작동하지 않습니다. 버튼이 독립적이지 않습니다. 문제를 확인하려면 버튼 중 하나를 클릭한 다음 즉시 다른 버튼을 클릭하세요. 잠시 후 두 버튼의 메시지가 모두 표시될 것으로 예상할 수 있습니다. 하지만 마지막 버튼의 메시지만 표시됩니다. 첫 번째 버튼의 메시지는 사라집니다.
버튼이 서로 간섭하는 이유는 무엇인가요? 문제를 찾아서 해결하세요.
https://codesandbox.io/s/fix-debouncing-mr9m7r?file=/App.js
과제 4/4: 최신 상태 읽기
이 예에서는 "보내기"를 누른 후 메시지가 표시되기까지 약간의 지연 시간이 있습니다. "hello"를 입력하고 보내기를 누른 다음 입력을 다시 빠르게 편집합니다. 편집한 후에도 알림에는 여전히 "hello"(버튼을 클릭할 당시의 상태 값)가 표시됩니다.
일반적으로 이 동작은 앱에서 원하는 동작입니다. 하지만 일부 비동기 코드에서 특정 상태의 최신 버전을 읽어야 하는 경우가 가끔 있을 수 있습니다. 알림에 클릭 당시의 텍스트가 아닌 현재 입력 텍스트를 표시하는 방법을 생각해낼 수 있나요?
https://codesandbox.io/s/read-the-latest-state-l849gu?file=/App.js
발표요약)
1. useRef훅으로 참조를 추가 할 수 있다. <input ref={customRef}/>
2. ref는 현재값을 설정해도 리렌더링을 트리거 하지 않는다. (렌더링에 사용되지않는 값을 유지하여 사용할 때 필요)
3. 컴포넌트의 재렌더링 사이에 정보를 유지할 수 있다.
4. 렌더링 도중 읽거나/쓰기는 하지말것. (예측하기 어려움)