useState, useEffect Hook API 알아보기

October 06, 2020

이글은 https://codeburst.io/master-usestate-useeffect-react-hooks-api-103239f79672 블로그를 참고하였습니다.

useState Hook 이해하기

React에서 state 는 component의 동적 데이터를 저장합니다. React는 상태 변경 사항을 선택하여 필요할 때마다 UI를 다시 렌더링 합니다.

가장 단순한 예는 UI에 표시되는 카운트수를 저장하는 상태값이 존재하는 카운터 버튼 페이지이며, 카운트가 업데이트 될 때마다 동적으로 변경됩니다. 일반적으로 함수 컴포넌트가 클래스 컴포넌트보다 선호됩니다. 코드가 더 깔끔하고, 디버깅하기 쉽기 때문입니다.

import React, { Component } from "react";

class App extends Component {
  constructor() {
    super();
    // Initialise initial state
    this.state = {
      countA: 0,
      countB: 0
    };
  }

  handleClickA = () => {
    // increases 'countA' in current state by 1
    let newCount = this.state.countA + 1;
    console.log(`Current Count A: ${newCount}`);
    this.setState({
      countA: newCount
    });
  };
  handleClickB = () => {
    // increases 'countB' in current state by 1
    let newCount = this.state.countB + 1;
    console.log(`Current Count B: ${newCount}`);
    this.setState({
      countB: newCount
    });
  };
  handleClickAB = () => {
    // increases both 'countA' & 'countB' in current state by 1
    let newCountA = this.state.countA + 1;
    let newCountB = this.state.countB + 1;
    console.log(
      `Current Count A : ${newCountA} | Current Count B : ${newCountB} `
    );
    this.setState({
      countA: newCountA,
      countB: newCountB
    });
  };

  render() {
    return (
      <>
        <div>
          <p>Count A: {this.state.countA}</p>
          <button onClick={this.handleClickA}>Increase Count A</button>
        </div>
        <div>
          <p>Count B: {this.state.countB}</p>
          <button onClick={this.handleClickB}>Increase Count B</button>
        </div>
        <div style={{ marginTop: "20px" }}>
          <hr />
          <button onClick={this.handleClickAB}>Increase Both Counts</button>
        </div>
      </>
    );
  }
}

export default App;

클래스 컴포넌트의 경우 클래스에서 this.tate = {...} 로 상태값을 초기화 하고, 상태를 수정하기 위해 this.state({...}) 를 사용합니다.

버튼 수를 늘리기 위해서는 다소 긴 Handler 함수를 만들어야 합니다. 이제 함수 컴포넌트로 정확히 동일한 앱을 작성하고, 비교해 보겠습니다.

함수 컴포넌트 사용하기

함수 컴포넌트에 상태를 저장하고, 업데이트 하기 위해서는 useState Hook API를 사용해서 실행할 수 있습니다.

import React, { useState } from "react";

function App() {
  const [countA, setCountA] = useState(0); //initialise countA to be 0
  const [countB, setCountB] = useState(0); //initialise countB to be 0

  const handleClickA = () => setCountA(prev => prev + 1);
  const handleClickB = () => setCountB(prev => prev + 1);
  const handleClickAB = () => {
    setCountA(prev => prev + 1);
    setCountB(prev => prev + 1);
  };
  return (
    <>
      <div>
        <p>Count A: {countA}</p>
        <button onClick={handleClickA}>Increase Count A</button>
        {/* <button onClick={()=>setCountA(prev => prev + 1)}>Increase Count A</button> */}
      </div>
      <div>
        <p>Count B: {countB}</p>
        <button onClick={handleClickB}>Increase Count B</button>
        {/* <button onClick={()=>setCountB(prev => prev + 1)}>Increase Count B</button> */}
      </div>
      <div style={{ marginTop: "20px" }}>
        <hr />
        <button onClick={handleClickAB}>Increase Both Counts</button>
      </div>
    </>
  );
}

export default App;

함수 컴포넌트로 작성된 코드가 다른 어떠한 상태를 처리하더라도 클래스형 컴포넌트보다 훨씬 더 깔끔하게 처리합니다.

useState 사용 방법

import React, { useState } from "react";
...
function App(){
   const [count, setCount] = useState(0)
   return(
    ...
   )
}

비구조화 할당된 배열의 첫번째 변수는, 생성할 state의 이름이 되고, 두번째 변수는 state를 업데이트 하는데 사용되는 핸들러 함수입니다. 그리고 useState() Hook는 이 상태에 대해 설정된 초기 상태가 됩니다.

상기 예제의 경우, count state는 0이 되며, count 상태값을 변경하기 위해 setCount 함수를 사용해야 합니다.

state를 업데이트 하는데 있어 자주 하는 실수 혹은 이슈들

const [count, setCount] = useState(0)
...
// Bad Practice
setCount(count + 1)
// Good Practice
setCount((prev)=>(prev + 1))

이 예제 중, 더 나은 방법은 prev(혹은 다른 변수 이름)을 통해 setCount 함수 콜백을 통해 이전 상테에 액세스 하는 것입니다. 이는 앱이 다른 연결로 인해 복잡해짐에 따라 상태를 건너뛸 수 있기 때문입니다.

useEffect Hook 알아보기

useEffect 는 다른 상태에서 구현 하려는 코드를 캡슐화 하는데 사용되는 Hook 입니다. React의 클래스 라이프 사이클 메소드에 익숙하다면, useEffect Hook은 componentDidMount, componentDidUpdatecomponentWillUnmount 를 모두 하나로 구현할 수 있는 기능을 가지고 있다고 볼 수 있습니다.

클래스 컴포넌트와 함수 컴포넌트를 비교해보겠습니다.

클래스형 컴포넌트

import React, { Component } from "react";

class App extends Component {
  constructor() {
    super();
    // Initialise initial state
    this.state = {
      count: 0,
      message: ""
    };
  }
  // Called right after component is mounted
  componentDidMount() {
    console.log("componentDidMount is called");
    // 'mock' api call to retrieve a message
    setTimeout(() => {
      let messageData = "This is a sample message retrieved";
      this.setState({ message: messageData });
      console.log("message is retrieved and set");
    }, 2000);
  }
  // Called everytime there is an update to component in DOM
  componentDidUpdate() {
    console.log(`componentDidUpdate is called`);
  }
  /*
  // Called everytime there is an update to component in DOM
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log(`componentDidUpdate is called`);

      // Called everytime there is an update to 'count' state
    if (this.state.count !== prevState.count) {
      console.log(`new count is ${this.state.count}`);
    }
  }
  */

  handleClick = () => {
    // increases 'count' in current state by 1
    let newCount = this.state.count + 1;
    this.setState({
      count: newCount
    });
  };
  render() {
    return (
      <>
        <div>
          <p>Message: {this.state.message}</p>
          <hr />
          <p>Count: {this.state.count}</p>
          <button onClick={this.handleClick}>Increase Count</button>
        </div>
      </>
    );
  }
}

export default App;

App 구성 요소가 처음에 마운트 된 직후 componentDidMount 가 어떻게 호출 되었는지 확인합니다. componentDidUpdate 도 컴포넌트에 대한 업데이트가 있을 때마다 호출되었습니다. 다음은 가장 일반적으로 사용 되는 두가지 클래스 라이프사이클 메소드입니다.

일부는 기존 Hook를 사용하여 컴포넌트에서 복제할 수 없습니다. ex) getDerivedStateFromError등을 사용해야 하는 경우 클래스 컴포넌트를 사용해야 합니다.

함수 컴포넌트 사용하기

import React, { useState, useEffect } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("");

  // Called right after component is mounted
  useEffect(() => {
    console.log("componentDidMount is called");
    // 'mock' api call to retrieve a message
    setTimeout(() => {
      let messageData = "This is a sample message retrieved";
      setMessage(messageData);
      console.log("message is retrieved and set");
    }, 2000);
  }, []);

  // Called everytime there is an update to component in DOM
  useEffect(() => {
    console.log(`componentDidUpdate is called`);
  });

  // Called everytime there is an update to 'count' state
  useEffect(() => {
    console.log(`new count: ${count}`);
  }, [count]);

  const handleClick = () => {
    // increases 'count' in current state by 1
    setCount(prev => prev + 1);
  };
  return (
    <>
      <div>
        <p>Message: {message}</p>
        <hr />
        <p>Count: {count}</p>
        <button onClick={handleClick}>Increase Count</button>
      </div>
    </>
  );
}

export default App;

보다 일반적인 라이프사이클 메소드만 수행하는 경우 클래스 컴포넌트와 똑같은 동작을 구현할 수 있으므로 함수 컴포넌트가 선호됩니다. useEffectcomponentDidMountcomponentDidUpdate 를 대체하여 사용 가능합니다.

useEffect 사용 방법

1. 시작때 컴포넌트가 마운트 된 이후 실행시키기

useEffect(() => {
...
}, []);

시작때 마운트된 이후 실행시키기 위해 두번째 인수로 빈 배열 [] 를 추가합니다. 이것은 컴포넌트가 마운트된 직후 한 번만 호출됩니다.

일반적으로 데이터 조회를 위한 비동기 API 호출이 여기에서 호출됩니다. 컴포넌트가 APP을 중단하지 않고, 초기 state를 렌더링 할 수 있도록, API에서 데이터를 조회하기 전 제대로 초기화 되어 있는지 확인합니다.

예를 들어, state는 [apimessages, setApiMessages] = useState('') 로 초기화 되지만, <p> apimessages[0] </p> 로 렌더링 되어 APP이 중단됩니다. 대신 빈 배열([])로 초기화 해야 합니다.

또한, 메모리 누수를 방지하기 위해서 컴포넌트를 마운트 해제한 이후 정리해야 하는 경우도 마찬가지입니다. return을 이용하여 컴포넌트가 unmount 될때 callback을 실행합니다.

useEffect(() => {
  startConnection()
  return ()=> {
    closeConnection() // Clean-up function
  }
}, []);

이 예제는 componentDidUnmount 와 비슷합니다.

2. 컴포넌트에 모든 렌더링 업데이트에 대해 호출

렌더링이 완료될 때마다 호출됩니다. 이를 위해서는 두번째 인수를 포함시키지 않습니다.

useEffect(() => {
...
});

3. 특정 state 값이 변경된 경우에만 실행

두 번째 인수로 지정된 배열에 정의된 state를 감시하고, 정의된 state가 변경되면 Hook을 실행할 수 있도록 합니다. 그런 다음 업데이트된 state 값으로 작업을 수행할 수 있습니다.

useEffect(() => {
   console.log(`updated count is ${count}`)
   ...
}, [count]);

이것은 일반적으로 사용되는 useEffect 구현으로 특정 state값을 보고 업데이트된 state 값으로 작업을 수행합니다.

다른 장소 (ex: onClick 함수) 에서 상태 값을 직접 사용할 수 있는데 왜 이렇게 해야 하는지 궁금할 수 있습니다.

이전 상태 (즉, setCount를 통해 새로운 상태를 설정 한 후 컴포넌트가 다시 렌더링 되기 전의 상태)의 상태 값을 사용하고 있으므로 불가능합니다.

useState setter function

주로 배열 또는 객체 데이터 type을 사용한 조작과 관련된 예제를 제공합니다. 물론 데이터 조작을 위한 함수를 사용할 수 있지만, setter 함수 내에서 모든 작업을 수행한다면 더 간소화 할 수 있습니다. ex) setNewState((prev) ⇒ (...));

// 상태값 뒤집기
setCollapse((prev) => (!prev));

// 이름값으로 배열의 항목 삭제
setArrayFiltered((prevArr) => (prevArr.filter((e) => (e !== name)));

// index 값으로 배열 삭제
setArrayFiltered(prevArr => prevArr.filter((e, i) => i !== index));

// array내 item push
setArray(prevArr => prevArr.concat(newItem)); // 방법1
setArray(prevArr => [...prevArr, newItem]); // 방법2

// 특정 index에 값 추가하기
setArray(prevArr => [...prevArr.slice(0, index), newItem, ...prevArr.slice(index)]);

// object value update
setData(prev => ({
	...prev,
	'key': newArr,
});

// object 외 추가 특정 사항 추가하기
setData((newArr,prev) => {
	let newArr = prev.key.filter((item, i) => i !== index);

	return {
		...prev,
		'key': newArr,
	};
});

// object내 arry update
setData((prev) => {
	return {
		...prev,
		'key': [...prev.key, newItem],
	};
});
...