코로 넘어져도 헤딩만 하면 그만

리액트를 편리하게 쓰는 라이브러리, '리덕스' 본문

CODE STATES 44

리액트를 편리하게 쓰는 라이브러리, '리덕스'

꼬드리 2023. 4. 24. 12:11

 

Single Sorce of Truth, 핵심은 '중앙집중적'

리덕스는 리액트를 편리하게 쓸 수 있게 돕는 상태 관리 라이브러리 중 하나입니다. 물론 비슷한 기능을 하는 다른 라이브러리들이 있지만 현재는 리덕스가 가장 대중적으로 쓰이고 있다고 보시면 됩니다.

그렇다면 왜 리액트에 굳이 리덕스까지 써서 상태 관리를 해줘야 할까요? 리덕스는 왜 쓰는 거죠?

 

 

🚩리덕스를 사용하면 편한 이유

1) 너무 복잡해... props 줄이기

리액트에서는 기본적으로 상태 state를 전하기 위해 상위 컴포넌트의 props를 하위 컴포넌트에게 내리고, 하위 컴포넌트에서 이를 받아쓰는 방식을 채택합니다. 이는 리액트가 추구하는 단방향 흐름과 관련이 있습니다. 직접적으로 아래쪽에서 상위의 상태를 바꿀 수 없다는 것이죠.

뭐... props를 한두번 내리는 것 정도는 괜찮습니다. 다섯번 정도까지는 props를 내릴 수도 있을 겁니다(벌써 복잡해지기 시작하지만요). 하지만 100번째 하위 컴포넌트에서 props를 받고 싶다면요? 100번이나 컴포넌트 사이에서 props를 내리는 과정이 필요해요. 또 기껏 만들어두니 컴포넌트 구조에 대변동이 있다면? 중간 컴포넌트가 빠진다면? 일일이 props를 수정해 주어야 하니 규모가 커질수록 끔찍하게 번거로워지겠죠. 코드도 복잡해지고요.

 

바로 이 경우 요긴하게 리덕스를 사용합니다. 리덕스는 자주 사용될 중요한 상태들을 하나의 파일로 저장소처럼 보관해줍니다. 이제 리액트의 모든 컴포넌트들이 이렇게 만든 하나의 파일에서 상태state를 바로 꺼내 쓸 수 있는 거에요. 따라서 리덕스를 활용하면 리액트에서 컴포넌트끼리 props를 줄줄이 내려가며 props drilling 하던 현상을 없애주기 때문에 전체적으로 코드가 짧아지고, 관리가 편해집니다.

 

2) 어디서 문제가 생긴 거지? 리액트 상태 쉽게 관리

앞서 리덕스는 중요한 상태들을 하나의 파일(저장소)에 담아둔다는 걸 설명했습니다. 따라서 리덕스를 사용하면 '상태'를 관리하는 게 훨씬 쉬워집니다. 만약 수많은 컴포넌트에서 자유롭게 state를 갖다 쓰고 있었다면 갑자기 오류가 생겼을 때 버그를 추적하기 어려워지죠.

하지만 리덕스를 쓰면 사용되는 상태의 아래에 미리 어떻게 수정할지 원하는 방법(type)들을 전부 기입해둡니다. 이후 state를 가져와 쓰고 싶은 컴포넌트에서 미리 적어둔 방법들type을 달라고 요청합니다. 이제는 이유 모를 에러가 발생한다면 개별 컴포넌트들을 뒤질 게 아니라, 처음 state를 기입해둔 저장소에서 찾고 수정하며 관리해줄 수 있습니다. 정말 상태를 관리하는게 쉬워졌습니다.

 

리덕스를 왜 많이 쓰는지 감이 오시나요?

대형 프로젝트에서는 redux 같은 상태 관리가 반드시 필요할 것 같습니다. 고도로 복잡해진 구조 내에서 에러 발생 범위를 하나하나 찾고 검수하며 props 뚫는 건 너무 비효율적이니까요.

 

다시 정리하자면, 리덕스는 하나의 상태를 갖습니다. 상태는 일종의 객체입니다. 이처럼 리덕스는 하나의 객체 내부에 모든 상태들을 우겨넣는 방식으로 기존 리액트에서 문제가 되던 복잡성을 낮춰줍니다. 또 이렇게 만든 상태는 너무 중요하기에 외부에서 철저하게 차단시켜 광범위한 수정을 막습니다. 우리는 이 데이터를 직접 쓰지는 못하고 dispatcher과 reducer을 통해서만 수정할 수 있습니다. 이런 방식으로 외부에서 직접 제어 못하게 막아, 예기치 않게 state가 함부로 바뀌지 않도록 해줍니다. 데이터를 잘 보존해서 결과를 예측가능하게 해주는 거죠. 

 

 

🚩리덕스에서 기본적 흐름은 다음과 같습니다.

리덕스는 MVC의 단점을 극복하기 위한 단방향 Flux 패턴을 적용하고 있습니다.

Action → Dispatch → Reducer → Store

  1. 상태가 변경되어야 하는 이벤트가 발생하면, 변경될 상태에 대한 정보가 담긴 Action 객체가 생성됩니다.
  2. 이 Action 객체는 Dispatch 함수의 인자로 전달됩니다.
  3. Dispatch 함수는 Action 객체를 Reducer 함수로 전달해 줍니다.
  4. Reducer 함수는 Action 객체의 값을 확인하고, 그 값에 따라 전역 상태 저장소 Store의 상태를 변경합니다.
  5. 상태가 변경되면, React는 화면을 다시 렌더링 합니다.

오.. 슬슬 낯선 단어들이 등장합니다. 차례대로 한번 들여다 볼까요?

 

Action: 어떤 액션을 취할 것인지 정의해 놓은 객체. type은 필수 지정해야 합니다.

해당 Action 객체가 어떤 동작을 하는지 명시해 주는 역할이고 대문자와 Snake Case로 작성합니다. 또한 필요에 따라 payload를 작성해 구체적인 값을 전달합니다.

Action을 직접 작성하기보다는 Action 객체를 생성하는 함수(액션 생성자)를 만들어 사용하는 경우가 많다고 해요.

// payload가 필요 없는 경우
{ type: 'INCREASE' }

// payload가 필요한 경우
{ type: 'SET_NUMBER', payload: 5 }
// payload가 필요 없는 경우 action생성자
const increase = () => {
  return {
    type: 'INCREASE'
  }
}
// payload가 필요한 경우
const setNumber = (num) => {
  return {
    type: 'SET_NUMBER',
    payload: num
  }
}

 

 

Dispatch: Reducer로 Action을 전달해주는 함수. Dispatch의 전달인자로, Action 객체가 전달됩니다.

이 Action 객체를 전달받은 Dispatch 함수는 이후 Reducer를 호출하죠.  

// Action 객체를 직접 작성하는 경우
dispatch( { type: 'INCREASE' } );
dispatch( { type: 'SET_NUMBER', payload: 5 } );

// 액션 생성자(Action Creator)를 사용하는 경우
dispatch( increase() );
dispatch( setNumber(5) );

 

 

Reducer: Dispatch로부터 받은 Action 객체의 type 값에 따라 상태를 변경시키는 함수. 이때, Reducer는 순수함수여야 합니다. 외부 요인으로 인해 기대한 값이 아닌 엉뚱한 값으로 상태가 변경되는 일이 없어야 하기 때문입니다.

Reducer을 생성할 때는 초기 상태를 인자로 요구합니다. 이후 switch를 사용해서 Action의 type에 따라 분기해줍니다.

마지막으로 리듀서의 리턴값은 새로운 '상태'가 되죠.

const count = 1

// Reducer를 생성할 때에는 초기 상태를 인자로 요구합니다.
const counterReducer = (state = count, action) => {

  // Action 객체의 type 값에 따라 분기하는 switch 조건문입니다.
  switch (action.type) {

    //action === 'INCREASE'일 경우
    case 'INCREASE':
			return state + 1

    // action === 'DECREASE'일 경우
    case 'DECREASE':
			return state - 1

    // action === 'SET_NUMBER'일 경우
    case 'SET_NUMBER':
			return action.payload

    // 해당 되는 경우가 없을 땐 기존 상태를 그대로 리턴
    default:
      return state;
	}
}
// Reducer가 리턴하는 값이 새로운 상태가 됩니다.

만약 여러 개의 Reducer를 사용하는 경우 combineReducers 메서드를 사용해서 하나의 Reducer로 합쳐줄 수 있습니다.

 

 

Store: 상태가 관리되는 오직 하나뿐인 저장소. Redux 앱의 state가 저장되어 있는 공간입니다. 아래와 같이 생성합니다.

import { createStore } from 'redux';

const store = createStore(rootReducer);

 


이제, 이렇게 만들어준 개념들을 하나로 묶어주는 과정이 필요합니다. Reduce Hook이 그 역할을 해줄 겁니다.

useSelector(), useDispatch()라는 두 메서드에 주목해볼까요?

 

⭕useDispatch()

Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환하는 메서드

import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
dispatch( increase() )
console.log(counter) // 2

useSelector()

컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드.

// Redux Hooks 메서드는 'redux'가 아니라 'react-redux'에서 불러옵니다.
import { useSelector } from 'react-redux'
const counter = useSelector(state => state)
console.log(counter) // 1

 

 

🚩리덕스의 세 원칙

리덕스에서는 세 가지 원칙이 존재합니다. 이 원칙들을 기억하면 리덕스를 사용할 때 도움이 될 거에요.

 

1. Single source of truth

동일한 데이터는 항상 같은 곳에서 가지고 와야 합니다.

즉, Redux엔 데이터를 저장할 Store라는 단 하나뿐인 공간이 있습니다.

 

2. State is read-only

상태는 읽기 전용. React에서 상태 갱신함수로만 상태를 변경할 수 있었던 것처럼 Redux에서의 상태도 직접 변경할 수 없습니다. Action 객체가 있어야만 상태를 변경할 수 있다는 뜻이죠.

 

3. Changes are made with pure functions

변경은 순수함수를 사용해서만 가능합니다.

상태가 엉뚱한 값으로 변경되는 일이 없도록, Reducer은 순수함수로 작성되어야 합니다.

 

 

출처: https://redux.js.org/

Comments