[PEAUTY] 상태관리에 대한 회고록
[Peauty] 상태관리에 대한 회고록 🐻
우선 상태 관리란 무엇을 관리하는 것일까?
상태 관리란, 애플리케이션에서 데이터(상태)를 효율적으로 저장하고 업데이트 하는 방식을 말합니다.
상태는 무엇일까?
상태 관리하는 데이터들을 살펴보면 알 수 있는데, 사용자의 행동이나 애플리케이션의 변화에 따라 달라지는 데이터라고 볼 수 있습니다.
보통 리액트에서 import { useState } from "react";
로 호출해서 사용하는 것을 떠올릴 수 있겠습니다.
웹사이트가 UI에 동작하는데 필요한 모든 데이터를 상태라고 말합니다.
상태가 왜 중요할까?
이번 프로젝트에서 상태관리하는 경우가 꽤 많았습니다.
이 페이지는 mypage > detail
인데, 여기서도 modal
이나 toastMessage
, userData
등 상태관리해야할 것이 많이 보입니다.
이렇게 상태가 많아지면 어떤 값이 어디서 바뀌는 지에 대한 정리와 관리가 필요하기 때문에 상태 관리가 중요합니다!
React에서 상태관리가 중요한 이유
Vanilla JavaScript를 사용할 때는 주로 이벤트를 처리하고 DOM을 직접 수정하는데, React는 DOM 조작을 자동화하고, 상태 관리에 집중하도록 설계합니다.
예를 들어, 쇼핑몰에서 장바구니에 추가버튼을 눌렀을 때 화면이 즉시 업데이트되지 않는다면 불편함을 느낌!
⇒ React는 이를 간단하게 해결할 수 있도록 도와줍니다.
React는 컴포넌트 기반의 UI라이브러리로, 컴포넌트의 상태(State)에 따라 화면이 동적으로 업데이트 됩니다.
상태관리가 효율적이지 않으면 성능 저하, 불필요한 리렌더링, 데이터 동기화 문제등이 발생합니다.
정리
🔹 상태를 여러 컴포넌트에서 사용해야 할 때 🔹 Props Drilling(부모 → 자식 → 손자…)이 발생할 때 🔹 전역 상태(로그인 정보, 테마, 언어 설정 등)를 관리할 때 🔹 비동기 데이터(API 요청, 캐싱된 데이터 등)를 관리할 때
상태 관리 라이브러리
상태 관리 라이브러리 비교
라이브러리 | 사용 난이도 | 상태 저장 위치 | 주요 특징 |
---|---|---|---|
Context API | 쉬움 ⭐ | React 자체 제공 | 전역 상태 관리 가능하지만 최적화 필요 |
Redux | 어려움 ⭐⭐⭐ | Store (중앙 저장소) | 복잡한 상태 관리 가능 (Middleware 사용 가능) |
Zustand | 쉬움 ⭐ | Store (중앙 저장소) | 사용법 간단, Redux보다 가벼움 |
Recoil | 중간 ⭐⭐ | Atom (분산 저장소) | 개별 상태(Atom)로 구성, 비동기 상태 관리 지원 |
Jotai | 쉬움 ⭐ | Atom (간단한 상태 저장소) | React 상태 관리 방식을 단순화 |
MobX | 중간 ⭐⭐ | Store (중앙 저장소) | 클래스 기반 상태 관리, 자동 추적 가능 |
상태 관리 라이브러리 종류
-
Context API(리액트 기본 제공)
React
에서 기본 제공하는 전역 상태 관리 방법입니다.작은 프로젝트에 적합하지만 최적화 하지 않으면 불필요한 리렌더링이 발생합니다.
-
Redux(복잡한 상태 관리, 대규모)
상태를 중앙 저장소(Store)에 저장하고 모든 컴포넌트에서 접근 가능합니다.
액션(Action) ➡️ 리듀서(Reducer)➡️스토어(store)
Middleware (Thunk, Saga)와 함께 사용하여 비동기 상태 관리 가능합니다.
// 1. 액션 정의 const INCREMENT = "INCREMENT"; const incrementAction = () => ({ type: INCREMENT }); // 2. 리듀서 정의 const counterReducer = (state = { count: 0 }, action) => { switch (action.type) { case INCREMENT: return { count: state.count + 1 }; default: return state; } }; // 3. Redux 스토어 생성 const store = createStore(counterReducer); store.dispatch(incrementAction()); // 상태 변경 실행
➡ 액션을 통해 상태 변경을 요청하고, 리듀서가 새로운 상태를 반환하는 방식
-
Zustand (가벼운 상태 관리, Redux 대체)
Redux보다 간단하며 보일러플레이트 코드가 적습니다. React Hook 기반으로 직관적 사용 가능합니다. 비동기 상태 관리도 가능합니다.
import create from "zustand";
// Zustand 상태 정의
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}));
// 컴포넌트에서 사용
function Counter() {
const { count, increase } = useStore();
return <button onClick={increase}>Count: {count}</button>;
}
➡ Zustand는 중앙 저장소를 활용하면서도 간단한 API로 관리할 수 있음
- Recoil (페이스북에서 만든 라이브러리)
React에서 자연스럽게 사용할 수 있도록 설계됩니다.
Redux보다 간단하고 빠름, 비동기 상태 관리도 지원합니다.
import { atom, useRecoilState } from "recoil";
// 상태 정의 (Atom)
const counterState = atom({
key: "counterState",
default: 0,
});
// 컴포넌트에서 상태 사용
function Counter() {
const [count, setCount] = useRecoilState(counterState);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
➡ Recoil은 개별 상태(Atom)로 구성되며, 필요한 컴포넌트에서 직접 접근 가능
-
MobX (자동 상태 추적, 간단한 코드)
Observable
객체를 사용하여 상태를 자동으로 추적합니다. 클래스 기반 프로그래밍을 선호하는 경우 유용합니다.import { makeAutoObservable } from "mobx"; class CounterStore { count = 0; constructor() { makeAutoObservable(this); } increase() { this.count++; } } const counterStore = new CounterStore();
➡ MobX는 데이터 흐름을 자동으로 추적하고 필요한 부분만 업데이트함
전역 상태 관리 라이브러리 - Zustand🐻
저는 peauty
에서는 로컬 상태관리인 useState
를 사용했는데, 전역상태관리를 적용해보려고 합니다.
전역 상태 관리 라이브러리는 복잡한 애플리케이션에서 체계적으로 상태를 관리할 수 있습니다.
https://zustand.docs.pmnd.rs/getting-started/introduction
그 중에서도 zustand를 선택한 이유는 다음과 같습니다.
-
Redux보다 가볍고 사용이 쉽다.
-
보일러플레이트 코드(반복되는 코드)가 적다.
-
persist
,devtools
같은 미들웨어도 지원한다.로깅, 상태 영구저장(persistence)등 다양한 미들웨어를 지원해서 확장성이 높습니다.
-
Context API보다 성능이 좋다.
-
Flux 패턴을 따르지 않아 자유롭게 사용이 가능하다.
-
선택적 리렌더링이 가능
상태 값이 변경될 때만 해당 컴포넌트를 리렌더링하여, 불필요한 렌더링을 방지하여 성능을 향상 시킵니다.
-
중앙 집중식 상태 관리
중앙 집중식으로 상태를 관리하며, 단순하게 정의도니 작업을 통해 상태를 업데이트할 수 있습니다. 이는 Redux와 유사하지만, Reducer, Action 및 Dispatch를 만들어야 하는 Redux와 달리 훨씬 쉽습니다.
-
React Hooks과의 통합
Zustand는 React의 Hooks를 활용하여 상태를 관리하므로, 기존 React 패턴과 자연스럽게 통합됩니다.
peauty에 적용해보기
우선 store
폴더를 하나 생성해서 여기에서 userDetail에 대한 상태관리를 해보겠습니다.
위와 같이 상태관리가 필요한 항목에 대해 interface로 정의하고 이에대해서 동작할 코드를 작성합니다.
이게 이전에 hook으로 따로 빼서 사용하던 커스텀 훅인데, store을 사용해서
이렇게 했습니다.
또 userProfile
관련 store를 만들었습니다.
zustand
를 이용해서 useProfileStore라는 상태 저장소를 생성했습니다. 이 상태저장소는 사용자의 프로필을 관리하는 방식입니다.
import {create} from 'zustand';
에서 create
함수를 사용해서 상태 저장소를 생성합니다.
interface ProfileState{
profileData: GetCustomerProfileResponse
isLoading: boolean
err: string | null
fetchProfile: (userId: number) => Promise<void>
clearProfile: () => void;
}
profileDate
는 사용자의 프로필 데이터를 저장하는 상태입니다.GetCustomerProfileResponse
라고 저장해 놓은 타입을 따릅니다.isLoading
:데이터를 불러오는 동안true
로 설정됩니다.error
: API요청이 실패했을 때 에러 메시지를 저장하는 상태입니다.fetchProfile(userId: number)
: API를 호출해서profileData
를 업데이트하는 비동기 함수 입니다.clearProfile()
: 프로필 데이터를 초기화하는 함수입니다.
export const useUserProfileStore = create<ProfileState>((set) => ({
profileData: {},
isLoading: false,
error: null,
}))
create
함수를 통해서 상태저장소를 만들고 set
함수를 사용해서 상태 값을 업데이트 합니다.
featchProfile: async (userId: number) => {
set({isLoading: true})
try{
const data = await getCustomerProfile(userId)
set({profileData: data, isLoading: false})
} catch(error) {
set({error: '프로필을 가져오는데 실패했습니다.', isLoading: false})
}
}
fetchProfile
은 비동기 함수로, 사용자의 ID를 받아서 해당 프로필 데이터를 가져옵니다.
API요청을 시작할 때, isLoading을 true로 설정해서 로딩 상태를 표시할 수 있도록하고 요청이 성공하면 profileData
를 업데이트 하고 로딩 상태를 종료합니다.
이때, 요청을 실패한다면 error 상태를 업데이트하고 로딩을 종료합니다.
이 스토어를 사용해보겠습니다.
처음에 다음과 같이 있는 Deatil페이지에서
이렇게 호출 부분의 코드가 간결해졌습니다.
그리고 코드에서 직접하던 처리를 store로 빼서
위와 같은 코드에서
아래와 같이 변경했습니다.
상태관리 라이브러리를 쓰고 이점
- 프로필 데이터를 여러 컴포넌트에서 쉽게 공유 가능합니다.
- 상태 관리 로직이 컴포넌트에서 분리되어 유지보수가 용이합니다.
- 캐시처럼 사용할 수 있어 불필요한 API 호출 감소 가능합니다.
헷깔리는 용어 정리🤔
-
왜 store라는 이름을 사용할까?
통상적으로
store
는 데이터를 저장하는 곳(저장소)를 의미해서 많이 사용한다고 합니다. -
비동기 통신
비동기 통신(Asynchronous Communication)이란, 요청과 응답이 동시에 이루어지지 않습니다.
요청을 보낸 후에 응답을 기다리지 않고 다른 작업을 수행 할 수 있는 방식을 합니다. 이메일이나 편지를 떠올리기! (응~ 나는 보냈으니까 장땡이야~)
동기 통신은 그러면 반대로 전화를 거는 것 같이 요청을 보내면 응답이 올 때까지 기다려야합니다. 응답이 올 때까지 다른 작업을 수행할 수 없습니다.(블로킹)
🚀왜 비동기 통신이 필요할까?
-
UI/UX를 개선할 수 있습니다.
동기방식이면 응답이 오는 동안 UI가 멈춤!
-
성능 향상
여러 개의 요청을 동시에 처리해서 속도를 높일 수 있습니다.
-
서버 부하 감소
서버가 한 번에 너무 많은 요청을 처리하지 않도록 조절할 수 있습니다.
-
네트워크 지연 대응
네트워크 요청이 오래 걸려도 애플리케이션이 멈추지 않습니다.
각각 어디에 많이 사용할까?
동기 통신은 반드시 응답을 받아야하는 경우에 사용하는 것이 적합한데, 예시로는 사용자 입력 검증할때, 로그인과 같은 상황에서 아이디와 비밀번호가 유효한지 즉시 확인해야하는 경우에 사용합니다.
DB의 트랜젝션 처리도 동기 통신을 합니다. 예를 들어 은행이라고 치면, 하나의 작업이 완료되지 않으면 다음 작업을 진행해서는 안될 때!
입금, 출금 등이 요청 온 순차대로 진행해야하기 때문이죠!
비동기 통신은 시간이 오래 걸리는 작업을 처리하면서도, 다른 작업을 동시에 수행해야 하는 경우에 적합합니다!
API요청, 사용자 로그인 시에 백엔드 서버에서 인증 정보를 받아오는 경우 비동기 통신을 활용합니다.
또한 실시간 데이터 업데이트(WebSokets, SSE) 같은 상황과 파일 업로드 및 다운로드 상황에서도 웹 페이지가 멈추지 않고 계속 동작을 진행해야하기 때문에 비동기 통신을 활용합니다.
이메일/SMS 전송도 마찬가지며, 백그라운드 데이터 로딩(Lazy Loading)때 페이지가 로딩 된 후에 추가 데이터를 가져와 화면을 업데이트하는 무한 스크롤과 같은 경우에도 비동기 통신을 합니다.
느낀 점
코딩을 하면서 느끼는 점은 늘 설계의 중요성입니다.
초반 설계를 얼마나 탄탄하게 하는가 그리고 설계된 데이터가 수정됐을때 얼마나 유연하게 적용할 수 있는가가 중요한 것 같습니다.
zustand
를 적용하는 과정에서도 userData와 userProfileData를 분리해서 관리했는데, 각각 로그인 여부와 그 로그인한 유저의 데이터를 관리하는 역할을 했습니다.
어떤 부분을 어떻게 디테일하게 쪼개서 관리할 것인지에 대한 고민을 하게 됐습니다.
댓글남기기