Redux

kyuu·2021년 12월 15일
2
post-thumbnail

리덕스의 상태 변화 흐름

1. 초기 상태

  • root reducer 함수를 사용하여 만들어진 리덕스 스토어가 존재
  • 스토어는 root-reducer를 한번 호출하고, 리턴 값을 초기 상태로 저장한다.( Initial State 지정)
  • UI가 처음 렌더링 될 때, 리덕스 스토어의 상태를 렌더링에 활용하고, 변화를 구독한다.

2. 업데이트 과정

  • 유저가 버튼을 클릭하면, 유저의 행동에 연결된 디스패치를 실행해 액션을 일으킨다.
  • 스토어는 액션을 받아 현재 액션에 맞는 리듀서 함수를 실행하고, 그 린턴 값을 새로운 상태로 저장한다.
  • 스토어를 구독하고 있는 UI 컴포넌트에게 업데이트를 전파한다.
  • UI들은 새로운 상태가 업데이트 되었는지 확인하고, 다시 렌더링한다.

한 문장으로 Redux의 동작 과정을 설명하면, 아래와 같다.
(state, action) => newState

리덕스의 상태 변화 흐름

미들웨어란?

미들웨어는,Reducer에서 디스패치 된 액션을 처리하기 이전에, 사전에 지정된 중간 작업을 설정하는 역할을 한다.

즉, 미들웨어는 액션 - 리듀서 사이의 중간자이다.
여러 패키지에 따라 아래와 같은 작업을 수행한다.

  • 전달받은 액션을 콘솔에 기록한다. (redux-logger)
  • 액션이 리듀서에서 처리되기 전에 취소시켜버리거나, 다른 종류의 액션들을 추가적으로 디스패치 할 수 있다.

미들웨어 종류에 따른 세부 내용은 사용할 때 마다 정리한다.

Redux-toolkit

Redux-toolkit은 복잡한 리덕스 환경 설정, 상용구 코드의 단순화, 별도의 패키지 추가등을 쉽게 하기 위하여 추가된 패키지이다.

주요 API를 통하여 어떻게 리덕스 애플리케이션 구축이 단순화 되는지 알아보자.
전체적인 내용은, 블로그에서 참조한 내용임.

1. configureStore()

본래 리덕스 코어 라이브러리의 createStore() 함수의 추상화. 번거로운 설정 과정을 자동화하여 간단히 작성할 수 있음.

  • reducer: 리듀서에는 단일 함수를 전달하여 스토어의 루트 리듀서(root reducer)로 바로 사용할 수 있습니다. 또한 슬라이스 리듀서들로 구성된 객체를 전달하여 루트 리듀서를 생성하도록 할 수 있습니다. 이런 경우에는 내부적으로 기존 리덕스 combineReducers 함수를 사용해서 자동적으로 병합하여 루트 리듀서를 생성합니다.

  • middleware: 기본적으로는 리덕스 미들웨어를 담는 배열입니다. 사용할 모든 미들웨어를 배열에 담아서 명시적으로 작성할 수도 있는데요. 그렇지 않으면 getDefaultMiddleware를 호출하게 됩니다. 사용자 정의, 커스텀 미들웨어를 추가하면서 동시에 리덕스 기본 미들웨어를 사용할 때 유용한 방법입니다.

  • devTools: 불리언값으로 리덕스 개발자 도구를 끄거나 켭니다.
    preloadedState: 스토어의 초기값을 설정할 수 있습니다.

  • enchaners: 기본적으로는 배열이지만 콜백 함수로 정의하기도 합니다. 예를 들어 다음과 같이 작성하면 개발자가 원하는 store enhancer를 미들웨어가 적용되는 순서보다 앞서서 추가할 수 있습니다.

2. createAction()

기존 리덕스 코어 라이브러리에서 액션을 정의하는 과정은 액션 타입 상수와, 액션 생성자 함수를 분리하여 선언했어야 하지만, createAction 함수는, 이를 하나로 결합하여 추상화시켜 한번에 생성이 가능하다.


// BEFOR: Core Redux
const INCREMENT = 'counter/increment'

function increment(amount: number) { // 액션 생성자
  return {
    type: INCREMENT,
    payload: amount,
  }
}

const action = increment(3)
// { type: 'counter/increment', payload: 3 }


// AFTER: Redux-toolkit
import { createAction } from '@reduxjs/toolkit'

const increment = createAction('counter/increment')

const action = increment(3)
// { type: 'counter/increment', payload: 3 }

3. createSlice()

createAction, createReducer 함수가 내부적으로 사용되어 슬라이스 내부의 이름에 따라 리듀서와, 액션 생성자 액션 타입을 자동적으로 생성한다.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { nanoid } from 'nanoid'

interface Item {
  id: string
  text: string
}

// 투두 슬라이스
const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as Item[],
  reducers: {
    // 액션 타입은 슬라이스 이름을 접두어로 사용해서 자동 생성됩니다. -> 'todos/addTodo'
    // 이에 상응하는 액션 타입을 가진 액션이 디스패치 되면 리듀서가 실행됩니다.
    addTodo: {
      reducer: (state, action: PayloadAction) => {
        state.push(action.payload)
      },
      // 리듀서가 실행되기 이전에 액션의 내용을 편집할 수 있습니다.
      prepare: (text: string) => {
        const id = nanoid()
        return { payload: { id, text } }
      },
    },
  },
})

const { actions, reducer } = todosSlice
export const { addTodo } = actions

export default reducer 

타입스크립트 사용한 Redux Toolkit을 통한 프로젝트 셋업

1. Dispatch 타입과, Root State 타입 정의

configureStore는 추가 타입 설정을 하면 안된다. 하지만RootStateDispatch는 설정 파일에서 타입을 설정하고, 다른 파일에서 사용할 때 타입을 뽑아 사용하는 것이 안전하다.


import { configureStore } from '@reduxjs/toolkit'
// ...

const store = configureStore({
  reducer: {
    posts: postsReducer,
    comments: commentsReducer,
    users: usersReducer,
  },
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

2. Typed Hooks 정의하기

useDispatchuseSelector 훅은, pre-typed version으로 생성하여 사용하는 것이 낫다.

  • useSelector를 사용할 때 마다 (state: RootState) 타입을 매번 설정하지 않아도 된다.
  • Default useDispatch는 사용한 middleware를 알지 못하기 때문에 middleware 타입이 추가된 customized AppDispatch를 사용한다.
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

위와 같은 과정을 거치지 않고, 직접 타입을 작성하는 방법도 있다.

< useSelector 훅 타입정하기 >

interface RootState {
  isOn: boolean
}

// TS infers type: (state: RootState) => boolean
const selectIsOn = (state: RootState) => state.isOn

// TS infers `isOn` is boolean
const isOn = useSelector(selectIsOn)

또는, inline 버젼으로도 사용할 수 있다.

const isOn = useSelector((state: RootState) => state.isOn) // Inline Version

< useDispatch 훅 타입정하기 >

기본적으로 useDispatch는 리덕스 코어 타입에 지정된 Dispatch 타입을 반환하기 때문에, 타입 지정이 필요 없다.

const dispatch = useDispatch()

실제 프로젝트 파일 구조

아래 패키지 설치하여 사용한다.

  • Redux redux
  • React에서 Redux를 사용하기 위해 Wrapping된 react-redux
  • Redux를 조금 더 쉽게 사용하기 위한 @reduxjs/toolkit
  • Next.js에서 Redux를 사용하기 위해 next-redux-wrapper

User에 대한 State를 만들어, store 및 reducer를 어떻게 생성하고, 초기 구성은 어떻게 해야 하는지 정리한다.

Redux의 코드는 모두 store/ 하위 디렉토리에 저장한다.
리덕스 세팅은 모두 index.ts에서 진행하며, 각 state에 따른 reducer 정의는 별도 파일에 정의한다.
정의된 파일은 모두 index.ts에서 reducer들이 병합되며, 이는 rootReducer에 포함되어 적용된다.

세팅의 대략적인 과정은, 아래와 같다.

  1. Store 생성하기
  2. rootReducer 생성하기
  3. 각 state와 reducer 생성하기
  4. rootReducer에 병합시키기
  5. 사용

< store 생성하기 >

< rootReducer 생성하기 >

< State 타입 설정하기 >
사용할 상태값의 Type을 지정한다.

// types/reduxState.d.ts
export type SearchRoomState = {
    location: string;
    latitude: number;
    longitude: number;
    checkInDate: string | null;
    checkOutDate: string | null;
    adultCount: number;
    childrenCount: number;
    infantsCount: number;
};

< 초기 상태값 및 리듀서 설정하기 >

설정한 SearchRoomState 타입으로, 초기 상태값 객체로 정의하기.

// store/searchRooms.ts
const initialState: SearchRoomState = {
    location: "",
    latitude: 0,
    longitude: 0,
    checkInDate: null,
    checkOutDate: null,
    adultCount: 1,
    childrenCount: 0,
    infantsCount: 0,
};

toolkit의createSlice를 활용하여 리듀서 객체 생성. Redux-toolkitcreateSlice() 함수를 활용하여, 액션에 대한 타입 지정, 액션 생성자, reducer 함수 정의를 한번에 진행한다.
createSlice()함수는 생성된 리듀서를 반환하는 reducer와 액션 생성자 함수를 반환하는 actions으로 나뉜다.

1. 액션 타입 설정, 액션 생성자 생성
2. 리듀서 생성

리듀서는, 메소드 형태로 정의됨.

// store/searchRooms.ts
const searchRoom = createSlice({
    name: "searchRoom",
    initialState,
    reducers: {
        setLocation(state, action: PayloadAction<string>) {
            state.location = action.payload;
            return state;
        },
        //...
    },
});

만든 리듀서의 액션 생성자 함수들과, 리듀서를 외부에서 사용할 수 있게 내보낸다.

// searchRoom.ts
// 초기 상태 변수로 지정
export const searchRoomActions = {...searchRoom.actions};
export default searchRoom;

내보낸 리듀서를 rootReducer에 포함시킨다.

// store/index.ts
import searchRoom from "./searchRoom";

const rootReducer = combineReducers({
  	// ...
    searchRoom: searchRoom.reducer,
});

< 사용하기 >

  1. state 구독

    컴포넌트 내부에서 사용할 state를 구독한다. 이는 useSelector를 사용한다.

const location = myUseSelector((state) => state.searchRoom.location);
  1. Action으로 dispatch 사용
    useDispatch() 훅을 통해 dispatch 함수를 받고 이를 통해 내보낸 액션들 중 원하는 액션을 바꿔줄 상태값을 넣어주어 액션을 발생시킨다.
const dispatch = useDispatch();
dispatch(searchRoomActions.setLocation(value));
profile
!..!

0개의 댓글