이슈
airbnb 클론 서비스 과제를 진행하던 중 여행자 인원을 선택하는 모달에서
만약 성인 인원수의 카운트를 올리면 다른 목록의 인원수까지 같이 올라가는 이슈가 발생하였다.
문제
count 상태를 관리하는 useState hook 하나로 성인, 어린이, 유아의 count 수를 제어했다.
이를 해결하기 위해 각 목록마다 useState로 count수를 관리해주었더니 반복된 코드가 생기는 문제 발생!
해결
Custom hook으로 해결할 수 있었다.
Hook은 함수이다. React 버전 16.8부터 추가된 React 요소로서,
기존 Class 바탕의 코드 -> 함수형 컴포넌트에서도 상태 관리(useState)나 생명주기 기능(lifecycle features)을 해주는 함수이다.
장점
- 클래스형 컴포넌트에 비해 적은 양의 코드로 동일한 로직을 구현 가능
- 코드량은 적으나 시인성(명료함) 면에서 장점이 있다. (useState, useInputs 등 ... )
- 📌상태관리 로직의 재활용이 가능하다.
-> 컴포넌트를 만들다보면, 반복되는 로직이 자주 발생한다. 반복되는 로직들을, React Hooks API를 통해 하나의 함수로 분할 & 재사용하는 것이 'Custom Hooks' 이다.
Custom Hook은 개발자가 직접 만들 수 있는 Hook.
반복되는 로직을 묶어 하나의 컴포넌트로 만들듯이 반복되는 메서드를 하나로 묶어 사용한다. 보통 Input과 Fetch를 관리할 때 자주 쓰인다.
통상 use+"키워드"
훅 내에선, React Hooks API를 사용하여 원하는 기능을 구현하고 컴포넌트에서 무엇을 인수로 받아야 하고 무엇을 반환해야 하는 지를 사용자가 결정.
Rules of Hooks 규칙들을 준수. (그렇지 않으면, 예측못한 동작이 발생할 수 있으며 디버깅이 난해해진다.)
📒Rules of Hooks
1) 최상위(at the Top Level)에서만 Hook을 호출해야 합니다.
2) 오직 React 함수 내에서 Hook을 호출해야 합니다.
airbnb 클론서비스에 적용해보자!
👉이전 코드
import React, { useState } from 'react'
import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem'
export default function TravelerModal({ anchorEl, setAnchorEl }) {
//반복 로직 발생
const [adultCount, setAdultCount] = useState(0)
const onIncreaseAdultCount = () => setAdultCount(adultCount + 1)
const onDecreaseAdultCount = () => setAdultCount(Math.max(adultCount - 1, 0))
const [childCount, setChildCount] = useState(0)
const onIncreaseChildCount = () => setChildCount(childCount + 1)
const onDecreaseChildCount = () => setChildCount(Math.max(childCount - 1, 0))
const [babyCount, setBabyCount] = useState(0)
const onIncreaseBabyCount = () => setBabyCount(babyCount + 1)
const onDecreaseBabyCount = () => setBabyCount(Math.max(babyCount - 1, 0))
const [petCount, setPetCount] = useState(0)
const onIncreasePetCount = () => setPetCount(petCount + 1)
const onDecreasePetCount = () => setPetCount(Math.max(petCount - 1, 0))
(...)
return (
<MenuItem sx={{ fontSize: '20px', padding: '16px' }}>
<div className="flex justify-between w-full py-4 px-8">
<div className="flex flex-col">
<span className="font-bold">성인</span>
<span className="text-lg text-gray-500">13세 이상</span>
</div>
<div className="flex justify-between w-28 items-center">
<button
onClick={onDecreaseAdultCount}
className="items-center text-3xl px-4 py-1 border rounded-full border-gray-500"
>
-
</button>
<span className="p-4">{adultCount}</span>
<button
onClick={onIncreaseAdultCount}
className="items-center text-xl px-4 py-2 border rounded-full border-gray-500"
>
+
</button>
</div>
</div>
</MenuItem>
(...)
)}
👉custom hook 적용 코드
import React, { useState } from 'react'
import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem'
// count 상태 Custom Hook
function useCount(initialCount) {
const [count, setCount] = useState(initialCount)
const onIncrease = () => {
setCount((prevCount) => prevCount + 1)
}
const onDecrease = () => {
setCount((prevCount) => Math.max(prevCount - 1, -1, 0))
}
return [count, onIncrease, onDecrease]
}
export default function TravelerModal({ anchorEl, setAnchorEl }) {
const [adultCount, onIncreaseAdultCount, onDecreaseAdultCount] = useCount(0)
const [childCount, onIncreaseChildCount, onDecreaseChildCount] = useCount(0)
const [babyCount, onIncreaseBabyCount, onDecreaseBabyCount] = useCount(0)
const [petCount, onIncreasePetCount, onDecreasePetCount] = useCount(0)
(...)
return (
<MenuItem sx={{ fontSize: '20px', padding: '16px' }}>
<div className="flex justify-between w-full py-4 px-8">
<div className="flex flex-col">
<span className="font-bold">성인</span>
<span className="text-lg text-gray-500">13세 이상</span>
</div>
<div className="flex justify-between w-28 items-center">
<button
onClick={onDecreaseAdultCount}
className="items-center text-3xl px-4 py-1 border rounded-full border-gray-500"
>
-
</button>
<span className="p-4">{adultCount}</span>
<button
onClick={onIncreaseAdultCount}
className="items-center text-xl px-4 py-2 border rounded-full border-gray-500"
>
+
</button>
</div>
</div>
</MenuItem>
(...)
)}
공통된 로직의 모든 것을 커스텀 훅으로 만들면 되는데, 어떤 경우에는 사용하면 안될까?
하나의 컴포넌트에서 여러 개의 custom Hooks 사용 시 state 업데이트는 고유성을 유지해야 하며 서로에게 영향을 주어선 안된다.
그렇다면 이 조건을 만족하지 못하는 경우는 언제일까?
React.memo 는 마지막 렌더링 시의 props와 현재 props를 비교해서 그 결과가 같다면 리렌더링을 하지 않는다. 즉, 전달받은 State가 변경될 때만 리렌더링이 발생한다고 가정해보자.
X라는 State가 변경될 때 리렌더링이 발생하고, Y라는 State가 변경될 때 리렌더링이 발생해야 한다면
두 State는 모두 React.memo를 사용한 A 훅으로 만들어 질 것이다.
그러면 만약 X State가 변경되면 Y의 A 훅으로 인해서 업데이트가 발생하지 않고, 반대의 경우도 마찬가지이다.
결국 서로에게 영향을 주게 되므로, custom hooks로 적합하지 않다!
위 예시를 이어서 말하면,
문제가 발생했을 때, A 커스텀 훅을 봄으로 바로 해결할 수 있을까?
아니다 A가 사용되는 곳을 봐야한다.
-> A 커스텀 훅이 사용된 모든 커스텀 훅을 찾아봐야한다는 문제.