완로그
article thumbnail

⏳ 2023. 5. 29. - 2023. 6. 4.

 

컴포넌트 상태

저번 시간까지 전역 변수를 만들어 바뀌는 값을 처리했는데,

사실 리액트는 useState를 통해 상태를 관리할 수 있다.

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <div id="root"></div>
    <script type="text/babel">
      const rootEl = document.getElementById("root");

      const App = () => {
        const [keyword, setKeyword] = React.useState("");
        // useState의 구조는 이와 같다.
        // const keywordState = React.useState("");
        // const keyword = keywordState[0];
        // const setKeyword = keywordState[1];

        function handleChange(e) {
          setKeyword(e.target.value);
        }
        function handleClick(e) {}

        return (
          <>
            <input onChange={handleChange} />
            <button onClick={handleClick}>Search</button>
            <p>Looking for {keyword}</p>
          </>
        );
      };

      ReactDOM.render(<App />, rootEl);
    </script>
  </body>
</html>

 

저번 시간의 상태 관리를 useState를 통해 만들어보자.

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <div id="root"></div>
    <script type="text/babel">
      const rootEl = document.getElementById("root");

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

        function handleChange(e) {
          setTyping(true);
          setKeyword(e.target.value);
        }
        function handleClick(e) {
          setResult(`Found: ${keyword}`);
          setTyping(false);
        }

        return (
          <>
            <input onChange={handleChange} />
            <button onClick={handleClick}>Search</button>
            <p>{typing ? `Looking for ${keyword}` : result}</p>
          </>
        );
      };

      ReactDOM.render(<App />, rootEl);
    </script>
  </body>
</html>

컴포넌트 사이드 이펙트

변경이 일어날 때, 다른 곳에서도 부수적인 효과를 주기 위해 사용하는 훅 useEffect

useEffect를 사용하여 localStorage에 저장하는 효과를 만들어보자.

 

동기적인 처리를 위해 useState의 인수로 함수를 전달할 수 있다.

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <div id="root"></div>
    <script type="text/babel">
      const rootEl = document.getElementById("root");

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

        // React.useEffect(처음 나타날 때 실행하고 싶은 함수, 의존 배열)
        React.useEffect(() => {
          window.localStorage.setItem("keyword", keyword);
        }, [keyword]);

        function handleChange(e) {
          setTyping(true);
          setKeyword(e.target.value);
        }
        function handleClick(e) {
          setResult(`Found: ${keyword}`);
          setTyping(false);
        }

        return (
          <>
            <input onChange={handleChange} value={keyword} />
            <button onClick={handleClick}>Search</button>
            <p>{typing ? `Looking for ${keyword}` : result}</p>
          </>
        );
      };

      ReactDOM.render(<App />, rootEl);
    </script>
  </body>
</html>

useEffect의 두 번째 인자는 의존 배열인데, 3가지 상황이 있을 수 있다.

  1. 아예 없는 경우 : 컴포넌트가 리렌더링될 때마다 호출
  2. 빈 배열인 경우 : 처음에만 호출
  3. 의존값이 있는 경우 : 처음 호출이 일어나고, 의존값이 변경될 때마다 호출

커스텀 훅

localStorage에 keyword뿐만 아니라 result도 저장하고 싶다.

먼저 떠오르는 방법으로는, 의존성 배열에 result를 추가하면 되는 거 아닌가일 수 있다.

그러나 이 방법은 keyword가 변경될 때마다 result 값도 저장되므로 원하는 동작이 아닐 것이다.

 

그렇다면 useEffect를 keyword에 대해서, result에 대해서 전부 따로 따로 쓰면 된다!

하지만 이것은 굉장히 귀찮은 일이다.

 

그래서 있는게 커스텀 훅이다.

커스텀 훅은 use{Name}의 형태로 작성한다.

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <div id="root"></div>
    <script type="text/babel">
      const rootEl = document.getElementById("root");

      function 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 [result, setResult] = useLocalStorage("result");
        const [typing, setTyping] = useLocalStorage("typing", false);

        function handleChange(e) {
          setTyping(true);
          setKeyword(e.target.value);
        }
        function handleClick(e) {
          setResult(`Found: ${keyword}`);
          setTyping(false);
        }

        return (
          <>
            <input onChange={handleChange} value={keyword} />
            <button onClick={handleClick}>Search</button>
            <p>{typing ? `Looking for ${keyword}` : result}</p>
          </>
        );
      };

      ReactDOM.render(<App />, rootEl);
    </script>
  </body>
</html>

 


Hook flow

리액트는 어떻게 훅을 실행하는지 그 흐름을 알아보자.

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <div id="root"></div>
    <script type="text/babel">
      const rootEl = document.getElementById("root");

      const App = () => {
        console.log("!======= App render start =======!");
        const [show, setShow] = React.useState(() => {
          console.log("App useState");
          return false;
        });
        
        React.useEffect(() => {
          console.log("App useEffect: No DEPS");
        });
        React.useEffect(() => {
          console.log("App useEffect: EMPTY DEPS");
        }, []);
        React.useEffect(() => {
          console.log("App useEffect: [show]");
        }, [show]);
        
        function handleClick() {
          setShow((prev) => !prev);
        }
        
        console.log("======= App render end =======");
        return (
          <>
            <button onClick={handleClick}>Search</button>
            {show ? (
              <>
                <input />
                <p></p>
              </>
            ) : null}
          </>
        );
      };

      ReactDOM.render(<App />, rootEl);
    </script>
  </body>
</html>

렌더링이 끝나고 나서 Effect가 실행된다.

 

자식 요소가 있는 경우에는 어떤 흐름이 진행되는지도 알아보자.

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <div id="root"></div>
    <script type="text/babel">
      const rootEl = document.getElementById("root");

      const Child = () => {
        console.log("     !======= Child render start =======!");
        const [text, setText] = React.useState(() => {
          console.log("     Child useState");
          return "";
        });

        React.useEffect(() => {
          console.log("     Child useEffect: No DEPS");
        });
        React.useEffect(() => {
          console.log("     Child useEffect: EMPTY DEPS");
        }, []);
        React.useEffect(() => {
          console.log("     Child useEffect: [text]");
        }, [text]);

        function handleChange(e) {
          setText(e.target.value);
        }

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

        console.log("     ======= Child render end =======");
        return element;
      };

      const App = () => {
        console.log("!======= App render start =======!");
        const [show, setShow] = React.useState(() => {
          console.log("App useState");
          return false;
        });

        React.useEffect(() => {
          console.log("App useEffect: No DEPS");
        });
        React.useEffect(() => {
          console.log("App useEffect: EMPTY DEPS");
        }, []);
        React.useEffect(() => {
          console.log("App useEffect: [show]");
        }, [show]);

        function handleClick() {
          setShow((prev) => !prev);
        }

        console.log("======= App render end =======");
        return (
          <>
            <button onClick={handleClick}>Search</button>
            {show ? <Child /> : null}
          </>
        );
      };

      ReactDOM.render(<App />, rootEl);
    </script>
  </body>
</html>

자식 요소의 렌더링과 Effect 실행 후, 부모 요소의 Effect가 실행된다.

 

Effect 훅의 첫 인자는 컴포넌트가 처음 나타날 때 실행하고 싶은 함수라고 하였다.

이 함수는 또 다른 함수를 리턴할 수 있는데, 그 함수는 컴포넌트가 사라질 때 실행하는 함수(클린업 함수)이다.

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <div id="root"></div>
    <script type="text/babel">
      const rootEl = document.getElementById("root");

      const Child = () => {
        console.log("     !======= Child render start =======!");
        const [text, setText] = React.useState(() => {
          console.log("     Child useState");
          return "";
        });

        React.useEffect(() => {
          console.log("     Child useEffect: No DEPS");
          return () => {
            console.log("     [CLEAN UP] Child useEffect: No DEPS");
          };
        });
        React.useEffect(() => {
          console.log("     Child useEffect: Empty DEPS");
          return () => {
            console.log("     [CLEAN UP] Child useEffect: Empty DEPS");
          };
        }, []);
        React.useEffect(() => {
          console.log("     Child useEffect: [text]");
          return () => {
            console.log("     [CLEAN UP] Child useEffect: [text]");
          };
        }, [text]);

        function handleChange(e) {
          setText(e.target.value);
        }

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

        console.log("     ======= Child render end =======");
        return element;
      };

      const App = () => {
        console.log("!======= App render start =======!");
        const [show, setShow] = React.useState(() => {
          console.log("App useState");
          return false;
        });

        React.useEffect(() => {
          console.log("App useEffect: No DEPS");
          return () => {
            console.log("[CLEAN UP] App useEffect: No DEPS");
          };
        });
        React.useEffect(() => {
          console.log("App useEffect: EMPTY DEPS");
          return () => {
            console.log("[CLEAN UP] App useEffect: Empty DEPS");
          };
        }, []);
        React.useEffect(() => {
          console.log("App useEffect: [show]");
          return () => {
            console.log("[CLEAN UP] App useEffect: [show]");
          };
        }, [show]);

        function handleClick() {
          setShow((prev) => !prev);
        }

        console.log("======= App render end =======");
        return (
          <>
            <button onClick={handleClick}>Search</button>
            {show ? <Child /> : null}
          </>
        );
      };

      ReactDOM.render(<App />, rootEl);
    </script>
  </body>
</html>

값이 업데이트될 때는 클린업 함수가 먼저 실행되고, Effect 함수가 실행된다.

있던 것을 지우고 새로운 것으로 바꾸는, 불변의 상황을 만들기 위함으로 볼 수 있다.

profile

완로그

@완석이

프론트엔드 개발자를 꿈꾸는 완석이의 일기장입니다.