DiContainer

리캐치에서 새로웠던 것

  1. state를 rxjs , observable로 관리
  2. data, model layer와 presentation layer의 경계가 분명한 것 → useDiContainer ✨

현재 Recatch Architecture의 구조

recatch 의 프론트엔드 구조는 nextjs로 만들어진 recatch-nextjs와 도메인들을 정의해둔 여러 도메인 package 들로 구성되어 있는데요. 앱 전반을 두고 보자면 크게 domain, data, presentation layer로 구분 되어 있고 이 layer의 경계가 뚜렷하게 나뉘어져있어요.

domain: 도메인 model, repository interface를 정의해두는 곳

data: server에서 전달받는 dto, dto ↔ model converter, repositoryImpl

  • data access layer
  • repositoryImpl은 api layer에 의존하고 있기 때문에 data 폴더에 종속 시켰어요.

presentation: react의 components, hooks, context 정의

이러한 컨벤션을 좀 더 쉽게 유지 하기 위해서 import 관련 eslint rule도 설정해두었습니다. 레이어 분리에서 가장 중요한 건 어떤 레이어던 상위에 있는 레이어를 사용할 수 는 없다는 것입니다.

{
  files: ['/**/domain/**'],
  rules: {
    'no-restricted-imports': [
      'error',
      { patterns: ['/**/presentation/**', '/**/app/**'] },
    ],
  },
},

이렇게 layer 간의 분리를 신경쓰는 이유는 결국 재사용을 쉽게 하고 변경에 유연하게 하기 위함 입니다. 컴포넌트 내에서 도메인 로직이 정의되어 있는 경우를 예를 들어 볼게요.

const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  const [user, setUser] = useState<{
    id: string
    name: string
    email: string
    age: number
  } | null>(null)

  useEffect(() => {
    const fetchUser = async () => {
      const response = await axios.get(`/api/users/${userId}`)
      const userData = response.data

      // 도메인 논리: 나이 기반 사용자 등급 설정
      if (userData.age > 18) {
        userData.userType = 'adult'
      } else {
        userData.userType = 'minor'
      }

      setUser(userData)
    }

    fetchUser()
  }, [userId])

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>Type: {user.userType}</p> // 프레젠테이션 논리에서 도메인 논리를 활용한
      부분
    </div>
  )
}

export default UserProfile

예를 들어, 클라이언트에서 유저의 나이에 따라 유저 타입을 정해야하는 도메인 로직이 컴포넌트 내에서 정의한 경우입니다. 만약 유저 타입이 새롭게 추가되거나 조건이 변경 되는 경우 도메인이 아닌 이를 정의한 component 로직을 수정해야하는 불편함이 생깁니다. 도메인 로직이 응집되어 있지 않고 컴포넌트 내에 있으니 변경 지점들이 많아지게 되겠죠.

presentation에서 domain layer를 사용하는 방법

컴포넌트에서 직접 repositoryImpl, 즉 데이터 레이어에 접근하기 보다는 domain 레이어만 알게끔 만들고 싶었어요. 컴포넌트에서 실질적으로 호출해야할 repositoryImpl을 직접 import 한다면 의존성이 컴포넌트 내에서 만들어지게 됩니다. 의존성을 만들어주는 곳을 외부의 매개체에게 역할을 위임하면서 컴포넌트는 domain 레이어만 알 수 있도록 만들었어요.

React 환경에서 가장 쉽게 injection 할 수 있는 형태인 Context 를 사용하여 외부 의존성을 만들어주는 diContainer를 만들었습니다.

앱에서의 di 사용


di.tsx

// app/di.tsx
import { ActionRepository } from '@recatch/action'
import ActionRepositoryImpl from '../data/repositoryImpl'
import recatchAxios from '../data/axios'

interface DiContainer {
	actionRepository: ActionRepository
}

const actionAPI = new ActionAPI(recatchAxios)
const actionRepository = new ActionRepositoryImpl(actionAPI)

const diContainer: DiContainer {
	actionRepository: actionRepositoryImpl
}

export function DiContainerContextProvider({
  children,
}: {
  children: ReactNode
}) {
  return (
    <DiContainerContext.Provider value={diContainer}>
      {children}
    </DiContainerContext.Provider>
  )
}

export function useDiContainer() {
  return useContext(DiContainerContext)
}

app의 최상단에서 app 전역에서 사용할 수 있는 diContainer의 interface를 정의하고 앱 내에서 사용하게 될 instance들을 만듭니다. 위에서 설명한 대로 최상단의 context에서 diContainer 값을 주입합니다.

컴포넌트 내에서 diContainer 사용

// app/action/components/ActionSettingModal.tsx
function ActionSettingModal() {
  const { actionRepository } = useDiContainer()
  const [action, setAction] = useState()

  useEffect(() => {
    const _action = await actionRepository.getAction(id)
    setAction(_action)
  }, [])
}

이런 점이 편해요

  1. 테스트 하기 편해요.

만약 아래와 같이 repository를 import 했다면 아래 컴포넌트를 테스트 해야할 때, import 문을 mocking 하지 않아도 됩니다.

import actionRepository from '../data/actionRepositoryImpl'

function ActionSettingModal() {
  const [action, setAction] = useState()

  useEffect(() => {
    const _action = await actionRepository.getAction(id)
    setAction(_action)
  }, [])
}
  1. presentation layer에서는 구현체가 아닌 interface에 의존

컴포넌트(presentation)에서는 useDiContainer에서 반환하는 diContainer의 interface만 사용하고 있어요. 그렇기 때문에 이후 repository 구현체를 바꿔야할 일이 생겼다면 쉽게 변경할 수 있어요.

예. feature flag 도메인을 다루는 featureflagRepository가 기존에 localStorage로 관리 되고 있었던게 firestore 로 변경되어야한다면, 이 경우 interface는 유지되고 구현체만 바꿈으로써 코드의 변경을 최소화할 수 있어요.

마무리


Written by@Seokyung
가끔 개발 일지를 씁니다. 그리고..

GitHub