커스텀 훅

zimablue·2023년 9월 4일

react

목록 보기
12/14

커스텀 훅 사용 예시

유사한 두 컴포넌트가 있습니다.
매 초마다 새로운 카운터 상태를 설정하는데, ForwardCounter.js+BackwardCounter.js-로 카운팅합니다.
이 것 외에는 둘 모두 동일한 로직입니다


// ForwardCounter.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;

// 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;

유사한 두 컴포넌트에서 중복된 코드를 떼어내고 공통되는 코드를 갖는 함수를 만들어서 리팩토링하려고 합니다.
문제는 재사용하려는 코드가 useState나 useEffect같은 리액트 훅을 사용합니다.
이는 상태 갱신 함수를 호출함으로서 상태를 갱신하게 됩니다.
다른 함수에서 리액트 훅을 사용하는 것은 훅의 규칙에 의하면 불가능합니다.
따라서, 별도의 함수에 이러한 공통 로직을 아웃소싱하려면 커스텀 훅을 만들어야 합니다.



커스텀 훅 규칙

커스텀 훅에는 규칙이 있습니다.
커스텀 훅으로 만들어진 함수는 이름이 use로 시작해야 합니다.
이름 앞에 붙인 use는 리액트에게 이 함수가 커스텀 훅임을 알려주며, 리액트가 해당 함수를 내장 훅의 규칙에 따라 사용하겠다고 보장해주는 것입니다.

// use-counter.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]);
  
};

export default useCounter;



커스텀 훅의 state

컴포넌트 안에서 커스텀 훅을 호출하고, 예를 들어 컴포넌트가 어떤 상태나 효과를 등록한다면, 그 상태나 효과가 커스텀 훅을 사용하고 있는 컴포넌트에 묶이게 됩니다.
만약 다수의 컴포넌트에서 커스텀 훅을 사용하게 되면 모든 컴포넌트가 각자의 상태를 받게 됩니다.어떤 컴포넌트에서 커스텀 훅을 사용해도 다른 컴포넌트에서 커스텀 훅이 재실행되고 해당하는 모든 컴포넌트 인스턴스가 각자의 상태를 받게 됩니다.
동일한 커스텀 훅을 공유해도 공유되는 것은 로직일뿐 상태가 아닙니다.
따라서 커스텀 훅의 state를 return으로 반환하면 커스텀 훅을 사용하는 컴포넌트에서도 각각의 state를 사용할 수 있습니다.

// use-counter.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;
// ForwardCounter.js

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

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

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

export default ForwardCounter;
// 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;





Form 커스텀 훅 사용 예시

커스텀 훅

// use-input.js

import { useState } from "react";

const useInput = (validateValue) => {
  // input 입력값 상태 관리
  const [enteredValue, setEnteredValue] = useState("");
  // focus 해지 상태 관리
  const [isTouched, setIsTouched] = useState(false);

  // input 입력값 유효성 검사
  const valueIsValid = validateValue(enteredValue);
  // focus가 해지되었고, 입력값이 유효성에 맞지 않다면 true
  const hasError = !valueIsValid && isTouched;

  // 입력값 상태 변경
  const valueChangeHandler = (event) => {
    setEnteredValue(event.target.value);
  };

  // focus 해지 상태 변경
  const inputBlurHandler = () => {
    setIsTouched(true);
  };

  // 상태 초기화
  const reset = () => {
    setEnteredValue("");
    setIsTouched(false);
  };

  return {
    value: enteredValue,
    isValid: valueIsValid,
    hasError,
    valueChangeHandler,
    inputBlurHandler,
    reset,
  };
};

export default useInput;

form

// BasicForm

import useInput from "../hooks/use-input";

// 유효성 검사, 재평가할 이유가 없기 때문에 컴포넌트 밖 위치
const isNotEmpty = (value) => value.trim() !== "";
const isEmail = (value) => value.includes("@");

const BasicForm = (props) => {
  // firstName 커스텀 훅
  const {
    value: firstNameValue,
    isValid: firstNameIsValid,
    hasError: firstNameHasError,
    valueChangeHandler: firstNameChangeHandler,
    inputBlurHandler: firstNameBlurHandler,
    reset: resetFirstName,
  } = useInput(isNotEmpty);

  // lastName 커스텀 훅
  const {
    value: lastNameValue,
    isValid: lastNameIsValid,
    hasError: lastNameHasError,
    valueChangeHandler: lastNameChangeHandler,
    inputBlurHandler: lastNameBlurHandler,
    reset: resetLastName,
  } = useInput(isNotEmpty);

  // email 커스텀 훅
  const {
    value: emailValue,
    isValid: emailIsValid,
    hasError: emailHasError,
    valueChangeHandler: emailChangeHandler,
    inputBlurHandler: emailBlurHandler,
    reset: resetEmail,
  } = useInput(isEmail);

  // 모든 input 값이 유효성 검사를 통과하면 true
  let formIsValid = false;
  if (firstNameIsValid && lastNameIsValid && emailIsValid) {
    formIsValid = true;
  }

  // form을 제출할 경우 입력된 input 값 비우기
  const submitHandler = (event) => {
    event.preventDefault();
    if (!formIsValid) {
      return;
    }
    console.log("Submitted", firstNameValue, lastNameValue, emailValue);
    resetFirstName();
    resetLastName();
    resetEmail();
  };

  // focus가 해지되었고, 입력값이 유효성에 맞지 않았을 경우
  const firstNameClasses = firstNameHasError
    ? "form-control invalid"
    : "form-control";
  const lastNameClasses = lastNameHasError
    ? "form-control invalid"
    : "form-control";
  const emailClasses = emailHasError ? "form-control invalid" : "form-control";

  return (
    <form onSubmit={submitHandler}>
      <div className="control-group">
        <div className={firstNameClasses}>
          <label htmlFor="name">First Name</label>
          <input
            type="text"
            id="name"
            value={firstNameValue}
            onChange={firstNameChangeHandler}
            onBlur={firstNameBlurHandler}
          />
          {firstNameHasError && (
            <p className="error-text">Please enter a first name.</p>
          )}
        </div>
        <div className={lastNameClasses}>
          <label htmlFor="name">Last Name</label>
          <input
            type="text"
            id="name"
            value={lastNameValue}
            onChange={lastNameChangeHandler}
            onBlur={lastNameBlurHandler}
          />
          {lastNameHasError && (
            <p className="error-text">Please enter a last name.</p>
          )}
        </div>
      </div>
      <div className={emailClasses}>
        <label htmlFor="name">E-Mail Address</label>
        <input
          type="text"
          id="name"
          value={emailValue}
          onChange={emailChangeHandler}
          onBlur={emailBlurHandler}
        />
        {emailHasError && (
          <p className="error-text">Please enter a valid email Address.</p>
        )}
      </div>
      <div className="form-actions">
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default BasicForm;



useReducer 버전 hook

import { useReducer } from "react";

// reducer
const initialInputState = {
  value: "",
  isTouched: false,
};
const inputStateReducer = (prevState, action) => {
  if (action.type === "INPUT") {
    return { value: action.payload, isTouched: prevState.isTouched };
  }
  if (action.type === "BLUR") {
    return { isTouched: true, value: prevState.value };
  }
  if (action.type === "RESET") {
    return { isTouched: false, value: "" };
  }
  return prevState;
};

const useInput = (validateValue) => {
  // useReducer
  const [inputState, dispatch] = useReducer(
    inputStateReducer,
    initialInputState
  );

  // input 입력값 유효성 검사
  const valueIsValid = validateValue(inputState.value);
  // focus가 해지되었고, 입력값이 유효성에 맞지 않다면 true
  const hasError = !valueIsValid && inputState.isTouched;

  // dispatch로 입력값 상태 변경
  const valueChangeHandler = (event) => {
    dispatch({ type: "INPUT", payload: event.target.value });
  };

  // dispatch로 focus 해지 상태 변경
  const inputBlurHandler = () => {
    dispatch({ type: "BLUR" });
  };

  // dispatch로 상태 초기화
  const reset = () => {
    dispatch({ type: "RESET" });
  };

  return {
    value: inputState.value,
    isValid: valueIsValid,
    hasError,
    valueChangeHandler,
    inputBlurHandler,
    reset,
  };
};

export default useInput;

0개의 댓글