들어가며

React 애플리케이션이 복잡해질수록 상태 관리의 어려움이 증가합니다. 전역 상태 관리 라이브러리를 도입하면 이러한 문제를 해결할 수 있지만, 기존의 React 훅 사용 패턴도 함께 변경되어야 합니다. 이 가이드는 전역 상태 관리 라이브러리 도입의 이유와 그에 따른 훅 사용 가이드의 변화를 설명합니다.

 

결론

전역 상태 관리 라이브러리를 도입하면 다음과 같은 이점을 얻을 수 있습니다:

  1. Props Drilling 해결: 깊은 컴포넌트 계층에서도 쉽게 상태 공유
  2. 상태 동기화: 여러 컴포넌트 간 상태 일관성 보장
  3. 복잡한 로직 중앙화: 상태 관련 비즈니스 로직을 한 곳에서 관리
  4. 성능 최적화: 선택적 구독으로 불필요한 리렌더링 방지

하지만 전역 상태 관리 라이브러리를 도입할 때는 기존의 React 훅 사용 패턴도 함께 변경되어야 합니다:

  • useState: 로컬 상태만 관리, 전역 상태는 스토어에서
  • useMemo: 복잡한 계산은 스토어에서, 간단한 계산은 컴포넌트에서
  • useEffect: API 호출은 스토어에서, 컴포넌트 초기화는 컴포넌트에서

이러한 원칙을 따르면 유지보수하기 쉽고, 성능이 좋은 React 애플리케이션을 만들 수 있습니다.

전역 상태 관리 라이브러리를 사용하는 이유

1. Props Drilling 문제 해결

❌ 전역 상태 관리 없이

// Page Component
const ChatAgentListPage = () => {
    const [userPermissions, setUserPermissions] = useState([]);
    const [isResourceAdmin, setIsResourceAdmin] = useState(false);
    
    return (
        <div>
            <Header 
                userPermissions={userPermissions}
                isResourceAdmin={isResourceAdmin}
            />
            <AgentList 
                userPermissions={userPermissions}
                isResourceAdmin={isResourceAdmin}
            />
            <Sidebar 
                userPermissions={userPermissions}
                isResourceAdmin={isResourceAdmin}
            />
        </div>
    );
};

// List Component
const AgentList = ({ userPermissions, isResourceAdmin }) => {
    return (
        <div>
            {agents.map(agent => (
                <AgentListItem 
                    key={agent.id}
                    agent={agent}
                    userPermissions={userPermissions}
                    isResourceAdmin={isResourceAdmin}
                />
            ))}
        </div>
    );
};

// ListItem Component
const AgentListItem = ({ agent, userPermissions, isResourceAdmin }) => {
    return (
        <div>
            <AgentCard 
                agent={agent}
                userPermissions={userPermissions}
                isResourceAdmin={isResourceAdmin}
            />
        </div>
    );
};

✅ 전역 상태 관리 사용

// Page Component
const ChatAgentListPage = () => {
    return (
        <div>
            <Header />
            <AgentList />
            <Sidebar />
        </div>
    );
};

// List Component
const AgentList = () => {
    return (
        <div>
            {agents.map(agent => (
                <AgentListItem key={agent.id} agent={agent} />
            ))}
        </div>
    );
};

// ListItem Component
const AgentListItem = ({ agent }) => {
    const { isResourceAdmin } = usePermissionStore();
    
    return (
        <div>
            <AgentCard agent={agent} />
        </div>
    );
};

2. 상태 동기화 문제 해결

❌ 전역 상태 관리 없이

// Header Component
const Header = () => {
    const [userInfo, setUserInfo] = useState(null);
    
    useEffect(() => {
        // 사용자 정보 로드
        loadUserInfo().then(setUserInfo);
    }, []);
    
    return <div>Welcome, {userInfo?.name}</div>;
};

// Sidebar Component
const Sidebar = () => {
    const [userInfo, setUserInfo] = useState(null);
    
    useEffect(() => {
        // 동일한 사용자 정보를 다시 로드
        loadUserInfo().then(setUserInfo);
    }, []);
    
    return <div>User: {userInfo?.name}</div>;
};

✅ 전역 상태 관리 사용

// Header Component
const Header = () => {
    const { userInfo } = useUserStore();
    
    return <div>Welcome, {userInfo?.name}</div>;
};

// Sidebar Component
const Sidebar = () => {
    const { userInfo } = useUserStore();
    
    return <div>User: {userInfo?.name}</div>;
};

// 전역 상태에서 한 번만 로드
const useUserStore = createStore((set) => ({
    userInfo: null,
    loadUserInfo: async () => {
        const userInfo = await loadUserInfo();
        set({ userInfo });
    },
}));

3. 복잡한 상태 로직 중앙화

❌ 전역 상태 관리 없이

// 각 컴포넌트에서 개별적으로 권한 체크
const ChatAgentCard = () => {
    const isResourceAdmin = useMemo(() => {
        const userResourceRoles = JSON.parse(sessionStorage.getItem('userResourceRoles') || '[]');
        return userResourceRoles.some(role => 
            role.roleCode && role.roleCode.toLowerCase().includes('admin')
        );
    }, []);
    
    return <div>{isResourceAdmin && <SettingsButton />}</div>;
};

const Header = () => {
    const isResourceAdmin = useMemo(() => {
        const userResourceRoles = JSON.parse(sessionStorage.getItem('userResourceRoles') || '[]');
        return userResourceRoles.some(role => 
            role.roleCode && role.roleCode.toLowerCase().includes('admin')
        );
    }, []);
    
    return <div>{isResourceAdmin && <AdminMenu />}</div>;
};

✅ 전역 상태 관리 사용

// 중앙화된 권한 관리
const usePermissionStore = createStore((set) => ({
    userResourceRoles: [],
    isResourceAdmin: false,
    
    setUserResourceRoles: (roles) => {
        const isResourceAdmin = roles.some(role => 
            role.roleCode && role.roleCode.toLowerCase().includes('admin')
        );
        set({ userResourceRoles: roles, isResourceAdmin });
    },
}));

// 각 컴포넌트에서 간단하게 사용
const ChatAgentCard = () => {
    const { isResourceAdmin } = usePermissionStore();
    
    return <div>{isResourceAdmin && <SettingsButton />}</div>;
};

const Header = () => {
    const { isResourceAdmin } = usePermissionStore();
    
    return <div>{isResourceAdmin && <AdminMenu />}</div>;
};

전역 상태 관리 도입 시 훅 사용 가이드 변경

1. useState 사용 위치 변경

기존 가이드 (전역 상태 관리 없이)

// Page Component에서 전역 상태 관리
const Page = () => {
    const [globalState, setGlobalState] = useState();
    const [pageState, setPageState] = useState();
    
    return <List globalState={globalState} pageState={pageState} />;
};

새로운 가이드 (전역 상태 관리 사용)

// Page Component: 페이지 레벨 상태만 관리
const Page = () => {
    const [pageState, setPageState] = useState(); // 페이지 레벨 상태만
    const { globalState, setGlobalState } = useGlobalStore(); // 전역 상태는 스토어에서
    
    return <List pageState={pageState} />;
};

// 전역 상태는 별도 스토어에서 관리
const useGlobalStore = createStore((set) => ({
    globalState: null,
    setGlobalState: (state) => set({ globalState: state }),
}));

2. useMemo 사용 위치 변경

기존 가이드 (전역 상태 관리 없이)

// Page Component에서 복잡한 계산
const Page = () => {
    const [data, setData] = useState([]);
    const [filters, setFilters] = useState({});
    
    const processedData = useMemo(() => {
        // 복잡한 데이터 가공 로직
        return processData(data, filters);
    }, [data, filters]);
    
    return <List data={processedData} />;
};

새로운 가이드 (전역 상태 관리 사용)

// Page Component: 단순한 계산만
const Page = () => {
    const [pageFilters, setPageFilters] = useState({});
    const { processedData, setFilters } = useDataStore();
    
    // 페이지 필터 변경 시 전역 상태 업데이트
    useEffect(() => {
        setFilters(pageFilters);
    }, [pageFilters, setFilters]);
    
    return <List data={processedData} />;
};

// 전역 스토어에서 복잡한 계산 처리
const useDataStore = createStore((set, get) => ({
    rawData: [],
    filters: {},
    processedData: [],
    
    setData: (data) => {
        const { filters } = get();
        const processedData = processData(data, filters);
        set({ rawData: data, processedData });
    },
    
    setFilters: (filters) => {
        const { rawData } = get();
        const processedData = processData(rawData, filters);
        set({ filters, processedData });
    },
}));

3. useEffect 사용 위치 변경

기존 가이드 (전역 상태 관리 없이)

// 각 컴포넌트에서 개별적으로 API 호출
const Page = () => {
    const [data, setData] = useState([]);
    
    useEffect(() => {
        loadData().then(setData);
    }, []);
    
    return <List data={data} />;
};

새로운 가이드 (전역 상태 관리 사용)

// Page Component: 단순한 초기화만
const Page = () => {
    const { data, loadData } = useDataStore();
    
    useEffect(() => {
        loadData(); // 전역 스토어의 액션 호출
    }, [loadData]);
    
    return <List data={data} />;
};

// 전역 스토어에서 API 호출 관리
const useDataStore = createStore((set) => ({
    data: [],
    loading: false,
    error: null,
    
    loadData: async () => {
        set({ loading: true, error: null });
        try {
            const data = await loadData();
            set({ data, loading: false });
        } catch (error) {
            set({ error, loading: false });
        }
    },
}));

전역 상태 관리 도입 시 새로운 가이드

1. 상태 분류 기준

전역 상태 (Global State)

  • 사용자 정보: 로그인 상태, 권한, 프로필
  • 애플리케이션 설정: 테마, 언어, 설정
  • 공유 데이터: API 응답 데이터, 캐시된 데이터
  • 상태 관리: 로딩 상태, 에러 상태

페이지 상태 (Page State)

  • 페이지 레벨 UI 상태: 모달 열림/닫힘, 탭 선택
  • 페이지별 필터: 검색어, 정렬 옵션
  • 페이지별 설정: 페이지 크기, 현재 페이지

컴포넌트 상태 (Component State)

  • UI 상태: hover, focus, tooltip 표시
  • 애니메이션 상태: 로딩 스피너, 트랜지션
  • 폼 상태: 입력값, 유효성 검사

2. 새로운 훅 사용 가이드

Page Component

const Page = () => {
    // 페이지 레벨 상태만 관리
    const [pageState, setPageState] = useState();
    const [modalOpen, setModalOpen] = useState(false);
    
    // 전역 상태는 스토어에서 가져오기
    const { globalData, loadGlobalData } = useGlobalStore();
    const { userInfo } = useUserStore();
    
    // 페이지 초기화
    useEffect(() => {
        loadGlobalData();
    }, [loadGlobalData]);
    
    return (
        <div>
            <Header />
            <List data={globalData} />
            <Modal open={modalOpen} onClose={() => setModalOpen(false)} />
        </div>
    );
};

List Component

const List = ({ data }) => {
    // 리스트 레벨 상태만 관리
    const [sortOrder, setSortOrder] = useState('asc');
    const [expandedItems, setExpandedItems] = useState(new Set());
    
    // 전역 상태에서 필요한 데이터 가져오기
    const { userPermissions } = usePermissionStore();
    
    // 리스트 레벨 계산
    const sortedData = useMemo(() => {
        return [...data].sort((a, b) => {
            return sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
        });
    }, [data, sortOrder]);
    
    return (
        <div>
            {sortedData.map(item => (
                <ListItem 
                    key={item.id}
                    item={item}
                    isExpanded={expandedItems.has(item.id)}
                    onToggle={() => toggleExpanded(item.id)}
                />
            ))}
        </div>
    );
};

ListItem Component

const ListItem = ({ item, isExpanded, onToggle }) => {
    // 아이템 레벨 상태만 관리
    const [isHovered, setIsHovered] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    
    // 전역 상태에서 필요한 데이터 가져오기
    const { isResourceAdmin } = usePermissionStore();
    const { updateItem } = useItemStore();
    
    // 아이템 레벨 계산
    const canEdit = useMemo(() => {
        return isResourceAdmin && item.status === 'ACTIVE';
    }, [isResourceAdmin, item.status]);
    
    return (
        <div 
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={() => setIsHovered(false)}
        >
            <ItemCard 
                item={item}
                isHovered={isHovered}
                canEdit={canEdit}
                onEdit={() => updateItem(item.id)}
            />
        </div>
    );
};

Button Component

const Button = ({ onClick, children }) => {
    // 버튼 레벨 상태만 관리
    const [isPressed, setIsPressed] = useState(false);
    const [rippleEffect, setRippleEffect] = useState(null);
    
    // 전역 상태에서 필요한 데이터 가져오기
    const { isResourceAdmin } = usePermissionStore();
    
    // 버튼 활성화 상태 계산
    const isEnabled = useMemo(() => {
        return isResourceAdmin && !isPressed;
    }, [isResourceAdmin, isPressed]);
    
    return (
        <button
            disabled={!isEnabled}
            onMouseDown={() => setIsPressed(true)}
            onMouseUp={() => setIsPressed(false)}
            onClick={onClick}
        >
            {children}
        </button>
    );
};

3. 전역 상태 관리 라이브러리별 특징

Zustand (현재 프로젝트에서 사용)

// 간단하고 직관적인 API
const useStore = createStore((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
}));

// 사용
const Component = () => {
    const { count, increment } = useStore();
    return <button onClick={increment}>{count}</button>;
};

Redux Toolkit

// 더 구조화된 접근
const store = configureStore({
    reducer: {
        counter: counterSlice.reducer,
        user: userSlice.reducer,
    },
});

// 사용
const Component = () => {
    const count = useSelector((state) => state.counter.value);
    const dispatch = useDispatch();
    
    return <button onClick={() => dispatch(increment())}>{count}</button>;
};

Jotai

// 원자적 상태 관리
const countAtom = atom(0);
const incrementAtom = atom(null, (get, set) => {
    set(countAtom, get(countAtom) + 1);
});

// 사용
const Component = () => {
    const [count] = useAtom(countAtom);
    const [, increment] = useAtom(incrementAtom);
    
    return <button onClick={increment}>{count}</button>;
};

전역 상태 관리 도입 시 주의사항

1. 상태 분리 원칙

  • 전역 상태: 여러 컴포넌트에서 공유되는 상태
  • 로컬 상태: 특정 컴포넌트에서만 사용되는 상태
  • 파생 상태: 다른 상태로부터 계산되는 상태

2. 성능 최적화

// 선택적 구독으로 불필요한 리렌더링 방지
const Component = () => {
    // 전체 상태 구독 (비추천)
    const state = useStore();
    
    // 필요한 부분만 구독 (권장)
    const count = useStore((state) => state.count);
    const increment = useStore((state) => state.increment);
    
    return <button onClick={increment}>{count}</button>;
};

3. 상태 구조 설계

// 도메인별로 상태 분리
const useUserStore = createStore((set) => ({
    user: null,
    permissions: [],
    isAuthenticated: false,
    // 사용자 관련 액션들
}));

const useChatStore = createStore((set) => ({
    messages: [],
    currentChannel: null,
    // 채팅 관련 액션들
}));

const useWorkflowStore = createStore((set) => ({
    workflows: [],
    currentWorkflow: null,
    // 워크플로우 관련 액션들
}));

 

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기