Redux 활용하기

Do Ho Kim·2021년 9월 12일
0

작년 네이버 커넥트재단에서 열린 부스트캠프에 Final Project 때 Redux를 사용했던 경험에 대해서 소개한 글입니다.

Redux를 사용한 이유

팀에서는 Atomic Design pattern을 도입하게 되면서 상태 관리를 어떻게 하면 좋을 지 많이 고민했습니다. 전체적인 상태관리나 렌더링 흐름을 잡아주는 무엇인가가 필요했는데요. 실제 교육과정 중 Context API나 useReducer와 같은 Hook API를 배웠지만 많이 사용되는 Redux나 React에 최적화되어있는 Recoil과 같은 상태 라이브러리에 대해서 도전해보고 싶었습니다. 여러 고민을 하다가 중앙집중적이게 Store를 관리해서 개발할 수 있는 Redux를 도입하게 되었습니다.

첫 Redux의 도입

Redux를 처음 도입을 하게 되면서 여러 사이트에서 어떤 구조가 좋을 지 확인을 하게 되었습니다. 그러던 중 react-redux를 도입하면서 Container + Presenter Pattern을 이용해서 props를 통해 data와 action을 전달할 수 있다는 점을 알게 되었습니다.

/* ChannelContainer.tsx */

import { connect } from 'react-redux';
import { Channel } from './Channel';

const mapDispatchToProps = (dispatch: any) => {
  return {
    chatroomClick(selectedChatroomId: number) {
      dispatch({ type: 'UPDATESIDEBAR', selectedChatroomId });
    }
  };
}

export default connect(null, mapDispatchToProps)(Channel);

react-redux의 connect 함수를 이용해 Channel을 감싸는 고차 컴포넌트를 만들어냈고 dispatch를 통해 reducer 안에 있는 action을 쉽게 실행할 수 있었습니다.

/* Channel.tsx */
const Channel: React.FC<ChannelProps> = ({ children, chatroomClick, chatroomId, isPrivate = false, isSelect = false, ...props }) => {
  const history = useHistory();
  const handlingClick = () => {
    if (window.location.pathname !== `/client/${chatroomId}`) history.push(`/client/${chatroomId}`);
    chatroomClick(chatroomId);
  };

  return (
    <ChannelContainter isSelect={isSelect} {...props} onClick={handlingClick}>
       ...
    </ChannelContainer>
  );
};

그래서 컴포넌트는 props를 통해 전달받아 해당 함수나 데이터를 사용할 수 있게 되었고 쉽게 변동시켜 디자인적인 요소를 분리할 수 있었습니다.

/* index.tsx */
import Channel from './Channel/ChannelContainer';
export { Channel };

index에서는 Channel 컴포넌트 대신 ChannelContainer 컴포넌트를 두어 Channel의 이름으로 다른 파일에서 사용가능하도록 하였습니다.

불필요한 코드 생성을 고민하다

위의 방식대로 구현을 하면서 계속적으로 container를 생성해주고 불필요한 코드가 많이 생기는 것 같다는 생각이 들었습니다. 이러한 고민을 동료들의 피어리뷰를 받으면서 react-redux 라이브러리에 제공 hook인 useSelectoruseDispatch가 있다는 것을 알게 되었습니다. 직관적으로 컴포넌트에서 쉽게 store에 저장된 값을 가져오고 dispatch 시킬 수 있다는 점은 굉장히 매력적이었습니다.

/* Channel.tsx */
import { useDispatch } from 'react-redux';
import { pickChannel } from '@store/actions/chatroom-action';

const Channel: React.FC<ChannelProps> = ({ children, chatroomId, isPrivate = false, isSelect = false, ...props }) => {
  const history = useHistory();
  const dispatch = useDispatch();

  const handlingClick = () => {
    if (window.location.pathname !== `/client/${chatroomId}`) history.push(`/client/${chatroomId}`);
    dispatch(pickChannel({ selectedChatroomId: chatroomId }));
  };

	return (
	    <ChannelContainter isSelect={isSelect} {...props} onClick={handlingClick}>
	       ...
	    </ChannelContainer>
	  );
};

그래서 기존의 Container 코드를 과감하게 삭제하고 useDispatch와 action에 대한 2줄로 표현이 가능했습니다. 이를 통해 훨씬 가독성있는 코드를 완성할 수 있게 되었습니다. 또한 불필요한 파일(Connect를 이용한 File) 생성을 하지 않게 되었습니다.

Redux-saga 도입 결정

Hook 기반 코드로 Refactoring을 하던 도중 API 호출에 관한 부분이 비동기적으로 처리가 되지 않는 문제가 발생하였습니다. 어떠한 문제일 지 생각을 해보면서 일반적으로 reducer와 같은 부분은 순수함수로 데이터를 처리하기 때문에 redux의 구조에서 이러한 부분이 일어나는 것은 당연하다고 생각했습니다.

그래서 리덕스의 기본 구조는 지켜가면서 중간에 API 호출과 비동기 로직을 관리해주는 Redux-saga를 도입하게 되었습니다.

Saga는 다음과 같이 적용시킬 수 있습니다.

/* store/index.ts */
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { rootReducer } from './reducers';
import { rootSaga } from './sagas';

const sagaMiddleware = createSagaMiddleware();

export default createStore(rootReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(rootSaga);

redux에 있는 applyMiddleware를 이용해 store를 생성할 때 선언을 해주게 됩니다.

실제 작성한 saga 파일은 다음과 같습니다.

/* chatroom-saga.ts */
import { call, put, takeEvery } from 'redux-saga/effects';
import API from '@utils/api';
import { PICK_CHANNEL, PICK_CHANNEL_ASYNC } from '@store/types/chatroom-types';

function* pickChannelSaga(action: any) {
  const { selectedChatroomId } = action.payload;

  try {
    const chatroom = yield call(API.getChatroom, selectedChatroomId);
    yield put({
      type: PICK_CHANNEL,
      payload: { chatroom, selectedChatroomId }
    });
  } catch (e) {
    console.log(e);
  }
}

export function* chatroomSaga() {
  yield takeEvery(PICK_CHANNEL_ASYNC, pickChannelSaga);
}

채널을 선택하는 과정에서 chatroom 정보를 불러오는 과정에 API 호출과 같은 비동기 로직이 들어가게 됩니다. 이러한 점을 Saga에서 chatroom에 대한 정보를 call 함수를 통해 받아오고 put을 통해 reducer로 action과 payload를 전달하게 됩니다.

폴더 구조 정리

같이 개발을 해나가가면서 폴더 구조에 대해서 잡는 것은 당연한 부분이라고 생각했습니다. 그래서 프로젝트 초기에 어떻게 Store에 대한 구조를 잡는 것이 좋을 지 생각을 하게 되었고 향후 코드가 많아졌을 때 유지보수가 편하도록 구조를 고민하게 되었습니다.

image

actions : Component에서 쓰이는 함수를 관리하는 폴더
reducers : 상태와 액션에 대한 정의를 하는 폴더
sagas : saga에 대한 처리를 해주는 폴더
types : Store에서 쓰이는 타입을 관리하는 폴더

이렇게 관리하면서 좋았던 점은 폴더 별로 성격을 뚜렷하게 가져갈 수 있었다는 점이었습니다. 다른 컴포넌트로 하여금 참조하는 곳은 actions에 있는 함수뿐이었고 dispatch를 통해 쉽게 상태를 변경시킬 수 있었습니다. 도입은 하지 못했지만 테스팅에도 굉장히 용이하게 분리를 했기 때문에 쉬운 테스팅이 가능했을 것이라고 생각했습니다.

느낀 점

처음 러닝커브가 높다던 Redux의 도전을 하는 것은 굉장히 저에게는 큰 도전이었습니다. 3~4일 내내 Redux에 대한 정보를 계속 찾아다니고 적용해보고 하면서 조금이나마 저에게 React에 Redux를 어떻게 적용시키는 것이 좋을 지 고민하게 되었고 다른 사람과 협업을 위한 구조을 어떻게 만드는 것이 좋을 지 고민하게 되었습니다.

해당 구조를 짜면서 성공한 순간 정말 짜릿해서 팀원들을 부른 뒤 기분 좋은 소리를 계속했던 것 같습니다. 한 팀원분이 말하길 그때 기분이 정말 좋아보였다는 말을 할 정도로 웃음이 떠나지 않았던 것 같습니다.

같이 프론트 작업을 하는 팀원에게 해당 구조를 설명해주게 되는 순간이 있었습니다. 팀원 분은 구조에 대해서 이해하시고 빠른 시일 내에 같이 프로젝트의 상태 관리를 할 수 있게 되었고 그 모습에 정말 뿌듯했던 것 같습니다.

마치며

이번 프로젝트를 통해 Redux에 대한 도전은 성공적이었다고 생각합니다. 전체적인 상태관리를 통해 여러 컴포넌트에서 쉽게 상태를 변경하거나 가져올 수 있었고 단순히 props를 내려서 개발하는 것보다 100배 좋은 코드를 만들어낸 것 같아 좋았다고 생각합니다.

앞으로의 프로젝트에서도 상태 관리에 대해서 여러 라이브러리를 만나게 된다면 두려워하지않고 적용을 할 수 있겠다는 자신감을 얻었고 팀원을 위하는 구조를 짜는 방법에 대해서 충분한 고민을 거치는 힘을 얻었습니다.

profile
Front-End Developer

0개의 댓글