티스토리 뷰

개발바닥 유튜브를 보던 중 [React 불만 大 발설] 영상을 보고 내가 생각하는 React Best Practice를 정리해보면 좋겠다고 생각했습니다. (영상링크 * https://www.youtube.com/watch?v=1V6mQom0paI)

 
기본적으로 해당 영상에서 말씀하시는 내용 중 OOP가 FE에서 필요한 개념인가? 없으면 어떻게 짜는가? 라는게 영상 내용의 주요 주제 중 하나입니다. 
 
위의 내용들을 제 경험에 투영하여 작성하려하며, 온전히 제 경험에서 나온 내용이라 정답은 아니라는 점을 유의해 주시길 바랍니다.
 
해당 Practice는 Typescript를 기본으로 하기 때문에 Javascript프로젝트에 도입하기에는 한계가 있을 수 있는 점 유의해 주세요!

해당 Practice에서 사용한 주요 철학, 개념은 아래와 같습니다.

1. OOP

2. MVVM

+ 철저한 역할 분담

 

위 두 가지 주요 개념을 가지고 각각의 레이어가 철저하게 각자의 역할만을 담당하고 데이터의 흐름이 단방향이어야 한다는 기본 골자가 필요합니다. 이런 개념은 결국 각 레이어의 테스트를 분리시킬 수 있어서 컴포넌트가 없는 레이어의 경우 Mocking 없이 테스트 가능한 코드를 작성할 수 있도록 만들어 줍니다.

 

사용하는 라이브러리
0. React (+ CRA)

1. Typescript

2. MobX

 

프로젝트의 기본 셋업은 Creact React App를 이용했습니다. 상태 관리 툴은 MobX를 이용했는데 이는 모델을 Class를 이용해 관리하기 위함입니다.

컴포넌트의 경우 1회성으로 그려지고 그때 당시의 상태에 상당한 의존성을 가질 수밖에 없기 때문에 함수형 컴포넌트를 사용합니다. (사실 작성해야 하는 코드가 적고 심플해서 functional을 선호합니다)

그리고 컴포넌트를 작성하는 파일에는 다른 함수나 기능에 대한 정의를 하지 않기 때문에 더욱 심플한 코딩 방법이 좋은 것 같습니다.

 

프로젝트의 기본적인 폴더 구조는 위와 같습니다. 

- components: 모든 컴포넌트를 해당 폴더에 작성합니다. (전 아토믹 디자인 패턴을 선호합니다! 컴포넌트를 설계하는 철학 중 하나입니다.)

- modules: 모델과 비즈니스로직 처리를 위한 레이어를 작성합니다. (MobX코드가 기본적으로 해당 폴더에 집중됩니다.)

- pages(views): 각 페이지 컴포넌트를 작성하며 실제 라우팅과 파일명을 똑같이 매핑하여 사용합니다. 

- utils: 부가적인 유틸 함수를 작성하는 데 사용합니다. (보통 유틸의 경우 백엔드에서도 싱글턴 또는 전역으로 작성되므로 별도의 이유가 없다면 함수들을 중심으로 작성합니다.)

 

1. View 와 ViewModel

가장 먼저 개념을 잡고 넘어가야 할 내용은 뷰와 뷰 모델입니다. MVVM 패턴을 이미 알고 계신 분들은 이미 잘 아실 수 있으니 간단하게 정리하고 넘어가겠습니다.

 

뷰는 실제 사용자에게 보여주는 화면을 구현하고 렌더링 하는 역할을 담당하게 됩니다. 

뷰 모델은 화면에 보여주는 정보들만을 가지고 있으며 여러 모델에서 데이터를 읽어오게 됩니다. 

 

MVVM 패턴의 간략한 참조 방향성

import useHomeViewModel from "./home.viewmodel";

export default function HomePage() {
  const homeViewModel = useHomeViewModel();

  return (
    <>
      <p>count : {homeViewModel.count}</p>
      <div style={{ display: "flex", gap: "5px" }}>
        <button onClick={homeViewModel.addCount}> + </button>
        <button onClick={homeViewModel.subCount}> - </button>
      </div>
    </>
  );
}

위 내용은 View를 간단하게 구현한 코드입니다. (+버튼을 누르면 카운트가 1씩 증가하고 - 버튼을 누르면 1씩 감소하는 컴포넌트입니다.)

View는 순수한 UI코드만을 가지게 되며 표현에 필요한 데이터, 이벤트 함수들을 모두 뷰 모델을 참조하는 구조를 취하고 있습니다. 여기서 유의할 점은 viewModel은 view를 참조하지 않아야 한다는 점입니다. (단방향 바인딩) 위에 코드에서도 homeViewModel을 사용할 때 View자신을 넘겨주지 않습니다. 

 

import React, { useState } from "react";

export default function useHomeViewModel() {
  const [count, setCount] = useState(0);

  function addCount() {
    setCount(count + 1);
  }
  function subCount() {
    setCount(count - 1);
  }

  return {
    count,
    addCount,
    subCount,
  };
}

ViewModel에서도 보시는 것처럼 view를 참조하거나 직접적인 호출은 없습니다. 순수하게 자기 자신을 표현할 수 있어야 합니다. 위 구조는 custom hook을 활용하여 ViewModel을 구성합니다. 리엑트에서 기본적으로 제공하는 hook들은 최적화된 ReRendering 로직을 제공하여 편리하게 observe 패턴 비슷하게 구조를 가져갈 수 있다. 물론 이 이유도 단방향 바인딩을 유지하기 위해서입니다.

 

ViewModel은 실제 View 없이도 동작할 수 있어야 하며 기능이 완성적이어야 하지만 View와는 1 대 1로 구성해야 합니다. 이유는 ViewModel의 이름에도 나와 있듯이 View를 표현한 모델이기 때문입니다.

 

이제 View에서 모델을 가져오고 각 객체에 데이터와 역할을 할당할 것입니다. ViewModel은 각각의 Model들을 참조해야 하며 변화를 감지해야 합니다. 이를 해결하기 위해 다양한 전역 데이터 / 상태 관리 라이브러리인 MobX를 사용합니다. Redux를 활용하는 방법도 있지만 MobX를 사용하는 이유는 완전한 객체지향적인 구조를 유지할 수 있기 때문입니다. 뿐만 아니라 수많은 부가 코드를 줄일 수 있는 장점도 있습니다. 해당 부분은 각자 편한 상태 관리 라이브러리를 이용할 수 있지만 저는 강력하게 MobX를 추천합니다. (Redux를 이용하는 방법은 추후 다루도록 해보겠습니다.)

 

2. Model관리를 위한 MobX 패턴

 MobX를 위한 모델 관리를 들어가기 전에 우리는 몇 가지 규약을 정하고 시작해야 합니다. 해당 부분은 팀마다 다를 수 있습니다. 전 최대한 일반적인 용어를 활용하여 역할을 분리했습니다.

 

- Service: 각 역할에 대한 비즈니스 로직을 수행하며 데이터 패칭, 수정, 가공 등의 역할을 중계 및 담당합니다.

- Repository: 백엔드 서버와 통신을 통해 데이터를 실제로 가져옵니다. (API 통신 역할 담당)

- Object (Entity): 백엔드에서 가져온 데이터를 담을 수도 있고 자체적인 정보를 담습니다. 또한 해당 객체의 역할을 표현합니다.

- Dto (필요에 따라 사용): Repository에서 API를 호출하여 데이터를 받는 용도로 사용하는 데이터 객체입니다. 데이터만 담기 때문에 interface로 타입만 정의해도 괜찮습니다.

 

import Counter from "./counter";

class CounterService {
  counter: Counter;

  constructor() {
    this.counter = new Counter();
  }

  addCount() {
    this.counter.plus();
  }

  minusCount() {
    this.counter.minus();
  }

  get count() {
    return this.counter.value;
  }
}

export default CounterService;

간단한 CounterService입니다. CounterService는 counter객체를 관리하며 각각의 비즈니스 로직을 구성하게 됩니다. 예제의 복잡도가 낮아 그냥 counter를 리턴해주는 부분 하나만 있으면 되지 않을까? 라고 생각할 수 있지만 실제 복잡도가 올라가기 시작하면 비즈니스 로직을 구성하는 Service는 필수적으로 필요합니다.

 

모든 모델 객체를 직접 컨트롤하게 된다면 특정 비즈니스 규칙이나 로직에 대한 관리 비용이 기하급수적으로 증가합니다. 모델의 특정 기능을 두 개의 페이지에서 사용한다고 했을 때 서비스가 없이 뷰 모델에서 직접 관리한다면 기능은 두 개로 분리되며 완전히 같은 기능을 제공할 수 없으며 이를 위해 코드 전체의 비즈니스 로직을 점검해야 합니다. 따라서 Service를 이용하여 비즈니스 로직을 한 곳으로 집중시켜 관리 비용을 줄이고 보다 효율적인 테스트 환경을 조정할 수 있습니다.

 

import { makeAutoObservable } from "mobx";

class Counter {
  private _value: number;

  constructor() {
    makeAutoObservable(this);
    this._value = 0;
  }

  get value() {
    return this._value;
  }

  plus() {
    this._value++;
  }

  minus() {
    this._value--;
  }
}

export default Counter;

실 객체로 데이터의 관리를 담당하는 모델 부분입니다. 객체의 정보와 역할을 담당하며 해당 객체의 도메인 로직을 모두 객체에서 직접 소화하는데 실제 MobX를 이용한 observable데이터를 만드는 위치이기도 합니다.

 

우리는 데이터의 리스트를 보여줘야할 때도 있기 때문에 배열이나 다른 리스트 기반 자료구조를 사용할 수도 있습니다. 그럴 때에도 마찬가지로 각 데이터를 객체로 만들어 관리해야 합니다. 

 

1편 마무리

생각보다 내용이 길어져 2편으로 나누려 합니다. 2편에서는 1편에서 다루지 못한 Repository와 정말 표현만을 위한 데이터를 위한 interface 사용법 등을 소개하겠습니다.

 

내가 생각했을 때 UI를 통해 소비되고 변경되는 데이터는 객체로 관리하여 표현과 역할을 명확하게 할당해주어야 합니다. 이런 이유는 명확한대, OOP는 단순하게 Class만 사용하고자 하는 게 아닌 사람의 사고 영역을 컴퓨터에 이식한 방법이기 때문입니다. 그렇기 때문에 사용자가 직접 이용하는 프론트엔드라면 더더욱 사용자에 가까운 사고방식으로 코드가 작성돼야 한다고 생각합니다. 

반응형

'개발' 카테고리의 다른 글

나아가기 위해 버린다  (0) 2022.09.15
Service와 Interface  (0) 2022.09.14
악마는 디테일에 있다  (1) 2022.07.11
My React OOP + MVVM practice (feat. OOP, Typescript) -2-  (0) 2022.07.08
사이드 프로젝트 기술 스택 선정  (0) 2022.05.22
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함