본문 바로가기
공부/프론트

[React] redux 사용법

by 웅대 2024. 4. 26.
728x90
반응형

이전 포스팅에서 useReducer와 Context를 활용해서 루트에서 관리하는 여러 상태 값을 자식 컴포넌트에서 사용하는 방법에 대해서 알아보았다.

https://growth-coder.tistory.com/272

 

[React] useReducer와 Context로 상태값 관리

리액트에서 상태 값을 관리할 때 useState를 사용해서 쉽게 관리할 수 있다. 그런데 만약 관리해야 할 상태 값이 많아진다면 하나씩 useState를 사용하는 방식은 번거로울 수 있다. useState부터 useReduce

growth-coder.tistory.com

이번 포스팅에서는 상태 값을 관리할 때 redux를 사용해보려고 한다.

 

리덕스는 다음과 같이 단방향 구조로 되어있다.

 

이제 redux에서 상태 값을 변경하는 과정에 대해서 알아보자.

 

  1. 뷰에서 액션을 발생시킨다.
  2. 미들웨어가 액션에 따라서 특정한 기능을 수행한다.
  3. 리듀서에서 바뀐 상태 값을 스토어에게 전해준다.
  4. 스토어가 상태 값을 저장한다.
  5. 바뀐 상태 값을 뷰에 반영한다.

리덕스의 장점은 미들웨어가 존재한다는 것이다.

 

다양한 미들웨어를 통해서 액션에 따라 여러 기능들을 수행할 수 있다.

 

저번 포스팅에서 reducer와 Context로 만들었던 기능들을 redux로 만들어보자.

 

미들웨어는 간단하게 바뀐 상태 값의 정보를 콘솔에 찍는 기능을 만들어보자.

 

패키지 정리

  • react : React
  • react-redux : useSelector, useDispatch
  • redux : combineReducers
  • @reduxjs/toolkit : configureStore 
  • immer : produce

 

1. createReducer 생성

리액트에서는 상태 값을 불변 객체로 관리해야 한다.

 

이전 포스팅에서도 불변 객체로 관리하기 위해 전개 연산자를 사용했었다.

 

<이전 reducer 코드>

function reducer(state, action){
  switch(action.type){
    case 'setSubject':
      return {...state, subject: action.subject}
    case 'plusStudents':
      return {...state, students: action.students}
    case 'minusStudents':
      return {...state, students: action.students}
  }
}

 

오늘 만들어 볼 애플리케이션처럼 간단하다면 전개 연산자를 사용하는 것도 좋지만 만약 상태 값이 깊숙하게 들어간다고 한다면 이를 불변 객체로 관리하기가 까다로울 것이다.

 

이럴 때 immer 패키지의 produce를 사용하면 쉽게 관리할 수 있다.

 

const state = {
	name : "영어",
    students : 1
}

// 기존 코드
const newState = {
	...state,
    name : "국어"
}

// immer의 produce 사용
const newState = produce(state, draft => {
	draft.name = "국어"
})

produce를 사용해서 첫 번째 매개변수에는 초기 상태 값을 넣고 draft를 통해서 바로 값을 수정할 수 있다.

 

그냥 값을 수정하기만 하면 immer에서 불변 객체로 관리해준다.

 

이를 이용해서 createReducer 함수를 작성해보자.

 

import produce from "immer";
export default function createReducer(initialState, handlerMap) {
  return function (state = initialState, action) {
    return produce(state, (draft) => {
      const handler = handlerMap[action.type];
      if (handler){
        handler(draft, action)
      }
    });
  };
}

 

조금 복잡하지만 이후에 createReducer를 생성하는 코드와 함께 보면 이해하기 쉬울 것이다.

 

2. 타입, 액션 생성

리듀서를 생성하기 전에 리듀서를 복습해보자.

function reducer(state, action){
  switch(action.type){
    case 'setSubject':
      return {...state, subject: action.subject}
    case 'plusStudents':
      return {...state, students: action.students}
    case 'minusStudents':
      return {...state, students: action.students}
  }
}

 

  • reducer에는 state와 action이 존재한다.
  • action에는 type과 바꿀 상태 값이 존재한다.
  • action의 type에 따라 분기한다.

위 코드에서는 type을 raw string으로 직접 넣어주었는데 이를 상수로 정의해보자.

 

export const Types = {
    SET_NAME : "subject/SetName",
    INCREASE_STUDENT : "subject/IncreaseStudent",
    DECREASE_STUDENT : "subject/DecreaseStudent"
}

이제 이 Types에서 꺼내서 쓰면 된다.

 

type을 보면 앞에 prefix로 subject가 붙어있다.

 

이는 subject에 관련된 type이라는 것을 의미하고 다른 type과의 충돌을 방지하기 위해 넣어준다. (물론 이번에 만들 애플리케이션에서는 subject만 사용할 것이다.)

 

이제 action creator 함수를 만들어보자.

export const actions = {
    setName : (name)=>({
        type: Types.SET_NAME,
        name: name
    }),
    increaseStudent : (prevStudents)=>({
        type: Types.INCREASE_STUDENT,
        students: prevStudents+1
    }),
    decreaseStudent : (prevStudents)=>({
        type: Types.DECREASE_STUDENT,
        students: prevStudents-1
    })
}

 

action creator 함수는 말 그대로 action을 생성하는 함수이다.

 

이전 포스팅에서는 dispatch 함수의 파라미터로 action을 넣어서 상태 값을 변경했다.

<button onClick = {()=>dispatch({type: "setSubject", subject : "english"})}>영어</button>

 

action creator 함수는 저 dispatch 안에 들어갈 함수를 만들어준다.

 

우리가 action을 직접 입력하면 실수를 할 수도 있지만 action creator 함수를 사용하면 이러한 실수를 줄일 수 있다.

 

위 actions의 increaseStudent는 아래와 같이 사용할 수 있다.

 

const onClickIncrease = ()=>{dispatch(actions.increaseStudent(students))}
<Button name = "감소" onClick = {onClickDecrease}/>

 

자세한 내용은 아래에서 

3. 리듀서 생성

위에서 만든 Types와 actions와 위에서 만든 createReducer 함수를 바탕으로 reducer를 만들어보자.

 

const INITIAL_STATE = {
    name: "영어",
    students: 10
}

const subjectReducer = createReducer(INITIAL_STATE, {
    [Types.SET_NAME] : (state, action)=>{
        state['name'] = action.name
    },
    [Types.INCREASE_STUDENT] : (state, action)=>{
        state['students'] = action.students
    },
    [Types.DECREASE_STUDENT] : (state, action)=>{
        state['students'] = action.students
    }
})
export default subjectReducer

 

위에서 만든 createReducer 함수와 비교해보자. 자세히 보면 코드를 이해할 수 있을 것이다.

 

두 번째 매개변수에서 state의 상태 값에 직접 접근해서 값을 바꾸고 있는데 이는 immer 패키지가 알아서 불변 객체로 관리해준다.

 

전개 연산자를 통해서 불변 객체로 만드는 것보다 훨씬 직관적이다.

 

4. 미들웨어, 스토어 생성

미들웨어를 생성해보자.

 

간단하게 이전 상태 값과 바뀐 상태 값을 콘솔에 출력하는 미들웨어이다.

 

const logMiddleware = store => next => action => {
    console.log(`이전 상태 값 : ${JSON.stringify(store.getState())}`)
    const result = next(action)
    console.log(`바뀐 상태 값 : ${JSON.stringify(store.getState())}`)
}

 

combineReducers를 통해 reducer를 합쳐보자.

 

물론 이번 포스팅에서는 subjectReducer 하나만 사용했기 때문에 합치지 않아도 되지만 reducer가 많아지면 합치는 것이 좋기 때문에 합쳐보자.

 

import {combineReducers} from 'redux'

const reducer = combineReducers({
    subject: subjectReducer
})

 

키 값을 subject로 주고 value에 리듀서를 넣어주었다.

 

이제 state.subject 아래에 name과 students가 존재한다.

 

이제 store를 생성해보자. reducer와 middleware를 store 안에 등록하자.

 

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
    reducer: reducer,
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logMiddleware),
    devTools: true
})

 

5. 컴포넌트에서 상태 값 가져오기

먼저 root에서 store를 등록하자.

 

Provider 컴포넌트의 속성에 store를 넣어준다.

 

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store = {store}>
    <App />

    </Provider>
);

 

이제 사용할 곳에서는 useSelector를 통해서 상태 값을 가져올 수 있다.

 

    const name = useSelector(state => state.subject.name)
    const students = useSelector(state => state.subject.students)

 

상태 값을 변경하는 dispatch도 useDispatch를 통해서 가져올 수 있다.

 

   const dispatch = useDispatch()

 

이제 버튼을 클릭할 때 발생시킬 함수를 dispatch와 action creator 함수를 통해 만들어보자.

    const changeEnglish = ()=>{dispatch(actions.setName("영어"))}
    const changeKorean = ()=>{dispatch(actions.setName("국어"))}
    const onClickIncrease = ()=>{dispatch(actions.increaseStudent(students))}
    const onClickDecrease = ()=>{dispatch(actions.decreaseStudent(students))}

 

직접 type과 상태 값을 주는 것보다 간단해졌다.

 

    return (
        <div>
            <div>{`과목 : ${name}, 정원 : ${students}`}</div>
            <Button name = "영어" onClick = {changeEnglish}/>
            <Button name = "국어" onClick = {changeKorean}/>
            <Button name = "증가" onClick = {onClickIncrease}/>
            <Button name = "감소" onClick = {onClickDecrease}/>
        </div>
    )

 

위에서 사용된 Button 컴포넌트도 만들어보자.

 

<컴포넌트 코드>

import React from 'react';
import {useSelector, useDispatch} from 'react-redux'
import {actions} from '../state'

export default function StudentInfoContainer(){
    const name = useSelector(state => state.subject.name)
    const students = useSelector(state => state.subject.students)
    const dispatch = useDispatch()
    const changeEnglish = ()=>{dispatch(actions.setName("영어"))}
    const changeKorean = ()=>{dispatch(actions.setName("국어"))}
    const onClickIncrease = ()=>{dispatch(actions.increaseStudent(students))}
    const onClickDecrease = ()=>{dispatch(actions.decreaseStudent(students))}
    return (
        <div>
            <div>{`과목 : ${name}, 정원 : ${students}`}</div>
            <Button name = "영어" onClick = {changeEnglish}/>
            <Button name = "국어" onClick = {changeKorean}/>
            <Button name = "증가" onClick = {onClickIncrease}/>
            <Button name = "감소" onClick = {onClickDecrease}/>
        </div>
    )
}

 

간단하게 name과 함수를 받아서 click event를 부여한다. 

 

import React from 'react';

export default function Button({name, onClick}){
    return(
        <button onClick = {onClick}>{name}</button>
    )
}

 

잘 동작하는지 확인해보자.

미들웨어도 잘 동작하는지 확인해보자.

728x90
반응형

댓글