본문 바로가기

FrontEnd

[Epic React][Build an Epic React App][Context]

반응형

bookshelf/INSTRUCTIONS.md at exercises/07-context · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

 

전역 상태는 여러 문맥으로 쪼개야 한다. Xuck Redux

리액트 쿼리와 함께 글로벌 스테이트의 대부분이라 할수있는 서버 상태를 분리했다.

이제 아래 것들과 같은 것들은 각자의 전역 상태 문맥(컨택스트)에 집어넣어 활용할 수 있다.

즉, 글로벌 상태처럼 사용할 수 있으나, 각자의 상태에 의미를 부여하는 것이다.

  • 라우터
  • 사용자 인증 상태
  • 컴파운드 컴포넌트
    • "토스트" 알림
    • 모달
    • 포커싱

해당 방법의 장점

  • 컨텍스트 API를 통해 해당 문맥을 선언적으로 표현할 수 있다.
  • props drilling을 피한다.
  • useContext훅을 이용해 해당 컴포넌트의 관심사를 명확하게 보여줄 수 있다.

실습 : 사용자 인증 상태 컨텍스트 분리

1. AuthProvider

클라이언트와 연결을 수행한다.

회원관런 (로그인, 로그아웃, 회원가입) API를 제공한다.

웹페이지 접속 시 회원 정보를 가져온다.

인증이 필요한 API에 회원 정보를 제공한다.

/** @jsx jsx */
import {jsx} from '@emotion/core'

import * as React from 'react'
import {queryCache} from 'react-query'
import * as auth from 'auth-provider'
import {client} from 'utils/api-client'
import {useAsync} from 'utils/hooks'
import {FullPageSpinner, FullPageErrorFallback} from 'components/lib'

async function getUser() {
  let user = null

  const token = await auth.getToken()
  if (token) {
    const data = await client('me', {token})
    user = data.user
  }

  return user
}

//컨텍스트 생성
const AuthContext = React.createContext()

// devtool 지원
AuthContext.displayName = 'AuthContext'

// 컨텍스트 프로바이더.
function AuthProvider(props) {
  const {
    data: user,
    error,
    isLoading,
    isIdle,
    isError,
    isSuccess,
    run,
    setData,
    status,
  } = useAsync()

  React.useEffect(() => {
    run(getUser())
  }, [run])

  // 로그인
  const login = form => auth.login(form).then(user => setData(user))
  
  // 회원가입
  const register = form => auth.register(form).then(user => setData(user))
  
  // 로그아웃
  const logout = () => {
    auth.logout()
    queryCache.clear()
    setData(null)
  }

  if (isLoading || isIdle) {
    return <FullPageSpinner />
  }

  if (isError) {
    return <FullPageErrorFallback error={error} />
  }

  if (isSuccess) {
    const value = {user, login, register, logout}
    return <AuthContext.Provider value={value} {...props} />
  }

  throw new Error(`Unhandled status: ${status}`)
}

useClient훅과 useAuth를 통해 인증 관련 API를 제공한다.

또한 컨텍스트에서 value로 제공되는  {user, login, register, logout}에 주목하자.

컨텍스트를 하나의 클래스로 보고 변경을 캡슐화하면 dispatcher를 외부에 제공하지 않아도 된다.

// 컨텍스트 사용 훅
function useAuth() {
  const context = React.useContext(AuthContext)
  if (context === undefined) {
    throw new Error(`useAuth must be used within a AuthProvider`)
  }
  return context
}

// 인증이 필요한 API 
function useClient() {
  const {
    user: {token},
  } = useAuth()
  return React.useCallback(
    (endpoint, config) => client(endpoint, {...config, token}),
    [token],
  )
}

export {AuthProvider, useAuth, useClient}

useClient는 다음과 같이 사용한다.

const client = useClient();
client(`books/${bookId}`).then(data=>data.book);
인증 요청 API가 약간 변경되면 다른 모든 위치에서 업데이트할 필요 없이 이 하나의 모듈 내에서 처리할 수 있다.
훅을 사용하면 관심사를 쉽게 분리하고 합성할 수 있다.

2. 글로벌 프로바이더를 하나의 위치에(colocation)

src/context/index.js 위치에 모든 컨텍스트를 모은다.

import * as React from 'react'
import {BrowserRouter as Router} from 'react-router-dom'
import {ReactQueryConfigProvider} from 'react-query'
import {AuthProvider} from './auth-context'

const queryConfig = {
  queries: {
    useErrorBoundary: true,
    refetchOnWindowFocus: false,
    retry(failureCount, error) {
      if (error.status === 404) return false
      else if (failureCount < 2) return true
      else return false
    },
  },
}
function AppProviders({children}) {
  return (
    <ReactQueryConfigProvider config={queryConfig}>
      <Router>
        <AuthProvider>{children}</AuthProvider>
      </Router>
    </ReactQueryConfigProvider>
  )
}

export {AppProviders}

루트에서 이렇게 쓰면 된다.

import {loadDevTools} from './dev-tools/load'
import './bootstrap'
import * as React from 'react'
import ReactDOM from 'react-dom'
import {App} from './app'
import {AppProviders} from './context'

loadDevTools(() => {
  ReactDOM.render(
    <AppProviders>
      <App />
    </AppProviders>,
    document.getElementById('root'),
  )
})

 

 
반응형