본문 바로가기
  • 프론트엔드 개발자 세오세오 | Frontend dev Seo
Learn to Code

Redux 기본 개념 정리

by CEOSEO 2021. 5. 16.
728x90
반응형

https://redux.js.org/

 

Redux란?

프로젝트를 만들다 보면 아주 다양한 상태들이 필요하게 된다. 상태는 다크모드/라이트모드 라던가 언어 선택과 같이 전역(global)에 영향을 끼치는 것들도 있고, 특정 컴포넌트 안에서만 관리되는 로컬 상태들도 있다.

 

앱의 상태는 유저와의 인터렉션 등을 통해 계속 변하게 되는데, 이 상태를 올바르게 관리하는 것은 당연히 중요한 일이다. 그러나 이렇게 올바르게 관리하고 싶다라는 긍정적인 마음가짐과는 다르게 프로젝트의 규모가 어느 정도 갖춰질수록 상태 관리는 어려워진다. 리덕스는 이와 같은 경우에 사용하는 상태(state) 관리 라이브러리이며, 복잡한 구조 속에서 상태 관리를 훨씬 쉽게 만들어주는 역할을 한다.

 

 

 

 

 

 

 

상태 관리 툴의 역할

 

연습삼아 만들어보는 리액트 to-do list 등과 같은 작은 프로젝트는 컴포넌트가 복잡하게 얽혀있지 않다. 그렇기 때문에 '상태 관리가 왜 어렵다는 거지?!'라는 생각이 들 수도 있다. 하지만... 실제 규모가 큰 프로젝트 같은 경우엔 정말 깊은 레벨까지 컴포넌트가 뻗어있다. 아래 스크린샷은 넷플릭스에서 React dev tool로 본 컴포넌트들이다. 저건 사실 아주 극히 일부분이며, 스크롤을 몇번은 내려야 끝을 볼 수 있을 정도로 깊은 레벨로 작성되어있다.

 

넷플릭스 컴포넌트들의...한... 부분... 일부... 작디 작은 일부...

 

이와 같이 깊은 레벨로 복잡하게 내려가는 컴포넌트 구조가 있을 경우, 리덕스와 같은 상태 관리 툴은 아주 큰 도움이 된다. 전역 상태를 위한 저장소를 제공함은 물론이고, props drilling의 문제 또한 해결해주기 때문이다. Props drilling은 저 윗대 조상 컴포넌트 누군가의 상태를 저 아래 지하에 있는 자손 컴포넌트 중 누군가가 사용하고자 할 때, 중간에 있는 자손 컴포넌트들이 그 상태를 필요로 하지 않음에도 불구하고 본인들의 props로 계속 아래로 아래로 넘겨줘야만 하는 안타까운 형태를 뜻한다. 리덕스와 같은 상태 관리 툴을 적용한다면 이와 같은 복잡성을 해소할 수 있다. 또한, 한 곳에서 상태 변화를 관리하기 때문에 예측 가능성이 올라가고, 유지보수 또한 쉬워진다는 장점이 있다.

 

자식 컴포넌트 어딘가에서 발생한 상태 변화는 그 상태의 원 주인을 찾아가려면 긴 여정을 해야한다. 하지만 리덕스가 있다면, 모든 상태는 하나의 원천(source)에 기록이 되어 있기 때문에 불필요한 여행을 줄일 수 있다.

 

 

 

 

Redux의 기본 개념

Redux의 기본 개념을 찾아보면 나오는 3가지 단어들은 다음과 같다: store, action, reducer. 이 각각이 무엇이고 그래서 뭘 한다는 것인지 이해하는 것이 중요하다. 그런데, 시각적으로 도움을 받아 더 이해가 잘 되려고 찾아보는 그림들이 오히려 햇갈리게 느껴지는 분들이 있는 것 같다. 아래 세 개의 그림들을 참고해보자.

 

 

리덕스를 설명한 그림 1

 

 

리덕스를 설명한 그림 2

 

 

리덕스를 설명한 그림 3

 

 

 

이 그림들을 보고 공통적으로 알 수 있는 것은, 무언가 흐름이 있다는 것이다. 그런데 솔직히 이 그림들만 보아선 기본 개념이라는 것들을 이해하기가 쉽지 않다. 아마도 작성된 요소들 - 무언가 확실히 중요한 것이라서 개별 박스에 들어가 있는 게 분명한  store, actions, 그리고 reducer같은 것들 -이 나타내는 것이 다 다름에도 불구하고 저렇게 획일화된, 마치 모두가 같은 역할을 하지만 순서(order)만 다른 것 같다는 느낌을 주기 때문에 그런게 아닌가 싶다. 

 

 

 

store는 장소(place)이다.

상태 정보가 담겨있는 물류 창고

스토어는 앱의 모든 상태(state)들을 저장하고 있는 객체이다. 저장소 내지는 하나의 공간이라고 생각하면 된다. 스토어에 프로젝트의 모든 상태들이 들어가있고, 스토어를 통해서만 상태가 관리된다. 스토어의 상태를 변경하기 위한 유일한 방법은 action을 dispatch하는 것 뿐이다. 또한, 상태 변화에 따라 UI를 업데이트하기 위해 상태 변화를 감지하는 용도의 subscribe를 할 수도 있다.

 

 

// 옵션없이 작성된 스토어
const store = createStore(reducer)

// 옵션과 함께 작성된 스토어
const store = createStore(reducer, compose(applyMiddleware(thunk))

 

createStore(reducer, [preloadedState], [enhancer])

* preloadedState: 옵션으로 넣을 수 있는 초기값.

* enhancer(함수): Store enahncer. 제3자가 만든 기능들 (미들웨어 등)을 붙이고 싶을 때 사용한다.

* applyMiddleWare(): Redux에 내장되어 있는 유일한 store enhancer이다.

* thunk: 비동기적 액션을 가능하게 해주는 미들웨어이다.

* compose(...functions): 함수형 프로그래밍 기능으로, 주어진 함수들을 왼쪽부터 오른쪽까지 쭈욱 nested 형태로 조합해준다. 여러개의 store enhancer들을 묶어서 createStore()의 인자로 전달하는 목적으로 사용되었다.

 

// 스토어 메소드

1. getState()

앱의 현재 상태 트리를 리턴한다. 이 값은 리듀서가 리턴한 마지막 값과 동일하다.

 

2. dispatch(action)

액션을 디스패치한다. 상태 변경을 위한 유일한 방법이다. 액션이 디스패치되면, 현재의 getState() 결과와 주어진 action을 받은 리듀서가 호출된다. 그 후 리듀서의 리턴값은 변경된 새로운 상태값이며, 이 이후에 getState()의 값은 이 새로 변경된 값이 나오게 된다. 태 변화를 감지하는 listener들도 상태 변화를 인지하게 된다.

 

3. subscribe(listener)

상태 변경에 귀기울이는(!!) 리스너(change listener)를 추가하는 메소드이다. 액션이 디스패치될 때마다 불려진다. change listener로부터 dispatch()를 부르는 것도 가능하다. 그러나, subscribe()는 low-level API라서, 직접적으로 사용하기보단 React 바인딩을 쓰게 될 것이다.

 

4. replaceReducer(nextReducer)

현재 사용 중인 리듀서를 다른 리듀서로 변경한다. 난이도가 높은 API에 속하며, 앱에서 만약 code splitting이 필요하거나,

아니면 리듀서들을 다이나믹하게 로딩하고 싶은 경우에 사용한다. 리덕스를 위한 hot loading mechanism을 구축할 때도 필요하다.

 

 

action은 수동적으로 넘겨지는 정보(information)이다.

정보가 담긴 JS 객체

앱에서 무언가 상태 변화를 일으키는 요소가 발생했다는 정보가 담긴 객체이다. 위에서 말한, '그림이 햇갈리게 한다'는 것에서 가장 큰 역할을 하는 놈이다. 왜냐하면, 이름부터가 action이라서 뭔가 얘가 직접 무언가를 하는 능동적인(active) 요소라는 느낌이 드는데, 실상 얘는 자기가 스스로 무언가를 일으키는 요소가 아닌, 수동적으로(passively) 한곳에서 다른 곳으로 이동되어지는 물류 박스 내부에 자리잡은 서류같은 아이이기 때문이다. 액션은 액션 생성자(action creators)라고 불리는 함수들을 통해 만들어진다. 이 함수들은 리턴값으로 액션을 뱉어낸다.

 

// action: type 필수 작성

{
  type: DARK_MODE,
  payload: {
    ...state,
    darkMode: true
  }
}

 

 

 

 

reducer는 함수(function)이다. 

마치 재고 관리자같은 느낌이다

reducer는 함수이다. 즉, 그렇기 때문에 명백하게 수동적인 action과는 다르게 얘는 행동을 하는 동사(verb)와도 같다고 할 수 있다. 상태에 변화가 생기는 일이 발생하면, 그게 어떤 변화인지에 대한 정보가 담긴 action이 reducer 함수에게 전달된다. 그러면, reducer는 변화가 있기 전의 상태(accumulation)와 action이라는 이름으로 전달된 변화에 대한 정보를 가지고 새로운 상태(new state)를 만들어낸다. 그리고 그 새로운 상태는 store로 전달된다. 

 

// reducer

function reducer (initialState, action) {

  // 상태 변화 일으키는 로직
  
  return output
}


// 여러개의 리듀서를 한번에 묶을 수도 있다
const myReducer = combineReducers({reducer1, reducer2, reducer3})

 

리듀서를 사용하여 새로운 상태를 만들어낼때, 유의해야할 점은 불변성(immutability)를 유지해야 한다는 것이다. 기존의 상태를 변경하여 리턴하는 것이 아니라, 아예 새로운 상태를 리턴해야한다는 것이다. 이는 리턴값을 아래와 같은 방식으로 작성하면 쉽게 해결할 수 있다.

 

// 노노노
return Object.assign(state, newData)

// 이렇게:
return Object.assign({}, state, newData)

//또는 spread 사용
return {...state, ...newData}

 

 

 

 

맺으며

아래 이미지는 리덕스 공식문서에서 볼 수 있는 그림이다. UI에서 유저 인터렉션으로 인해 상태에 변화가 발생했을 때, 업데이트된 상태가 다시 UI에 반영되기까지 어떠한 절차가 진행되는지 확인할 수 있다. gif로 action이 넘겨지는 모습이 보여지는데.... 혹시 블로그에선 안보일수도 있으니 이미지를 클릭해서 직접 공식문서로 들어가보는걸 추천한다. (스크롤을 아주 밑으로 내리다 내리다 내리다보면 나온다!)

 

출처: 공식문서 (이미지 클릭시 이동)

 

 

 

 

 

728x90
반응형

댓글