[Redux] Redux

Dodam·2023년 8월 31일
0

[Redux]

목록 보기
2/11
post-thumbnail

Redux란 무엇인가?

리덕스는 애플리케이션의 상태를 관리하기 위한 오픈소스 자바스크립트 라이브러리이다.
대표적으로 리액트에서 많이 사용되지만 리액트만을 위한 라이브러리는 아니며 다른 환경에서도 사용할 수 있다.

리액트에서 사용하는 상태관리 라이브러리에는 Redux, MobX, Apollo-Client, Recoil 등이 존재하지만, npm 다운로드 수를 확인해보면 최근 6개월 간의 다운로드 수 중에서 리덕스가 압도적으로 높은 편이다.

기본 개념

리덕스는 다음과 같은 구조로 상태를 관리한다. Component에서 Action Creator를 통해 Action을 생성하고, 그 Action을 Dispatch함수로 실행시켜준다. 그러면 Store에서 해당 Reducer로 매칭되는 Action이 있는지 확인하고 Store에 저장된 상태를 변경시켜준다.

Action

action이란 특정 기능을 수행, 호출하기 위한 데이터를 표현해 놓은 객체이다. 리덕스에서 상태를 설정하기 위한 모든 요청은 이 action 객체를 통해 일어나게 된다.

const ADD_TODO = 'ADD_TODO'

{
	type: ADD_TODO,
    data: {
    	id: 0,
        text: "redux"
    }
}

action 객체는 type 필드를 필수적으로 가지고 있어야 하고, 그 외의 값들은 개발자 마음대로 넣어줄 수 있다.

interface AddTodo {
	type: typeof ADD_TODO;	// action type값을 상수로 미리 선언해 놓은 경우
  	data: {
    	id: number;
      	text: "redux"|"mobX"
    }
}

타입스크립트의 경우 action 객체의 인터페이스도 생성해줘야 한다.

Action Creator

Action Creator는 action을 만드는 함수이다. 리덕스를 사용하면서 필수적으로 필요한 함수는 아니지만 Action creator를 통해 쉽게 action을 생성해 상태를 관리할 수 있다.

const addTodo = (data: {id: number, text: "redux"|"mobX"}): AddTodo => ({
	type: ADD_TODO,
  	data
});

다음과 같이 단순하게 인자값을 받아와 action 객체 형태로 만들어 준다.

Reducer

reducer는 store의 상태에 변화를 주기 위해 사용되는 함수이다.

const reducer = (state: initialState, action: ReducerAction) => {
	switch(action.type) {
      case ACTION_NAME: {
      	return
      }
    }
}

첫 번째 인자는 초기 상태값을 받게 되는데, 초기 상태의 인터페이스를 만들어 적용시킨다. 두 번째 인자는 action으로 action들의 값들을 타입으로 만들어 적용시킨다. type ReducerAction = ACTION_NAME | ... 의 형식으로 구현한다. 그 후에 switch문을 사용해 action의 타입별로 store의 상태에 변화를 줄 수 있다.

Store

리덕스에서 상태를 저장하는 저장 공간이다. 리덕스에서는 한 애플리케이션당 하나의 store를 가지게 된다. store 안에는 현재 애플리케이션의 상태와 reducer가 들어있고, 추가적으로 몇 가지 내장 함수들이 들어있다.

dispatch

dispatch는 store의 내장함수 중 하나로, action을 발생시키는 것이라고 이해하면 편하다.
dispatch 함수에 action을 파라미터로 전달해 호출하면 store는 reducer 함수를 실행시켜서 해당 action을 처리하는 로직이 있다면 로직을 실행시켜준다.

Redux의 3가지 규칙

1. 하나의 애플리케이션 안에는 하나의 Store가 있다.
store는 단 하나만을 생성해서 상태를 관리할 수 있다. (여러 개의 store를 사용할 수는 있지만 권장하는 방법은 아니다.) 그 대신 reducer를 여러 개 만들어서 다양한 상태를 구분해 관리할 수 있다.

2. 상태는 읽기전용 이다.
리액트에서 상태에 불변성을 유지시켜주는 것과 비슷하다. 기존의 상태는 건드리지 않고 새로운 상태를 변경시켜준다. 마치 setState와 비슷하다. 리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 상태가 변경되었는지 검사를 확인하기 위해 shallow equality 검사를 하기 때문이다.

3. 변화를 일으키는 함수, Reducer는 순수한 함수여야 한다.
reducer 함수는 상태와 액션 객체를 인자로 받게 되는데, 이때 이전의 상태를 변경하는 것이 아닌 새로운 상태 객체를 만들어서 반환해야 한다. 그리고 같은 인자로 호출된 reducer는 언제나 같은 값을 반환하는 순수 함수로 만들어져야 한다.

동일한 인풋이라면 동일한 아웃풋이 있어야 한다. 그러나 일부 로직들 중 실행할 때마다 다른 결괏값이 나타날 수 있다. new Date()나 Math.random(), 네트워크 요청 등이 있는데 이러한 작업들은 결코 순수하지 않은 작업이므로, Reducer 함수의 바깥에서 처리해줘야 한다. 이런 것을 하기 위해 Redux Middleware를 사용한다.

React에서 Redux 사용하기

설치

리액트에서 리덕스를 사용하려면 다음 라이브러리들이 필요하다.

  • redux : Redux 모듈
  • react-redux : 리액트에서 리덕스를 사용하기 위한 유용한 도구들이 들어 있다.
  • 타입스크립트에서는 @types/react-redux를 추가로 설치한다.

module 생성

특정 기능을 구현하기 위해 필요한 action과 action creator, initialstate, reducer 함수가 들어있는 파일을 module 이라고 하며, 이 파일은 src/store/modules 경로에 저장한다.

Redux 공식 매뉴얼을 보면 action을 위한 파일과 reducer를 위한 파일을 따로 작성하는데 이것 말고 하나의 파일로 작성하는 방법을 Duck 패턴이라고 한다.

action type 정의하기

const ACTION_TYPE1 = "module-name/ACTION_TYPE1" as const;
const ACTION_TYPE2 = "module-name/ACTION_TYPE2" as const;
const ACTION_TYPE3 = "module-name/ACTION_TYPE3" as const;

Ducks 패턴을 사용할 땐 위와 같이 action 이름을 지을 때 문자열의 앞부분에 모듈 이름을 넣는다. 다른 모듈에서 작성하게 될 수도 있는 액션들과 충돌되지 않아야 하기 때문이다.

action creator 정의하기

interface Action {
	type: typeof ACTION_TYPE,
    data: {
    	id: number;
      	test: "redux"|"mobX";
    },
}

export const actionType1 = (data: {id: number, text: "redux"|"mobX"}): Action => ({
	type: ACTION_TYPE1,
  	data
});

...

action creator 함수를 정의할 땐 위와 같이 앞에 export를 선언해야 한다. 여기서 만든 함수들은 나중에 component에 리덕스를 연동하고 불러와서 사용하게 된다.

initialState 정의하기

interface InitialState {
	id: number;
  	text: string;
}

const initialState: InitialState = {
	id: 0,
  	text: "mobX"
}

initialState는 리덕스에서 전역으로 관리할 상태들의 초기 값들을 정의한다. initialState를 정의할 땐 인터페이스도 같이 정의해준다.

reducer 정의하기

type ReducerAction = ActionType1|... // action creator interface

const reducer1 = (state: InitialState = initialState, action: ReducerAction) => {
   switch (action.type) {
      case ACTION_TYPE1: {
      	return {
        	...state,
          	test: action.data.text,
       	}
      }
   }
}

export default reducer1

reducer 함수는 반드시 export default를 해줘야 한다. 나중에 store를 만들 때, 이 함수가 필요하다.

combineReducers로 reducer 합치기

다음과 같이 reducer가 여러 개일 때는 리덕스의 내장 함수인 combineReducers를 사용해 reducer들을 하나로 합칠 수 있다. 여러 개로 나눠진 reducer들을 sub reducer라고 하고, 하나로 합쳐진 reducer를 root reducer라고 한다.

src/store/modules/index.ts
import {combineReducers} from 'redux';
import reducer1, { InitialState1 } from './reducer1';
import reducer2, { InitialState2 } from './reducer2';

export interface RootState {
	reducer1: initialState1;
  	reducer2: initialState2;
}

export default combineReducers({
	reducer1,
  	reducer2,
  	// 다른 reducer들은 여기에 추가한다.
})

reducer들을 하나로 합칠 때 store의 모든 상태들의 타입인 RootState 인터페이스도 만들어줘야 한다. 나중에 상태값을 가져올 때 사용된다.

// 구조
{
	reducer1: {
    	id: 0,
        text: "mobX",
    },
    // 다른 reducer의 초기값들
}

reducer를 합치게 되면, root reducer의 초기값은 다음과 같은 구조가 된다.

store 만들기

store를 만들 때는 createStore라는 함수를 사용해 파라미터로 reducer를 넣어준다. 하나의 application에는 하나의 store가 있으므로 store는 앱이 시작되는 src/index.tsx에서 딱 한 번 만들면 된다.

src/index.tsx
import * as React from 'react';
import ReactDOM from 'react-dom';
// createStore와 root reducer 불러오기
import { createStore } from 'redux';
import rootReducer from './store/modules/index';

import App from './App';

// store 생성
const store = createStore(rootReducer);

const rootElement = document.getElementById('root');

ReactDOM.render(<App />, rootElement);
registerServiceWorker();

redux 개발자 도구 적용하기

redux 개발을 더욱 편하게 하기 위해 Redux Devtools라는 크롬 확장 프로그램을 활용하면 좋다.

// chrome redux devtools
const devTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__&& (window as any).__REDUX_DEVTOOLS_EXTENSION__();

store 연동

Provider를 사용해 React Projects에 store 연동

이제 만든 store를 리액트 컴포넌트에서 사용하기 위해 연동 시켜줘야 한다. 리액트에 Store를 연동할 때는 react-redux 라이브러리 안에 들어있는 Provider라는 컴포넌트를 사용한다. 기존의 TSX를 Provider로 감싸고, store는 Props로 Provider한테 넣어ㅜㄴ다.

src/index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';

import { createStore } from 'redux';
import rootReducer from './store/modules/index';
import { Provider } from 'react-redux';

import App from './App';

const devTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__();

const store = createStore(rootReducer, devTools);
console.log(store.getState());

const rootElement = document.getElementById('root');

ReactDOM.render(
	<Provider store={store}>
  		<App />
 	</Provider>,
	rootElement
);

여기까지가 리덕스를 사용하기 위한 준비 과정이다. 이제 각 컴포넌트마다 전역적으로 상태를 관리하고 싶다면 dispatch나 selector를 통해 상태를 호출하면 된다.

Dispatch를 통한 상태 업데이트

import { useDispatch } from 'react-redux';

const dispatch = useDispatch();

dispatch의 경우, hooks 문법으로 useDispatch라는 모듈을 사용해 가져오게 된다. 상태를 업데이튼 할 때는 생성한 dispatch 함수를 사용해 dispatch(action)의 형태로 상태를 업데이트 할 수 있다.

Selector를 사용해 상태값 가져오기

import { useSelector } from 'react-redux';

const {id, text} = useSelector((state: RootState) => state.reducer1);

이제 스토어에 있는 상태를 가져올 수 있다. useSelector라는 모듈을 사용해 가져오게 되는데, 가져오고 싶은 상태가 있는 reducer를 호출하면 상태를 가져올 수 있다.

profile
Good things take time

0개의 댓글