포스트

redux를 도입하자

redux를 도입하자

redux를 도입하자..

여태까지 개발을 하며 사실 redux를 반드시 써야한다! 라는 경험은 사실 드물었다. 전역으로 상태 관리를 해야하는 경우 대부분 contextAPI로 해결되었기때문이다. 사실 사용하면서도 빈번한 렌더링때문에 걱정이 된 적도 있었으나ㅎㅎ.. 전역 상태 관리 로직을 모두 바꿀만큼의 크리티컬한 케이스를 만난적이 없기때문에 잘 버텨왔다… 그런데, 이번 프로젝트를 진행하며 각각 다른 위치에서 호출되는 A,B라는 컴포넌트가 있고 A컴포넌트에서 api를 호출하면 B컴포넌트에 로딩바를 띄워야 하는 상황이 되었다. 사실 context로 처리하면 매우 간단하나, 로딩상태는 어디에서나 사용될 수 있고 전역에서, 많은 컴포넌트에서 사용될 것이다. 이러한 경우 context보다는 redux로 상태를 관리하는것이 더 좋겠다 싶어 redux를 적용하려고 한다! 그리고 필요할때마다 다시 확인하기 위해 블로그에 기록을 남긴다

redux란?

우선 redux가 무엇인지 간단하게 정리하자. redux는 크로스 컴포넌트 또는 app wide 상태를 위한 상태 관리 시스템이다. 쉽게 전역에서 상태를 관리할수 있도록 도와주는 아이이다. 상태는 크게 3가지로 분리된다. 로컬, 크로스 컴포넌트 , 그리고 앱 와이드 로컬은 말 그대로 해당 컴포넌트 안에서만 관리되는 상태이다. 크로스 컴포넌트와 앱와이드는 그 범위는 다르지만 모두 다수의 컴포넌트에 영향을 미치는 상태를 가리킨다. 일반적으로 props chaining , drilling을 통해 구축이 가능하나 contextAPI, redux와 같은 라이브러리를 사용해서 관리할 수도 있다.

사실 context는 이미 react에 내장되어있다. 그렇다면 왜 굳이 redux를 설치해서 사용하는걸끼?

왜 redux?

context도 매우 유용한 기능이다. 그러나 context는 잠재적인 단점이 있다.

  1. 설정과 관리가 매우 복잡해질 수 있다. 작은 앱이라면 괜찮으나 규모가 큰 앱에서는 여러개의 컨텍스트가 사용될때, 과도한 중첩등 복잡한 구조가 될 수 있다.
  2. 성능이 떨어진다. 실제로 react팀원이 contextAPI에 대해 데이터가 너무 자주 바뀌는 경우 context가 적합하지 않다고 언급한적이 있다. context는 해당 하나의 저장소에서 단 하나의 상태라도 구독하고 있는 모든 컴포넌트가 재평가되도록 한다. 즉 다른 상태가 업데이트 되더라도 리렌더링이 발생한다. 반면 redux는 특정 상태를 구독하고 있는 컴포넌트만 리렌더링한다. (단 객체타입이 아닌경우)

사실 요즘 redux외에도 수많은 상태관리 라이브러리들이 등장하고 있다. 다른 라이브러리에 대해서도 공부가 많이 필요하다ㅠㅠ..

redux의 작동방식

저장소

사실 redux에는 많은 요소가 존재하기때문에 처음 redux를 배울때 가장 어려웠던점이 이 동작원리이다. redux는 하나의 중앙 데이터 저장소이다. 여기서 데이터는 상태를 가리킨다. 즉 redux는 하나의 상태 저장소이다. 모든 상태가 하나의 저장소에 저장된다.

데이터 읽기

우리는 이 저장소에서 상태를 가져다가 컴포넌트에서 사용할 수 있다. 정확히는 저장된 상태가 변경되면 컴포넌트에서 그것을 인지하고 컴포넌트를 재평가할 수 있다. 단, 컴포넌트가 저장소가 변경되었다는 것을 알기 위해서는 저장소를 구독해야하며 구독 후에 저장소는 데이터가 변경될때 컴포넌트에게 알려주고 컴포넌트는 변경된 상태를 전달받는다.

데이터 수정

단순히 상태를 전달받는것 외에도 상태를 업데이트하는 기능도 필요하다. 여기서 중요한것은 컴포넌트는 저장소를 직접적으로 수정할 수 없다. 즉 데이터는 [저장소 => 컴포넌트] 의 방향으로만 흐르며 [컴포넌트=>저장소]는 불가능하다. 대신 컴포넌트는 reducer라는 것을 사용해 상태를 수정을 요청할 수 있다. reducer함수는 상태를 변환하는 역할을 한다.

그렇다면 컴포넌트는 어떻게 reducer에서 수정을 요청할 수 있을까? 컴포넌트는 reducer에서 수정을 요청하기 위해 action을 사용한다. action을 reducer로 보내서 “이러이러한 action을 수행해줘!” 라고 전달하는 것이다. 여기서 전달이라는 개념은 redux에서 dispatch라는 용어로 사용된다. 즉 component가 action을 reducer로 dispatch하는 것이다.

이제 reducer가 action을 받게 되면 action에 담긴 type과 data에 따라 새로운 상태를 내뱉는다. 그리고 reducer가 내뱉은 이 상태는 그대로 store의 기존 상태를 대체한다. 이제 저장소의 상태가 업데이트 되었으니 이 저장소를 구독중인 컴포넌트는 자신이 요청한대로 변경된 상태를 전달받을 것이다.

기초 설정

우선 redux 라이브러리를 설치해야한다. react에서 redux를 조금 더 편하기 사용할 수 있도록 도와주는 라이브러리인 react-redux도 같이 설치한다.

1
2
npm install redux react-redux

이제 저장소를 만들 폴더를 지정한다

1
2
3
4
src 
└─store
    ├─ index.js
    └─ [reducer].js

우선 간단하게 위와 같은 구조로 작성하였다. action들을 모아두는 actions폴더를 따로 정의하기도 하지만 우선 reducer를 작성할때 참고하기 위해 같은 파일내에 작업할 예정이다.

store 만들기

가장 먼저 상태 저장소가 될 store를 생성한다. store은 redux의 createStore라는 메서드를 통해 생성이 가능하다. store을 생성하면 이 저장소를 구독할 컴포넌트들이 store에 접근할 수 있도록 해주어야하는데 이때 방식은 context와 유사하다. react-redux의 요소 provider로 store에 접근할 모든 하위 컴포넌트를 wrap함으로서 하위 컴포넌트들이 store에 접근할 수 있도록 해주어야 한다. 그리고 react-redux가 제공하는 Provider는 redux가 생성한 store에 대한 정보가 없으므로 속성값으로 이를 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Provider } from "react-redux";
import store from "./store/index";
function App() {
  return (
    <div className="App">
      <Provider store={store}>
        <Layout>
       		 ...
        </Layout>
      </Provider>
    </div>
  );
}

초기상태, reducer , action 생성자 만들기

다음으로 reducer와 action 생성자를 만든다. action 생성자는 단순하게 객체타입의 액션을 만들어주는 함수이다. 액션은 type이라는 속성을 갖는 일반 객체로서, 액션 생성자를 만들지 않고 바로 type속성을 갖는 객체를 dispatch하여도된다.

reducer는 store폴더 하위에 별도의 reducer.js파일을 만들어서 작성한다. store와 같은 파일에서 관리하여도 되지만 reducer가 여러개가 되는 경우 파일이 과도하게 길어질 수 있다. reducer는 자동으로 state와 action을 매개변수로 전달받는다. 또한 새로운 상태 객체를 반환해야한다.

초기상태는 api호출 상태를 관리할 것이므로 상태 객체에 status 속성과 error 메시지를 담을 속성을 지정한다.

초기상태

1
2
3
4
const initialStatusState = {
  status: "",
  error: null,
};

api호출 상태를 변경하는 액션을 생성하는 changeStatus 생성자를 생성한다 액션생성자

1
2
3
4
5
6
7
export function changeStatus(data) {
  return {
    type: "CHANGE_STATUS",
    data: data,
  };
}

리듀서 함수는 action을 받아 새로운 상태와 error를 반환하도록 한다.

리듀서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const statusReducer = (state = initialStatusState, action) => {
  switch (action?.type) {
    case SUCCESS:
      return {
        ...state,
        status: SUCCESS,
        error: null,
      };
    case ERROR:
      return {
        ...state,
        status: ERROR,
        error: action.data.error,
      };

    default:
      return state;
  }
};

reducer와 store를 모두 작성했다면 둘을 연결시켜야한다. store에 이 reducer에서 상태값을 가져와! 라고 연결을 해주는 과정이다.

이제 초기 설정은 완료되었다! 추가로 상태를 구독할 컴포넌트에서 store를 구독하고 action을 dispatch하도록 설정해야한다.

상태를 읽어오기

이제 컴포넌트에서 저장소를 구독하고 데이터를 가져올 차례이다. 사실 redux만 사용한다면 구독을 위해서는 subscribe라는 함수를 사용한다.

예시로 구독자 함수를 만들고 해당 구독자를 연결해보자.

1
2
3
4
5
6
const subscriber = () => {
  let latestState = store.getState();
	console.log("receive new State !", latestState);
};

store.subscribe(subscriber);

위와 같이 코드를 작성하면 redux는 subsciber라는 구독자 함수를 인식하게되고 저장소가 변경되었을때 해당 함수를 트리거한다. 그리고 구독자함수 내에서 getState() 메서드로 최신 상태를 전달 받을 수 있다.

그러나 우리는 react-redux를 사용할 것이기때문에 위와같은 로직을 사용할일이 드물다. subscribe()와 getState() 대신 react-redux가 제공하는 useSelect()훅을 사용할 수 있다.

useSelector 사용하기

1
2
3
import {useSelctor} from 'react-redux'

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

useSelector를 사용하면 자동으로 해당 컴포넌트가 redux store에 구독등록?이 된다. 따라서 store가 업데이트 되면 해당 컴포넌트가 자동으로 재실행된다. 또한 구독중인 store에서 최신상태값을 조회해올 수 있다. store.getState()와 동일한 기능을 하며 useSelector의 인수로 넘기는 콜백함수에 현재 store의 상태값이 매핑되고 그 상태값에서 원하는 속성을 꺼내기만 하면된다. 또한 컴포넌트가 unmount될때 구독은 자동으로 해지된다는것도 기억하자.

주의할것은 현재 reducer를 combineReducer로 결합했기때문에 특정 reducer에 속하는 상태값을 꺼내기 위해서는 combineReducer에 지정된 속성명을 거쳐야한다는 것이다. 위 코드에서 첫번째 status 속성이 그 역할을 한다.

useSelector를 사용할때 명심할 것은 가져오는 state값에 따라 렌더링여부가 결정된다는 것이다. 우리는 현재 state.status.status만을 가져오고 있으므로 state.status.status가 변경되었을때만 해당 컴포넌트가 재평가된다. 만약 state.status.error 가 변경되었다면 해당 컴포넌트는 재평가되지 않는다. 단

1
2
(state)=>({state : state.status.status });

와 같이 가져오려는 state를 객체로 감싼다면 react-redux는 객체안을 읽지못하고, 결국 가져오려는 값이 무엇인지 몰라 무조건 컴포넌트를 재렌더링 해버린다. 만약 state에서 두개이상의 상태값을 조회할때 하나의 상태값만 변경되는 케이스가 빈번하다면 객체타입을 피하는것이 좋을 것이다.

대신 useSelector을 여러번 사용하거나 react-redux의 shallowEqual 함수를 useSelector의 두번째 매개변수로 전달해주면 된다. 보다 자세한 내용은 useSelector최적화를 참고하는것을 추천한다.

### 상태 업데이트하기 마지막으로 상태 업데이트하기만 남았다. 상태를 업데이트하기 위해서는 컴포넌트내에서 action을 dispatch하면 된다. 진행순서는 다음과 같다.

  1. 액션생성자 함수를 import 한다.
  2. useDispatch() 훅으로 dispatch함수를 가져온다.
  3. 액션생성자함수를 실행시켜 dispatch로 전달한다.

실제 코드로 예시를 본다면,

1
2
3
4
5
6
7
8
9
  const dispatch = useDispatch();

  const submitSearchFormHandler = async (e) => {
    e.preventDefault();
    dispatch(changeStatus("PENDING"));
	...
    dispatch(changeStatus(error?"ERROR" : "SUCCESS");
  };
  

api를 호출하면 useDispatch()함수로 가져온 dispatch 에 changeStatus라는 액션생성자함수를 호출해 생송헌 액션을 전달한다. 해당 액션생성자 함수는 상태의 status속성을 업데이트하는 액션을 생성한다. 이제 reducer로 해당 action이 전달될 것이며 전달한 action에 맞추어 state가 업데이트된다. “PENDING”이라는 값을 전달했으므로 state.status는 PENDING 업데이트되며 해당 상태를 구독중인 모든 컴포넌트에 전달된다. api호출을 종료하면 결과값에 따라 “ERROR”, “SUCCESS”를 전달한다. 다시 state.status는 SUCCESS 혹은 ERROR로 업데이트되며 해당 상태를 구독중인 모든 컴포넌트에 전달된다.

정리하기

상태 업데이트까지 모두 완료되었다. 사실 redux에는 더 많은 기능이 있다. 또한 redux를 제대로 사용하기 위해서는 리덕스 미들웨어의 사용은 필수불가결하다고 할 수 있다. 앞으로 시간이 된다면 관련 내용도 블로그에 정리를 해볼 예정이다. 또한 redux를 편하게 사용하기 위한 toolkit도 존재한다! 사실 redux만 사용하기에는 reducer 코드가 너무 길어지거나, 기존 상태값을 수정하면 안된다는 규칙이 redux의 불편요소중 하나이다.. 다만 toolkit을 사용한다면 기존 createRedux, reducer 정의 방식이 모두 달라지므로 다시 redux를 공부하는 기분을 낼 수 있다ㅎㅎ.. 해당 내용도 나중에 기회가 되면 한번 정리해보고 싶다.

참고

React 완벽 가이드 with Redux, Next.js, TypeScript
벨로퍼트와 함께하는 모던 리액트

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.