리덕스(Redux) 라이브러리

taehyung·2023년 11월 3일

React.js

목록 보기
17/24

리덕스 용어

State

상태는 애플리케이션의 데이터를 나타내며, 스토어에 저장됩니다. 리덕스의 핵심 목적은 이 상태를 중앙에서 관리하고 예측 가능하게 만드는 것입니다.


Action

{
	type: 'ADD_TODO',
    data: {
    	id:1,
        text:'리덕스배우기'
    }
}
{
  	type: 'CHANGE_INPUT',
  	text: '안녕하세요'
}

상태 변경의 종류를 나타내는 객체, 일반적으로 type 속성을 포함하고 추가 데이터를 담을 수 있다. type속성의 이름으로 어떤 목적으로 사용되는 액션인지 구분한다.

예시) 'INCREASE' : 값을 증가시키는 액션

Action 생성 함수

//일반 함수
function addTodo(data){
  return {
    type: 'ADD_TODO',
    data
  }
}

//화살표 함수
const changeInput = (text) => {
	return {
    	type: 'CHANGE_INPUT',
      	text
    }
}

어떤 변화를 일으켜야 할 때마다 액션 객체를 만들어야 하는데 매번 액션 객체를 직접 작성하기 번거롭고, 만드는 과정에서 실수로 정보를 놓칠 수 있기때문에 함수로 만들어서 사용하면 편리합니다.


reducer

const initialState = {
	counter : 1
}

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

리듀서는 이전 상태와 액션을 받아서 새로운 상태를 반환하는 함수입니다. 리듀서는 순수한 함수여야 하며, 동일한 입력에 대해서는 항상 동일한 출력을 생성해야 합니다. 이를 통해 예측 가능한 상태 업데이트를 보장합니다


Store

스토어는 현재 애플리케이션의 상태와 리듀서를 가집니다. 리듀서를 사용하여 스토어의 상태를 변경하고, 상태가 변경될 때마다 뷰를 업데이트합니다. 한 개의 프로젝트는 단 하나의 스토어만 가질 수 있습니다.


Dispatch

디스패치는 스토어의 메서드중 하나입니다. 액션을 스토어에 전달하여 상태를 변경하는 함수입니다. 액션을 발생시키는 이 함수는 사용하면 리듀서가 호출되어 상태가 업데이트됩니다.

dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어서 호출합니다.


Subscribe

구독은 스토어의 메서드중 하나입니다. 상태가 변경될 때 콜백 함수를 호출하는 함수입니다. store.subscribe(Func) 형식으로 사용하면 액션이 디스패치되어 상태가 업데이트 될 때마다 호출됩니다.

const listener = () => {
	console.log('상태가 업데이트됨')
}

const unsubscribe = store.subscribe(listener);

unsubscribe()

Middleware

미들웨어는 액션을 디스패치한 후, Reducer 함수가 실행되기 전에 추가적인 작업을 수행할 수 있는 확장 기능입니다. 예를 들어, 비동기 작업 처리, 로깅, 라우팅 등의 작업을 처리할 때 사용됩니다.


단방향 데이터 흐름 (One-Way Data Flow)

리덕스는 단방향 데이터 흐름을 따릅니다. 액션을 디스패치하면 스토어의 상태가 변경되고, 변경된 상태가 뷰로 전파되어 화면을 업데이트합니다.


리덕스의 세 가지 규칙

단일 스토어(Store)
하나의 애플리케이션에는 하나의 스토어만 존재해야 합니다. 다중 스토어는 특정 업데이트가 빈번하게 발생하거나, 애플리케이션의 특정 부분을 완전리 분리시킬 때 사용하기도 합니다. 하지만 상태관리가 혼잡해지므로 지양하는게 좋습니다.

읽기 전용 상태 (Read Only)
리덕스 상태는 읽기 전용입니다. 불변성을 유지하여 새로운 상태를 반환해야만 컴포넌트가 리렌더링 됩니다.

리듀서는 순수한 함수
리듀서는 순수한 함수여야 합니다. 순수한 함수는 다음 조건을 만족합니다.

  • 리듀서 함수는 이전 상태와 액견 객체를 파라미터로 받습니다.
  • 파라미터 값 외에는 의존하면 안됩니다.
  • 불변성을 유지하여 상태를 반환해야합니다.
  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 같은 결과를 반환해야 합니다 ( 멱등성 )

리듀서 함수 내부에서 랜덤 값을 만든다.
Date 함수를 사용하여 현재 시간을 가져온다.
네트워크 요청을 한다.

위와 같은 상황엔 파라미터가 같아도 다른 결과를 만들 수 있기 때문에 사용하면 안됩니다.

리듀서 함수 밖에서 처리하거나 리덕스 미들웨어에서 처리해야합니다.
주로 네트워크요청과 같은 비동기작업은 미들웨어에서 처리합니다.


아래부터 글 다듬어서 정리 할 것

프로젝트에 리덕스를 적용할 때 가장 많이 사용하는 UI패턴은 프레젠테이셔널 컴포넌트컨테이너 컴포넌트를 분리하는 것입니다.

프레젠테이셔널 컴포넌트 : 상태관리가 이루어지지 않고 Props를 받아와서 화면에 UI를 표현만해주는 컴포넌트 ( UI만 표현 )

컨테이너 컴포넌트 : 리덕스를 이용하여 상태를 받아오거나 리덕스 스토어에 액션을 디스패치 하기도하는 비즈니스 로직이 있는 컴포넌트 ( 리덕스 사용 비즈니스로직 컴포넌트 )

위 패턴이 필수는 아니지만 적용하면 코드의 응집도가 높아지므로 유지보수, 코드의 재사용에 큰 이점이 있습니다.

리덕스를 사용할 때
액션+타입, 액션 생성 함수, 리듀서코드 를 작성해야합니다. 이 코드들을 분류하는 두 가지 선택지가 있습니다.

  • 액션타입 , 액션 생성 함수, 리듀서코드 각기 다른 3개의 파일로 분류 ( 일반 방식 )
  • 액션타입, 액션 생성 함수, 리듀서코드 하나의 파일에 기능별로 분류 ( Ducks패턴 혹은 모듈 방식 )

편한 방법으로 사용하면 됩니다.

저는 Ducks 패턴, 모듈 방식으로 작성하는게 관리에 용이할것같아서 사용해보려고 합니다.

리액트를 다루는 기술 서적의 간단한 카운터 예제를 이용하겠습니다.

모듈 정의하기

1. 액션타입 정의하기

가장먼저 어떤 기능이 들어갈것인지 생각하고 액션타입을 정의합니다. 액션타입은 반드시 대문자로 정의해주세요.

// modules/counter.js
const INCREASE = 'counter/INCREASE'
const DECREASE = 'counter/DECREASE'

모듈이름/액션이름 과 같은 형태로 작성합니다.

단순히 액션이름만 지정한다면 프로젝트 크기에따라 충돌하는 일이 발생할 수 있습니다.

INITIALIZE 같은 네이밍은 충돌의 여지가 다분한 네이밍입니다. 앞에 모듈명을 붙혀주면 충돌할 확률이 적어지겠죠?

2. 액션 생성 함수 정의하기

// modules/counter.js
const INCREASE = 'counter/INCREASE'
const DECREASE = 'counter/DECREASE'

//{type: INCREASE} 를 리턴하는 함수
export const increase = () => ({type: INCREASE})
export const decrease = () => ({type: DECREASE})

3. 초기상태 및 리듀서 함수 정의하기

// modules/counter.js
const INCREASE = 'counter/INCREASE'
const DECREASE = 'counter/DECREASE'

//{type: INCREASE} 를 리턴하는 함수
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;

하나의 Store 에는 하나의 리듀서만 담을 수 있습니다.

import {createStore} from 'redux';

const counterStore = createStore(reducer) // O
const counterStore = createStore(reducer,reducer) // X

하나의 프로젝트에는 하나의 Store 만 사용하길 권장드렸었는데요. reducer 의 갯수가 늘어나도 스토어의 갯수는 하나여야만 합니다.

그렇게 때문에 기존에 만들었던 여러개의 리듀서를 하나로 합쳐서 스토어를 만들어야합니다.

그 작업은 리덕스에서 제공하는 combineReducers 라는 메소드를 사용하면 가능합니다.

//파일 이름을 index.js로 생성하면 import 할 때 디렉터리 명까지만 입력해도 자동으로 index.js를 불러옵니다.
//사용 예시 ) import rootReducer from './modules'

// /modules/index.js
import { combineReducers } from "redux"; //리덕스에서 제공하는 리듀서들을 병합해주는 메소드
import counter from "./counter"; //병합할 리듀서
import todos from "./todos"; //병합할 리듀서

//병합
const rootReducer = combineReducers({
  counter, // counter : counter 축약문
  todos,
});

export default rootReducer;

그리고 그런 작업을 마친 하나의 리듀서를 rootReducer 라고 합니다.

리액트에서 리덕스 적용, 스토어 생성은 최상위 index.js 파일에서 하는것을 권장합니다.
아무런 의미없이 rootReducer 라는 이름을 짓지는 않겠죠?

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import rootReducer from "./modules";
import { createStore } from "redux";
import { Provider } from "react-redux"; //리덕스 Provider 임포트

const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}> //상태를 사용할 하위 컴포넌트들 부모요소로 위치 | 스토어를 Provider의 Props로 전달해야 합니다.
      <App />
    </Provider>
  </React.StrictMode>
);

현재 createStore 는 과거의 문법이 되어버려서 삭선이 생깁니다.confiureStore 문법 사용을 권장하는데, 추후에 리덕스 툴킷을 배울 때 포스팅 해보겠습니다.

Redux DevTools
Redux DevTools 설치 및 적용에는 두가지 방법이 있습니다.

1. 크롬 확장프로그램 설치 및 생성
Redux DevTools 는 리덕스 개발자 도구이며, 크롬 확장 프로그램으로 설치하여 사용할 수 있습니다.

  1. 크롬 웹스토어에서 Redux DevTools 확장프로그램을 설치한다.
  2. 리액트 프로젝트의 Store 생성 시 아래와 같이 적용한다.
    const store = createStore( //스토어 생성
    	rootReducer, // 루트리듀서
    	window.__REDUX_DERTOOLS_EXTENSION__ && window.__REDUX_DEV_TOOLS_EXTENSION__() //Redux Dev Tools 적용
    );

2. npm 패키지 설치

npm install redux-devtools-extentions

스토어 생성

import { composeWithDevTools } from 'redux-devtools-extension'; 

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

이제 개발자도구에서 Redux 탭을 이용하여 스토어의 상태를 확인할 수 있습니다.

Container 만들기

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

const CounterContainer = () => <Counter />;

export default CounterContainer;

위 컴포넌트를 리덕스와 연동하려면 redux 에서 제공하는 connect 메소드를 사용해야 합니다.

const makeContainer = connect(mapStateToProps, mapDispatchToProps)

makeContainer(연동할 타깃 컴포넌트)

mapStateToProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 전달

mapDispatchToProps : 액션 생성 함수를 컴포넌트의 props로 전달

connect 메소드는 함수를 반환하고, 반환된 함수에 파라미터에 컴포넌트를 전달하면, 전달된 컴포넌트는 리덕스와 연동됩니다.

profile
Front End

0개의 댓글