들어가며
React 개발에서 useState, useMemo, useEffect 등의 훅을 어디서 사용해야 할지 판단하는 것은 종종 어려운 문제입니다. 이 가이드는 컴포넌트 계층 구조에서 각 훅을 언제, 어디서 사용해야 하는지에 대한 명확한 기준을 제시합니다.
컴포넌트 계층 구조와 훅 사용 원칙
기본 계층 구조
Page Component (최상위)
├── List Component (데이터 목록)
│ ├── ListItem Component (개별 아이템)
│ │ └── Button Component (액션 버튼)
│ └── Filter Component (필터링)
└── Header Component (헤더)
종합 판단 기준
1. 상태의 범위 (Scope)
- 전역/페이지 레벨: Page Component
- 리스트 레벨: List Component
- 아이템 레벨: ListItem Component
- 컴포넌트 내부: 해당 컴포넌트
2. 데이터의 소유권 (Ownership)
- 데이터를 소유하는 컴포넌트에서 State 관리
- 데이터를 사용하는 컴포넌트에서는 Props로 받기
3. 성능 영향 (Performance Impact)
- 계산 비용이 높은 연산: useMemo 사용
- 불필요한 리렌더링 방지: 적절한 위치에서 최적화
4. 재사용성 (Reusability)
- 재사용 가능한 (순수 함수 컴포넌트) 컴포넌트: Props로 상태 받기
- 특정 컨텍스트 (특정 도메인 용도로 명시된) 컴포넌트: 내부에서 State 관리
실무 적용 가이드
✅ 권장 패턴
// Page: 전역 상태 관리
const Page = () => {
const [globalState, setGlobalState] = useState();
const processedData = useMemo(() => processData(globalState), [globalState]);
return <List data={processedData} />;
};
// List: 리스트 상태 관리
const List = ({ data }) => {
const [listState, setListState] = useState();
const filteredData = useMemo(() => filterData(data, listState), [data, listState]);
return filteredData.map(item => <ListItem key={item.id} item={item} />);
};
// ListItem: 아이템 상태 관리
const ListItem = ({ item }) => {
const [itemState, setItemState] = useState();
const derivedState = useMemo(() => deriveState(item, itemState), [item, itemState]);
return <Button item={derivedState} />;
};
// Button: 버튼 상태 관리
const Button = ({ item }) => {
const [buttonState, setButtonState] = useState();
return <button onClick={() => setButtonState(!buttonState)}>Click</button>;
};
❌ 피해야 할 패턴
// 잘못된 예: 하위 컴포넌트에서 전역 상태 직접 관리
const Button = () => {
const [globalState, setGlobalState] = useState(); // ❌
return <button>Click</button>;
};
// 잘못된 예: 상위 컴포넌트에서 세부 상태 관리
const Page = () => {
const [buttonHoverState, setButtonHoverState] = useState(); // ❌
return <Button onHover={setButtonHoverState} />;
};
결론
React 훅의 사용 위치는 상태의 범위, 데이터의 소유권, 성능 영향, 재사용성을 종합적으로 고려하여 결정해야 합니다.
- Page Component: 전역 상태, API 호출, 복잡한 데이터 가공
- List Component: 리스트 상태, 필터링, 페이징
- ListItem Component: 개별 아이템 상태, 파생 상태 계산
- Button Component: 버튼 내부 상태, 시각적 피드백
이러한 원칙을 따르면 유지보수하기 쉽고, 성능이 좋은 React 애플리케이션을 만들 수 있습니다.
useState 사용 위치 판단 기준
1. Page Component에서 사용해야 하는 경우
✅ 전역 상태나 페이지 레벨 상태
// Page Component
const ChatAgentListPage = () => {
// 페이지 전체에 영향을 미치는 상태
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [searchQuery, setSearchQuery] = useState('');
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
return (
<div>
<SearchBar
value={searchQuery}
onChange={setSearchQuery}
/>
<AgentList
agents={agents}
onAgentSelect={setSelectedAgent}
/>
</div>
);
};
판단 기준:
- 여러 하위 컴포넌트에서 공유되는 상태
- 페이지 전체의 동작에 영향을 미치는 상태
- API 호출과 관련된 상태
2. List Component에서 사용해야 하는 경우
✅ 리스트 관련 상태
// List Component
const AgentList = ({ agents, onAgentSelect }) => {
// 리스트 내부 상태
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [filteredAgents, setFilteredAgents] = useState(agents);
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
return (
<div>
{filteredAgents.map(agent => (
<AgentListItem
key={agent.id}
agent={agent}
isExpanded={expandedItems.has(agent.id)}
onToggle={() => toggleExpanded(agent.id)}
/>
))}
</div>
);
};
판단 기준:
- 리스트 내부의 정렬, 필터링 상태
- 리스트 아이템들의 상호작용 상태
- 리스트 레벨의 UI 상태
3. ListItem Component에서 사용해야 하는 경우
✅ 개별 아이템 상태
// ListItem Component
const AgentListItem = ({ agent, isExpanded, onToggle }) => {
// 개별 아이템의 내부 상태
const [isHovered, setIsHovered] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<AgentCard
agent={agent}
isHovered={isHovered}
isLoading={isLoading}
/>
</div>
);
};
판단 기준:
- 개별 아이템의 UI 상태 (hover, loading, tooltip 등)
- 아이템 내부의 상호작용 상태
- 다른 아이템에 영향을 주지 않는 상태
4. Button Component에서 사용해야 하는 경우
✅ 버튼 내부 상태
// Button Component
const ActionButton = ({ onClick, children }) => {
// 버튼 내부 상태
const [isPressed, setIsPressed] = useState(false);
const [rippleEffect, setRippleEffect] = useState<{ x: number; y: number } | null>(null);
return (
<button
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onClick={handleClick}
>
{children}
</button>
);
};
판단 기준:
- 버튼의 시각적 피드백 상태
- 애니메이션 관련 상태
- 버튼 내부의 상호작용 상태
useMemo 사용 위치 판단 기준
1. Page Component에서 사용해야 하는 경우
✅ 복잡한 데이터 가공
// Page Component
const ChatAgentListPage = () => {
const [agents, setAgents] = useState([]);
const [filters, setFilters] = useState({});
// 복잡한 필터링과 정렬
const processedAgents = useMemo(() => {
return agents
.filter(agent => {
if (filters.category && agent.category !== filters.category) return false;
if (filters.status && agent.status !== filters.status) return false;
return true;
})
.sort((a, b) => {
if (filters.sortBy === 'name') return a.name.localeCompare(b.name);
if (filters.sortBy === 'createdAt') return new Date(b.createdAt) - new Date(a.createdAt);
return 0;
});
}, [agents, filters]);
return <AgentList agents={processedAgents} />;
};
판단 기준:
- API 응답 데이터의 복잡한 가공
- 여러 필터 조건이 적용되는 데이터 처리
- 계산 비용이 높은 연산
2. List Component에서 사용해야 하는 경우
✅ 리스트 레벨 계산
// List Component
const AgentList = ({ agents, searchQuery }) => {
// 검색 결과 계산
const filteredAgents = useMemo(() => {
if (!searchQuery) return agents;
return agents.filter(agent =>
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
agent.description.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [agents, searchQuery]);
// 페이징 데이터
const paginatedAgents = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return filteredAgents.slice(startIndex, startIndex + pageSize);
}, [filteredAgents, currentPage, pageSize]);
return (
<div>
{paginatedAgents.map(agent => (
<AgentListItem key={agent.id} agent={agent} />
))}
</div>
);
};
판단 기준:
- 리스트 내부의 필터링, 정렬, 페이징
- 검색 결과 계산
- 리스트 레벨의 통계 계산
3. ListItem Component에서 사용해야 하는 경우
✅ 개별 아이템 파생 상태
// ListItem Component
const AgentListItem = ({ agent, userRoles }) => {
// 권한 체크
const canEdit = useMemo(() => {
return userRoles.some(role =>
role.roleCode && role.roleCode.toLowerCase().includes('admin')
);
}, [userRoles]);
// 포맷된 데이터
const formattedAgent = useMemo(() => ({
...agent,
displayName: agent.name.toUpperCase(),
lastActive: formatDate(agent.lastActive),
statusColor: getStatusColor(agent.status)
}), [agent]);
return (
<div>
<AgentCard
agent={formattedAgent}
canEdit={canEdit}
/>
</div>
);
};
판단 기준:
- 개별 아이템의 파생 상태 계산
- 권한 체크
- 데이터 포맷팅
4. Button Component에서 사용해야 하는 경우
✅ 버튼 상태 계산
// Button Component
const ActionButton = ({ agent, userPermissions }) => {
// 버튼 활성화 상태
const isEnabled = useMemo(() => {
return userPermissions.includes('EDIT') &&
agent.status === 'ACTIVE' &&
!agent.isLocked;
}, [userPermissions, agent.status, agent.isLocked]);
// 버튼 스타일
const buttonStyles = useMemo(() => ({
backgroundColor: isEnabled ? '#007bff' : '#6c757d',
cursor: isEnabled ? 'pointer' : 'not-allowed',
opacity: isEnabled ? 1 : 0.6
}), [isEnabled]);
return (
<button
style={buttonStyles}
disabled={!isEnabled}
>
Edit
</button>
);
};
판단 기준:
- 버튼의 활성화 상태 계산
- 조건부 스타일 계산
- 툴팁 텍스트 계산
useEffect 사용 위치 판단 기준
1. Page Component에서 사용해야 하는 경우
✅ API 호출과 초기화
// Page Component
const ChatAgentListPage = () => {
const [agents, setAgents] = useState([]);
const [loading, setLoading] = useState(true);
// 초기 데이터 로드
useEffect(() => {
const loadAgents = async () => {
try {
setLoading(true);
const data = await agentService.getAgents();
setAgents(data);
} catch (error) {
console.error('Failed to load agents:', error);
} finally {
setLoading(false);
}
};
loadAgents();
}, []);
// 필터 변경 시 데이터 재로드
useEffect(() => {
if (filters.category) {
loadAgentsByCategory(filters.category);
}
}, [filters.category]);
return <AgentList agents={agents} loading={loading} />;
};
판단 기준:
- 페이지 초기화
- API 호출
- 전역 이벤트 리스너
- 페이지 레벨의 사이드 이펙트
2. List Component에서 사용해야 하는 경우
✅ 리스트 관련 사이드 이펙트
// List Component
const AgentList = ({ agents, onSelectionChange }) => {
const [selectedAgents, setSelectedAgents] = useState([]);
// 선택 변경 시 부모에게 알림
useEffect(() => {
onSelectionChange(selectedAgents);
}, [selectedAgents, onSelectionChange]);
// 스크롤 위치 저장
useEffect(() => {
const handleScroll = () => {
localStorage.setItem('agentListScrollPosition', window.scrollY.toString());
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div>
{agents.map(agent => (
<AgentListItem
key={agent.id}
agent={agent}
isSelected={selectedAgents.includes(agent.id)}
onToggle={() => toggleSelection(agent.id)}
/>
))}
</div>
);
};
판단 기준:
- 리스트 내부 상태 변경 시 부모에게 알림
- 리스트 관련 이벤트 리스너
- 리스트 레벨의 사이드 이펙트
3. ListItem Component에서 사용해야 하는 경우
✅ 개별 아이템 사이드 이펙트
// ListItem Component
const AgentListItem = ({ agent, isVisible }) => {
const [isLoaded, setIsLoaded] = useState(false);
// 아이템이 화면에 보일 때 로드
useEffect(() => {
if (isVisible && !isLoaded) {
loadAgentDetails(agent.id).then(() => {
setIsLoaded(true);
});
}
}, [isVisible, isLoaded, agent.id]);
// 아이템 언마운트 시 정리
useEffect(() => {
return () => {
// 정리 작업
cleanupAgentResources(agent.id);
};
}, [agent.id]);
return <AgentCard agent={agent} isLoaded={isLoaded} />;
};
판단 기준:
- 개별 아이템의 지연 로딩
- 아이템 관련 이벤트 리스너
- 아이템 언마운트 시 정리 작업
4. Button Component에서 사용해야 하는 경우
✅ 버튼 관련 사이드 이펙트
// Button Component
const ActionButton = ({ onClick, children }) => {
const [isPressed, setIsPressed] = useState(false);
// 키보드 이벤트 처리
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onClick();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClick]);
return (
<button
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onClick={onClick}
>
{children}
</button>
);
};
판단 기준:
- 버튼 관련 이벤트 리스너
- 버튼 상태 변경 시 사이드 이펙트
- 버튼 언마운트 시 정리 작업
'웹 개발 > 프론트엔드 ∕ React' 카테고리의 다른 글
| [React] 전역 상태 관리 vs 커스텀 훅: 언제 무엇을 사용해야 할까? (0) | 2025.10.22 |
|---|---|
| [React] 전역 상태 관리 라이브러리 도입과 변경되는 React 훅 사용 가이드 (0) | 2025.10.22 |
| [React] rc-tree 라이브러리를 통한 treeview 공통 컴포넌트 만들기 (0) | 2023.05.09 |
| [React] 개발 참고자료 목록 (0) | 2022.10.23 |
| [React] 기본 실습 (0) | 2022.06.21 |





최근댓글