React기본) Hooks & Context - Basic Hooks

lbr·2022년 8월 11일
0

Hooks

Hooks은 React에 아주 커다란 변화를 준, 최근에 나온 기술입니다.

클래스 컴포넌트에서만 state를 사용하고, lifecycle을 사용할 수 있었던 부분을,
함수 컴포넌트에서도 사용가능하게 해 준 기술입니다.

하지만, 단순히 state와 lifecycle을 사용할 수 있다는 점을 넘어서, 컴포넌트의 state와 관련된 로직을 재사용할 수 있다는 점에서 매우 큰 의의가 있는 기술입니다.

Basic Hooks

Basic Hooks에는 3가지가 있습니다.

  • useState
  • useEffect
  • useContext(Context API 에서 다룹니다.)

Hook은 v16.8 부터 추가된 기능입니다.

useState

state를 대체 할 수 있습니다.
함수 컴포넌트에서 특정 속성을 state 처럼 사용할 수 있게 해줍니다.

먼저, state의 count속성만 useState로 만들어 보는 예제를 해보겠습니다.

const [count, setCount] = useState(0);
const [스테이트 값, 스테이트 변경 함수] = useState(스테이트 초기값);
  • useState(0) : userState의 인자로 초기값을 넣을 수 있습니다.
  • 반환값은 배열입니다. 배열의 첫번째 item에는 state의 속성이, 두번째 item에는 그 속성을 변경하는 함수가 들어갑니다.

예시1

  • setCount(count + 1)를 호출하면 컴포넌트가 다시 실행이 됩니다.
    다시 실행이 되면 count가 setCount()의 인자로 넘겨준 값으로 바뀌게되고, return 부분의 {count} 가 바뀐 값으로 다시 출력됩니다.
  • 즉, setCount의 역할은 count의 값을 바꾸고, 이 컴포넌트(함수)를 다시 실행해줍니다.

예시2

const [state, setState] = useState({count: 0});
const [스테이트 값, 스테이트 변경 함수] = useState(스테이트 초기값);

위에서 한 예제1은 useState가 count 만을 의미하고 있습니다.

이번에는 count가 아니라 객체를 사용해서 { count: 0} 변경해 보겠습니다. state들을 하나의 객체에 몰아서 사용하겠다는 의미입니다.

  • useState()에 초기값으로 0이 아닌 이젠 객체가 들어가야 합니다. { count: 0 }
  • 값에 접근을 할 때에도 객체로 바뀌었으니 count가 아니라 state.count 입니다.
  • 변경 함수에도 count + 1 이 아닌 { count: state.count + 1 } 을 인자로 주어야 합니다.

추가

위 두 예제는 기존 state 값에 의존적으로 변경하는 로직을 작성했었습니다.

예제1) setState(count + 1);
예제2) setState( { count: state.count + 1 } );

이런 경우에는 setState의 인자로 함수를 많이 사용합니다.

setState((state) => {
  return {
    count: state.count + 1,
  };
});

화살표 함수를 줄여서 아래처럼 표현도 가능합니다.

setState((state) => ({
    count: state.count + 1,
  }));

이렇게 함수로 작성하는 것이 중요한 이유는
나중에는 setState만 사용하는 것이 아니기 때문에 setState가 어떤것에 의존해서 사용하고 있는지가 매우 중요해집니다.
useState 말고도 useEffect 등 다른Hook들과 사용하게 되면, 디펜던시 라는것이 들어가게 됩니다.
이전에 사용했던 방식은 디펜던시가 외부에서 들어오는 state를 디펜던시로 가지고 있지만, 지금처럼 setState 안에서 state를 인자로 주고 새로운 state를 만들어서 return하게 되면 setState가 사용하는 state는 외부(useState에서 만들었던 state)에 의존적이지 않고, setState 인자로 있는 내부 함수만 있으면 그냥 이 함수의 인자로 들어오기 때문에 state에 의존적으로 처리하지 않게 됩니다.
그래서 이 방식은 꼭 이해하고 있어야 합니다.

function 컴포넌트에서 useState를 사용할 수 있게 되면서, 더이상 function 컴포넌트는 stateless funcion component 가 아니게 되었습니다.

왜 function 컴포넌트에서 useState를 사용하게 만들었는가?

  • 컴포넌트 사이에서 상태와 관련된 로직을 재사용하기 어렵습니다.
    - 컨테이너 방식 말고, 상태와 관련된 로직.
    - 참고로 컨테이너 방식은 props를 이용해서 로직을 재사용한다는 느낌
  • 복잡한 컴포넌트들은 이해하기 어렵습니다.
  • Class 는 사람과 기계를 혼동시킵니다.
    - 컴파일 단계에서 코드를 최적화하기 어렵게 만든다.
  • this.state 는 로직에서 레퍼런스를 공유하기 때문에 문제가 발생할 수 있습니다.

useEffect

라이프 사이클 훅을 대체 할 수 있습니다.

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

대체할 수 있다는 것이지 동등한 역할을 한다는 것은 아닙니다.

React.useEffect(() => {
  console.log("componentDidMount");
  
  return () => {
  	// cleanup
    // componentWillUnmount
  }
}, []);

React.useEffect(() => {
  console.log("componentDidMount & componentDidUpdate by count");
  
  return () => {
    // cleanup
    // 다음 컴포넌트를 ComponentDidUpdate직전
  }
}, [count]);

useEffect의 두번째 인자로 React.DependencyList가 들어갑니다.
[](빈배열)을 넣은 경우 : 최초에만 실행이 됩니다.
아무것도 넣지 않은 경우 : 항상 render가 된 직후에는 무조건 첫번째 인자로 들어온 함수를 실행하라는 의미입니다.
[state] : 해당 state 변경시에만 동작합니다.

componentWillUnmount의 역할은 useEffect() 안의 return으로 반환하는 함수에서 합니다.
전문용어로는 cleanup이라고 합니다.
다음 render때, useEffect에 전달한 함수내용을 실행하기 전에, return으로 반환한 함수의 내용을 먼저 실행하고나서 useEffect로 전달된 함수가 실행됩니다.
첫 mount 때에는 cleanup이 실행되지 않습니다.

function App() {
  console.log("App start");
  const [count, setCount] = React.useState(0);
  console.log("after useState", count);

  React.useEffect(() => {
    console.log(
      '"componentDidMount & componentDidUpdate by count',
      count
    );
    return () => {
      console.log("cleanup by count", count);
    };
  }, [count]);

  function click() {
    setCount((count) => count + 1);
  }

  console.log("App render end");
  return (
    <div>
      <p>You clicked {count} times </p>
      <button onClick={click}>Click me</button>
    </div>
  );
}

실행결과

cleanup은 다음번 render를 위해 app컴포넌트가 재실행한 후 동작합니다. 하지만 이전값을 그대로 가지고 있습니다.

다른 예

function Child() {
  console.log("   Child start");
  const [state, setState] = React.useState(0);

  React.useEffect(() => {
    console.log("   Child componentDidMount");
    return () => {
      console.log("   Child componentWillUnmount");
      // cleanup
      // componentWillUnmount
    };
  }, []);
  React.useEffect(() => {
    console.log(
      "   Child componentDidMount & componentDidUpdate : ",
      state
    );
    return () => {
      console.log("   Child cleanup state : ", state);
    };
  }, [state]);
  console.log("   Child end");
  return <div>Child component. state: {state}</div>;
}

function App() {
  console.log("App start");
  const [count, setCount] = React.useState(0);
  const [flag, setFlag] = React.useState(true);
  console.log("after useState", count);

  React.useEffect(() => {
    console.log("componentDidMount");
    return () => {
      console.log("componentWillUnmount");
      // cleanup
      // componentWillUnmount
    };
  }, []);

  React.useEffect(() => {
    console.log("componentDidMount & componentDidUpdate by count", count);
    return () => {
      console.log("cleanup by count", count);
    };
  }, [count]);

  function click() {
    setCount((count) => count + 1);
  }

  function click2() {
    setFlag((flag) => !flag);
  }

  console.log("App render end");
  return (
    <div>
      <p>You clicked {count} times </p>
      <button onClick={click}>Click me</button>
      {flag ? <Child /> : null}
      <button onClick={click2}>toggle Child</button>
    </div>
  );
}

실행결과

컴포넌트를 render에서 제거하면 useEffect() 의 cleanup함수가 실행됩니다.
단, 다른 effect에는 반응하지않고, 제거시에만 동작하는 cleanup함수는 useEffect에 디펜던시로 빈 배열을 준 함수입니다.
그렇게 render에서 제거되면 cleanup함수는 업데이트때와는 다르게,(부모 컴포넌트가 존재할 경우) 1.부모 컴포넌트가 재실행되고, 2. 부모 컴포넌트의 render가 끝난 후에 3. 삭제된 자식 컴포넌트의 cleanup이 실행됩니다.
즉, 자식컴포넌트의 effect함수는 동작하지 않고, 바로 cleanup함수만 불려집니다.

참고 사이트

useEffect 완벽 가이드

https://www.rinae.dev/posts/a-complete-guide-to-useeffect-ko

0개의 댓글