Javascript/React

리덕스 기본 개념 이해하기

kyoulho 2024. 7. 21. 15:49

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

리덕스 관련 필수 패키지

npm i redux @reduxjs/toolkit react-redux

redux와 @reduxjs/tookit(RTK 패키지) 은 프레임워크와 무관하므로 앵귤러나 뷰에서도 사용할 수 있다.

 

앱 수준 상태

useState 훅은 컴포넌트가 유지해야 할 상태를 관리하는 용도로 사용된다. 그런데 여러 컴포넌트가 상태들을 함께 공유하는 형태로 만들 때가 많은데, 이처럼 앱을 구성하는 모든 컴포넌트가 함께 공유할 수 있는 상태를 앱 수준 상태(app-level states) 줄여서 '앱 상태'라고 합니다.

Provider 컴포넌트와 store 속성

리덕스는 리액트 컨텍스트에 기반을 둔 라이브러리다. 즉, 리덕스 기능을 사용하려면 리액트 컨텍스트의 Provider 컴포넌트가 최상위로 동작해야 한다. 따라서 react-redux 패키지는 다음처럼 Provider 컴포넌트를 제공한다.

import {Provider} from 'react-redux'

상태 객체

리덕스 기능을 사용할 때는 먼저 다음처럼 앱 수준 상태를 표현하는 AppState와 같은 타입을 선언해야 한다. 리덕스 저장소는 이러한 AppState 타입 데이터를 저장하는 공간이다. 

export type AppState = { today: Date }

리듀서, 액션

리듀서는 현재 상태와 액션이라는 2가지 매개변수로 새로운 상태를 만들어서 반환한다. 액션(Action)은 플럭스에서 온 용어로서 type이란 이름의 속성이 있는 평범한 자바스크립트 객체를 의미한다. redux 패키지는 다음처럼 액션 객체의 타입을 선언하고 있다. 이 액션 선언문은 type 속성이 반드시 있어야 한다는 의미이다.

 

// Reducer 선언문
export type Reducer<S = any, A extends Action = AnyAction> = (
    state: S | undefined,
    action: A
) => S

// Action 선언문
export interface Action<T = any> {
    type: T
}

스토어 객체 관리 함수

스토어는 애플리케이션의 전체 상태를 보관하는 중앙 저장소이다. RTK 패키지는 리듀서에서 반환한 새로운 상태를 스토어에 정리해 관리하는 configureStore 함수를 제공한다. 

// configureStore 함수 선언문
export declare function configureStore<S, A, M>(options: ConfigureStoreOptions<S, A, M>):
EngancedStore<S, A, M>;

// ConfigureStoreOptions 타입 정의
export interface ConfigureStoreOptions<S, A, M> {
   reducer
   middleware?
   devTools?
   reloadedState?
   engancers?
}

useSelector

리덕스 저장소에 스토어의 상탯값을 반환해 주는 훅이다.

export function useSelector<TState, TSelected>(
    selector: (state: TState) => TSelected
): TSelected;

// 예제
const today = useSelector<AppState, Date>(state => state.today)

리덕스 액션

리덕스에서 액션은 저장소의 특정 속성값만 변경하고 싶을 때 사용하는 방법이다. 리덕스 액션은 반드시 type이란 이름의 속성이 있어야 하므로 redux 패키지의 Action 타입을 교집합 구문으로 추가해 준다.

// redux의 Action
type Action<T extends string = string> = {
    type: T;
};

// 사용 예제
import {Action} from "redux";

export type SetTodayAction = Action<'setToday'> & {
    today: Date
}

useDispatch

useDispatch는 dispatch 함수를 반환한다. dispatch 함수를 사용하면 다음과 같은 형태로 리덕스 저장소에 저장된 AppState 객체의 멤버 전부나 일부를 변경할 수 있다. 다음은 type 속성값이 'setToday'인 액션을 dispatch() 함수를 통해 리덕스 저장소로 보내는 코드이다.

// dispatch 함수 선언
interface Dispatch<A extends Action = UnknownAction> {
    <T extends A>(action: T, ...extraArgs: any[]): T;
}

// 사용 예제
import {useDispatch} from 'react-redux'

const dispatch = useDispatch()
dispatch({type: 'setToday', today: new Date()})

정리

리덕스 저장소에 저장된 앱 상태의 일부 속성값을 변경하려면 일단 액션을 만들어야 한다. 그리고 액션은 반드시 dispatch함수를 통해 전달되어야 한다. 그리고 액션이 리덕스 저장소에 전달될 때 리듀서가 관여한다.

또한 리듀서에 첫 번째 매개변수값 state는 리덕스 저장소에서 두 번째 매개변수는 dispatch 함수로 전달되어 온 action이 전달된다.

 

리듀서 예제

AppState.ts

리덕스 저장소에 저장될 앱 상태 타입

export type AppState = {
    today: Date
}

Actions.ts

reducer 함수와 dispatch 함수에 전달될 Action 타입

import {Action} from "redux";

export type SetTodayAction = Action<'setToday'> & {
    today: Date
}

rootReducer.ts

이전의 state를 깊은 복사하여 새로운 값을 만들어야 하며, type이 매치하지 않을 때에는 이전의 값을 반환하도록 하여야 한다.

import {AppState} from "./AppState";
import {SetTodayAction} from "./Actions";

const initialAppState = {
    today: new Date()
}

export const rootReducer = (state: AppState = initialAppState, action: SetTodayAction) => {
    switch (action.type) {
        case 'setToday': {
            return {...state, today: action.today}
        }
    }
    return state // 필수
}

useStore.ts

import {configureStore} from "@reduxjs/toolkit";
import {rootReducer} from "./rootReducer";
import {useMemo} from "react";

// initializeStore 함수는 Redux 스토어를 생성하고 구성한다.
const initializeStore = () => {
    // configureStore는 Redux Toolkit에서 제공하는 함수로, 스토어를 쉽게 설정할 수 있게 해준다.
    return configureStore({
        // rootReducer를 스토어의 리듀서로 설정
        reducer: rootReducer,
        // 기본 미들웨어를 설정한다.
        // getDefaultMiddleware는 Redux Toolkit에서 제공하는 함수로 기본 미들웨어를 가져온다.
        middleware: getDefaultMiddleware => getDefaultMiddleware()
    });
}

// useStore 훅은 컴포넌트에서 Redux 스토어를 사용할 수 있게 해준다.
export function useStore() {
    // useMemo 훅을 사용하여 initializeStore 함수를 호출하고, 반환된 스토어 인스턴스를 메모이제이션한다.
    return useMemo(() => initializeStore(), []);
}

ReduxClock.tsx

import {useDispatch, useSelector} from "react-redux";
import {AppState} from "../store";
import {useInterval} from "../hooks";
import {Div, Title} from "../components";

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-4 text-3xl"}>{today.toLocaleTimeString()}</Title>
            <Title className={"mt-4 text-2xl"}>{today.toLocaleDateString()}</Title>
        </Div>
    )
}

App.tsx

import React from 'react'
import './App.css'
import ReduxClock from "./pages/ReduxClock";
import {Provider as ReduxProvider} from 'react-redux';
import {useStore} from "./store";


export default function App() {
    const store = useStore()
    return (
        <ReduxProvider store={store}>
            <main className={"p-8"}>
                <ReduxClock/>
            </main>
        </ReduxProvider>
    )
}

 

useReducer

useReducer 훅은 react 패키지에서 제공하는 훅으로 리덕스의 리듀서와 사실상 똑같은 기능을 수행한다. useReducer 훅은 Redux의 Provider와 같은 컨텍스트 없이 사용한다. 이 때문에 리덕스의 상태는 앱의 모든 컴포넌트에서 접근할 수 있지만(즉, 전역상태), useReducer 훅의 상태는 다른 훅 함수들처럼 useReducer 훅을 호출한 컴포넌트 안에서만 유효하다는 차이가 있다.(즉, 지역상태)

const [상태, dispatch] = useReducer(리듀서, 상태_초깃값)

ReduxClock.tsx

useReducer를 이용하도록 변경할 수 있다.

import {AppState} from "../store";
import {useInterval} from "../hooks";
import {Div, Title} from "../components";
import {useReducer} from "react";
import {SetTodayAction} from "../store/Actions";

export default function ReduxClock() {
    const [{today}, dispatch] = useReducer(
        (state: AppState, action: SetTodayAction) => {
            switch (action.type) {
                case "setToday":
                    return {...state, today: new Date()}
            }
            return state;
        },
        {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"}>ReduxClock</Title>
            <Title className={"mt-4 text-3xl"}>{today.toLocaleTimeString()}</Title>
            <Title className={"mt-4 text-2xl"}>{today.toLocaleDateString()}</Title>
        </Div>
    )
}

'Javascript > React' 카테고리의 다른 글

리덕스 미들웨어  (0) 2024.07.29
리듀서 활용하기  (0) 2024.07.22
useContext  (0) 2024.07.21
FileReader 클래스  (0) 2024.07.20
useRef, useImperativeHandle, forwardRef  (0) 2024.07.20