[prop drilling 해소] - useContext #리액트 2차 개인과제 리팩토링

간단한 과제 소개

  • 포켓몬 데이터 목록을 이용하여 나만의 포켓몬을 추가/삭제할 수 있다. 
  • 포켓몬 상세 정보를 확인할 수 있다.

 


💡 prop drilling

컴포넌트의 위계가 깊어짐에 따라 props가 불필요하게 많아지는걸 의미한다. 대충 하위 컴포넌트가 생겨남에 따라 어쩔 수없이 props를 전달'만'하는 컴포넌트가 많아지는 경우를 생각하면 편하다. 이번에 진행한 과제에서 context나 redux를 사용하지 않고 구현을 했더니 간단한 프로젝트임에도 꽤나 거슬릴정도로 props를 계속 전달해주어야 했다. 

 

나는 위의 사진으로 보이는 UI를 다음 컴포넌트들과 같이 구성했다.

  • `Dashboard` : 상단에 나만의 포켓몬과 추가한 포켓몬 카드들이 있는 컴포넌트
  • `PokemonList` : 하단에 전체 포켓몬 목록이 있는 컴포넌트
  • `PokemonCard` : 각 포켓몬의 정보를 확인할 수 있는 컴포넌트
  • `Button` : 포켓몬을 추가/삭제할 수 있는 버튼 컴포넌트

그림으로 보면 다음과 같다. (발퀄 ㅈㅅ)

 

📍 기존 props 전달 방식

페이지의 기본 동작을 아주 간단하게 설명해보자면 `PokemonList` 컴포넌트에서 반복하며 `PokemonCard` 를 호출한다. 각각의 `PokemonCard` 는 `Button` 컴포넌트를 갖고 있다. 그러면 데이터의 출력이 어떤 식으로 이루어져있는지 살펴보자. (코드는 최소화하고 순서나 로직등에 대해서만 설명함)

 

 

`페이지(Dex.jsx)`에서 리스트 출력

 

Dex (리스트 페이지 컴포넌트)

  1. `Dex` 에서 `pokemonList(전체 포켓몬 리스트)`,  `addPokemon(추가 핸들러 함수)`, `text(버튼에 보여질 텍스트)` 를 `PokemonList`에넘겨준다. 

PokemonList (포켓몬 리스트 컴포넌트)

  1. props로 전달받은 `pokemonList` 배열을 순회하며 `PokemonCard`를 호출한다. 
  2. map의 첫 번째 인자 `pokemon`객체 데이터, `addPokemon`, `text`를 전달한다. (여기서 `addPokemon`과 `text`는 부모에게 받아서 전달만 해줌)

PokemonCard (각 포켓몬 카드 컴포넌트)

  1. props로 전달받은 `pokemon`데이터를 가지고 UI를 구성한다.
  2. `addPokemon`과 `text`를 `Button`에 전달한다. 

Button (버튼 컴포넌트)

  1. props로 전달받은 `addPokemon`로 추가 이벤트를 호출하고 `text`를 보여준다.

 

📍 문제점

`Dex`페이지에서 위의 3개의 데이터 (`pokemonList`, `addPokemon`, `text`)를 전달할 필요가 없다.

전달이 필요한 가장 가까운 상위 컴포넌트에서 `context`값을 꺼내 전달해주면 좋을 것 같다.

 

그리고 위에서는 설명하진 않았지만 `addPokemon`과 `removePokemon`과 같이 로직이 들어간 함수 같은것도 굳이 `Dex` 페이지에서 정의하지 않아도 좋을 것 같다. (깔끔하게 컴포넌트를 사용하기 위함)

 

💡 변경

일반 state값은 물론, 함수나 로직도 전부 `context` 로 이동시켰다. 

import { createContext, useContext, useState } from "react";

const PokemonContext = createContext();

export function usePokemon() {
  return useContext(PokemonContext);
}

export function PokemonProvider({ children }) {
  const [selectedPokemon, setSelectedPokemon] = useState([]);
  const [allPokemonList] = useState(() => {
    /* 어쩌구 저쩌구 포켓몬 전체 목록데이터 불러오는 로직 */
  });

  const addPokemon = (pokemonId) => {
   /* 어쩌구 저쩌구 추가 로직*/
   setSelectedPokemon([...selectedPokemon, pokemonId]);
  };

  const removePokemon = (pokemonId) => {
    /** 어쩌구 저쩌구 삭제 로직*/
    setSelectedPokemon([...removeArray]);
  };

  return (
    <PokemonContext.Provider
      value={{ allPokemonList, selectedPokemon, addPokemon, removePokemon }}
    >
      {children}
    </PokemonContext.Provider>
  );
}

원래 `Dex`가 갖고있던 state나 함수들을 전부 옮겼다. 드디어 `Dex`는 페이지로서의 기능만 충실하게 수행할 수 있게 되었다. 추가적으로 다른 곳에서 `context`를 호출할 때 편하게 쓸 수 있도록 구성되어있다. 

 

원래 호출해서 쓰려면.. 

 

(1)

`PokemonContext`와 `useContext`를 import해야한다. `useContext(PokemonContext)` 뭐 이런식으로.. 

하지만 커스텀 훅을 사용하여 완성본(?)을 return하면 이렇게 쓰면 된다. `usePokemon()`

 

(2) 

원래 `context` 영역을 잡고 싶으면 `Dex`에서 `<PokemonContext.Provider value={{....}}>` 이런 식으로 써야한다. 

하지만 위의 코드처럼 컴포넌트를 리턴할 때 값을 주입한 상태로 return을 해주게 되면 `Dex`에서 `<PokemonProvider>` 만 깔끔하게 사용해주면 된다. 

 

📍 변경 목적

  • 불필요한 props의 전달을 없애려고 context를 사용했다. 
  • 페이지나 컴포넌트에 최소한의 비즈니스 로직을 사용하는게 좋다. (UI위주의 로직으로 구성하는게 좋다. 깔끔!)
  • 프로젝트의 규모가 커질수록 유지보수에서 큰 장점을 보인다. (prop drilling 언제 다 디버깅해요..)

 

댓글

Designed by JB FACTORY