들어가며

React 개발에서 상태 관리는 중요한 고민거리입니다. 전역 상태 관리 라이브러리와 커스텀 훅은 모두 상태 관리의 해결책이지만, 각각의 특징과 사용 시기가 다릅니다. 이 가이드는 두 접근 방식의 차이점과 언제 어떤 것을 사용해야 하는지에 대한 명확한 기준을 제시합니다.

결론 및 권장사항

현재 프로젝트의 권한 관리에 대한 권장사항

전역 상태 관리를 사용해야 하는 이유:

  1. 여러 컴포넌트에서 공유: Header, Sidebar, ChatAgentCard 등에서 동일한 권한 정보 사용
  2. 상태 동기화 필요: 로그인 시 권한이 변경되면 모든 컴포넌트에서 즉시 반영
  3. 복잡한 권한 로직: Resource Admin, Feature Admin 등 다양한 권한 체크
  4. 성능 최적화: sessionStorage 파싱을 한 번만 수행하고 결과를 캐시

일반적인 선택 기준

전역 상태 관리를 사용해야 하는 경우:

  • ✅ 여러 컴포넌트에서 공유되는 상태
  • ✅ 상태 변경이 다른 컴포넌트에 영향을 미치는 경우
  • ✅ 복잡한 상태 로직이 있는 경우
  • ✅ 상태가 애플리케이션 전반에 걸쳐 중요한 경우

커스텀 훅을 사용해야 하는 경우:

  • 컴포넌트별 독립적인 상태
  • 재사용 가능한 로직
  • ✅ 단순한 계산이나 변환
  • ✅ 폼 상태, API 호출 로직 등

최종 권장사항

현재 프로젝트의 권한 관리에는 전역 상태 관리를 사용하는 것이 적합합니다. 이유는:

  1. 코드 중복 제거: 권한 체크 로직을 한 곳에서 관리
  2. 일관성 보장: 모든 컴포넌트에서 동일한 권한 정보 사용
  3. 성능 최적화: 불필요한 sessionStorage 파싱 방지
  4. 유지보수성 향상: 권한 로직 변경 시 한 곳만 수정

이러한 접근 방식을 통해 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

 

전역 상태 관리 vs 커스텀 훅의 차이점

1. 상태의 지속성 (Persistence)

전역 상태 관리

// Zustand Store
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>;
};

특징:

  • 상태가 컴포넌트 생명주기와 독립적으로 유지
  • 컴포넌트가 언마운트되어도 상태가 보존
  • 여러 컴포넌트 간 상태 공유 가능

커스텀 훅

// 커스텀 훅
export const useMenuPermission = () => {
    const permissionedFeatureCodes = JSON.parse(
        sessionStorage.getItem('permissionedFeatureCodes') || '[]'
    );

    const hasPermission = useMemo(
        () => (permission: MenuPermission) => {
            return permissionedFeatureCodes.includes(permission);
        },
        [permissionedFeatureCodes]
    );

    return { hasPermission };
};

// 컴포넌트에서 사용
const ChatAgentCard = () => {
    const { hasPermission } = useMenuPermission();
    
    return <div>{hasPermission('ADMIN') && <SettingsButton />}</div>;
};

특징:

  • 컴포넌트가 마운트될 때마다 새로운 인스턴스 생성
  • 컴포넌트가 언마운트되면 상태도 함께 사라짐
  • 각 컴포넌트에서 독립적인 상태 관리

2. 상태 공유 범위 (Sharing Scope)

전역 상태 관리

// 전역 상태: 여러 컴포넌트에서 동일한 상태 공유
const Header = () => {
    const { isResourceAdmin } = usePermissionStore();
    return <div>{isResourceAdmin && <AdminMenu />}</div>;
};

const Sidebar = () => {
    const { isResourceAdmin } = usePermissionStore(); // 동일한 상태
    return <div>{isResourceAdmin && <AdminPanel />}</div>;
};

const ChatAgentCard = () => {
    const { isResourceAdmin } = usePermissionStore(); // 동일한 상태
    return <div>{isResourceAdmin && <SettingsButton />}</div>;
};

커스텀 훅

// 커스텀 훅: 각 컴포넌트에서 독립적인 상태
const Header = () => {
    const { hasPermission } = useMenuPermission();
    return <div>{hasPermission('ADMIN') && <AdminMenu />}</div>;
};

const Sidebar = () => {
    const { hasPermission } = useMenuPermission(); // 독립적인 상태
    return <div>{hasPermission('ADMIN') && <AdminPanel />}</div>;
};

const ChatAgentCard = () => {
    const { hasPermission } = useMenuPermission(); // 독립적인 상태
    return <div>{hasPermission('ADMIN') && <SettingsButton />}</div>;
};

3. 성능 특성 (Performance Characteristics)

전역 상태 관리

// 선택적 구독으로 성능 최적화
const usePermissionStore = createStore((set) => ({
    userResourceRoles: [],
    isResourceAdmin: false,
    userFeatureRoles: [],
    // ... 다른 상태들
}));

// 필요한 부분만 구독
const Component = () => {
    const isResourceAdmin = usePermissionStore((state) => state.isResourceAdmin);
    // userResourceRoles나 다른 상태가 변경되어도 리렌더링되지 않음
    
    return <div>{isResourceAdmin && <AdminButton />}</div>;
};

커스텀 훅

// 매번 새로운 계산 수행
export const useMenuPermission = () => {
    const permissionedFeatureCodes = JSON.parse(
        sessionStorage.getItem('permissionedFeatureCodes') || '[]'
    );

    const hasPermission = useMemo(
        () => (permission: MenuPermission) => {
            return permissionedFeatureCodes.includes(permission);
        },
        [permissionedFeatureCodes]
    );

    return { hasPermission };
};

// 각 컴포넌트에서 독립적으로 계산
const Component = () => {
    const { hasPermission } = useMenuPermission(); // 매번 새로운 인스턴스
    
    return <div>{hasPermission('ADMIN') && <AdminButton />}</div>;
};

언제 무엇을 사용해야 할까?

1. 전역 상태 관리를 사용해야 하는 경우

✅ 여러 컴포넌트에서 공유되는 상태

// 사용자 인증 상태 - 여러 컴포넌트에서 공유
const useAuthStore = createStore((set) => ({
    isAuthenticated: false,
    user: null,
    login: (user) => set({ isAuthenticated: true, user }),
    logout: () => set({ isAuthenticated: false, user: null }),
}));

// Header, Sidebar, MainContent 등에서 모두 사용
const Header = () => {
    const { isAuthenticated, user } = useAuthStore();
    return <div>{isAuthenticated ? `Welcome, ${user.name}` : 'Please login'}</div>;
};

✅ 상태 변경이 다른 컴포넌트에 영향을 미치는 경우

// 채팅 상태 - 한 컴포넌트에서 변경하면 다른 컴포넌트도 업데이트
const useChatStore = createStore((set) => ({
    messages: [],
    currentChannel: null,
    addMessage: (message) => set((state) => ({
        messages: [...state.messages, message]
    })),
}));

// 채팅 입력 컴포넌트
const ChatInput = () => {
    const { addMessage } = useChatStore();
    
    const handleSend = (message) => {
        addMessage(message); // 메시지 추가
    };
    
    return <input onKeyPress={handleSend} />;
};

// 채팅 목록 컴포넌트
const ChatList = () => {
    const { messages } = useChatStore(); // 자동으로 업데이트됨
    
    return (
        <div>
            {messages.map(message => (
                <div key={message.id}>{message.text}</div>
            ))}
        </div>
    );
};

✅ 복잡한 상태 로직이 있는 경우

// 워크플로우 상태 - 복잡한 비즈니스 로직
const useWorkflowStore = createStore((set, get) => ({
    workflowNodeInstancesMap: {},
    workflowNodeExecutionResults: {},
    
    updateWorkflowNodeInstance: (nodeId, data) => {
        const currentNode = get().workflowNodeInstancesMap[nodeId] || {};
        const newNode = { ...currentNode, ...data };
        
        // 복잡한 검증 로직
        if (isValidNode(newNode)) {
            set((state) => ({
                workflowNodeInstancesMap: {
                    ...state.workflowNodeInstancesMap,
                    [nodeId]: newNode,
                },
            }));
        }
    },
}));

2. 커스텀 훅을 사용해야 하는 경우

✅ 컴포넌트별 독립적인 상태

// 폼 상태 - 각 폼마다 독립적인 상태
export const useForm = (initialValues) => {
    const [values, setValues] = useState(initialValues);
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState({});
    
    const handleChange = (name, value) => {
        setValues(prev => ({ ...prev, [name]: value }));
        setTouched(prev => ({ ...prev, [name]: true }));
    };
    
    const handleSubmit = (onSubmit) => {
        const validationErrors = validate(values);
        if (Object.keys(validationErrors).length === 0) {
            onSubmit(values);
        } else {
            setErrors(validationErrors);
        }
    };
    
    return { values, errors, touched, handleChange, handleSubmit };
};

// 각 폼에서 독립적으로 사용
const LoginForm = () => {
    const { values, errors, handleChange, handleSubmit } = useForm({
        email: '',
        password: ''
    });
    
    return (
        <form onSubmit={() => handleSubmit(handleLogin)}>
            <input 
                value={values.email}
                onChange={(e) => handleChange('email', e.target.value)}
            />
        </form>
    );
};

✅ 재사용 가능한 로직

// API 호출 로직 - 여러 컴포넌트에서 재사용
export const useServices = () => {
    const query = useQuery({
        queryKey: ['services'],
        queryFn: async () => {
            const response = await serviceService.getServices();
            return response.services;
        },
    });

    return {
        services: query.data,
        isLoading: query.isLoading,
        error: query.error,
        refetch: query.refetch,
    };
};

// 여러 컴포넌트에서 재사용
const ServiceList = () => {
    const { services, isLoading } = useServices();
    return <div>{services?.map(service => <div key={service.id}>{service.name}</div>)}</div>;
};

const ServiceSelector = () => {
    const { services, isLoading } = useServices();
    return <select>{services?.map(service => <option key={service.id}>{service.name}</option>)}</select>;
};

✅ 단순한 계산이나 변환

// 데이터 변환 로직 - 단순한 계산
export const useMenuPermission = () => {
    const permissionedFeatureCodes = JSON.parse(
        sessionStorage.getItem('permissionedFeatureCodes') || '[]'
    );

    const hasPermission = useMemo(
        () => (permission: MenuPermission) => {
            return permissionedFeatureCodes.includes(permission);
        },
        [permissionedFeatureCodes]
    );

    return { hasPermission };
};

현재 프로젝트의 권한 관리 분석

현재 상황: ChatAgentCard에서 직접 권한 체크

// ChatAgentCard.tsx
const isResourceAdmin = useMemo(() => {
    try {
        const userResourceRoles: Role[] = JSON.parse(
            sessionStorage.getItem('userResourceRoles') || '[]'
        );
        return userResourceRoles.some((role) =>
            role.roleCode && role.roleCode.toLowerCase().includes('admin')
        );
    } catch (error) {
        console.error('Failed to parse userResourceRoles:', error);
        return false;
    }
}, []);

문제점 분석

  1. 코드 중복: 다른 컴포넌트에서도 동일한 로직 반복
  2. 일관성 부족: 각 컴포넌트에서 개별적으로 권한 체크
  3. 유지보수 어려움: 권한 로직 변경 시 여러 곳 수정 필요
  4. 성능 이슈: 매번 sessionStorage 파싱 및 계산

권장 해결책: 전역 상태 관리 사용

1. 권한 스토어 생성

// store/permissionStore.ts
interface PermissionState {
    userResourceRoles: Role[];
    userFeatureRoles: Role[];
    isResourceAdmin: boolean;
    isFeatureAdmin: boolean;
    
    setUserRoles: (resourceRoles: Role[], featureRoles: Role[]) => void;
    hasResourceRole: (roleCode: string) => boolean;
    hasFeatureRole: (roleCode: string) => boolean;
}

export const usePermissionStore = createStore<PermissionState>((set, get) => ({
    userResourceRoles: [],
    userFeatureRoles: [],
    isResourceAdmin: false,
    isFeatureAdmin: false,
    
    setUserRoles: (resourceRoles, featureRoles) => {
        const isResourceAdmin = resourceRoles.some(role => 
            role.roleCode && role.roleCode.toLowerCase().includes('admin')
        );
        const isFeatureAdmin = featureRoles.some(role => 
            role.roleCode && role.roleCode.toLowerCase().includes('admin')
        );
        
        set({ 
            userResourceRoles: resourceRoles, 
            userFeatureRoles: featureRoles,
            isResourceAdmin,
            isFeatureAdmin
        });
    },
    
    hasResourceRole: (roleCode) => {
        const { userResourceRoles } = get();
        return userResourceRoles.some(role => 
            role.roleCode && role.roleCode.toLowerCase().includes(roleCode.toLowerCase())
        );
    },
    
    hasFeatureRole: (roleCode) => {
        const { userFeatureRoles } = get();
        return userFeatureRoles.some(role => 
            role.roleCode && role.roleCode.toLowerCase().includes(roleCode.toLowerCase())
        );
    },
}));

2. 로그인 시 권한 설정

// hooks/useAuth.ts
export const useAuth = () => {
    const { setUserRoles } = usePermissionStore();
    
    const login = async (username: string, password: string) => {
        try {
            const data = await authService.login(username, password);
            
            // 기존 sessionStorage 설정
            sessionStorage.setItem('accessToken', data.accessToken);
            sessionStorage.setItem('userName', data.userName);
            
            // 전역 상태에 권한 설정
            const resourceRoles = data.roles.filter((role) => role.type === RoleType.RESOURCE);
            const featureRoles = data.roles.filter((role) => role.type === RoleType.FEATURE);
            setUserRoles(resourceRoles, featureRoles);
            
            return data;
        } catch (err) {
            throw err;
        }
    };
    
    return { login, logout };
};

3. 컴포넌트에서 간단하게 사용

// ChatAgentCard.tsx
const ChatAgentCard = ({ id, name, description, isFavorite, onFavoriteToggle, onClick, roles }) => {
    const { isResourceAdmin } = usePermissionStore();
    
    return (
        <Box>
            {/* ... 기존 코드 ... */}
            {isResourceAdmin && (
                <Tooltip title={t('button.settings')}>
                    <IconButton onClick={(e) => handleSettingsClick(id)}>
                        <SettingsIcon />
                    </IconButton>
                </Tooltip>
            )}
        </Box>
    );
};

 

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