이 글은 김민준(velopert)님의 리액트를 다루는 기술을 참조하였습니다.
목차
1. What is Redux?
2. React-Redux VS Context API
1. What is Redux?
리액트는 아무래도 프레임 워크가 아니다 보니 너무 자유롭다는 느낌이 들 때가 있다.
자유롭다는 것은 단순히 좋은 의미로 들릴 수도 있지만, 개발자의 숙련도에 전적으로 의존하게 된다.
초심자 입장에선 어떻게 사용해야 할지는 커녕, 뭐가 뭔지 이해조차 불가능한 거대한 장벽에 가로막히게 된다.
따라서 리액트 관련 포스팅은 항상 구조와 원리를 먼저 파악하고 설명하는 것이 중요하다고 생각한다.
지금까지 내가 작성한 포스팅을 읽어본 사람이라면 useState에 의해 관리되는 Local State의 단점을 이미 알고 있을 것이다.
state가 됐건, 함수가 됐건 어쨌든 속해있는 컴포넌트가 소멸되면 state 정보도 같이 사라진다.
또한, 공통적으로 쓰여야 할 데이터가 흩어져 있기 때문에 이를 공유하기 위해서는 부모·자식 컴포넌트 간에 props와 state로 전달해야 하는데 생각을 해보자.
애초에 리액트는 대규모 플랫폼을 위한 라이브러리인데, 수많은 컴포넌트 간에 props와 state만으로 상태 정보를 공유하려면 데이터의 흐름을 추적하는 것이 굉장히 까다로워진다.
심지어 LCA가 root인 경우에는 얼마나 불필요한 작업들이 수반될 수 있겠는가?
(이처럼 상위 컴포넌트 state를 목표 하위 컴포넌트로 props로 전달하는 행위를 state drilling이라고 한다.)
이런 다양한 문제점들을 타개할 해결책이 필요했고, 그 결론이 바로 일부 상태와 로직들을 전역으로 떼어놓는 것이었다.
리덕스가 리액트만을 위한 라이브러리는 아니다.
Angular, jQuery, vanilla JavaScript 등의 다양한 프레임워크와도 작동될 수 있도록 설계되어진 자바스크립트 앱 상태 관리 라이브러리다.
리덕스는 FLUX 구조를 기반으로 생성되어 단방향으로 동작한다. (완전히 동일하지는 않다.)
action이 발생하면 dispatcher에 의해 변경된 state가 store에 저장되어 view가 변화하는 일련의 흐름을 갖는다.
❗Redux VS Flux
리덕스가 Flux를 기반으로 만들어졌지만 분명한 차이점들이 존재한다.
- 리덕스에는 dispatcher라는 개념이 조금 다르다. state는 read-only이기 때문에 전달만 한다.
- 단일 store, 단일 root 만이 존재한다. (여러 store을 만들 수는 있지만, 관리가 힘들어진다.)
- 불변성을 위해 리듀서는 순수한 함수여야 한다.
위의 3가지는 Redux의 3가지 원칙이기도 하다. Flux와의 차이가 곧 원칙이었던 것이다.
리덕스의 구조와 원리를 간단하게 그러보면 다음과 같다.
Action 객체가 Dispatch 메서드에 전달되고, Dispatch는 Reducer를 호출하여 새로운 State를 생성한다. (불변성)
Action
어떤(What) state가 어떻게(How) 업데이트 되어야 하는지 정의한다.
트리거에 의해 state 변경이 필요하다고 판단되면 dispatch(action) 형태로 action 객체를 발생시킨다.
const INCREASE = "INCREASE" // action의 type 지정
export const increase = () => ({ type: INCREASE, payload, }); // action creators
export를 붙여줌으로써 다른 컴포넌트에서 쉽게 action 발생시킬 수 있게 된다. (필수는 아니다.)
Reducer
리듀서는 이전 상태 정보(state)와 액션 객체(action)를 인자로 받는다.
그렇게 받아온 데이터들로 새로운 state를 만들어 리턴하면 render가 발생하여 화면이 바뀐다.
이 때, state가 객체나 배열인 경우, 불변성 유지를 위해 스프레드 연산자나 concat 메서드를 사용한다.
useReducer와는 달리 switch문의 default 부분에서 에러가 아닌, state를 그대로 리턴시킨다.
const counter = (state = initialState, action) { // initialState가 객체라고 가정
switch (action.type) {
case INCREASE:
return {
...state,
someting: action.payload
}
default:
return state
}
}
나중에는 reducer들을 하나의 Root reducer로 합쳐주어야 해서 modules/index.js에 만들어준다.
import { combineReducers } from 'redux';
import something1 from './something1';
import something2 from './something2';
const rootReducer = combineReducers({
something1,
something2
});
export default rootReducer;
Store
어플리케이션 상태가 보관된다. 폴더 구성을 어떻게 하느냐에 따라 이야기가 조금 달라진다.
공식적으로는 actions, constants, reducers 디렉토리를 모두 분리하지만 새로운 액션을 만들 때마다
파일을 3개씩 수정해야 하는 불편함 때문에 Ducks 패턴을 기준으로 이야기 하겠다.
Ducks 패턴은 그냥 modules 디렉토리에 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 하나의 파일에 몰아 넣어버린다.
중요한 것은 어떤 패턴을 채택하건 간에 모든 것은 하나의 스토어에서 관리되어야 한다는 것이다.
스토어 안에는 state와 reducer, 여러 내장 함수들이 들어있다.
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root');
);
useSelector
import { useSelector } from "react-redux";
const {
counter: { sourceValue, sinkValue },
} = useSelector((state) => state);
store를 사용하려는 컴포넌트에서 useSelector을 이용하여 객체를 꺼내올 수 있다.
대충 이런 흐름이 발생하게 된다.
2. React-Redux VS Context API
다음으로 넘어가기 전에..우선 여기까지 오면 의문이 생긴다.
Context API도 전역 상태 관리를 해주고 Redux도 동일하다면 둘의 차이란 없는 것인가?
그렇지 않다. 둘은 엄연히 다른 목적을 가지고 있다.
Context는 상태관리 도구보다는 props drilling을 회피하기 위한 파이프를 만들기 위한 목적에 가깝다.
상태를 관리한다?
1. initialState를 저장한다
2. current State를 읽을 수 있다.
3. state를 Update 할 수 있다.
Context로도 구현할 수는 있다. 하지만 정확히는 useState와 useReducer hook이 있기 때문에 가능한 것이지
Context API 자체만으로 상태 관리가 가능하다고 보기는 힘들다.
그렇다고 해서 useReducer와 React-Redux가 서로 동일하지도 않다. 둘 또한 많은 기능과 동작의 차이가 있다.
- Context는 현재 state를 전달하는 반면, React-Redux는 현재 리덕스 Store 인스턴스를 전달한다.
- useReducer로 state를 생성하면 Context 내부의 일부 데이터만 필요한 컴포넌트도 강제로 모두 리렌더링된다. 하지만 React-Redux는 저장소의 특정 부분만 사용할 수 있다.
- Context는 React 환경에 의존한다. React-Redux는 UI에 독립적이다.
- React DevTools는 현재 state 확인만 가능하지만, Redux Devtools는 전달된 action, payload, 처리 이후 상태 등 시간에 따른 변화를 확인할 수 있다.
- Redux Devtools는 미들웨어가 존재한다.
즉, Context API는 규모가 작고 정적인 state의 전달에 주로 쓰고 상태 전파를 구현하기에는 성능 이슈가 발생한다.
물론 이것 또한 React.memo와 useMemo를 사용하여 커버할 수 있지만, 그럴 바엔 리덕스를 쓰는게..?