본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -Updating Arrays in State(배열 state 업데이트)

JavaScript 배열은 변경 가능하지만 state에 저장할 때는 변경이 불가능한 것으로 취급해야 하며, 객체와 마찬가지로 state에 저장된 배열을 업데이트하려면, 새로운 배열을 만들고(또는 기존 배열의 복사본을 만들거나) 새 배열을 사용하도록 state를 설정해야 한다.


Updating arrays without mutation(변이 없이 배열 업데이트하기)

 

JavaScript에서 배열은 객체의 또 다른 종류일 뿐이다. 따라서, 객체와 마찬가지로 React state의 배열은 읽기 전용으로 취급해야한다. 즉, arr[0] = "new"와 같이 배열 내부의 항목을 재할당하거나, push(), pop()과 같이 배열을 변이하는 메서드를 사용해서는 안된다.

 

대신, state 설정 함수에 새 배열을 전달하고 싶다면, state의 원래 배열에서 filter() 및 map() 과 같이 비변이 메서드를 호출하여 새배열을 만들어야 한다.

 

  avoid(mutates the array)
비추천(배열 직접 변이)
perfer(returns a new array)
추천(새 배열 반환)
adding(추가) push, unshift concat, [...arr] : spread syntax
removing(삭제) pop, shift, splice filter, slice
replacing(교체) splice, arr[i] = ... map
sorting(정렬) reverse, sort copy the array first
배열을 복사한 다음 처리

또는 위 표의 두 열에 있는 메서드를 모두 사용할 수 있는 Immer를 사용할 수도 있다


Adding to an array(배열에 추가하기)

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

위의 예제는 push() 메서드를 사용해서 배열을 변이시키고 있는데, 이를 대신할 수 있는 방법에 대해서 알아보자.

 

전개 구문(spread syntax) 사용하기

// push와 같이 배열의 마지막 요소에 추가
setArtists( // Replace the state
  [ // with a new array
    ...artists, // that contains all the old items
    { id: nextId++, name: name } // and one new item at the end
  ]
);

// unshift와 같이 배열의 첫번째 요소에 추가
setArtists([
  { id: nextId++, name: name },
  ...artists // Put old items at the end
]);

Removing from an array(배열에서 제거하기)

 

배열에서 항목을 제거하는 가장 쉬운 방법은 필터링을 하는 것인데, filter 메서드를 사용할 수 있다.


Transforming an array(배열 변경하기)

배열의 일부 또는 모든 항목을 변경하려면 map()을 사용해서 새로운 배열을 만들 수 있다.


Replacing items in an array(배열에서 항목 교체하기)

배열의 항목을 바꿀때에서 map을 사용할 수 있다. map의 콜백함수는 두 번째 인자로 항목의 인덱스를 받게 되는데, 이를 사용하여 원래 항목(첫 번째 인수)을 반환할지 아니면 다른 것을 반화할지 결정할 수 있다.

  function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // Increment the clicked counter
        return c + 1;
      } else {
        // The rest haven't changed
        return c;
      }
    });
    setCounters(nextCounters);
  }

 

Inserting into an array(배열에 삽입하기)

 

배열은 시작과 끝이 아닌 특정 위치에 항목을 삽입하고자 할때는 slice() 메서드를 사용할 수 있다.


Making other changes to an array(배열에 다른 변경 사항 적용하기)

 

배열을 반전시키거나 정렬을 하고 싶을 때에는 원래 배열을 변이 시키는 reverse나 sort을 직접 사용할 수 없고, 배열을 먼저 복사한 다음 변이하면 된다.

const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

const [list, setList] = useState(initialList);

function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

하지만, 배열을 복사하더라도 배열 내부의 기존 항목을 직접 변이할 수는 없다. 얕은 복사의 경우 원래 배열과 동일한 항목이 포함되기 때문이다. 즉, 복사된 배열 내부의 객체를 수정하면 기존 state를 변이하는 것이다.

const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);

위의 예제에서 nextList와 list는 서로 다른 배열이지만, nextList[0]과 list[0]은 같은 객체를 가리킨다. 


 

Updating objects inside arrays(배열 내부의 객체 업데이트하기)

 

객체는 실제로 배열 내부에 위치하지 않기 때문에, 위의 예제와 같이 list[0]과 같이 중첩된 필드를 변경할 때는 주의해야한다.

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    const myNextList = [...myList];
    const artwork = myNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setMyList(myNextList);
  }
  
  // map을 사용해서 기존 state 항목을 변이시지키 않는 방법
  function handleToggleMyList(artworkId, nextSeen) {
    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    const yourNextList = [...yourList];
    const artwork = yourNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setYourList(yourNextList);
  }

// map을 사용해서 기존 state 항목을 변이시지키 않는 방법
  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

위 예제에서 두 개의 개별 작품 목록의 초기 state가 동일하다. 두 목록은 분리되어야 하지만 변이로 인해 state가 실수로 공유되어 한 목록의 체크박스를 선택하면 다른 목록에 영향을 미친다. 따라서, map을 사용해서 이전 항목에 대한 변이 없이 업데이트된 버전으로 대체할 수 있다.

 

즉, 이미 있는 state의 artwork을 다루는 경우에는 복사본을 만들어야하며, 일반적으로 방금 만든 객체만을 변이해야한다. 


Write concise update logic with Immer(Immer로 간결한 업데이트 로직 작성하기)

 

Immer는 변이 구문을 사용하여 작성하더라도 자동으로 사본 생성을 처리해주어 중첩 배열을 업데이트할 때 유용하다.

updateMyTodos(draft => {
  const artwork = draft.find(a => a.id === artworkId);
  artwork.seen = nextSeen;
});

Immer를 사용하면 위의 예제코드에서도 원본 state를 변이하는 것이 아니라 Immer가 제공하는 특별한 draft 객체를 변이하는 것이다.


* 참고 : React 공식문서(https://react-ko.dev/learn)