리액트에서 재사용되는 로직을 관리할 수 있는 방법 -> 사용자 정의 훅과 고차 컴포넌트 ## 사용자 정의 훅 (custom hook) 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것이 바로 사용자 정의 훅. [[#고차 컴포넌트]]는 리액트가 아니어도 사용할 수 있는 방법이지만 사용자 정의 훅은 리액트에서만 사용할 수 있는 방법이다. [[3.1 리액트의 모든 훅 파헤치기]]에서 살펴본 훅을 기반으로 새로운 훅을 만드는 기법이다. 사용자 정의 훅의 규칙 중 하나는 이름이 'use'로 시작해야 한다는 점이다. 사용자 정의 훅 뿐 아니라 리액트의 모든 훅들의 이름은 'use'로 시작한다. 사용자 정의 훅에도 이런 이름 규칙을 적용해서 다른 개발자도 해당 함수가 리액트 훅이라는 것을 빠르게 인지할 수 있다는 장점이 있다. ```tsx // http 요청하는 사용자 정의 훅 function useFetch<T>( url: string, { method, body }: { method: string; body?: XMLHttpRequestBodyInit } ) { // 응답 결과 상태 const [result, setResult] = useState<T | null>(null); // 로딩 상태 const [isLoading, setIsLoading] = useState(false); // 정상 응답 여부 상태 const [ok, setOk] = useState<boolean | null>(null); // HTTP Status const [status, setStatus] = useState<number | null>(null); useEffect(() => { const abortController = new AbortController(); (async () => { setIsLoading(true); const response = await fetch(url, { method, body, signal: abortController.signal, }); setOk(response.ok); setStatus(response.status); if (response.ok) { const apiResult = await response.json(); setResult(apiResult); } setIsLoading(false); })(); return () => { abortController.abort(); }; }, [url, method, body]); return { result, isLoading, ok, status }; } ``` 이렇게 fetch에 동반되는 반복적인 로직과 공통된 상태들을 하나의 custom hook으로 만들어두면 사용하는 쪽에서는 useFetch 훅만 사용해도 손쉽게 중복되는 로직을 관리할 수 있다. 사용자 정의 훅은 내부에 useState, useEffect 같은 훅을 가지고 자신만의 원하는 훅을 만드는 기법니다. 내부에서 훅을 사용하기 때문에 당연히 앞서 등장했던 [[3.1 리액트의 모든 훅 파헤치기#훅의 규칙|훅의 규칙]]들을 따라야 한다. 그리고 이 규칙을 적용받기 위해서는 use로 시작하는 이름을 가져야 한다. 만약 use로 시작하는 이름이 아니라면 에러가 발생한다. - [GitHub - uidotdev/usehooks: A collection of modern, server-safe React hooks – from the ui.dev team](https://github.com/uidotdev/usehooks) - [GitHub - streamich/react-use: React Hooks — 👍](https://github.com/streamich/react-use) - [GitHub - alibaba/hooks: A high-quality & reliable React Hooks library. https://alibaba.github.io/hooks/](https://github.com/alibaba/hooks) ## 고차 컴포넌트 고차 컴포넌트는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다. 고차 컴포넌트는 고차 함수의 일종으로 자바스크립트의 일급 객체, 함수의 특징을 이용하므로 굳이 리액트가 아니더라도 자바스크립트 환경에서 널리 쓰일 수 있다. ### React.memo 리액트에서 가장 유명한 고차 컴포넌트는 리액트에서 제공하는 API 중 하나인 React.memo다. 리액트 컴포넌트가 리렌더링하는 조건에는 여러 가지가 있지만 그 중 하나는 부모 컴포넌트가 렌더링될 때이다. 이는 자식 컴포넌트의 props 변경 여부와 관계 없이 발생한다. ```tsx function ChildComponent({ msg }: { msg: string }) { useEffect(() => { console.log("자식 컴포넌트 렌더링!"); }); return <div>{msg}</div>; } function Component() { const [state, setState] = useState(0); useEffect(() => { console.log("컴포넌트 렌더링!"); }); function handleClick() { setState((prev) => prev + 1); } return ( <div> <p>State: {state}</p> <button onClick={handleClick}>Increment</button> <ChildComponent msg="안녕하세요!" /> </div> ); } ``` ![[15e85b42-d566-44b7-abb9-3a8b153c2cd1.png]] 이렇게 props의 변화가 없는 상황에서도 자식 컴포넌트가 리렌더링되는 것을 방지하기 위해서 만들어진 리액트의 고차 컴포넌트가 바로 React.memo이다. React.memo는 props를 비교해서 props가 변경되지 않았다면 이전에 메모이제이션 해 둔 컴포넌트를 반환한다. ```tsx const ChildComponent = memo(function ({ msg }: { msg: string }) { useEffect(() => { console.log("자식 컴포넌트 렌더링!"); }); return <div>{msg}</div>; }); function Component() { const [state, setState] = useState(0); useEffect(() => { console.log("컴포넌트 렌더링!"); }); function handleClick() { setState((prev) => prev + 1); } return ( <div> <p>State: {state}</p> <button onClick={handleClick}>Increment</button> <ChildComponent msg="안녕하세요!" /> </div> ); } ``` ![[fc157ca4-b945-419e-aeb6-ff20a2f5485a.png]] 이렇게 memo로 감싸주면 ChildComponent의 props를 확인해서 동일하게 `'안녕하세요'` 문자열인 것을 보고 새로 컴포넌트를 렌더링하지 않고 이전의 컴포넌트를 사용한다. 이 방식은 앞서 클래스 컴포넌트를 다룰 때 나왔던 [[2.3 클래스 컴포넌트와 함수 컴포넌트#shouldComponentUpdate|PureComponent]]와 매우 유사하다고 볼 수 있다. React.memo는 컴포넌트고 값이라는 관검에서 본 것이므로 useMemo를 사용해서도 동일하게 메모이제이션할 수 있긴 하다. ```tsx function Component() { const [state, setState] = useState(0); useEffect(() => { console.log("컴포넌트 렌더링!"); }); const MemoizedChildCompoent = useMemo(() => { return <ChildComponent msg="안녕하세요!" />; }, []); function handleClick() { setState((prev) => prev + 1); } return ( <div> <p>State: {state}</p> <button onClick={handleClick}>Increment</button> {MemoizedChildCompoent} </div> ); } ``` 하지만 useMemo를 사용하는 경우에는 값을 반환받기 때문에 JSX 함수 방식이 아닌 `{MemoizedChildCompoent}` 같이 `{}` 을 사용한 할당식을 사용한다는 차이점이 있다. 필요하다면 이런 방식으로 구현할 수도 있지만 코드를 읽는 입장에서는 컴포넌트인지를 한눈에 알아보기 어려워 혼선이 있을 수 있으므로 목적과 용도가 뚜렷한 memo를 쓰는 것이 더 좋다. ### 고차 함수 만들어보기 리액트의 함수 컴포넌트도 결국 함수이기 때문에 고차 컴포넌트를 만들기 전에 함수를 기반으로 고차 함수를 만드는 것을 먼저 알아보자. 고차 함수의 사전적인 정의는 함수를 인수로 받거나 결과로 반환하는 함수이다. 가장 대표적인 고차 함수가 `Array.prototype.map`이다. ```js const list = [1, 2, 3, 4]; const doubledList = list.map((el) => el * el); ``` 이렇게 map은 함수인 `(el) => el * el` 을 받아서 매 원소에 적용해서 새로운 배열을 만든다. map 뿐 아니라 비슷하게 사용하는 forEach나 reduce 등도 고차 함수임을 알 수 있다. ```js // 즉시 실행 함수로 setter를 만들기 const setState = (function () { const currentIndex = index; return function (value) { global.states[currentIndex] = value } }) ``` 여기서 setState도 고차 함수의 정의 중 하나인 '함수를 결과로 반환하는 함수'에 부합하므로 고차 함수라고 할 수 있다. 그럼 고차 함수를 직접 만들어보자. 두 값을 더하는 add 함수와 2를 더하는 add2 함수를 고차 함수로 만들어보자. ```js function add(a) { return function (b) { return a + b; } } const add2 = add(2); const result = add2(3); console.log(result); // 5 ``` 클로저 개념을 적용해보면. `a=2`이라는 정보가 담긴 클로저가 `add2`에 포함되었고, `add2(3)`을 호출하면서 클로저에 담긴 `a=2`라는 정보를 활용해서 2+3의 결과를 반환할 수 있게 되었다. 이렇게 고차 함수를 활용하면 함수를 인수로 받거나 함수를 반환해서 완전히 새로운 결과를 만들어 낼 수 있다. 리액트의 함수 컴포넌트도 함수이므로 고차 함수를 하용하면 다양한 작업을 할 수 있다. ### 고차 함수를 활용한 리액트 고차 컴포넌트 만들어보기 사용자 인증 정보에 따라 인증된 사용자에게는 개인화된 컴포넌트를, 그렇지 않은 사용자에게는 별도로 정의된 공통 컴포넌트를 보여주는 시나리오를 만들어보자. 고차 함수의 특징에 따라 개발자가 만든 또 다른 함수를 반환할 수 있다는 점에서 이런 경우에 고차 컴포넌트를 사용하면 매우 유용하다. ```tsx interface LoginProps { loginRequired?: boolean; } function withLoginComponent<T>(Component: ComponentType<T>) { return function WithLogin(props: T & LoginProps) { const { loginRequired = false, ...rest } = props; if (loginRequired) { return <div>로그인이 필요합니다.</div>; } return <Component {...(rest as T)} />; }; } const Component = withLoginComponent(function Sample(props: { value: string }) { return <div>{props.value}</div>; }); function App() { const isLoggedIn = true; return ( <div> <Component value="Hello, World!" loginRequired={isLoggedIn} /> </div> ); } ``` 함수 컴포넌트 Component 를 withLoginComponent이라는 고차 컴포넌트로 감싸두었다. 이 컴포넌트는 loginRequired가 true이면 로그인이 필요하다는 메시지를 띄우고, false이거나 존재하지 않는다면 원래의 함수 컴포넌트가 반환해야 할 결과를 그대로 반한다. 이렇게 고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 더 큰 영향력을 컴포넌트에 미칠 수 있다. 단순히 값을 반환하거나 부수 효과를 실행하는 사용자 정의 훅과는 다르게 고차 컴포넌트는 컴포넌트의 결과물에 영향을 미칠 수 있는 다른 공통된 작업을 처리할 수 있다. 훅의 이름 규칙과 비슷하게 고차 컴포넌트도 with으로 시작하는 이름을 가져야 한다. 훅처럼 강제되는 규칙은 아니지만 withRouter처럼 리액트 커뮤니티에 관례적으로 쓰이는 규칙이다. 고차 컴포넌트를 사용할 때 주의할 점 중 하나는 부수 효과를 최소화해야 한다는 것이다. 고차 컴포넌트는 반드시 컴포넌트를 인수로 박데 되는데, 그 받은 컴포넌트의 props를 임의로 건드리지 않는 것이 좋다. 만약에 고차 컴포넌트에서 props를 수정한다면, 고차 컴포넌트를 사용하는 쪽에서는 전달하는 props가 어떻게 수정될지 모른다는 우려를 가지고 개발해야 한다는 불편함이 있다. 만약 컴포넌트에 무언가 추가적인 정보를 제공해야 한다면 별도의 props로 내려주는 것이 좋다. 마지막으로는 여러 개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 오히려 복잡성이 높아진다는 것이다. 고차 컴포넌트가 늘어날수록 개발자는 이것이 어떤 결과를 만들어 낼 지 예측하기 어려워진다. 따라서 고차 컴포넌트는 최소한으로 사용하는 것이 좋다. ## 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까? ### 사용자 정의 훅이 좋은 경우 리액트에서 제공하는 훅으로만 공통 로직을 처리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다. custom hook은 값을 반환하기만 할 뿐 그 값을 바탕으로 무엇을 할지는 개발자에게 달려 있다. 이 점에서 custom hook은 컴포넌트 내부에 미치는 영향을 최소화해서 개발자가 원하는 방향으로만 사용할 수 있다는 장점이 있다. 대부분의 고차 컴포넌트는 렌더링에 미치는 영향이 크므로 사용자 정의 훅에 비해서 예측하기가 어렵다. 따라서 단순히 동일한 로직을 반복해서 사용하거나. 특정한 훅의 동작을 취하게 하고 싶다면 사용자 정의 훅을 사용하는 것이 좋다. ### 고차 커포넌트가 좋은 경우 함수 커포넌트의 반환값, 증 랜더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하자. 고차 컴포넌트는 공통화 된 렌더링 로직을 처리하기에 매우 훌륭한 방법이다.