[React] Redux를 리액트 프로젝트에 사용해보기

2022. 8. 31. 16:15·Front-End/React

이 포스팅은 저자 김민준 님의 ‘리액트를 다루는 기술’을 참고하여 작성되었습니다.

React 프로젝트에서 Redux를 이용한 상태관리

  • 리액트에서 리덕스를 사용할 때 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컨포넌트를 분리하는 것이다.
    • 프레젠테이셔널 컴포넌트 : 상태관리가 이루어지지 않고, 그저 props를 받아와서 화면에 UI를 보여주기만 하는 컴포넌트
    • 컨테이너 컴포넌트 : 리덕스와 연동된 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치 하기도 한다.

카운터 컴포넌트 만들기

숫자를 더하고 뺄 수 있는 카운터 컴포넌트 생성

components/Counter.js

import React from 'react';

const Counter = ({ number, onIncrease, onDecrease }) => {
  return (
    <div>
      <h1>{number}</h1>
      <div>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  );
};

export default Counter;

App.js에 렌더링

import React from 'react';
import Counter from './components/Counter';

const App = () => {
  return (
    <div>
      <Counter number={0} />
    </div>
  );
};

export default App;

할 일 목록 컴포넌트 만들기

  • 해야 할 일을 추가하고, 체크하고, 삭제할 수 있는 할일 목록 컴포넌트 생성

components/Todos.js

import React from 'react';

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input type="checkbox" />
      &nbsp;&nbsp;
      <span>예제 텍스트</span>
      &nbsp;&nbsp;
      <button>삭제</button>
    </div>
  );
};

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

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input />
        <button type="submit">등록</button>
      </form>
      <div>
        <TodoItem />
        <TodoItem />
        <TodoItem />
        <TodoItem />
        <TodoItem />
      </div>
    </div>
  );
};

export default Todos;

App 컴포넌트에서 카운터 아래에 렌더링

App.js

import React from 'react';
import Counter from './components/Counter';
import Todos from './components/Todos';

const App = () => {
  return (
    <div>
      <Counter number={0} />
      <hr />
      <Todos />
    </div>
  );
};

export default App;

리덕스 관련 코드 작성하기

액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식을 Ducks 패턴이라고 하며 이 패턴을 사용해서 코드를 작성

counter 모듈 작성

modules/counter.js 생성

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성함수 만들기
export const increase = () => ({type: INCREASE});
export const decrease = () => ({type: DECREASE});

//초기 상태 및 리듀서 함수 만들기
//초기 상태
const initialState = {  
    number: 0
};

//리듀서 함수
function counter(state = initialState, action) { 
    switch (action.type) {
        case INCREASE:
            return {
                number: state.number + 1
            };
        case DECREASE:
            return {
                number: state.number - 1,
            };
        default:
            return state;
    }
}

export default counter;
// export : 여러개를 내보낼 수 있고
// export default : 단 한 개만 내보낼 수 있다.
  • 액션 타입 정의 : 액션 타입은 대문자로 정의하고, 문자열 내용을 정의할때는 ‘모듈 이름/액션 이름’과 같은 형태로 작성한다.
  • 액션 생성 함수 만들기 : 앞부분에 export 키워드가 들어가며, 이 함수를 다른 파일에서 불러와 사용 가능하다.
  • 초기 상태 및 리듀서 함수 만들기 : 초기상태에는 number 값을 설정해 줌, 리듀서 함수에는 현재 상대를 참조하여 객체를 반환하는 코드를 작성

todos 모듈 만들기

modules/todos.js 생성

// 액션 타입 정의
import Todos from "../components/Todos";

const CHANGE_INPUT = 'todos/CHANGE_INPUT'; //인풋값 변경
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크해제
const REMOVE = 'todos/REMOVE'; // todo를 제거함

//액션 생성 함수 만들기
export const changeInput = input => ({
    type: CHANGE_INPUT,
    input
});
let id = 3;  //insert가 호출될 때마다 1씩 더해짐, todo객체가 사전에 2개 미리 들어갈것이라서 초기값 3
export const insert = text => ({
    type: INSERT,
    todo: {
        id: id++,
        text,
        done: false,
    }
});
export const toggle = id => ({
    type: TOGGLE,
    id
});
export const remove = id => ({
    type: REMOVE,
    id
});

//초기 상태 및 리듀서 함수 만들기
const initialState = {
    input: '',
    todos: [
        {
            id: 1,
            text: '리덕스 기초 배우기',
            done: true
        },
        {
            id: 2,
            text: '리액트와 리덕스 사용하기',
            done: false
        },
    ]
};

function todos(state = initialState, action) {
    switch (action.type) {
        case CHANGE_INPUT:
            return{
                ...state,
                input: action.input
            };
        case INSERT:
            return{
                ...state,
                input: state.todos.concat(action.todo),
            };
        case TOGGLE:
            return{
                ...state,
                todos: state.todos.map(todo =>
                todo.id === action.id ? {...todo, done: !todo.done} : todo)
            };
        case REMOVE:
            return{
                ...state,
                todos: state.todos.filter(todo =>
                    todo.id !== action.id )
            };
        default:
            return state;
    }
}

export default todos;
  • 액션 타입 정의 : ‘모듈 이름/액션 이름’과 같은 형태로 작성
  • 액션 생성 함수 만들기 : counter와 달리, 액션 생성함수에 파라미터가 들어간다.
  • 초기상태 및 리듀서 함수 생성 : 객체에 한개 이상의 값이 들어가므로 불변성을 유지해 주어야 한다. → spread 연산자를 활용해서 작성

루트 리듀서 만들기

리듀서를 여러개 만들었는데, store를 만들때는 리듀서를 하나만 사용할수 있기 때문에 리듀서를 하나로 합쳐주는 combineReducers 라는 유틸함수를 이용한다.

 

modules/index.js

import {combineReducers} from "redux";
import counter from "./counter";
import todos from "./todos";

const rootReducer = combineReducers({
    counter,
    todos,
});
export default rootReducer;

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

리액트 애플리케이션에 리덕스를 적용하는 작업은 src/index.js에서 이루어진다.

스토어 만들기

리액트 컴포넌트에서 스토어 사용을 위해 App을 Provider 컴포넌트로 감싸준다.

 

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
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>
);

reportWebVitals();

Redux DevTools의 설치 및 적용

https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related?hl=ko-KR

구글 크롬 Redux DevTools 설치 및

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 reportWebVitals from './reportWebVitals';
import {createStore} from "redux";
import rootReducer from "./modules";
import {Provider} from "react-redux";
import {composeWithDevTools} from "redux-devtools-extension";

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

reportWebVitals();

개발자 도구의 Redux 탭에서 Action 및 State의 상태를 확인 할 수 있다.

컨테이너 컴포넌트 만들기

리덕스 스토어에 접근하여 원하는 상태를 받아오고, 액션도 디스패치 하기위해서 스토어와 연동된 컨테이너 컴포넌트를 만들어 보자.

CounterContainer 만들기

src/containers/CounterContainer.js

import React from 'react';
import Counter from "../modules/counter";

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

export default CounterContainer;

위의 컴포넌트를 리덕스와 연동하려면 react-redux에서 제공하는 connect를 사용해야 한다.

connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
  • mapStateToProps : 리덕스 스토어 안에 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
  • mapDispatchToProps : 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수

CounterContainer 컴포넌트에서 connect를 사용

 

src/containers/CounterContainer.js

import React from 'react';
import Counter 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: () => {
        console.log('increase')
    },
    decrease: () => {
        console.log('decrease')
    },
});

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

mapStateToProps와 mapDispatchProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달된다.

  • mapStateToProps : state를 파라미터로 받아오며, 이 값은 현재 스토어가 지니고 있는 상태를 가리킨다.
  • mapDispatchProps : store의 내장함수 dispatch를 파라미터로 받아온다.

App에서 Counter 컴포넌트를 CounterContainer로 교체

 

App.js

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

export default App;

버튼을 누르면 콘솔에 decrease와 increase가 찍힌다.

console.log 대신 액션 객체를 불러와서 dispatch

 

src/containers/CounterContainer.js

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import {decrease, increase} 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);

일반적으로 위와 같이 mapStateToProps, mapDispatchToProps 를 미리 선언해놓고 사용하지만 connet 내에 익명함수 형태로 사용해도 된다.

 

containerws/CounterContainers.js

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import {decrease, increase} 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);

bindActionCreators를 사용해서 디스패치 하는 방법

  • 컴포넌트에서 액션을 디스패치 편하게 할 수 있도록 제공해주는 유틸 함수
import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import {decrease, increase} from "../modules/counter";
import {bindActionCreators} from "redux";

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);

mapDispatchToProps 에 해당하는 파라미터를 함수 형태가 아닌 액션 생성함수로 이루어진 객체 형태로 디스패치 하는 방법

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

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

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

TodosContainer 만들기

connect 함수를 사용하고, mapDispatchToProps를 짧고 간단하게 쓰는 방법을 적용

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

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

export default connect(
  // 비구조화 할당을 통해 todos를 분리하며
  // state.todos.input 대신 todos.input을 사용
  ({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }),
  {
    changeInput,
    insert,
    toggle,
    remove,
  },
)(TodosContainer);

App 컴포넌트에서 Todos컴포넌트를 TodosContainer 컴포넌트로 교체

import './App.css';
import Todos from "./components/Todos";
import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";

const App = () => {
  return (
      <div>
        <CounterContainer/>
          <hr/>
          <TodosContainer />
      </div>
  );
}

export default App;

Todos 컴포넌트에서 props를 사용하도록 구현

import React from 'react';

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' : 'none' }}>
        {todo.text}
      </span>
            &nbsp;&nbsp;
            <button onClick={() => onRemove(todo.id)}>삭제</button>
        </div>
    );
};

const Todos = ({
                   input, // 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;

 

반응형
'Front-End/React' 카테고리의 다른 글
  • [React] input태그 상태 관리
  • Props란? Props를 사용하는 방법
  • [React] Redux 이해하기
  • [React] 리액트 라우터(react-router-dom) v6 사용법
LightSource
LightSource
어제보단 발전한 오늘의 나를 위한 블로그
    반응형
  • LightSource
    LightSourceCoder
    LightSource
  • 전체
    오늘
    어제
    • 분류 전체보기 (152)
      • Git (4)
      • Language (6)
        • Java (6)
      • Back-End (63)
        • Spring Boot (4)
        • MyBatis (1)
        • Oracle (1)
        • PL SQL (3)
        • JPA (26)
        • Spring Data JPA (5)
        • Spring MVC (8)
        • Spring (12)
        • Spring Security (2)
        • Redis (1)
      • Front-End (38)
        • 아이오닉 (2)
        • JSP (7)
        • JavaScript (4)
        • React (16)
        • TypeScript (3)
        • Angular (6)
      • AWS (1)
      • CI & CD (1)
      • 개발지식 (13)
        • 네트워크 (9)
        • CS 지식 (4)
      • 데이터모델링 (2)
      • Tool (1)
      • 프로젝트 (5)
      • 독후감 (2)
      • 잡생각 (0)
      • 면접 준비 (1)
      • 알고리즘 (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    리액트
    배열요소삭제
    배열요소수정
    배요소열추가
    react
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
LightSource
[React] Redux를 리액트 프로젝트에 사용해보기
상단으로

티스토리툴바