[Redux] Redux 개념 학습

Yalstrax·2021년 7월 9일
1

Front End

목록 보기
5/8
post-thumbnail

Redux를 공부한 내용을 기록하고자 정리해보았습니다. 잘못된 부분이 있다면 댓글 부탁드립니다! 🙌


Redux 🔮

Redux를 한마디로 정의하자면 자바스크립트 앱에서 예측 가능한 상태 관리를 가능하게 하는 컨테이너 입니다.

React 에선 컴포넌트 내에서 State를 관리 합니다. 형제 컴포넌트 간 데이터를 주고 받을 경우, 부모 컴포넌트에 State를 정의해 부모 컴포넌트를 통해서 주고 받습니다.
형제 컴포넌트 간 직접적인 데이터 전달은 불가능 합니다.

만약, 컴포넌트의 갯수가 많아지고, 데이터를 주고 받는 경우가 많아진다면, 거쳐야할 컴포넌트가 많아집니다. 그 때마다, 부모 컴포넌트에서 Props로 데이터를 전달하는 것은 생산성과 코드의 가독성을 떨어뜨립니다.

이를 Redux로 개선할 수 있습니다.

Redux를 사용하기 전에, 세가지 원칙을 항상 기억하고 준수해야 합니다.

  • Single Source of Truth

단방향 데이터 흐름을 위해, 데이터를 저장하는 하나뿐인 저장 공간 Store에서 데이터를 관리해야 합니다.

  • State is Read-Only

React에서도 State를 변경할 때, State에 새로운 값을 할당하는 것이 아니고, setState를 사용해 State를 변경했습니다. Redux도 마찬가지로, 불변성을 지키기 위해, Action을 통해 State를 업데이트합니다.

  • Changes are made with Pure Functions

State를 갱신하는 함수는 오직 순수 함수로만 갱신합니다.

1. Flux

ReduxReducerFlux를 합친 단어입니다.
Flux에 대해 간단하게 정리해보겠습니다.

Flux는 데이터가 이동하는 패턴입니다.
그림에서 보시듯, 단방향 데이터 흐름입니다.

View는 우리가 보는 화면, UI입니다.
Store는 데이터 및 상태가 저장되는 공간입니다.
Action은 데이터 및 상태를 업데이트하기 위한 행동입니다.
DispatcherActionStore로 전달하는 전달자의 역할을 수행합니다.

그래서, 단방향 데이터 흐름인 Flux 패턴을 간략히 정리하자면,

View에서 상태를 변경하기 위한 이벤트가 발생하면, 그 로직에 맞는 ActionDispatcher의 전달인자로 전달합니다.
Action을 담은 DispatcherStore로 전달하고, Store에서 Action을 수행하여 상태를 업데이트합니다.
업데이트된 저장 공간은 다시 View를 통해 사용자에게 보여집니다.

2. Reducer (Feat. 귀멸의 칼날)

그렇다면, ReducerFlux 패턴에서 추가된 개념인데, 이 Reducer는 어떤 역할을 할까요?

먼저, Redux의 단방향 데이터 흐름을 간략히 짚어보겠습니다.

  1. UI(View)에서 어떤 이벤트가 발생한다.
  2. 그 이벤트과 관련된 Action을 호출한다.
  3. Action은 type과 payload를 포함한다.
  4. Dispatcher의 전달인자로 Action을 담아 Reducer에게 전달해준다.
  5. Reducer는 Action의 type에 따라 기능을 수행한다.
  6. Store가 업데이트되고, 그 상태를 UI(View)에 표현한다.

아직도 저 개념이 잘 와닿지 않아 제 나름대로의 예시를 생각해보았습니다. 적절한 예시가 아니라면, 피드백 부탁드립니다! 🙏


귀살대

귀멸의 칼날 세계관에서 혈귀를 죽이기 위해 만들어진 비공인 준 군사조직

구분View(UI)StoreActionDispatcherReducer
예시귀멸의 칼날 배경(세상)귀살대임무꺽쇠 까마귀귀살대원


귀멸의 칼날의 배경은 다이쇼 시대라고 합니다.View는 우리가 보는 귀멸의 칼날의 배경(세상) 입니다.

세상에서 어떤 사건(이벤트)가 발생합니다. 귀멸의 칼날 세계관에서의 사건은 민간인이 혈귀에게 잡아 먹힌 경우가 될 수 있겠습니다.

민간인이 혈귀에게 잡아먹히는 사건이 발생했습니다. 귀살대의 당주 우부야시키 카가야는 이 소식을 듣고, 임무(Action)를 정의합니다. 임무는 다음과 같습니다.

임무(action) {
  수행자(type) : '렌고쿠 쿄쥬로',
    대상(payload) : {
    민간인을 죽인 혈귀 1
  }
}

귀살대원들에게 이를 전달하기 위해, 꺽쇠 까마귀(Dispatcher)에게 임무를 전달합니다.

꺽쇠 까마귀로부터 임무를 전달받은 귀살대원(Reducer)들은 임무의 내용을 확인하고, 수행자(type)가 자신이 아니면 임무를 수행하지 않습니다.

누가 수행해야 하는 임무인가?

임무의 수행자로 지정된 귀살대원은 임무를 수행합니다.

임무 수행에 있어 실패한다는 가정은 배제하겠습니다. 맡은 책무를 다 해야하니까요.

임무가 해결되면, 귀살대(Store)에 업데이트됩니다. 민간인을 죽인 혈귀는 귀살대원이 죽였고, 귀멸의 칼날 세상은 그 혈귀가 없어진 새로운 상태로 갱신되었습니다.

여기서 Reducer의 예시로 귀살대원을 들었습니다.
ReducerActiontype에 따라 Action을 수행합니다. 이를 swith case 또는 if 조건문으로 구분할 수 있는데, 아래와 같이 Reducer를 작성해볼 수 있습니다.

const 귀살대원 = (이 세상의 혈귀들, 임무) => {
  switch (임무.수행자) { 
    case '렌고쿠 쿄쥬로':   // 임무의 수행자가 렌고쿠라면 아래와 같이 수행한다.
      return Object.assign({}, 이 세상의 혈귀들.filter((혈귀) => 혈귀.이름 !== 임무.대상) 
      //이 세상의 혈귀들의 목록 중, 임무에 담긴 대상을 제외한 나머지 혈귀들을 새로 반환한다.
    default:
      return 이 세상의 혈귀들;
  } // 수행자가 렌고쿠가 아니라면, 임무를 수행하지 않는다. 
}

예시가 적절한가요? 💁

뭔가 글을 쓰다보니 오히려 Redux를 더 혼란스럽게 만드는 예시인 것 같습니다... 😓


Reducer는 데이터 흐름에서 다음과 같은 위치에 위치합니다.

View -> Action -> Dispatcher -> Reducer(s) -> Store -> View

공식 문서의 말을 빌리면, Reducer는 기본적으로 Action에서 정보를 가져와 Store에 저장된 이전 상태와 함께 새로운 상태로 축소(Reduce)합니다.

위에 기술한 Flux 패턴과 다른 점은, Reducer에 조건을 설정하여, 인자로 들어온 Action의 타입에 맞는 Action만 실행하고, 조건에 부합하지 않는 Action이라면 기본값을 반환한다는 것입니다.

그래서, Reducer를 아래와 같이 구현할 수 있습니다.

(prevState, action) => newState

첫번째 인자로 현재 상태를 받고, 두번째 인자로 Action을 받습니다.
Action의 구성은 아래와 같습니다.

{
  type: 'TODO_ADD',
  todo: { id: '0', name: 'learn redux', completed: false },
}

Reducer의 두번째 인자로 들어온 Actiontype에 따라, 해당하는 type에 대한 조건이 Reducer에 존재한다면 함수를 실행하는 것입니다.

즉, Redux에서 Actiontype을 꼭 지정해줘야합니다. 그 아래에 있는 데이터를 가리키는 부분은 payload라 하여, 문자열부터 객체까지 다양한 payload를 담을 수 있습니다.

if 조건문을 사용하거나, switch case를 사용하여 결정할 수 있습니다.

쇼핑몰 홈페이지에서 장바구니에 물건을 추가하는 상황을 예시로 들어보겠습니다.

const itemReducer = (state, action) => {
  switch (action.type) { 
    case ADD_TO_CART:   // action type이 ADD TO CART인 경우
      return Object.assign({}, state, {
        cartItems: [...state.cartItems, action.payload]
      }) 
      // state 데이터에 payload로 들어온 값을 추가한 새로운 배열을 할당하여 반환합니다.
    default:
      return state;
  } // 다른 type의 Action이 들어오면 기본값을 반환합니다.
}

이렇게 ReducerAction을 구성할 수 있습니다. 이제, 구성한 요소들을 컴포넌트와 연결을 해야 합니다. 연결하기 위해 Redux hooks를 사용할 수 있습니다.

  • useSelector()

useSelector()는 컴포넌트와 State를 연결하는 역할을 합니다. 이 메소드를 통해 StoreState에 접근할 수 있습니다.

이 메소드를 적용하기 위해, Presentational componentContainer component 개념을 알아야합니다.

Presentational component는 사용자에게 어떻게 보여질 지의 기능만 수행하는 컴포넌트입니다.

Container component는 사용자가 어떤 이벤트를 발생시키거나, 상태의 변화가 발생할 때 이를 어떻게 동작할 지, 그 기능을 수행하는 컴포넌트입니다. 즉, useSelector() 메소드는 이 Container component에서 사용하는 것으로 State에 접근할 수 있습니다.

  • useDispatch()

useDispatch()Action 객체를 Reducer로 전달해주는 메소드입니다. 이 메소드가 정의되는 위치는 Action을 실행시키는 클릭, 키보드 입력 등 이벤트가 발생되는 컴포넌트가 될 수 있습니다.

3. Redux의 장점

글을 마무리하기 전에, Redux를 사용하는 것으로 얻을 수 있는 이점들을 정리해보겠습니다.

  • 상태를 예측 가능하게 만들어 준다.

Reducer는 순수 함수로 작동하기 때문에, 다음 상태가 어떻게 변화될 지 예측이 가능합니다.

즉, 테스트 케이스를 작성하기 용이합니다.

  • 유지 보수 용이

컴포넌트가 많아져 State 또는 데이터를 Props로 전달한다면, 컴포넌트에서 컴포넌트로, 컴포넌트에서 컴포넌트로... 소위 Props Drilling이 발생합니다. Props를 전달하는 과정에서 버그가 발생하면, Props를 가진 모든 컴포넌트를 수정해야합니다.

Redux를 통해 전역 상태에서 데이터를 관리한다면, ActionState의 로그를 기록할 수 있어, 어떤 Action에서 문제가 발생했는지 추적이 가능합니다.

  • 위 기능을 수행하는 디버깅 툴(Redux Dev Tool)을 제공합니다.

제가 공부한 내용을 바탕으로 제 언어로 풀어서 정리해보았습니다. 잘못된 부분이 있다면 피드백 부탁드리며, 긴 글 읽어주셔서 감사드립니다! 😘

profile
즐겁다면 그것만으로 만만세!

0개의 댓글