[React] Custom Hooks에 대하여

SuamKang·2023년 7월 30일
0

React

목록 보기
24/34
post-thumbnail

리엑트에서 제공하는 여러 hook중에 개발자가 더 활용할 수 있는 기능으로 Custom hook을 제공하고 있다.
그럼 이 커스텀 훅을 왜 쓰는지 보기 전에 무엇인지를 더 자세하게 볼 필요가 있을것 같다고 생각이 들었다.

Custom Hooks


Custom Hooks은 결국 정규 함수이다.
하지만 이 안에 상태를 설정할 수 있는 로직을 포함한 함수인것이다.

이 커스텀 훅을 만들게 되면, 재사용 가능한 함수에 상태를 설정하는 로직을 아웃소싱 할 수 있게 된다.
그리고, 정규 함수와는 다르게, 커스텀 훅은 다른 커스텀 훅을 포함한 다른 리액트 훅을 사용할 수 있다.


쉽게 말하면,
커스텀 훅으로 규정한 함수형 컴포넌트 안에서 useState나 useEffect, useReducer등을 통해 관리하는 리액트 상태를 활용할 수 있고, 이를통해 다양한 컴포넌트에서 호출이 가능한 것이다.

Custom hook === 로직 재사용이 가능한 메커니즘


예시


BackwardCounter.js

import { useState, useEffect } from 'react';

import Card from './Card';

const BackwardCounter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter - 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <Card>{counter}</Card>;
};

export default BackwardCounter;

FowardCounter.js

import { useState, useEffect } from 'react';

import Card from './Card';

const ForwardCounter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <Card>{counter}</Card>;
};

export default ForwardCounter;

위 두개의 카운터 함수형 컴포넌트를 구성하였다.
하나는 useEffect로 시간을 1초 지연하여 시간의 흐름에따라 증가하는 카운터 함수이고 하나는 그 반대이다.
매우 비슷한 로직임을 확인할 수 있다.

물론, 그냥 필요한 기능에 따라 컴포넌트를 만들고 해당 컴포넌트에 props를 사용해 설정하는 방법으로도 구성할 수 있지만 실제 애플리케이션에선 상호간에 연관된 작업을 수행하는 서로 다른 컴포넌트들이 있는 경우가 많기도 하다는 점이다.


따라서, 이 정방향, 역방향 카운터에도 중복된 코드가 존재하기때문에 이 부분을 따로 떼어내는 리펙토링을 하는것이 좋다.

공통된 부분을 따로 떼어내서 커스텀 훅으로 만들어 보는것이다!!


현재 이 두 함수형 컴포넌트에서 재활용 하려는 코드가 useState나 useEffect같은 리액트 훅이고, 이는 상태 갱신함수를 호출함으로써 상태를 갱신하고 있는걸 볼 수 있다.


앞서 배웠던 리액트 훅 규칙에 의하면
다른 함수안에서 이러한 리액트 훅(useState, useEffect 등등)을 사용하는 것은 불가능하다.

그게 가능하기 위해선

  1. 리액트 컴포넌트 함수
  2. 커스텀 훅
    에서만 사용 가능하다.

그렇기 때문에 이를 커스텀 훅을 이용해서 만드는 것이다.

1. 독립된 파일에 로직 저장하기


먼저 따로 떼어내고 싶은 로직을 독립적인 파일로 구성해준다.

hooks라는 디렉토리를 만들어 하위에 use-counter라는 파일을 만들었다.
(파일 이름앞엔 use라는 네이밍을 사용해주는 편이 보편적이다.)

그리고 나서 평소 함수형 컴포넌트를 제작하는 것과 동일하게 작성하되, 함수명은 use를 붙여 표현하도록 한다. 이건 필수이다!!!

그래야 이를 리액트가 커스텀 훅이라고 판단하여 해당 함수를 훅의 규칙에 따라 사용하겠다고 보장해주기 때문이다.

2. 재사용할 로직 추가하기


그다음은 간단하다.
우선 재사용할 로직을 그대로 함수안에 추가해 주자.

useCounter.js

import {useState, useEffect} from 'react'


const useCounter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

};

export default useCounter;

일단 이렇게 추가해주고 이 커스텀 훅을 사용할 컴포넌트에 가서 여타 다른 모듈을 추가하는것과 동일하게 호출해주자.

import { useState, useEffect } from 'react';

import Card from './Card';
import useCounter from '../hooks/use-counter';

const ForwardCounter = () => {

  useCounter();
  
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <Card>{counter}</Card>;
};

export default ForwardCounter;

이렇게 함수가 호출되면 useCounter훅에 있던 모든 코드가 실행 될것이고, 상태가 등록되면 그 상태나 효과가 커스텀훅을 사용하고 있는 컴포넌트(예시에선 FowardCounter.js)에 묶이게 된다.

그리고 이 FowardCounter컴포넌트가 아닌 다른 컴포넌트에서도 이 훅을 사용하게 되면, 모든 컴포넌트가 각자의 상태를 받게 된다.

중요한점은
적용하는 모든 컴포넌트 인스턴스가 각자의 상태를 받는거라 로직만 공유하지 상태를 공유하진 않는다.


이렇게 ForwardCounter에서 useCounter를 호출했으므로 상태는 ForwardCounter에 대해 설정하고 효과도 역시 FowardCounter에서 발동한다.

그럼 useCounter에서 다루는 그 상태들은 어떻게 접근하고 출력해야 할까??

3. 출력하기(jsx)


커스터 훅도 함수이므로 어떤 것이든 반환할 수 가 있는데,
여기서 useCounter는 이 counter를 사용할 것이기에 이를 반환만 하면 된다.

useCounter.js

import {useState, useEffect} from 'react'


const useCounter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);


  return counter;
};

export default useCounter;

이 반환하는 상태는 여러개면 배열도 가능하고 객체나 숫자도 가능하다.
이 카운터 예시에선 숫자 타입이 될테다!

따라서 사용되는 FowardCounter컴포넌트에서 이를 이렇게 이용할 수 있겠다.


FowardCounter.js

import Card from "./Card";
import useCounter from "../hooks/use-counter";

const ForwardCounter = () => {
  const counter = useCounter();

  return <Card>{counter}</Card>;
};

export default ForwardCounter;

이렇게 하면 useCounter가 상수 counter로 값을 반환하기 때문에 컴포넌트 안의 상수에 값을 저장할 수 있게 된다.

이로써 매우 간단해 진것같다.
이렇게 설정하고 다시 동작하면 정상적으로 동작하는걸 볼 수 있다.

커스텀 훅을 통해 컴포넌트간에 특정 로직을 공유할 수 있게 되는것도 확인 해보았다.




✔️ 또다른 컴포넌트에 연결&구성하기


하지만, 여기서 문제가 있다.
현재는 만든 useCounter 커스텀 훅의 로직이 정방향으로만 흐르는 상태의 로직으로 구성된것이다.
만약 역방향으로 카운팅 되는 컴포넌트에도 적용하려면 이 로직을 조금 바꿔줄 필요가 있다.

바로 함수를 쓸때 사용하는것처럼 인자와 매개변수를 받아들이게 설정해주는것이다.

그럼 어떻게 매개변수를 설정할것인가이다.
내가 원하는것은 카운터가 어떻게 증가하고 감소하는지 제어하는 지표가 필요하다.

내가 생각한 방법은
받아드리는 매개변수가 불리언 플래그로 해결을 꽤 할수도 있을것 같았다.

useCounter.js

import { useState, useEffect } from "react";

const useCounter = (forwards = true) => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
        if(forwards){
            setCounter(prevCounter => prevCounter + 1)
        }else {
            setCounter(prevCounter => prevCounter - 1)
        }
    }, 1000);

    return () => clearInterval(interval);
  }, [forwards]);

  return counter;
};

export default useCounter;

useCounter커스텀 훅의 매개변수로 forwards라는 불리언 플래그를 설정하고 기본값을 true로 한 상태에서 useEffect안 로직에 조건부로 true일때는 정방향으로, false일땐 역방향으로 로직을 분리하여 설정해줄 수도 있었다.


이렇게 되면 당연히 이 useEffect에선 외부에서 의존하는 값이 생기기 마련이니 매개변수로 받았던 forwards를 의존성으로 추가해 주어야 변경되는 불리언 값에따라 맞는 로직이 설정되어 적용이 잘 될것이기 때문이다.

위와 같이 설정을 한 후에 BackwardCounter컴포넌트에 적용을 하게 되면,

BackwardCounter.js

import Card from "./Card";
import useCounter from "../hooks/use-counter";

const BackwardCounter = () => {
  const counter = useCounter(false);

  return <Card>{counter}</Card>;
};

export default BackwardCounter;

사용하는 useCounter커스텀 훅의 기본값의 형태에 false를 추가하여 조건에 맞게 상태가 역방향으로 카운팅이 되게 설정할 수 있게 된다.

ForwardCounter.js

import Card from "./Card";
import useCounter from "../hooks/use-counter";

const ForwardCounter = () => {
  const counter = useCounter();

  return <Card>{counter}</Card>;
};

export default ForwardCounter;

ForwardCounter는 useCounter()안의 인자값으로 아무것도 지정해 주지않아도 기본값이 true로 설정 되어 있어서 괜찮다.

아쉬운점

위 방법대로 구성해도 좋다만, 뭔가 아쉽다.
인위적인 방법이기도 할것 같아서 좀 더 효과적으로 사용하는 시나리오를 연습해보자.

profile
마라토너같은 개발자가 되어보자

1개의 댓글

comment-user-thumbnail
2023년 7월 30일

정리가 잘 된 글이네요. 도움이 됐습니다.

답글 달기