TIL | #20 Vanilla Redux와 React Redux

trevor1107·2021년 4월 11일
0

2021-04-09(금)

Redux

리덕스는 자바스크립트 어플리케이션에서 가장 많이 쓰는 상태관리 라이브러리이다. 리덕스를 사용하면 컴포넌트들의 상태 관련 로직들을 파일들로 분리시켜서 더욱 효율적으로 관리할 수 있다. 또한 컴포넌트끼리 상태를 공유하게 될 때 여러 컴포넌트를 거치지 않고도 손쉽게 상태 값을 전달할 수 있다.

그리고 리덕스의 미들웨어라는 기능을 통하여 비동기 작업, 로깅등의 확장형태의 작업들을 쉽게할 수 있도록 도와주기도 한다.

Redux의 3가지 규칙

  1.  단일 스토어
    여러개의 스토어를 사용하는 것이 불가능 하지 않지만, 특정 업데이트가 빈번하게 일어나거나, 어플리케이션의 특정 부분을 완전히 분리 시킬 때 여러개의 스토어를 만들 수 있다. 하지만 상태 관리가 복잡해진다.

  2. 읽기 전용 상태
    리덕스 상태는 읽기 전용이다. immer, ...
    리덕스 상태를 업데이트 할 때 기존의 객체는 건드리지 않고,새로운 객체를 생성해야한다.
    리덕스에서 불변성을 유지하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교검사를 하기 때문이다.
    객체의 변화를 감지할 때 객체의 깊은 곳 까지 비교할 필요가 없다.

  3. 리듀서는 순수하다
    변화를 일으키는 리듀서 함수는 순수한 함수여야 한다. 순수한 함수는 아래와 같은 조건을 만족해야 한다.
    (1) 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 만든다.
    (2) 파라미터 외의 값에는 의존하면 안된다.
    (3) 이전 상태는 절대로 건드리지 말고 변화를 준 새로운 상태 객체를 만들어서 반환해야 한다.
    (4) 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 한다.
    리듀서를 쓸 때에는 위의 조건들을 지켜야 한다.
    리듀서 함수내부에서 랜덤 값, 네트워크 요청, 시간 가져오기 ... 등 파라미터가 같아도 다른 결과를 만들어 낼 수 있다.
    이러한 작업은 리듀서 함수 밖에서 처리하길 권한다.
    주로 네트워크 요청과 같은 비동기 작업은 미들웨어를 통해 관리한다.

vanilla-redux

바닐라 자바스크립트에 리덕스를 사용해보자

yarn global add parcel-bundler 또는 npm i -g parcel-bundler

yarn 설치후에 잘 되지 않는다면, npm으로 설치하길 바란다.
그리고 PowerShell에 입력시 권한이 낮아 안될수 있으니 그런 경우 권한을 주거나, cmd에서 명령어를 실행하길 권한다.

프로젝트 폴더를 생성 후 비주얼 스튜디오 코드를 열어서

yarn init -y or npm init -y → index.html 파일 생성 → 기본 html 입력 → parcel index.html

이제 코드 수정 및 저장시 자동으로 반영되고 localhost:1234로 접속하면 결과를 확인할 수 있다.

// index.html
<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link rel="stylesheet" href="./index.css" />
    </head>
    <body>
        <div class="toggle"></div>
        <hr />
        <h1>0</h1>
        <button id="increase">+1</button>
        <button id="decrease">-1</button>
        <script src="./index.js"></script>
    </body>
</html>
// index.css
.toggle {
    border: 2px solid black;
    width: 64px;
    height: 64px;
    border-radius: 32px;
    box-sizing: border-box;
}

.toggle.active {
    background: yellow;
}
import { createStore } from 'redux';

const divToggle = document.querySelector('.toggle');
const counter = document.querySelector('h1');
const btnIncrease = document.querySelector('#increase');
const btnDecrease = document.querySelector('#decrease');

// 액션 이름
// 이름은 문자열 형태로, 주로 대문자로 작성한다.
const TOGGLE_SWITCH = 'TOGGLE_SWITCH';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';

// 액션 생성 함수
// type 값을 반드시 가지고 있어야 한다.
const toggleSwitch = () => ({ type: TOGGLE_SWITCH });
const increase = (difference) => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });

// 초기 값 설정
const initialState = {
    toggle: false,
    counter: 0,
};

// 리듀서 함수 정의
// 리듀서 함수가 맨 처음 호출될 때 state 값은 undefined 이다.
// state가 undefined라면 initailState를 기본 값으로 설정한다.

// 불변성 유지를 해야하는데 구조가 깊어지면 굉장히 번거롭고
// 가독성이 저하되기 때문에, 리덕스 상태는 최대한
// 깊지 않은 구조로 설게를 하고, 만약 깊어지거나 배열도 함께
// 다루는 경우 immer를 사용하면 더욱 쉽게 사용이 가능하다.
function reducer(state = initialState, action) {
    // action.type에 따른 분기 처리, 불변성 유지를 지켜주어야한다.
    switch (action.type) {
        case TOGGLE_SWITCH:
            return {
                ...state,
                toggle: !state.toggle,
            };

        case INCREASE:
            return {
                ...state,
                counter: state.counter + action.difference,
            };
        case DECREASE:
            return {
                ...state,
                counter: state.counter - 1,
            };
        default:
            return state;
    }
}

// 스토어 생성, 인자는 리듀서 함수
const store = createStore(reducer);

const render = () => {
    const state = store.getState(); // 현재 상태를 받아옴

    if (state.toggle) {
        divToggle.classList.add('active');
    } else {
        divToggle.classList.remove('active');
    }

    counter.innerText = state.counter;
};

render();

// 스토어의 상태가 바뀔 때 마다 render함수를 호출한다.
// 함수형태의 값을 전달한다.
// 추후 액션이 발생하여 상태가 업데이트 될 때 마다 호출된다.
store.subscribe(render);

divToggle.onclick = () => {
    store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
    store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
    store.dispatch(decrease());
};

react-redux

우리가 전에 했던 context와 느낌이 비슷하고 더욱 확장된 형태의 라이브러리가 아닌가 싶다.
모듈을 추가한다 yarn add redux react-redux

리덕스 기본적인 폴더링은 actions, constants, reducers 폴더를 생성하고 분류하여 작업한다. actions와 reducers는 특정 상태에 대해 같은 이름의 js파일을 사용한다. 예를들어 actions에 counter.js가 있다면 reducers에도 counter.js를 만들어서 각각 역할에 맞는 코드를 작성하는 것이다.

수정시 여러 폴더를 오가며 수정하는 것에 불편함을 느낀 개발자들이 만든 덕스 폴더링이란 방법으로 폴더를 나누지않고 한 폴더에 정의하는 방법도 있다.

1. 액션

상태에 어떠한 변화가 필요하면 action이 발생한다.
액션 객체는 반드시 type을 가지고 있어야 한다.

{
	type: 'TOGGLE_VALUE',
	text: '안녕',
}

{
	type: 'ADD_TODO',
	data: {
		id: 1,
		text: '오늘 할 일',
	}
}

2. 액션 생성 함수

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

function addTodo(data){
  return {
    type: 'ADD_TODO',
    data: '데이터',
  }
}
const change = text=>({
  type: 'Input',
  text
})

3. 리듀서

변화를 일으키는 함수이다. 액션을 만들고 발생시키면 리듀서가 현재 상태로 전달 받은 액션 객체를 파라미터로 받아온다.
이 두 값을 참고해서 새로운 상태를 만들어 반환한다.

const initialState = {counter:1};

function reducer(state = initialState, action){
  switch(action.type)
  {
    case INCREMENT: 
      return {
        counter: state.counter + 1
      }
    default:
      return state;
  }
}

4. 스토어

프로젝트에 리덕스를 적용하기 위해 스토어를 만든다.
한개의 프로젝트에는 단 하나의 스토어만 가질 수 있다.
스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어있다.

5. 디스패치

스토어 내장함수이며, 디스패치는 액션을 발생시키는 기능을 한다.
dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어서 호출한다.
디스패치가 호출되면 스토어는 리듀서 함수를 실행시켜서 새로운 상태를 만들어준다.

6. 구독(subscribe)

스토어 내장함수이며, subscribe함수 안에 리스너 함수를 파라미터로 넣어서 호출해주면 리스너 함수가 액션이 디스패치 되어 상태가 업데이트 될 때 마다 호출된다.

const listener = ()=>{
  console.log('상태 업데이트');
}
const unSubscribe = store.subscribe(listener);

리액트 프로젝트에서 리덕스를 사용할 때 사용하는 패턴

  • 컨테이너 컴포넌트
    리덕스와 연동되어 있는 컴포넌트, 리덕스로 부터 ㅜ상태를 받아오기도 하고, 리덕스 스토어에 액션을 디스패치 하기도 한다.
  • 프레젠테이셔널 컴포넌트
    주로 상태관리가 이루어지지 않고 props만 받아와서 화면에 UI를 보여주기만 하는 컴포넌트이다.

그리고 우리가 리덕스의 개발에 더욱 도움을 주는 크롬 익스텐션을 소개한다.

크롬 웹 스토어에서 ReduxDevTools를 설치한다 → yarn add redux-devtools-extension

우리는 Ducks 폴더링 기법으로 사용된 예제를 만나보자.

// 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;
// components/Todos.js
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>
            <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
                        key={todo.id}
                        todo={todo}
                        onToggle={onToggle}
                        onRemove={onRemove}
                    />
                ))}
            </div>
        </div>
    );
};

export default Todos;

컨테이이너에서 컴포넌트와 리덕스를 연결시킨다.

// containers/CounterConatiner.js
import React from 'react';
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} />
    );
};


// mapStateToProps, mapDispatchToProps에서 반환하는 객체
// 내부의 값들은 컴포넌트의 props로 전달된다.
// export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
export default connect(
    (state) => ({
        number: state.counter.number,
    }),
    {
        increase,
        decrease,
    }
)(CounterContainer);
// 리덕스와 연동하려면 connect함수를 사용해야한다.
// mapStateToProps
// 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수
// mapDispatchToProps
// 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수
// connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
// containers/TodosContainer.js
import React from 'react';
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';

import Todos from '../components/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를 분리
    ({ todos }) => ({
        input: todos.input,
        todos: todos.todos,
    }),
    {
        changeInput,
        insert,
        toggle,
        remove,
    }
)(TodoContainer);
// moudules/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;
// modules/todos.js

// 액션 타입 정의
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

// 액션 생성 함수
export const changeInput = (input) => ({
    type: CHANGE_INPUT,
    input,
});

let id = 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,
                todos: 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;

독스 폴더링 패턴으로 modules 폴더에 필요한 리덕스를 정의하고 index.js에 모아서 export한다.

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

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

export default rootReducer;
// App.js
import React from 'react';
import CounterContainer from './containers/CounterContainer';
import Todos from './components/Todos';

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

export default App;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import rootReducer from './modules';

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

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
profile
프론트엔드 개발자

0개의 댓글