본문 바로가기
JavaScript/React

React) 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 ② - 리액트 애플리케이션에 리덕스 적용하기

by 박채니 2022. 12. 15.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

 

리액트 애플리케이션에 리덕스 적용하기

 

스토어를 생성하고 리액트 애플리케이션에 리덕스를 작용하는 작업은 src/index.js에서 이루어집니다.

 

스토어 만들기

 

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './modules';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

modules/index.js에 생성해놓았던 리듀서들을 하나로 모은 rootReducer를 불러오고 이를 통해 store를 생성하였습니다.

 

Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기

 

리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸줍니다.

이 때, store를 props로 전달해 주어야 합니다.

 

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

App 컴포넌트를 Provider로 감싸주었고 store를 props로 넘겨주었습니다.

 

Redux DevTools의 설치 및 적용

 

Redux DevTools

- 리덕스 개발자 도구

 

크롬 웹 스토어에서 Redux DevTools를 설치해주면 리덕스 스토어를 만드는 과정에서 아래와 같이 적용할 수 있습니다.

 

예시

const store = createStore(
  rootReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSTION__()  
);

 

하지만 패키지를 설치해 적용하면 코드가 훨씬 깔끔해집니다.

$ yarn add redux-devtools-extension

 

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import { devToolsEnhancer } from 'redux-devtools-extension';

const store = createStore(rootReducer, devToolsEnhancer());

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

브라우저에서 개발자 도구 - Redux를 선택하면 리덕스 개발자 도구가 잘 나오는 것을 확인할 수 있습니다.

 


컨테이너 컴포넌트 만들기

 

컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아 오고, 액션도 디스패치 해주어야 합니다.

이렇듯 리덕스 스토어와 연동된 컴포넌트컨테이너 컴포넌트라고 합니다.

 

CounterContainer 만들기

 

src/containers/CounterContainer.js

import Counter from "../components/Counter";

const CounterContainer = () => {
    return <Counter />;
}

export default CounterContainer;

 

해당 컴포넌트를 리덕스와 연동하려면 react-redux에서 제공해주는 connect 함수를 사용해야 합니다.

 

사용 예시

connect(mapStateToProps, mapDispatchToProps) (연동할 컴포넌트)

 

mapStateToProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수

mapDispatchToProps : 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수

 

connect 함수를 호출하고 나면 또 다른 함수를 반환하게 됩니다.

반환된 함수의 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 생성!!

 

예시

const makeContainer = connect(mapStateToProps, mapDispatchToProps)
makeContainer(타깃 컴포넌트)

 

containers/CounterContainer.js

import { connect } from "react-redux";
import Counter from "../components/Counter";

const CounterContainer = ({ number, increase, decrease }) => {
    return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
    );
};

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

const mapDispatchToProps = dispatch => ({
    // 임시함수
    increase: () => {
        console.log('increase');
    },
    decrease: () => {
        console.log('decrease');
    }
});

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

컨테이너 컴포넌트를 store에 접근하여 값을 가져오거나 dispatch도 해준다고 하였습니다.

그 후 UI만을 표시하는 프레젠테이셔널 컴포넌트(Counter)에 props로 값들을 넘겨주게 되는 것이죠.

 

또한, 생성한 CounterContainer가 리덕스와 연동되도록 connect를 이용한 것을 확인할 수 있습니다.

mapStateToProps와 mapDispatchToProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달되었고,

mapStateToProps는 state를 파라미터로 / mapDispatchToProps는 dispatch 함수를 파라미터로 받게됩니다.

 

App.js

import Todos from "./components/Todos";
import CounterContainer from "./containers/CounterContainer";

function App() {
  return (
    <div>
      <CounterContainer />
      <hr />
      <Todos />
    </div>
  );
}

export default App;

 

이번엔 console.log 대신 액션 생성 함수를 불러와 액션 객체를 만들고 디스패치 해주겠습니다.

 

containers/CounterContainer.js

import { connect } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = ({ number, increase, decrease }) => {
    return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
    );
};

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

const mapDispatchToProps = dispatch => ({
    // 임시함수
    increase: () => {
        dispatch(increase());
    },
    decrease: () => {
        dispatch(decrease());
    }
});

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

counter 모듈에서 액션 생성 함수를 export 로 보내주었기 때문에 가져와 사용할 수 있습니다.

가져온 액션 생성 함수를 dispatch하여 처리했습니다.

버튼에 따라 숫자도 변경되고 Redux 탭에서도 해당 값들이 잘 표시됩니다.

 

mapStateToProps와 mapDispatchToProps를 미리 선언하여 사용할 수도 있지만, connect 함수 내부에서 익명 함수 형태로 사용해도 괜찮습니다.

import { connect } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = ({ number, increase, decrease }) => {
    return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
    );
};

export default connect(state => ({
        number: state.counter.number
    }), 
    dispatch => ({
        increase: () => dispatch(increase()),
        decrease: () => dispatch(decrease())
    })
)(CounterContainer);

 

☆ 컴포넌트에서 액션을 디스패치하기 위해 각 액션 생성 함수를 호출하고 dispatch 시켜주는 작업이 다소 번거로울 수도 있습니다.

이의 경우 리덕스에서 제공하는 bindActionCreators 유틸 함수를 사용해 간편히 처리할 수 있습니다.

 

containers/CounterContainer.js

import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = ({ number, increase, decrease }) => {
    return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
    );
};

export default connect(state => ({
        number: state.counter.number
    }), 
    dispatch => 
        bindActionCreators(
            {
                increase,
                decrease
            },
            dispatch
        )
)(CounterContainer);

이보다 더 간단하게 사용할 수도 있습니다.

 

import { connect } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = ({ number, increase, decrease }) => {
    return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease} />
    );
};

export default connect(state => ({
        number: state.counter.number
    }), 
    {
        increase,
        decrease
    }
)(CounterContainer);

mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어주어 간편하게 처리할 수도 있습니다.

이렇게 하면, connect 함수가 내부적으로 bindActionCreators 작업을 대신해주게 됩니다.

 

TodoContainer 만들기

 

containers/TodoContainer.js

import { connect } from "react-redux";
import Todos from "../components/Todos";
import { changeInput, insert, toggle, remove } from "../modules/todos";

const TodoContainer = ({
    input,
    todos, 
    changeInput,
    insert,
    toggle,
    remove
}) => {
    return (
        <Todos 
            input={input}
            todos={todos}
            onChangeInput={changeInput}
            onInsert={insert}
            onToggle={toggle}
            onRemove={remove}
        />
    )
};

export default connect(
    ({ todos }) => ({
        input: todos.input,
        todos: todos.todos
    }),
    {
        changeInput,
        insert,
        toggle,
        remove
    }
)(TodoContainer);

todos 모듈에서 작성했던 액션 생성 함수와 상태 안에 있던 값을 컴포넌트의 props로 전달해 주었습니다.

 

App.js

import CounterContainer from "./containers/CounterContainer";
import TodoContainer from "./containers/TodoContainer";

function App() {
  return (
    <div>
      <CounterContainer />
      <hr />
      <TodoContainer />
    </div>
  );
}

export default App;

 

components/Todos.js

const TodoItem = ({ todo, onToggle, onRemove }) => {
    return(
        <div>
            <input 
                type="checkbox" 
                onClick={() => onToggle(todo.id)}
                checked={todo.done}
                readOnly={true}
            />
            <span style={{ textDecoration: todo.done ? 'line-through' : '' }} >{todo.text}</span>
            <button onClick={() => onRemove(todo.id)}>삭제</button>
        </div>
    )
};

const Todos = ({
    input, // 인풋에 입력되는 텍스트
    todos, // 할 일 목록이 들어있는 객체
    onChangeInput,
    onInsert,
    onToggle,
    onRemove
}) => {
    const onSubmit = e => {
        e.preventDefault();
        onInsert(input);
        onChangeInput(''); // 등록 후 인풋 초기화
    };

    const onChange = e => onChangeInput(e.target.value);

    return(
        <div>
            <form onSubmit={onSubmit}> {/* 제출 방지 */}
                <input value={input} onChange={onChange} />
                <button type="submit">등록</button>
            </form>
            <div>
                {todos.map(todo => (
                    <TodoItem 
                        todo={todo} 
                        key={todo.id}
                        onToggle={onToggle}
                        onRemove={onRemove}
                    />
                ))}
            </div>
        </div>
    );
};

export default Todos;

추가, 삭제, 체크 등이 다 잘 되는 것을 확인할 수 있습니다.