Props Drilling 프로젝트를 Redux로

상현·2023년 11월 15일
2

내일배움캠프

목록 보기
3/8
post-thumbnail

개인 과제 중 하나의 과제를 Props Drilling하게 먼저 작성하고, 완성이 되면 Context API를 활용해 전역 상태를 관리하도록 하고, 그 후엔 Redux를 활용하여 상태를 관리하도록 바꾸는 것으로 진행 하였다.

Props Drilling ➡️ Context API ➡️ Redux

과제 개요

하나의 그룹 아티스트를 선택하고, 그룹 멤버들에게 팬레터를 남길 수 있도록 하는 과제를 진행하였다.
필수 구현 기능은 다음과 같다.

  • 팬레터 CRUD 구현 (작성, 조회, 수정, 삭제)
  • 아티스트별 게시물 조회 기능 구현
  • 원하는 아티스트에게 팬레터 등록 구현
  • 팬레터 상세 화면 구현
  • 상세화면에서 팬레터 내용 수정 구현
  • 상세화면에서 팬레터 삭제 구현

나는 아이돌에 대해 잘 모르기에.. 드래곤볼 캐릭터들을 선정해서 진행했다.

Props Drilling

우선 Context도, 어떤 상태 관리 라이브러리도 사용하지 않았을 경우다. 내가 사용한 state는 모두 전역적으로 사용되기 때문에 하나의 부모 컴포넌트에서 모두 선언한 뒤 아래로 내려줄 수 밖에 없었다.

생략된 컴포넌트가 많지만 그냥 보기에도 단순히 하위 컴포넌트에서 사용하기 위해 쓸데없는 단계를 거치고 있다.

문제는 이것만이 아니다.

단순히 알람창을 띄우는 것인데 상관 없는 모든 컴포넌트가 리렌더링 된다. 왜냐면 알람을 띄우기 위한 state가 가장 상단 컴포넌트에 위치하기 때문이다.

Context API

이를 조금이라도 개선하기 위하여 리액트가 기본적으로 제공하는 Context를 사용해보자.
전역적으로 사용되는 state를 크게 letter state, modal state, alert state 3가지로 나눌 수 있다.

따라서 우선 다음과 같이 Context를 설정하는 파일 3개를 생성 했다.

각 파일은 다음과 같이 되어 있다.

letter-context.js

export const LetterProvider = ({children}) => {
  const data = localStorage.getItem('letters');
  const letterState = useState(JSON.parse(data) || initLetters);

  const actions = useMemo(
    () => ({
      add(name, from, content) {
        letterState[1](prev => {
          let newLetters = [...prev];
          newLetters.push(new Letter(name, from, content));
          localStorage.setItem('letters', JSON.stringify(newLetters));
          return newLetters;
        });
      },
      remove(id) {
        letterState[1](prev => {
          let findIndex = prev.findIndex(v => v.id === id);
          let newLetters = [...prev];
          newLetters.splice(findIndex, 1);
          localStorage.setItem('letters', JSON.stringify(newLetters));
          return newLetters;
        });
      },
      modify(id, content) {
        letterState[1](prev => {
          let findIndex = prev.findIndex(v => v.id === id);
          let newLetter = {...prev[findIndex], ...{content: content}};
          let newLetters = [...prev];
          newLetters.splice(findIndex, 1, newLetter);
          localStorage.setItem('letters', JSON.stringify(newLetters));
          return newLetters;
        });
      },
    }),
    [],
  );

  return (
    <LetterActionContext.Provider value={actions}>
      <LetterContext.Provider value={letterState}>{children}</LetterContext.Provider>
    </LetterActionContext.Provider>
  );
};

export const useLetterState = () => {
  const value = useContext(LetterContext);
  if (value === undefined) {
    throw new Error('letter context is null');
  }
  return value;
};

export const useLetterActions = () => {
  const value = useContext(LetterActionContext);
  if (value === undefined) {
    throw new Error('letter action context is null');
  }
  return value;
};

단순히 state를 가지는 Contextstate를 조작하는 action을 담는 Context 2가지를 만들고, 하나의 Provider에 담아서 넘길 수 있도록 하고, Provider에서 생성한 state를 사용할 수 있도록 custom hook을 만들었다.

이러한 작업을 통해 각 컴포넌트에서는 다음과 같이 사용할 수 있다.

Home.jsx

const Home = () => {
  return (
    <>
      {/* 1.  Z전사 나열 */}
      <CharacterContainer />
      {/* 2. 응원 메시지 나열  */}
      <LetterProvider> // custom Provider 사용
        <AllLetterContainer />
      </LetterProvider>
    </>
  );
};

AllLetterContainer.jsx

const AllLetterContainer = () => {
  const [letters] = useLetterState(); // custom hook 사용

  return (
    <AllLetterSection>
      <LetterContainer>
        {letters.map(letter => {
          return <LetterRow key={letter.id} letter={letter} />;
        })}
      </LetterContainer>
    </AllLetterSection>
  );
};

따라서 Context를 통해 다음과 같이 Props Drilling 현상을 막을 수 있게 됐다.

문제점

Props Drilling 현상은 막았지만, 여전히 문제점은 존재한다. Provider가 내려주는 state에 변화가 생기면 Provider가 감싸고 있는 모든 컴포넌트가 리렌더링 된다.

따라서 여전히 알람창을 띄웠을 뿐인데 상관 없는 컴포넌트들이 리렌더링 되고 있다..

Redux

이제 마지막으로 Redux로 전환해보자.
Redux 사용을 위해 다음과 같은 폴더와 파일들을 추가 했다.

store.js

const rootReducer = combineReducers({letters, modal, customAlert});

const store = createStore(rootReducer);

export default store;

letters.js

const data = localStorage.getItem('letters');
const initialState = JSON.parse(data) || initLetters;

// action values
const ADD = 'letters/add';
const REMOVE = 'letters/remove';
const UPDATE = 'letters/update';

// action creator
export const addLetter = (name, content, from) => {
  return {
    type: ADD,
    name: name,
    content: content,
    from: from,
  };
};

export const removeLetter = id => {
  return {
    type: REMOVE,
    id: id,
  };
};

export const updateLetter = (id, content) => {
  return {
    type: UPDATE,
    id: id,
    content: content,
  };
};

// reducer: 'state에 변화를 일으키는' 함수
// input: state와 action
const letters = (state = initialState, action) => {
  let findIndex = action.id && state.findIndex(v => v.id === action.id);
  let newLetters = [...state];

  switch (action.type) {
    case ADD:
      newLetters.push(new Letter(action.name, action.from, action.content));
      localStorage.setItem('letters', JSON.stringify(newLetters));
      return newLetters;
    case REMOVE:
      newLetters.splice(findIndex, 1);
      localStorage.setItem('letters', JSON.stringify(newLetters));
      return newLetters;
    case UPDATE:
      let newLetter = {...state[findIndex], ...{content: action.content}};
      newLetters.splice(findIndex, 1, newLetter);
      localStorage.setItem('letters', JSON.stringify(newLetters));
      return newLetters;
    default:
      return state;
  }
};

export default letters;

Redux를 사용했더니 Props Drilling과, Proivder로 감싸주는 태그까지 전부 사라져 훨씬 깔끔해졌다.

리렌더링도 마찬가지다.

상관 없는 컴포넌트들이 더 이상 리렌더링 되지 않는다..!!


이렇게 순차적으로 발전을 시켜보니 각각의 장단점과 사용법에 대해서 한번 더 익힐 수 있었다..!!

profile
프론트엔드 개발자

0개의 댓글