커스텀 훅과 훅 플로우

dev0408·2021년 11월 4일
6

Get to Know React

목록 보기
4/8
post-thumbnail

목표

  • 커스텀 훅을 만들어 본다.
  • 훅이 어떻게 발생하는지 흐름에 대해 알아본다.

1. 커스텀 훅(Custom Hook) 만들기

1-1. 커스텀 훅의 필요성

  const App = () => {
    const [keyword, setKeyword] = React.useState(() => {
      return window.localStorage.getItem("keyword") || "";
    });
    const [typing, setTyping] = React.useState(false);
    const [result, setResult] = React.useState("");

    React.useEffect(() => {
      window.localStorage.setItem("keyword", keyword);
    }, [keyword]);
	
    // 생략
  };
  • 위 코드는 keyword state 변수의 상태가 바뀔 때마다 localStorage에 저장하고 typing과 result state 변수는 초기화만 된 코드이다.

🤔 의문

  • keyword state 변수 뿐만 아니라 typing, result state 변수 까지 localStorage에 저장하고 싶다면 어떻게 해야 할까?
  • 일반적으로 useState를 여러번 선언해 독립적으로 상태를 관리하는 것처럼 useEffect도 여러번 선언할 것이다.
    const [keyword, setKeyword] = React.useState(() => {
      return window.localStorage.getItem("keyword") || "";
    });
    const [typing, setTyping] = React.useState(false);
    const [result, setResult] = React.useState("");

    React.useEffect(() => {
      window.localStorage.setItem("keyword", keyword);
    }, [keyword]);

    React.useEffect(() => {
      window.localStorage.setItem("typing", typing);
    }, [typing]);

    React.useEffect(() => {
      window.localStorage.setItem("result", result);
    }, [result]);
  • 하지만 동일한 동작을 수행하는 코드가 반복되니 효율성도 떨어질 뿐만 아니라 코드가 지저분해 보인다. 이를 효율적으로 만들기 위해서는 반복되는 코드를 하나로 묶어 함수화 작업을 해야한다. 리액트에서는 훅을 커스텀하는 것을 커스텀 훅(Custom Hook)이라고 부른다.

1-2. 커스텀 훅

  • 보통 use{Name}의 형태로 Name에는 함수의 특성에 맞는 이름을 넣어주면 된다.
  // 생략

  const useLocalStorage = (itemName, value = "") => {
    const [state, setState] = React.useState(() => {
      return window.localStorage.getItem(itemName) || value;
    });

    React.useEffect(() => {
      window.localStorage.setItem(itemName, state);
    }, [state]);

    return [state, setState];
  };

  const App = () => {
    const [keyword, setKeyword] = useLocalStorage("keyword");
    const [typing, setTyping] = useLocalStorage("typing", false);
    const [result, setResult] = useLocalStorage("result");

    const onChange = (e) => {
      setKeyword(e.target.value);
      setTyping(true);
    };

    const onClick = () => {
      setTyping(false);
      setResult(`search result of ${keyword}`);
    };

    const onKeyPress = (e) => {
      if (e.key === "Enter" && e.keyCode === 0) {
        onClick();
      }
    };

    // 생략
  • 공통되는 useState와 useEffect를 함수화하여 useLocalStorage이라는 커스텀 훅을 만들었다.
  • useLocalStorage 매개변수에 localStorage item 이름을 나타내는 itemName과 초기 값을 설정하는 value에 해당하는 인자를 넘겨준다.
  • 매개변수 value에 기본 값을 설정하였는데 이를 기본값 매개변수라고 하며 Mozila - 기본값 매개 변수에서 확인할 수 있다.
  • 위의 코드에서는 value에 해당하는 매개변수 값이 없다면 공백으로 기본값을 준다.
  • useLocalStorage 커스텀 훅을 사용해서 간편하게 keyword 뿐만 아니라 typing, result state 변수도 localStorage에 값을 저장할 수 있게 되었다.


2. 훅 플로우(Hook Flow)

  • useState, useEffect 등과 같은 훅들이 언제 호출되고 언제 사라지는지, 그리고 컴포넌트가 여러 개일 때에 각 컴포넌트의 훅은 언제 호출 되는지 등에 대해서도 알아야 한다.

  • 훅 플로우를 알기 위한 예제로 아래 코드는 버튼을 클릭하면 show state 변수를 이용해서 input 태그를 보여주거나 숨긴다. 그리고 p태그에 text를 계속 넣어준다.

  const rootElement = document.getElementById("root");

  const App = () => {
    const [show, setShow] = React.useState(false);
    const [text, setText] = React.useState("");

    const onClick = () => {
      setShow((prev) => !prev);
      setText((prev) => prev + "test ");
    };

    return (
      <>
        <button onClick={onClick}>search</button>
        {show ? (
          <>
            <input />
          </>
        ) : null}
        <p>{text}</p>
      </>
    );
  };

  ReactDOM.render(<App />, rootElement);
 

❗️ 참고

  • 위 코드에서 onClick 함수를 보면 setShow와 setText의 인자로 prevState를 넘겨 주었다.
  • 리액트에서는 useState로 만들어진 set함수들은 이전 값이 인자로 넘어오기 때문에 이 인자를 활용하여 코드를 더 간결하게 짤 수 있다.
  • 아래 코드는 이전 값을 사용하지 않았을 때와 사용했을 때의 코드이다.
// 이전 값 사용 X
const onClick = () => {
  if(show) {
    setState(false);
  } else {
    setState(true);
  }
};
// 이전 값 사용 O
const onClick = () => {
  setShow((prev) => !prev);
};

2-1. 훅의 호출 타이밍(단일 컴포넌트)

  • App 컴포넌트가 처음 렌더링 됐을 때, useState와 useEffect 그리고 App 컴포넌트 렌더링이 끝났을 때 로그를 찍어보면 다음과 같다.
  const App = () => {
    console.log("App Component Render Start");
    const [show, setShow] = React.useState(() => {
      console.log("App Component UseState");
      return false;
    });
    const [text, setText] = React.useState("");

    React.useEffect(() => {
      console.log("App Component UseEffect No Defendency Array ");
    });

    React.useEffect(() => {
      console.log("App Component UseEffect Empty Defendency Array");
    }, []);

    React.useEffect(() => {
      console.log("App Component UseEffect Defendency Array");
    }, [show]);

    console.log("App Component Render End");
    // 	생략
  };

  ReactDOM.render(<App />, rootElement);
 
  • 결과를 확인해보면 다음 순서와 같다.
  1. App 컴포넌트 렌더 시작
  2. useState 호출
  3. App 컴포넌트 렌더 끝
  4. useEffect 호출

🤔 의문

  • useEffect는 App 컴포넌트의 렌더링이 끝났을 때 발생한다는 것을 알 수 있다.
  • 그렇다면 useEffect 간 순서는 어떤 흐름으로 동작하는 것일까?

useEffect를 선언한 순서대로 호출된다. 가령, 위 코드에서 useEffect 간 순서를 바꿔보면 다음과 같은 결과를 볼 수 있다.

  const App = () => {
    // 생략
    
    React.useEffect(() => {
      console.log("App Component UseEffect Defendency Array");
    }, [show]);
    
    
    React.useEffect(() => {
      console.log("App Component UseEffect Empty Defendency Array");
    }, []);

    React.useEffect(() => {
      console.log("App Component UseEffect No Defendency Array ");
    });

    // 	생략
  };
  • 가장 늦게 선언한 의존성 배열이 없는 useEffect가 가장 늦게 호출되었다.
 

2-2. 훅의 호출 타이밍(부모 - 자식 컴포넌트)

  • 다음은 위에서 다뤘던 코드를 input과 text 부분을 따로 자식 컴포넌트(Child)로 뺀 코드이다.
  const Child = () => {
    console.log("Child Component Render Start");
    const [text, setText] = React.useState(() => {
      console.log("Child Component useState");
    });

    const onChange = (e) => {
      setText(e.target.value);
    };

    const element = (
      <>
        <input onChange={onChange} />
        <p>{text}</p>
      </>
    );

    console.log("Child Component Render End");
    return element;
  };

  const App = () => {
    console.log("App Component Render Start");
    const [show, setShow] = React.useState(() => {
      console.log("App Component UseState");
      return false;
    });

    React.useEffect(() => {
      console.log("App Component UseEffect Defendency Array");
    }, [show]);

    React.useEffect(() => {
      console.log("App Component UseEffect Empty Defendency Array");
    }, []);

    React.useEffect(() => {
      console.log("App Component UseEffect No Defendency Array ");
    });

    const onClick = () => {
      setShow((prev) => !prev);
    };

    console.log("App Component Render End");

    return (
      <>
        <button onClick={onClick}>search</button>
        {show ? <Child /> : null}
      </>
    );
  };
  • show state 변수 값에 따라 Child 컴포넌트를 호출하고 지운다.
 
  • 개발자 도구의 콘솔을 보면 App 컴포넌트의 렌더가 끝나고 Child 컴포넌트의 렌더가 시작되는 것을 알 수 있다.
  • 또한, Child 컴포넌트가 렌더가 끝나고 App 컴포넌트의 useEffect 함수들이 실행되는 것을 볼 수 있다.

🤔 의문

  • 그렇다면 Child 컴포넌트에서 useEffect는 언제 실행될까?
const Child = () => {
    console.log("Child Component Render Start");
    const [text, setText] = React.useState(() => {
      console.log("Child Component useState");
    });

    // 생략

    React.useEffect(() => {
      console.log("Child Component UseEffect Defendency Array");
    }, [text]);

    React.useEffect(() => {
      console.log("Child Component UseEffect Empty Defendency Array");
    }, []);

    React.useEffect(() => {
      console.log("Child Component UseEffect No Defendency Array ");
    });

    // 생략

    console.log("Child Component Render End");
    return element;
  };
  • Child 컴포넌트에 useEffect를 추가하고 콘솔을 살펴보면 다음과 같다.
 
  • 정리하면 다음과 같이 실행된다.
  1. App 컴포넌트의 렌더링(useState 포함)
  2. Child 컴포넌트의 렌더링 (useState 포함)
  3. Child 컴포넌트의 useEffect
  4. App 컴포넌트의 useEffect
  • useEffect를 호출하는 시점은 부모 컴포넌트보다 자식 컴포넌트가 먼저 호출되는 것을 확인할 수 있다.

2-3. useEffect의 Clean-up

  • 리액트에서는 Clean-Up 이라는 기능을 제공하는 데 이는 이전에 셋업했던 이펙트를 지우고 다시 생성해주는 역할을 한다.
  • Clean-Up은 return 문을 통해 실행할 수 있다.
  const Child = () => {
    // 생략

    React.useEffect(() => {
      console.log("Child Component UseEffect Defendency Array");
      return () => {
        console.log("Child Component Clean-Up Defendency Array");
      };
    }, [text]);

    React.useEffect(() => {
      console.log("Child Component UseEffect Empty Defendency Array");
      return () => {
        console.log("Child Component Clean-Up Empty Defendency Array");
      };
    }, []);

    React.useEffect(() => {
      console.log("Child Component UseEffect No Defendency Array ");
      return () => {
        console.log("Child Component Clean-Up No Defendency Array");
      };
    });

    // 생략
  };

  const App = () => {
    // 생략

    React.useEffect(() => {
      console.log("App Component UseEffect Defendency Array");
      return () => {
        console.log("App Component Clean-Up Defendency Array");
      };
    }, [show]);

    React.useEffect(() => {
      console.log("App Component UseEffect Empty Defendency Array");
      return () => {
        console.log("App Component Clean-Up Empty Defendency Array");
      };
    }, []);

    React.useEffect(() => {
      console.log("App Component UseEffect No Defendency Array ");
      return () => {
        console.log("App Component Clean-Up No Defendency Array");
      };
    });

    // 생략
  };
  • 이제까지의 코드를 실행하면 다음과 같은 결과가 발생한다.

 
  • 처음 App 컴포넌트가 렌더링 되면 useState와 useEffect가 호출된다.

 
 
  • 버튼을 눌러 Child 컴포넌트가 렌더링되고 App 컴포넌트의 Clean-Up이 호출되었다.
  • 그 후, Child 컴포넌트와 App 컴포넌트의 useEffect가 호출되었다.

 
 
  • input 창에 값을 입력하면 Child 컴포넌트가 렌더링 되고 Clean-Up이 호출되었다. 그 후, useEffect가 호출된 것을 볼 수 있다.

 
 
  • 다시 버튼을 눌러 Child 컴포넌트를 제거하면 Child, App 순으로 Clean-Up이 호출되고 그 후, App의 useEffect가 호출된 것을 볼 수 있다.
  • 다시 정리해보면 다음과 같이 실행된다.
  1. App 컴포넌트의 렌더링(useState 포함)
  2. Child 컴포넌트의 렌더링 (useState 포함)
  3. Child 컴포넌트의 Clean-Up
  4. App 컴포넌트의 Clean-Up
  5. Child 컴포넌트의 useEffect
  6. App 컴포넌트의 useEffect
  • 이처럼 원하는 시점에 원하는 기능을 넣기 위해서는 부모-자식 간 컴포넌트의 호출 시점을 잘 알아야 한다.

참고

profile
개발자가 되고 싶은 개발자

0개의 댓글