Redux

WooSeong·2021년 5월 17일
0

학습 노트

목록 보기
20/22
post-thumbnail

Why Redux?

자바스크립트 싱글 페이지 어플리케이의 복잡도가 증가하고 있습니다. 다뤄야 할 기능이 많고 보여줘야 할 UI가 늘어만 갑니다. 이 말은 다시 말하자면 관리해야할 '상태'가 무수히 많아 진다는 의미입니다.

기능이 추가되면 상태를 사용해야할 컴포넌트가 늘어나고, 컴포넌트간 상호영향이 복잡하게 꼬이면서 스파게티 코드가 되어 버립니다. 설계한 프로그래머도 어느 시점에서 어느 상태를 갖는지 예측할 수 없게 되어버리면 결국 새로운 기능을 추가하기에도 어렵고 오류는 증가합니다.

리덕스의 공식문서에선 다음과 같이 말합니다.

복잡함은 변화(mutation)나 비동기(asyncronicity)와 같이 사람이 추론해내기 어려운 두 가지 개념을 섞어서 사용한다는 데서 옵니다.

React 에서는 이 문제를 해결하기 위해서 뷰 레이어에서 비동기와 DOM 조작을 없애 버립니다. 하지만 React는 데이터 관리에 관여하지 않습니다. Redux는 상태 관리에 초점을 두어 상태 변화가 일어나는 시점에 제약을 두어 상태 변화를 예측 가능하게 해줍니다.

Redux 의 3가지 원칙

Single source of truth(진실은 하나의 근원으로부터)

어플리케이션엔 하나의 단일 상태 저장소만 존재 합니다. 모든 상태는 하나의 객체 트리 구조에서 관리 됩니다. 상태를 이곳 저곳에서 분리해서 관리하게 된다면 코드가 방대해 질 수록 어떤 상태를 참조해야 하는지 혼란해 지기 시작합니다. 모든 상태를 한 곳에서 관리 한다면 상태 참조를 직관적으로 할 수 있으며 변화하는 상태에 대해 예측이 쉬워집니다.

State is read-only(상태는 읽기 전용)

상태를 변화시키는 유일한 방법은 action을 통해 새로운 상태 객체를 받아 store의 이전 상태를 대체하는 것 뿐입니다. 상태를 직접적으로 변화시키는 것은 불가능 합니다.(상태는 immutable 해야 합니다.) 모든 상태는 중앙에서 관리되고 모든 액션은 엄격한 순서에 의해 하나하나 실행 되기 때문에, 신경써서 관리해야할 미묘한 경쟁 상태는 없습니다.

Changes are made with pure functions(변화는 순수 함수로 작성되어야한다.)

action에 의한 상태 변화를 일으키는 reducer는 순수 함수로 작성되어야 합니다. reducer에는 side-effect를 일으키는 함수를 사용할 수 없으며 오직 이전 상태와 action을 받아 다음 상태를 반환할 뿐입니다. 이 말은 reducer는 내부 변수만을 이용해 immutable한 방법으로 변경한 다음 상태를 리턴할 뿐이라는 의미 입니다. 비동기 처리는 reducer에서 할 수 없습니다.

원칙을 따르는 (제약에 따른) 세부 동작

Designing State Structure

리덕스의 상태는 항상 자바스크립트 순수 객체 또는 순수 배열의 형태로 작성 되어야 합니다. 상태의 직렬화를 유지하는 것이 핵심으로 언제나 json화를 쉽게 할 수 있도록 작성 해야 하는것을 의미합니다. 다시 말해 상태에는 함수나 map,set과 같은 메서드를 사용하면 안됩니다.

리덕스의 제어권 외부에 새로운 상태를 두는 것도 허용 됩니다! 전역 상태를 따르지 않아도 되고 지역 상태만 필요한 경우 리덕스 제어권 외부의 상태를 새로 설정할 수 있습니다.

Designing Actions

actions는 type 속성을 포함한 자바스크립트 순수 객체 입니다. action은 어플리케이션 내부에서 일어날 것으로 생각되는 이벤트 들을 의미합니다. actions는 어떤 변화가 일어나는지를 묘사할 수 있는 최소한의 정보만을 갖습니다. actions는 dispatch에 실려 reducer에게 전달되어 상태에 변화를 일으키기 때문에 변화될 값에 대한 정보를 필요로 할 수 있습니다.

action.payload

//actions의 간단한 예
{type: 'todos/todoAdded', payload: todoText}

payload는 actions로 하여금 변화 값을 실어 나를수 있게 해줍니다. payload 속성은 코드 컨벤션에 의한 이름이기 때문에 원하는 이름으로 변경하여 사용할수도 있습니다. '실어 나른다'는 관점에서 payload는 적당한 이름이기 때문에 시멘틱하게 사용하기 위해 그대로 사용하는 것이 좋다 생각합니다.

action creators(액션 생산자)

액션이 디스패치에 실려 갈때 객체 리터럴 형식으로 호출될 수 있습니다만 액션 생산자를 통해 함수 호출로 보낼수도 있습니다. 이런 방식은 선택사항이며, 액션 생산자를 사용한 이점은 다음과 같습니다.

  • 모든 액션은 한 곳에 모아 볼 수 있기 때문에 액션에 대한 파악과 수정이 빠릅니다.
  • 액션 생산자를 재 작성하여 미들웨어와 조건에 따른 작동을 추가 할 수 있습니다.
  • 액션을 보내는 부근의 추가적인 로직을 실제 컴포넌트에서 분리하여 생각할 수 있도록 도와줍니다.
//action creators
//actions.js
export const ADD_TODO = 'ADD_TODO'

export addTodo = (text) => {
	return {
		type: ADD_TODO,
		text
	}
}

//AddTodo.js
import{ addTodo } from './actions'

//컴포넌트 내부 어딘가의 이벤트 핸들러 안
dispatch(addTodo('Study all night'));

Reducers 작성

리듀서는 리덕스에서 가장 중요한 부분입니다. 리듀서가 있기에 store에 접근하여 상태를 변경할 수 있게 됩니다. 앞서 말한것과 같이 리듀서는 순수 함수여야만 합니다. 만일 리듀서가 순수 함수가 아니라면 상태를 변화시키는 시점에 다음 상태를 예측할 수 없게 됩니다. 순수 함수이기 때문에 전달 받은 인자만을 사용하여 다음 상태를 immutable 하게 리턴해야 합니다.

reducer의 구성 요소

리듀서는 전달 인자로 '상태' 와 '액션'을 받아 새로운 상태를 리턴 합니다.

reducer(state, actions) => newState

Root Reducers

리덕스는 정확히 한가지의 리듀서만을 가집니다. 이 한가지의 리듀서를 루트 리듀서라 부르며 루트 리듀서는 나중에 store를 생성하는 createStore에 넘겨지게 됩니다.

루트 리듀서의 책임은 다음과 같습니다.

  • 디스패치 되는 모든 액션을 제어하는 일
  • 전체적인 새로운 상태에 대한 연산
  • 위 두가지 일을 매번 이벤트 발생시에 행합니다.

모든 리듀서는 초기 상태가 필요합니다! 초기 상태엔 가짜 상태를 넣어 전체적인 구조를 나타냅니다.

리듀서는 default 파라미터를 이용하여 초기 상태를 지정해 줄 수 있습니다.

Rules of Reducers(리듀서의 규칙)

  • 새로운 상태를 연산할때 반드시 전달받은 상태와 액션만을 사용하여야 합니다.
  • 상태에 대한 직접적인 수정은 금지 됩니다. 상태를 수정하려 하면 immutable한 방법을 통해 수정해야 합니다. 새로운 객체나 배열을 받아 기존 상태를 대체하는 것이 보통의 immutable한 수정입니다.
  • 리듀서는 비동기 처리나 부수 효과가 있으면 안됩니다. 순수 함수로 작성해야 합니다.

여러개의 리듀서??

컴포넌트의 설계적 측면에서 여러개의 리듀서가 필요 할 수 있습니다. 하지만 리덕스는 정확히 한가지의 리듀서만을 가진다고 앞서 말하였습니다. 여러개의 리듀서를 사용하기 위해서 필요한 것이 바로 combineReducers 입니다.

어플리케이션의 모든 상태 변경 로직을 하나의 리듀서 함수에 넣으면 함수가 매우 길어지게 되어 유지보수가 어려워지고 가독성도 떨어집니다. 기본적으로 함수는 짧아야 하며 한 가지의 일만 수행 하는 것이 이상적 입니다. 리덕스 리듀서도 함수이기 때문에 함수를 다시 잘개 쪼갤 수 있습니다.

쪼개진 리듀서를 (리듀서 로직을 분리하는 기준에 의하면 상태 트리의 특정 부분을 업데이트 하는 리듀서를 의미) 슬라이스 리듀서라 부릅니다.

//reducres/index.js
import { combineReducers } from 'redux';
import itemReducer from './itemReducer';
import notificationReducer from './notificationReducer';

//combineReducers를 이용해 슬라이스 리듀서들을 루트 리듀서로 묶어 줍니다.
const rootReducer = combineReducers({
	itemReducer,
	notificationReducer
});

export default rootReducer

//store/store.js
import rootReducer from '../reducers/index';

//루트 리듀서를 사용해 Store를 만듭니다.
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));

Cmarket 스프린트에서 발췌

useSelector

분리된 리듀서에 대한 상태를 스토어에서 불러오기 위해서 사용하는 것이 바로 useSelector입니다. useSelector는 리덕스의 리액트 커스텀 훅입니다. 인자로 하나의 함수를 받으며 이 함수는 바로 selector 함수 입니다.

selector 함수는 인자로 리덕스 스토어 전체를 받으며 상태에서 특정 값을 읽어 해당 값을 리턴합니다.

슬라이스 리듀서는 각각의 상태를 갖습니다. 따라서 각각의 리듀서가 담당하는 컴포넌트와 연결 시키기 위해서는 useSelector를 사용해 해당 리듀서의 상태를 가져와야 합니다.

//store를 받아 itemReducer 상태만 받아옵니다. state는 이제 itemReducer 상태 입니다.
const state = useSelector(state => state.itemReducer)
//useSelector의 내부 익명 함수(selector 함수)의 state는 다음과 같습니다.
//itemReducer : { store }, notificationReducer : { store }

Cmarket 스프린트에서 발췌

Redux Store

리덕스 스토어는 상태와 액션, 리듀서들을 모아 작동하는 하나의 앱이 되도록 관리하는 공간 입니다. 스토어는 다음과 같은 일을 합니다.

  • 현재 어플리케이션의 상태를 보관합니다.
  • 현재 상태에 대한 접근을 가능하게 합니다. store.getState()
  • 현재 상태를 다음 상태로 업데이트 합니다. store.dispatch(actions)
  • 리스너를 구독합니다. store.subscribe(listener)
  • 리스너의 구독을 해제 합니다. store.subscribe(listener)의 리턴으로 unscribe 함수를 받아.. ⇒ react hook의 작동 방식과 유사

스토어는 어플리케이션에서 단 하나만 존재해야 합니다. 만일 데이터 처리 로직을 분리하고 싶으면 스토어를 여러개 두는 대신 리듀서를 분리한 후 combineReducers를 이용하여 루트 리듀서로 합치세요!

리덕스 스토어는 하나의 루트 리듀서를 가집니다! createStore API를 통해 새로운 store를 생성할 수 있습니다.

createStore API

createStore(reducer, [preloadedState], [enhancer])
  • reducer : 하나의 유일한 루트 리듀서(슬라이스 리듀서를 결합한 하나의 리듀서)
  • preloadedState : 선택사항(optional), 스토어의 초기 상태 객체(자바스크립트 순수 객체)
  • enhancer : 선택사항(optional), 미들웨어 등의 서드파티 기능을 store에 추가하기 위한 인자
import { compose, createStore, applyMiddleware } from "redux";
import rootReducer from '../reducers/index';
import thunk from "redux-thunk";

//window(v8 런타임)에 리덕스 개발자툴이 있니? 있으면 그걸로 compose : 없으면 내장 compose
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;
//rootReducer를 넘기고, 선택사항으로 인핸서를 사용 redux-thunk 미들웨어를 적용!
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));

export default store;

Cmarket 스프린트에서 발췌

Dispatching Actions(액션 내보내기)

리덕스에서 상태를 변화 시키기 위해선 반드시 액션을 통해야 합니다. 스토어는 디스패치를 통해 이 액션을 리듀서에 내보내어 상태 변화를 일으킵니다.

디스패치를 호출할때 마다 매번 다음과 같은 과정을 거칩니다.

  • 스토어는 루트 리듀서를 호출 합니다
    • 슬라이스 리듀서가 존재한다면 루트 리듀서 내부의 슬라이스 리듀서를 호출합니다.
  • 디스패치를 통해 받은 액션과 현재 상태를 호출한 리듀서에 넘깁니다.
  • 리듀서는 받은 인자를 바탕으로 연산을 실행해 변화된 상태를 리턴합니다.
  • 리듀서에게 리턴 받은 새로운 상태를 자신(스토어)에 저장 합니다.
  • 모든 리스너 구독 콜백 함수를 호출합니다.
  • 콜백 함수의 호출이 성공적이라면 store.getState()를 통해 최신의 상태를 읽습니다.

Redux data flow(미들웨어를 사용할 경우)

Redux 공식문서 Redux 기반/Async Logic and Data Fetching 에서 발췌

  • UI 뷰 레이어의 기반은 현재 상태 입니다.
  • UI의 특정 컴포넌트에 이벤트가 발생 합니다.
    • 이벤트 핸들러 내부에 있는 dispatch가 작동합니다.
    • dispatch는 해당하는 액션을 미들웨어로 보냅니다.
  • 미들웨어는 비동기 처리를 합니다. 비동기 처리가 완료된 응답을 다시 dispatch를 통해 스토어로 보냅니다.
  • 스토어는 루트 리듀서를 호출합니다.
    • 루트 리듀서가 슬라이스 리듀서로 이루어져 있다면 해당하는 슬라이스 리듀서를 호출합니다.
  • 리듀서는 상태를 변화시킵니다.(새로운 상태를 리턴)
  • 변화된 새로운 상태가 스토어에 저장됩니다.
  • 상태가 변화 되었기 때문에 re-rendering 됩니다.
profile
성장하는 개발자를 꿈꿉니다

0개의 댓글