React를 이제 막 시작한 사람들은 Redux의 두려움이 크다. 하지만 컨셉은 그리 어렵지 않다. Redux는 application 전체의 상태(state)관리하기 위한 library이다.
React는 부모 컴포넌트에서 자식 컴포넌트에 props로 data나 state 변화를 넘겨주기 쉽다. 자식이 부모에게 넘어주는 것도 그리 어렵지 않다. 그런데 사돈의 팔촌이나, 저~~ 멀리 있는 컴포넌트에서 서로의 state 변화를 감지하려면 상당히 복잡해지기 시작한다.
이를 해결하기위해 어떻게 하면 좋을까? window 글로벌 객체에서 관리할까? 브라우저 storage에서? 이렇게 하면 state가 변경되더라도, 알고 싶은 컴포넌트에서 변경된 것을 감지하지 못한다. 아니면 최상단 컴포넌트에서 state를 관리하게 하고 자식의 자식의 자식에 넘겨주면 될까? 만약 depth가 너무 깊을 경우 전달하고-전달하고-전달하고.. 너무 복잡해진다.
이를 해결할 수 있는 것이 Flux pattern이다. Flux는 Facebook에서 개발한 data flow를 위한 architecture이다. pattern이라고 불러도 좋다. Redux, mobX같은 것은 Flux 아키텍쳐를 구현한 라이브러리라고 생각하면 된다. 혹은 react의 context api로도 해결 가능하며, 요즘에(19년도) redux대신 context api로 대체하는 프로젝트도 종종 보인다.
Flux data flow를 살펴보자.
Application의 전체 state는 store라고 불리는 곳에서 관리된다. store는 redux의 상태값(state)를 갖는 객체이다. action은 state 변화를 일으킬 수 있는 하나의 현상이다. action을 dispatch(발행)해서 store에 저장하고, state가 변경되면 view에서 감지하게 된다.
action은 사용자가 일으키는 이벤트라고 생각해도 되는데, 위의 그림과 같이 view단에서 action이 일어날 수도 있다. view에서 action이 일어나면 -> 다시 dispatcher에 의해 store에 저장되고 -> state가 변경되면 -> 필요한 view에서 감지를 알아차린다.
다시 한 번 용어 정리를 해보려고 한다. 기술적으로 작성한 것도 있는데, 일단은 읽고 뒤에서 마저 설명하도록 하겠다. Redux에서도 사용하는 용어이다.
-
action: 상태 변화를 일으킬 수 있는 현상. 예를 들어 사용자의 입력, 웹 요청 완료 등이다. data type은 plain object이다. type 프로퍼티를 꼭 갖고 있다. type이란 어떤 행동을 설명하는 string이다. 아래와 같이 구조이다.
{ type: 'SHOW_MODAL' }
- store: application의 전체 state를 가지고 있는 객체. app에는 단 하나의 store를 갖고 있는 것이 좋다고 한다.
- view: 리액트에서는 component라고 생각하면 된다.
리덕스의 특징
리덕스의 3가지 특징이 있다. 많은 책이나 블로그에서 설명하는 주요 컨셉인데 공식 docs에서 읽고 넘어가자.
이 중에서 알고 넘어갈 것이 있다.
- State는 읽기 전용이다. 컴포넌트에서는 state를 참조만 할 수 있고, (setState처럼) 변경할 수는 없다. state를 변경하고 싶다면, Action을 dispatch(발행)해서 app의 state를 변경해야만 한다. 이렇게 해야 데이터의 흐름이 한 방향으로만 흐르게 된다.
- reducer는 pure function(순수 함수)으로 이뤄져야 한다. 순수 함수란 같은 입력 값을 넣으면 같은 출력 값을 내는 함수를 의미한다. reducer는 이전의 state와 action을 입력 받고, 이로 인해 변화한(next) state를 return한다. 즉, reducer는 action에 의해 state를 변경시키는 함수라고 생각하면 된다.
리덕스 설치
Create React App 기준으로 설명!
npm install --save redux react-redux
react-redux은 redux를 react에서 사용하기 좋도록 만들어진 것이다.
원래 state의 변화를 감지하거나, action을 발행하려면 store.dispatch(), store.subscribe() 등의 메서드를 매번 사용해야 하는데, react-redux로 component에서 store를 쉽게 읽고, action을 쉽게 dispatch할 수 있도록 해준다.
react app에서 store를 사용하려면 <Provider>
를 사용해야 한다. 시작점, 즉 가장 최상위 컴포넌트를 Provider로 래핑하고 속성에 store를 전달한다.
//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './App'
//리듀서 필요
//const store = createStore();
const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
리덕스의 구성 요소
위의 코드에서 store를 생성하려면 reducer가 필요하다. reducer가 무엇인지 보고 마저 코드를 완성하자.
Reducer
리듀서는 store가 가지고 있는 state를 변화시키기 위한 함수이다. 리듀서는 이전의 state와 action을 인자로 받고, 이 action의 내용(type)에 따라 state를 변화시킨다.
(state, action) => nextState
redux의 createStore함수에 reducer를 인자로 주어야 한다. 일단 reducer를 정의해보자. 아래는 modal이라는 reducer를 정의한 것이다.
//src/ModalReducer.js
function modal(state = null, action) {
switch(action.type) {
case 'SHOW_MODAL':
return {
...state,
showModal: true
};
case 'CLOSE_MODAL':
return {
...state,
showModal: false
};
default:
return state;
}
}
각 action type 별로 새로운(next) state를 생성하여 return하면 된다.
위의 코드에서 SHOW_MODAL
이라는 action type과, CLOSE_MODAL
이라는 action type에 따라 return하는 state가 다르다.
SHOW_MODAL
이라는 action type이 전달되면 showModal 이라는 상태를 true로 변경하고
CLOSE_MODAL
이라는 action type이 전달되면 showModal 이라는 상태를 false로 변경한다.
위에서 state가 없을 때 null로 세팅했는데 초기 상태를 주기도 한다.
const INITIAL_STATE = {
showModal: true
}
function modal(state = INITIAL_STATE, action) {
//생략
Store
리덕스는 createStore라는 함수로 store를 생성한다. store는 app에 오직 하나만 있다. 이 유일한 store를 사용해 app 전체의 상태를 관리한다.
리덕스의 스토어를 생성하는 경우에는 리듀서가 필요하다. 위에서 리듀서를 생성했으니 index.js를 수정하자. 첫 번째 매개변수에 리듀서를 전달한다.
//index.js
import { createStore } from 'redux'
import modalReducer from './ModalReducer';
const store = createStore(modalReducer);
Action
Action에 대해 다시 한 번 설명하자면 사용자의 입력이나 UI 조작, 웹 요청 완료같이 어떠한 상태 변화를 일으킬 수 있는 현상이다. 즉, 어떤 조작인지 정보를 갖고 있는 자바스크립트 객체이다. 조작 정보는 type 프로퍼티에 지정한다.
action 객체는 아래와 같이 생겼다.
{
type: 'SHOW_MODAL',
payload: {
options: {
title: '모달 제목',
dim: true
}
}
}
다음과 같은 특징을 가진다.
- action은 plain object이다.
- action은 반드시 type 프로퍼티를 가져야만 한다. 일반적으로 type은 어떤 행동을 설명하는 문자열이다.
- action은 payload를 가질 수도 있다. payload는 특정 액션에 부가적인 데이터를 전달하고 싶을 때 사용한다. 물론 payload가 아닌 다른 다른 프로퍼티명을 사용해도 된다.
action의 type은 reducer에서도 사용해야되기 때문에 상수로 관리하자. 예제코드에서는 적용하지 않았다.
보통 action은 객체 리터럴로 바로 정의하는 것이 아니라 Action Creator(액션 생성자) 함수로 action을 생성한다. 동일한 구조의 action 객체를 만들기 위함이다. Action Creator로 만들지 않으면 action 객체 구조가 일정하지 않을 수도 있기 때문이다. 즉, action 객체를 return하는 함수를 호출해서 action을 만든다.
위의 action을 Action Creator로 만든다면 아래와 같다. showModal이라는 이름으로 만들었다.
const showModal = options => ({
type: 'SHOW_MODAL',
payload: { options }
});
modal을 보여줄 때 사용할 options을 매개변수로 받고, action 객체를 리턴하는 간단한 함수이다. action의 type은 ‘SHOW_MODAL’이며, modal을 보여줄 때 필요한 options라는 데이터를 payload에 추가하였다.
다음과 같이 Action Creator를 사용해 action을 생성한다.
showModal({
title: '로그인',
dim: true
});
action의 발행과 state 변화 감지
원래 action을 dispatch(발행)하고 싶다면 store.dispatch() 메서드를 사용한다. react-redux에서는 사용이 조금 다르므로 알아만 두고 가자.
store.dispatch(showModal({
title: '로그인',
dim: true
}));
위의 action을 dispatch(발행)하면 ‘SHOW_MODAL’이라는 type을 가진 액션이 발행되고 -> 리듀서에 의해 상태가 변화된다.
react에서는 특정 컴포넌트에서 store의 상태를 얻을 때는 connect함수를 사용한다. connect는 store를 특정 컴포넌트에 전달하는 역할을 한다.
import React from "react";
import { connect } from 'react-redux';
import { showModal, closeModal } from '../../ModalAction';
const Main = props => {
return (
<>
<div onClick={props.showModal}>열기</div>
<div onClick={props.closeModal}>닫기</div>
</>
);
};
const mapStateToProps = state => ({
show: state.showModal
});
const mapDispatchToProps = dispatch => ({
showModal: () => dispatch(showModal()),
closeModal: () => dispatch(closeModal()),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Main);
connect의 첫 번째 매변수는 mapStateToPropse이다. 영어그대로, store의 state를 해당 컴포넌트의 props로 전달하겠다는 의미이다.
두번째 매개변수는 mapDispatchToPros이다. 컴포넌트에서 액션을 발행(dispatch)하고 싶을 때, actionCreator로 생성된 action을 dispatch할 수 있는 함수를 컴포넌트의 props로 넘길 수 있도록 한다.
combineReducers
combineReducers는 리덕스에서 제공하는 메서드로, 각 리듀서를 하나로 모아주는 역할을 한다.
import { combineReducers } from 'redux';
import authReducer from './AuthReducer';
import ModalReducer from './ModalReducer';
const rootReducer = combineReducers({
auth: authReducer,
modal: ModalReducer
});
export default rootReducer;
redux-thunk
여기서 부터 wip.. redux-thunk는 redux의 미들웨어이다. 미들웨어란, 어떤 기능을 사용하기 좋게 확장한 것인데 redux-thunk도 redux의 미들웨어중 하나이다.
redux-thunk는 비동기 처리를 해주는 미들웨어이다. 원래 action내에서 fetch가 불가능하기 때문에 비동기를 처리하려면 해당 미들웨어를 설치해야 한다.
action의 rule에 무조건 object를 return해야한다는 것에서 확장할 수 있다. action은 action object나 function을 return할 수 있다.