커스텀 훅, 그게 뭔데?

제이제이·2022년 11월 26일
1
post-thumbnail

커스텀 훅🪡

커스텀 훅은 간단히 말해서 리액트의 훅을 이용해서 사용자가 직접 만드는 함수예요.

useState, useEffect, useCallback 등의 리액트 hook으로 구성해서 원하는 기능을 만들고 원하는 값을 반환해서 컴포넌트에서 사용해요.

리액트 hook은 최상단 함수(함수 컴포넌트)외의 함수에서는 사용할 수 없지만 커스텀 훅에서는 사용할 수 있기 때문에 일반 함수보다 더욱 유연하고 유용하게 사용할 수 있어요.

그리고 두개 이상의 컴포넌트에서 여러번 사용되더라도 각각 호출한 컴포넌트 안에서 state와 effect가 실행된 것이기 때문에 독립적으로 동작해요.

왜 만들까?🤔

커스텀 훅을 이용하면 여러 관심사로 이루어진 복잡한 로직을 분리할 수 있기 때문에 단순하고 읽기 좋은 컴포넌트를 만들 수 있어요.

재사용되는 로직이라면 당연히 유용하고, 외부 라이브러리를 사용할 때 훅으로 만들어 사용한다면가 그 라이브러리가 업데이트 되더라도 메인 로직과 분리되어 있기 때문에 유지보수 하기에도 좋아요.

학습 이유✏️

이전에 게더타운같은 서비스를 만들어봤는데, 캐릭터가 이동하는 것 같은 canvas와 game 관련 로직들만 컴포넌트와 분리해 class로 만들어서 사용했었어요. 이때 함수형인 지금의 리액트에서 class 대신할 방법이 없을까에 대한 고민이 있었어요.

새로운 프로젝트를 진행하면서 다른 팀원이 작성한 커스텀 훅들을 보게되었어요. 컴포넌트의 로직들을 훅으로 별도로 분리해서 상태관리하는 것을 보면서 유용하다고 느꼈고 학습의 필요성을 느꼈어요. 훅을 이용하면 그 안에서 useState로 상태관리, useEffect로 초기설정 등 다양한 활용도가 있다고 생각했고 관심사 분리에도 좋고 class의 대안으로 사용할 수 있을 것 같다는 생각도 들었어요.

이름 짓는 법🏷

커스텀 훅은 리액트의 기본 hook 처럼 use로 시작하는 이름으로 만들어야 해요. 리액트의 hook은 최상단 함수(함수 컴포넌트)에서만 호출될 수 있다는 규칙이 있는데, 커스텀훅은 리액트의 hook을 사용해서 만들기 때문에 이 규칙이 지켜져야해요. use라는 이름으로 시작해야만 한눈에 이 규칙이 적용되었는지를 파악할 수 있어요. 또한 cra에도 포함된 eslint-plugin-react-hooks로 자동으로 규칙을 체크할 때도 중요한 규칙이에요.

적용해보기💪

지금 진행하는 프로젝트는 유저가 처음에 단어를 제출하면 다음 유저가 그 단어를 그림으로 표현하고, 그 다음 유저가 그 그림을 보며 다시 단어를 제출하는 드로잉 게임이에요. (참고-갈틱폰) 이 프로젝트의 QuizReplySection 이라는 컴포넌트에 커스텀 훅을 적용해봤어요.

변경 전 코드

  • 유저가 퀴즈를 출제하는 0번 라운드일 때만 사용되는 기능(placeholder에 랜덤단어 보여주기, 유저가 퀴즈를 안내면 랜덤단어를 서버로 전송하기)과 유저가 답을 제출할 때 사용되는 기능이 모두 모여있었어요.
function QuizReplySection() {
    const isDraw = useRecoilValue(isQuizTypeDrawState);
    const { curRound } = useRecoilValue(roundNumberState);
    const quizReplyContent = useRecoilValue(quizReplyState);
    const [placeholder, setPlaceholder] = useState('그림을 보고 답을 맞춰보세요!');
    const [userAnswer, setUserAnswer] = useState('');
    const [quizSubmitted, setQuizSubmitted] = useRecoilState(quizSubmitState);

    useEffect(() => {
        setRandomWordToPlaceholder();
    }, [quizReplyContent]);

    function setRandomWordToPlaceholder() {
        // 0번 라운드일때만 인풋 플레이스홀더에서 유저에게 랜덤 단어를 보여준다.
        if (curRound === 0 && quizReplyContent !== '') {
            setPlaceholder(quizReplyContent);
        }
    }

    function submitBtnHandler() {
        setQuizSubmitted(!quizSubmitted);
        // 변경하기 버튼을 누른 경우에는 return.
        if (quizSubmitted) return;

        if (userAnswer === '' && curRound === 0) {
            sendRandomWordToServer();
            return;
        }

        sendUserWordReplyToServer();
    }

    function sendRandomWordToServer() {
        // 0번 라운드일 때, 유저가 출제한 퀴즈의 값이 없을 경우 이전에 받았던 랜덤 단어가 제출된다.
        emitSubmitQuizReply({ quizReply: { type: 'ANSWER', content: quizReplyContent } });
    }

		function sendUserWordReplyToServer() {
        // 유저가 입력한 값이 서버로 제출된다.
        emitSubmitQuizReply({ quizReply: { type: 'ANSWER', content: userAnswer } });
    }

    return (
        <Container>
            //(컴포넌트 생략)
        </Container>
    );
}

변경 후 코드

  • 퀴즈를 출제하는 0번 라운드에서만 사용되는 기능은 useZeroRound에서 만들고 꺼내서 쓸 수 있도록 했어요.
  • QuizReplySection에는 유저가 답을 제출할 때와 관련된 기능만 드러나게 되었어요.
function QuizReplySection() {
    const isDraw = useRecoilValue(isQuizTypeDrawState);
    const { curRound } = useRecoilValue(roundNumberState);
    const [userAnswer, setUserAnswer] = useState('');
    const [quizSubmitted, setQuizSubmitted] = useRecoilState(quizSubmitState);
    const { placeholder, sendRandomWordReplyToServer } = useZeroRound(curRound);

    function submitBtnHandler() {
        setQuizSubmitted(!quizSubmitted);
        // 변경하기 버튼을 누른 경우에는 return.
        if (quizSubmitted) return;

        if (userAnswer === '' && curRound === 0) {
            sendRandomWordReplyToServer();
            return;
        }

        sendUserWordReplyToServer();
    }

    function sendUserWordReplyToServer() {
        // 유저가 입력한 값이 서버로 제출된다.
        emitSubmitQuizReply({ quizReply: { type: 'ANSWER', content: userAnswer } });
    }

    return (
        <Container>
           //(컴포넌트 생략)
        </Container>
    );
}
function useZeroRound(curRound: number) {
    const quizReplyContent = useRecoilValue(quizReplyState);
    const [placeholder, setPlaceholder] = useState('그림을 보고 답을 맞춰보세요!');

    useEffect(() => {
        setRandomWordToPlaceholder();
    }, [quizReplyContent]);

    function setRandomWordToPlaceholder() {
        // 0번 라운드일 때만 인풋 플레이스홀더에서 유저에게 랜덤 단어를 보여준다.
        if (curRound === 0 && quizReplyContent !== '') {
            setPlaceholder(quizReplyContent);
        }
    }

    function sendRandomWordReplyToServer() {
        // 0번 라운드일 때, 유저가 출제한 퀴즈의 값이 없을 경우 이전에 받았던 랜덤 단어가 제출된다.
        emitSubmitQuizReply({ quizReply: { type: 'ANSWER', content: quizReplyContent } });
    }

    return { placeholder, sendRandomWordReplyToServer };
}

느낀 점❕

커스텀 훅을 처음 접했을 때는 생소해서 말 그대로 ‘그게 뭔데..’라는 생각이 들었어요. 하지만 사용 방법과 목적을 알고 적용해보니 앞으로 자주 사용할 것 같아요! 로직을 분리하는 것 외에도 재사용이나 외부 라이브러리를 감싸는 용도로도 시도해볼 예정입니다 :)

참고자료

자신만의 Hook 만들기 - React

profile
그날그날의 최선을 다하기💪 피드백은 언제나 환영입니다!

0개의 댓글