리덕스의 핵심 원칙, 컴포넌트, API
- 리덕스는 플럭스 아키텍처의 주요 특징에 영감을 받아 만들어졌지만, 그 자체의 기반 원칙과 다양한 컴포넌트를 가지고 대규모 애플리케이션에서 상태 관리를 처리한다. 이번 절에서 리덕스의 내부 구조와 사용법을 명확히 이해하고 나면 중급에서 고급 수준의 질문에 답할 수 있게 될 것이다.
리덕스의 핵심 원칙은 무엇인가?
- 리덕스는 다음과 같은 세 가지 주요 원칙을 가지고 있다. 이 원칙은 라이브러리를 더 잘 이해하는 데 도움을 준다.
- 단일 진실 공급원 : 애플리케이션 전역 상태는 단일 스토어에 객체 트리 형태로 저장된다. 모든 상태가 한 곳에 존재하기 때문에 단일 진실 공급원이라고 한다. 이 단일 트리 구조는 애플리케이션을 디버깅하고 검사하는 것을 더 쉽게 만들어준다. 결과적으로 이전에는 구현하기 어려웠던 실행 취소(Undo)나 다시 실행(Redo) 기능을 더 쉽게 구현할 수 있다. 애플리케이션의 전체 상태는 다음과 같이 getState()로 가져올 수 있다.
console.log(store.getState()) // Returns the todos list
♣ 참고 : 리덕스의 이러한 단일 스토어 접근법은 플럭스의 여러 스토어와 구별되는 가장 큰 차이점 중 하나다.
- 이 단일 트리는 개발 중에소 상태를 유지하는데 도움이 되어 더 빠른 개발 주기를 꾀할 수 있다.
- 상태는 읽기 전용 : 상태를 변경할 수 있는 유일한 방법은 어떠한 일이 발생했는지를 설명하는 객체 형태의 액션을 내보내는 것 뿐이다. 즉, 애플리케이션의 상태를 직접 변경할 수 없으며, 대신 액션을 전달해서 상태 변경 의도를 나타낸다. 다음 코드는 액션을 디스패치해서 cities 상태에 새로운 도시를 추가하는 방법이다.
store.dispatch({
type: 'ADD_CITY',
payload: 'London',
})
- 이 액션은 순수 자바스크립트 객체이기 때문에 직렬화, 저장, 로깅할 수 있으며, 디버깅 목적으로 재현할 수 있다.
- 변경은 순수 함수로만 일어난다 : 상태가 액션에 의해 어떻게 변경되는지 지정하려면 리듀서(Reducer)를 작성해야 한다. 리듀서는 이전 상태와 액션을 인수로 받아 새로운 상태를 반환하는 순수 함수다. 새로운 상태 객체가 반환되며, 기존 상태를 수정하지 않는 다는 점을 명심해야 한다. 다음 예제의 리듀서는 새로운 도시를 추가하며, cities 상태 변수를 업데이트해서 반환한다.
function cities(cities = [], action) {
switch (action.type) {
case 'ADD_CITY':
return [
...cities,
{
name: action.payloadname,
position: 1,
},
]
default:
return cities
}
}
- 처음에는 애플리케이션을 단일 리듀서로 작성하는 것으로 시작할 수 있다. 애플리케이션의 규모가 커지면 크기가 큰 리듀서를 상태 트리의 특정 부분을 관리하는 여러 작은 리듀서로 나눌 수 있다. 또한 리듀서가 호출되는 순서를 제어하고 추가 데이터를 전달해서 애플리케이션에서의 공통 작업에 재사용할 수 있다.
리덕스는 어떻게 동작하는가? 리덕스의 메인 컴포넌트는 무엇인가?
- 리덕스 시스템은 애플리케이션의 전체 상태를 중앙 스토어에 보관하는 방식으로 동작한다. 리덕스 프로바이더(provider)의 자식이 각 UI 컴포넌트는 한 컴포넌트에서 다른 컴포넌트로 props를 전달하지 않고도 이 중앙 스토어에 접근할 수 있다. 리덕스 워크플로의 전체 과정은 액션, 리듀서, 스토어라는 세 가지 주요 핵심 컴포넌트를 기반으로 한다.
- 이러한 핵심 컴포넌트를 활용한 리덕스의 워크플로를 다음 코드의 간단한 할 일 예제 애플리케이션을 통해 이해해 보자. 예제에서는 먹기와 달리기 같은 일상활동을 할 일로 간주하고, 리덕스 워크플로를 사용해 스토어에 추가한다.
- 액션(Action) : 액션은 단순 자바스크립트 객체로, 어떠한 종류의 작업을 수행할지를 나타내는 타입 필드와 상태를 변경하는 데 사용되는 기타 데이터를 포함하고 있다. 액션은 애플리케이션 데이터(예: 폼 데이터, 사용자 상호 작용, API 요청 등)를 리덕스 스토어로 보내는 유일한 방법이다. 이러한 모든 액션은 액션 생성기를 통해 만들어지며, 액션 생성기는 액션을 반환하는 함수일 뿐이다. 다음 코드는 todo 액션을 반환하는 addTodo라는 액션 생성기다.
function addTodo(todo) {
return {
type: 'ADD_TODO',
payload: todo,
}
}
- 앞서 언급한 액션은 할 일(todo) 인수도 포함하고 있다. 이 액션은 스토어의 dispatch(addTodo)에 의해 실행되며, 이 메서드는 액션을 스토어로 보낸다.
- 리듀서(Reducer) : 액션은 무엇을 해야 할 지 설명하지만, 어떻게 해야 할지는 알려주지 않는다. 따라서 리듀서는 액션에 대한 응답으로 스토어의 상태가 어떻게 변경될지 처리하는 데 사용된다. 리듀서는 받는 액션 타입에 따라 이벤트를 처리하는 이벤트 리스너와 유사하다. 리듀서에는 새로운 할 일(todo)을 추가하는 로직이 포함돼 있으며, 다음과 같은 코드로 나타낼 수 있다.
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO':
const { name, priority } = action.payload
return [...state.todos, { name, priority }]
default:
return state
}
}
♣ 참고 : 새로운 상태가 무엇인지 결정하기 위해서 꼭 switch 문을 사용해야 하는 것은 아니다. if/else 등 다른 프로그래밍 요소도 가능하다.
- 앞서 언급된 리듀서는 초기 상태와 액션을 인수로 받는다. 만약 switch 조검문이 ADD_TODO 액션 타입과 일치하면 기존 상태에서 todos를 복사해서 새로운 todo 값으로 todos를 업데이트하고, 새롤운 할 일 목록을 반환한다. 그렇지 않으면 변경되지 않은 todos가 포함된 기존 상태가 반환된다. 애프리케이션에서 todo를 수정, 삭제, 필터링하는 등 가능한 액션에 따라 더 많은 기능 케이스를 추가할 수 있다.
- 스토어(store) : 스토어는 애플리케이션 상태와 리액트 컴포넌트와 스토어에 액션을 디스패치해서 수정한, 동일한 상태를 유지하는 데 사용된다. 리덕스는 스토어를 생성하고, 스토어에 접근하며, 스토어에 액션을 디스패치하기 위한 다음과 같은 헬퍼 메서드를 제공한다.
- createStore 또는 configureStore
- dispatch()
- getState()
- 스토어(store) : 스토어는 애플리케이션 상태와 리액트 컴포넌트와 스토어에 액션을 디스패치해서 수정한, 동일한 상태를 유지하는 데 사용된다. 리덕스는 스토어를 생성하고, 스토어에 접근하며, 스토어에 액션을 디스패치하기 위한 다음과 같은 헬퍼 메서드를 제공한다.
- 이러한 헬퍼 메서드는 스토어에서 할 일(todo) 상태를 생성하거나 수정하는 데 사용된다. 다음 코드를 살펴보자.
import { createStore } from 'redux'
import todoReducer from 'reducers/todoReducer'
const store = createStore(todoReducer) // Create a store
const firstTodo = addTodo({ name: 'Running', priority: 2 })
console.log(firstTodo)
store.dispatch(firstTodo) // Dispatch a todo
const secondTodo = addTodo({ name: 'Eating', priority: 1 })
console.log(secondTodo)
store.dispatch(secondTodo)
console.log(store.getState()) // Returns the todos list
- 이 코드에서는 새로운 할 일인 todo 액션을 생성해서 기존 할 일 목록을 업데이트 하기 위해 스토어에 디스패치 했다. 업데이트된 todos 상태에도 접근 가능하다.
- createStore 메서드는 지원 중단(deprecated) 됐으며, 대신 리덕스 팀은 RTK의 configureStore 메서드를 사용하도록 권장한다. configureStore 메서드는 스토어 설정에 대한 추가적인 기본값을 제공하고, 리덕스 개발자 도구 확장 기능도 자동으로 포함한다. 앞의 코드에서는 리덕스의 createStore를 사용했는데, RTK를 소개할 때 configureStore 메서드의 사용법을 볼 수 있을 것이다.
♣ 참고 : 애플리케이션의 규모가 커지면 스토어의 상태 중 특정 부분의 상태 정보에는 셀렉터(Selector)라는 함수를 사용해 접근 할 수 있다. 메모이제이션된 셀렉터 함수를 제공하는 reselect 라이브러리가 유명하다.
- 스토어 기능을 확장하기 위해 스토어 강화자(store enhancer)나 미들웨어를 추가하는 것도 가능하다.
리덕스를 비 리액트 UI 라이브러리와 함께 사용할 수 있는가?
- 리덕스는 주로 리액트 및 리액트 네이티브 라이브러리와 함께 사용되지만 다른 UI 라이브러리와 함께 사용할 수도 있다.(즉, 리덕스는 다양한 UI 라이브러리의 데이터 스토어로 작동한다.) 하지만, UI 프레임워크나 라이브러리와 리덕스를 통합하기 위해서는 UI 바인딩 라이브러리를 사용해야 한다. 예를 들어, react-redux는 리액트와 리덕스 라이브러리를 결합하기 우한 공식 바인딩 라이브러리다. 마찬가지로 AngularJS, Angular, Vue, Mithril, Ember 등과 같은 다른 여러 라이브러리를 위한 바인딩 라이브러리도 사용할 수 있다. 리덕스는 다른 코드에서 사용할 수 있는 구독 메커니즘이 있지만 주로 리액트 또한 유사한 라이브러리를 통해 생성된 선언적인 뷰나 UI와 통합할 떼 유용하다.
리듀서가 따라야 하는 규칙은 무엇인가?
- 리덕스에서 리듀서 컴포넌트는 몇가지 특정 규칙을 따라야 한다. 이 규칙들은 다음과 같다.
- 리듀서는 현재 상태와 액션 인수를 기반으로 새로운 상태 값을 파생시켜야 한다.
- 리듀서는 기존 상태를 수정해서는 안 된다. 그러나 기존 상태를 복사해서 복사된 값에 변경을 가하는 불변 업데이트 방식은 실행 가능하다.
- 리듀서는 비동기 로직을 수행하거나 무작위 값을 계산하거나 부수 효과를 일으키는 것이 허용되지 않는다.
- 이 규칙을 따르는 함수를 순수 함수라고도 한다. 따라서 리듀서는 단순히 순수 함수라도 볼 수 있다. 이러한 규칙을 따름으로써 리듀서는 버그 없이 리덕스 코드와 상태를 예측 가능하게 만든다.
mapStateToProps()와 mapDispatchToProps() 메서드의 차이점은 무엇인가?
- mapStateToProps() 메서드는 연결된 컴포넌트가 필요로 하는 스토어 데이터의 일부분을 선택하는 데 사용되는 유틸리티 함수다. 선택된 상태는 connect()가 적용된 컴포넌트에 props 형태롤 전달된다. 이 방법을 통해 메서드는 전체 애플리케이션 상태를 컴포넌트에 전달하는 것을 피할 수 있게 도움을 준다.
- 다음 예제에서는 WeatherReport 컴포넌트에 city 값을 props로 전달해서 날씨 정보를 찾는다.
const mapStateToProps = (state) => {
return {
city: state.user.address.city,
}
}
connect(mapStateToProps)(WeatherReport)
- 이제 WeatherReport 컴포넌트는 city만을 prop으로 받을 수 있게 된다. 리덕스 코드를 리액트 컴포넌트에서 분리함으로써 이 컴포넌트를 애플리케이션의 다른 곳에서 쉽게 사용할 수 있게 된다.
<WeatherReport city={city} />
- 이 함수의 축약 표현은 mapState이며, 스토어의 상태가 변경될 때마다 함수가 호출된다.
- mapDispatchToProps() 메서드는 컴포넌트가 디스패치할 필요가 있는 액션을 지정하는 데 사용되는 유틸리티 함수다. 이 함수는 액션 디스패치 함수를 props로 제공한다. 다음 함수는 리액트 컴포넌트인 WeatherReport에 필요한 액션을 지정한다.
const mapDispatchToProps = (dispatch) => {
return {
changeCity: (city) => {
dispatch(changeCity(city))
},
}
}
- 위 코드는 city를 변경하는 액션을 수행한다. 이는 컴포넌트에서 props.dispatch(changeCity(city))와 같은 장황한 표현 대신 props.changeCity(city) 액션을 호출함으로써 이뤄진다.
- mapDispatchToProps() 함수에 대한 객체 축약 표현을 사용해도 된다. 이 접근 방식에서 리덕스는 (...args) => disptch(changeCity(...args)) 같은 또 다른 함수로 감싸고, 그 래퍼 함수를 컴포넌트에 prop으로 전달한다.
- 앞의 코드는 다음과 같이 간소화 될 수 있다.
const mapDispatchToProps = {
toggleCity,
}
- 요약하자면 mapStateToProps 함수는 저장된 데이터를 컴포넌트에 랜더링하는 데 사용하며, mapDispatchToProps는 액션 생성기를 컴포넌트의 prop으로 제공하는 데 사용된다.
스토어 강화자란 무엇인가?
- 스토어 강화자(store enhancer)는 스토어 행성 함수(예: createStore)를 인수로 받아 새로운 향상된 스토어 생성 함수를 반환하는 고차 함수다. 이것은 리덕스 스토어를 맞춤 설정하는 데 도움이 되며, dispatch, getState, subscribe 같은 스토어 메서드를 오버라이드 한다.
- 다음 코드를 살펴보면 스토어 강화자 구현을 어떻게 구성하는 지 확인할 수 있다.
const ourCustomEnhancer =
(createStore) => (reducer, initialState, enhancer) => {
const customReducer = (state, action) => {
// Logic to return new state
}
const store = createStore(customReducer, initialState, enhancer)
//Add enhancer logic
return {
...store,
//Override the some store properties or add new properties
}
}
- 스토어 강화자는 리액트 고차 컴포넌트(Higher-Order Component; HOC) 개념과 매우 유사하다. 따라서 HOC를 컴포넌트 강화자(Component Enhancer)라고 부를 수도 있다.
♣ 참고 : 미들웨어는 리덕스의 디스패치 함수에 추가 기능을 제공하며, 강화자는 리덕스 스토어에 추가 기능을 제공한다.
- 실시간 애플리케이션은 외부 API 호출, 랜덤 값 생성, 파일 저장, 로컬 스토리지 업데이트와 같은 부수 효과를 포함하는 로직을 포함한다. 기본적으로 리덕스는 이러한 종류의 부수 효과를 실행하는 것을 지원하지 않는다. 그러나 리덕스 미들웨어를 사용하면 디스패치된 액션을 가로채고, 부수 효과를 포함한 추가적인 복잡한 동작을 주입할 수 있다.
'FE > 리액트 인터뷰 가이드' 카테고리의 다른 글
| 6. 리덕스: 최고의 상태 관리 솔루션 - 4 (0) | 2025.10.04 |
|---|---|
| 6. 리덕스: 최고의 상태 관리 솔루션 - 3 (0) | 2025.10.04 |
| 6. 리덕스: 최고의 상태 관리 솔루션 - 1 (1) | 2025.10.04 |
| 3. 훅: 함수 컴포넌트에 state와 다른 기능 추가하기 - 3 (0) | 2025.09.24 |
| 3. 훅: 함수 컴포넌트에 state와 다른 기능 추가하기 - 2 (1) | 2025.09.23 |