You will learn
- ref๋ก ๋ฆฌ์กํธ๊ฐ ๊ด๋ฆฌํ๋ DOM ๋ ธ๋์ ์ ๊ทผํ๋ ๋ฐฉ๋ฒ
- ref์
useRefํ ์ ๊ด๊ณ - ๋ค๋ฅธ ์ปดํฌ๋ํธ์ DOM ๋ ธ๋์ ์ ๊ทผํ๋ ๋ฐฉ๋ฒ
- ์ธ์ ๋ฆฌ์กํธ๊ฐ ๊ด๋ฆฌํ๋ DOM์ ์์ ํด๋ ๋๋์ง
DOM ๋ ธ๋ ์กฐ์
๊ธฐ๋ณธ์ ์ผ๋ก ๋ฆฌ์กํธ๋ DOM ์กฐ์์ ๊ฐ๋ฐ์ ๋์ ํด์ค๋ค. ์ ์ธํ ๋ฐฉ์์ผ๋ก UI๊ฐ ์ด๋ป๊ฒ ๊ทธ๋ ค์ ธ์ผ ํ๋์ง์ ๋ํ ์ค๊ณ๋๋ง ์์ฑํ๋ฉด ๋ฆฌ์กํธ๊ฐ ๋งค ์๊ฐ ๊ทธ์ ์๋ง๊ฒ ๊ทธ๋ ค์ฃผ๋๊ฒ์ธ๋ฐ, ๊ทธ๋์ ํน์ ํ ๋ฐฉ๋ฒ์ ํตํด์๋ง ๋ฆฌ์กํธ ๋ฐ์ ์์๋ค์ ์กฐ์ข ํ ์ ์๋ค.
๊ณต์ ๋ฌธ์์์๋ ํ์ถ๊ตฌ "Escape Hatches" ๋ผ๊ณ ํํํ๋๋ฐ ๋ง ๊ทธ๋๋ก ๋ฆฌ์กํธ ๋ฐ์ ๋๊ฐ๊ฒ ํด์ฃผ๋ ๋ฐฉ๋ฒ๋ค์ด๋ค. ์ฌ๊ธฐ์๋ ref์ effect ๋ ๋ฐฉ์์ด ์๋๋ฐ ref๋ก๋ ์ธ๋ถ์ ์ง์ ์ ์ผ๋ก ์ ๊ทผํด์ ์ ์ดํ ์ ์๊ณ , effect๋ก๋ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ์ ์ธ๋ถ ์์คํ ์ ๋๊ธฐํ ํ ์ ์๋ค. ๊ทธ๋ฆฌ๊ณ ๋์ด ํจ๊ป ์ฌ์ฉ๋๋ ๊ฒฝ์ฐ๊ฐ ๋งค์ฐ ์ฆ๋ค.
const myRef = useRef(null);
//...
<div ref={myRef} />
์ด๋ ๊ฒ ์ฐธ์กฐํ ๋
ธ๋์ ref๋ฅผ ๊ฑด๋ด์ฃผ๋ฉด ๋ฆฌ์กํธ๋ myRef.current์ ์ด ๋
ธ๋๋ฅผ ๋ฃ๋๋ค. ๊ทธ๋ฆฌ๊ณ myRef.current.scrollIntoView() ์ฒ๋ผ div ๋
ธ๋์ ์ฌ์ฉํ ์ ์๋ ๋ธ๋ผ์ฐ์ API๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
ref ์ฝ๋ฐฑ
์ฌ์ ์ ๋ฏธ๋ฆฌ ๋ช๊ฐ์ ref๊ฐ ํ์ํ์ง ์ ์ ์๋ ๊ฒฝ์ฐ๋ ๋ง์๊ฒ์ด๋ค. items ์ ๊ฐ์๋งํผ ref ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค๋ฉด ์ด๋ป๊ฒ ํด์ผํ ๊น?
์ฒซ ๋ฒ์งธ ๋ฐฉ๋ฒ์, items ๋ฅผ ๋ด์ ๋ถ๋ชจ ๋
ธ๋๋ฅผ ๋ง๋ค๊ณ ๊ทธ ๋ถ๋ชจ ๋
ธ๋์ ๋ํ ref ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ref.current.querySelectorAll()๋ก ์์ ๋
ธ๋๋ฅผ ์ฐพ๋ ๋ฐฉ๋ฒ์ด ์๋ค.
๋ ๋ฒ์งธ ๋ฐฉ๋ฒ์ ref ์ฝ๋ฐฑ์ ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ์ง์ ํ ๋ ธ๋์ ref ์์ฑ์ ref ์ฝ๋ฐฑํจ์๋ฅผ ๋ฃ๋๋ฐฉ๋ฒ์ด๋ค.
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) {
// Initialize the Map on first usage.
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[8])}>
Bella
</button>
</nav>
<div>
<ul>
{catList.map((cat) => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
map.set(cat, node);
return () => {
map.delete(cat);
};
}}
>
<img src={cat.imageUrl} />
</li>
))}
</ul>
</div>
</>
);
}
function setupCatList() {
const catCount = 10;
const catList = new Array(catCount)
for (let i = 0; i < catCount; i++) {
let imageUrl = '';
if (i < 5) {
imageUrl = "https://placecats.com/neo/320/240";
} else if (i < 8) {
imageUrl = "https://placecats.com/millie/320/240";
} else {
imageUrl = "https://placecats.com/bella/320/240";
}
catList[i] = {
id: i,
imageUrl,
};
}
return catList;
}
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด itemsRef์ ๋น์ด์๋ map์ ์ง์ ํ๋ค.
์ด๋ ๊ฒ ํ๋ฉด ๋ ๋๋ง์ด ๋๋๊ณ ๊ฐ li ๋
ธ๋๋ณ๋ก ref๋ฅผ ์ง์ ํ ๋ ์์ ์๋ ํจ์๊ฐ ์คํ์ด ๋๋ฉด์ map์ cat๊ฐ์ฒด๊ฐ ํค๋ก ๊ฐ li ๋
ธ๋๋ค์ด ๊ฐ์ผ๋ก ๋ค์ด๊ฐ๊ฒ ๋๋ค.
์ฆ ๋ ๋ฐฉ๋ฒ ๋ชจ๋ ์ ๋์ ์ธ ๊ฐ์๋๋ก ref๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ์ง ์๊ณ ํ๋์ ref ์์ ๋ฃ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ๊ณ ์๋๋ฐ, ์ด๊ฒ ๊ฐ๋ฅํ ์ด์ ๋ ref๊ฐ ๋ชจ๋ ์ข ๋ฅ์ ๊ฐ์ ๋ด์ ์ ์๋ ๊ฐ์ฒด์ด๊ธฐ ๋๋ฌธ์ด๋ค.
๋ค๋ฅธ ์ปดํฌ๋ํธ์ DOM ๋ ธ๋ ์ ๊ทผํ๊ธฐ
์์ ์์๋ ํ ์ปดํฌ๋ํธ ์์์ DOM ๋ ธ๋๋ฅผ ์กฐ์ข ํ๊ธฐ ์ํด ์ฌ์ฉํ๋๋ฐ, ๋ค๋ฅธ ์ปดํฌ๋ํธ์์๋ ref๋ฅผ ์ง์ ํ ๋ ธ๋์ ์ ๊ทผํ ์ ์๋ค.
๋ํ์ ์ธ ์ฌ๋ก๊ฐ ๋ถ๋ชจ์์ ์์ ์ปดํฌ๋ํธ์ DOM ๋ ธ๋์ ์ ๊ทผํ๋ ๊ฒ์ด๋ค. ๋ถ๋ชจ ์ปดํฌ๋ํธ์์ ๋น์ด์๋ ref๋ฅผ ์์ฑํ๊ณ ์์ ์ปดํฌ๋ํธ์ ์์ฑ์ผ๋ก ๋๊ฒจ์ฃผ๋ฉด, ์์ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง ๋ ๋ ํด๋น ๋ ธ๋์ ref๋ฅผ ์ง์ ํ๋ค.
React 19 ์ด์ ๊น์ง๋
forwardRef๋ฅผ ์ฌ์ฉํด์ผ ํ์ผ๋ ์ด์ ๋ref๋ฅผ prop์ผ๋ก ์ ๋ฌํ ์ ์๊ฒ ๋๋ค.
useImperativeHandle๋ก ์ ๊ทผ ๋ฒ์ ์ ํํ๊ธฐ
function MyInput({ ref }) {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
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>
</>
);
}
์ด๋ ๊ฒ ๋ถ๋ชจ์๊ฒ ์๊ธฐ ๋
ธ๋๋ฅผ ref๋ก ๋๊ฒจ์ฃผ๊ณ ์๋ MyInput ์ปดํฌ๋ํธ๋ ๋ถ๋ชจ๊ฐ ์ ํด์ง ํ๋๋ง ํ ์ ์๊ฒ ์ ํํ ์ ์๋ค. ์ฌ๊ธฐ์ ์ ํํ๋ input ๋
ธ๋์ ref์ธ realInputRef๋ ์์ ์ปดํฌ๋ํธ ์์ ๋จธ๋ฌผ๊ณ ๋ถ๋ชจ๊ฐ ์ ๊ทผํ๋ inputRef.current๋ ์์ ์ปดํฌ๋ํธ์์ useImperativeHandle๋ก ๋ง๋ ํน์ํ ๊ฐ์ฒด๋ค.
์ด ๋ฐฉ๋ฒ์ ์์ ์ปดํฌ๋ํธ๊ฐ ๋ถ๋ชจ์๊ฒ ์ฌ์ฉ ๊ฐ๋ฅํ API๋ฅผ ์ ๊ณตํ๋๊ฒ๊ณผ ๊ฐ์๋ฐ, ๊ทธ๋์ API์ ์ด์ ์ธ ๋ด๋ถ ๊ตฌํ ์ถ์ํ๋ฅผ ์ป์ ์ ์๋ค. ์๋ฅผ๋ค์ด ์์ ์ปดํฌ๋ํธ์ ๋ง์ ๋ณํ๊ฐ ์์ด์ ํฌ์ปค์ค๋ฅผ ์คฌ์๋ input์ฐฝ์ ์์ ๋ณํ๋ ์ ๋๋ฉ์ด์
์ด ์ฌ์ฉ๋๋ค๊ณ ํ์. ๊ทธ๋ฌ๋ฉด ์ด ๋ณ๊ฒฝ์ฌํญ์ ๋ถ๋ชจ ์ปดํฌ๋ํธ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ์์ฑ๋์ด์ผ ํ๋ค. ๋ฐ๋ฉด useImperativeHandle ์ ์ฌ์ฉํ๋ฉด ์์ ์ปดํฌ๋ํธ์ ๋ณ๊ฒฝ์ฌํญ์ด ์์ ์ปดํฌ๋ํธ ์์ ๋จธ๋ฌด๋ฅผ ์ ์๋ค.
์ธ์ ref๊ฐ ์ฐ๊ฒฐ๋๋๊ฐ
๋ฆฌ์กํธ์์ ํ๋ฉด์ ์ ๋ฐ์ดํธ ํ๋ ๊ณผ์ ์ ๋ ๋์ ์ปค๋ฐ ๋ ๋จ๊ณ๋ก ๋๋๋ค. ๋ ๋ ๋จ๊ณ์์๋ ์ปดํฌ๋ํธ๋ค์ ์ฝ์ด ๊ฐ์ DOM ํธ๋ฆฌ๋ฅผ ๋ง๋ค๊ณ , ์ปค๋ฐ ๋จ๊ณ์์ ํ๋ฉด์ ์ ์ฉํ๋ค.
๊ทธ๋ฆฌ๊ณ ref๋ ์ปค๋ฐ๋จ๊ณ์์ ๋ ธ๋๋ค๊ณผ ์ฐ๊ฒฐ๋๋ค. ์ด๋ ์ฆ ref๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ์ดํํธ์ฒ๋ผ, ํ๋ฉด์ด ๋ค ๊ทธ๋ ค์ ธ์ผ ์คํ๋ ์ ์๋ ํ ๊ณผ ํจ์์์ ์ฌ์ฉํด์ผ ํ๋ค๋ ์๋ฏธ๋ค.
flushSync , ๋๊ธฐ ๋ฐฉ์์ผ๋ก ์ํ ์
๋ฐ์ดํธ
function handleAdd() {
const newTodo = { id: nextId++, text: text };
setText('');
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์๋ก์ด todo๋ฅผ ์ถ๊ฐํ๋ฉด ๊ฐ์ฅ ๋ง์ง๋ง์ ์ถ๊ฐ๋ todo, ์ฆ ์ง๊ธ ์ถ๊ฐํ todo๋ก ์คํฌ๋กค์ ์ด๋ํ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ผ๋ ๊ฒ์ ์ ์ ์๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์๋ ๋ฌธ์ ๊ฐ ์๋๋ฐ, ์ํ ์
๋ฐ์ดํธ๊ฐ ๋ฆฌ์กํธ์ ๋ฐฐ์นญ ์ฒ๋ฆฌ ๋ฐฉ์ ๋๋ฌธ์ '์ฆ์' ์ํ๋๋๊ฒ ์๋๋ผ๋ ์ ์ด๋ค. setTodo ํจ์๊ฐ ํธ์ถ๋๋ฉด ์์๊ฐ์ ์๋ก์ด todo ๋
ธ๋๊ฐ ๋ง๋ค์ด์ง๊ธด ํ์ง๋ง ๋ฐ๋ก ๋ค์์์ listRef.current ๋ก ๋ฆฌ์คํธ ๋
ธ๋์ ์ ๊ทผํ ๋๊น์ง๋ ๋ง๋ค์ด์ง์ง ์๋๋ค. ๊ทธ๋์ ์ด์ ์ ๋ง๋ค์ด์ง todo, ํ ์นธ์ฉ ๋ฐ๋ ค์ ์คํฌ๋กค์ด ์ด๋ํ๊ฒ ๋๋๊ฒ์ด๋ค.
function handleAdd() {
const newTodo = { id: nextId++, text: text };
flushSync(() => {
setText('');
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
์ด๋ ๊ฒ flushSync๋ฅผ ์ฌ์ฉํ๋ฉด ์ํ ์
๋ฐ์ดํธ๋ฅผ '์ฆ์' ์งํํ๊ณ ๋ค์ ์ฝ๋๋ฅผ ์ฝ๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋๋ค. ์ด๋ฅผํ
๋ฉด await๊ณผ ๋น์ทํ๋ค๊ณ ๋ณผ ์ ์๋ค.
ํ์ง๋ง ๋ฆฌ์กํธ๋ฅผ ๊ฐ๋ฐํ๋ฉด์ ๊ดํ ๋ฆฌ๋ ๋๋ง์ ๋ฐฐ์นญ ์ฒ๋ฆฌ ๋ฐฉ์์ผ๋ก ์ค์ ํ๊ฒ์ ์๋๊ฒ์ด๋ค. ๋ฐ๋ผ์ ์ด ๋ฐฐ์นญ ๋ฐฉ์์ ๊ฐ์ ๋ก ๋ฌด์ํ๋ flushSync๋ ์ต์ํ์ผ๋ก ์ฌ์ฉ๋์ด์ผ ํ ๊ฒ์ด๋ค.
์์ ๋ฌธ์
1,2,4๋ฒ ๋ฌธ์ ๋ ์ฌ์์ ์๋ต
3. ์ด๋ฏธ์ง ์คํฌ๋กค
export default function CatFriends() {
const selectedRef = useRef(null);
const [index, setIndex] = useState(0);
return (
<>
<nav>
<button onClick={() => {
flushSync(() => {
if (index < catList.length - 1) {
setIndex(index + 1);
} else {
setIndex(0);
}
});
selectedRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}}>
Next
</button>
</nav>
<div>
<ul>
{catList.map((cat, i) => (
<li
key={cat.id}
ref={index === i ?
selectedRef :
null
}
>
<img
className={
index === i ?
'active'
: ''
}
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
Next ๋ฒํผ์ ๋๋ฅด๋ฉด index๊ฐ flushSync๋ก ์
๋ฐ์ดํธ๋๋ฉด์ ๋ฆฌ๋ ๋๋ง
-> index์ ์ผ์นํ๋ ๋
ธ๋์ selectedRef ์ฐ๊ฒฐ
-> scollIntoView

