[React] 커스텀 훅 사용하기 (useFetch, useInput)

정민·2022년 11월 2일
0

🤔Hook?

공식 문서

클래스 컴포넌트와 생명주기 메서드를 이용하여 작업을 하던 기존 방식에서 벗어나 함수형 컴포넌트에서도 더 직관적인 함수를 이용하여 작업할 수 있게 만든 기능 (= class를 작성하지 않고도 state와 다른 react의 기능들을 사용할 수 있게 해주었다.)

⚠️Hook 규칙

1. 최상위에서만 Hook을 호출해야 한다.

반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하면 안된다.

⇒ 이래야 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.

2. 오직 React 함수 내에서 Hook을 호출해야 한다.

Hook을 일반적인 JavaScript 함수에서 호출하면 안된다.

⇒ 이래야 컴포넌트의 모든 상태 관련 로직을 소스코드에 명확하게 보이도록 할 수 있다.

이 두가지 규칙을 강제하는 eslint-plugin-react-hooks 라는 플러그인이 있는데, 이 플러그인은 CRA 에 기본적으로 포함되어 있다.

🎨커스텀 훅을 만들어보자

커스텀 훅은 React의 특별한 기능이라기보다는, 기본적으로 Hook의 디자인을 따르는 관습이다.

💡커스텀 훅을 사용하면 좋은 점

컴포넌트에서 반복되는 로직을 함수로 뽑아내서, 쉽게 재사용 되게 할 수 있다.

⚠️ 커스텀 훅 규칙

커스텀 훅은 이름이 use 로 시작하는 자바스크립트 함수이다. 또한 다른 Hook 을 호출할 수 있다.

내가 구현했던 기존의 로그인 코드

import { useState, useEffect, useCallback } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useUserDispatch } from "../../contexts/userContext";
import * as style from "./style";

const LoginContainer = () => {
	const [loginDisabled, setloginDisabled] = useState(true);
  const [loginInput, setLoginInput] = useState({
    id: "",
    password: "",
  });
  const { id, password } = loginInput
  const navigate = useNavigate();
  const dispatch = useUserDispatch();

  const handleLoginClick = async () => {
    const response = await fetch("로그인 API 주소", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        id: id,
        password: password,
      }),
    });
    const json = await response.json();
    if (!response.ok) {
      alert(json.message);
      resetInput();
    } else {
      dispatch({ type: "SET_ID", id: json.id });
      dispatch({ type: "SET_LOGIN", isLogin: true });
      navigate("/nickname");
    }
  };

  const handleButtonChange = () => {
    const isNumberPassword = /[0-9]/g;
    const isAlphaPassword = /[a-z]/gi; //10~20 영어, 숫자만 허용
    id.length >= 6 &&
    password.length >= 10 &&
    isNumberPassword.test(password) &&
    isAlphaPassword.test(password)
      ? setloginDisabled(false)
      : setloginDisabled(true);
  };

  const resetInput = () => {
    setLoginInput({
      id: "",
      password: "",
    });
    setloginDisabled(true);
  };

  return (
    <div css={style.containerStyle}>
      <input
        css={style.inputStyle}
        value={id}
        onChange={(e) => {
          setLoginInput({ ...loginInput, id: e.target.value });
        }}
        placeholder="아이디를 입력하세요. (6자리 이상)"
        onKeyUp={handleButtonChange}
      />
      <input
        css={style.inputStyle}
        type="password"
        value={password}
        onChange={(e) => {
          setLoginInput({ ...loginInput, password: e.target.value });
        }}
        placeholder="비밀번호를 입력하세요. (영문 숫자 포함 10자리 이상)"
        onKeyUp={handleButtonChange}
      />
      <button
        css={style.loginButtonStyle}
        onClick={handleLoginClick}
        disabled={loginDisabled}
      >
        로그인
      </button>
      <Link css={style.linkStyle} to="/signup">
        <button css={style.signupButtonStyle}>회원가입</button>
      </Link>
    </div>
  );
};

export default LoginContainer;

훗날 회원가입, 기타 등등 기능 구현을 생각했을 때, fetchinput 관리는 많은 재사용이 될 것 같다. 그러면 위 두개를 커스텀 훅으로 구현해서 useFetchuseInput 을 만들어보자!

useFetch

import { useCallback } from 'react';

const useGet = (url: string) => {
    const get = useCallback(async () => {
        const response = await fetch(url);
        const data = await response.json();
        if (!response.ok) {
            throw new Error(data.message);
        } else {
            return data;
        }
    }, [url]);

    return get;
};

const usePost = (url: string) => {
    const get = useCallback(async (body: object) => {
        const response = await fetch(url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
        });
        const data = await response.json();
        if (!response.ok) {
            throw new Error(data.message);
        } else {
            return data;
        }
    }, [url]);

    return get;
}

export { useGet, usePost };

만약 서버에서 api 요청을 하는 fetch 를 받아올 때, 예상치 못한 오류가 발생하면 자동으로 에러 객체를 반환하도록 하였다.

response.ok 를 검사하는 것의 의미는, 반환 코드가 200~299 사이인지를 검사하는 것으로, 만약 서버, 혹은 클라이언트 오류로 400~599 를 리턴할 때 자동으로 에러를 throw하게 하는 것이다.

그리고 이후 컴포넌트에서 해당 훅을 사용할 때, try-catch로 에러를 검사해 그에 맞는 처리를 할 수 있도록 유도하였다.

useInput

import { useState, useCallback } from 'react';

function useInput (initialInput: any) {
  const [input, setInput] = useState(initialInput);
  const onChange = useCallback((e: any) => {
    const { name, value } = e.target;
    setInput((input: any) => ({ ...input, [name]: value }));
  }, []);
  const reset = useCallback(() => setInput(initialInput), [initialInput]);
  return [input, onChange, reset];
}

export { useInput };

input 을 관리할 때 필요한 것이 state , state 변경 함수, state 리셋 함수이다.

따라서 위의 세개를 대신 생성해주는 훅을 구현해, 실제 컴포넌트에서는 이 훅을 호출하는 것만으로 위 세개를 바로 사용할 수 있도록 구현하였다.

Custom Hook을 적용한 코드

import { useState, useEffect, useCallback } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useUserDispatch } from "../../contexts/userContext";
import { usePost } from "../../hooks/useFetch";
import { useInput } from "../../hooks/useInput";
import * as style from "./style";

const LoginContainer = () => {
  const [loginDisabled, setloginDisabled] = useState(true);
  const [ { id, password }, onInputChange, resetInput ] = useInput({
    id: "",
    password: "",
  })
  const navigate = useNavigate();
  const dispatch = useUserDispatch();
  const postLogin = usePost("로그인 API 주소")

  const handleLoginClick = async () => {
    try {
      const data = await postLogin({id: id, password: password});
      dispatch({ type: "SET_ID", id: data.id });
      dispatch({ type: "SET_LOGIN", isLogin: true });
      navigate("/nickname");
    } catch (error) {
      alert(error);
      resetInput();
    }
  };

  const handleButtonChange = () => {
    const isNumberPassword = /[0-9]/g;
    const isAlphaPassword = /[a-z]/gi; //10~20 영어, 숫자만 허용
    id.length >= 6 &&
    password.length >= 10 &&
    isNumberPassword.test(password) &&
    isAlphaPassword.test(password)
      ? setloginDisabled(false)
      : setloginDisabled(true);
  };

  return (
    <div css={style.containerStyle}>
      <input
        css={style.inputStyle}
        name="id"
        value={id}
        onChange={onInputChange}
        placeholder="아이디를 입력하세요. (6자리 이상)"
        onKeyUp={handleButtonChange}
      />
      <input
        css={style.inputStyle}
        type="password"
        name="password"
        value={password}
        onChange={onInputChange}
        placeholder="비밀번호를 입력하세요. (영문 숫자 포함 10자리 이상)"
        onKeyUp={handleButtonChange}
      />
      <button
        css={style.loginButtonStyle}
        onClick={handleLoginClick}
        disabled={loginDisabled}
      >
        로그인
      </button>
      <Link css={style.linkStyle} to="/signup">
        <button css={style.signupButtonStyle}>회원가입</button>
      </Link>
    </div>
  );
};

export default LoginContainer;

전 코드에 비해 드라마틱한 변화는 없지만, 훨씬 코드가 간결해진 것을 볼 수 있다!

profile
괴발개발~

0개의 댓글