🔥 학습목표
- React Hook에 대해 이해하고 설명할 수 있다.
- 함수 컴포넌트와 클래스 컴포넌트의 차이를 이해한다.
- 함수 컴포넌트에서 Hook을 사용하는 이유를 설명할 수 있다.
- useMemo, useCallback의 쓰임새와 작성 방법에 대해 공부한다.
- Custom Hooks의 쓰임새와 작성 방법에 대해 학습한 뒤 실습을 진행한다.
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
this.handleIncrease = this.handleIncrease.bind(this);
}
handleIncrease = ()=>{
this.setState({
counter: this.state.counter+1
})
}
render(){
return(
<></>
)
}
}
function Counter(){
const [counter, setCounter] = useState(0);
const handleIncrease = ()=>{
setCounter(counter + 1);
}
return (
<></>
)
}
객관적으로 봤을 때 클래스형 컴포넌트 보다 보기 편한 게 사실이다.
다만 함수 컴포넌트는 상태 값을 사용하거나 최적화할 수 있는 기능들이 조금 부족했는데,
그 부분들을 보완하기 위해 나온 개념이 바로 Hook 이다.
함수 컴포넌트가 state를 조작하고 최적화 기능을 사용할 수 있게 해주는 메서드.
즉 상태 및 여러 기능을 편리하게 사용할 수 있도록 도와주는 메서드를 말한다.
hook은 function으로만 리액트를 사용할 수 있게 해주는 것이므로 클래스형 컴포넌트에선 동작하지 않는다.
리액트 함수의 최상위에서만 호출해야 한다. 반복문, 조건문, 중첩 함수 내에서는 실행 불가능하다.
└▷ 컴포넌트 안에는 여러 번 Hook들이 사용될 수 있는데, 리액트는 이런 Hook들을 호출되는 순서대로 저장 해놓는다.
리액트 함수 내에서만 사용해야 한다.
└▷ 리액트 함수형 컴포넌트나 Custom Hook이 아닌 다른 일반 JS 함수 안에서는 호출 불가능하다.
특정 값을 재사용하고자 할 때 사용하는 Hook
아래 코드를 보자.
function Calculator({value}){
const result = calculate(value);
return <>
<div>
{result}
</div>
</>;
}
Calculator
함수는 calculate()
연산을 수행한 결과 result를 엘리먼트로 감싸 출력한다.
이때 Calculator
함수의 매개변수에 저장되는 value
값이 변하지 않는 이상 result
값은 그대로일 것이다.
그러나 브라우저가 리렌더링 될 때마다 컴포넌트는 해당 함수를 계속해서 호출하게 될 것이고, 그 때마다 result
값은 또다시 계산 된다.
만약 calculate()
함수가 엄청 복잡한 기능을 수행한다면 이러한 작업은 시스템에 무리를 가져다 줄 것이다.
그렇다면 value
값이 동일한 경우 이전에 계산한 값을 그대로 쓰는 게 어떨까? 그러기 위해 사용하는 게 바로 useMemo다.
import { useMemo } from "react";
function Calculator({value}){
const result = useMemo(() => calculate(value), [value]);
return <>
<div>
{result}
</div>
</>;
}
value
값이 동일할 시 그대로 사용할 변수 result
값을 useMemo()
로 감싸주면 끝난다.
이렇게 되면 이전에 구축된 렌더링과 새롭게 구축되는 렌더링을 비교했을 때 value
값이 동일한 경우 이전 렌더링의 value
값을 그대로 재활용한다.
바로 Memoization 로직을 useMemo가 대신 구현해주는 것이다.
세 입력 값 중 어떤 걸 수정할 때마다 setState()
가 실행되어 화면이 리렌더링 되고 add()
함수는 새로 계산된다.
그런데 이름을 입력할 경우에는 두 연산 값은 변함 없기 때문에 굳이 add()
함수를 재실행 할 필요가 없다.
따라서 useMemo(()=>add(val1, val2), [val1, val2])
로 감싼 다음 val1, val2
값이 이전 렌더링과 동일한 경우 이전에 계산해둔 값을 재사용한다.
콘솔창을 확인해보면 이름이 변경될 때는 add()
함수가 실행되지 않는다.
🌠 checkpoint -
memo
로 최적화를 만들기 전에...
🎁 다른 방법은 없을까?
함수의 재사용을 위해 사용하는 Hook
아래 코드를 보자.
function Calculator({x, y}){
const add = () => x + y;
return <>
<div>
{add()}
</div>
</>;
}
Calculator
함수는 두 개의 인자 x
, y
를 전달 받아 add 함수를 수행한 뒤 값을 출력한다.
이 함수는 컴포넌트가 렌더링 될 때마다 새롭게 만들어질 것이다.
하지만 역시나 x
, y
값이 변하지 않는다면 그 결과는 이전 렌더링 값과 동일할 것이고, 그 말은 즉 이 함수 또한 메모리에 저장해 두었다가 다시 꺼내서 사용하면 된다는 뜻이다.
import React, { useCallback } from "react";
function Calculator({x, y}){
const add = useCallback(() => x + y, [x, y]);
return <>
<div>
{add()}
</div>
</>;
}
어떤 함수에 useCallback
을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.
하지만 단순히 컴포넌트 내에서 함수를 반복 생성하지 않기 위해 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해를 불러일으킨다.
useCallback은 자식 컴포넌트의 props로 함수를 전달해줄 때 사용하기 좋은 기능이다.
input
란에 숫자를 바꾸면 컴포넌트가 리렌더링 되고 getItems()
함수가 새롭게 생성되어 새로운 결과를 계산한다.
그런데 dark mode 버튼을 눌러도 컴포넌트가 리렌더링 되어 getItems()
함수가 새로 생성 되고 <List>
컴포넌트의 조건부 useEffect
는 getItems
함수가 변했다고 판단하여 수행된다.
이때 getItems
함수에 useCallback
을 사용하여 input
값이 변하지 않는 경우엔 함수를 새로 생성하지 않고 이전에 사용한 함수 연산을 그대로 사용한다.
그렇게 되면 getItems
함수가 저장된 주소가 그대로이기 때문에 <List>
컴포넌트의 useEffect
가 실행되지 않는다.
콘솔 창을 확인하면 dark mode를 눌렀을 땐 '아이템을 가져옵니다'
메세지를 출력하지 않는다.
개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
등 반복되는 로직을 동일한 함수에서 작동하고 싶을 때 커스텀 훅을 사용한다.
이로써
아래 두 개의 컴포넌트가 있다.
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);
};
});
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>
);
}
위 두 개의 컴포넌트에서는 사용자가 온라인 상태인지 확인하는 동일한 로직이 존재한다.
Custom Hook을 사용하면 이 로직을 빼내서 두 컴포넌트에 공유할 수 있다.
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
이렇게 만든 Custom Hook은 내부에 다른 React 내장 Hook을 사용할 수 있다.(useState, useEffect...)
일반 함수 내부에서는 Hook을 부를 수 없는 데 Custom Hook은 가능하다는 점에서 차이가 있다.
이제 Custom 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을 정의할 때는 함수 이름 앞에 use
를 붙이는 것이 규칙이다.
대개의 경우 프로젝트 내 hooks 디렉토리에 Custom Hook을 위치시킨다.
Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 한다. 즉, return 하는 값이 조건부여서는 안 된다. 따라서 위 예시에서도 Hook은 온라인 상태 여부를 boolean 으로 반환한다.
같은 Custom Hook을 사용했다고 두 개의 컴포넌트가 같은 state를 공유하진 앟는다. 그저 로직만 공유할 뿐 state는 컴포넌트 내에 독립적으로 정의되어 있다.
const useFetch = ( initialUrl:string ) => {
const [url, setUrl] = useState(initialUrl);
const [value, setValue] = useState('');
const fetchData = () => axios.get(url).then(({data}) => setValue(data));
useEffect(() => {
fetchData();
},[url]);
return [value];
};
export default useFetch;
import { useState, useCallback } from 'react';
function useInputs(initialForm) {
const [form, setForm] = useState(initialForm);
// change
const onChange = useCallback(e => {
const { name, value } = e.target;
setForm(form => ({ ...form, [name]: value }));
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return [form, onChange, reset];
}
export default useInputs;
여기까진 내가 이해를 잘 하고 적용하는 법을 터득한 줄 알았다...
하지만 두 번째 실습에서 전혀 착각이었음을 깨달았다. 내가 여태까지 Hook의 기초, 아니, 리액트의 기본도 제대로 알지 못한 채로 코딩을 했구나... 싶었다.
일단 비슷하지만 디테일이 살짝 다른 두 가지 코드가 있다.
첫 번째는 Q&A 멘토님께서 알려주신 코드.
가장 먼저, 입력란에 성, 이름을 입력하면 실행되는 onChange 함수는 모든 <input>
태그마다 반복 될 것이다. 이 작업을 Hook으로 빼내면 코드가 단축 되는 효과를 볼 수 있다.
이 공통 된 부분을 도려내서 customHook으로 만든 게 util/useInput.js
이다.
useInput
훅은 입력란에 대한 onChange 이벤트 리스너인 changeFormData
함수를 정의하고 있고, 값이 변동될 때마다 업데이트 되는 data
state를 관리한다. 반환 값은 입력값 data
, changeFormData
이벤트 리스너다.
그런데 여기서 한 가지 의문이 들었다.
App.js
의 9번 줄에서 한 번 훅을 실행하고(초기화),
입력란에 값이 들어가면 리렌더링 되어 해당 컴포넌트가 재실행 되는데,
9번 줄의 form은 계속해서 const [form, chageFormData] = useInput((초기값))
일 뿐인데 form.firstName
과 form.lastName
을 출력하면 바뀐 값이 저장되어 있는 거다.
useInput()
훅이 다루는 상태 data
는 갱신 된다고 쳐도, form
은 항상 useInput
에 초기값을 넘겨주고 있는데 왜?
🌠 질문에 대한 답변
🎁 출처
함수형 컴포넌트의 라이프 사이클을 다시 떠올려 훅이 실행되는 과정을 되짚어보자.
- 마운트(Mount) - 리액트가 처음으로 구성요소를 렌더링하고 초기 Real DOM을 빌드할 때이다.
- 렌더링(Rendering) - DOM 생성을 위해 함수가 호출(or 클래스 기반 메서드가 호출) 될 때이다.
컴포넌트가 render 될 때는 mount 과정을 거치지만, props, state가 변경되어 render 될 때에는 mount를 거치지 않는다.
마운트 → (훅 생성/실행) → 렌더링 → state 값 변경 → (기존에 갖고 있던 훅 재실행) → 리렌더링
이 되는 것이다.
기존에 갖고 있던 훅을 재실행 한다는 것이 바로 form
값이 갱신되는 부분이다.
나는 상태값에 변화가 일어나 화면이 리렌더링 되면, 함수처럼 재실행 되어 또 한 번 초기값을 넘겨주는 코드가 실행되는 줄 알았다.
그런데 그렇게 되면 더이상 Hook이 아닌 거다.
기존의 Hook이 존재하는 상태에서, 리렌더링 되기 전에 그냥 useEffect Hook처럼Custom Hook만 다시 재실행 된다.
회색으로 표시한 인자 값은 더이상 넘겨주지 않는 거다.
data
값은 방금 입력받은 값으로 갱신 된 상태이기 때문에
return [data, chageFormData]
를 통해
form
이 갱신 되는 거였다.
함수가 다시 재실행 되는 것처럼 초기화 인자를 넘겨주는 계산을 수행하는 줄만 알았다.
그냥 갱신한 상태값을 지닌 상태로 Hook만 발동하는 거였다.
사실 아직도 조금 신기하다.
단, 언마운트 된 뒤 다시 실행하면 초기값을 넘겨주는 처음 단계로 돌아간다.
두 번째는 굿모닝 세션에서 코치님이 알려주신 코드. 형식은 같지만 사소한 디테일이 살짝 다르다.
제출 버튼을 클릭하면 입력란 안에 들어있는 글자들이 리셋되는 기능이 추가되었다.