일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- tanstack query
- 파이썬 for in
- 파이썬 딕셔너리
- 리액트
- 리액트 훅
- 파이썬 slice
- Next 팀 프로젝트
- REACT
- 한글 공부 사이트
- 파이썬 enumerate
- 내일배움캠프
- 타입스크립트 리액트
- typeScript
- 타입스크립트 props
- 파이썬 replace
- React Hooks
- 내일배움캠프 프로젝트
- 리액트 프로젝트
- 내일배움캠프 최종 프로젝트
- 타입스크립트
- 파이썬 for
- 코딩테스트
- 리액트 공식문서
- JavaScript
- useState
- useEffect
- 자바스크립트
- 프로그래머스
- 파이썬 반복문
- 내배캠 프로젝트
- Today
- Total
sohyeon kim
[React] 리액트 공식문서 정리 2 : 상호작용성 더하기 - 렌더링, 스냅샷, 배칭, 배치 업데이트, 객체 불변성 유지, Immer 본문
[React] 리액트 공식문서 정리 2 : 상호작용성 더하기 - 렌더링, 스냅샷, 배칭, 배치 업데이트, 객체 불변성 유지, Immer
aotoyae 2024. 10. 18. 15:27
💡 리액트 공식문서 정리 : 상호작용성 더하기
1. 렌더링 : 컴포넌트를 화면에 표시하기 위한 준비
렌더링 과정
- 렌더링을 트리거(요청, 발동)
- 컴포넌트를 호출해 화면에 표시할 내용을 파악, “렌더링”은 React에서 컴포넌트를 호출하는 것
- DOM 이 최신 센더링 출력과 일치하도록 수정
컴포넌트 렌더링이 일어나는 데에는 두 가지 이유가 있다.
- 컴포넌트의 초기 렌더링인 경우
: 루트 컴포넌트 호출 후, appendChild() DOM API 를 사용해 생성한 모든 DOM 노드를 화면에 표시 - 컴포넌트의 state가 업데이트된 경우
: state 업데이트가 일어나 렌더링을 트리거한 컴포넌트 호출 후, DOM이 최신 렌더링과 일치하도록 변경된 부분만 수정
2. 스냅샷으로서의 state : 리렌더링을 요청하면, 렌더링이 일어난 시점의 상태를 기억한다.
컴포넌트가 호출되면, React 는 마치 선반에 있는 듯한 state 를 렌더링에 맞게 변경하고 그 스냅샷을 제공한다.
이후 새로운 UI 의 스냅샷을 JSX 에 반환하고 화면이 업데이트 된다.
아래 예시를 보면, setNumber 가 세 번 호출되므로 숫자가 0에서 3으로 증가할 것으로 예상할 수 있다.
하지만 카운트는 한 번만 증가한다. 버튼의 클릭 핸들러가 React 에 지시한 작업에 대해 알아보자.
- setNumber(number + 1): number는 0이므로 setNumber(0 + 1)입니다.
- React는 다음 렌더링에서 number를 1로 변경할 준비를 합니다.
- setNumber(number + 1): number는 0이므로 setNumber(0 + 1)입니다.
- React는 다음 렌더링에서 number를 1로 변경할 준비를 합니다.
- setNumber(number + 1): number는 0이므로 setNumber(0 + 1)입니다.
- React는 다음 렌더링에서 number를 1로 변경할 준비를 합니다.
❗️ state를 설정하면 다음 렌더링에 대해서만 변경된다. 이 렌더링에서 number 는 항상 0 이므로 state 를 1 로 세 번 설정한 것.
시간 경과에 따른 state 변경은 어떨까?
아래와 같은 상황에서 alert 에 타이머를 설정한다면?
똑같이 0이 표시된다.
- state 변수의 값은 이벤트 핸들러의 코드가 비동기적이더라도 렌더링 내에서 절대 변경되지 않는다.
- 해당 렌더링의 onClick 내에서, setNumber(number + 5)가 호출된 후에도 number의 값은 계속 0이다.
- 이 값은 컴포넌트를 호출해 React가 UI의 “스냅샷을 찍을” 때 “고정”된 값.
- React는 렌더링의 이벤트 핸들러 내에서 state 값을 “고정”으로 유지한다.
- 변수와 이벤트 핸들러는 다시 렌더링해도 “살아남지” 않는다. 모든 렌더링에는 고유한 이벤트 핸들러가 있다.
3. state 업데이트 큐 & batching : React 는 다음 렌더링이 일어나기 전, 값에 대한 여러 작업을 수행한다.
(위의 setNumber 를 세 번 호출했던 예시를 보자. number 의 값이 항상 0인 상태.)
➡️ React는 state 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다린다. (실행을 마친 후 state 업데이트)
이 때문에 리렌더링은 모든 setNumber() 호출이 완료된 이후에만 일어난다.
주문을 모두 받아(주문을 변경하고, 다른 사람의 주문도 받아) 주방으로 가는 웨이터와 같다.
이렇게 하면 너무 많은 리렌더링이 발생하지 않고도, 여러 컴포넌트에서 나온 다수의 state 변수를 업데이트할 수 있다.
(이는 이벤트 핸들러의 코드가 완료될 때까지 UI 가 업데이트되지 않는다는 의미.)
Batching 이라고 부르는 이 동작은 앱이 훨씬 빠르게 실행되도록 한다.
❓ 그럼 다음 렌더링 전에 동일한 state 변수를 여러 번 업데이트 하려면? (위의 conter 예시와 같은 내용)
- setNumber(number + 1) 와 같은 다음 state 값을 전달하는 대신,
setNumber(n => n + 1) 와 같이 이전 큐의 state를 기반으로 다음 state를 계산하는 함수를 전달할 수 있다.
❗️ 이는 단순히 state 값을 대체하는 게 아니라, React 에게 "state 값으로 무언가를 해!" 라고 지시하는 것.
이벤트 핸들러 내 코드 작동 방식을 살펴보자.
- setNumber(n => n + 1): n => n + 1 함수를 큐에 추가. 1 반환.
- setNumber(n => n + 1): n => n + 1 함수를 큐에 추가. 2 반환.
- setNumber(n => n + 1): n => n + 1 함수를 큐에 추가. 3 반환
여기서 n => n + 1 은 업데이터 함수.
** 업데이터 함수는 순수해야 하며 결과만 반환해야 한다. 다른 사이드 이펙트를 실행하려고 하지 말 것.
** Strict 모드에서 React 는 각 업데이터 함수를 두 번 실행(두 번째 결과는 버림)하여 실수를 찾을 수 있도록 도와준다.
❓ state 를 교체한 후 업데이트하면 어떻게 될까?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
답은 6씩 증가된다.
이 이벤트 핸들러가 React 에 지시하는 작업은 다음과 같다.
- setNumber(number + 5) : number는 0 이므로 setNumber(0 + 5)입니다. React 는 큐에 “5로 바꾸기” 를 추가한다.
- setNumber(n => n + 1) : n => n + 1 는 업데이터 함수이다. React 는 해당 함수를 큐에 추가한다.
queued update | n | returns |
”replace with 5” | 0 (unused) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
업데이터 함수의 인수 명명 규칙
- 해당 state 변수의 첫 글자로 지정하는 것이 일반적이다.
- 좀 더 자세한 코드를 선호한다면, setEnabled(enabled => !enabled) 와 같이 전체 state 변수 이름을 반복하거나,
setEnabled(prevEnabled => !prevEnabled) 와 같은 접두사를 사용한다.
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
4. 객체 State 업데이트 : 새로운 객체를 생성하거나 기존 객체의 복사본을 만들어 사용한다.
state 는 객체를 포함한 모든 종류의 자바스크립트 값을 가질 수 있다.
하지만, state 가 가진 객체를 직접 변경해선 안된다.
객체를 업데이트하고 싶을 땐, 새로운 객체를 생성해(또는 기존 객체의 복사본을 만들어) state 가 이를 사용하도록 한다.
'변경' 이란? '교체' 란?
const [x, setX] = useState(0);
setX(5);
숫자, 문자열, 불리언과 같은 값들은 변경할 수 없는, '읽기 전용' 을 의미하는 '불변성'을 가진다.
이 값을 교체하기 위해선 리렌더링이 필요.
위 예시에서 x state 는 0 에서 5로 바뀌었지만 숫자 0 자체는 바뀌지 않았다. 이런 원시 값들은 변경할 수 없다.
const [position, setPosition] = useState({ x: 0, y: 0 });
position.x = 5;
// 객체를 변경하는 것은 렌더링을 발생시키지 않으며, 이전 렌더 “스냅샷”의 state를 바꾼다.
기술적으로 객체 자체의 내용은 바꿀 수 있다. 이것을 변경(mutation) 이라고 한다.
하지만 state 의 객체들이 변경 가능할지라도, 원시값과 같이 불변성을 가진 것처럼 다루어야 한다.
객체를 변경하는 대신 '교체' 해야한다.
State 를 읽기 전용인 것처럼(불변한 것으로) 다루자!
다시 말하면, state에 저장한 자바스크립트 객체는 어떤 것이라도 읽기 전용인 것처럼 다루어야 한다.
// 리렌더링을 발생시키기 위해 새 객체를 생성하여 state 설정 함수로 전달
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
위 코드는 아래와 같이 작성할 수 있다.
변경은 이미 state 에 존재하는 객체를 변경할 때만 문제가 된다.
따라서 방금 만든 객체를 수정하는 것은 아직 다른 코드가 해당 객체를 참조하지 않기 때문에 괜찮다.
이것을 지역 변경 local mutation 이라고 하며, 렌더링하는 동안 지역 변경을 할 수 도 있다.
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);
전개 구문으로 객체 복사하기
위 예시처럼 x, y 를 같이 갱신하는 것이 아니라, 새 객체에 기존 데이터도 포함하고 싶다면 전개 구문을 이용해 객체를 복사한다.
setPosition({
...x, // 이전 필드 복사
y: e.clientY // 새로운 부분은 덮어쓰기
});
❗️ 전개 구문은 얕은 복사를 한다. 중첩된 프로퍼티를 업데이트할 시 여러 번 사용해야 한다.
// 중첩 객체
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
// person.artwork.city 만 업데이트
setPerson({
...person, // 다른 필드 복사
artwork: { // artwork 교체
...person.artwork, // 동일한 값 사용
city: 'New Delhi' // 하지만 New Delhi!
}
});
person 안에 artwork 가 들어있는 것처럼 보이지만, 그저 주소를 가리키는 것으로 생각하자.
person 의 artwork.city 를 변경하면 다른 곳의 city 에도 영향이 미친다.
Immer 로 간결한 업데이트 로직 작성하기
Immer란?
- 중첩 state 의 평탄화를 도와주는 라이브러리
- state 구조를 바꾸고싶지 않을 때, 간편하게 중첩 전개할 수 있도록 해준다.
- 쉽고 편리하게 변경 구문을 사용할 수 있게 해주며 복사본 생성을 도와준다.
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
// 바로 위에서 얘기한 법칙을 깨고, 객체를 변경하는 것처럼 보이지만
// 일반적인 변경과는 다르게 이것은 이전 state 를 덮어쓰지 않는다.
왜 React 는 state 변경을 권장하지 않는가?
- 디버깅 : state 를 변경하지 않는다면, 렌더링 사이에 state 가 어떻게 바뀌었는지 명확하게 알 수 있다.
- 최적화 : 보편적인 React 최적화 전략은 이전 props 또는 state 가 다음 것과 동일 할 때 일을 건너뛰는 것에 의존한다.
state 를 변경하지 않는다면, 변경사항이 있는지 확인하는 작업이 매우 빨라지고,
prevObj === obj 를 통해 내부적으로 아무것도 바뀌지 않았음을 확인할 수 있다. - 요구사항 변화 : 취소/복원, 변화 내역 조회, 이전 값으로 폼 재설정하기 등의 기능은 아무것도 변경되지 않았을 때 더 쉽다.
메모리에 state 의 이전 복사본을 저장하며 적절한 상황에 다시 사용할 수 있기 때문.
변경을 하게 되면 나중에 이러한 기능들을 추가하기 어려울 수있다. - 새로운 기능 : React 는 이 규칙을 바탕으로 새 기능들을 개발한다. state 의 과거 버전을 변경한다면 새 기능을 사용하지 못할 수도!
5. 배열 State 업데이트 : 새로운 배열을 생성하거나 기존 배열의 복사본을 만들어 사용한다.
배열은 다른 종류의 객체로, 자바스크립트에서는 변경이 가능하지만 state 로 저장할 땐 변경할 수 없도록(읽기 전용으로) 처리해야 한다.
객체와 마찬가지로 새로운 배열을 생성해(또는 기존 배열의 복사본을 만들어) state 가 이를 사용하도록 한다.
배열 업데이트 시 참고
- arr[0] = 'bird' 와 같이 내부의 항목을 재할당해서는 안되며, push() 나 pop() 같은 함수로 배열을 변경해선 안된다.
- 대신 새 배열을 set 함수에 전달. filter() 나 map() 같은 함수를 통해 원본 배열로부터 새 배열을 생성하고, state 에 설정.
비선호 (배열을 변경) 🙅♂️ | 선호 (새 배열을 반환) 🙆♂️ | |
추가 | push, unshift | concat, [...arr] 전개 연산자 |
제거 | pop, shift, splice | filter, slice |
교체 | splice, arr[i] = ... 할당 | map |
정렬 | reverse, sort | 배열을 복사한 이후 처리 |
❗️ slice, splice 사용 주의 : slice 사용 권장
- slice 를 사용하면 배열 또는 그 일부를 복사할 수 있다.
- splice 는 배열을 변경한다. (항목을 추가하거나 제거한다.)
// 배열에 항목 추가 (순서를 조절할 수 있다. 배열 시작 부분이나 끝, 중간)
setArtists( // 아래의 새로운 배열로 state를 변경.
[
...artists, // 기존 배열의 모든 항목에,
{ id: nextId++, name: name } // 마지막에 새 항목을 추가.
]
);
// 배열에서 항목 제거
setArtists(
artists.filter(a => a.id !== artist.id)
);
// 배열 변환
const nextCounters = counters.map((c, i) => {
if (i === index) {
return c + 1; // 클릭된 counter를 증가시킵니다.
} else {
return c; // 변경되지 않은 나머지를 반환합니다.
}
});
setCounters(nextCounters);
// 배열 뒤집기
const nextList = [...list];
nextList.reverse();
setList(nextList);
// 배열 내부의 객체 업데이트
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
return { ...artwork, seen: nextSeen }; // 변경된 *새* 객체를 만들어 반환.
} else {
return artwork; // 변경시키지 않고 반환.
}
}));
Immer 로 간결한 업데이트 로직 작성하기
Immer 를 이용하면 map 을 사용하지 않고도 간단하고 안전하게 state 를 변경할 수 있다.
- 이는 원본 state 를 변경하는 것이 아니라, Immer 에서 제공하는 특수 draft 객체를 변경하기 때문.
- push() 나 pop() 같은 변경 함수들도 draft의 컨텐츠에 적용할 수 있다.
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
🔗 https://ko.react.dev/learn/adding-interactivity
'React' 카테고리의 다른 글
[React] 리액트 공식문서 정리 4(1) : 탈출구 - useRef, DOM 직접 조작, 리렌더링 방지 (0) | 2024.11.25 |
---|---|
[React] 리액트 공식문서 정리 3(1) : State 관리하기 - 상태를 쉽게 업데이트 하도록 구조화 (0) | 2024.11.23 |
[React] 리액트 공식문서 정리 1 : UI 표현하기 - 불변성, JSX, key, 컴포넌트, 순수 함수, 트리 (14) | 2024.10.16 |
[React] Context 외부에서 데이터를 사용하려 할 때의 에러 처리, useContext (0) | 2024.10.11 |
[React] useState & useRef & react-hook-form : 제어/비제어 컴포넌트, useRef 단점, 언제 무엇을 써야 하는가 (0) | 2024.08.13 |