리액트 클린코드 - State
TIL 24. 02. 29
클린코드 리액트 - 상태
점점 어려워지는 상태관리. 이유는?
- 상태의 종류
- 컴포넌트 상태, 전역 상태, 서버 상태
- 상태 변경, 상태 최적화
- 렌더링 최적화, 불변성
- 상태 관리자
-
상태관리가 점점 어려워지는 이유는 상태 관리를 위한 일들이 계속 생기기 때문이다.
-
여러 상태들이 생기고 (컴포넌트 상태, 전역 상태, 서버 상태)
-
해당 상태를 관리하기 위한 방법론이 등장 (상태 변경, 상태 최적화)
-
상태를 변경할 때 발생하는 이슈를 해결하기 위한 렌더링 최적화와 불변성 유지 (렌더링 최적화, 불변성)
-
상태 관리를 위한 관리자를 배우고 최적화 (상태 관리자)
-
상태란 무엇일까?
상태(State)란 리액트 컴포넌트에서 관리되는 데이터로서, 컴포넌트의 렌더링에 영향을 미치는 모든 데이터라고 할 수 있다.
하나의 페이지에서 실시간으로 데이터를 주고 받는 방식의 SPA 구조로 웹 애플리케이션을 개발하게 되면서 많은 상태를 관리할 필요가 생겼다.
상태는 사용자의 상호작용이나 다양한 이벤트에 따라 변경되어 컴포넌트를 리렌더링하도록 유발한다. 상태를 잘 관리해야 애플리케이션의 예측 가능성을 높이고 버그를 줄이며 불필요한 리렌더링을 막을 수 있다.
올바른 상태 초기값 설정하기
아래의 코드를 살펴보면 겉으로는 문제가 없어 보일 수 있으나 오류가 발생한다. list의 초기값이 설정되어 있지 않아 undefined으로 초기화되는데 undefined 객체는 map 메서드를 호출할 수 없기 때문이다. 게다가 useEffect의 실행 시점은 렌더링 이후이므로 페칭한 데이터가 저장되지 않았다.
import { useState, useEffect } from 'react';
function InitState() {
const [list, setList] = useState();
useEffect(() => {
const fetchData = async () => {
const res = await fetch('https://api.example.com/data');
const result = await res.json();
setList(result);
};
fetchData();
}, []);
return (
<>
{list.map((item) => ( // ❌ TypeError
<Item item={item} />
))}
</>
);
}
따라서 list의 값이 있는지 확인하기 위해 다음과 같이 배열인지 검증하는 과정이 필요하다.
return (
<>
{Array.isArray(list) && list.map((item) => (
<Item item={item} />
))}
</>
);
하지만 이 코드는 상태의 초기값만 잘 지정했다면 작성하지 않아도 되는 방어코드이다.
const [list, setList] = useState([]);
업데이트 되지 않는 변수를 리액트 컴포넌트 내부에 두지 말 것
student1 변수는 리액트 컴포넌트 내부에 있기 때문에 렌더링 될 때마다 student1 객체의 참조값도 매번 새롭게 생성된다. StudentInfo 컴포넌트로 전달한 props이 항상 변경되었다고 간주하여 불필요한 리렌더링이 발생한다.
import { useState, useEffect } from 'react';
function NotUpdateValue() {
const student1 = {
name: 'Kim',
gender: 'M'
}
return (
<StudentInfo student={student1} />
);
}
이러한 경우 리액트 상태로 바꾸거나 외부로 내보내는 방법이 있다. 정적 데이터이며 변경될 필요가 없는 경우에는 상태로 만들 필요가 없다. 컴포넌트의 상태나 속성이 변경되면 리렌더링이 발생한다. 업데이트 되지 않는 상수나 고유한 값들은 컴포넌트 외부에 선언하자.
변하지 않는 값을 컴포넌트 밖에 선언하면, 이 값은 동일한 메모리 상의 객체나 값을 참조하게 된다.
import { useState, useEffect } from 'react';
const student1 = {
name: 'Kim',
gender: 'M'
}
function NotUpdateValue() {
return (
<StudentInfo student={student1} />
);
}
플래그 상태 사용하기
플래그 값은 프로그래밍 언어에서 특정 조건 혹은 흐름 제어를 위한 조건을 불리언으로 나타내는 값이다. 아래의 코드는 매 렌더링마다 다시 계산이 되어 isLogin에 저장되기 때문에 굳이 상태가 필요하지 않다.
일반적으로는 추가 요구사항이 생긴다면 if문을 사용하여 해당 조건을 추가할 것이다. 하지만 조건들이 계속 추가될수록 코드의 가독성이 떨어지며 수정하기 어려운 코드가 된다. 플래그 변수를 컴포넌트 내부에서 정의하고 잘 관리하여 useState 대신 플래그로 상태를 정의할 수 있다.
function FlagState() {
const isLogin =
hasToken &&
hasCookie &&
isValidCookie &&
!isNewUser &&
isValidToken;
return <div>{isLogin && '안녕하세요!'}</div>
}
불필요한 상태 제거하기
불필요한 상태를 만들게 되면 리액트에 의해 관리되는 값이 늘어나 렌더링에도 영향을 준다. 예를 들어 살펴보자. 투두리스트에서 완료된 항목만 화면에 나타내고자 한다.
import { useEffect, useState } from 'react';
function DoneTodoList({ todos }) {
const [todoList, setTodoList] = useState(todos);
const doneList = todos.filter((todo) => todo.completed);
return (
<ul>
{doneList.map((todo) => (
<li key={todo.id}>
{todo.title} - {todo.description}
</li>
))}
</ul>
);
}
doneList
는 todos
로부터 계산할 수 있다. 굳이 상태로 관리할 필요 없이 컴포넌트 내부에서 계산된 값을 사용하여 렌더링하면 된다. 컴포넌트 내부의 변수는 렌더링마다 고유의 계산된 값을 가진다.
게다가 todos
prop이 상위 컴포넌트로부터 업데이트될 경우, 리액트는 자동으로 컴포넌트를 새로운 props와 함께 리렌더링한다. 따라서 useState
를 통해 상태를 만들 필요가 없다.
상태 관리나 사이드 이펙트 로직 없이 props를 바로 사용하기 때문에 코드가 훨씬 간결하고 명확하다.
function DoneTodoList({ todos }) {
const doneList = todos.filter((todo) => todo.completed);
return (
<ul>
{doneList.map((todo) => (
<li key={todo.id}>
{todo.title} - {todo.description}
</li>
))}
</ul>
);
}
useState 대신 useRef 사용하기
특정 플래그값 혹은 컴포넌트 내부에서 관리되지만 한 번 고정된 값을 계속해서 사용하는 값은 useState 대신 useRef를 사용할 수 있다. 예를 들면 isMounted
와 같이 컴포넌트의 전체적인 수명과 동일하게 데이터가 유지되는 경우이다.
function Component() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
if (!isMounted) setMounted(true);
}, [isMounted])
return <div>{isMounted && '컴포넌트 마운트'}</div>;
}
useState를 통해 값이 만들어졌다는 건 결국 setState가 동작하며 리렌더링이 발생한다는 것이다. useState 대신 useRef를 사용하여 컴포넌트의 생명주기와 동일한 리렌더링 되지 않는 상태를 만들 수 있다.
function Component() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, [])
return <div>{isMounted.current && '컴포넌트 마운트'}</div>;
}
연관된 상태 단순화하기
비동기 통신 이후 상태에 맞는 컴포넌트를 렌더링하려고 한다. Loading
, Finish
, Error
는 하나의 상태가 true일 때 나머지 상태는 false로 설정되어야 한다.
function Component({ url }) {
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isFail, setIsFail] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setIsSuccess(false);
setIsFail(false);
try {
const response = await fetch(url);
if (response.ok) {
setIsSuccess(true);
} else {
setIsFail(true);
}
} catch (error) {
setIsFail(true);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (isLoading) return <LoadingComponent />;
if (isError) return <ErrorComponent />;
if (isFinish) return <SuccessComponent />;
}
예시의 상태들은 비동기 상태와 연관되어 있는 상태들이다. 상태를 여러 개 만들어 관리하다 보면 사이드 이펙트가 점점 많아지므로 연관된 상태를 하나로 묶어서 관리하는 방법이 있다. 상태를 만들 때 연관된 것들끼리 묶어서 처리하면 에러를 방지하고 하나의 객체로 여러 상태를 관리하기 쉽다.
import React, { useEffect, useState } from 'react';
function Component({ url }) {
const [fetchStatus, setFetchStatus] = useState({
isLoading: false,
isSuccess: false,
isFail: false,
});
useEffect(() => {
const fetchData = async () => {
setFetchStatus((prevState) => ({ isLoading: true, isSuccess: false, isFail: false });
try {
const response = await fetch(url);
if (response.ok) {
setFetchStatus((prevState) => ({ ...prevState, isSuccess: true }));
} else {
setFetchStatus((prevState) => ({ ...prevState, isFail: true }));
}
} catch (error) {
setFetchStatus((prevState) => ({ ...prevState, isFail: true });
} finally {
setFetchStatus((prevState) => ({ ...prevState, isLoading: false }));
}
};
fetchData();
}, []);
const { isLoading, isError, isFinish } = fetchStatus;
if (isLoading) return <LoadingComponent />;
if (isSuccess) return <ErrorComponent />;
if (isFail) return <SuccessComponent />;
}
export default Component;
useReducer로 리팩토링하기
useReducer
를 사용하면 여러 상태가 연관되었을 때 상태를 구조화할 수 있다. 상태 업데이트 로직을 컴포넌트 밖으로 분리하여 컴포넌트가 깔끔해지며, 다양한 상태 변화를 더 명시적으로 처리할 수 있게 된다. 또한 의도하지 않은 상태 변경이 발생하는 것도 방지할 수 있다.
내부 로직이 추상화 되어 있기 때문에 action type만 보고도 어떤 동작이 일어날지 예측할 수 있으며, 리듀서 함수 내에서 모든 상태 업데이트를 관리할 수 있으므로 상태 변화를 일관되게 유지할 수 있다.
import React, { useEffect, useReducer } from 'react';
// 초기 상태 정의
const initialState = {
isLoading: false,
isSuccess: false,
isFail: false
};
// 액션 타입 정의
const actionTypes = {
FETCH_LOADING: 'FETCH_LOADING',
FETCH_SUCCESS: 'FETCH_SUCCESS',
FETCH_FAILURE: 'FETCH_FAILURE',
};
// 리듀서 함수 정의
const reducer = (state, action) => {
switch (action.type) {
case actionTypes.FETCH_LOADING:
return { isLoading: true, isSuccess: false, isFail: false };
case actionTypes.FETCH_SUCCESS:
return { isLoading: false, isSuccess: true, isFail: false };
case actionTypes.FETCH_FAILURE:
return { isLoading: false, isSuccess: false, isFail: true };
default:
return initialState
}
};
function Component({ url }) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const fetchData = async () => {
dispatch({ type: actionTypes.FETCH_LOADING });
try {
const response = await fetch(url);
if (response.ok) {
dispatch({ type: actionTypes.FETCH_SUCCESS });
} else {
dispatch({ type: actionTypes.FETCH_FAILURE });
}
} catch (error) {
dispatch({ type: actionTypes.FETCH_FAILURE });
}
}
fetchData();
}, []);
const { isLoading, isSuccess, isFail } = state;
if (isLoading) return <LoadingComponent />;
if (isFail) return <ErrorComponent />;
if (isSuccess) return <SuccessComponent />;
}
상태 로직을 Custom Hooks로 뽑아내기
Custom Hooks을 사용했을 때의 장점은 다음과 같다.
- 로직의 재사용: 컴포넌트 간에 공유할 수 있는 로직을 커스텀 훅으로 만들어 비즈니스 로직이나 상태 관리 등을 쉽게 재사용
- 코드의 간결성: 코드의 가독성을 높이고 관심사를 분리하여 더 깔끔하고 관리하기 쉬운 코드를 작성
- 컴포넌트 구조 개선: 컴포넌트의 로직을 외부로 분리하여 컴포넌트는 UI에 집중
fetchData와 state 로직을 확장성 있게 재활용해보자.
import React, { useEffect, useReducer } from 'react';
const initialState = { ... };
const actionTypes = { ... };
const reducer = (state, action) => {
switch (action.type) {
case actionTypes.FETCH_LOADING:
return { isLoading: true, isSuccess: false, isFail: false };
case actionTypes.FETCH_SUCCESS:
return { isLoading: false, isSuccess: true, isFail: false };
case actionTypes.FETCH_FAILURE:
return { isLoading: false, isSuccess: false, isFail: true };
default:
return initialState;
}
};
function useFetchData(url) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const fetchData = async () => {
dispatch({ type: actionTypes.FETCH_LOADING });
try {
const response = await fetch(url);
if (response.ok) {
dispatch({ type: actionTypes.FETCH_SUCCESS });
} else {
dispatch({ type: actionTypes.FETCH_FAILURE });
}
} catch (error) {
dispatch({ type: actionTypes.FETCH_FAILURE });
}
};
fetchData();
}, [url]);
return state;
}
function Component({ url }) {
const { isLoading, isSuccess, isFail } = useFetchData(url);
if (isLoading) return <LoadingComponent />;
if (isFail) return <ErrorComponent />;
if (isSuccess) return <SuccessComponent />;
}
렌더링 되는 부분이 아닌 부분을 완전히 추상화하여 컴포넌트가 아주 단순해졌다. Custom Hooks를 사용하면 코드를 확장성 있고 재사용 가능하게 작성할 수 있다.
댓글남기기