함수 컴포넌트와 클래스 컴포넌트의 차이를 학습하고, 함수 컴포넌트에서 Hook을 사용하는 이유를 이해합니다.
Hook의 사용 규칙에 대해 학습하고 이해합니다.
useMemo의 쓰임새와 작성 방법에 대해 학습합니다.
useCallback의 쓰임새와 작성 방법에 대해 학습합니다.
custom hooks의 쓰임새와 작성 방법에 대해 학습합니다.
요즈음에는 대부분 함수형 컴포넌트로 구현하지만, 예전에는 클래스 컴포넌트를 사용했다.
컴포넌트를 클래스로 작성하게 되면 복잡하고 가독성이 떨어지기 때문에, 점진적으로 컴포넌트의 형태는 함수형으로 넘어가게 됐다.
다만 이전까지의 함수 컴포넌트는 클래스 컴포넌트와는 다르게 상태 값을 사용하거나 최적화할 수 있는 기능들이 조금 미진했는데, 그 부분들을 보완하기 위해 Hook이라는 개념을 도입했다.
Hook은 React 16.8에 새로 추가된 기능입니다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해 줍니다.
Hook은 다르게 말하면 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메서드이다.
Hook은 class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않는다. 사용 시 아래 에러가 발생한다.
React Hook "useState" cannot be called in a class component
Hook을 사용할 때는 아래의 두 가지 규칙을 준수해야 한다.
반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한 대로 동작하지 않을 우려가 있다.
컴포넌트 안에는 useState
나 useEffect
같은 Hook들이 여러 번 사용될 수 있는데, React는 이 Hook을 호출되는 순서대로 저장해 둔다.
그런데 조건문, 반복문 안에서 Hook을 호출하게 되면 호출되는 순서대로 저장을 하기 어려워지고, 결국 예기치 못한 버그를 초래하게 될 수 있게 되는 것이다.
리액트 함수형 컴포넌트나 커스텀 Hook이 아닌 다른 일반 JavaScript 함수 안에서 호출해서는 안 된다.
...
window.onload = function () {
useEffect(() => {
// do something...
}, [counter]);
}
...
위 코드처럼 window의 요소가 모두 준비가 되면 useEffect()가 호출되었으면 좋겠다고 생각해서 window.onload라는 함수에서 useEffect를 호출하게 되면, 에러가 발생한다.
애초에 Hook은 React의 함수 컴포넌트 내에서 사용되도록 만들어진 메서드이기 때문에 근본적으로 일반 JavaScript 함수 내에서는 정상적으로 돌아가지 않는다.
컴포넌트는 기본적으로 상태가 변경되거나 부모 컴포넌트가 렌더링이 될 때마다 리렌더링을 하는 구조로 이루어져 있다. 그러나 너무 잦은 리렌더링은 앱 성능을 저하시킨다.
너무 많은 리렌더링이 발생하면,
Too many re-renders. React limits the number of renders to prevent an infinite loop
이라는 에러가 발생한다.
(리액트는 무한 루프의 방지를 위해 최대 렌더링 횟수에 제한을 두고 있는데, 그 회수를 넘어섰다는 뜻)
앞서 말 했듯이, React Hook은 함수 컴포넌트가 상태를 조작하고 및 최적화 기능을 사용할 수 있게끔 하는 메서드인데,
그중 렌더링 최적화를 위한 Hook도 존재하는데, useCallback과 useMemo가 바로 그 역할을 하는 Hook이다.
useMemo은 특정 값(value)을 재사용하고자 할 때 사용하는 Hook
function Calculator({value}){
const result = calculate(value);
return <>
<div>
{result}
</div>
</>;
}
위 코드는 useMemo
를 사용하지 않고 작성한 코드로,
해당 컴포넌트는 props로 넘어온 value
값을 calculate
라는 함수에 인자로 넘겨서 result
값을 구한 후, <div>
엘리먼트로 출력 하고 있다.
만약 여기서 calculate
가 내부적으로 복잡한 연산을 해야 하는 함수라 계산된 값을 반환하는 데에 시간이 몇 초 이상 걸린다면 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속해서 호출할 것이고, 그때마다 시간이 몇 초 이상 소요가 될 것이다.
이는 당연히 사용자 경험에 악영향을 미친다.
🔽이제 useMemo
를 사용한 예시를 살펴보자.🔽
/* useMemo를 사용하기 전에는 꼭 import해서 불러와야 한다. */
import { useMemo } from "react";
function Calculator({value}){
const result = useMemo(() => calculate(value), [value]);
return <>
<div>
{result}
</div>
</>;
}
첫번째 파라미터: 어떻게 연산할지 정의하는 함수
두번째 파라미터: deps 배열두번째 파라미터의 배열 안에 넣은 내용이 바뀌면, 등록한 함수를 호출해서 값을 연산해주고, 만약에 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용한다.
value
값이 계속 바뀌는 경우라면 어쩔 수 없겠지만, 렌더링을 할 때마다 이 value
값이 계속 바뀌는 게 아니라면, 이 값을 어딘가에 저장을 해뒀다가 다시 꺼내서 쓸 수만 있다면 굳이 calculate
함수를 호출할 필요도 없을 것이다. 여기서 useMemo
Hook을 사용할 수 있다.
useMemo
를 호출하여 calculate
를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value
값이 동일할 경우에는 이전 렌더링의 value
값을 그대로 재활용할 수 있게 된다.
이는 메모이제이션(Memoization) 개념과 긴밀한 관계가 있다.
메모이제이션(Memoization)은 알고리즘에서 자주 나오는 개념으로, 기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말한다.
메모이제이션을 적절히 사용한다면 굳이 중복 연산을 할 필요가 없기 때문에 앱의 성능을 최적화할 수 있다.
useMemo는 바로 이 개념을 이용하여 복잡한 연산의 중복을 피하고 React 앱의 성능을 최적화시킨다.
(물론, 직접 memoization을 구현할 수도 있으나, useMemo
Hook을 호출한다면 이런 로직을 직접 구현하는 것을 대신해 주기 때문에 훨씬 간편하다)
useCallback
또한useMemo
와 마찬가지로 메모이제이션 기법을 이용한 Hook으로,useMemo
가 값의 재사용을 위해 사용하는 Hook이라면,
useCallback
은 함수의 재사용을 위해 사용하는 Hook이다.
예시로 아래 코드를 보자.
function Calculator({x, y}){ const add = () => x + y; return <> <div> {add()} </div> </>; }
- 이 Calculator 컴포넌트 내에는
add
라는 함수가 선언이 되어 있고, 이add
함수는 props로 넘어온x
와y
값을 더해<div>
태그에 값을 출력하고 있다.
이 함수는 해당 컴포넌트가 렌더링 될 때마다 새롭게 만들어질 것이다.
useMemo
와 마찬가지로, 해당 컴포넌트가 리렌더링 되더라도
그 함수가 의존하고 있는 값인 x와 y가 바뀌지 않는다면,
함수 또한 메모리 어딘가에 저장해 뒀다가 다시 꺼내서 쓸 수 있을 것!
이때 useCallback
Hook을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.
즉 x
와 y
값이 동일하다면 다음 렌더링 때 이 함수를 다시 사용하게 되는 것이다.
useCallback
을 사용해서 위 코드를 다시 작성해 보자.
/* useCallback를 사용하기 전에는 꼭 import해서 불러와야 한다 */
import React, { useCallback } from "react";
function Calculator({x, y}){
const add = useCallback(() => x + y, [x, y]);
return <>
<div>
{add()}
</div>
</>;
}
useCallback
은 함수를 호출을 하지 않는 Hook이 아니라, 그저 메모리 어딘가에 함수를 꺼내서 호출하는 Hook이기 때문에, 괄목할 만한 최적화를 느낄 수는 없다.
따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있다.
그러면 대체 언제 useCallback을 사용하는 게 좋을까?
➡ 자식 컴포넌트의 props로 함수를 전달해 줄 때 useCallback을 사용하기 좋다!
useCallback은 참조 동등성에 의존한다. React는 JavaScript 언어로 만들어진 오픈소스 라이브러리이기 때문에 기본적으로 JavaScript의 문법을 따라간다.
JavaScript에서 함수는 객체.
객체는 메모리에 저장할 때 값을 저장하는 게 아니라 값의 주소를 저장하기 때문에, 반환하는 값이 같을지라도 일치연산자로 비교했을 때 false
가 출력된다.
React는 리렌더링 시 함수를 새로이 만들어서 호출 하는데, 새로이 만들어 호출된 함수는 기존의 함수와 같은 함수가 아니게 된다.
그러나 useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용한다는 것과 같다.
따라서 React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있게 된다.
개발자가 스스로 훅을 커스텀해서 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용하게 된다.
Custom Hook을 사용하게 되면,
상태관리 로직의 재활용이 가능하고
클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며
함수형으로 작성하기 때문에 보다 명료하다. (e.g. useSomething
)
(use로 시작하는 것이 국룰!)
//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트 function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
//FriendListItem : 친구가 online일 때 초록색으로 표시하는 컴포넌트 function FriendListItem(props) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); }
위 두 컴포넌트는 정확하게 같은 코드 부분을 공유하고 있다. 바로 아래 부분.
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
커스텀 훅을 사용해서 이 로직을 빼내서 두 컴포넌트에서 공유하도록 해보자.
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
두 컴포넌트에서 사용하기 위해 동일하게 사용되고 있는 로직을 분리하여 함수 useFriendStatus
로 만든다.
이렇게 Custom Hook을 정의할 때는 일종의 규칙이 필요하다.
- Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙
- 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치
- Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 한다.
즉 return 하는 값은 조건부여서는 안 된다. 그렇기 때문에 위의 이useFriendStatus
Hook은 온라인 상태의 여부를boolean
타입으로 반환한다.
이렇게 만들어진 Custom Hook은 Hook 내부에 useState
와 같은 React 내장 Hook을 사용하여 작성할 수 있다. 일반 함수 내부에서는 React 내장 Hook을 불러 사용할 수 없지만 Custom Hook에서는 가능하다는 걸 잘 알아두자!
이제 이 useFriendStatus
Hook을 두 컴포넌트에 적용해보자.
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
확실히 훨씬 간단명료하고 더 직관적으로 확인이 가능하다!
🚨주의!🚨
같은 Custom Hook을 사용했다고 해서 두 개의 컴포넌트가 같은 state
를 공유하는 것은 아니다. 그저 로직만 공유할 뿐, state
는 컴포넌트 내에서 독립적으로 정의된다.