Redux 소개와 기본 개념
1. 상태 관리의 필요성
상태는 웹페이지내에서 눈에 보이는 데이터들(ex. 메뉴, 게시글 제목, 게시글 내용)뿐만 아니라 서버와 주고 받아야하는 데이터를 의미한다.
이렇게 눈에 보이는 것과 눈에 보이지 않는 모든 것을 상태라고 하고 이를 관리하는 것을 상태 관리라고 한다.
상태 관리는 리액트 애플리케이션에서 중요한 개념 중 하나이다. 이는 애플리케이션의 동작을 결정하고, 사용자 인터페이스를 동적으로 업데이트하는 데 필수적인 역할을 한다.
효과적인 상태 관리를 통해 리액트 애플리케이션의 성능과 유지보수성을 크게 향상시킬 수 있다.
2. 상태의 구분
우리는 상태를 세 가지로 구분할 수 있다.
Local State
Local State
란 데이터가 변경되어서 하나의 컴포넌트에 속하는 UI에 영향을 미치는 상태를 말한다.
- ex) 사용자의 입력을 받아서 useState를 사용해 그 입력을 모든 키 입력과 함께 state 변수에 저장
- ex) 세부 정보 필드를 켜고 끄는 토글 버튼
Cross-Component State
Cross-Component State
란 데이터가 변경되어서 다수의 컴포넌트에 영향을 미치는 상태를 말한다.
- useState를 사용해 관리한다. (약간 복잡하면 useReducer를 사용!)
- ex) 모달 오버레이를 열거나 닫는 버튼이 있는 경우, 다수의 컴포넌트에 영향을 미칠 수 있다. (다수의 컴포넌트가 협력하여 모달을 표시하거나 감추는 상태)
- 이 경우 useState/ useReducer를 사용하게 되면 prop을 주변 컴포넌트에 넘겨줘야 한다. 이 때 prop drilling이 발생한다.
- ex) 모달 오버레이를 열거나 닫는 버튼이 있는 경우, 다수의 컴포넌트에 영향을 미칠 수 있다. (다수의 컴포넌트가 협력하여 모달을 표시하거나 감추는 상태)
App-Wide State
App-Wide State
란 데이터의 변경이 특정 컴포넌트가 아닌 애플리케이션의 모든 컴포넌트에 영향을 미치는 상태를 말한다.
- ex) 사용자 인증에 의한 탐색바 변경
- 이 경우도 마찬가지로 prop drilling이 발생한다.
3. Redux란?
Redux의 정의
Redux는 아주 유명한 서드파티 리액트 라이브러리이다. 리덕스는 애플리케이션에 존재하는 하나의 중앙 데이터(상태) 저장소 이다. 절대로 2개 이상은 갖지 않는다!
리덕스에 전체 애플리케이션의 모든 상태를 저장해둘 수 있다. 리덕스 내부에 저장된 데이터를 우리는 컴포넌트에서 사용할 수 있다.
Redux의 작동 원리
1) 데이터를 컴포넌트에서 사용하기
컴포넌트에서 리덕스에 저장된 데이터를 사용하기 위해서는 Subscription
을 설정해야 한다. 구독을 설정하게 되면, 데이터가 변경될 때마다 저장소가 컴포넌트에게 알려주게 된다. 그러면 컴포넌트는 필요한 데이터를 받게 된다.
2) 데이터 변경하기
우리가 설정한 Reducer Function
을 사용하여 데이터를 변경할 수 있다. 그러면 컴포넌트와 Reducer Function을 어떻게 연결할까? 트리거를 사용하면 된다. 컴포넌트가 Action
을 발송하면 리덕스는 그 액션을 Reducer에 전달하고 그 작업을 리듀서가 수행한다.
Redux의 핵심 원칙
Single source of truth
Redux에서는 상태 관리를 쉽게 하기 위해 하나의 상태 Tree를 사용한다. 이는 디버그와 검사를 더욱 쉽게하여 개발을 더욱 빠르게 할 수 있게 한다.
State is read-only
컴포넌트는 절대로 저장된 데이터를 직접 조작하지 않는다. 따라서 구독을 하는 것이며, 데이터는 절대로 반대 방향으로 흐르지 않는다.
State는 action에 의해서만 수정된다.
Changes are made with pure functions
Reducer는 이전의 상태와 action을 받아 다음 상태를 반환하는 순수함수이다. Reducer를 하나만 사용하는 것에서부터 App이 커져갈수록 Reducer를 잘개 쪼개어 상태 Tree의 일부만 다룰 수 있도록 할 수 있다.
4. React Context vs Redux
React Context와 Redux는 모두 크로스 컴포넌트 상태와 앱 와이드 상태를 관리할 수 있게 도와준다. 그렇다면 React Context가 있는데 왜 굳이 Redux를 사용해야 하는걸까?
React Context의 특징
React Context는 몇 가지 잠재적인 단점을 가지고 있다.
1) React Context를 사용하면 설정이 아주 복잡해질 수 있고, 상태 관리가 상당히 복잡해질 수 있다.
- 소형/중형 애플리케이션에서는 문제가 되지 않을 가능성이 높지만, 많은 컴포넌트와 내용이 있는 엔터프라이즈 수준의 대형애플리케이션의 경우 심하게 중첩된 JSX코드가 만들어지게 된다.
2) 성능의 문제
테마를 변경하거나 인증과 같은 저빈도 업데이트의 경우 React Context를 사용하는게 좋지만, 데이터가 자주 변경되는 경우 성능이 저하된다.
Redux의 장점
1) 크고 복잡한 앱에서 확장성이 높다
2) 액션에 따른 모든 변경을 추적할 수 있다.
3) "특정 상태 조각이 언제 변경되었으며 데이터는 어디에서 왔는지" 동작을 예측할 수 있다.
언제 Redux를 선택해야 할까?
둘 다 중요하기 때문에 둘 중 하나를 선택하라는 말이 아니다. 둘을 섞어서 사용하게 될 수도 있다. 다만 아래와 같은 경우에는 Redux를 유용하게 사용할 수 있다.
- 앱의 여러 위치에서 필요한 상태의 양이 많은 경우
- 상태 업데이트 빈도수가 높은 경우
- 큰 규모의 코드베이스를 가지고 많은 사람들이 작업하는 경우
- 시간이 지남에 따라 상태가 어떻게 업데이트되는지 확인해야 하는 경우
5. Redux 예제
Redux의 핵심 요소들을 설명하는 간단한 예제를 살펴보자.
1) Reducer 함수 정의하기
Reducer는 애플리케이션의 상태 변화를 담당하는 순수 함수이다. 항상 두 개의 매개변수를 받아야 하고, 새로운 상태의 객체를 반환해야 한다.
순수 함수란?
동일한 입력값에 대해 항상 동일한 출력값을 반환해야 하며, 외부 상태를 변경하거나 부수적인 효과를 발생시키지 않는 함수
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return { counter: state.counter + 1 };
}
if (action.type === "decrement") {
return { counter: state.counter - 1 };
}
return state;
};
2) 저장소(store) 생성
Redux store는 애플리케이션의 상태를 저장한다. createStore
함수로 store를 생성할 수 있고, 이 때 리듀서를 인수로 전달합니다.
const store = redux.createStore(counterReducer);
스토어 생성 시, Redux는 리듀서를 실행하여 초기 상태를 설정한다. 따라서 리듀서의 state
매개변수에는 기본값을 반드시 제공해야 한다.
3) 구독자(Subscriber) 등록
Redux 스토어는 상태 변경을 감지하고 필요한 작업을 수행할 수 있도록 구독(Subscribe) 메서드를 제공한다. 구독자는 상태가 변경될 때마다 실행된다.
const counterSubscriber = () => {
const latestState = store.getState(); //최신 상태 갖고 오기
console.log(latestState);
};
store.subscribe(counterSubscriber);
4) 액션(Action) 발송
액션은 상태를 변경하는 데 필요한 정보를 담은 JavaScript 객체이다. 액션 객체는 type
속성을 필수로 가져야 하며, 이를 통해 리듀서가 어떤 작업을 수행해야 하는지 결정한다. 이 때 dispatch
메서드를 통해 액션을 발송하면 리덕스에 의해 리듀서가 실행된다.
store.dispatch({ type: "increment" });
store.dispatch({ type: "decrement" });
전체 코드
const redux = require("redux");
// 1. 리듀서 정의
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return { counter: state.counter + 1 };
}
if (action.type === "decrement") {
return { counter: state.counter - 1 };
}
return state;
};
// 2. 스토어 생성
const store = redux.createStore(counterReducer);
// 3. 구독자 등록
const counterSubscriber = () => {
const latestState = store.getState();
console.log(latestState);
};
store.subscribe(counterSubscriber);
// 4. 액션 발송
store.dispatch({ type: "increment" });
store.dispatch({ type: "decrement" });