안녕하세요 웹 YB 김다현입니다.
이번 4주차에서는 state 업데이트 큐, 객체 state 업데이트 하기, 배열 state 업데이트하기 챕터를 공부해봤는데요!
그 중 객체 state 업데이트 하기, 배열 state 업데이트하기에 대해 다뤄보려 합니다.
사실,,, 이번주차에는 할 게 너무 많은 바람에 왜? 라는 질문을 많이 못 던지고 개념 정리만 하게 된 느낌이라 아쉽네요,,,ㅜㅜ
할 게 끝나면 바로 딥다이브~ 해볼게요 ㅎ,ㅎ
state와 불변성
state란?
React에서 useState를 사용하면 숫자, 문자열, 불리언, 객체 등 모든 자바스크립트 값을 state로 저장할 수 있다.
const [x, setX] = useState(0); // 숫자 state
const [position, setPosition] = useState({ x: 0, y: 0 }); // 객체 state
불변성이란?
불변성이란 데이터를 "수정하지 않고 새로운 데이터로 교체하는 것"을 말한다.
숫자, 문자열, 불리언 같은 원시값은 변경이 불가능하니까 이 원칙을 자연스럽게 따르고 있는데, 객체는 수정이 가능하니 React에서는 객체도 불변성을 지켜야 한다.
왜 불변성을 지켜야 할까?
React는 state를 읽기 전용으로 다룬다. state를 직접 수정하면 React는 변경 사항을 감지하지 못해 렌더링이 발생하지 않는다.
✖️ 잘못된 코드
position.x = 5; // 직접 변경
position.y = 10; // 직접 변경
위 코드에서 React는 state가 변경되었다는 사실을 모르기 때문에 컴포넌트가 업데이트 되지 않는다.
setPosition({ x: 5, y: 10 }); // 새로운 객체로 교체
이렇게 해야 React가 state가 변경되었다는 것을 감지하고 리렌더링을 수행한다.
객체 state 업데이트 하기
객체를 교체해야 하는 이유
React는 state를 관리할 때 기존 state와 새로운 state를 비교해서 변경 여부를 판단한다.
객체의 내용만 수정하면 React는 객체가 변경되었다고 판단하지 못해 업데이트가 이루어지지 않는다.
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div
onPointerMove={(e) => {
position.x = e.clientX; // 직접 수정
position.y = e.clientY; // 직접 수정
}}
style={{ width: '100vw', height: '100vh' }}
>
<div
style={{
transform: `translate(${position.x}px, ${position.y}px)`,
position: 'absolute',
width: 20,
height: 20,
backgroundColor: 'red',
}}
/>
</div>
);
}
이 코드는 position 객체를 직접 변경하므로 React는 렌더링을 발생시키지 않는다. 따라서 화면에서 점이 움직이지 않음!
onPointerMove={(e) => {
setPosition({ x: e.clientX, y: e.clientY }); // 새로운 객체로 교체
}}
이 코드는 React에게 '이제 새로운 객체로 state를 교체해줘' 라고 요청하는 것과 같다.
React는 변경을 감지하고 리렌더링을 수행한다.
객체를 안전하게 업데이트 하는 방법
1) 전개 연산자로 객체 복사하기
객체의 일부를 업데이트하고 나머지는 유지하고 싶다면, 전개 연산자 (...) 를 사용하면 된다.
// 전체 객체 복사 후 일부만 수정
setPosition({
...position, // 기존 position 객체를 복사
x: e.clientX // x만 새 값으로 덮어쓰기
});
2) 중복된 객체 업데이트
중첩된 객체를 업데이트할 땐 안쪽부터 복사하면서 바깥까지 복사해야 한다.
const [person, setPerson] = useState({
name: 'Niki',
artwork: { title: 'Blue Nana', city: 'Hamburg' }
});
setPerson({
...person, // person 객체 복사
artwork: { // artwork 객체 복사
...person.artwork, // 기존 artwork 복사
city: 'New Delhi' // city만 수정
}
});
이런 방식은 불변성을 유지하면서 안전하게 state를 업데이트할 수 있다.
Immer로 간결하게 작성하기
Immer란?
Immer는 객체를 쉽게 복사하고 변경할 수 있게 도와주는 라이브러리다.
Immer를 사용하면 직접 객체를 변경하는 것처럼 보이지만 내부적으로는 복사본을 생성한다.
import { useImmer } from 'use-immer';
const [person, updatePerson] = useImmer({
name: 'Niki',
artwork: { title: 'Blue Nana', city: 'Hamburg' }
});
updatePerson((draft) => {
draft.artwork.city = 'New Delhi'; // 변경처럼 보이지만 복사본에 적용
});
이 방식은 코드의 가독성을 높이고 복잡한 객체를 다룰 때 유용하게 쓸 수 있다.
React에서 배열 State를 관리하는 원칙
배열은 변경 가능하다?
자바스크립트에서 배열은 객체의 한 종류이기 때문에 내부 항목을 수정하거나 추가하는 등의 변경 작업이 가능하다.
하지만 React의 state로 사용되는 배열은 읽기 전용처럼 다뤄야 한다.
왜 배열은 직접 수정하면 안 될까?
React는 state가 변경되었는지 판단하기 위해 얕은 비교를 사용한다.
배열을 직접 수정하면 배열의 참조가 변하지 않으므로 React가 state가 바뀌었다고 인식하지 못해 리렌더링이 발생하지 않는다.
얕은 비교가 뭘까?
얕은 비교란 객체나 배열의 참조값(메모리 주소)를 비교하는 방법이다.
React에서 state 변경 여부를 판단하거나 React.memo 같은 최적화 기능에서 사용된다.
깊은 비교는 객체의 모든 속성을 하나하나 확인하므로 배열이나 객체가 클 경우 성능에 영향을 줄 수 있다.
따라서 불변성을 유지하며 얕은 비교로도 변경 사항을 감지할 수 있도록 작성하는 것이 중요하다.
✖️ 잘못된 코드
arr[0] = 'bird'; // 배열 항목 재할당
arr.push('cat'); // 배열에 항목 추가
이렇게 하면 배열의 내부 값이 변경되었지만 React는 이를 감지하지 못해 UI가 업데이트되지 않는다.
올바른 배열 업데이트 방법
React에서는 배열을 직접 수정하지 않고 새로운 배열을 만들어 state로 교체해야 한다.
setArr([...arr, 'bird']); // 새 배열 생성 후 교체
- ... 전개 연산자를 사용해 기존 배열을 복사하고 새 항목을 추가해 새로운 배열을 생성한다.
- React는 새로운 배열을 감지하고 리렌더링을 트리거 한다.
배열을 업데이트할 때 사용하는 함수들
비선호 (배열을 변경) | 선호 (새 배열을 반환) | 설명 |
push, unshift | concat, [...arr] | 새로운 배열을 생성하여 항목 추가 |
pop, shift, splice | filter, slice | 특정 조건의 항목만 포함한 새 배열 생성 |
splice, arr[i] = | map | 기존 배열의 일부 항목만 변경한 새 배열 생성 |
reverse, sort | 배열 복사 후 reverse/sort | 배열을 변경하지 않고 정렬 및 뒤집기 |
slice와 splice의 차이점?
slice는 배열을 복사하는 것.
원본 배열은 변경되지 않고 새로운 배열을 반환한다. React에서 사용을 권장한다.
const newArr = arr.slice(0, 2); // arr의 첫 두 항목 복사
splice는 배열을 수정하는 것.
원본 배열을 직접 변경하며 React에서 권장하지 않는다.
arr.splice(1, 1); // arr의 1번째 항목 제거 (원본 배열 변경)
배열 업데이트 시 권장되는 함수들
항목 추가 -> concat 또는 전개 연산자
setArr([...arr, 'newItem']); // 배열 뒤에 항목 추가
setArr(['newItem', ...arr]); // 배열 앞에 항목 추가
항목 제거 -> filter
setArr(arr.filter(item => item !== 'unwantedItem'));
항목 변경 -> map
setArr(arr.map(item => item === 'oldItem' ? 'newItem' : item));
정렬 또는 뒤집기 -> 배열 복사 후 sort / reverse
setArr([...arr].reverse()); // 복사 후 뒤집기
객체는 배열 내부에 있지 않다?
배열 안의 객체는 배열이 가리키는 독립적인 값이다.
배열은 객체의 참조를 저장하는 주소 목록 같은 역할을 한다.
const list = [
{ id: 1, title: 'Artwork A' },
{ id: 2, title: 'Artwork B' },
];
const listCopy = [...list]; // 배열 복사
listCopy[0].title = 'Updated Artwork A'; // 객체 내부 수정
console.log(list[0].title); // 'Updated Artwork A' (원본도 변경됨)
listCopy는 새로운 배열이지만, 배열 안의 객체는 원본 배열의 객체와 같은 참조값을 가진다.
따라서 객체 내부를 수정하면 원본 배열의 객체도 함께 바뀐다.
동일한 객체를 참조할 때 발생하는 문제
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
];
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(initialList);
myList와 yourList는 같은 객체를 참조하는 배열이다.
한쪽에서 객체의 값을 수정하면 다른 배열에서도 변경된 값이 나타난다.
const myNextList = [...myList]; // 배열 복사
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // 객체 내부 변경 (원본에도 영향)
setMyList(myNextList);
배열은 복사했지만 객체 자체는 복사하지 않는다. 따라서 객체를 변경하면 원본도 영향을 받아 버그가 발생한다.
그럼 어떻게 해결할까?
map을 사용해 새 객체를 생성하기
setMyList(
myList.map(artwork =>
artwork.id === artworkId
? { ...artwork, seen: nextSeen } // 변경된 새 객체 반환
: artwork // 변경 없는 객체는 그대로 반환
)
);
map은 배열의 각 항목을 순회하며 변경이 필요한 항목만 새 객체로 대체한다.
...artwork는 기존 객체를 복사하고 seen 필드만 덮어쓴다.
왜 map을 사용해야 할까?
map은 배열의 모든 항목을 새 객체로 만들기 때문에, 원본 객체를 수정하지 않는다.
원본 배열과 객체의 불변성을 유지하면서 React가 변경 사항을 감지할 수 있다.
Immer로 간결하게 작성하기
Immer를 사용하면 복사와 객체 생성 과정을 직접 작성하지 않아도 된다.
draft라는 특별한 객체를 사용해서 마치 원본을 수정하듯이 작성할 수 있다.
import { useImmer } from 'use-immer';
const [myList, updateMyList] = useImmer(initialList);
updateMyList(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen; // 마치 원본을 수정하듯 작성
});
내부에선 어떤 일이 벌어질까?
1️⃣ Immer는 draft 객체를 만들어 원본처럼 보이게 제공
2️⃣ draft에 대한 변경 사항을 기록한 뒤, 새로운 배열과 객체를 생성
3️⃣ 최종적으로 React는 새로운 참조값을 사용해 변경을 감지
Immer를 왜 쓸까?
복잡한 객체와 배열 업데이트 로직을 간단히 작성 가능해 코드가 간결해진다.
Immer가 내부적으로 복사와 객체 생성을 처리해 불변성을 유지한다.
코드 가독성이 향상된다.
map 썼을 때
setMyList(
myList.map(artwork =>
artwork.id === artworkId
? { ...artwork, seen: nextSeen }
: artwork
)
);
Immer 썼을 때
updateMyList(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
궁금한 점
뭔가 다들 프로젝트를 하거나 아님 과제 등을 하면서 state 업데이트를 잘못 관리해 발생한 문제는 없었는지,,, 복잡한 배열과 객체 state를 관리해본 경험이 있는지? 각자의 경험이 궁금해요!
'나야, 리액트 스터디' 카테고리의 다른 글
[week4] - 상호작용성 더하기 - State 업데이트 큐, 객체 state 업데이트하기, 배열 state 업데이트하기 (3) | 2024.11.17 |
---|---|
[week 4] 객체 state 업데이트하기, 배열 state 업데이트하기 (3) | 2024.11.17 |
[week4] 상호작용성 더하기 - State 업데이트 큐, 객체 state 업데이트하기, 배열 state 업데이트하기 (4) | 2024.11.17 |
[week3]- 이벤트처리 (4) | 2024.11.11 |
[week3] - 이벤트에 응답하기, State: 컴포넌트의 기억 저장소, 랜더링 그리고 커밋, 스냅샷으로서의 state (2) | 2024.11.10 |