자바스크립트에서 배열은 변경이 가능하지만 상태 안에 배열을 저장할 때는 불변적인 요소처럼 여겨야해요. 객체와 같이 상태에 저장된 객체를 업데이트할 때는 새로운 배열을 만들거나 기존 배열의 복사본을 만들고 새로운 배열을 사용하여 상태를 설정해야해요.
이 페이지에서는
- 리액트 상태 안에서 배열에 항목을 어떻게 추가, 제거, 변경하는지
- 배열의 내부에서 객체를 어떻게 업데이트 하는지
- 어떻게 Immer를 사용하여 반복되어 나오는 구문을 줄인 채 객체를 복사하는지
를 알아볼 거예요.
Updating arrays without mutation | 변이 없이 배열 업데이트하기
자바 스크립트에서 배열은 객체의 다른 형태에요. 객체와 같이 리액트 상태 안에서의 배열은 읽기 전용으로 여겨야해요. 이는 즉, arr[0] = 'bird'
와 같이 배열 안에 새 항목을 재할당하면 안되고, 배열을 변경할 때는 push()
나 pop()
과 같은 메서드도 사용하면 안된다는 것을 의미해요.
대신에, 배열을 업데이트 할 때마다 상태의 셋팅함수에 새로운 배열을 전달할 거예요. 그러려면 filter()
나 map()
과 같은 변이가 없는 메서드를 호출하여 상태 안에 있는 기존 배열에서 새로운 배열을 만들어야해요. 그렇게 만들어진 최종 배열로 상태를 설정할 수 있어요.
아래의 참조표는 자주 사용되는 배열 연산자에요. 리액트 상태 안에서 배열을 다룰 때, 왼쪽 열에 있는 메서드는 피하고 오른쪽 열에 있는 메서드를 사용하세요.
사용 지양(배열 변이) | 사용 추천(새 배열 반환) | |
추가 | push , unshift |
concat , [...arr] 스프레드 구문 (예시) |
삭제 | pop , shift , splice |
filter , slice (예시) |
대체 | splice , arr[i] = ... 할당 |
map (예시) |
정렬 | reverse , sort |
먼저 배열을 복사하기 (예시) |
아니면, Immer를 사용해서 양 쪽 열 모두의 메서드를 사용할 수 있어요.
함정
slice나 splice는 비슷하지만 굉장히 달라요.
-slice
는 배열 또는 배열의 일부를 복사해요.
-splice
는 (항목을 추가 또는 삭제하여) 배열을 변이해요.
상태에서 객체나 배열을 변이하고 싶지 않을 것이기 때문에 리액트에서는slice
(p
가 없어요!) 를 훨씬 더 자주 사용할거예요. 객체 업데이트하기 문서에서는 변이가 무엇이고 왜 상태에 사용하면 안되는지를 설명하고 있어요.
Adding to an array | 배열 추가하기
push()
는 배열을 변이시키고 이는 아마 여러분이 원치 않을 거예요.
대신 기존 항목들의 뒤에 새로운 항목을 포함하고 있는 새로운 배열을 만드세요. 여러 가지 방법 중 가장 쉬운 방법은 ...
배열 스프레드 구문을 사용하는 거예요.
setArtists( // 상태를 대체하세요.
[ // 새로운 배열로 대체하세요.
...artists, // 모든 기존 항목들을 갖고 있고
{ id: nextId++, name: name } // 끝에는 새로운 항목을 가진 새로운 배열로요.
]
);
이제는 잘 작동해요.
배열 스프레드 문법은 항목을 기존의 ...artists
전에 위치시켜서 항목을 앞에 추가할 수 있도록 해줘요.
setArtists([
{ id: nextId++, name: name },
...artists // 기존의 항목들을 끝에 넣으세요.
]);
이 방법에서 스프레드 문법은 배열의 끝에 추가하여 push()
의 일을 수행할 수 있고, 배열의 맨 처음에 추가하여 unshift()
을 모두 수행할 수 있어요. 위의 샌드박스에서 해보세요!
Removing from an array | 배열 삭제하기
배열에서 항목을 삭제하는 가장 쉬운 방법은 해당 항목을 필터로 걸러내는 거예요. 즉, 해당 아이템을 갖고 있지 않은 새로운 배열을 만들 거예요. 이를 위해서는 filter
메서드를 사용해야해요. 아래 예시를 확인하세요.
"Delete" 버튼을 몇 번 클릭하고 버튼의 클릭 핸들러를 보세요.
setArtists(
artists.filter(a => a.id !== artist.id)
);
여기서 artists.filter(a => a.id !== artist.id)
는 "ID가 artist.id
와는 다른 artists
로 구성된 새로운 배열을 만드세요."라는 의미에요. 다른말로 하면, 각 예술가의 "Delete" 버튼은 해당 예술가를 배열에서 걸러낼 것이고 최종 배열로 리렌더링을 요청할 거예요. filter
가 기존 배열을 수정하지 않는다는 점에 주목하세요.
Transforming an array | 배열 변형하기
만약 배열의 일부 또는 모든 항목을 바꾸고 싶다면 map()
을 사용하여 새로운 배열을 생성할 수 있어요. map
에 전달한 함수는 데이터 또는 인덱스(혹은 둘 다)에 기반 하여 어떤 아이템으로 무엇을 할 것인지를 결정해요.
이 예시에서 배열은 두 원과 하나의 정사각형의 좌표를 갖고 있어요. 버튼을 누른다면, 원만 50 픽셀 아래로 이동해요. 이는 map()
을 사용하여 데이터의 새로운 배열을 만들어서 수행해요.
Replacing items in an array | 배열에서 항목 대체하기
하나 또는 그 이상의 항목들을 배열에서 다른 값으로 대체하고픈 경우는 특히나 흔한 경우에요. arr[0] = 'bird'
와 같은 할당은 기존 배열을 변이시키기 때문에 여기서도 map
을 사용할 수 있어요.
항목을 대체하려면 새로운 배열을 map
을 사용해서 만드세요. map
호출 안에서 두번에 인자로 항목의 인덱스를 받아요. 이를 사용하여 기존 항목(첫 번째 인자)를 반환해야하는지 혹은 다른 것들을 반환해야하는지를 결정하세요.
Inserting into an array | 배열에 추가하기
때때로 처음도 끝도 아닌 임의의 위치에 항목을 추가하고 싶을 수도 있어요. 이를 위해서는 ...
배열 스프레드 구문을 slice()
메서드와 함꼐 사용하세요. slice()
메서드는 배열을 조각으로 잘라야해요. 항목을 추가하려면 추가하고 싶은 지점 이전까지의 조각, 새로운 항목, 그리고 기존 배열의 남은 부분의 조각을 펼친 배열을 만드세요.
이 예시에서 Insert 버튼은 항상 1
번 인덱스에 추가해요.
Making other changes to an array | 배열에 다른 변화 주기
스프레드 구문이나 map()
이나 filter()
같이 변이를 일으키지 않는 메서드 만으로는 할 수 없는 일들도 있어요. 여러분이 배열을 뒤집거나 정렬하고 싶다고 생각해볼게요. 자바스크립트의 reverse()
나 sort()
메서드는 기존 배열을 변이하기 때문에 이들을 직접적으로 배열에 사용하면 안돼요.
그러나 배열을 먼저 복사한 다음에 이들을 변이시킬 수는 있어요.
예시:
이 예시에서 먼저, [...list]
스프레드 구문을 사용하여 기존 배열의 복사본을 생성했어요.이제 본사본이 있기 때문에 nextList.reverse()
나 nextList.sort()
와 같은 변이 메서드를 사용하거나 심지어는 nextList[0] = "something"
로 각 항목을 할당할 수도 있어요.
그러나 배열을 복사한다고 하더라도 그 안에서 직접적으로 기존 항목을 바꿀 수 없어요. 왜냐하면 복사는 얕은 복사여서 새로운 배열은 기존과 동일 항목들을 갖고 있기 때문이에요. 따라서 만약 복사항 배열 안에있는 객체를 수정한다면, 기존 상태를 변이하는 거예요. 예를 들어 아래와 같은 코드는 문제에요.
const nextList = [...list];
nextList[0].seen = true; // 문제점: list[0]을 변형했어요.
setList(nextList);
nextList
와 list
는 서로 다른 배열임에도 불구하고 nextList[0]
과 list[0]
은 같은 객체를 가리켜요. 따라서 nextList[0].seen
을 수정하면 list[0].seen
을 수정하는 것과 같아요. 이를 상태 변이라고 하는데, 피해야하는 거예요! 이 이슈를 자바스크립트 중첩 객체 업데이트하기와 동일한 방법으로 해결할 거예요. 바꾸고 싶은 각 항목을 변이 대신 복사하면 돼요. 이제 어떻게 하는지 알려줄게요.
Updating objects inside arrays | 배열 안에서 객체 업데이트 하기
객체는 실제로 배열 "안에" 있는 것이 아니에요. 코드 "안에서는" 그렇게 보이겠지만 배열에 있는 각각의 객체는 배열이 "가리키는" 별개의 값이에요. 이는 list[0]
과 같은 중첩된 필드를 바꿀 때 조심해야하는 이유에요. 또 다른 사람의 작품 목록은 배열의 동일한 요소를 가리켜요!
중첩된 상태를 업데이트할 때, 업데이트 하고 싶은 지점에서 최상위까지 복사본을 만들어야해요. 이 작업이 어떻게 동작하는지 살펴볼게요.
이 예시에서 두 개의 작품 목록은 같은 초기 상태를 갖고 있어요. 이들이 분리되어야하지만 변이로 인하여 이들의 상태는 의도치 않게 공유되고 한 배열의 체크 박스는 다른 배열에 영향을 줘요.
문제는 아래와 같은 코드 안에 있어요.
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // 문제점: 기존의 항목 업데이트
setMyList(myNextList);
myNextList
배열 자체는 새로운 배열이지만 항목들 자체는 기존 myList
배열과 동일해요. 따라서 artwork.seen
을 변경하는 것은 기존의 작품을 바꿔요. 해당 작품 항목은 yourList
에도 있고, 그래서 버그가 발생해요. 이와 같은 버그는 생각하기 어렵지만 다행스럽게도 이들은 상태 변이를 피하면 사라져요.
기존 항목을 변이 없이 업데이트된 버전으로 대체하려면 map
을 사용하세요.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 바뀐 값을 넣어서 새로운 객체 생성하기
return { ...artwork, seen: nextSeen };
} else {
// 변화 없음
return artwork;
}
}));
여기서 ...
은 객체의 복사본을 만드는데 사용된 객체 스프레드 문법이에요.
이 방법으로 기존 상태 항목 중 어느 것도 변이되지 않고 버그를 고칠 수 있어요.
일반적으로, 이제 막 생성한 객체만을 변형해야해요. 만약 새로운 작품을 추가한다면 해당 객체를 변형할 수 있지만, 만약 이미 상태에 있는 무언가를 다룬다면 복사본을 만들어야해요.
Write concise update logic with Immer | Immer로 간결하게 로직 업데이트하기
중첩된 배열을 변이 없이 업데이트하는 것은 다소 반복적이에요. 객체처럼요.
- 일반적으로 몇 단계 깊이 이상으로 상태를 업데이트 하지는 않아요. 만약 상태 객체가 너무 깊다면, 이들을 다르게 재구조화하여 평평하게 만들고 싶을 거예요.
- 반약 상태 구조를 바꾸고 싶지 않다면 Immer를 사용하는 것을 추천해요. Immer는 편리하지만 변형 구문을 사용하여 작성하고 복사본을 생성할 수 있도록 만들어줘요.
Art Bucket List라는 예시는 Immer로 작성되었어요.
Immer를 사용하면 artwork.seen = nextSeen
과 같은 변형도 괜찮다는 점에 주목하세요.
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
왜냐하면 기존 상태를 변이하지는 않았지만 Immer에서 제공되는 특별한 draft
객체를 변이했기 때문이에요. 비슷하게, push()
나 pop()
과 같은 변이 메서드를 draft
의 콘텐츠에 추가할 수도 있어요.
화면의 뒤쪽에서 Immer는 draft
로 이미 완료한 변화에 따라 항상 다음 상태를 스크래치로부터 만들어요. 이는 이벤트 핸들러가 아무런 상태 변이 없이 굉장히 간결하게 유지해줘요.
Recap | 요약
- 상태에 배열을 넣을 수 있지만 바꿀 수는 없어요.
- 배열을 변이하는 대신 새로운 배열을 생성하고 그 배열로 상태를 업데이트하세요.
[...arr, newItem]
배열 스프레드 구문을 사용하여 새로운 항목으로 배열을 생성하세요.- 걸러진 또는 변형된 항목들로 새로운 배열을 만들기 위해서는
filter()
나map()
을 사용하세요. - Immer를 사용하여 코드를 간결하게 유지할 수 있어요.
Challenges | 도전 과제
1. 장바구니의 항목 업데이트하기
handleIncreaseClick
로직을 채워서 "+" 를 두르면 해당하는 숫자가 증가하도록 만드세요.
map
함수를 사용하여 새로운 배열을 만들고 ...
객체 스프레드 구문을 사용하여 새로운 배열을 위하여 바뀐 객체의 복사본을 생성하세요.
2. 장바구니에서 항목 제거하기
이 장바구니는 "+" 버튼은 동작하지만 "-" 버튼이 아무 것도 하지 않아요. 이벤트 핸들러를 추가하여 "-" 버튼을 눌렀을 때 해당 상품의 count
개수가 증가하도록 만드세요. 만약 "-" 버튼을 눌렀을 때 카운트가 1이라면, 상품은 자동적으로 장바구니에서 삭제돼요. 0을 절대 보여주지 마세요.
map
을 사용해서 새로운 배열을 생성하고 filter
를 사용하여 count
가 0
인 상품을 제거하세요.
3. 변이 없는 메서드를 사용하여 변이 해결하기
이번 예시에서 App.js
안에 있는 모든 이벤트 핸들러는 변이를 사용해요. 결론적으로, 할 일의 수정과 삭제는 동작하지 않아요. 변이하지 않는 메서드를 사용하여 handleAddTodo
, handleChangeTodo
그리고 handleDeleteTodo
을 다시 작성해보세요.
handleAddTodo
에서 배열 스프레드 구문을 사용할 수 있어요. handleChangeTodo
에서 새로운 배열을 map
을 생성하세요. handleDeleteTodo
에서 filter
를 사용하여 새로운 배열을 만드세요. 이제 목록은 알맞게 작동해요.
4. Immer를 사용하여 변이 해결하기
이 예시는 이전 도전과제와 동일한 예시에요. 이번에는 Immer를 사용하여 변이를 해결해보세요. 편의를 위하여 useImmer
는 이미 불러왔기 때문에 이를 사용하여 상태 변수 todos
를 바꾸세요.
Immer를 사용하면 Immer가 여러분에게 제공하는 draft
의 일부만을 변경하는 한, 변이되는 방식으로 코드를 작성할 수 있어요. 이 답안은 모든 변이가 draft
에서만 발생하기 때문에 코드는 잘 작동해요.
Immer를 통해 변이하는 방식과 변이하지 않는 방식을 함꼐 사용할 수도 있어요.
예를 들어서 이번에는handleChangeTodo
와 handleDeleteTodo
는 변이되지 않는 map
과 filter
메서드를 사용하는 동안, handleAddTodo
는 Immer draft
는 변이되어서 구현되었어요.
Immer를 사용하면 각각의 경우마다 가장 자연스러운 스타일을 고를 수 있어요.
'리액트 공식문서 | React Docs > Learn > Learn React' 카테고리의 다른 글
[Managing State] Reacting to Input with State | 상태로 입력창에 반응하기 (0) | 2024.02.22 |
---|---|
[Managing State] Managing State Overview | 상태 관리하기 개요 (0) | 2024.02.22 |
[Adding Interactivity] Updating Objects in State | 상태에서 객체 업데이트하기 (0) | 2024.02.21 |
[Adding Interactivity] Queueing a Series of State Updates | 일련의 상태 업데이트를 대기열에 넣기 (0) | 2024.02.19 |
[Adding Interactivity] State as a Snapshot | 상태는 스냅샷이다 (1) | 2024.02.19 |