React -> 렌더링 결과물에 맞춰 DOM 변경을 자동으로 처리
React가 관리하는 DOM 노드에 접근하기 위해 useRef Hook을 가져온다.
import { useRef } from 'react';
컴포넌트 안에서 ref를 선언하기 위해 훅 사용
const myRef = useRef(null);
ref를 DOM 노드로 가져와야하는 JSX tag에 ref 속성으로 전달
<div ref={myRef}>
useRef Hook -> current라는 단일 속성을 가진 객체 반환
초기에는 myRef.current가 null
React가 이 div에 대한 DOM 노드를 생성할 때, React는 이 노드에 대한 참조를 myRef.current에 넣는다.
이 DOM 노드를 이벤트 핸들러에서 접근하거나 노드에 정의된 내장 브라우저 API를 사용 가능
myRef.current.scrrollIntoView();
버튼을 클릭하면 input 요소로 포커스 이동
import { useRef } from 'react';
export default function Form(){
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} /> // 이 <input>의 DOM 노드를 inputRef.current에 넣어줘
<button onClick={handleClick}>
focut the input
</button>
</>
);
}
이미지 3개가 있는 캐러셀, 각 버튼은 브라우저 scrollIntoView() 메서드를 해당 DOM 노드로 호출하여 이미지를 중앙에 배치
import { useRef } from 'react';
export default function CatFriends() {
const firstCatRef = useRef(null);
const secondCatRef = useRef(null);
const thirdCatRef = useRef(null);
function handleScrollToFirstCat() {
firstCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function handleScrollToSecondCat() {
secondCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
}
);
}
function handleScrollToThirdCat() {
thirdCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
return (
<>
<nav>
<button onClick={handleScrollToFirstCat}>
Neo
</button>
<button onClick={handleScrollToSecondCat}>
Millie
</button>
<button onClick={handleScrollToThirdCat}>
Bella
</button>
</nav>
<div>
<ul>
<li>
<img
src="https://placecats.com/neo/300/200"
alt="Neo"
ref={firstCatRef}
/>
</li>
<li>
<img
src="https://placecats.com/millie/200/200"
alt="Millie"
ref={secondCatRef}
/>
</li>
<li>
<img
src="https://placecats.com/bella/199/200"
alt="Bella"
ref={thirdCatRef}
/>
</li>
</ul>
</div>
</>
);
}
때때로 목록의 아이템마다 ref가 필요할 수도 있고, 얼마나 많은 ref가 필요할지 예측할 수 없는 경우도 있다.
<ul>
{items.map((item) => {
// 작동하지 않는다.
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Hook은 컴포넌트의 최상단에서만 호출되어야 하기 때문이다
useRef를 반복문, 조건문 혹은 map() 안 쪽에서 호출할 수 없다.
문제를 해결하는 방법
-또 다른 해결책은 ref 속성에 함수를 전달하는 것 => ref 콜백이라고 한다.
React는 ref를 설정할 때 DOM 노드와 함께 ref 콜백을 호출하며, ref를 지울 때에는 null을 전달한다.
-> 이를 통해 자체 배열이나 Map을 유지하고, 인덱스나 특정 id를 사용하여 어떤 ref에든 접근 가능
긴 리스트에서 특정 노드에 스크롤하기 예시:
import { useRef, useState } from "react";
export default function CatFriends() {
const itemsRef = useRef(null);
const [catList, setCatList] = useState(setupCatList);
function scrollToCat(cat) {
const map = getMap();
const node = map.get(cat);
node.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
function getMap() {
if (!itemsRef.current) {
// 처음 사용하는 경우, Map을 초기화합니다.
itemsRef.current = new Map();
}
return itemsRef.current;
}
return (
<>
<nav>
<button onClick={() => scrollToCat(catList[0])}>Neo</button>
<button onClick={() => scrollToCat(catList[5])}>Millie</button>
<button onClick={() => scrollToCat(catList[9])}>Bella</button>
</nav>
<div>
<ul>
{catList.map((cat) => (
<li
key={cat}
ref={(node) => {
const map = getMap();
map.set(cat, node);
return () => {
map.delete(cat);
};
}}
>
<img src={cat} />
</li>
))}
</ul>
</div>
</>
);
}
function setupCatList() {
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
}
return catList;
}
itemRef -> 하나의 DOM 노드를 가지고 있지 않다.
대신에 식별자와 DOM 노드로 연결된 Map을 가지고 있다.(Ref는 어떤 값이든 가질 수 있다.)
모든 리스트 아이템에 있는 ref 콜백은 Map 변경을 처리한다.
<li
key={cat.id}
ref={node => {
conse map = getMap();
// Add to the Map
map.set(cat, node);
return () => {
// Remove from the Map
map.delete(cat);
}
}
}
>
-> 나중에 Map에서 개별 DOM 노드를 읽을 수 있다.
Strict Mode가 활성화되어 있으면 개발 모드에서 ref 콜백이 두 번 실행된다.
input 같은 브라우저 요소를 출력하는 내장 컴포넌트에 ref를 주입할 때 React는 ref의 current 프로퍼티를 그에 해당하는 DOM 노드로 설정
MyInput과 같이 직접 만든 컴포넌트에 ref를 주입할 때는 null이 기본적으로 주어진다.
import { useRef } from 'react';
function MyInput({ ref }){
return <input ref={ref} />;
}
function MyForm() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />
}
부모 컴포넌트인 MyForm에서 ref를 생성하고, 이를 자식 컴포넌트인 MyInput으로 전달
MyInput은 ref를 input에 넘겨줌
input -> 내장 컴포넌트이므로, React는 해당 ref의 .current 속성을 input DOM 요소로 설정
MyInput에 전달된 ref는 DOM 입력 요소로 전달된다.
부모 컴포넌트에서 DOM 노드의 focus()를 호출할 수 있게 되었다.
-> 부모 컴포넌트에서 DOM 노드의 CSS 스타일을 직접 변경하는 등의 예상치 못한 작업 발생 가능
import { useRef, useImperativeHandle } from 'react';
function MyInput({ ref }){
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 오직 focus만 노출
focus() {
realInputRef.current.focus();
},
}));
return <input ref={realInputRef} />;
};
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>Focus the input</button>
</>
);
MyInput 내부의 realInputRef는 실제 input DOM 노드를 가지고 있다.
useImperativeHandle을 사용해 React가 ref를 참조하는 부모 컴포넌트에 직접 구성한 객체를 전달하도록 지시.
-> Form 컴포넌트 안쪽의 inputRef.current는 focus 메소드만 가지고 있다.
이 경우 ref는 DOM 노드가 아닌 useImperativeHandle 호출에서 직접 구성한 객체가 된다.
React의 모든 갱신은 두 단계로 나뉜다.
일반적으로 렌더링하는 중에 ref에 접근하는 것을 선호하지 않는다.
DOM 노드를 보유하는 ref도 첫 렌더링에서 DOM 노드는 아직 생성되지 않아서 ref.current는 null인 상태이다. 그리고 갱신에 의한 렌더링에서 DOM 노드는 아직 업데이트되지 않아 둘 다 ref를 읽기엔 너무 이르다.
React는 ref.current를 커밋 단계에서 설정
DOM을 변경하기 전에 React는 관련된 ref.current 값을 미리 null로 설정
DOM을 변경한 후 React는 즉시 대응하는 DOM 노드로 다시 설정
대부분 ref 접근은 이벤트 핸들러 안에서 일어난다.
ref를 사용하여 뭔가를 하고 싶지만, 시행할 특정 이벤트가 없을 때 Effect가 필요할 수도 있다.
import { useState, useRef } from 'react';
export default function TodoList() {
const listRef = useRef(null);
const [text, setText] = useState('');
const [todos, setTodos] = useState(
initialTodos
);
function handleAdd() {
const newTodo = { id: nextId++, text: text };
setText('');
setTodos([...todos, newTodo]);
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
return (
<>
<button onClick={handleAdd}>
Add
</button>
<input
value={text}
onChange={e => setText{e.target.value}
/>
<ul ref={listRef}>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
)}
</ul>
</>
);
}
let nextId = 0;
let initialTodos = [];
for(let i = 0; i < 20; i++){
initialTodos.push({
id: nextID,
text: 'Todo #' + (i + 1)
});
}
문제는 다음 두 줄에 존재
setTodos([...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
React에서 state 갱신 -> 큐에 쌓여 비동기적으로 처리됨
여기서 setTodos가 DOM을 바로 업데이트하지 않기 때문에 문제가 발생
할 일 목록의 마지막 노드로 스크롤할 때, DOM에 아직 새로운 투두가 추가가 안 된 상태이다.
이를 해결하기 위해 React가 DOM 변경을 동기적으로 수행하도록 할 수 있다.
react-dom 패키지의 flushSync를 가져오고 state 업데이트를 flushSync 호출로 감싼다.
flushSync(() => {
setTodos([...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
.
마지막 할 일 -> 스크롤하기 전에 항상 DOM에 추가되어 있을 것이다.
ref는 React에서 벗어나야 할 때만 사용해야 한다.
DOM을 직접 수정하는 시도를 한다면 React가 만들어 내는 변경 사항과 충돌을 발생시킬 위험을 감수해야 한다.
React 조건부 렌더링과 state를 사용해 노드 존재 여부를 토글
DOM API의 remove()를 사용해 React의 제어 밖에서 노드를 강제적으로 삭제
import { useState, useRef } from 'react';
export default function Counter() {
const [show, setShow] = useState(true);
const ref = useRef(null);
return (
<div>
<button
onClick={()=>{
setShow(!show);
}}>
Toggle with setState
</button>
<button
onClick={()=>{
ref.current.remove();
}}>
Remove from the DOM
</button>
{show && <p ref={ref}>Hello World</p>}
</div>
);
}
DOM 요소를 직접 삭제한 뒤 setState를 사용하여 다시 DOM 노드를 노출하는 것 -> 충돌 발생
React가 관리하는 DOM 노드를 직접 바꾸려 하지 말 것
<div ref={myRef}>로 React가 myRef.current에 DOM Node를 대입하도록 지시비디오 재생과 멈춤 챌린지
import { useState, useRef } from 'react';
export default function VideoPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const videoRef = useRef(null);
function handleClick() {
const nextIsPlaying = !isPlaying;
setIsPlaying(nextIsPlaying);
if(nextIsPlaying){
videoRef.current.play();
}
else {
videoRef.current.pause();
}
}
return (
<>
<button onClick={handleClick}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<video ref={videoRef} width="250">
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
/>
</video>
</>
)
}
videoRef를 useRef(null)로 초기화 후 선언,
video의 ref로 추가
handleClick 함수에서 로직 설정
videoRef.current.play() || videoRef.current.pause()
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
});
selectedRef를 선언하고 조건적으로 현재 활성화된 이미지에 전달
<li ref={index === i ? selectedRef : null}>
index === i 조건이 만족할 때 이 이미지가 선택된 이미지임을 알 수 있다.
-> 그 li는 selectedRef를 받는다.
selectedRef.current가 현재 선택된 올바른 DOM 노드를 올바르게 가리키도록 한다.
스크롤 전에 React가 DOM 변경을 끝내기 위해 flushSync 호출이 필요하다는 걸 주의할 것
-> 그렇지 않으면 selectedRef.current는 항상 이전에 선택된 아이템을 가리키고 있을 것이다.