ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Redux] 공식문서 파먹기(5) - Async Logic and Data Fetching
    JavaScript 2023. 6. 5. 21:47

    해당 포스팅은 리덕스 공식문서를 번역하여 정리한 글입니다.

     

    이전에 살펴본 리듀서 작성에서의 주의점은 사이드 이펙트를 발생시키면 안된다는 주의점이 있었습니다.

    그러면 비동기 처리로 데이터를 패칭해오는 동작을 리듀서 내부에서는 처리할 수 없다는 말인데 어떻게 처리해야할까요?

     

    이전에 살펴본 middleware는 디스패치를 중심으로 파이프라인을 형성하는 인핸서였습니다.

    이번 포스팅에서는 미들웨어를 이용해 리덕스에서 비동기 및 데이터 패칭을 처리하는 방법을 알아보겠습니다.

     

    리덕스의 스토어는 비동기처리를 알지못합니다. 무슨 얘기냐 하면 단지 액션이 디스패치되고 리듀서가 호출되며 새로운 상태를 업데이트하는 로직을 수행할 뿐, 내부에 비동기 처리가 일어나도 이 동작이 비동기 처리인지 아닌지 구분할 수 없다는겁니다.

     

    그래서 미들웨어를 통해서 스토어의 외부에서 비동기처리를 도와줘야합니다.

    미들웨어는 디스패치를 중심으로 파이프라인을 형성한다고 이전 포스팅에서 작성했었습니다. 해당 미들웨어에서 getState, Dispatch에 접근이 가능하기때문에 미들웨어를 통해 비동기 처리를 해야합니다.

     

     

    미들웨어를 통해 비동기 처리하기

    import { client } from '../api/client'
    
    const delayedActionMiddleware = storeAPI => next => action => {
      if (action.type === 'todos/todoAdded') {
        setTimeout(() => {
          // 1초 뒤 실행
          next(action)
        }, 1000)
        return
      }
    
      return next(action)
    }
    
    const fetchTodosMiddleware = storeAPI => next => action => {
      if (action.type === 'todos/fetchTodos') {
        // todo 서버에 데이터 패칭
        client.get('todos').then(todos => {
          // 받아온 데이터를 기반으로 디스패치
          storeAPI.dispatch({ type: 'todos/todosLoaded', payload: todos })
        })
      }
    
      return next(action)
    }

    미들웨어를 작성하는 방법은 지난 포스팅에서 기록했습니다.

    미들웨어 함수를 작성하고 내부에 비동기처리 로직을 작성합니다.

    아래의 예시에서는 dispatch를 미들웨어에서 호출할 수 있다는것을 보여줍니다.

     

     

    비동기 함수 미들웨어 작성하기

    이전 예제 코드에서는 미들웨어 내부에서 비동기 처리를 하고 디스패치를 호출했습니다.

    이번에는 비동기 로직을 밖으로 빼보겠습니다.

     

    미들웨어에서는 액션객체를 함수형태로 작성하여 인자로 받을겁니다 그래서 액션이 함수 타입일 경우에 바로 호출합니다.

    const asyncFunctionMiddleware = storeAPI => next => action => {
      if (typeof action === 'function') {
        return action(storeAPI.dispatch, storeAPI.getState)
      }
      return next(action)
    }

    액션이 함수일경우 dispatch와 getState를 인자로 전달해 바로 호출하고 아닌 경우에는 일반 미들웨어처럼 동작하게끔 작성했습니다.

    이렇게 미들웨어를 작성할 경우 더 확장성있고 스토어와 소통하기가 요긴해집니다.

     

    그 다음 비동기 액션을 함수형태로 작성합니다.

    const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware)
    const store = createStore(rootReducer, middlewareEnhancer)
    
    // 액션을 함수로 전달할 경우 인자로 dispatch와 getState를 전달해야합니다
    const fetchSomeData = (dispatch, getState) => {
      // HTTP 통신을 작성
      client.get('todos').then(todos => {
        dispatch({ type: 'todos/todosLoaded', payload: todos })
        // 스토어에서 새로운 상태가 잘 반환됐는지 확인
        const allTodos = getState().todos
        console.log('Number of todos after loading: ', allTodos.length)
      })
    }
    
    store.dispatch(fetchSomeData)

     

    미들웨어를 더 확장성있게 작성하는 방법을 알아보았습니다.

     

    리덕스의 비동기 데이터 플로우

    이전 포스팅에서 리덕스의 데이터 플로우를 정리했었는데 이번엔 비동기 처리가 추가된 데이터 플로우를 정리해보겠습니다.

    1. 리듀서 작성
    2. combineReducers로 슬라이스 리듀서들을 루트리듀서에 병합
    3. 미들웨어(인핸서) 작성
    4. createStore(rootReducer, initialState?, enhancers)로 스토어 생성

    여기까지는 스토어 생성의 과정입니다.

     

    액션이 디스패치되는 데이터 플로우는

    1. 액션 발생 dispatch(action)호출
    2. 이때 미들웨어가 존재하여 파이프라인을 생성합니다
    3. 미들웨어에 비동기 처리가 있어 미들웨어에서 비동기처리를 처리한 후 다음 미들웨어를 호출
    4. 미들웨어 파이프라인에 미들웨어가 없다면 리듀서를 호출
    5. 리듀서에서 새로운 상태를 반환

     

    이게 다 입니다. 중간에 디스패치에서 리듀서가 호출되기 전에 미들웨어에서 비동기 처리를 도와주는것 이외에는 추가되는게 없습니다.

     

    Redux-thunk

    미들웨어의 비동기처리를 도와주는 서드파티로 리덕스 청크가 있습니다.

    thunk란?
     - 어떤 동작의 처리를 미루기 위해 함수 형태로 감싼 것

    쉽게 말해서 비동기처리를 미들웨어에서 처리하기위해 액션을 객체가 아닌 함수형태로 디스패치해서 미들웨어의 비동기 처리 이후 액션이 디스패치되도록 도와줍니다.

     

    import { createStore, applyMiddleware } from 'redux'
    // thunkMiddleware API를 불러와서
    import thunkMiddleware from 'redux-thunk'
    import { composeWithDevTools } from 'redux-devtools-extension'
    import rootReducer from './reducer'
    
    // middleware에 compose해줍니다
    const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))
    
    // 이제 action객체 대신 thunk함수(action함수)를 dispatch에 전달하여 사용할 수 있습니다
    const store = createStore(rootReducer, composedEnhancer)
    export default store;

    사용방법은 간단합니다. thunk middleware를 불러와 applyMiddleware에 전달하여 compose해서 createStore에 전달합니다.

    이제 액션객체를 디스패치하지않고, thunk함수를 디스패치하여 비동기 처리를 미들웨어에서 처리합니다.

     

    // todoSlice.js
    // thunk function
    
    const initialState = []
    
    export async function fetchTodos(dispatch, getState) {
        const res = await client.get('url');
    
        const beforeState = getState();
        console.log(beforeState.todos.length);
    
        dispatch({type: 'todos/todoAdded', payload: res.todos})
    
        const afterState = getState();
        console.log(afterState.todos.length);
    }
    
    export default function todosReducer(state = initialState, action) {
      switch (action.type) {
        case 'todos/todosLoaded': {
          // 새로운 값을 반환하여 기존의 todos배열을 서버의 todos배열로 대체
          return action.payload
        }
        default:
          return state
      }
    }
    
    // index.js
    import { fetchTodos } from './todoSlice'
    dispatch(fetchTodos)

     

    간단한 예시를 들어보면 todosSlice.js 파일에서는 initialState에 전달할 Todos 배열이 필요하므로 해당 파일에서 thunk함수를 작성해서 Todos 배열을 가져옵니다.

     

    해당 함수를 index.js에서 가져와 dispatch에 전달합니다.

    사실 thunk함수 내부에서 dispatch를 호출하고 store.dispatch에서는 해당 함수를 전달해주기만합니다.

     

    또한 thunk함수 내부에서 getState를 호출해서 값이 갱신됐는지 체크할 수 있습니다.

     

     

    새로운 할일 저장하기

    thunk함수를 따로 작성하여 초기 Todos배열을 가져왔습니다.

    이제 새로운 todos아이템을 추가해보려고 합니다.

    todos아이템을 추가하여 서버에 보내고, 서버에서 새로운 항목이 추가된 아이템을 다시 보내줄때까지 기다려야하는 문제가 있습니다.

    thunk 함수는 todoSlice.js 파일에 작성했는데, 여기서 새로운 할일과 이전 할일을 구분하지 못하는 문제입니다.

     

    export function saveNewTodos(text) {
        return async function fetchNewTodos(dispatch, getState){
            const initalTodos = {text};
            const res = client.post(url, { todos: initialTodos };
            dispatch({ type: 'todo/todoAdded', payload: res.todos })
        }    
    }

    위와 같이 외부 함수(saveNewTodos)는 text를 인자로 받습니다.

    그리고 비동기 thunk 함수를 반환하는 패턴으로 위의 문제를 해결할 수 있습니다.

     

    이제 위의 함수를 유저가 입력하는 Header 컴포넌트에서 사용해보겠습니다.

     

    // Header.jsx
    import { saveNewTodos } from 'todoSlice';
    import { useDispatch } from 'react-redux'
    
    const Header = () => {
      const [text, setText] = useState('')
      const dispatch = useDispatch()
    
      const handleChange = e => setText(e.target.value)
    
      const handleKeyDown = e => {
        const trimmedText = text.trim()
        if (e.which === 13 && trimmedText) {
          // thunk 함수를 반환하는 함수를 호출하여 할당해주고 유저가 입력한 값을 인자로 전달
          const saveNewTodoThunk = saveNewTodo(trimmedText)
          // 디스패치에 thunk함수를 전달
          dispatch(saveNewTodoThunk)
          setText('')
        }
      }
      
      return (// 생략)
    }

     

    위처럼 thunk 비동기 함수를 반환하는 함수를 이용해 새로운 값을 저장할 수 있습니다.

     

    이제 마지막으로 reducer의 처리만 추가해주면 됩니다.

    const initialState = []
    
    export default function todosReducer(state = initialState, action) {
      switch (action.type) {
        case 'todos/todoAdded': {
          // 새로 받아온 배열만 추가해서 반환
          return [...state, action.payload]
        }
        default:
          return state
      }
    }

    서버에 요청해 응답한 값은 아예 새로운 ID를 포함한 객체입니다.

    사실 reducer에서는 기존의 값과 새로운 값을 합쳐서 반환해주면 끝입니다.

     

     

    redux-thunk를 이용해 비동기 처리를 도와주는 thunk함수를 작성하는 방법을 알아보았습니다.

    댓글

Designed by Tistory.