의존성을 배제한 개발

개발 의존성

의존성이 커질수록 제품은 더 복잡해지고 문제가 발생하기 쉬워진다.

제품 개발의 의존성을 줄이는 아키텍처 설계에 대해 신경을 쓰고 도입해보자.

의존성으로 인한 문제

직군 간의 의존성

  • BE <--> FE, PM <--> FE, 상호간 커뮤니케이션 문제로 인한 개발 시간 증가

  • 기획 정의서가 준비될 때 까지 기다린다거나 BE API spec을 기다리는 등의 대기시간 낭비

모듈 간의 의존성

  • 코드 레벨에서 모듈간의 의존성이 커질수록 복잡도가 커지게 된다.

  • 코드 리팩토링 또는 기능이 수정 & 추가되어야 하는 상황에서 개발 난이도가 증가하게 된다.

일정 상의 의존성

  • 일정 지연으로 인한 개발 시간 준수가 어렵게 된다.

  • 다른 테스크들을 병행하면서 개발할 때 컨텍스트 스위칭 비용이 발생하여 집중도 하락 및 개발 시간 증가

BE 의존성 줄이기

Mocking이 필요한 상황

  • 개발 과정에서 준비되지 않은 서버 API 응답 처리를 위해 사용

    • API Mocking Layer 구현으로 Mocking 여부에 대한 차이를 파악할 필요없도록 구성

  • API Mocking을 통해 정확한 API Interaction 모델링과 성공/실패 케이스를 모의하여 디버깅 필요

    • Client 이슈는 대부분 Data 문제로 잘못된 API 호출, 오류처리 누락, 예상치 못한 response 반환같은 상황 등

MSW (Mock Service Worker)

  • Service Worker API를 이용해 HTTP 요청 가로챔

  • API 요청 발생 시 가로채서 준비한 Mock Data로 응답

  • 별도의 Mock Server 없이 Mocking 가능

Service Worker

  • 웹 어플리케이션의 백그라운드 스레드에서 실행됨

  • UI Block 없이 연산 가능

  • Network 요청 가로채서 응답 모킹 가능

동작 원리

  1. 브라우저 request 발송

  2. worker는 해당 요청을 받고 클론한다음 MSW로 전달

  3. 전달받은 요청과 일치하는 목 데이터가 존재하는지 조회

  4. 존재한다면 ServiceWorker에 Mock Response 전달

  5. ServiceWorker는 전달받은 응답을 브라우저에게 전달

MSW 설정

Install

npm install msw -D
// crate mockServiceWorker file in public folder
npx msw init public/ --save

Request Handler 설정

  • response 값 설정 (code, data)

  • URL/RegExp/특정 조건의 request 캡처

  • HTTP method 별 조건 적용

// src/mocks/products/handler.ts
import { rest } from 'msw'
import productsData from './products.json'

import type { RestRequest, ResponseComposition, DefaultBodyType, RestContext } from 'msw'
import { ProductResponse } from '@/api/products'

const getProducts = (
    req: RestRequest,
    res: ResponseComposition<ProductResponse>,
    ctx: RestContext
) => {
    return res(ctx.status(200), ctx.json({ products: productsData }))
}

const getProductsHandler = [rest.get(`${process.env.NEXT_PUBLIC_URL}/api/products`, getProducts)];

export default getProductsHandler

Worker Setup

  • 구현한 handler들을 worker에 삽입

// src/mocks/browser.ts
import { setupWorker } from 'msw'
import productHandler from './products/handler'

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...productHandler)
  • worker 실행 (production 빌드는 수행 안되도록 env 조건 설정)

import { worker } from '@/mocks/browser';

if (process.env.NODE_ENV === 'development') {
  worker.start();
}

export default function App() {//...}

Next.js 에서 MSW 이슈

  1. Next.js는 서버사이드에서도 API 호출할 수 있기 때문에 브라우저 모킹뿐만아니라 서버모킹(node)가 필요

  2. MSW init 이전 API 호출 발생될 수 있음

    1. MSW를 init 후 Component load 필요 -> HOC 구성

    2. initMock 함수를 dynamic import 후 실행, loaded 상태 설정을 통해 children 로드 진행

    3. 만약 MSW를 사용하지 않는 경우 최초 상태값 설정을 통해 바로 Component를 로드하도록 구성

import { setupServer } from 'msw/node'
import productHandler from './products/handler'

export const server = setupServer(...productHandler)
 // src/mocks/index.ts
export async function initMocks() {
    console.log('init mocks')
    if (typeof window === 'undefined') {
        const { server } = await import('./server')
        server.listen()
    } else {
        const { worker } = await import('./browser')
        worker.start()
    }
}
 // src/mocks/MSWProvider
 import { PropsWithChildren, useState, useEffect } from 'react';

const isMockingEnabled = Boolean(process.env.NEXT_PUBLIC_API_MOCKING) 
    && process.env.NODE_ENV !== 'production';

export const MSWProvider = ({ children }: PropsWithChildren) => {
    const [isLoaaded, setIsLoaded] = useState(!isMockingEnabled)

    useEffect(() => {
        const init = async () => {
            if (isMockingEnabled) {
                const fn = await import('./index').then((res) => res.initMocks)
                await fn()
                setIsLoaded(true)
            }
        }
        !isLoaaded && init()
    }, [isLoaaded])

    if (!isLoaaded) {
        return null
    }

    return (
        <>
            {children}
        </>
    )
}

API Layer DI

의존성(Dependency)

의존성이란 두 모듈간의 연결, 서로 간 관계가 있는 경우 의존성이 있다고 본다.

  • 한 모듈이 다른 모듈을 사용할 때

    • 다른 모듈을 생성할 때

    • 다른 모듈의 함수를 호출할 때

    • 다른 모듈을 인자로 받아 사용할 때

의존성이 위험한 이유

  • 한 모듈의 변경이 다른 모듈에 영향을 끼침

  • 이러한 영향이 모듈간 전파되어 알 수 없는 Side Effect 발생

의존성 주입(DI, Dependency Injection)

  • Inversion of Control

  • 모듈의 의존성이 내부가 아닌 외부로부터 주입 받는 형태

  • Component의 props로 의존성 주입

의존성 분리 장점

  • 모듈간 의존성 감소

  • 재사용성 증가

  • 유닛테스트 용이

  • 가독성 증가

Tree shaking

나무를 흔들어 죽은 나뭇잎을 떨어트리는 것처럼, 사용하지 않는 코드를 제거하는 것을 의미

즉, 사용하는 코드들만 사용해서 빌드하는 것

Next.js Tree shaking 적용

v13.5 이상 버전만 사용 가능한 방법

 // next.config.js
 module.exports = {
   experimental: {
     optimizePackageImports: ['package-name'],
   },
 }
  • experimental.optimizePackageImports 에 패키지를 추가하면 자동으로 tree shaking 적용

  • 많이 사용되는 대부분의 라이브러리들(ex: lodash 등)은 기본적으로 추가 되어있어 따로 적용하지 않아도됨

v13.1 < version < v13.5 사용 가능한 방법

 module.exports = {
   swcMinify: true,
    experimental: {
     modularizeImports: {
       antd: {
         transform: 'antd/lib/{{member}}',
       },
       lodash: {
         transform: 'lodash/{{member}}',         
       }
     }
   },
 }
  • experimental.modularizeImports 에 패키지와 transform할 형태 정의

빌드시--verborse 옵션을 추가하여 build시 실행시간을 측정할 수 있다.

npm run build --verborse

결론

Barrel < Barrel with [modularizeImports | optimizePackageImports] < default import

Barrel

  • Barrel 파일은 여러 모듈이나 파일을 하나의 집합으로 내보내기 위해 사용되는 파일을 가리킨다.

  • Barrel export pattern -> 여러 모듈이나 파일을 묶어서 간결하게 내보내는 디자인 패턴

Last updated