일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 타입스크립트
- 코딩테스트
- 파이썬 for in
- 파이썬 replace
- 파이썬 딕셔너리
- typeScript
- 타입스크립트 리액트
- 파이썬 enumerate
- useState
- 자바스크립트
- 내배캠 프로젝트
- REACT
- useEffect
- 내일배움캠프 최종 프로젝트
- tanstack query
- JavaScript
- 리액트 훅
- 프로그래머스
- React Hooks
- 리액트 프로젝트
- 리액트 공식문서
- 파이썬 for
- 내일배움캠프
- 리액트
- 타입스크립트 props
- 파이썬 반복문
- 내일배움캠프 프로젝트
- Next 팀 프로젝트
- 한글 공부 사이트
- 파이썬 slice
- Today
- Total
sohyeon kim
[React] 리액트 공식문서 정리 3(1) : State 관리하기 - 상태를 쉽게 업데이트 하도록 구조화 본문
💡 리액트 공식문서 정리 : State 관리하기 - State 를 사용해 Input 다루기, State 구조 선택하기
애플리케이션이 커짐에 따라 state 가 어떻게 구성되는지, 데이터가 컴포넌트 간에 어떻게 흐르는지에 대해 파악해두면 도움이 된다.
불필요하거나 중복된 state 는 버그의 흔한 원인.
state 를 잘 구성하는 방법, state 업데이트 로직을 유지 보수 가능하게 관리하는 방법,
멀리 있는 컴포넌트 간의 state 공유 방법에 대해 알아보자.
1. State 를 사용해 input 다루기 : 선언형 UI, 선언형 프로그래밍
UI를 세밀하게 직접 조작하는 것(명령형)이 아니라 각각의 시각적 state로 UI를 묘사하는 것을 의미.
React 에서는 (코드로) UI 를 직접 조작할 필요가 없다. 대신에 무엇을 보여주고 싶은지 선언하기만 하면 된다.
그럼 React 는 어떻게 UI 를 업데이트 해야 할지 이해할 것.
React 에서 UI 를 구현하는 과정
- 컴포넌트의 다양한 시각적 state를 확인하세요.
- 무엇이 state 변화를 트리거하는지 알아내세요.
- useState를 사용해서 메모리의 state를 표현하세요.
- 불필요한 state 변수를 제거하세요.
- state 설정을 위해 이벤트 핸들러를 연결하세요.
1️⃣ 컴포넌트의 다양한 시각적 state 확인하기
먼저 사용자가 볼 수 있는 UI 의 모든 state 를 시각화
- Empty : 폼은 비활성화된 “제출” 버튼을 가지고 있다.
- Typing : 폼은 활성화된 “제출” 버튼을 가지고 있다.
- Submitting : 폼은 완전히 비활성화되고 스피너가 보인다.
- Success : 폼 대신에 “감사합니다” 메시지가 보인다.
- Error : “Typing” state와 동일하지만 오류 메시지가 보인다.
export default function App() {
return (
<>
{statuses.map(status => (
<section key={status}>
<h4>Form ({status}):</h4>
<Form status={status} />
</section>
))}
</>
);
}
2️⃣ 무엇이 state 변화를 트리거하는지 알아내기
두 종류의 인풋 유형으로 state 변경을 트리거할 수 있다.
- 버튼을 누르거나, 필드를 입력하거나, 링크를 이동하는 것 등의 휴먼 인풋(종종 이벤트 핸들러가 필요)
- 네트워크 응답이 오거나, 타임아웃이 되거나, 이미지를 로딩하거나 하는 등의 컴퓨터 인풋
두 가지 경우 모두 UI 를 업데이트하기 위해 state 변수를 설정해야 한다.
- 텍스트 인풋을 변경하면 (휴먼) 텍스트 상자가 비어있는지 여부에 따라 state를 Empty에서 Typing 으로 또는 그 반대로 변경 필요
- 제출 버튼을 클릭하면 (휴먼) Submitting state 변경 필요
- 네트워크 응답이 성공적으로 오면 (컴퓨터) Success state 변경 필요
- 네트워크 요청이 실패하면 (컴퓨터) 해당하는 오류 메시지와 함께 Error state 변경 필요
3️⃣ 메모리의 state 를 useState 로 표현하기
이 과정은 단순함이 핵심. 각각의 state smms '움직이는 조각'이다. 조각은 적을수록 좋다. 복잡한 건 버그를 일으키기 마련!
먼저 반드시 필요한 state 로 시작해보자. 예를 들면 인풋의 answer. 그리고 (존재한다면) 가장 최근에 발생한 error.
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
이제 위에서 나열했던 나머지 시각적 state 도 살펴보자.
어떤 state 를 사용할지에 대한 방법은 여러가지이므로 모든 시각적 state 를 커버할 수 있는 확실한 것을 먼저 추가하자.
최고의 방법은 아닐 수도 있지만, state 를 리팩터링하는 것도 과정 중 하나이니 괜찮다.
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
4️⃣ 불필요한 state 변수 제거하기
state 구조를 리팩토링하는 데 시간을 투자하면 컴포넌트는 더 이해하기 쉬워지고 불필요한 중복은 줄어들 것이다.
리팩토링의 목표는 state 가 사용자에게 유효한 UI 를 보여주지 않는 경우를 방지하는 것.
예시로, 오류 메세지가 나타났는데 인풋이 비활성화되어 있어 유저가 오류를 수정할 수 없는 상황은 원하지 않을 것이다!
위 state 변수에 관한 질문
- state가 역설을 일으키지는 않나요?
예를 들어 isTyping 과 isSubmitting 이 동시에 true 일 순 없다. 이러한 역설은 보통 state 가 충분히 제한되지 않았음을 의미.
여기엔 두 boolean 에 대한 네 가지 조합이 있지만 오직 유효한 state 는 세 개뿐이다.
이러한 불가능한 state 를 제거하기 위해 'typing', 'submitting', 'success' 세 값을 하나의 status 로 합칠 수 있다. - 다른 state 변수에 이미 같은 정보가 담겨있진 않나요?
isEmpty 와 isTyping 은 동시에 true 가 될 수 없다. 이를 각각의 state 변수로 분리하면 싱크가 안맞거나 버그가 발생 위험이 있다.
이 경우엔 isEmpty 를 지우고 answer.length === 0 으로 체크할 수 있다. - 다른 변수를 뒤집었을 때 같은 정보를 얻을 수 있진 않나요?
isError 는 error !== null 로도 대신 확인할 수 있기에 필요하지 않다.
이러한 정리 과정을 거치면 세 가지(일곱 개에서 줄어든!) 필수 변수 만 남게 된다.
// 어느 하나를 지웠을 때 정상적으로 작동하지 않는다는 점에서 이것들이 모두 필수라는 것을 알 수 있다.
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
🧐 Reducer 를 사용하여 '불가능한' state 제거
폼 state 를 나타내는데 충분한 세 가지 변수를 구성했지만, 여전히 부자연스러운 일부 중간 state 가 있다.
예를 들어, error 가 null 이 아닌데 status 가 success 인 것은 말이 되지 않는다.
state 를 좀 더 정확하게 모델링하기 위해 리듀서로 분리하면 여러 state 변수를 하나의 객체로 통합하고 관련된 모든 로직도 합칠 수 있다.
5️⃣ state 설정을 위해 이벤트 핸들러 연결하기
이러한 코드가 명령형 프로그래밍 예시보단 길지만 조금 더 견고하다.
모든 상호작용을 state 로 표현하게 되면 이후 새로운 시각적 state 가 추가되더라도 기존 로직의 손상을 막을 수 있다.
또한 상호작용 자체의 로직을 변경하지 않고도 각각의 state 에 표시되는 항목을 변경할 수 있다.
if (status === 'success') {
return <h1>That's right!</h1>
}
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
2. State 구조화 : 불필요하고 중복된 데이터를 제거해, 오류 없이 상태를 쉽게 업데이트하자!
- 연관된 state 그룹화하기 : 두 개 이상의 state 가 항상 동시에 업데이트된다면, 단일 state 로의 병합을 고려하라.
- state 의 모순 피하기 : 여러 state 조각을 서로 모순되고 불일치할 수 있는 방식으로 구성한다면 실수가 발생할 수 있다.
- 불필요한 state 피하기 : 렌더링 중에 컴포넌트의 props 나 기존 state 변수에서 일부 정보를 계산할 수 있다면, 컴포넌트의 state 에 해당 정보를 넣지 말아야 한다.
- state 의 중복 피하기 : 여러 상태 변수 간 또는 중첩된 객체 내에서 동일한 데이터가 중복될 경우 동기화를 유지하기 어렵다.
+ 선택된 아이템은 따로 state 에 저장 : 선택과 같은 UI 패턴의 경우, 객체 자체가 아닌 ID 또는 인덱스를 state에 유지하자. - 깊게 중첩된 state 피하기 : 깊게 계층화된 state 는 업데이트하기 쉽지 않다. 가능하면 평탄한 방식으로 구성하자.
1️⃣ 연관된 state 그룹화하기
단일 state 변수와 다중 state 변수 사이에서 무엇을 사용할지 불확실한 경우
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const [position, setPosition] = useState({ x: 0, y: 0 });
위 두 가지 방식 모두 사용 가능하지만, 두 개의 state 가 항상 함께 변경된다면 단일 state 변수로 통합하는 것이 좋다.
(state 가 객체인 경우 다른 필드를 명시적으로 복사하지 않고 하나의 필드만 업데이트할 순 없다. setPosition({ ..position, x: 100 }) 과 같이 복사 필수)
그러면 마우스 커서의 움직임에 따라 빨간 점의 두 좌표가 모두 업데이트되는 예시처럼 항상 동기화를 유지하는 것을 잊지 않을 것이다.
2️⃣ state 의 모순 피하기
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
위 코드는 불가능한 state 를 허용하는데, 둘 중 한 state 를 호출하는 것을 잊은 경우
isSending 과 isSent 가 동시에 true 인 상황에 처할 수 있다. (컴포넌트가 복잡할수록 이해하기 어려워진다.)
두 상태는 동시에 true 가 되어서는 안되기에,
typing(초깃값), sending, sent'세 가지 유효한 상태 중 하나를 가질 수 있는 status state 변수로 대체하는 것이 좋다.
const [status, setStatus] = useState('typing');
3️⃣ 불필요한 state 피하기
컴포넌트의 props 나 다른 state 에서 일부 정보를 얻을 수 있다면, 그 정보를 가진 state 를 또 생성하지 않아야 한다.
아래 예시에선 렌더링 중에 항상 firstName과 lastName에서 fullName을 계산할 수 있기 때문에 state에서 제거하자.
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName; // state XXX, 렌더링 중 계산됨
➕ Props 를 state 에 미러링하지 마세요.
다음 코드는 불필요한 state 의 예시로, color state 는 messageColor prop 으로 초기화된다.
문제는 부모 컴포넌트가 나중에 다른 값의 messageColor 를 전달한다면('blue' 대신 'red'), color state 는 업데이트되지 않는다!
state 는 첫 번째 렌더링 중에만 초기화된다.
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
대신 messageColor prop 을 직접 사용하거나, 다른 이름을 지정하려면 상수를 사용한다.
const color = messageColor;
이렇게 하면 부모 컴포넌트에서 전달된 prop 과 동기화를 잃지 않는다.
Props 를 상태로 '미러링'하는 것은 특정 prop 에 대한 모든 업데이트를 무시하기를 원할 때에만 의미가 있다.
그 경우엔 prop 의 이름을 initial 또는 default 로 시작해 새로운 값이 무시됨을 명확히 하자.
function Message({ initialColor }) {
const [color, setColor] = useState(initialColor);
4️⃣ state 의 중복 피하기
아래 메뉴 목록 컴포넌트로 하나의 여행 간식을 선택할 수 있다.
하지만 이는 좋지 않다. selectedItem 의 내용이 items 목록 내의 항목 중 하나와 동일하다.(중복된다.)
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]); // 선택한 간식 저장 {id: 0, title: 'pretzels'}
이 경우 아래처럼 같은 내용이지만 동기화가 무너진 것을 볼 수 있다.
중복을 제거하고, 필수적인 state 만 유지하자!
selectedItem 객체 대신 selectedId 를 state 로 유지한 후, item 배열에서 해당 id 로 selecteItem 을 가져오자.
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0); // 선택한 item id 저장 0
const selectedItem = items.find(item =>
item.id === selectedId
);
이제 선택한 항목 input 을 편집하면 아래 메세지가 즉시 업데이트된다.
setItems 가 다시 렌더링하도록 유발하고, items.find(...) 가 업데이트된 제목의 항목을 찾을 것!
➕ 선택된 아이템은 따로 state 에 저장(다중 선택 구현)
다중 선택을 구현하려면 state 구조를 배열로 변경해야 한다.
const [selectedId, setSelectedId] = useState(null);
const [selectedIds, setSelectedIds] = useState([]);
//..
function handleToggle(toggledId) {
if (selectedIds.includes(toggledId)) {
setSelectedIds(selectedIds.filter(id =>
id !== toggledId
));
//..
하지만 배열을 사용했을 때 사소한 단점은 각 항목에 대해 includes() 를 통해 선택 여부를 확인한다는 것으로,
배열이 매우 큰 경우 이러한 배열 검색은 선형 시간이 걸리고, 개별 항목마다 검사를 수행하기에 성능상 문제가 될 수 있다.
이를 해결하기 위해, state 에 Set 을 대신 보관하여 has() 연산으로 빠른 검사를 수행할 수 있다.
const [selectedIds, setSelectedIds] = useState(new Set());
//..
function handleToggle(toggledId) {
// Create a copy (to avoid mutation).
const nextIds = new Set(selectedIds);
if (nextIds.has(toggledId)) {
nextIds.delete(toggledId);
} else {
//..
5️⃣ 깊게 중첩된 state 피하기
행성, 대륙, 국가로 구성된 여행 계획을 상상해 보자. 예시처럼 중첩된 객체와 배열을 사용해 여행 계획의 state 를 구성하고 싶을 수도 있다.
export const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
}, {
// ...
}, {
id: 45,
title: 'Tycho',
childPlaces: []
}]
}, {
id: 46,
title: 'Mars',
childPlaces: [{
id: 47,
title: 'Corn Town',
childPlaces: []
}, {
id: 48,
title: 'Green Hill',
childPlaces: []
}]
}]
};
이제 방문한 장소를 삭제하는 버튼을 추가하고 싶다.
중첩된 state 를 업데이트하려면 변경된 부분부터 모든 객체의 복사본을 만들어야 한다.
(이러한 코드는 매우 장황해질 수 있다.)
아래와 같이 state 를 평탄(정규화)하게 만들면 중첩된 항목을 업데이트하는 것이 더 쉬워진다.
// 위 코드처럼 각 place 가 자식 장소의 배열을 가지는 트리 구조 대신,
// 각 장소가 자식 장소 ID 의 배열을 가지도록 했다.
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 42, 46],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 26, 34]
},
// ...
48: {
id: 48,
title: 'Green Hill',
childIds: []
}
};
🔗 https://ko.react.dev/learn/managing-state
State 관리하기 – React
The library for web and native user interfaces
ko.react.dev