들어가며
React 개발에서 상태 관리는 중요한 고민거리입니다. 전역 상태 관리 라이브러리와 커스텀 훅은 모두 상태 관리의 해결책이지만, 각각의 특징과 사용 시기가 다릅니다. 이 가이드는 두 접근 방식의 차이점과 언제 어떤 것을 사용해야 하는지에 대한 명확한 기준을 제시합니다.
결론 및 권장사항
현재 프로젝트의 권한 관리에 대한 권장사항
전역 상태 관리를 사용해야 하는 이유:
- 여러 컴포넌트에서 공유: Header, Sidebar, ChatAgentCard 등에서 동일한 권한 정보 사용
- 상태 동기화 필요: 로그인 시 권한이 변경되면 모든 컴포넌트에서 즉시 반영
- 복잡한 권한 로직: Resource Admin, Feature Admin 등 다양한 권한 체크
- 성능 최적화: sessionStorage 파싱을 한 번만 수행하고 결과를 캐시
일반적인 선택 기준
전역 상태 관리를 사용해야 하는 경우:
- ✅ 여러 컴포넌트에서 공유되는 상태
- ✅ 상태 변경이 다른 컴포넌트에 영향을 미치는 경우
- ✅ 복잡한 상태 로직이 있는 경우
- ✅ 상태가 애플리케이션 전반에 걸쳐 중요한 경우
커스텀 훅을 사용해야 하는 경우:
- ✅ 컴포넌트별 독립적인 상태
- ✅ 재사용 가능한 로직
- ✅ 단순한 계산이나 변환
- ✅ 폼 상태, API 호출 로직 등
최종 권장사항
현재 프로젝트의 권한 관리에는 전역 상태 관리를 사용하는 것이 적합합니다. 이유는:
- 코드 중복 제거: 권한 체크 로직을 한 곳에서 관리
- 일관성 보장: 모든 컴포넌트에서 동일한 권한 정보 사용
- 성능 최적화: 불필요한 sessionStorage 파싱 방지
- 유지보수성 향상: 권한 로직 변경 시 한 곳만 수정
이러한 접근 방식을 통해 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
전역 상태 관리 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;
}
}, []);
문제점 분석
- 코드 중복: 다른 컴포넌트에서도 동일한 로직 반복
- 일관성 부족: 각 컴포넌트에서 개별적으로 권한 체크
- 유지보수 어려움: 권한 로직 변경 시 여러 곳 수정 필요
- 성능 이슈: 매번 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>
);
};
'웹 개발 > 프론트엔드 ∕ React' 카테고리의 다른 글
| [React] 전역 상태 관리 라이브러리 도입과 변경되는 React 훅 사용 가이드 (0) | 2025.10.22 |
|---|---|
| [React] 훅 사용 위치 가이드: 언제 어디서 사용해야 할까? (0) | 2025.10.22 |
| [React] rc-tree 라이브러리를 통한 treeview 공통 컴포넌트 만들기 (0) | 2023.05.09 |
| [React] 개발 참고자료 목록 (0) | 2022.10.23 |
| [React] 기본 실습 (0) | 2022.06.21 |





최근댓글