javascript 상태관리 라이브러리
리덕스는 특히 React와 함께 사용될 때 유용하지만 다른 라이브러리나 프레임워크와도 사용이 가능하다.
공식문서에 써있는 내용은 다음과 같다.
Redux is a predictable state container for JavaScript apps.
It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
예측 가능하고 일관적인 동작? 이를 이해하기 위해선 MVC패턴
과 FLUX패턴
에 대한 이해가 필요하다.
MVC란, Model-View-Controller의 약자로 애플리케이션을 세 가지 역할로 구분한 개발 방법론이다.
아래의 그림처럼 사용자가 Controller를 조작하면
=> Controller는 Model을 통해 데이터를 가져오고
=> 그 데이터를 바탕으로 View를 통해 시각적 표현을 제어하여 사용자에게 전달
하게 된다.
이러한 패턴을 성공적으로 사용하면, 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직이 서로 영향없이 쉽게 고칠 수 있는 애플리케이션을 만들 수 있게 된다. 즉, 비즈니스 로직과 UI로직을 분리하여 유지보수를 독립적으로 수행할 수 있게 된다.
그런데 Controller가 Model도 관리하고, View도 관리해야하는 구조에서, 만약에 다루는 데이터의 종류와 화면이 훨씬 많아지게 된다면!? Controller에 다수의 Model과 View가 복잡하게 연결되어 있는 상황이 발생하게 될 수 있다.
하나의 Model의 수정에 따라 여러 View가 수정되는가 하면, 반대로 View에서의 상호작용에 따라 여러 Model이 수정될 수도 있다.
즉, 정확히 어떤 데이터가 어떤 View를 통해서 수정이 되고, 어떤 View가 정확히 어떤 모델로부터 데이터를 받는지 그 흐름을 추적하는 것이 복잡해진다.
이러한 복잡성을 해결하고자 등장한 것이 바로 Flux 패턴이다. MVC 패턴의 복잡성을 해결하기 위해, Flux 패턴에서는 데이터가 한 방향으로만 흐르도록 했다.
위는 기본적인 Flux의 형태로, 어떤 Action이 발생하면, Dispatcher에서 이를 받아와 해석한 후 Store에서 저장된 정보에 변경을 가하고, 그 결과가 View로 다시 전달되도록 하는 흐름을 갖고 있다.
그런데 모바일에서 터치를 하는 것처럼, 웹에서도 사용자가 View를 통해서 클릭 같은 액션을 발생시킬 수 있다. 그런 경우도 고려하면 아래와 같은 흐름이 만들어진다.
이렇게 할 경우 action 객체만 잘 따라가도, 정확히 어떤 데이터 변화가 일어나고 있는지 추적할 수 있게 된다. View에서 사용자와의 상호작용에 따라 새로운 Action 객체가 생성되더라도 항상 Dispatcher를 통하기 때문에 데이터의 흐름이 한 방향으로 흐르게 되는 것이다.
자, 이제 다시 리덕스로 돌아오자.
그래서 리덕스가 뭐고, 왜 쓴다는건데?
지금까지 위에서 MVC패턴과 Flux 패턴을 소개한 이유는, 리덕스는 Flux패턴을 기반으로 한 것이기 때문이다.
기존 리액트 같은 경우, 자식 컴포넌트들간의 다이렉트 데이터 전달은 불가능했기에 부모 컴포넌트로 'state 끌어올리기'나 불필요한 props drilling 등이 이슈가 되면서 전역적인 state 관리법이 필요해졌다.
리덕스는 store 단 한 곳에서 모든 상태 관리가 가능하다. 이 스토어에 모든 어플리케이션 상태가 저장된다. 상태의 변경은 액션을 발생시키고 리듀서가 액션을 처리해서 이전 상태와 액션을 받아 새로운 상태를 반환한다.
또한 리덕스는 단방향 데이터 흐름을 따르기 때문에 상태 변화를 예측하기 쉽고, 디버깅도 용이하다. 또한 여러 컴포넌트 간의 상태를 공유하거나 상태 변경 로직을 중앙 집중화하여 관리하기 때문에 코드 유지보수성을 높여준다.
위에서 읽었던 공식문서의 예측 가능하고 일관적인 동작이라는 글이 이제는 이해가 될 것이다!
그럼 Redux의 구조에 대해 살펴보도록 하자.
Store는 전역 상태의 컨테이너. state가 관리되는 오직 하나뿐인 저장소이다.
어떤 action을 취할 것인지 정의해놓은 type이 필수로 지정된 객체
Dispatch에서 전달받은 Action 객체의 type에 따라서 상태를 변경시키는 함수
즉, Action 객체가 dispatch() 메소드에 전달되고
dispatch()를 통해 Reducer를 호출하고
Reducer는 새로운 Store를 생성하는 것이다.
The global state of your application is stored in an object tree within a single store.
The only way to change the state is to emit an action, an object describing what happened.
To specify how the state tree is transformed by actions, you write pure reducers.
액션 이름 정의
-액션 이름은 고유해야 한다
액션 생성 함수 생성
-액션 객체는 type을 포함한 객체이다
const ADD_SUBSCRIBER = 'ADD_SUBSCRIBER'
const addSubscriber = () =>{
return {
type: ADD_SUBSCRIBER
}
}
const initialState={
subscribers: 100
}
const reducer = (state=initialState, action) =>{
swith (action.type){
case ADD_SUBSCRIBER;
return {
...state,
subscribers: state.subscribers+1
// 기존state복사해서 새로운 객체 반환해야 함!
}
default: return state;
}
}
import {createStore} from 'redux'
const store = createStore(reducer);
//작성한 reducer을 토대로 스토어 생성
store.dispatch(addSubscriber())
store.subscribe(()=>{
console.log(store.getState())
// getState:현재 store에 있는 상태를 출력한다.
})
store.dispatch(addSubsriber()) //({subscribers:101})
store.dispatch(addSubsriber()) //({subscribers:102})
store.dispatch(addSubsriber()) //({subscribers:103})
store.dispatch(addSubsriber()) //({subscribers:104})
리액트에서 리덕스를 사용하다보면 당연히 코드량이 많기에 src 폴더 안에 actions, reducers, store, components, pages 폴더로 분할해 작성하게 된다. 그리고 모든 reducers들을 모아 reducer.js 에 합쳐주면 된다.
import { combineReducers } from 'redux';
import itemReducer from './itemReducer';
import notificationReducer from './notificationReducer';
const rootReducer = combineReducers({
itemReducer,
notificationReducer
});
export default rootReducer;
그리고 store 폴더 안에 스토어를 생성한 후에 리듀서를 등록하면 끗!
// store 폴더 안에 store.js => 스토어를 생성한 다음 리듀서를 등록한다.
import { createStore } from "redux";
import rootReducer from '../reducers/index';
const store = createStore(rootReducer);
export default store;
import React from 'react';
import { addToCart } from '../actions/index'; // 액션 가져오기
import { useSelector, useDispatch } from 'react-redux'; // 리덕스 훅 가져오기
function ItemListContainer() {
const state = useSelector(state => state.itemReducer); // 스토어에서 itemReducer로 등록된 상태 가져오기
const { items, cartItems } = state; // 상태가 객체이고 구조분해 한다.
const dispatch = useDispatch(); // 상태 업데이트 할 dispatch() 메소드 가져오기
// 클릭 이벤트
const handleClick = (item) => {
dispatch(addToCart(item.id)); // dispatch()를 이용해서 action을 리듀서에 전달
}
... 생략
}
그러나 리덕스는 action value, action creator, initialState, reducer 등 작성해야 할 코드량이 너무 많다보니 여러 불만이 속출(?)하였다. 그래서 코드는 더 적고 사용법은 더 간편해진 리덕스 툴킷이 등장하였으니!
그것은 다음 시간에 알아보도록 하겠다~!!
오늘의 일기:
이전에 멘토님이 말씀하시길, 라이브러리든 프레임워크든 사용할 때 공식문서 보면 개발자들이 어떤 이유에서 자기들이 피땀흘려 거북목 되어가며 이걸 만들었는지 다 써있으니 제발 읽어보라고 했던 게 생각났다.
확실히 그냥 유명하다고 사용하는 게 아니라 그 배경을 알고나니 해당 라이브러리에 대한 이해도 높아졌고, 활용도 잘 할 수 있을 것 같다는 생각이 들었다.
시간이 좀 걸리긴했지만 공식문서도 읽으면서 천천히 정리할 수 있어서 좋았다!
참고사이트: Redux의 데이터 흐름과 Flux패턴 by Summer
리액트 앱에서 Redux로 상태관리 하기 by 365kim
Redux공식문서
Redux(리덕스)란? (상태 관리 라이브러리) by 하나몬
리덕스(Redux)를 왜 쓸까? 그리고 리덕스를 편하게 사용하기 위한 발악 (i) by velopert
리액트 리덕스 (Redux) 튜토리얼 순한맛 1/2 by 데브리
아마 이게 제일 이해하기 쉬울걸요? React + Redux 플로우의 이해 by carrot useless