간단한 서버를 만들고, custom hook 을 사용하여 서버에서 데이터를 받아올 수 있다.
useRef 에 대해 알아보자. 그리고 custom hook 에 대해 알아보자.
컴포넌트의 생애주기 전체에 걸쳐서 유지되는 객체이다. 즉, 컴포넌트가 없어질 때까지 동일한 객체가 유지된다.
객체 자체는 값은 아니고, 값을 참조하기 위한 객체이다. 그래서 값을 언제든지 변경할 수 있다.
만약, 여러 컴포넌트에서 동일한 useRef 객체를 참조하면 이들은 동일한 객체를 공유하게 된다.
아래 TimerControl
컴포넌트가 10번 호출된다면 ref.value
는 10이 된다.
const ref = { value: 1 }
function TimerControl() {
// useEffect 문제로 한 번만 늘어나진 않지만, 일단 한 번만 늘어난다고 가정한다.
ref.value += 1;
return (...);
}
상태(state)가 변경되면 해당 컴포넌트와 하위 컴포넌트는 다시 렌더링한다. 하지만 레퍼런스 객체의 현재 값(current)는 바뀌더라도 렌더링에 영향을 주지 않는다.
다시 렌더링 되지 않기 때문에 UI 로는 바뀐 객체의 현재 값을 확인할 수 없다.
컴포넌트의 생애주기는 컴포넌트가 생기고 없어질 때까지이다. 컴포넌트가 없어지면 가상 돔으로 들어갔던 컴포넌트 요소가 빠진다.
input 태그의 id를 관리할 때 많이 쓴다.
input 태그를 가진 컴포넌트를 여러 곳에서 쓴다면, 각 컴포넌트 id는 달라야 하며 id가 생애주기 동안 유지가 되야 한다.
function TimerControl() {
const ref = useRef(`input-${Math.random()}`);
return (
<div>
<label htmlFor={id.current}>
Search
</label>
<input
id={id.current}
...
/>
</div>
);
}
useEffect 등을 쓰면 Closure 문제를 만나게 된다. (Closure 문제는 우리가 useEffect 에서 변수가 Capture 되고 Bind 된다는 것에 대한 이해가 부족해서 일어나는 문제이다.)
Closure 문제에 대한 예를 들어보자. useEffect 가 실행되고 5초 뒤에 filterText 값을 console 에 출력하고 있다. 5초 이내에 filterText 의 값을 바꾸면 console 에는 바뀐 filterText 값이 찍힐까? 아니다. 처음에 Caputer, Bind 된 값인 '' 이 찍힌다.
const [filtetText, setFilterText] = useState('');
useEffect(() => {
setTimeout(() => {
console.log(filterText);
}, 5_000);
}, []);
return <input onChange={(e) => setFilterText(e.target.value)} />;
바뀐 값을 출력하고 싶다면 다음과 같이 useRef 를 사용할 수 있다. query 가 값을 참조하는 객체라서 가능한 일이다.
const [filtetText, setFilterText] = useState("");
const query = useRef(""); // 추가
// 추가
useEffect(() => {
query.current = filtetText;
}, [filtetText]);
useEffect(() => {
setTimeout(() => {
console.log(query.current);
}, 5_000);
}, []);
return <input onChange={(e) => setFilterText(e.target.value)} />;
이렇게 코딩할 일은 절대 없긴 하다.
로직을 재사용하기 위한 제일 쉬운 방법이다.
Custom Hooks 을 하는 방법은 너무 쉽다. Refactoring 으로 Extract Function 을 수행하면 된다.
컴포넌트가 PascalCase 로 이름을 붙였다면, Hook은 use
로 시작하는 camelCase로 이름을 붙이면 된다.
hooks/useFetchProducts.ts
export default function useFetchProducts() {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
const fetchProducts = async () => {
const url = 'http://localhost:3000/products';
const response = await fetch(url);
const data = await response.json();
setProducts(data.products);
};
fetchProducts();
}, []);
return products;
}
이제 useFetchProducts
를 여기저기서 불러다가 쓰면 된다.
setProducts 는 useFetchProducts
에서만 쓰는 함수이다. 그래서 이렇게 관심사의 분리로 컴포넌트를 떨구면 setProducts
가 캡슐화돼서 setProducts
를 실수로 잘못 쓰는 문제를 해소시킨다. 컴포넌트가 명확해진 것이다. 부르는 컴포넌트에서는 setProducts
가 쓰이든 말든 관심사 밖이기 때문에 상관이 없다.
Custom Hook 을 잘 쓰게 되면 앞으로 테스트 코드를 작성할 때 Mocking 도 수월하게 할 수 있다. 그리고 Hook 자체를 유닛 테스트 할 수 있어서 편하다.
Hook 호출은 규칙이 있어서 단순하게 쓰도록 노력해야 한다.
⚠️ 다음과 같이 콜백 함수나 조건문 안에서 Hook 을 호출하면 안된다. ⚠️
if(playing) {
const products = useFetchProducts();
console.log(products);
}
이런 형태로 특정 조건이 걸렸을 때 다시 부르고 싶다면, fetchProducts() 같은 re-load 하는 함수를 products 처럼 Return 해준다. 그리고 특정 조건에 fetchProducts() 를 호출한다.
컴포넌트의 깔끔함을 위해서 custom hook 을 쓰는 연습을 많이 해야겠다. useEffect 는 외부와 연결을 위해서 쓰는 hook 이니까 무조건 custom hook 으로 빼도 될 듯 싶다.
usehook-ts 라이브러리에 대해 알아보자.