ReactJS 문서를 계-속 읽고있다! 아마 담주 초정도까지는 계속 읽고, 잘몰랐던 내용에 대해서 작성할 것 같다 ㅎㅎ 그렇다면 오늘도 화이띵..
useState 와 useRef는 React에 의해 제공되며, useRef는 seState 위에 구현될 수 있다.
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
useRef는 setter 가 없는 일반적인 state 변수로 생각하면 된다. 대신 ref.current처럼 사용해야한다.
ref.current 가 언제 변하는지 React는 모르기 때문에 렌더링할 때 읽어도 컴포넌트의 동작을 예측하기가 어렵다. 따라서 렌더링 중에 일부 정보가 필요한 경우 state 를 대신 사용해야한다.
| 항목 | ref | state |
|---|---|---|
| 역할 | DOM 요소나 컴포넌트 인스턴스에 직접 접근 | 컴포넌트 렌더링에 영향을 주는 상태 관리 |
| 리렌더링 여부 | 값이 변경되어도 리렌더링을 트리거하지 않음 | 값이 변경되면 컴포넌트가 리렌더링됨 |
| 초기화 방법 | useRef()를 사용하여 초기화 | useState()를 사용하여 초기화 |
| 사용 목적 | DOM 조작, 저장소 역할(타이머, 외부 라이브러리 등) | UI 렌더링에 영향을 주는 데이터 관리 |
| 데이터 보존 | 컴포넌트가 리렌더링되어도 값이 유지됨 | 상태 변경 시 리렌더링이 발생하며 데이터가 바뀜 |
| 직접 조작 여부 | 값을 직접 수정 가능 (e.g. ref.current = value) | setState() 함수를 통해서만 상태를 업데이트 가능 |
| 주요 사용처 | 포커스 관리, 스크롤 위치, DOM 요소 접근 등 | 사용자 입력, UI 상태, 비즈니스 로직 상태 관리 |
ref를 많이 관리하기 위해서 ref 콜백 방식을 사용하면 된다. ref 콜백은 ref 속성에 콜백 함수를 전달하는 방법으로 DOM 요소나 컴포넌트가 렌더링 되거나 업데이트할 때 호출한다.
<ul>
{items.map((item) => {
// 작동하지 않습니다!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
위와 같은 코드는 useRef Hook이 map() 안쪽에서 호출했기 때문에 오류가 발생한다. Hook은 컴포넌트의 최상단에서만 호출되어야한다. 이런 문제를 해결하기 위해서는 ref 콜백 방식을 사용한다. React는 ref를 설정할 때 DOM 노드와 함께 ref 콜백을 호출하며 ref를 지울 때 null을 전달한다.
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])}>Tom</button>
<button onClick={() => scrollToCat(catList[5])}>Maru</button>
<button onClick={() => scrollToCat(catList[9])}>Jellylorum</button>
</nav>
<div>
<ul>
{catList.map((cat) => (
<li
key={cat}
ref={(node) => {
const map = getMap();
// ref 처리하기
if (node) {
map.set(cat, node);
} else {
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;
}
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
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 컴포넌트의 DOM 입력 요소를 그대로 노출함으로서 Form 컴포넌트에서 MyInput 컴포넌트를 모두 조작할 수 있게 되었다! 이럴 때 focus() 만 사용하게끔 제한할 수 있는 방법이 있는데..!
import {
forwardRef,
useRef,
useImperativeHandle
} from 'react';
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 오직 focus만 노출합니다.
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} 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>
</>
);
}
위와 같이 useImperativeHandle를 사용하면 오직 focus() 함수만 사용할 수 있다!
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)
});
}
위와 같은 코드를 실행해보면, 최근에 추가한 이전 todo로 scroll이 되는 현상을 볼 수 있다. 분명 todo 추가를 한 후에 scroll 이벤트가 발생하는데 왜 그럴까?
react에서는 state 갱신은 큐에 쌓여 비동기적으로 처리되기 때문에 scrollIntoView() 이벤트가 발생한 후 DOM에 todo 가 추가된다. 이런 문제를 해결하기 위해서 flushSync를 사용하여 동기적으로 DOM을 변경하도록 해야한다.
import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';
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 };
// 이렇게 사용하면 된다.
flushSync(() => {
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)
});
}