본문 바로가기

javascript/react

[React] custom hook

 프로그래밍 할때 우리는 코드의 여러 부분에서 공통적으로 혹은 유사하게 사용되는 로직을 함수라는 형태로 묶어 따로 분리한다. 이 경우 재사용성 및 유지보수가 좋아진다는 장점이 있다.

 예를 들어 특정 API에  http 요청을 하는 경우를 생각해보자. 하나의 API는 보통 유사한 url을 통해 외부에 정보를 제공해주기 때문에 주소 뒷단을 조금 바꾸면 다른 정보를 얻을 수 있는 경우가 많다. 정보를 fetch로 가져온다고 가정하면, 함수로 분리하지 않은 경우 필요한 정보가 있을 때마다 fetch 메서드에 url 및 headers 등의 각종 정보를 바꿔가며 사용해야 하지만, 만약 정보를 가져오는 전 과정 ( fetch 후 json ... ) 을 하나의 로직으로 생각하여 함수로 관리한다면 구현 방식에 따라 뒷단의 주소를 변경하는 것 만으로도 해당 API로부터 적절하게 정보를 가져올 수 있을 것이다.

 리액트에서 위와 같은 로직들은 보통 리액트 라이브러리가 제공하는 각종 hook을 포함하는 경우가 많다. 이때 hook들은 사용되는 위치에 대해 일련의 제약 조건을 가지기 때문에 일반적인 함수에서 처리할 수 없다. 따라서 hook이 포함될 수 있으며, 기존 컴포넌트 내부에서 구현되었던 로직을 가질 수 있는 함수가 필요한데, 리액트에서는 이러한 목적으로 custom hook을 지원한다.

custom hook

 기존의 hook들처럼 custom hook 역시 이름이 use~ 로 시작된다. 리액트는 이러한 이름으로 시작하는 함수만을 custom hook으로 판단하며, useState, useRef 등 의 hook을 내부에서 사용할 수 있도록 허락한다.

const useHttp = ({ req }: { req?: RequestInit } = {}) => {
    const [isWaiting, setIsWaiting] = useState(false);
    const [error, setError] = useState<Error | null>(null);
    
    //... some logic
    
    return {
    	val1 : val1,
        val2 : val2
        //...
    }
}

hook 내부에서 일련의 로직에 따라 함수가 진행되며, 외부로 노출시켜야 하는 값이 반환된다. 이때 노출되는 반환값의 타입은 지정되어 있지 않으며, 개인의 구현 방식에 따라 다양하게 존재할 수 있다.

ex) http fetch 요청을 위한 custom hook

import { useCallback, useState } from "react";

interface RequestInit {
    body?: BodyInit | null;
    cache?: RequestCache;
    credentials?: RequestCredentials;
    headers?: HeadersInit;
    integrity?: string;
    keepalive?: boolean;
    method?: string;
    mode?: RequestMode;
    redirect?: RequestRedirect;
    referrer?: string;
    referrerPolicy?: ReferrerPolicy;
    signal?: AbortSignal | null;
    window?: null;
}


const useHttp = ({ req }: { req?: RequestInit } = {}) => {
    const [isWaiting, setIsWaiting] = useState(false);
    const [error, setError] = useState<Error | null>(null);

    const fetchFunc = useCallback(async (url: string | string[]) => {
        setIsWaiting(true);
        setError(null);
        let json = null;
        try {
            if (!Array.isArray(url)) {
                const httpget = await fetch(url, req);
                if (httpget.ok) {
                    json = await httpget.json();
                }
            } else { // url is string 
                const all = await Promise.allSettled(
                    url.map(async inner_url => {
                        const inner_req = await fetch(inner_url, req);
                        const json = await inner_req.json();
                        return json;
                    })
                );
                return all.map(val => val.status === "fulfilled" ? val.value : val.reason);

            }
        }
        catch (err) {
            setError(err as Error);
        }
        setIsWaiting(false);
        return json;
    }, [req]);

    return {
        isWaiting,
        error,
        fetchFunc
    }
};

export default useHttp;

useHttp hook은 내부에서 대기 상태를 알리는 isWaiting 및 발생한 에러를 알리는 error에 대한 상태를 관리한다. 입력받은 requestInit 설정을 기반으로 return을 통해 반환되는 fetchFunc 에 원하는 url을 제출하여 특정 request를 받아올 수 있다. fetchFunc는 다수의 url이 들어오는 경우와 하나의 url이 들어오는 경우 다른 동작을 보이지만, 둘다 async/await 기반으로 동작한다는 공통점이 있다.

 

예제

https://github.com/blaxsior/JS_Study/tree/react-customhook-example-withGenshinApi

https://genshin.dev/

위에서 보인 useHttp hook을 이용하여 genshin.dev ( 팬이 만든 원신 API ) 로부터 원신 캐릭터들의 정보를 가져와 해당 정보를 보여준다.

  • Genshin API은 캐릭터 목록 정보를 문자열 배열 형태로 제공한다.
  • 각 캐릭터들은 일정한 포맷의 객체 형태로 반환된다.

useHttp를 이용하여 처음 fetch할 때는 string[]으로, 캐릭터 각각을 가져올 때는 any[]로 가져온다.

캐릭터들의 정보는 CGrid 컴포넌트에 넘겨진다. 해당 컴포넌트는 캐릭터들의 정보를 CInfoBox 컴포넌트에 담아 flex 방식으로 나열한다. 결과는 다음과 같다.

fetch를 통해 정보를 제대로 가져온 모습을 볼 수 있다. 

 

요약

custom hook은 리액트의 hook들을 사용할 수 있도록 지정된 특별한 함수로, 일반적인 hook들처럼 이름이 use~ 의 형태를 띈다. 여러 컴포넌트에서 반복해서 사용될 수 있는 로직을 내부에서 처리하고, 특정 결과를 반환받아 사용하도록 구성된다. 

'javascript > react' 카테고리의 다른 글

[React] 타입스크립트와 children 사용  (0) 2022.10.01
[React] react-router  (0) 2022.01.06
[React] react-redux  (0) 2021.12.24
[React] redux에 대한 개념  (0) 2021.12.23
[React] memo & useCallback  (0) 2021.12.21