본문 바로가기

FrontEnd/React

[React] 리덕스 기본 개념 이해하기

리덕스와 리덕스 관련 필수 패키지

 리액트 제작사인 메타(meta)는 리액트를 처음 발표할 때 플럭스(flux)라고 부르는 앱설게 구격을 함께 발표했다.  플럭스는 앱의 수준 상태, 즉 여러 컴포넌트가 공유하는 상태를 리액트 방식으로 구현하는 방법이다.   이후 플럭스 설계 규격을 준수하는 오픈소스 라이브러리인 리덕스(redux)가 등장했으며 가장 많이 사용하는 패키지이다.

 

Flux 패턴

 

 리덕스를 사용하려면 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>
  )
}

 

ReduxProvider를 적용 시 발생하는 store 속성값이 설정되어 있지 않아 발생하는 오류이다.

 

 

 리덕스 저장소와 리듀서, 액션 알아보기

  타입스크립트 언어로 리덕스 기능을 사용하려면 앱 수준 상태를 표현하는 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>
  )
}

 

실행결과 : 시간이 시계처럼 정상적으로 변경되는 것을 확인할 수 있다.