끄적끄적 코딩
article thumbnail
Published 2023. 7. 1. 03:33
[React] 리덕스 라이브러리 React

리덕스

  • 가장 많이 사용되는 리액트 상태 관리 라이브러리
  • 모든 state를 store라는 중앙 상태 저장소를 사용하여 저장하고 reducer 함수를 통해 수정.
  • 컴포넌트의 상태 업데이트 관련 로직을 다른 파일로 분리하여 효율적으로 관리할 수 있게 해줌.
  • 컴포넌트끼리 똑같은 상태를 공유해야 할 때도 다른 컴포넌트를 거치지 않고 손쉽게 상태 값을 전달하거나 업데이트 가능.
  • 전역 상태를 관리할 때 효과적.
    • Context API를 통해서도 똑같은 작업을 수행할 수 있지만 프로젝트 규모가 클 경우에는 리덕스를 사용하는 편이 좋음. (상태를 더욱 체계적으로 관리)


장점

1. props drilling 방지

- props drilling이란 컴포넌트가 특정값을 사용하기 위해 여러 상위 컴포넌트에서 props를 정의하고 전달받는 과정을 말함.
- 이 과정은 여러 컴포넌트에서 사용하지 않는 props를 정의해야 하고 필요없는 코드를 증가 시킬 수 있음.
- 리덕스는 store라는 state 저장소에서 사용하고 싶은 state만 끌어와서 쓸 수 있기 때문에 이런 필요없는 코드를 줄일 수 있음.

2. 컴포넌트를 순수 함수로 유지

- 리액트의 컴포넌트는 어떤 외부 상태에 의존하지도 않고 변경시키지도 않는 순수 함수로 유지되도록 하기를 권장함. (동일한 인자가 전달되면 언제나 동일한 값을 반환하길 원함)
- 다음과 같은 상황에서 컴포넌트는 순수 함수를 유지할 수 없는데 이 때 사이드 이펙트들을 store로 위임하여 순수 함수로 만들어 줄 수 있음.

1) 외부 데이터를 가져와 사용하는 컴포넌트
- 데이터의 변경에 따라 다른 값을 반환할 수 있음.

2) 브라우저 이벤트에 의존하는 컴포넌트
- 브라우저 이벤트는 예측할 수 없으며 이벤트에 대한 처리는 언제나 다른 결과를 가져올 수 있음.

3. 코드의 유지 보수성을 높이고 작업 효율도 극대화해줌.

 

리덕스 개념

액션(action)

상태에 어떠한 변화가 필요하면 액션이란 것이 발생.

하나의 객체로 표현됨.

{
	type: 'TOGGLE_VALUE';
	data: {
		id: 1,
		text: '안녕하세요'
	}
}

type은 액션의 이름으로 액션 객체가 반드시 가지고 있어야 하는 필드.

그 외의 값은 상태 업데이트를 할 때 참고해야 할 값.


액션 생성 함수

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

변화를 일으켜야 할 때마다 액션 객체를 만들어야 하는데 직접 작성하면 번거롭고 실수가 발생할 수 있음.

이러한 일을 방지하기 위해 함수로 만들어서 관리.

function addTodo(data) {
	return {
		type: 'ADD_TODO',
		data
	};
}
// 화살표 함수로도 만들 수 있음.

const changeInput = text => ({
	type: 'CHANGE_INPUT',
	text
});



리듀서(reducer)

변화를 일으키는 함수.

액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아옴.

이 두 값을 참고하여 새로운 상태를 만들어서 반환.

const initialState = {
  counter: 1,
};
// state(이전 상태)가 없다면 initialState로 초기화.
function reducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return {
        counter: state.counter + 1,
      };
    default:
      return state;
  }
}

 

스토어(store)

중앙 상태 저장소.

프로젝트에 리덕스를 적용하기 위해 스토어를 만듬.

한 개의 프로젝트단 하나의 스토어만 가질 수 있음.

스토어 안엔 현재 애플리케이션 상태와 리듀서가 들어가 있으며 몇 가지 중요한 내장 함수를 지님.


디스패치(dispatch)

스토어의 내장 함수 중 하나.

액션을 발생시키는 것 = 상태 변경을 요청하는 것.

액션 객체를 파라미터로 넣어 호출. ( dispatch(action) )

이 함수가 호출되면 스토어는 리듀서 함수를 실행시켜 새로운 상태를 만들어 줌.


구독(subscribe)

스토어의 내장 함수 중 하나.

콜백 함수를 파라미터로 넣어서 호출해 주면 콜백 함수가 액션이 디스패치되어 상태가 업데이트될 때마다 호출.

const listener = () => {
  console.log("update");
};

// 상태가 변화할 때마다 listener()를 호출하고 구독을 취소할 수 있게 해주는 함수 반환.
const unsubscribe = store.subscribe(listener);

unsubscribe(); // 추후 구독을 비활성화할 때 함수를 호출.

 

리액트 없이 쓰는 리덕스

리덕스는 리액트에 종속된 라이브러리가 아님. 다른 UI 라이브러리/프레임워크와 함께 사용할 수도 있으며 바닐라 자바스크립트와 함께 사용할 수도 있음.

이번엔 바닐라 자바스크립트 환경에서 리덕스를 사용하여 리덕스의 핵심 기능과 작동 원리를 이해해 봅시다.


Parcel로 프로젝트 만들기

parcel이라는 번들러를 사용하여 아주 쉽고 빠르게 웹 애플리케이션 프로젝트를 구성할 수 있음.

1. 설치

$ yarn global add parcel-bundler

2. 프로젝트 디렉토리를 생성한 후 package.json 파일 생성.

$ mkdir vanilla-redux
$cd vanilla-redux
$ yarn init -y

3. index.html과 index.js, index.css 파일 만들고 코드 작성 후 개발용 서버 실행.

$ parcel index.html
개발 서버는 파일을 저장할 때마다 자동으로 새로 고침됨.

4. 리덕스 모듈 설치

$ yarn add redux

 

간단한 UI 구성하기

index.html을 수정하여

// index.css

.toggle {
    border: 2px solid black;
    width: 64px;
    height: 64px;
    border-radius: 32px;
    box-sizing: border-box;
}

.toggle.active {
    background: yellow;
}
// index.html
<html>
  <head>
    <link rel="stylesheet" href="index.css" type="text/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>

이러한 화면을 구성함.


DOM 레퍼런스 만들기

여기선 UI를 관리할 때 별도의 라이브러리를 사용하지 않기 때문에 querySelector를 통해 DOM 노드를 가져와 직접 수정해 주어야 함.

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와 action 값을 받음.

...
// state가 undefined일 때는 initialState를 기본값으로 사용
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;
  }
}

리듀서 함수가 맨 처음 호출될 때 이전 상태(state)는 undefined이므로 initialState를 기본값으로 설정.

또한 리듀서는 상태의 불변성을 유지하면서 데이터에 변화를 일으켜야 하므로 spread 연산자를 사용.

→ 객체 구조가 복잡해지면 spread 연산자로 불변성을 관리하기 힘드므로 리덕스의 상태를 최댜한 깊지 않은 구조로 유지하거나 immer 라이브러리를 사용하여 리듀서를 작성하면 좋음.


스토어 만들기

스토어를 만들 때는 createStore 함수 사용.

import를 해와야 하며 파라미터로 리듀서 함수를 넣어 주어야 함.

import { createStore } from "redux";
...
const store = createStore(reducer);

해당 코드를 작성하면 createStore로 표시됨을 볼 수 있음.

→ 이는 @reduxjs/toolkit의 권장 사용을 유도하기 위해서 createState가 권장되진 않지만 사용을 금하지 않는다는 의미.


reduxjs/toolkit의 사용이 권장되는 이유

1. 개발 생산성 향상
- Redux에서 반복적으로 작성해야 하는 보일러플레이트 코드를 줄여줌.
- createAction, createReducer 함수를 사용하여 간단하고 명확한 코드를 작성할 수 있고 액션과 리듀서를 결합한 slice를 사용하여 코드 구조화를 용이하게 함.

2. 불변성 유지
- 내부적으로 Immer 라이브러리를 사용하여 불변성(immutable)을 유지하면서 상태 업데이트를 직관적으로 처리할 수 있게 해줌.

3. 비동기 작업 관리
- 비동기 작업을 처리하기 위한 간편한 방법을 제공.
- createAsyncThunk 함수를 사용하여 비동기 작업을 처리하고 createSlice와 함께 사용하여 상태 업데이트 및 에러 처리를 간단하게 관리할 수 있음.

4. Redux 애플리케이션 개발 및 디버깅을 용이하게 함.


render 함수 만들기

이 함수는 상태가 업데이트될 때마다 호출되며 리액트의 render와는 다르게 html을 사용하여 만들어진 UI 속성을 상태에 따라 변경해 줌.

...
const render = () => {
  const state = store.getState(); // 현재 상태를 불러옴.
  // 토글 처리
  if (state.toggle) {
    divToggle.classList.add("active");
  } else {
    divToggle.classList.remove("active");
  }

  // 카운터 처리
  counter.innerText = state.counter;
};



구독하기

상태가 바뀔 때마다 render 함수가 호출되도록 store의 내장 함수인 subscribe를 사용.

파라미터로 함수 형태의 값을 전달.

여기선 subscribe 함수를 직접 사용하지만 컴포넌트에서 리덕스 상태를 조회하는 과정에서 react-redux라는 라이브러리가 이 작업을 대신해 주기 때문에 리액트 프로젝트에서 리덕스를 사용할 때는 이 함수를 직접 사용하지 않음.

(...)

render(); // 초기 렌더링
store.subscribe(render);

 

액션 발생시키기

액션을 발생시키기 위해 스토어의 내장 함수인 dispatch를 사용해야 하고 파라미터로 액션 객체를 넣어줌.

(...)

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


업데이트가 일어나는 단계

  1. divToggle을 클릭하면 toggleSwitch라는 액션 객체를 액션 생성 함수로부터 반환 받음.
  2. 그 액션 객체를 통해 dispatch 함수를 호출하여 상태 변경 요청.
  3. dispatch가 리듀서를 호출.
  4. 리듀서는 현재 상태와 생성된 액션 객체를 인자로 받아서 새로운 상태 객체를 만들어서 반환.
  5. 상태가 변화하였으므로 subscirbe되어 있는 render 함수가 호출이 되면 UI가 변화하게 됨.

 

리덕스의 세 가지 규칙

리덕스를 프로젝트에서 사용할 때 지켜야 할 세 가지 규칙을 알아봅시다.

1. 단일 스토어

- 하나의 애플리케이션 안에는 하나의 스토어가 들어 있음.
- 여러 개의 스토어를 사용하는 것이 불가능하진 않지만 상태 관리가 복잡해질 수 있으므로 권장 X.

2. 읽기 전용 상태

- 리덕스 상태는 읽기 전용.
- 기존 리액트에서 state의 불변성을 유지시켜야 했던 것 처럼 리덕스에서도 상태를 업데이트 할 때 기존의 객체는 건드리지 않고 새로운 객체를 생성해 주어야 함.
- 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교(shallow equality) 검사를 하기 때문.

3. 리듀서는 순수한 함수

- 변화를 일으키는 리듀서 함수는 순수한 함수여야 함.
- 순수한 함수는 다음 조건을 만족.

  • 이전 상태와 액션 객체를 파라미터로 받음.
  • 파라미터 외의 값에는 의존하면 안됨.
  • 이전 상태는 절대 건드리지 않고 새로운 상태 객체를 만들어서 반환. (불변성 유지)
  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환.

따라서 리듀서 함수를 작성할 때는 위 네 가지 사항을 주의하여 작성해야 함.

예를 들어 리듀서 함수 내부에서 랜덤 값을 만들거나 Date 함수를 사용하여 현재 시간을 가져오거나 네트워크 요청을 한다면 파라미터가 같아도 다른 결과 값이 나올 수 있으므로 사용하면 안됨.

이러한 작업은 리듀서 함수 바깥에서 처리하거나 액션을 만드는 과정에서 처리해도 되고 리덕스 미들웨어에서 처리해도 됨.

검색 태그