리덕스와 리덕스 관련 필수 패키지
리액트 제작사인 메타(meta)는 리액트를 처음 발표할 때 플럭스(flux)라고 부르는 앱설게 구격을 함께 발표했다. 플럭스는 앱의 수준 상태, 즉 여러 컴포넌트가 공유하는 상태를 리액트 방식으로 구현하는 방법이다. 이후 플럭스 설계 규격을 준수하는 오픈소스 라이브러리인 리덕스(redux)가 등장했으며 가장 많이 사용하는 패키지이다.
리덕스를 사용하려면 redux와 @reduxjs/toolkit(RTK)패키지, react-redux 패키지를 설치해야 한다. redux와 RTK는 프레임워크와 무관하므로 리액트 외 앵귤러나 뷰에서도 사용할 수 있다. 다만 react-redux는 리액트와 함께 동작하므로 다른 프레임워크에서는 사용할 수 없다.
npm i redux @reduxjs/toolkit react-redux
앱 수준 상태 알아보기
useState 훅은 컴포넌트가 유지해야 할 상태를 관리하는 용도로 사용된다. 그런데 여러 컴포넌트가 상태를 함께 공유하는 형태로 상태관리를 해야하는 경우도 존재하는데 이런 상태를 앱 수준 상태(app-level states)라고 하며 줄여서 '앱 상태'라고 한다.
Provider 컴포넌트와 store 속성
리덕스는 리액트 컨텍스트에 기반을 둔 라이브러리이기 때문에 리덕스 기능을 사용하려면 리액트 컨텍스트의 Provider 컴포넌트가 취상위에 배치 되어야 한다. 따라서 react-redux 패키지는 Provider 컴포넌트를 제공한다. 아래 내용부터는 Provider를 ReduxProvider라는 별칭으로 말하도록 하겠다.
ReduxProvider를 App.tsx 파일에 적용 시 다음처럼 store 속성값이 설정되어 있지 않아 오류가 발생한다. 앞으로 오류가 발생하는 배경을 알아보자.
// 별칭을 이용해 RedexProvider라는 이름으로 사용하자.
import {Provider as ReduxProvider} from 'react-redux'
import ReduxClock from './pages/ReduxClock'
import UseReducerClock from './pages/UseReducerClock'
export default function App() {
return (
<ReduxProvider>
<main>
<UseReducerClock />
<ReduxClock />
</main>
</ReduxProvider>
)
}
리덕스 저장소와 리듀서, 액션 알아보기
타입스크립트 언어로 리덕스 기능을 사용하려면 앱 수준 상태를 표현하는 AppState와 같은 타입을 선언해야 한다. 시계 앱을 만든다면 AppState는 다음처럼 작성할 수 있다.
export type AppState = {
today : Date
}
리덕스 저장소(redux store)는 AppState 타입 데이터를 저장하는 공간이다. 리덕스 저장소를 생성하려면 리듀서 함수를 알아야 한다.
리덕스에서 리듀서(reducer)는 현재 상태와 액션이라는 2가지 매개변수로 새로운 상태를 만들어서 반환한다. 즉 Action이라는 타입을 통해 리덕스 상태를 업데이트 하는 함수이다.
// Reducer 선언문
// state는 어떤값이든 다들어올 수 있으며 undefined일수도있다.
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
action : A
) => S
// Action 선언문
// type은 속성이 있는 평범한 자바스크립트 객체를 의미한다.
// 액션 선언문은 type 속성이 반드시 있어야 한다.
export interface Action<T = any> {
type : T
}
스토어 객체 관리 함수
스토어(Store)는 리덕스의 중심이 되는 객체로, 앱의 상태를 관리하고 액션을 디스패치할 수 있는 기능을 제공한다. 여기선 @reduxjs/toolkit의 configureStore를 사용해 스토어를 구성한다.
// 단순하게 표현한 configureStore 함수 선언문
// ConfigureStoreOptions 제네릭타입 매개변수를 1개 입력받는 함수이다.
export declare function configureStore<S, A, M> (options: ConfigureStoreOptions<S, A, M>):
EnhancedStore<S, A, M>;
// ConfigureStoreOptions 타입을 단순하게 표현한 예
// 필수 속성인 reducer와 middleware 등 선택 속성 4개로 구성되어 있다.
export interface ConfigureStoreOptions<S, A, M> {
reducer
middleware?
devTools?
reloadedState?
enhancers?
}
우선 기본 앱 파일에 지금까지 내용을 추가해 전체적인 맥락을 확인한다. 그 후 전형적인 리덕스 앱의 소스 구조로 변경해보자.
import type {Action} from 'redux'
import {Provider as ReduxProvider} from 'react-redux'
import {configureStore} from '@reduxjs/toolkit'
import ReduxClock from './pages/ReduxClock'
import UseReducerClock from './pages/UseReducerClock'
type AppState = { today: Date } // 앱 수준 상태
const initialAppState = { today: new Date() } // 앱 수준 상태 초기화
const rootReducer = (state: AppState = initialAppState, action: Action) => state // 리듀서 선언
// rootReducer를 store에 연결 및 기본 미들웨어 추가
const store = configureStore({
reducer: rootReducer
middleware: getDefaultMiddleware => getDefaultMiddleware()
})
export default function App() {
return (
// ReduxProvider 컨텍스트에 리덕스 스토어를 전달해 하위 컴포넌트에서 상태 접근 가능
<ReduxProvider store={store}>
<main className="p-8">
<UseReducerClock />
<ReduxClock />
</main>
</ReduxProvider>
)
}
기본 앱 파일 분리하기
위에서 본 앱 파일의 형태로는 복잡해보일 수 있다. 그러므로 전형적인 리덕스 앱 소스 구조로 바꾸는 과정을 진행하자. 왼쪽 이미지와 같이 store 안에 각각의 역할에 맞는 파일들을 생성해서 관리한다.
우선 AppState 선언문을 분리한다. 이 선언문은 Redux 상태의 타입을 정의한다.
export type AppState = {
today: Date
}
다음으로는 rootReducer 관련 코드를 분리한다. initialAppState는 Redux 상태의 초기값이며, 현재날짜와 시간을 기본값으로 설정한다. rootReducer는 Redux의 Reducer로 state와 action을 받아 새로운 상태를 반환하는 함수이다.
import type {Action} from 'redux'
import type {AppState} from './AppState'
const initialAppState = {
today: new Date()
}
export const rootReducer = (state: AppState = initialAppState, action: Action) => state
마지막으로 useStore 커스텀 훅을 만들어 configureStore 관련 코드를 분리한 후 useMemo 훅을 사용해 메모리 효율을 향상시킨다. configureStore는 Redux Toolkit에서 제공하는 함수로, Redux 스토어를 간단히 설정할 수 있게 한다.
reducer는 상태를 업데이트하는 rootReducer를 지정한다. middleware는 Redux 미들웨어를 지정하는 배열이다.
import {configureStore} from '@reduxjs/toolkit'
import {useMemo} from 'react'
import {rootReducer} from './rootReducer'
const initializeStore = () => {
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => getDefaultMiddleware()
})
return store
}
export function useStore() {
const store = useMemo(() => initializeStore(), [])
return store
}
최종적으로 다시 앱 파일에 리덕스 관련 코드들을 적용해 코드를 더욱 간결하게 만든다.
import {Provider as ReduxProvider} from 'react-redux'
import {useStore} from './store'
import ReduxClock from './pages/ReduxClock'
import UseReducerClock from './pages/UseReducerClock'
export default function App() {
const store = useStore()
return (
<ReduxProvider store={store}>
<main className="p-8">
<UseReducerClock />
<ReduxClock />
</main>
</ReduxProvider>
)
}
useSelector 훅 사용하기
리덕스 스토어 패키지가 제공하는 훅 중 하나인 useSelector 훅은 스토어에 상태값을 반환해주는 역할을 한다. useSelector를 사용한 함수에서 리덕스 스토어의 상태값이 바뀌면 해당 값을 가져와 컴포넌트를 렌더링 시킨다.
useSelector 훅은 제네릭 함수로 구현되어 있는 것을 확인할 수 있다.
import {useSelector} from 'react-redux'
// useSelector 선언문
export function useSelector<TState, TSelected>(
selector: (state: TState) => TSelected
): TSelected;
useSelector 훅을 통해 AppState 타입의 today 속성값을 얻는 예를 확인하기 위해 코드를 추가한다. 이 코드를 추가함으로 today를 컴포넌트 속성으로 구현하지 않아도 될 수 있게 되었다.
import {useSelector, UseSelector} from 'react-redux'
import type {AppState} from '../store'
import {Div, Subtitle, Title} from '../components'
export default function ReduxClock() {
const today = useSelector<AppState, Date>(state => state.today)
return (
<Div className="flex flex-col items-center justify-center mt-16">
<Title className="text-5xl">reduxClock</Title>
<Title className="mt-5 text-3xl">{today.toLocaleTimeString()}</Title>
<Subtitle className="mt-4 text-2xl">{today.toLocaleDateString()}</Subtitle>
</Div>
)
}
리덕스 액션 알아보기
ReduxClock 컴포넌트의 시간이 시계처럼 동작하려면 리덕스 저장소의 today 값을 현재 시간으로 변경해야 하며 동시에 ReduxClock 컴포넌트를 다시 렌더링해야 한다.
리덕스 액션은 저장소의 특정 상태를 변경할 때 사용하는 객체이며 반드시 type 속성을 포함해야 하며 이는 어떤 동작을 수행할지를 나타낸다. 우선 액션 타입을 정의하는 코드를 추가한다.
import type {Action} from 'redux'
export type SetTodayAction = Action<'setToday'> & {
today: Date
}
export type Actions = SetTodayAction
이처럼 액션의 type 속성을 통해 리듀서에서 분기문 처리를 할 수 있게 type 속성의 타입을 'setToday'로 정할 수 있다.
const rootReducer = (state: AppState = initialState, action: Action): AppState => {
// type 속성은 리듀서에서 switch~case문 같은 분기문을 통해 type속성에 따라 적절하게 분기하는 용도로 사용
switch (action.type) {
case 'setToday'
return {...state} // 'setToday'를 반영한 새로운 상태 반환
}
}
리덕스 리듀서 알아보기
앞서 설명한 내용들을 토대로 리듀서 코드를 수정한다. setTodayAction을 반영해 today값이 변경될 때 action type을 통해 스토어의 값을 변경할 수 있다.
import type {Actions} from './actions'
import type {AppState} from './AppState'
const initialAppState = {
today: new Date()
}
export const rootReducer = (state: AppState = initialAppState, action: Actions) => {
switch (action.type) {
case 'setToday': {
return {...state, today: action.today} // prevState값에 새로운 값을 설정해 반환
}
}
return state
}
useDispatch 훅 사용하기
useDispatch 훅은 리덕스 스토어의 dispatch() 함수를 사용해 리덕스 저장소의 AppState 객체의 멤버 전부나 일부를 변경할 수 있다.
type 속성 값이 'setToday'인 액션을 dispatch() 함수를 통해 리덕스 저장소로 보내는 코드이다.
import {useDispatch} from 'react-redux'
const dispatch = useDispatch()
dispatch({type: 'setToday', today: new Date()})
dispatch 함수와 리듀서 간의 관계 이해하기
리덕스 저장소와 리듀서, 액션과 dispatch() 함수의 관계는 아래와 같다. 이는 리덕스 저장소에 저장된 앱 수준 상태의 일부 속성값을 변경하기 위해선 액션을 먼저 만들어야 한다. 그리고 액션은 반드시 dispatch() 함수로 리덕스 저장소에 전달해야 한다. 마지막으로 액션이 리덕스 저장소에 전달될 때 리듀서가 관여한다.
[dispatch(액션)] → [리듀서] → [리덕스 저장소]
리덕스 저장소는 앱 수준 상태를 저장하는 것이 목적이므로 리듀서의 첫 번째 매개변수는 리덕스 저장소를 가리킨다. 또한 액션은 반드시 dispatch() 함수로 전달하므로 dispatch(액션) 코드가 실행되면 두 번째 매개면수 action이 리듀서로 전달된다.
// 리듀서에 전달되는 state와 action 매개변수 생성 주체
fucntion reducer(state → 리덕스 저장소, action → dispatch(액션))
시계 완성하기
useDispatch 훅을 호출해 dispatch()함수를 얻어, dispatch(액션)를 초단위로 호출해 시계 코드를 완성하자.
import {useSelector, useDispatch} from 'react-redux'
import type {AppState} from '../store'
import {Div, Subtitle, Title} from '../components'
import {useInterval} from '../hook'
export default function ReduxClock() {
const today = useSelector<AppState, Date>(state => state.today)
const dispatch = useDispatch()
useInterval(() => {
dispatch({type: 'setToday', today: new Date()})
})
return (
<Div className="flex flex-col items-center justify-center mt-16">
<Title className="text-5xl">reduxClock</Title>
<Title className="mt-5 text-3xl">{today.toLocaleTimeString()}</Title>
<Subtitle className="mt-4 text-2xl">{today.toLocaleDateString()}</Subtitle>
</Div>
)
}
useReducer 훅 사용하기
useReducer 훅은 리덕스의 리듀서와 똑같은 기능을 수행하며 다른 훅 함수들처럼 ReduxProvider와 같은 컨텍스트 없이 사용한다. 이렇기에 리덕스의 상태는 전역상태로 어느 컴포넌트에서든 접근(전역)할 수 있지만 useReducer 훅의 상태는 useReducer 훅을 호출한 컴포넌트 안(지역상태)에서만 유효하다는 차이가 있다.
useReducer 훅을 사용하면 여러 번의 useState와 useCallback 훅 호출 코드를 간결하게 구현할 수 있다. 리덕스의 리듀서와의 차이점은 초기 상태값을 설정하는 부분이 다르며 두번째 매개변수로 초기값을 설정한다.
import React, {useReducer} from 'react'
// const [상태, dispatch] = useReducer(리듀서, 상태_초기값)
cnost [{today}, dispatch] = useReducer((state: AppState, action: AppActions) => {}, {today: new Date()})
useReducer를 적용한 시계 코드를 추가한다.
import {useReducer} from 'react'
import type {AppState} from '../store'
import type {SetTodayAction} from '../store/actions'
import {Div, Title, Subtitle} from '../components'
import {useInterval} from '../hook'
export default function UseReducerClock() {
const [{today}, dispatch] = useReducer(
(state: AppState, action: SetTodayAction) => {
switch (action.type) {
case 'setToday':
return {...state, today: new Date()}
}
}, {today: new Date()})
useInterval(() => { dispatch({type: 'setToday', today: new Date()}) })
return (
<Div className="flex flex-col items-center justify-center mt-16">
<Title className="text-5xl">UseReducerClock</Title>
<Title className="mt-5 text-3xl">{today.toLocaleTimeString()}</Title>
<Subtitle className="mt-4 text-2xl">{today.toLocaleDateString()}</Subtitle>
</Div>
)
}
'FrontEnd > React' 카테고리의 다른 글
[React] 리덕스 미들웨어 이해하기 (0) | 2024.12.13 |
---|---|
[React] 리듀서 활용하기 (0) | 2024.12.11 |
[React] useContext 훅 이해하기 (0) | 2024.12.10 |
[React] useRef와 useImperativeHandle 훅 이해하기 (0) | 2024.12.02 |
[React] useEffect와 useLayoutEffect훅 이해하기 (1) | 2024.12.01 |