들어가며

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>
    );
};

판단 기준:

  • 버튼 관련 이벤트 리스너
  • 버튼 상태 변경 시 사이드 이펙트
  • 버튼 언마운트 시 정리 작업

 

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