[React] state 더 잘 활용하기

SuamKang·2023년 6월 17일
0

React

목록 보기
9/34
post-thumbnail

하나의 컴포넌트안에서 여러개의 상태들이 있는데
이 상태가 비슷하거나 같은 개념이 여러번 반복되는 상태라면

이 state들을 관리하는방법으로
usetState의 값으로 객체를 전달하는것이다.


그 객체 안에 들어가는 상태들은 키와값 형태로 지정했던 타입을 준수하며 초기값을 설정해 주면 된다.

이 state객체 하나로 여러개의 독립적인 state가 아니라 1개의 state처럼 관리 할 수 있기 때문에 더 가독성이 좋고 편리한 이점이 생길 수 있다.



아래 예시는 기존에 useState를 개별적인 상태로 다 지정을 해서 작성한 코드이다.

A

function ExpenseForm() {
  const [enteredTitle, setEnteredTitle] = useState("");
  const [enteredAmount, setEnteredAmount] = useState("");
  const [enteredDate, setEnteredDate] = useState("");

  const titleChangeHandler = (event) => {
    setEnteredTitle(event.target.value);
  };

  const amountChangeHandler = (event) => {
    setEnteredAmount(event.target.value);
  };

  const dateChangeHandler = (event) => {
    setEnteredDate(event.target.value);
  };

  return (
    <form>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input
            type="text"
            onChange={titleChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input
            type="number"
            min="0.01"
            step="0.01"
            onChange={amountChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            step="2022-12-31"
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
}

export default ExpenseForm;

아래는 하나의 객체안에 비슷한 개념의 상태를 하나의 객체로 관리하는 방법으로 작성한 코드이다.

B

function ExpenseForm() {

 const [userInput, setUserInput] = useState({
   enteredTitle: "",
   enteredAmount: "",
   enteredDate: "",
 })

  const titleChangeHandler = (event) => {
    setUserInput({
       ...userInput,
       enteredTitle: event.target.value,
    })
  };

  const amountChangeHandler = (event) => {
    setUserInput({
        ...userInput,
        enteredAmount: event.target.value,
    })
  };

  const dateChangeHandler = (event) => {
    setUserInput({
        ...userInput,
        enteredDate: event.target.value,
    })
  };

  return (
    <form>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input
            type="text"
            onChange={titleChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input
            type="number"
            min="0.01"
            step="0.01"
            onChange={amountChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            step="2022-12-31"
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
}

export default ExpenseForm;

다만, 이렇게 하나의 객체안에 여러개의 상태로 관리하게 될 때,
상태에 변화가 개별적으로 생길 경우
변화가 없는 데이터들은 변경 시 잃어버리지 않도록 꼭 확인 절차가 필요하다.

기본적으로 사용자의 새로운 입력을 이 객체에 설정한다면
입력받지않은 다른 데이터들의 키들은 버려지게 된다.
왜냐하면 state를 업데이트할 때 리엑트는 이전 state와 병합하지 않기 때문이다.

단순히 이전 state를 새것으로 대체하는것 뿐이다.

그렇기 때문에 하나의 state로 접근하여 하나의 객체를 관리한다면 수동으로 나머지 데이터들을 복사해주어야 한다.

복사시, 스프레드 연산자를 사용한다.(ES6 문법)

이는 불변성의 개념으로써,
자료형 타입이 참조자료형인 객체이기 때문에 새로운 값으로 업데이트가 될 때 주솟값을 새로운 값으로 대체하게 되고 이는 온전히 새로운 값으로 대체되는것이기 때문에 데이터를 잃지 않으려면 기존 값을 그대로 불러와서 복사해주어야 하는것이다.



✔️ 이전 state에 의존하는 state업데이트


이전 state에 의존하고 있을 경우 어떻게 state를 잘 업데이트 할 수 있을까?

바로 prevState를 사용하는것이다.

이렇게 이전 state에 의존하고 있다는 것은 여러개의 상태로 접근하는게 아닌 위 예시코드 처럼 하나의 상태로 접근하는 경우이며, 변경되지 않는 값들을 복사해서 잃지 않게 해줘야한다는 것이다.

따라서 기존 값을 복사하기 위해 이전 state의 스냅샷(prevState)에 의존하고 그다음 새로운 값으로 오버라이드 한다.

이는 state를 업데이트 할 때마다 기억해야 하는 아주 중요한 규칙 중 하나다.
만약 해당 state가 하나씩 증가하는 카운터를 관리 한다면
상태를 업데이트 할 때마다 업데이트 하는 그 함수를 위한 대체 폼을 사용해야 한다!

바로 갱신함수를 호출하고 그 인자로 콜백함수를 전달해야한다.

    setUserInput(() => {});

그리고 이렇게 함수에 전달하는 콜백함수는 리엑트에 의해 자동으로 실행되고, 업데이트 함수의 state를 위해서 콜백함수의 인자로 이전 state의 스냅샷을 받는다. 이는 prevState로 칭한다.

그리고 나서 함수안에선 새로운 state의 스냅샷(업데이트 한 state)을 반환해주면 된다.

    setUserInput((prevState) => {
       return {...prevState, enteredTitle: event.target.value}
    });

그렇다면 왜 prevState를 사용해서 적용해야하는걸까?

많은경우엔 두가지 방법 다 괜찮지만, 리엑트가 상태 업데이트 스케줄을 갖고 있어서 바로 샐행하지 않는다.

따라서 이론적으로 동시에 수많은 상태 업데이트를 계획한다면
'B'코드 방법으로 진행시, 오래되거나 잘못된 state 스냅샷에 의존할 수 있게 된다.


즉, 이전 state 스냅샷으로 접근하는 방법을 사용한다면, 리엑트는 갱신함수 안에 있는 콜백함수에서 가장 최신state의 스냅샷이라는 것과 항상 계획된 상태 업데이트를 염두해 두고있는게 보장되며 이는 좀 더 안전한 방법이다.

정리하자면,
이전상태에 따라 상태를 업데이트 하려면 prevState를 활용한 함수로 새로운 상태를 업데이트 하는 접근법으로 코드를 작성해야할것이다.

아래는 prevState을 적용해 B코드를 수정한 코드이다.

function ExpenseForm() {
    const [userInput, setUserInput] = useState({
      enteredTitle: "",
      enteredAmount: "",
      enteredDate: "",
    })

  const titleChangeHandler = (event) => {
    setUserInput((prevState) => {
      return { ...prevState, enteredTitle: event.target.value };
    });
  };

  const amountChangeHandler = (event) => {
   setUserInput((prevState) => {
      return { ...prevState, enteredAmount: event.target.value };
    });
  };

  const dateChangeHandler = (event) => {
    setUserInput((prevState) => {
      return { ...prevState, enteredDate: event.target.value };
    });
  };

  return (
    <form>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input
            type="text"
            onChange={titleChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input
            type="number"
            min="0.01"
            step="0.01"
            onChange={amountChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            step="2022-12-31"
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
}

export default ExpenseForm;
profile
마라토너같은 개발자가 되어보자

0개의 댓글