본문 바로가기

프로그래밍/React

React(리액트) - React Redux 알아보기

공부하면서 쓰는 글이니 오류가 있을 수 있습니다. 있다면 알려주시면 감사하겠습니다.!

리액트 리덕스는 기존에 상태 관리 로직을 다른 곳으로 분리시켜 관리할 수 있게 해주는 라이브러리입니다. 

 

간단한 프로젝트는 그대로 사용해도 되지만 프로젝트가 크다면 복잡한 상태 관리를 리덕스를 이용해서 상태(state) 관리를 분리하여 유지 보수성을 높이고 효율적으로 프로젝트를 개발할 수 있습니다.

 

리덕스는 React뿐만 아니라 anguler, Vue, 순수 자바스크립트에서도 사용이 가능합니다.

 

이번에는 React에서 편하게 사용할 수 있도록 만들어진 reudx ,react-redux를 알아봅니다.

 

Presentational 컴포넌트와 Conatiner 컴포넌트

리덕스를 사용할 때 많이 사용하는 패턴인 Presentational 컴포넌트와 Conatiner 컴포넌트를 분리하는 패턴을 많이 사용합니다. 

 

Presentational 컴포넌트는 Props로 데이터를 받아와서 UI를 처리만 하는 컴포넌트입니다. 

 

Container 컴포넌트는 리덕스로 상태를 변경하며 데이터를 처리하고 처리한 데이터를 Presentational 컴포넌트에 전달하는 역할을 합니다.

 

이렇게 컴포넌트를 분리하여 재사용성과 유지 보수성이 높아지고 코드를 쉽게 파악할 수 있는 장점이 있습니다.

 

리덕스 용어 정리

react-redux에서 사용하는 용어를 먼저 알아보겠습니다.

액션(Action)

액션은 상태가 변경될 때 어떤 상태가 변경될지 알려주는 객체입니다.

{
    type: "UP"
}
//또는
{
    type: "UP",
    value: 1
}

액션 객체에는 type이 필수적으로 들어가야며 type은 변경되어야 하는 액션의 이름입니다.

또한 상태가 변경될 데이터가 객체에 추가될 수 있습니다.

액션 함수

액션 함수는 액션 객체를 만들어 주는 함수입니다.

export const up = () => ({ type: "UP" });
export const down = () => ({ type: "DOWN" });
export const add = (number) => ({
    type: "ADD",
    count: {
        number: number
    }
});

추가할 데이터가 있다면 함수 파라미터로 전달된 데이터를 이용해 추가합니다.

리듀서(Reducer)

리듀서는 액션 객체를 받아 변경되는 상태 객체를 반환하는 함수입니다. 리듀서는 state, action 인자를 가집니다.

내부에서는 이 두 개의 인자를 이용해 새로운 상태를 반환합니다. 리듀서의 함수 이름은 원하는 이름으로 지어도 됩니다.

const initialState = {
    number: 0,
    list: [{
        number:0
    }]
};

function counter(state = initialState, action) {
    switch (action.type) {
        case "UP":
            return { number: state.number + 1 };
        case "DOWN":
            return { number: state.number - 1 };
        case "ADD":
            return {
                ...state,
                list: state.list.concat(action.count)
            }
        default:
            return state;
    }
}

initalState는 기본 상태 값으로 처음 리듀서 함수가 호출될 때 기본값으로 사용됩니다. 리듀서 함수에서도 불변성을 유지해야 하므로 새로운 상태를 만들어 반환해야 합니다.

 

action에서 받아오는 데이터가 있다면 Spread 연산자를 이용해 객체를 복사하고 action 객체에 추가한 데이터에 접근하여 새로운 값을 넣어줍니다.

디스패치(Dispatch)

dispatch는 스토어에서 가지고 있는 내장 함수이며 상태를 업데이트시키기 위해 액션 함수를 호출하여 액션 객체를 전달하거나 액션 객체를 직접 전달하는 함수입니다. dispatch가 호출되면 정의한 리듀서에 전달되며 상태가 업데이트됩니다.

스토어(Store)

스토어는 state와 reducer를 가지고 있으며 필요한 내장 함수가 같이 있습니다.

화면 컴포넌트 만들기

여기서 컴포넌트는 Presentational 컴포넌트이며 화면에 숫자가 나오고 늘리거나 줄이고 저장하는 컴포넌트를 만듭니다.

// components/Counter.jsx
import React from "react";

const Counter = ({ number, list, onUp, onDown, onAdd }) => {
    return (
        <div>
            <h1>{number}</h1>
            <div>
                <button onClick={onUp}>업</button>
                <button onClick={onDown}>다운</button>
                <button onClick={() => onAdd(number)}>추가</button>
            </div>
            <ul>
                {list.map((n,i) => <li key={i}>{n.number}</li>)}
            </ul>
        </div>
    );
};

export default Counter;

//App.js
import "./App.css";
import Counter from "./components/Counter";

function App() {
    return (
        <div>
            <Counter />
        </div>
    );
}

export default App;

Counter에는 숫자와 버튼 이벤트를 props로 받습니다.

 

리덕스 코드 만들기

리덕스 코드들은 한 파일에 몰아 모두 작성합니다. 이러한 방식을 Ducks 패턴이라고 합니다.

액션 타입, 액션 함수, 리듀서 함수를 counter 파일에 작성하며 이러한 파일을 모듈이라고 합니다.

// modules/counter.js
// 액션 타입
const UP = "counter/UP";
const DOWN = "counter/DOWN";
const ADD = "counter/ADD";

// 액션 함수
export const up = () => ({ type: UP });
export const down = () => ({ type: DOWN });
export const add = (number) => ({
    type: ADD,
    count: {
        number: number
    }
});

// 기본 state 값
const initialState = {
    number: 0,
    list: [{
        number:0
    }]
};

// reducer 함수
function counter(state = initialState, action) {
    switch (action.type) {
        case UP:
            return {
                ...state,
                number: state.number + 1
            };
        case DOWN:
            return {
                ...state,
                number: state.number - 1
            };
        case ADD:
            return {
                ...state,
                list: state.list.concat(action.count)
            }
        default:
            return state;
    }
}

export default counter;

액션 타입을 선언할 때 대문자로 선언하며 내용은 '모듈/액션 이름'으로 작성합니다.(중복 방지)

 

액션 함수는 액션 객체를 만들어 주고 reducer함수는 액션에 따라 number 값을 올리거나 내리고 번호를 저장하여 새로운 state 객체를 반환해줍니다.(ADD 액션)

 

스토어 만들기

상태를 변경해주는 reducer 함수와 액션 함수도 만들었으니 이제 스토어에 리듀서 함수와 상태를 적용해야 합니다.

// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { createStore } from "redux";
import counter from "./modules/conuter";

const store = createStore(counter);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById("root")
);

createStore() 함수를 이용해 reducer를 지정하고 스토어를 만듭니다. 리덕스를 사용할 때 여러 스토어를 만들 수 있지만 여러 개를 사용하면 상태 관리가 복잡해질 수 있으니 하나만 사용하는 것이 좋습니다.

 

만든 스토어를 사용하려면 react-redux에 있는 Provider 컴포넌트를 이용해 App 컴포넌트를 감싸야합니다.

 

Provider는 컴포넌트들이 스토어를 사용할 수 있도록 합니다. 스토어 사용은 Hooks이나 connect를 이용해 사용할 수 있습니다.

 

Contianer 컴포넌트 만들기

컨테이너 컴포넌트는 리덕스 스토어를 이용하여 필요한 상태를 사용하며 액션을 발생시킬 수 있도록 액션 함수를 넘겨주는 컴포넌트입니다.

 

컨테이너 컴포넌트에서 리덕스 스토어에 접근하려면 connect 함수를 사용하여 접근해야 합니다.

 

connect 함수

connect 함수의 인수는 mapStateToProps, mapDispatchToProps, merageProps, options입니다.

 

mapStateToProps 파라미터는 함수로 전달되어야 하며 이 함수는 state 파라미터를 전달받으며 스토어에 있는 state를 가져와 새로운 객체를 반환해야 합니다.

 

mapDispatchToProps 파라미터는 함수 또는 객체로 전달되어야 하며 함수에 하나의 파라미터를 가지는 함수로 전달되면 함수에 전달되는 파라미터는 dispatch가 전달되며 dispatch를 사용하는 객체를 반환해야 합니다.

 

이 두 파라미터들은 store에 구독되면서 store가 업데이트되면 새로 호출이 됩니다. 

 

세 번째, 네 번째 파라미터는 있거나 없어도 되며 자세한 사항은 공식 문서를 참조해보시기 바랍니다.

 

connect 함수를 이용해 컨테이너 컴포넌트를 생성합니다.

// containers/CounterContainer.jsx

import Counter from "../components/Counter"
import { up, down, add } from "../modules/conuter";
import { connect } from "react-redux";

const CounterContainer = ({ number, list, up, down, add }) => {
    return <Counter number={number} list={list} onUp={up} onDown={down} onAdd={add}/>
}

const mapStateToProps = (state) => ({
    number: state.number,
    list: state.list
});

const mapDispatchToProps = (dispatch) => ({
    up: () => dispatch(up()),
    down: () => dispatch(down()),
    add: (number) => dispatch(add(number))
});

export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);

이렇게 connect를 이용해서 필요한 상태와 액션 함수를 전달하면 고차 컴포넌트(HOC)를 반환합니다.

 

connect 함수가 반환해준 함수에 다시 컨테이너 컴포넌트를 인수로 전달하면 state를 가져오는 함수와 dispatch 하는 함수가 추가된 래퍼 컴포넌트가 만들어집니다. 

 

이렇게 만들어진 래퍼 컴포넌트 CounterContainer 컴포넌트를 App.js에서 사용하면 됩니다.

 

좀 더 편리성을 높여보자

 

connect 함수를 사용할 때 dispatch를 받는 익명 함수로 감싸 액션 생성 함수를 가지는 객체를 만들어 주었습니다. 

const mapDispatchToProps = (dispatch) => ({
    up: () => dispatch(up()),
    down: () => dispatch(down()),
    add: (number) => dispatch(add(number))
});

만약 액션 생성 함수가 많아질 경우 번거 dispatch를 호출하는 익명 함수를 만드는 게 번거로울 수 있습니다. 

 

이럴 경우 좀 더 편하게 redux가 지원하는 bindActionCreators() 함수를 사용하면 dispatch를 호출하는 익명 함수를 만들지 않아도 알아서 만들어집니다.

const mapDispatchToProps = (dispatch) => (
    bindActionCreators({
        up,
        down,
        add
    }, dispatch)
)

이것도 번거롭다면 바로 객체로 넘겨주어도 됩니다.

const mapDispatchToProps = {
    up,
    down,
    add
}

이렇게 그냥 바로 객체로 액션 생성 함수를 넘기면 connect 함수 내부에서 bindActionsCreators를 호출하여 dispatch를 호출할 수 있는 형태로 만들어 줍니다.

 

이밖에도 액션 타입과 액션 생성 함수의 가독성을 높여주는 라이브러리인 redux-actions를 이용할 수 있습니다.

 

Hooks 사용하기

리덕스에서 지원하는 hooks를 사용하면 간단하게 리덕스를 사용할 수 있습니다.

// containers/CounterContainer.jsx

import Counter from "../components/Counter"
import { up, down } from "../modules/conuter";
import { useSelector, useDispatch } from "react-redux";

const CounterContainer = () => {
    const number = useSelector(state => state.number);
    const dispatch = useDispatch();
    return <Counter number={number} onUp={() => dispatch(up())} onDown={() => dispatch(down())} />
};

export default CounterContainer;

mapStateToProps를 대신해서 사용할 수 있는 useSelector Hooks이 있습니다. useSelector Hook을 이용하면 간단하게 state를 가져올 수 있습니다.

 

mapDispatchToProps는 useDispatch Hooks으로 간단히 dispatch를 호출할 수 있습니다.

 

또한 useStore Hooks을 사용하여 간단하게 스토어도 생성이 가능합니다.

useStore Hooks은 많이 사용은 하지 않으니 알고 있으면 되겠습니다.

const store = useStore();

 

글에서 다루지 않은 정보들도 있으니 필요하다면 공식 문서에서 더 많은 정보를 확인하시기 바랍니다.

 

참고

리액트를 다루는 기술(개정판) - 김민준 지음

https://redux.js.org/api/api-reference

https://react-redux.js.org/api/provider

 

Provider | React Redux

API > Provider: providing the Redux store to your React app

react-redux.js.org