반응형

소프트웨어는 탄생과 동시에 생명 주기를 갖는다.

소프트웨어 개발자는 소프트웨어가 변경될 가능성에 더 신경써야 한다.

 

소프트웨어 개발 수명 주기 (Software Development Life Cycle,  SDLC ), 출처 - 위키피디아

 

소프트웨어는 시간의 흐름에 따라 언젠가 죽음을 맞이하게 될텐데 초기 설계 단계에서부터 변경과 폐기에 유연한 소프트웨어를 개발해야한다. 

 

특히 프론트엔드 개발은 개발 패러다임이 자주 바뀌거나

사용하던 라이브러리의 버전업 & 유지보수가 끝나는 일이 잦은 편이라

코드 폐기 & 수정에 유연한 프로그램을 만들어야한다. 

 

그럼 프론트엔드에서 어떻게 유연한 구조를 가질 수 있을까?

 

개발자로 일하면서 기존 코드의 수정이 너무 힘들어서 좀 더 좋은 아키텍처를 만들수는 없을지, 

어떻게 변경에 유연한 소프트웨어를 만들지 궁금해서 몇주간 찾아봤고,

해답을 모듈화와 추상화에서 찾았다.

 

모듈화

모듈화 예시

 

모듈화란 큰 시스템을 작은 부분으로 나누는 프로그래밍 설계 기법이다.

작은 부분들을 모듈이라고 부르며, 각 모듈은 독립적으로 기능을 수행하고 다른 모듈과는 추상화된 인터페이스를 통해 상호작용한다. 

 

사실 리엑트에서 컴포넌트를 사용하고 컴포넌트들을 조합해서 개발하고 있다면 모듈화를 잘 활용하다고 볼 수 있지만

인터페이스로 상호작용하라는 말이 잘 안와닿을수도 있다.

 

예를 들어 아래 그림 같이 MUI 라이브러리의 버튼을 사용한다고 해보자.

 

import React from 'react';
import Button from '@mui/material/Button';

const ExampleList = () => {
  ... // 생략

  return (
  ....
    <Button variant={'primary'} color={'blue'}>
      confirm
    </Button>
    <Button variant={'primary'} color={'red'}>
      exit
    </Button>
   ....
  );
};

 

그런데 MUI의 라이브러리의 지원이 끝났고 Ant.d 라이브러리나 다른 라이브러리 버튼으로 교체한다고 생각해보자.

 모든 페이지의 MUI button을 찾아서 수정해야할텐데 상상만 해도 끔찍하다...

 

해당 방식을 피하기 위해선 아래처럼 "Button" 을 모듈화 하고 Prop으로 필요한 값, 함수들을 내려보내는 식으로 해결하면 된다.

 

// MyButton.js
import React from 'react';
import Button from '@mui/material/Button';

const MyButton = ({ variant, color, children, ...props }) => {
  return (
    <Button variant={variant} color={color} {...props}>
      {children}
    </Button>
  );
};

export default MyButton;

 

상당히 간단한 일이지 않을까? 아니, 사실 개발하면서 우리가 계속 해오고 있던 일이다.

 

버튼의 상세 구현은 Mybutton 컴포넌트 내부 로직에 격리되어 있고, 외부에서 prop을 통해 필요한 값을 주입하면서 서로 통신하고 있다. 

prop을 사용하는 컴포넌트와 실제 구현된 컴포넌트를 연결하는 인터페이스로 쓰고, List는 모듈화 된 Button을 내부 구현을 몰라도 가져다 쓸 수 있는 셈이다.

 

모듈화는 레고 조립과 같다. 자동차에서 타이어를 교체하기 위해서는 타이어만 바꿔 끼우면 되지 자동차 엔진 내부 구조까지  알아야할 필요가 없을 것이다. 

 

이를 컴포넌트 주도 개발 - CDD (컴포넌트를 모듈 단위로 개발하는 및 설계 방법론)라 부른다. 

 

모듈화를 잘 만들기 위해선 객체 지향 방법론의 SOLID를 참고하면 좋을 듯 싶다.

특히, 의존 역전 원칙(DIP) - "고수준(추상화된 인터페이스)은 저수준(상세 구현)에 의존하지 말아야 한다."

을 지키면 좋은 설계 구조를 가질 수 있다.

 

테스트

각 로직을 모듈화했다면 테스트 코드도 작성하면 좋을것 같다.

테스트는 변경에 유연한 컴포넌트 구조를 만들기 위한 핵심이다.

각 모듈은 유닛 테스트로 안정성을 보장하고,

통합 테스트로 연결된 모듈끼리 안정적인지 보장하면 소프트웨어가 견고해질 것 같다. 

 

테스트는 항상 동일한 input에서는 동일한 output이 보장되어야한다.

 

만약 특정 요일에만 세일하는 함수을 테스트한다고 해보자.

 

const isSale = () => {
    const date = new Date();
    const day = date.getDay(); // 0이 일요일, 1이 월요일
	
    return day === 1;
};

 

해당 함수를 테스트하면 월요일에만 성공할 것이다. 또한, 이 함수를 사용하는 모든 모듈들은 월요일에만 성공할 것이다.

그렇다고 이 함수만을 mocking하면 통합 테스트를 하는 의미가 없을것이다.

이 함수가 통합 테스트에서 정상 작동하는 보장이 없으므로 mocking은 최후의 대안이다.

 

왜 이 함수는 테스트가 힘들까?

왜냐하면 함수가 순수하지 않은 부분(Date 객체 - side Effect가 있는 부분)이 있기 때문이다.

순수하지 않은 함수는 오염된다. 오염된 함수는 다른 모듈도 오염시킨다. 

 

비순수함수는 최대한 회피하거나 격리시켜야 한다.

 

회피

 

회피의 예시로는 parameter를 사용하는 방식이 있다.

const isSale = ({ date: new Date() }) => {
    const day = date.getDay(); // 0이 일요일, 1이 월요일
	
    return day === 1;
};

순수하지 않은 부분을 외부(parameter)로 밀어내고 default Parameter로 변경시키면 

순수함수인척 눈속임이 가능하다.

 

격리

 

격리의 예시로는 네트워크 콜이 있을 것이다. API call은 서버 상태에 따라 결과가 달라지므로 어쩔 수 없이 격리 시키고 mocking시켜야 한다.

 

결론

비순수함수의 격리

모듈끼리 통신하는 부분은 순수함수로 작성하는것이 최선이다만 sideEffect가 발생하는 부분은 외부로 밀어내서 격리 시키거나 회피하는것이 좋다.

 

로직의 모듈화 

 

그렇다면 "컴포넌트 내부"는 어떨까?

컴포넌트를 개발하면서 api나 여러 함수, 조건문들이 엮여서 유지보수에서 어려움을 겪은적이 있을것이다.

 

컴포넌트 내부 로직들도 모듈화를 시키고 마치 레고처럼 조합해서

필요한 부분만 교체하고 폐기시키면 더 좋지 않을까?

 

그럼 무엇을 나눌 수 있을까?

나누기 전에 무엇이 있는지 정의부터 해야할 것 같다.

 

예시 그림들은 https://martinfowler.com/articles/modularizing-react-apps.html 에서 가져왔다.

해당 글은 코드 예시를 들어서 상세히 설명해주고 있으니 읽어보는것이 좋을것 같다.

 

컴포넌트 내부 

 

1. 모든게 섞여있는 컴포넌트

모든게 섞여있는 single Component

리엑트 컴포넌트 내부에는 크게 아래의 4가지로 묶을 수 있을것 같다.

 

1. 네트워크 (API 콜)

 

네트워크를 통해 가져오는 데이터들이다. 

순수하지 않은 특성(side Effect)를 가지고 있다. 

 

2. 상태

 

지역 상태 - useState나

전역 상태 - redux, justand, context 등 view를 제어하는 로직들을 말한다.

state를 변경하는 action과 observing되서 변경되는 state들로 이루어져 있다.

 

3. 도메인 로직 

여기서 3번 도메인 로직이 좀 생소한 개념일수도 있는데

예를 들어서 쇼핑 결제 파트를 만들때 원으로 결제할지 혹은 달러나 엔으로 결제할지등의 오프라인의 문제를 프로그래밍으로 해결하기 위한 방법을 말한다. 

 

도메인 로직은 프로그래밍이 아닌, 외부 이벤트에 따라 변경될 가능성이 높고 따라서

모듈화를 통하여 저수준 (상세 구현)을 격리하고 인터페이스를 통해 통신하는 방식이 유지보수에 도움이 된다.

 

4. UI 로직 (render)

 

최종적으로 브라우저에 보여지는 부분을 말한다.

리엑트의 JSX는 babel를 통해 React.CreateElement로 변환되고 이는 최종적으로 브라우저의 HTML로 반영된다

 

여기서 4번 UI 부분은 디자인에 의해 자주 변경되고 1,2,3 번(logic)들에 의해 최종적으로 브라우저에 반영되는 결과물이다.

따라서 view와 logic을 일단 분리시켜야 유지보수가 편해질것 같다.

 

2. view와 logic이 분리된 컴포넌트 

logic은 hook으로 분리시키고, view 부분은 logic에 따라서 반영되는 순수한 UI 컴포넌트가 되었다.

또한 logic을 분리시켰으니 다양한 UI에 hook를 재사용 가능할 것 같다.

 

그런데 아까 보았던 1,2,3번 logic이 아직 모듈로 분리가 되지는 않았다. (예시 사진에 state가 분리되있긴 하지만 전역 상태는 hook에 있다고 치자.)

 

도메인 로직을 처리할때 엔화로 결제할지, 아님 원으로 결제할지가 중요하지

실제 프론트엔드가 redux를 쓰는지 context를 쓰는지 뭘 쓰는지는 중요하지 않고 알 필요도 없는게 이상적이다.

따라서, state logic과 도메인 로직은 분리되어야 나중 라이브러리 교체(redux->justand 등등)도 수월해진다.

 

마찬가지로 네트워크 API를 사용할때 상세 구현인

웹소캣을 사용하는지, api 콜을 기억해두었다 localStorage에서 꺼내와서 쓰는지가

다른 로직 입장에선 주 관심사가 아니다. 그냥 내가 필요한 데이터만 요청해서 꺼내와서 쓰면 된다.

 

또한, 테스트 측면에서도 네트워크 부분은 비순수함수이므로 격리되어야 한다.

 

따라서 각각 logic은 분리되고 인터페이스로 통신하는것이 이상적이다.

 

3. logic이 모듈화된 컴포넌트 

 

이제 각 로직이 모듈화되었고 서로 인터페이스로 따라 통신하면서 레고처럼

로직을 교체하기 쉬운 구조가 된 것 같다.

 

분리를 통해 유연해진 컴포넌트를 작성한 것 같다.

 

logic을 교체할때 좋은 방식은 무엇이 있을까?

 

예를 들어서, useHook에서 도메인 변경 - "결제 방식을 엔화에서 원화로 바꾸는 예시"를 들어보자.

의존성 주입에서 영감을 받아서 prop으로 도메인 "전략"을 바꾸면 좋을 것 같다.

 

객체 지향을 사용해도 되고 아니면 함수를 고차함수로 사용해도 되고 취향에 따라 사용하면 된다.

아래 예시는 객체 지향의 전략 패턴이다.

전략 패턴 예시

 

코드로 보면 다음과 같다.

 

export interface PaymentStrategy {
  getRoundUpAmount(amount: number): number;

  getTip(amount: number): number;
}

export class PaymentStrategyAU implements PaymentStrategy {
  get currencySign(): string {
    return "원";
  }

  getRoundUpAmount(amount: number): number {
    return Math.floor(amount + 1);
  }

  getTip(amount: number): number {
    return parseFloat((this.getRoundUpAmount(amount) - amount).toPrecision(10));
  }
}

 

interface를 사용해서  고수준에서 클래스의 "동작"를 정의했다.

그리고 interface를 상속한 클래스에서 실제 클래스의 "작업"을 구현했다.

 

  export const useRoundUp = (amount: number, strategy: PaymentStrategy) => {
    const [agreeToDonate, setAgreeToDonate] = useState<boolean>(false);
    ....
  
    const { total, tip } = useMemo(
      () => ({
        total: strategy.getRoundUpAmount(amount),
        tip: strategy.getTip(amount),
      }),
      [agreeToDonate, amount, strategy]
    );
    
    ...
  
    return {
      .....
    };
  };

 

위 코드는 hook에서 domain을 위에서 주입받은 예시이다.

strategy는 상황에 따라 바꿔끼면 다양한 domain을 상황에 따라 갈아끼울 수 있다.

 

여담으로

특별 할인 이벤트할때마다 react를 다시 빌드하고 싶지 않으면

프론트엔드에서 도메인 로직을 관리하기 보다는 백엔드에서 주 정책을 관리하는편이 좋다. (api call을 통해 주입받는게 좋음)

 

백엔드에서 data로 what을, 프론트에서 UI/UX로 how를 풀어나가는 편이 좋은것이라 생각한다.

 

 

reference

 

구글 엔지니어는 이렇게 일한다 - 일부 부분에서 영감을 받음 

https://product.kyobobook.co.kr/detail/S000061352347?utm_source=google&utm_medium=cpc&utm_campaign=googleSearch&gclid=CjwKCAjwm4ukBhAuEiwA0zQxkwPDg9tDbJxxFQn99zEAQXUJzUseeGWdT7iUhjtNc5yC5GNZl10M8BoCvCgQAvD_BwE 

 

구글 엔지니어는 이렇게 일한다 | 타이터스 윈터스 - 교보문고

구글 엔지니어는 이렇게 일한다 | 구글은 어떻게 개발하고 코드를 관리하는가지난 50년의 세월과 이 책이 입증한 사실이 한 가지 있습니다. 바로 '소프트웨어 엔지니어링의 발전은 결코 정체되

product.kyobobook.co.kr

 

https://martinfowler.com/articles/modularizing-react-apps.html

 

Modularizing React Applications with Established UI Patterns

Learn how to apply established UI patterns for a more organized and maintainable codebase and discover the benefits of layering architecture in React development

martinfowler.com

 

반응형

'Front-end > React' 카테고리의 다른 글

Next 14 tree shaking 관련 조사  (0) 2024.02.27
jotai 내부 구조 훑어보기  (0) 2023.11.25
react HOC와 decorator  (0) 2023.05.14
react Dialog tab trap 만들기  (1) 2023.05.01
프론트엔드 테스트 정리 요약  (1) 2022.11.30

+ Recent posts