[TIL/React] 2023/05/30

원민관·2023년 5월 30일
0

[TIL]

목록 보기
78/159
post-thumbnail

Async Logic & Data Fetching 🟣

Reference: https://redux.js.org/tutorials/essentials/part-5-async-logic#writing-async-thunks

Thunks and Async Logic 🟡

Using Middleware to Enable Async Logic 🔵

  1. UI에서 Deposit Button을 클릭함

  2. onClick Event에서 dispatch를 통해 '액션생성함수'를 '호출'하여 '액션'을 '생성'함

  3. 생성된 액션은 middleware(=thunk)에 의해 인식됨 middleware(=thunk)는 액션의 타입을 확인하고, 해당 액션을 처리할 수 있는지 여부를 판단함

  4. 액션의 payload data를 기반으로 비동기 작업을 시작함(=api에 비동기 작업에 대한 요청(request)을 수행함)

  5. api는 요청(request)에 대한 응답(response) 과정을 수행함

  6. 비동기 작업이 완료되면, '작업의 결과에 따른 액션'을 dispatch를 통해 Reducer로 전달함

  7. Reducer는 새로운 액션을 수신하여 상태를 업데이트하고, application의 상태를 변경함

Thunk Functions 🔵

middleware(=thunk)가 Redux Store에 추가되면, store.dispatch()를 통해 직접적으로 'thunk 함수'를 전달할 수 있다. Thunk 함수는 항상 dispatchgetState를 인자로 받으며, 필요에 따라 해당 인자들을 thunk 함수 내부에서 사용할 수 있다.

const store = configureStore({ reducer: counterReducer })

const exampleThunkFunction = (dispatch, getState) => {
  const stateBefore = getState()
  console.log(`Counter before: ${stateBefore.counter}`)
  dispatch(increment())
  const stateAfter = getState()
  console.log(`Counter after: ${stateAfter.counter}`)
}

store.dispatch(exampleThunkFunction)

counterReducer가 store에 위치한 상태이다. exampleThunkFunction은 변수명 그대로, middleware 역할을 수행하는 'thunk Fn'이다. dispatch와 getState를 인자로 받고 있으며, 가장 먼저 현재의 counter 상태를 출력해준다. 'dispatch(increment())'는 dispatch 함수를 사용하여 increment 액션을 디스패치한다. increment 액션생성함수를 호출하여 액션을 생성하고 디스패치하는 것을 의미한다. 이후 업데이트 된 counter 상태를 출력하고, 'store.dispatch()'를 통해 thunk Fn을 디스패치한다.

const logAndAdd = amount => {
  return (dispatch, getState) => {
    const stateBefore = getState()
    console.log(`Counter before: ${stateBefore.counter}`)
    dispatch(incrementByAmount(amount))
    const stateAfter = getState()
    console.log(`Counter after: ${stateAfter.counter}`)
  }
}

store.dispatch(logAndAdd(5))

그런데, 일반적인 액션 객체를 dispatching하는 것과 통일성을 유지하기 위해서, 위 코드처럼 'thunk 액션 생성자'를 작성하고 내부에서 thunk Fn을 반환하는 것이 일반적이다. 추가적으로, 보통 thunk는 'slice' 파일에 작성한다. 'createSlice'에는 thunk를 정의하기 위한 자체적인 지원 기능이 없다.

Writing Async Thunks 🔵

Thunk Fn 내부에는 setTimeout, Promises, async/await와 같은 비동기 로직이 포함될 수 있다.

Redux에서 data를 fetching하는 로직은 일반적으로 다음과 같은 '예측 가능한 패턴'을 따른다.

  1. 요청이 진행 중임을 나타내기 위해, 요청 전에 'start' 동작이 전달된다. 이는 중복 요청을 건너뛰거나 UI에서 로딩 표시기를 보여주는 데 사용될 수 있다.

  2. 비동기 요청이 이루어진다.

  3. 요청 결과에 따라, 비동기 로직은 결과 데이터를 포함하는 'success' 동작이나, 오류 세부 정보를 포함하는 'failure' 동작을 전달한다. 리듀서 로직은 양쪽 경우에 로딩 상태를 지운다. 'success'의 경우에는 결과 데이터를 처리하고, 'failure'의 경우에는 오류 값을 저장하여 나중에 표시할 수 있다.

위 3단계는 필수적인 것은 아니지만 일반적으로 사용된다.(가령, 요청이 성공한 결과에만 관심이 있다면, 요청이 완료될 때 단일 'success' 동작을 전달하고 'start' 및 'failure' 동작은 건너뛸 수 있다.)

Redux Toolkit은 createAsyncThunk API를 제공하여 이러한 동작의 생성과 전달을 구현할 수 있도록 지원한다.

Loading Posts 🟡

Extracting Posts Selectors 🔵

const postsSlice = createSlice(/* omit slice code*/)

export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions

export default postsSlice.reducer

export const selectAllPosts = state => state.posts

export const selectPostById = (state, postId) =>
  state.posts.find(post => post.id === postId)

현재 postsSlice 상태는, post 배열 하나로 구성되어 있다. 이를 post 배열과 로딩 상태 필드를 포함한 객체로 변경해야 한다.

한편 'PostList'와 같은 UI 컴포넌트들은 useSelector 훅으로 state.posts에서 포스트를 읽으려고 하며, 해당 필드가 배열임을 가정하고 있다. 이러한 위치도 변경하여 새로운 데이터와 일치하도록 수정해야 한다.

reducer의 데이터 형식을 변경할 때마다 컴포넌트를 매번 다시 작성해야 하는 번거로움은 피하는 것이 좋다. slice 파일에서 재사용할 수 있는 selector 함수를 정의하고, 각각의 컴포넌트에서는 selector 로직을 반복하지 않고 필요한 데이터를 추출하기 위해 해당 selector를 사용하도록 한다. 상태 구조를 다시 변경해야 할 경우 slice 파일의 코드만 업데이트하면 된다.

'PostList' 컴포넌트는 모든 포스트 목록을 읽어와야 하며, 'SinglePostPage'와 'EditPostForm' 컴포넌트는 포스트의 ID를 사용하여 단일 포스트를 조회해야 한다. 이를 처리하기 위해 postSlice.js에서 두 개의 selector를 내보낸 것이다.

주의할 점은 이러한 selector 함수의 state 매개변수는, useSelector 내부에 직접 작성하는 익명 selector와 마찬가지로 root Redux 상태 객체이다.

slice 파일에서 정의한, 재사용성이 높은 selector 함수는 개별 컴포넌트에서 다음과 같이 활용된다.

// omit imports
import { selectAllPosts } from './postsSlice'

export const PostsList = () => {
  const posts = useSelector(selectAllPosts)
  // omit component contents
}
// omit imports
import { selectPostById } from './postsSlice'

export const SinglePostPage = ({ match }) => {
  const { postId } = match.params

  const post = useSelector(state => selectPostById(state, postId))
  // omit component logic
}
// omit imports
import { postUpdated, selectPostById } from './postsSlice'

export const EditPostForm = ({ match }) => {
  const { postId } = match.params

  const post = useSelector(state => selectPostById(state, postId))
  // omit component logic
}

데이터 조회를 '캡슐화'하기 위해 재사용 가능한 selector를 작성하는 것은 좋은 아이디어다. 다만, 반드시 모든 곳에서 캡슐화, 추상화를 해야 하는 것은 아니다.

* 캡슐화는 '정보은닉', 추상화는 '복잡한 것들을 간단한 것처럼 보이게 하는 작업' 정도로 이해하자. 캡슐화는 추상화의 수단이라고 볼 수 있겠다.

Loading State for Requests 🔵

API 호출을 수행할 때 다음 네 가지 가능한 상태로, 진행 상황을 'a small state machine'으로 볼 수 있다.

{
  // Multiple possible status enum values
  status: 'idle' | 'loading' | 'succeeded' | 'failed',
  error: string | null
}
  1. 요청이 아직 시작되지 않은 상태

  2. 요청이 진행 중인 상태

  3. 요청이 성공하고 필요한 데이터를 얻은 상태

  4. 요청이 실패하고 오류 메세지가 있을 가능성이 있는 상태

'isLoading: true'와 같은 불리언 값으로 이러한 정보를 추적할 수도 있겠지만, 단일 열거형 값으로 추적하는 것이 더 좋은 방식이라고 한다. 문자열 상태 이름은 고정 값은 아니다. 가령 loading 대신에 pending을 사용해도 문제가 되지 않는다.

-> 위 파트는 솔직히 잘 와닿지 않는다.

profile
Write a little every day, without hope, without despair ✍️

0개의 댓글