목표
상태를 여러개 사용하다보면 상태 간 "꼬임"이 발생할 수도 있고 동기화를 놓쳐버려서 오류를 야기할 수도 있다.
따라서 오류를 최소화하기 위해서 state를 단순화시키고 불필요한 state는 제거하는 것이 좋다.
데이터베이스 구조를 정규화해서 버그 발생 가능성을 줄이는 것과 유사하다.
방법들
더 나은 상태를 관리하는 가장 좋은 방법은 state를 최대한 단순하게 만드는 것 이다.
- 연관된 state 그룹화 : 항상 한번에 업데이트해야하는 상태값들이 있다면 여러 개의 state로 분리하는 것 보다 하나의 object로 묶어서 하나의 state로 관리하자.
- state의 모순 피하기 : 서로 다른 상태 간에 논리적으로 불가능한 값을 가질 수 있는 환경을 제거하자.
- 불필요한 state 제거하기 : 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 계산할 수 있는 정보들은 state로 관리하기보다는 일반 변수로 사용하자.
- state의 중복 피하기 : 동기화를 최소화할 수 있도록 상태 간에 중복된 정보는 최소화하자.
- 깊게 계층화된 state 구조는 피하기 : state는 가급적 단순한 구조로 관리하자.
State가 아닌 경우
아래의 경우는 리액트 공식 문서에서 명확하게 state로 관리해서는 안된다고 말하는 케이스들이다.
- 시간이 지나도 변하지 않는 것
- 부모로부터 props를 통해 전달되는 것
- 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한 것
연관된 state 그룹화
그룹화란 여러 개의 state로 분리하지 않고 하나의 state로 묶어서 관리하는 것을 말한다.
아래의 경우 그룹화하는 것이 좋다.
- 항상 같이 업데이트 해야하는 변수들
- 요소 수가 가변적으로 변할 수 있는 배열이나 필드가 유동적으로 추가/삭제될 수 있는 객체
예를 들어, 대부분 같이 업데이트해야하는 두 개의 변수 <pre>x</pre>, <pre>y</pre>가 있다고 하자.
그 경우 <pre>x</pre>와 <pre>y</pre>를 별도의 상태로 관리하기 보다는 하나의 상태로 관리하는 것이 좋다.
나쁜 예시 1
const [x, setX] = useState(0);
const [y, setY] = useState(0);
좋은 예시 1
const [position, setPosition] = useState({ x: 0, y: 0 });
또는 <pre>type User = { id: string; name: string; }</pre> 형태를 가지고 배열이 증가/감소 할 수 있는 경우도 하나의 staet로 관리하는 것이 좋다.
나쁜 예시 2
const [user1, setUser1] = useState({ id: "user1", name: "Amy" });
const [user2, setUser2] = useState({ id: "user2", name: "Kale" });
좋은 예시 2
cosnt [users, setUsers] = useStaet([
{ id: "user1", name: "Amy" },
{ id: "user2", name: "Kale" },
])
State의 모순 피하기
모순되는 상태가 발생할 가능성이 있는 상태들은 만들지 않는 것이 좋다.
예를 들어, 결과 상태를 나타내는 변수 <pre>isSuccess</pre>와 <pre>isError</pre> 가 있을 때,
논리적으로는 <pre>isSuccess</pre>와 <pre>isError</pre>는 동시에 같은 값을 가지지 않는 것이 좋다.
나쁜 예시
아래 코드는 <pre>isSuccess</pre>와 <pre>isError</pre>가 같은 값을 가질 수 있을 뿐더러,
<pre>isSuccess</pre>나 <pre>isError</pre> 중 하나라도 동기화를 깜빡하면 논리적인 오류이다. (동작은 하겠지만)
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
function sentSucceed() {
setIsSuccess(true);
setIsError(false);
}
좋은 예시
아래처럼 state 간에 모순이 발생하지 않도록 하고, 가독성을 위해서 상수를 선언하는 것이 낫다.
const [status, setStatus] = useState<"none" | "success" | "error">("none");
const isSuccess = status === "success";
const isError = status === "error";
불필요한 state 제거하기
렌더링 과정 중에 계산할 수 있는 정보라면 상태로 관리하기 보다는 컴포넌트 내부에 변수(const)로 관리하는 것이 좋다.
예를 들어
- props나 다른 state에서 가져오면 되거나
- 그 정보들로 단순히 계산만 하면 되는 값들
이다.
두 개의 상태 <pre>firstName</pre>, <pre>lastName</pre>가 있고 fullName을 사용자에게 노출하거나 필요한 경우
<pre>fullName</pre> 라는 상태를 추가로 두기보다 firstName과 lastName을 조합해서 fullName을 만드는 것이 낫다.
렌더링 과정 중에 계산이 되는 것이기에 불필요하게 상태를 관리할 필요가 없기 때문이다.
나쁜 예시 1
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
좋은 예시 1
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName;
특히 의도된 일부의 경우를 제외하고는 props를 state에 미러링하지 않아야 한다.
최초에 props로 전달된 값이 state의 initialValue로 세팅만 되고,
이후에 props의 값이 바뀌더라도 state는 업데이트 되지 않음을 유의해야한다.
나쁜 예시 2
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
// ...
}
좋은 예시 2
function Message({ messageColor }) {
const color = messageColor;
// ...
}
State의 중복 피하기
동일한 내용을 여러군데의 상태에서 관리하지 않는 것이 좋다.
아래 예시처럼, 선택된 값이 무엇인지 관리하기 위한 상태로 <pre>const selectedItem: Item = {...}</pre>을 유지하기 보다는
선택된 아이템이 무엇인지 식별할 수 있는 key값만을 가지는 것이 좋다.
만약 <pre>items</pre>가 편집 가능하고 선택된 아이템 정보를 다 들고 있다면 items 편집 시 selectedItem도 같이 업데이트 해야줘야한다.
반대로 말하자면 items는 업데이트를 했지만, selectedItem을 업데이트 하지 않았다면 문제가 발생하게 된다는 것이다.
즉, 향후 발생할 수 있는 에러를 제거하기 위함이다.
나쁜 예시
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(
items[0]
);
좋은 예시
selectedItem도 결국 렌더링 과정 중에 items와 selectedItemId를 이용하여 계산할 수 있는 값이다.
동기화 위험을 줄이고 불필요한 상태를 제거하기 위해서는 상태간 중복되는 정보를 최소화하는 것이 좋다.
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
const [items, setItems] = useState(initialItems);
const [selectedItemId, setSelectedItemId] = useState(0);
const selectedItem = items.find(item => item.id === selectedId);
깊게 계층화된 state 구조는 피하기
depth가 깊게 유지되는 state 구조는 피하고, 단순한 구조의 state를 유지하는 것이 좋다.
중첩된 구조의 state를 업데이트 하는 것은 변경된 부분부터 그 아래 단의 모든 객체의 복사본을 만들어야 함이고,
이는 코드 동작의 복잡성을 증가시킨다.
나쁜 예시
const [items, setItems] = useState({
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
}]
}]
}]
});
좋은 예시
중첩된 구조보다는 flat한 구조가 state 업데이트를 쉽게할 수 있다.
const [items, setItems] = useState({
0: {
id: 0,
title: '(Root)',
childIds: [1, 42, 46],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 26, 34]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4, 5, 6 , 7, 8, 9]
},
});
참고
React 공식 문서 : https://ko.react.dev/learn/choosing-the-state-structure
'Coding > [Web] Frontend' 카테고리의 다른 글
[React] 0(숫자) && <Component/>은 0을 렌더링한다. (0) | 2025.01.03 |
---|---|
[React] 다른 방식으로 조건부 렌더링을 하는 컴포넌트는 동일할까? (0) | 2025.01.03 |
[React] Controlled, Uncontrolled Component (0) | 2024.10.07 |
[Javascript] null, undefined (0) | 2024.01.11 |
[React] useImperativeHandle (forwardRef의 ref 커스터마이징) (1) | 2024.01.04 |
댓글