React-11 Redux (23/03/13)

nazzzo·2023년 3월 15일
0

Redux



지금까지 Context API를 사용하여 전역상태를 관리하는 방법에 대해 알아봤습니다


그런데 왜 많은 개발자들이 Redux와 같은 상태관리 라이브러리를 사용할까요?


이유는 다양하지만 첫 순위로는 리덕스의 미들웨어 사용이 가지는 장점이 크기 때문입니다


리덕스는 액션과 리듀서 사이에서 미들웨어를 실행시키는데요
이 미들웨어 함수는 일반적으로 다음과 같은 구조를 가지고 있습니다

const middleware = store => next => action => {
  // 미들웨어 로직
  return next(action);
}

위 코드에서 store는 Redux 스토어 객체를, next는 다음 미들웨어 함수를 가리키는 함수를,
action은 디스패치된 액션 객체를 의미합니다
그리고 미들웨어의 결과물에 따라서 어떤 Reducer 함수를 실행시킬지 결정합니다



1. Redux 연습하기


노드JS환경에서 리덕스를 써서 간단한 카운터 기능을 만들어보겠습니다


  1. 사전 세팅

먼저 연습용 디렉토리를 생성합니다

redux_example
redux_setting

*연습용 디렉토리에서는 CommonJS import문을 사용할 예정


[redux_example]

npm install redux
mkdir src && cd src
mkdir store && cd store
vi index.js

  1. 전역상태를 생성합니다 (createContext와 같은 역할)
const { createStore } = require("redux")
// import { createStore } from 'redux'


// 첫번째 인자는 reducer 함수입니다 ... 리턴은 함수의 초기값(state)
const store = createStore(()=>{})


console.log(store)
// Function: dispatch, subscribe, getState, replaceReducer, '@@observable'

console.log(store.getState())
// undefined ... 초기값을 지정하는 함수가 아직 정의되지 않았기 때문입니다

// 첫번째 매개변수는 이전 상태값, 두번째 매개변수는 액션
const reducer = (state, action) => {
  return {
    counter: 0,
  }
}

const store = createStore(reducer)

console.log(store.getState())
// {counter: 0}

// dispatch 사용법이 조금 달라졌습니다
store.dispatch({ type: "increment" })
// {counter: 0} { type: "increment" }

디렉토리 구조

|-- src
|--- reducers
|---- index.js
|--- store
|---- index.js


  1. 상태는 다음과 같이 업데이트할 수 있습니다

[reducers/index]

const initialState = { counter: 0 }


export const rootReducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { counter : state.counter + 1 }
    case "decrement":
      return { counter : state.counter - 1 }
    default:
      return initialState
  }
}
const store = createStore(reducer)


// 상태 바꾸기
store.dispatch({ type: "increment" })
store.dispatch({ type: "increment" })
store.dispatch({ type: "decrement" })

console.log(store.getState())
// { counter : 1 }



2. 초기 상태 설정


아래와 같이 객체 형태로 상태를 전역 상태를 만들어보겠습니다

{
  "counter" : 0,
  "user" : {
  	"userid" : "web7722",
    "username" : "kim",
  },
  "board" : [
    {
      "id": 1,
      "subject" : "board 1"
    }
  ]
}

[reducers/index]

// 초기 상태 설정
const initialState = {
  counter: 0,
  user: {},
  board: [],
}

export const rootReducer = (state, action) => {
  switch (action.type) {
    case "increment":
      // 스프레드 연산자를 사용해야 user, board에 관한 상태가 유지됩니다
      return { ...state, counter : state.counter + 1 }
    case "decrement":
      return { ...state, counter : state.counter - 1 }
    default:
      return initialState
  }
}
const store = createStore(reducer)



이제 각각의 상태에 관한 로직을 나누어볼까요

바꿀 디렉토리 구조

|--src
|--reducers
|---user.js
|---counter.js
|---index.js

[counter.js]

const initialState = {
	number: 0,
}
exports const counterReducer = (state, action) => {
	switch(action.type) {
      case "increment":
        return { number : state.number + 1 }
      case "decrement":
        return { number : state.number - 1 }
      default:
        return initialState
    }
}

[user.js]

const ADD = "USER/ADD";
const add = (userid, username) => {
	return { type: ADD, payload: { userid, username } };
};

const initialState = {
	userid: "",
	username: "",
};

const userReducer = (state = initialState, action) => {
	switch (action.type) {
		case ADD: {
			const { userid, username } = action.payload;
			return {
				...state,
				userid,
				username,
			};
		}
		default:
			return state;
	}
};

module.exports = { userReducer, add };

분할된 코드를 합치기 위해 combineReducers라는 함수를 호출합니다


[index.js]

const { combineReducers } = require("redux")
const { counterReducer, userReducer } = require("./index")

export const rootReducer = combineReducers({
	counter: counterReducer,
  	user: userReducer,
})
// {counter: { number : 0 }, user: { userid: "", username: "" }}

[store]

const { add } = require("../reducers/user")

store.dispatch(add('web7722', 'kim'))
console.log(store.getState())
// {counter: { number : 0 }, user: { userid: "web7722", username: "kim" }}

combineReducers를 사용하니 스프레드 연산자 없이도 다른 객체의 상태가 출력되네요

  • 액션 타입은 찾기 어렵기 때문에(대소문자 구분) 상수로 따로 빼서 사용하는 것을 추천합니다
  • 위의 예제처럼 액션 생성함수(add)를 따로 만들기도 합니다

(redux-toolkit에 대해서도 알아볼 것!)



3. middleware ~ Redux-Thunk


본격적으로 미들웨어 사용에 대해 알아보겠습니다


redux-thunk는 리덕스 미들웨어의 일종으로 액션 객체 대신에 함수를 디스패치할 수 있게 해줍니다
이 함수는 dispatchgetState를 매개변수로 받아서, 액션 객체를 조작하는데 사용합니다

특히 비동기 작업을 처리하는데 용이하기 때문에 많이 사용합니다


npm install redux-thunk를 써서 설치할 수 있지만

생각보다 구조가 단순하기 때문에 직접 구현해볼 수도 있습니다
그래서 오늘은 설치 대신 직접 구현해보기로...


const { createStore, applyMiddleware } = require("redux")
const { rootReducer } = require("../reducer")
const { add } = require("../reducers/user")

const createThunkMiddleware = (arguments) => {
	// next 함수는 express에서 많이 쓰던 그것과 같은 역할 
    return (store) => (next) => (action) => {
      console.log("hello world")
      console.log(type of action, action)
      return next(action)
    }
}

// 두번째 인자는 미들웨어 실행을 뜻합니다
const store = createStore(rootReducer, applyMiddleware(createThunkMiddleware()))


store.dispatch({ type: "increment" })
// hello world!
// object { type: "incremnet" }

console.log(store.getState())
// { counter: { number: 1 }, user: {..} } ~ next함수가 실행

  • store.dispatch() 함수가 실행될 때, createThunkMiddleware() 함수에서 정의된 로그("hello world")가 출력됩니다
  • console.log(store.getState())는 최종적으로 변경된 상태를 출력

const { createStore, applyMiddleware } = require("redux")
const { rootReducer } = require("../reducer")
const { add } = require("../reducers/user")

const createThunkMiddleware = (arguments) => {
    return (store) => (next) => (action) => {
	  if (typeof action === 'function') {
        console.log("함수라면")
        return action()
      }
      return next(action)
    }
}

store.dispatch(()=> {"호출"})
// 함수라면
// 호출

위 예제에서 상태는 바뀌지 않았습니다. 아직 reducer 함수가 호출되지 않았기 때문에...

그런데 dispatch 함수 안에서 다시 한번 dispatch를 사용하면 어떻게 될까요


store.dispatch(()=> {
	store.dispatch({ type: "increment" })
})

console.log(store.getState())

// 함수라면
// { counter: { number: 1 }, user: {..} } ~ next함수가 실행

dispatch 인자가 함수이기 때문에 if문의 로그가 실행 => next 함수로 인해 미들웨어 재실행 =>
dispatch 인자가 객체이기 때문에 if문의 로그 실행 X => 상태 변경!

즉, 상태를 바꾸는 과정에서 중간에 로직을 조작할 수 있는 함수(미들웨어)가 추가된 것을 알 수 있습니다



const api = () => {
	// axios...
  
  	store.dispatch({ type: "increment" })
}


store.dispatch(api)
console.log(store.getState())
|-- src
|--- api
|--- reducers
|--- store

실제 요청 로직이 담기는 코드는 전부 api 디렉토리 안에 담는 것이 좋습니다


오늘은 여기까지

0개의 댓글