티스토리 뷰

끝이 없는 프로젝트 리팩토링.... 분명 기능 구현은 끝났는데 처리할 부분이 왤케 많은지....

 

이렇게 UI에서 변경되는 부분이 없음에도 불구하고 리렌더링되는 것이 참으로 마음에 걸렸습니다.

여기에 꽂혀서 3일동안 고민한건 비밀...

🛑 불필요한 카드 리렌더링

처음에는 PokemonCard 컴포넌트 내에서 모든 이벤트 처리 로직을 작성했었습니다.

const PokemonCard = ({ item = {} }) => {
    const dispatch = useDispatch();

    const myPokemon = useSelector((state) => state.myPokemon);
    
    const handelGoDetail = () => {
        if (Object.keys(item).length === 0) {
            return;
        }
        navigate(`/pokemon-detail?id=${item.id}`);
    };

    const handleAddBtn = (e, item) => {
        e.stopPropagation();
        if (myPokemon.length >= 6) {
            toast.error('최대 6마리까지 등록할 수 있습니다.');
            return;
        }
        dispatch(addPokemon({ pokemon: item }));
        toast.info(`${item.korean_name} 등록 완료`);
    };

    const handleRemoveBtn = (e, item) => {
        e.stopPropagation();
        dispatch(removePokemon({ id: item.id }));
        toast.info(`${item.korean_name} 삭제 완료`);
    };

    const isSelected = myPokemon.some((pokemon) => pokemon.id === item.id);

    return (
        <StPokemonCard onClick={handelGoDetail}>
            {isSelected ? (
                            <StPokemonBtn $props={'remove'} onClick={(e) => handleRemoveBtn(e, item)}>
                                삭제
                            </StPokemonBtn>
                        ) : (
                            <StPokemonBtn $props={'add'} onClick={(e) => handleAddBtn(e, item)}>
                                등록
                            </StPokemonBtn>
            )}
        </StPokemonCard>
    );
};

 

위와 같이 추가나 삭제 버튼을 누를 경우 redux-toolkit으로 관리하는 myPokemon의 state가 변경되므로 카드 컴포넌트들이 무조건 리렌더링 되는 문제가 발생했습니다.

 

 

✅  상위 컴포넌트에서 이벤트 위임을 통한 이벤트 로직 처리

<StPokemonList onClick={handelClickPokemonCard}>
    {MOCK_DATA.map((item) => (
        <PokemonCard key={item.id} item={item} isSelected={isSelected(item)} />
    ))}
</StPokemonList>

 

const myPokemon = useSelector((state) => state.myPokemon);

const handelClickPokemonCard = (e) => {
    const itemId = e.target.closest('[data-id]').getAttribute('data-id');
    const item = MOCK_DATA.find((pokemon) => pokemon.id === Number(itemId));

    if (!item) return; 

    const actionType = e.target.getAttribute('data-type');

    if (actionType === 'add') {
        if (myPokemon.length >= 6) {
            toast.error('최대 6마리까지 등록할 수 있습니다.');
            return;
        }
        dispatch(addPokemon({ pokemon: item }));
        toast.info(`${item.korean_name} 등록 완료`);
    } else if (actionType === 'remove') {
        dispatch(removePokemon({ id: item.id }));
        toast.info(`${item.korean_name} 삭제 완료`);
    } else {
        navigate(`/pokemon-detail?id=${item.id}`);
    }
}

const isSelected = (item) => myPokemon.some((pokemon) => pokemon.id === item.id);

 

위와 같이 PokemonCard를 묶고 있던 상위 컴포넌트인 PokemonList로의 이벤트 위임을 통해 해결했습니다.

이제 PokemonCard는 단순히 props를 전달 받아 전달된 데이터를 표현해주는 컴포넌트로 변경되었습니다.

PokemonCard를 memo를 이용하여 묶게 되면 리렌더링은 상위 컴포넌트인 PokemonList에서만 일어나고 PokemonCard로 전달된 props에 변화가 없을 경우 PokemonCard는 불필요한 렌더링이 일어나지 않게 됩니다.

 

 

 

리렌더링을 최적화하는 과정에서, 이벤트 위임을 통해 코드의 중복을 줄이고 성능을 향상시킬 수 있었습니다. 그럼에도 불구하고, 가독성 측면에서는 복잡도가 다소 증가하는 것 같아 조금 아쉬운 점도 있었습니다.

✅ 장점

  • 리렌더링 최적화 : 이벤트를 상위 컴포넌트에서 처리하도록 하여, PokemonCard 컴포넌트의 불필요한 리렌더링을 방지할 수 있었습니다. 이제는 UI 업데이트가 필요한 부분만 리렌더링되어 성능이 좋아졌습니다.
  • 코드 재사용성: 이벤트 처리 로직을 상위 컴포넌트로 올리고 커스텀 훅으로 분리함으로써 코드가 더 간결해지고 재사용성이 높아졌습니다. 같은 로직을 Dashboard와 List에서 모두 사용할 수 있게 되었습니다.

🛑 단점

  • 가독성 : 이벤트 위임을 적용한 덕분에 중복 코드는 줄어들었지만, 상위 컴포넌트의 책임이 늘어나 코드가 길어지고 복잡해지는 경향이 있었습니다. 특히, 이벤트 위임을 처리하는 로직이 길어지면서 가독성이 조금 떨어진 것 같습니다.

뭐 각각의 장단점이 있어 어느정도의 타협을 통해 둘 중에 하나를 선택해야 할 것 같습니다~

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함