본문 바로가기
Frontend/ReactJS(완)

React - 17장 : 리액트 리덕스 실습 예제(React-Redux)

by VictorMeredith 2023. 2. 28.

<리액트를 다루는 기술>

React - 1장 : React 이해

React - 2장 : JSX

React - 3장 : 컴포넌트

React - 4장 : 이벤트 핸들링

React - 5장 : ref. DOM에 이름 달기 

React - 6장 : 컴포넌트 반복
React - 7장 : 컴포넌트의 LifeCycle
React - 8장 : React Hooks 총정리

React - 9장 : 컴포넌트의 스타일링

React - 10장 : 빠르게 TODO앱 실습

React - 11장 : 컴포넌트 성능 최적화

React - 12장 :  immer를 사용하여 더 쉽게 불변성 유지하기

React - 13장 : 리액트 라우터로 SPA 개발하기

React - 14장 : 뉴스 뷰어 예제프로젝트

React - 15장 : ContextAPI

React - 16장 : 리덕스 라이브러리의 이해

React - 17장 : 리덕스 실습(현재)

 

리덕스(Redux) 를 쓰는 이유 ? :

- 리덕스는 상태관리 라이브러리이다.

- 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 별개로 관리가 가능하므로 유지보수가 좋다

- 여러 컴포넌트에서 동일한 상태를 공유해야 할 경우 유용하다.

- 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 쉽게 최적화가 가능하다.

 

17-1. 작업환경 설정

- 터미널에 npx create-react-app react-redux-tutorial (CRA로 프로젝트 만들기)

- 터미널에 cd react-redux-tutorial

- 터미널에 npm i redux react-redux

 

17-2. UI 준비하기

- 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것이다.

- 프레젠테이셔널 컴포넌트 : 상태관리가 주로 없고, props를 받아와서 UI 표시 정도만 하는 컴포넌트

- 컨테이너 컴포넌트 : 리덕스와 연동된 컴포넌트

합리적이네

- 예제에서는 프레젠테이셔널은 src/components에 저장

- 컨테이너는 src/containers에 저장한다.

 

17.2.1 카운터 컴포넌트 만들기

- 숫자를 더하고 뺄 수 있는 카운터 컴포넌트를 만들어보자.

 

src/components/Counter.jsx

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에서 렌더링해보자

App.js

import Counter from './components/Counter'
import './App.css';

function App() {

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

export default App;

- 터미널에서 npm start 해서 확인한다

 

17.2.2 할 일 목록 만들기

- 해야 할일을 추가하고, 체크하고, 삭제할 수 있는 누구나 다하는 투두컴포넌트를 만들어본다.

 

/src/components/Todos.jsx

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

const Todos = ({input, todos, onChangeInput, onInsert, onToggle, onRemove})=>{
    const onSubmit = e => {
        e.preventDefault();
    };

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

export default Todos;

- 받아오는 props는 나중에 사용하는 걸로 해둔다.

 

App.js에 렌더링 해보자.

import Counter from './components/Counter'
import Todos from './components/Todos';
import './App.css';

function App() {

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

export default App;

 

지금까지 결과물

옛날 생각나는 눈물나는 코린이의 투두리스트

17-3. 리덕스 세팅하기

구조1 ) 

일반적인 구조

- actions, constants, reducers 폴더에 기능별로 파일을 하나씩 만드는 방식

- 코드를 종류에 따라 분류, 정리가 가능하지만 세 종류의 파일을 모두 수정하면서 왔다리갔다리 해야한다

- 리덕스의 공식문서에서도 추천하지만, 불편하다.

 

구조2)

이게 더 일반적인거 아닌가

- 액션 타입, 액션 생성, 리듀서 함수를 기능별로 파일 하나에 다 몰아서 작성하는 방식

- Ducks 패턴이라고 부른다. 

 

위의 두가지 구조로 주로 사용되며, 딱히 정해진 건 없고 회사에서 정해준대로 하자.

 

17-3.1 counter 모듈 작성하기

17.3.1.1 액션 타입 정의

/modules/counter.js

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

- 액션 타입은 대문자로 정의, 문자열 내용은 '모듈이름/액션이름' 으로 작성해준다. (겹침 방지)

 

17.3.1.2 액션 생성 함수 만들기

/modules/counter.js

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

export const increase = ()=> ({ type: INCREASE });
export const decrease = ()=> ({ type: DECREASE });

- export 키워드로 다른곳에서 사용할 수 있도록 한다.

 

17.3.1.3 초기 상태 및 리듀서 함수 만들기

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

export const increase = ()=> ({ type: INCREASE });
export const decrease = ()=> ({ type: DECREASE });

const initialState = {
    number: 0
}

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

- 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성해준다.

- cf) export 키워드는 여러개 내보낼 수 있다. export default는 하나만 내보낼 수 있다

- cf2) export 키워드는 import { increase, decrease } from './counter.js' 이렇게

- cf3) export default 키워드는 import counter from './counter.js' 이렇게 한다.

 

17-3.2 todos 모듈 만들기

17.3.2.1 액션 타입 정의하기

modules/todos.js

const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋값 변경
const INSERT = 'todos/INSERT'; // 새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; // todo 체크 토글
const REMOVE = 'todos/REMOVE'; // todo삭제

 

 

17.3.2.1 액션 생성 함수 만들기 

- 이번엔 파라미터가 필요하다.

 

moduels/todos.js

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씩 더해진다.

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

- insert 함수는 액션 객체를 만들 때 파라미터 외에 사전에 이미 선언되어 있는 id라는 값에도 의존한다.

- id값이 3인 이유는 다음번에 초기 상태를 작서할 때 todo 객체 두개를 사전에 미리 넣어둘 것이므로 3이다.

 

17.3.2.3 초기 상태 및 리듀서 함수 만들기

- 불변성 유지를 위해 spread 연산자를 잘 활용한다.

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씩 더해진다.

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
        },
    ]
}

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

export default todos;

 

17-3.3 루트 리듀서 만들기

- 나중에 createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야 한다.

- 그러므로 기존에 만들었던 리듀서를 하나로 합쳐주어야 한다.

- combineReducers라는 리덕스 제공함수를 사용한다.

 

modules/index.js

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

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

export default rootReducer;

 

17-4. 리덕스 적용하기

- 스토어를 만들고 리액트에 적용한다.

/src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import {createStore} from 'redux';
import App from './App';
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>
);

 

17-5. 컨테이너 컴포넌트 만들기

17-5.1 CounterContainer 만들기

/containers/CounterContainer.jsx

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

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

export default CounterContainer;

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

- 사용법 : connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)

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

- mapDispatchToProp는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수

- connect함수를 호출하면 또 다른 함수를 뱉는다. 이 함수에 컴포넌트를 파라미터로 넣으면 된다

결론은 이렇다

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

 

/containers/CounterContainer.jsx

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

 

이제 App.js 에서 렌더링해준다

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

function App() {

  return (
    <div className="App">
      <CounterContainer></CounterContainer>
      <hr />
      <Todos></Todos>
    </div>
  );
}

export default App;

 

17-5.2 TodosContainer 만들기

containers/TodosContainer.jsx

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}) => ({
        input: todos.input,
        todos: todos.todos,
    }),
    {
        changeInput,
        insert,
        toggle,
        remove
    },
)(TodosContainer);

 

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' : 'none'}}>{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 type="text" value={input} onChange={onChange} />
                <button type="submit">등록</button>
            </form>
            <div>
                {
                    todos.map(todo => (
                        <TodoItem todo={todo} key={todo.id} onToggle={onToggle} onRemove={onRemove}></TodoItem>
                    ))
                }
            </div>
        </div>
    )
}

export default Todos;

 

App.js

import CounterContainer from './containers/CounterContainer';
import TodosContainer from './containers/TodosContainer';

function App() {

  return (
    <div className="App">
      <CounterContainer></CounterContainer>
      <hr />
      <TodosContainer></TodosContainer>
    </div>
  );
}

export default App;

 

- 작업 끝! 

- 하다보니 느낀 건 따라치기는 별 도움이 안된다..

- 이번 포스팅처럼 옛날 문법은 대충 알아두는 것으로만 하고, 이제 다음부터는 쉬운 mz세대들의 리덕스를 알아보자.

- 다음 편에 추가로 문법이 훨씬 쉬운 redux/toolkit 과 또한 관련된 미들웨어 세팅을 포스팅해야겠다.

댓글