리덕스(Redux)는 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리이다. 자바스크립트 앱을 위한 상태 컨테이너라고 생각하면 된다. React와 같은 component환경이 아니어도 값의 변화를 저장하고 관리하기 위해 리덕스를 사용할 수 있다. 즉, 리액트만을 위한 Library는 아니다. 리액트 뿐만 아니라 Augular, jQuery, vanilla JavaScript 등 다양한 framework와 작동되게 설계되었다.
리덕스는 Flux 구조*를 기반으로 생성되었기 때문에 단방향으로 일관적으로 동작하고, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동하며, 테스트하기 쉬운 앱을 작성하도록 도와준다.
Flux 구조 *
Flux 패턴은
action
이 발생하면dispatcher
에 의해store
에 변경된 사항이 저장되고, 그 저장된 사항에 의해view
가 변경되는 단방향 패턴을 보이고 있다.
여기서dispatcher
란 어플리케이션에서 발생한action
들을 정리해주는 역할을 하며Store
란 어플리케이션의 데이터들이 저장되는 장소이다.
이러한 Flux 패턴의 가장 큰 장점은 개발 흐름이 단방향으로 흐르기 때문에 훨씬 파악하기 쉽고 코드의 흐름이 예측 가능(Predictable)하다는 것이다.+ 리덕스 구조
Flux는 Facebook에서 만든 client-side web applications을 구축할 때 사용하는 application architecture(앱 구조), design pattern(디자인 패턴)이다.
MVC (Model–View–Controlle)구조 의 단점을 보완할 목적으로 개발된 Flux는 대규모 프로젝트에서 너무 복잡해지는 MVC구조의 단점을 보완하는 단방향 데이터 흐름(unidirectional data flow)의 구조이다.
리덕스는 Flux에 영감을 받아 개발되었지만 몇 가지 중요한 차이점이 존재한다.
차이점은 있지만 Flux와 리덕스의 구조는 매우 유사하다. 결국 리덕스는 Flux 패턴을 좀 더 쉽고 정돈된 형태로 쓸 수 있게 도와주는 라이브러리라고 볼 수 있다.
프로젝트의 규모가 작은 경우에는 컴포넌트 구조가 단순하고 관리해야 할 state도 많지 않기 때문에 굳이 리덕스와 같은 상태관리 라이브러리를 사용하지 않아도 된다.
하지만 프로젝트의 규모가 커질수록 컴포넌트의 수가 늘어나고 구조가 복잡해지면서 관리해야 하는 state 도 늘어나게 된다.
이 경우 기존의 state
전달 방식은 props
를 통해 이루어지기 때문에, state drilling** 과 같은 문제가 발생한다.
state
를 하위 컴포넌트로 계속해서 전달해 목표 컴포넌트까지 props
를 통해 전달하는 행위이러한 props
를 통한 state
전달 방식은 로직상의 큰 문제는 없지만, 코드의 가독성이 떨어지고 해당 컴포넌트에서 사용하지 않는 (전달만을 위해 존재하는) props
들이 많이 생기면서 비효율적인 데이터 흐름이 발생하게 된다. 상위 컴포넌트에서 props의 이름을 변경할 경우 해당 props가 거쳐간 모든 컴포넌트에서 props 이름을 수정해주어야 하기 때문에 코드 관리 측면에서도 좋지 않다.
store
를 통해 모든 state
와 상태 관리 로직
을 저장, 유지할 수 있게 되며 원하는 Component로만 data를 전달할 수 있다.리덕스에서 반드시 지켜져야 할 3가지 규칙이 있다.
리덕스에서는 하나의 App에는 하나의 스토어만 두어 여러 개의 스토어를 구독하여 발생할 수 있는 혼란을 피한다. dispatcher
동작 간에 하나의 스토어 상태를 구독하는데, 해당 스토어가 어딘지 찾기 어려워 진다.
그로인해 생기는 혼란을 찾아내 수정하는 것도 어려워 진다.
때문에, 여러가지 reducer
를 조합하여 하나의 store
로 생성한다. combineReducers
메서드를 사용해 여러개의 reducer
를 하나의 store
로 구성할 수 있다.
특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분리시키게 될 때 여러개의 스토어를 만들 수도 있지만 권장 사항이 아니며, 여러개의 스토어가 존재하는 경우에는 리덕스 개발 도구를 사용하지 못한다.
state
)는 읽기전용(read-only)이다.리덕스에서도 리액트와 마찬가지로 기존의 상태는 건들이지 않고 새로운 상태 객체를 생성하여 상태를 업데이트 해준다. 이를 위해 spread-syntax(...
)나 concat
, Object.assign
등을 사용한다.
순수함수는 동일한 인풋이라면 언제나 동일한 아웃풋이 있어야 한다. 즉, 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야한다.
리듀서 함수는 파라미터로 state
와 action
객체를 받는다. 이때, 리듀서 함수는 인자로 받아온 state
는 변경하지 않고, action
을 통해 변경한 새로운 state 객체를 만들어서 반환해야한다.
순수하지 않은 작업들
new Date()
,Math.random()
,axios.get()
와 같은 일부 작업들은 실행 할 때마다 다른 결과값이 나타날 수 있다. 이런 작업들은 순수하지 않은 작업이므로, 리듀서 함수의 바깥에서 처리해주어야 한다. 이러한 작업들을 처리해주기 위해 리덕스 미들웨어를 사용한다.
"Action 객체는 Dispatch 메서드에 전달되고, Dispatch는 Reducer를 호출해서 새로운 state를 생성한다."
state
가 업데이트될 때, 어떻게 업데이트 할지를 정의해주는 객체.
쉽게 말해 액션이란 어플리케이션에서 일어나는 모든 사건들 중에서 상태 변화가 필요한 사건들을 말한다.
Redux에서는 어떤 사건이 발생하여 state
값 변경이 필요하면 action
을 발생시킨 후, 이 객체를 dispatch()
함수의 인자로 넘겨준다. 그러면 dispatch()
함수가 Reducer
함수를 호출해 Reducer
함수를 실행시켜 새로운 state
를 생성한다.
type
을 필수로 가지고 있어야한다(그 외의 값은 자유롭게 추가해주면 된다). action의 type은 일반적으로 문자열 상수로 정의된다.const ADD_TODO = 'ADD_TODO' // action의 type을 정의
const ADD_TODO = 'ADD_TODO'
const addTodo = (text) => {
return {
type: ADD_TODO,
text
}
}
액션 생성자 함수(Action creator)
액션 생성함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함이다. 그래서 보통 함수 앞에
export
키워드를 붙여서 다른 파일에서 불러와서 사용한다.export const addTodo = (text) => { return { type: ADD_TODO, text } }
- 리덕스 사용 시 액션 생성함수를 사용이 필수는 아니다. 액션을 발생시킬 때마다 직접 액션 객체를 작성해도 된다.
reducer
는 두 가지 인자를 받는데, 첫 번째는 이전 상태 정보(state
)가 들어오고 두 번째 인자는 아까 위에서 발생한 액션 객체(action
)가 들어온다. 리듀서 함수가 상태를 업데이트하면 그에 따라서 render가 다시 일어나 화면이 바뀌게 된다.
리듀서는 받아온 현재 state
에 action
을 적용한 새로운 state
를 리턴해주는 함수다.
state
와 action
객체를 파라미터로 받아온다.state
가 객체나 배열일 경우 스프레드 연산자를 사용하거나 concat
메소드를 사용하는 식으로 원래의 배열이나 객체(state
)를 수정하지 않아야 한다는 점에 주의하자!리듀서 함수에서는 action
의 type
에 따라 변화된 state
를 반환하게 된다.
const itemReducer = (state = initialState, action) {
switch (action.type) {
case ADD_TO_CART:
return Object.assign({}, state, {cartItems: [...state.cartItems, action.payload]});
case REMOVE_FROM_CART:
const filtered = state.cartItems.filter(el => el.itemId !== action.payload.itemId);
return { ...state, cartItems: filtered };
default:
return state;
}
};
useReducer
에서와 달리, 리덕스의 리듀서에서는 default:
부분에 기존 state
를 그대로 반환하도록 작성해야 한다.useReducer
에선 일반적으로 default:
부분에 throw new Error('Unhandled Action')
과 같이 에러를 발생시키도록 처리하는게 일반적이다.스토어는 어플리케이션의 상태가 보관되는 하나의 저장소이다. 어플리케이션의 모든 상태는 하나의 스토어에서 관리된다.
스토어 안에는 현재의 앱 상태state
와 리듀서reducer
, 추가적인 내장 함수들(dispatch
, subscribe
등)이 들어있다.
import { createStore } from 'redux';
import rootReducer from '../reducers/index';
const store = createStore(rootReducer);
이처럼 store을 생성하고 reducer을 연결하여 어플리케이션에 연결하게 된다.
useSelector
const items = useSelector((store) => store.cartReducer);
useSelector
를 통해 스토어의 특정 state
를 가져올 수 있다.
리덕스로 구현한 간단한 쇼핑몰 어플리케이션에서 Add to Cart
버튼 클릭 시 Shopping Cart
의 뱃지의 숫자가 늘어나게 되는 과정의 구동 방식을 정리해보면 아래와 같다.