
배열은 JavaScript에서 변경 가능하지만 상태에 저장할 때는 변경할 수 없는 것으로 처리해야 한다. 객체와 마찬가지로 상태에 저장된 배열을 업데이트라면 새 배열을 생성(또는 기존 배열의 복사본 만들기)한 다음 새 배열을 사용하도록 상태를 설정해야 한다.

JavaScript에서 배열은 또 다른 종류의 객체일 뿐이다. 객체와 마찬가지로 리액트 상태의 배열도 읽기 전용으로 처리해야한다. 이는 arr[0] = 'bird' 와 같이 배열 내부의 항목을 재할당해서는 안되며, push() 와 pop() 같이 배열을 변경하는 메서드를 사용해서는 안된다는 의미이다.
대신, 배열을 업데이트할 때마다 상태 설정 함수에 새 배열을 전달해야한다. 그렇게 하려면 filter() 와 map() 과 같은 변경되지 않는 메서드를 호출하여 해당 상태의 원래 배열에서 새 배열을 만들 수 있다. 그런 다음 상태를 새 배열로 설정할 수 있다.
다음은 일반적인 배열 작업에 대한 참조 테이블이다. 리액트 상태 내에서 배열을 처리할 때 왼쪽열의 메서드를 피하고 대신 오른쪽 열의 메서드를 사용해야한다.
또는 두 열의 메서드를 모두 사용할 수 있는 Immer를 사용할 수 있다.
💡 Pitfall안타깝게도 slice 와 splice 는 이름이 비슷하지만 매우 다르다.
slice 는 배열이나 그 일부를 복사한다.splice 항목을 삽입하거나 삭제하기 위해 배열을 변형한다.리액트에서는 상태에 있는 객체나 배열을 변경하고 싶지 않기 때문에 slice (p가 아님)를 훨씬 더 자주 사용하게 될 것이다. 객체 업데이트에서는 mutation이 뭔지 상태에 권장되지 않는 이유를 설명한다.
push() 원하지 않는 배열을 변경한다.
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>
</>
);
}
대신 기존 항목과 끝에 새 항목을 포함하는 새 배열은 만든다. 이를 수행하는 방법은 여러 가지가 있지만 가장 쉬운 방법은 ... 스프레드 구문을 사용하는 것이다.
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
]
);
이제 잘 작동할 것 이다.
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={() => {
setArtists([
...artists,
{ id: nextId++, name: name }
]);
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
배열 스프레드 구문을 사용하면 항목을 원래 아티스트 앞에 배치하여 항목 앞에 추가할 수도 있다.
setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);
이러한 방식으로 스프레드는 배열의 끝에 추가하여 push() 작업을 수행하고 배열의 시작 부분에 추가하여 unshift() 작업을 모두 수행할 수 있다.
배열에서 항목을 제거하는 가장 쉬운 방법은 해당 항목을 필터링하는 것이다. 즉, 해당 항목을 포함하지 않는 새 배열을 생성하게 된다. 이렇게 하려면 filter 메서드를 사용하면 된다.
import { useState } from 'react';
let initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [artists, setArtists] = useState(
initialArtists
);
return (
<>
<h1>Inspiring sculptors:</h1>
<ul>
{artists.map(artist => (
<li key={artist.id}>
{artist.name}{' '}
<button onClick={() => {
setArtists(
artists.filter(a =>
a.id !== artist.id
)
);
}}>
Delete
</button>
</li>
))}
</ul>
</>
);
}
“삭제” 버튼을 몇 번 클릭하고 클릭 핸들러를 살펴봐라.
setArtists(
artists.filter(a => a.id !== artist.id)
);
여기서 artists.filter(a => a.id !== artist.id) 는 artist.id 와 ID가 다른 artists 로 구성된 배열을 생성한다.는 의미이다. 즉, 각 아티스트의 “삭제” 버튼을 누르면 해당 아티스트를 배열에서 필터링한 다음 결과 배열로 다시 렌더링하도록 요청한다. filter 는 원래 배열을 수정하지 않는다.
배열의 일부 또는 전체 항목을 변경하려면 map() 을 사용하여 새 배열을 만들 수 있다. map 에 전달할 함수는 데이터나 인덱스(도는 둘다)를 기반으로 각 항목을 어떻게 처리할지 결정할 수 있다.
이 예에서 배열은 두 개의 원과 하나의 정사각형의 좌표를 보유한다. 버튼을 누르면 원만 50px 아래로 이동한다. 이는 map() 을 사용하여 새로운 데이터 배열을 생성함으로써 수행된다.
import { useState } from 'react';
let initialShapes = [
{ id: 0, type: 'circle', x: 50, y: 100 },
{ id: 1, type: 'square', x: 150, y: 100 },
{ id: 2, type: 'circle', x: 250, y: 100 },
];
export default function ShapeEditor() {
const [shapes, setShapes] = useState(
initialShapes
);
function handleClick() {
const nextShapes = shapes.map(shape => {
if (shape.type === 'square') {
// No change
return shape;
} else {
// Return a new circle 50px below
return {
...shape,
y: shape.y + 50,
};
}
});
// Re-render with the new array
setShapes(nextShapes);
}
return (
<>
<button onClick={handleClick}>
Move circles down!
</button>
{shapes.map(shape => (
<div
key={shape.id}
style={{
background: 'purple',
position: 'absolute',
left: shape.x,
top: shape.y,
borderRadius:
shape.type === 'circle'
? '50%' : '',
width: 20,
height: 20,
}} />
))}
</>
);
}
배열에서 하나 이상의 항목을 바꾸는 것이 특히 일반적이다. arr[0] = 'bird' 와 같은 할당은 원래 배열을 ㅂ녀경하므로 대신 map 을 사용하는 것이 좋다.
항목을 교체하려면 map 을 사용하여 새 배열을 만든다. map 호출에서 두 번째 인수로 항목 인덱스를 받게 된다. 이를 사용하여 원래 항목(첫 번째 인수)을 반환할지 아니면 다른 항목을 반환할지 결정한다.
import { useState } from 'react';
let initialCounters = [
0, 0, 0
];
export default function CounterList() {
const [counters, setCounters] = useState(
initialCounters
);
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);
}
return (
<ul>
{counters.map((counter, i) => (
<li key={i}>
{counter}
<button onClick={() => {
handleIncrementClick(i);
}}>+1</button>
</li>
))}
</ul>
);
}
때로는 시작도 끝도 아닌 특정 위치에 항목을 삽입하고 싶을 수도 있다. 이렇게 하려면 ... 스프레드 구문을 slice() 메서드와 함께 사용할 수 있다. slice() 메서드를 사용하면 배열의 “슬라이스” 잘라 낼 수 있다. 항목을 삽입하려면 삽입 지점 앞에 슬라이스를 스프레드 한 다음 새 항목, 원래 배열의 나머지 부분을 스프레드한 배열을 만든다.
이 예에서 삽입 버튼은 항상 인덱스 1 에 삽입된다
import { useState } from 'react';
let nextId = 3;
const initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState(
initialArtists
);
function handleClick() {
const insertAt = 1; // Could be any index
const nextArtists = [
// Items before the insertion point:
...artists.slice(0, insertAt),
// New item:
{ id: nextId++, name: name },
// Items after the insertion point:
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={handleClick}>
Insert
</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
스프레드 구문과 map() 과 filter() 와 같은 비변환 메서드만으로는 수행할 수 없는 몇 가지 작업이 있다. 예를 들어, 배열을 뒤집거나 정렬할 수 있다. JavaScript reverse() 와 sort() 메서드는 원래 배열을 변경하므로 직접 사용할 수 없다.
하지만 먼저 배열을 복사한 다음 변경할 수 있다.
예 :
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies' },
{ id: 1, title: 'Lunar Landscape' },
{ id: 2, title: 'Terracotta Army' },
];
export default function List() {
const [list, setList] = useState(initialList);
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
return (
<>
<button onClick={handleClick}>
Reverse
</button>
<ul>
{list.map(artwork => (
<li key={artwork.id}>{artwork.title}</li>
))}
</ul>
</>
);
}
여기서는 [...list] 스프레드 구문을 사용하여 먼저 원본 배열의 복사본을 만든다. 이제 복사본이 있으므로 nextList.reverse() 또는 nextList.sort() 와 같은 변경 메서드를 사용하거나 nextList[0] = "something" 으로 개별 항목을 할당할 수도 있다.
하지만 배열을 복사하더라도 배열 내부의 기존 항목을 직접 변경할 수는 없다. 이는 얕은복사이기 때문이다. 새 배열에는 원본 배열과 동일한 항목이 포함된다. 따라서 복사된 배열 내부의 객체를 수정하면 기존 상태가 변경된다. 예를 들어, 이와 같은 코드는 문제다.
const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);
nextList 와 list 는 서로 다른 배열이지만 nextList[0] 와 list[0] 은 동일한 객체를 가리킨다. 따라서 nextList[0].seen 을 변경하면 list[0].seen 도 변경된다. 이것은 피해야할 mutation이다. 중첩된 JavaScript 객체를 업데이트하는 것과 비슷한 방법으로 이 문제를 해결할 수 있다. 즉, 변경하는 개별 항목을 변경하는 대신 복하는 것이다. 방법은 다음과 같다.
객체는 실제로 배열 “내부”에 위치하지 않는다. 코드에서는 “내부”에 있는 것처럼 보일 수 있지만 배열의 각 객체는 배열이 “가리키는” 별도의 값이다. 이것이 list[0] 과 같이 중첩된 필드를 변경할 때 주의해야 하는 이유이다. 다른 사람의 작품 목록이 배열의 동일한 요소를 가리킬 수 있다.
중첩된 상태를 업데이트할 때 업데이트하려는 지점부터 최상위 수준까지 복사본을 만들어야한다. 이것이 어떻게 작동하는지 보자.
이 예에서는 두 개의 개별 작품 목록이 동일한 초기 상태를 갖는다. 격리되어야 하지만 mutation으로 인해 상태가 실수로 공유되고 한 목록의 확인란을 선택하면 다른 목록에 영향을 준다.
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);
}
function handleToggleYourList(artworkId, nextSeen) {
const yourNextList = [...yourList];
const artwork = yourNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setYourList(yourNextList);
}
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>
);
}
문제가 되는 코드는 여기다.
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);
myNextList 배열 자체는 새로운 것이지만 항목 자체는 원래 myList 배열과 동일하다. 따라서 artwork.seen 을 변경하면 원본 아트웍 항목이 변경된다. 해당 삽화 항목도 yourList 에 있으므로 버그가 발생한다. 이와 같은 버그는 생각하기 어려울 수 있지만 다행이도 상태 변경을 피하면 사라진다.
map 을 사용하면 이전 항목을 변형 없이 업데이트된 버전으로 대체할 수 있다.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
... 는 객체의 복사본을 만드는 데 사용되는 스프레드 구문이다.
이 접근 방식을 사용하면 기존 상태 항목이 변경되지 않으며 버그가 수정된다.
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) {
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) {
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>
);
}
일반적으로 방금 만든 객체만 변경해야한다. 새로운 작품을 삽입하는 경우에는 이를 변경할 수 있지만 이미 상태에 있는 작품을 다루는 경우에는 복사본을 만들어야한다.
mutation없이 중첩 배열을 업데이트하면 약간 반복될 수 있다.
다음은 Immer로 재작성된 Art Bucket List 예시다.
import { useState } from 'react';
import { useImmer } from 'use-immer';
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, updateMyList] = useImmer(
initialList
);
const [yourList, updateYourList] = useImmer(
initialList
);
function handleToggleMyList(id, nextSeen) {
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});
}
function handleToggleYourList(artworkId, nextSeen) {
updateYourList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
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>
);
}
Note how with Immer, mutation like artwork.seen = nextSeen is now okay:
Immer을 사용하면 artwork.seen = nextSeen 과 같은 mutation이 괜찮다.
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
원래 상태를 변경하는 것이 아니라 Immer에서 제공하는 특수 draft 객체를 변경하기 때문이다. 마찬가지로 draft 내용에 push() 와 pop() 과 같은 변경 메서드를 적용할 수 있다.
뒤에서 Immer는 항상 draft 에 대한 변경 사항에 따라 처음부터 다음 상태를 구성한다. 이렇게 하면 상태를 변경하지 않고도 이벤트 핸들러를 매우 간결하게 유지할 수 있다.
[...arr, newItem] 배열 스프레드 구문을 사용하여 새 항목이 포함된 배열을 만들 수 있다.filter() 와 map() 을 사용하여 필터링되거나 변환된 항목이 포함된 새 배열을 만들 수 있다.