🤔리덕스란 무엇인가?
자바스크립트에서 상태관리를 할 수 있는 라이브러리로, 변경되는 상태 데이터를 전역 저장소에 보관하여 관리함으로써 상태관리의 복잡성을 해소할 수 있는 도구이다. 특히 리액트 프로젝트에서 많이 사용되지만, 리액트와는 독립적인 라이브러리로 바닐라 자바스크립트에서도 사용할 수 있다.
리액트에서 컴포넌트 단위로 코드를 작성하고, 컴포넌트 안에서 상태가 존재한다면 여러 컴포넌트에서 해당 상태에 접근하기 위해 필연적으로 props drilling 이 일어날 수밖에 없다. 이는 코드의 복잡성을 증가시키고 그렇기 때문에 유지보수가 어렵고 앱의 동작을 예측하기 힘들어진다. 리덕스에서는 상태와 컴포넌트 구조를 분리시킴으로써 상태가 업데이트되는 흐름을 추적하기 쉽게 하고, 결과적으로 컴포넌트를 구성하는 코드가 화면에만 집중할 수 있도록 도와준다.
리덕스는 리액트와 많이 사용되기 때문에 react-redux 라이브러리를 사용해 더 편리하게 사용할 수 있고, 또 기존 redux 보다 더 편리하게 개선된 redux-toolkit 라이브러리를 사용하도록 권장하고 있다.
⁉️리덕스의 구성 요소와 흐름
전역저장소 Store의 개념
- 앱의 전역 상태가 저장되는 공간으로, store에 저장되는 state는 직접 변경해서는 안된다!
- 대신 변경을 위한 정보를 담은 Action 객체를 생성하고, dispatcher를 통해 그 정보를 store에게 전달한다.(dispatch)
- Action이 전달되면 store는 root reducer를 실행시켜서 기존 상태를 기반으로 계산된 새로운 상태를 리턴한다.
- sotre는 subscribers 에게 상태가 업데이트 되었음을 알려서, UI가 화면을 업데이트 할 수 있도록 한다.
State, Actions, Reducers
// 초기 상태값을 선언해준다.
const initialState = { value: 0 };
// reducer 함수 (state, action) => newState
// 인자로 현재 state와 action 객체를 받고 있으며, 기본 값은 앞서 선언한 초기 상태값으로 할당해준다.
function counterReducer(state = initialState, action) {
// action의 type 필드의 값에 따라서 다른 상태값을 리턴할 수 있도록
// switch 문으로 분기한다. (새로운 상태를 리턴할 수 있다면 if...else든 loop이든 상관없음)
switch (action.type) {
// action.type의 값이 아래와 같으면 새로운 상태값을 복사하여 업데이트 한 뒤 리턴한다.
case "counter/incremented":
return { ...state, value: state.value + 1 };
case "counter/decremented":
return { ...state, value: state.value - 1 };
// 만약 위 case에 해당되지 않는다면, 업데이트되지 않은 '기존 상태'를 리턴한다.
default:
return state;
}
}
- 먼저 상태가 될 초기 값을 변수에 저장한다.(initial state) 리덕스에서 상태는 일반적으로 여러 값이 들어있는 객체로 만들어서 관리한다.
- reducer 함수를 정의한다.
- reducer 함수는 두 개의 매개변수를 받는데 하나는 초기 상태값 그리고 다른 하나는 Action 객체이다. 우리가 사용하고자하는 상태의 초기값은 이 reducer 함수를 통해서 결정된다고 볼 수 있다.
- reducer 함수는 type 필드의 값에 따라 상태를 업데이트 하기 때문에, 액션 객체는 필수적으로 이를 포함해야 하고 또 그 의미를 알아보기 쉽게 작성해야 한다. 예를 들어 'domain/eventName' 이런 방식으로 작성할 수 있다. 해당 액션이 일어날 카테고리와 정확히 어떤 이벤트가 발생해야 하는지를 구별해서 작성한다.
- action 객체의 type에 따라서 새로운 상태를 리턴한다. 이 상태를 리턴할 때는 기존의 상태를 수정하여 리턴하는 것이 아니라 복사하여 새로운 상태에 수정하여 리턴한다. (불변성)
Store
// Redux store with Vanila JS
// createStore 함수로 새 store를 만들며 앞서 정의한 reducer 함수를 인자로 전달한다.
const store = Redux.createStore(counterReducer);
// 어떤 이벤트가 발생했을 때 실행될 함수로, store의 getState메서드로 포함된 상태를 가져온다.
// 그리고 현재 value의 입력값을 state로 변경하여 화면에 렌더링한다.
function render() {
const state = store.getState();
valueEl.innerHTML = state.value.toString();
}
// store의 subscribe 함수를 사용하면 store에서 업데이트가 발생했을 때 해당 콜백함수를 실행시킨다.
// 이 경우 상태가 업데이트되면 화면 렌더링이 되어야 하므로 render 함수를 넘겨준다.
store.subscribe(render);
- reducer 함수까지 작성이 완료되면, 리덕스의 createStore API로 새 store를 작성한다. 이 저장소 생성 함수에 store에서 사용할 reducer 함수를 전달해줄 수 있다.
- reducer 함수가 하나라면 그 함수를 넘겨주면 되고, 여러개의 함수가 있다면 redux 에서 제공하는 {combineReducers} 함수를 사용해 하나의 root reducer 로 정의한 뒤 전달하면 된다.
- react-redux 라이브러리를 사용하면 store를 쉽게 만들게 도와주는 Provider 컴포넌트와, state에 쉽게 접근할 수 있는 useSelector hook을 제공하고 있다.
UI
// UI 코드 : 특정 객체를 클릭하면 dispatch 함수로 객체를 넘겨주는데,
// 이 객체가 reducer에서 참조하는 action 객체이다.
// action 객체는 type 필드를 필수적으로 가지고 있어야 한다!!
document
.getElementById("increment")
.addEventListener("click", function () {
store.dispatch({ type: "counter/incremented" });
});
document
.getElementById("decrement")
.addEventListener("click", function () {
store.dispatch({ type: "counter/decremented" });
});
- 리덕스는 보통 리액트와 사용하는 경우가 많기 때문에, 이 때는 react-redux 라이브러리를 추가로 사용해 useDispatch와 useSelector로 action을 전달하고 state에 접근할 수 있다.
- 위 예시에서는 action 객체를 직접 작성하여 전달하고 있지만 보통 함수를 만들어 사용하고, 이 함수를 Action Creator 액션 생성자 함수라고 부른다. 액션 생성자 함수의 예시는 아래와 같다.
export const increaseValue = value => {
return {
type: "counter/incremented",
// 추가적인 데이터가 있다면 payload 필드에 담아서 전달한다.
payload: {
value,
},
};
};
Redux 데이터 플로우 : Dispatch Actions to Store & Reducer
UI 컴포넌트에서 상태를 변경시킬 수 있는 이벤트가 감지된 경우, Action 객체를 생성하고 이 객체를 Dispatch 로 Store에 넘긴다. 이렇게 하면 앞서 정의한 reducer 함수가 action 객체의 type을 검사해서 새로운 상태를 리턴하고, store의 상태가 업데이트 되면 subscriber 함수에 의해서 render 함수가 호출되어 UI가 변경된 상태로 업데이트 된다.
정리하자면,
- Action 객체는 방금 무슨 일이 일어났는지, 어떤 일을 해야하는지 그 행동을 담고있는 객체이고,
- Dispatch는 저장소에 '지금 무슨 일이 일어났다!!!' 고 알리는 역할을 한다.
- Store 안에는 현재 state와 reducer 함수가 존재하는데, action 객체가 전달이 되면 Reducer가 '그래? 무슨 일이 일어났는데?' 하고 그 객체의 type을 검사해서 적절한 값을 리턴해준다.
- 그렇게 리턴된 값은 store의 새로운 상태로 업데이트가 되고, 만약 subscribe 함수를 사용해서 업데이트를 감지할 수 있다면 감지된 이후에 함수가 실행되는 것이다.
‼️3가지 중요한 원칙!
리덕스를 사용하기 위해 지켜야 할 3가지 원칙을 소개한다.
1. Single Source Of Truth : 하나의 전역 상태는 하나의 저장소에서 관리되어야 한다!
전역으로 관리되는 상태가 있다면 반드시 하나의 저장소에서 관리해야한다. 상태 업데이트 흐름을 추적하기 쉽고, 전체 코드를 분산시키지 않을 수 있다.
추가적으로, 모든 상태 데이터를 전역으로 관리할 필요는 없고, 필요에 따라 로컬 상태와 전역 상태로 구분하여 관리할 수도 있다.
2. State is Read-Only : 상태는 읽기전용입니다.
상태를 업데이트 할 수 있는 유일한(!) 방법은, action 객체를 생성해 dispatch 함수로 전달해주는 것 뿐이다. 이런 방법을 사용하면 UI에서 상태 데이터를 잘못 참조할 일도 없고, 흐름을 추적하기 쉽기 때문에 관리에 용이하다. 절대 상태를 직접 변경하지 마라.
3. Changes are Made with Pure Reducer Functions : 상태 업데이트는 Reducer 함수를 이용해라.
이 때 사용되는 reducer 함수는 입력 => 출력만을 기능으로 하는 순수 함수여야 한다. 상태를 업데이트 할 때는 다른 값이 아니라 이전의 값을 가지고 새로운 상태를 계산해야한다.
위 원칙을 준수할 때 리덕스를 목적에 맞게 사용할 수 있다!
✨FLUX 패턴
기존에 양방향 데이터 흐름을 가지던 MVC (Model View Controller) 패턴의 문제점을 해결하기 위해 페이스북이 만든 애플리케이션 디자인 패턴이다. 많은 상호작용을 가진 앱에서 이러한 양방향 데이터 흐름은 코드를 복잡하게 하고 버그를 일으킨다는 문제점이 있었는데, 페이스북이 제시한 FLUX 패턴에서는 단방향 데이터 흐름을 사용하여 문제를 해결한다.
FLUX 패턴은 아래와 같은 흐름으로 생겼다. Action -Dispatcher- Store- View의 흐름으로 가다가 View에서 상호작용이 발생하면, Store의 데이터를 변경하는게 아니라 다시 Action을 발생시켜서 Dispatcher - Store의 흐름으로 View에 전달되는 방식이다. 아래 그림을 보면 알겠지만 리덕스는 이 FLUX 패턴을 기반으로 작동하고 있다. (단방향 데이터 흐름)
이런 흐름은 데이터가 업데이트되는 흐름을 파악하기 쉬워지고 따라서 시스템의 동작을 예측하게 해준다는 장점이 있다. 그래서 우리가 복잡하고 거대한 애플리케이션에서 리덕스를 가지고 상태관리를 하는 것이다.
🔍참고자료
'Stacks > Redux' 카테고리의 다른 글
[Redux] redux-toolkit 폴더 구조 / 비동기처리 - createAsyncThunk 사용법 (0) | 2023.01.17 |
---|---|
[Redux] React-Redux 라이브러리 : Counter 예제만들기 (0) | 2022.09.05 |