간단한 서버를 만들고, custom hook 을 사용하여 서버에서 데이터를 받아올 수 있다.
React Hooks 에 대해 알아보자.
React Hooks 는 React 16.8 버전에서 처음 도입되었다. 기본 방식에 있던 몇 가지 문제를 해결했다.
기본 방식의 문제점은 다음과 같다.
1. Wrapper Hell (HoC): 고차 컴포넌트
컴포넌트 로직을 재사용하기 위해서 컴포넌트를 다른 컴포넌트로 감싸고 Props 로 내려준다. 이 고차 컴포넌트 방식은 코드를 복잡하게 만들고, 컴포넌트 계층 구조를 복잡하게 만든다.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
2. Huge Components
로직이 컴포넌트 안에 들어가면서 컴포넌트가 거대해지는 문제가 있다. 이로 인해 유지보수가 어렵다.
3. Confusing Classes
클래스형 컴포넌트를 사용할 때, this 키워드가 어떤 것을 가리키는지 혼란스러울 수 있다.
React Hooks 는 React 를 쓰는 방식을 완전히 바꾼 커다란 변화이다. 함수형 컴포넌트에서 hooks 로 모든 게 처리가 가능하다.
기존에 있던 클래스형 컴포넌트를 없애건 아니지만 이제는 기존으로 돌아가는 게 불가능하다.
기존
Class Component
로 만든다.Function Component
로 작성한다. 현재
그냥 Function Component
만 사용한다.
상태 관리 유무를 바로 알기 어렵다. === 신경쓰지 않아도 되는 부분이다.
기존에는 Class Component 면 상태를 관리하는 컴포넌트라고 예상할 수 있었다. 지금은 State Hook (useState) 을 이용해서 처리하니까 알 수도 없고 신경 쓸 필요도 없게 됐다. 패러다임이 바뀐 것이다.
대신 알 수 없게 되면서 조금 어려워진 점이 있다. Props 가 완전히 같으면 컴포넌트를 실행하지 않게 하고 싶을 때가 있다. 이때 useMemo 같은 hook 으로 처리해야 한다. 근데 내부에서 뭔가를 처리해야 하는 게 있으면 문제가 생길 가능성이 있다. (그래서 섣부른 최적하는 하지 말라는 얘기가 나온다.)
복잡한 요소는 전부 Hook 으로 격리 및 재사용이 가능하다.
기존에는 컴포넌트 안에 많은 것이 들어있어서 컴포넌트가 거대해지는 문제점이 있었다. HoC 같은 걸로 빼지 않으면 굉장히 복잡해졌는데, Hooks 로 빼는 건 너무 쉽다.
렌더링 이후에 해야 할 일, 즉 React의 외부와 관련된 일 을 정해줄 수 있다.
그래서 공식문서에서는 '정말로 외부랑 동기화 하는 게 아니면 사용하지 말아라' 라고 나온다.
useEffect
는 React 의 외부와 동기화 하는 일(Side-Effect)을 처리해야 한다.
기본적으로 렌더링 때마다 실행되므로, 의존성 배열을 통해서 언제 이펙트를 실행할 지 지정할 수 있다.
(= 불필요한 경우에 건너뛸 수 있다.)
함수를 리턴함으로써 종료 처리를 할 수 있다.
다음은 외부 동기화[synchronization] 에 대한 것이다. 이 정도는 useEffect 를 안 쓴다고 크게 문제가 되지 않지만, useEffect를 같이 쓰는 습관을 들이자.
// No
document.title = `Now: ${new Date().getTime()}`;
이렇게 useEffect 를 사용해서 처리하는 게 훨씬 우아한 방법이다.
// Yes
useEffect(() => {
document.title = `Now: ${new Date().getTime()}`;
});
우리가 원하는 것은 다음과 같다.
하지만 OFF 상태일 때 시간이 멈추지 않는 문제가 발생한다.
function Timer() {
useEffect(() => {
setInterval(() => {
document.title = `Now: ${new Date().getTime()}`;
}, 100);
});
return (
<p>Playing</p>
);
}
export default function TimerControl() {
const [playing, setPlaying] = useState(false);
const handleClick = () => {
setPlaying(!playing);
};
return (
<div>
{playing ? (
<Timer />
) : (
<p>Stop</p>
)}
<button type="button" onClick={handleClick}>
Toggle
</button>
</div>
);
}
버튼 OFF 를 누르면 Timer 컴포넌트 자체가 없어진다. 이때 setInterval 도 같이 없애야 한다.
이를 위해서 useEffect에 함수 종료 처리(Clean Up)를 넣어야 한다.
function Timer() {
useEffect(() => {
const id = setInterval(() => {
document.title = `Now: ${new Date().getTime()}`;
}, 100);
// Clean Up
return () => {
console.log('End of Effect');
cleanInterval(id);
};
});
return (
<p>Playing</p>
);
}
export default function TimerControl() {
const [playing, setPlaying] = useState(false);
const handleClick = () => {
setPlaying(!playing);
};
return (
<div>
{playing ? (
<Timer />
) : (
<p>Stop</p>
)}
<button type="button" onClick={handleClick}>
Toggle
</button>
</div>
);
}
처음에 한번만 실행하기
의존성 배열에 아무것도 지정하지 않으면 맨 처음에 딱 한번만 실행된다. 주로 API를 호출해서 데이터를 얻을 때 사용한다.
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();
}, []);
위에서 작성한 함수를 Effect 밖으로 빼는 것으로 수정해도 정상적으로 작동한다.
(하지만 다른 예시에서는 오류가 날 수 있다. 아래 '함수를 Effect 안으로 옮겨야 하는 이유'를 보자.)
const [products, setProducts] = useState<Product[]>([]);
const fetchProducts = async () => {
const url = 'http://localhost:3000/products';
const response = await fetch(url);
const data = await response.json();
setProducts(data.products);
};
useEffect(() => {
fetchProducts();
}, []);
useEffect 완벽가이드 - 함수를 Effect 안으로 옮겨야 하는 이유
다음과 같은 예시가 있다. useEffect 는 클로저라서, 맨 처음에 잡힐 때 바깥에 있던 변수들을 캡쳐한 다음에 그걸 바인딩해서 사용한다.
그러면 변수의 값이 바뀌어도 동기화하는데 실패할 수 있다.
function SearchResults() {
const [query, setQuery] = useState('react');
function getFetchUrl() {
// ...
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
// ...
}
useEffect(() => {
fetchData();
}, []);
// ...
}
이런 문제를 해결할 방법은 '함수를 Effect 안으로' 옮기면 된다.
function SearchResults() {
const [query, setQuery] = useState('react');
useEffect(() => {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, [query]);
// ...
}
<React.StrictMode>
로 컴포넌트 전체를 감쌀 경우, 예상치 못한 Side Effect 를 찾으려고 Effect 등을 두 번씩 실행한다.
평소에는 큰 문제가 없지만, API 등을 사용하면 이상하다고 느낄 수 있으니 참고하자.
단, Strict 모드는 개발 모드에서만 활성화되기 때문에, 프로덕션 빌드에는 영향을 끼치지 않는다.
다음 예시를 보자. 의존성 배열에 있는 userId
의 값이 바뀌면 useEffect 가 실행된다.
이전에 불러오던 것을 멈출 수 없다. 그래서 아까 값을 불러오던 것과 지금 불러오는 것, 둘 다 업데이트를 하게 된다.
누가 먼저 응답이 올 지 모른다. 먼저 실행했다고 먼저 응답이 오는 것은 아니다.
useEffect(() => {
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
}, [userId])
그래서 다음과 같이 ignore
변수를 이용해 종료 처리를 해야 한다. 그러면 userId
가 바뀔 때 이전의 ignore
는 true
가 돼서 setTodos(json)
이 무시된다.
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId])
fetch 처리하는 부분을 다른 컴포넌트로 옮기려고 하면, fetch 하는 부분 뿐만 아니라 import 하는 부분도 옮겨야 한다. 그래서 복붙 과정에서 실수할 수도 있다.
얘네를 단순한게 옮길 수 있는 방법이 있는데 다음에 알아보자.