어째 방법론에 대한 글을 2주째 쓰는 느낌인데, 최근에 리팩토링 하거나 기존 기능에서 디벨롭할 일이 많아지면서 자연스럽게 방법론이나 구조화에 관심이 가는 것 같다😅
특히 최근에 작성한 컴포넌트 내 로직이 너무너무 복잡해서 설명을 위해 간략히 ERD를 짜다 나도 헷갈리는 상황이 발생했다. 이제는 더 이상 물러날 곳이 없다는 생각이 들었고(ㅠㅠ) 비슷한 이유로 Storybook 도입도 고민 중이었기에 코드 분리를 어떻게 할 수 있는지 예제를 좀 공부해보려고 한다.
오늘의 포스팅은 아래 두 글을 많이 참고하였습니다.
꼭 한 번씩 읽어봐주셨음 조켄네…
공부한 내용 + 공부하며 드는 생각들 조금 정리한 느낌으로 무난하게 작성해봤어요😁
리액트는 자바스크립트 라이브러리의 하나로, 사용자 인터페이스를 만들기 위해 사용된다.
단골 질문 중 하나로 꼽히는게 라이브러리 VS 프레임워크(ex. React, Next.js)에 대한 질문인 것 같은데, SSR 도입 여부에 대해 의논하기 위해 자료를 찾다가 다양한 비교를 접하며 이 개념을 비로소 온전히 받아들였다.
Next.js 공식문서에서는 주요 기능들에 대해 다음과 같이 정의하고 있다.

즉, Next.js에서 제공하는 기능들이 Routing, Rendering, Data Fetching, Styling, Optimization, Typescript인 것인데 이 부분에서 React를 라이브러리로 분리하는 이유를 느낄 수 있었다.
React는 위 기능들 중에서 render에 해당하는 거의 일부분만을 담당할 뿐, Next.js가 제공하는 기능들 중 다수를 이용하기 위해서는 서드파티 라이브러리를 이용해야 한다.
(ex. 라우팅을 위한 react-router-dom 설치, styling을 위한 styled-components 설치 등)
리액트 애플리케이션이라고 언급하긴 했지만, 사실 리액트 애플리케이션이라는 것은 존재하지 않습니다. 자바스크립트나 타입스크립트로 작성된 프론트엔드 애플리케이션이 뷰를 나타내기 위해 리액트를 사용했을 뿐이죠. 이들을 리액트 애플리케이션이라고 부르는 것은 Java EE 애플리케이션을 JSP 애플리케이션이라고 부르지 않는 것처럼 적절하지 않다고 생각합니다.
따라서 React는 프레임워크와 달리 완전한 생태계를 제공하지 않고 사용자가 직접 제어할 수 있으므로(제어의 흐름이 개발자에게 있음) 라이브러리로 분류될 수 있다고 생각한다.
위에서 React가 “사용자 인터페이스를 만들기 위해 사용되는” 라이브러리라 언급했으나, 막상 코드를 짜다보면 React가 사용자 인터페이스만을 담당하는 코드로 만들기는 상당히 어려워진다(😞).
실제로도 구조화 혹은 컴포넌트 분리 등이 없이 코드를 짜다보면 계산 부분, 상태 관리 부분, View와 관련된 부분이 모두 중구난방으로 섞여 있는데 이는 결국 렌더링 최적화 문제로까지 이어졌다.
이 글에서는 "리액트 애플리케이션"을 리액트를 뷰로만 사용하는 일반 애플리케이션으로 재구성하는 데 사용할 수 있는 몇 가지 패턴과 기법에 대해 설명하고자 합니다(뷰는 큰 노력 없이 다른 뷰 라이브러리로 바꿀 수도 있습니다).
여기서 중요한 점은 애플리케이션 내에서 코드의 각 부분이 어떤 역할을 하는지 분석해야 한다는 것입니다(같은 파일에 담겨 있을 수도 있습니다). 뷰와 뷰와 관련되지 않은 로직을 분리하고, 뷰와 관련되지 않은 로직을 책임에 따라 더욱 세분화하여 적재적소에 배치해야 합니다.
이 글을 참고하게 된 계기도 위의 단락들을 보고 나서였는데, 작금의 나에게 꼭 필요한 생각이었다. 리액트를 뷰를 담당하는 라이브러리로써 사용하지 않고, 프레임워크로써의 동작을 기대하고 코드를 작성하고 있었으며 이 때문에 로직의 분리가 어려웠던 것 같다.
사실 단순히 View로만 동작하는 코드만 작성하고, 아주 적고 기본적인 기능만을 이용한다면 모듈화는 큰 의미가 없다고 생각한다. (사실 오히려 코드 가독성이 떨어질지도…) 하지만 대부분의 서비스가 커지고 있는 것이 현실이기 때문에 서비스가 커지면 커질수록, 기능이 늘면 늘수록 로직의 분리가 요구되는 듯하다.

그래서 위 글에서는 단계별로 구조를 구축하는 방법에 대해 설명하고 있다.

최초의 legacy 코드가 아닐까… 모든 서드파티 컴포넌트 로직과 네트워크 통신 로직이 한 컴포넌트 안에 합쳐져 있는 구조로 작성하는 경우이다. 참고로 이렇게 작성하면 800줄 +@가 나오는 아찔한 경험을 한 적이 있다.

기존의 단일 컴포넌트에서 보다 발전한 형태로, 컴포넌트를 이용해 구조를 나누게 된다. Container / Presentational 두 가지의 구성요소를 가지며 이를 통해 각각의 역할을 더 분명히 나누었다.
가장 예를 들기 쉬운 구조가 List를 작성할 때 Card.tsx를 만드는 경우라고 생각한다.
하지만 이 역시도 문제가 있으니…
애플리케이션이 성장함에 따라 뷰 외에도 네트워크 요청을 보내고, 뷰에서 사용할 수 있도록 데이터를 다른 형태로 변환하고, 데이터를 수집하여 서버로 다시 전송하는 등의 작업이 필요합니다. 이러한 코드가 컴포넌트 내부에 있는 것은 사용자 인터페이스에 관한 것이 아니기 때문에 적절하지 않습니다. 또한 일부 컴포넌트는 너무 많은 내부 상태를 갖게 됩니다.
따라서 상태 관리적 측면에서의 분리가 필요해진다.

기존의 두 가지 요소 이외에 react hook을 추가하여, Network request와 Domain logic을 이곳으로 분리하게 되었다. 상태와 사이드이팩트 분리라고 정의하기도 하는 것 같다. (사실 커스텀 훅 쓰는 이유긴 하니까…)
3단계도 사실 완전히 SRP를 따르고 있다고는 말할 수 없는 코드이다. hook이 상태 관리만 다루는 게 아니라 단순한 계산과 network request 등 다른 로직도 수행하고 있기 떄문에… 이런 부분들을 추출해서 만든 형태가 아래와 같다고 볼 수 있겠다.

Domain object와 Infrastructure 요소가 추가되었고, hook은 이제 단순히 state 관리만을 책임질 수 있게 되었다.
Domain Object?
이러한 간단한 객체들은 데이터 매핑(한 형식에서 다른 형식으로)을 처리하고, 필요에 따라 null을 확인하며 폴백 값을 사용할 수 있습니다. 또한 이러한 도메인 객체가 증가함에 따라 더 깔끔하게 만들기 위해 상속 또는 다형성이 필요하다는 것을 알게 됩니다. 따라서 다른 곳에서 유용하다고 생각했던 많은 디자인 패턴을 여기 프런트엔드 애플리케이션에 적용했습니다.
패턴을 파악하고, 객체들을 보다 계층화시키는 단계이다. (아무래도 SRP에 더 가까워지는 방향이겠지… )
어떠한 사용자 인터페이스에도 속하지 않고, 기본 데이터가 원격 서비스에서 온 것인지, 로컬 스토리지에서 온 것인지, 캐시에서 온 것인지에 대해서도 신경 쓰지 않는 객체들이 많다는 것입니다. 이에 이들을 다른 계층으로 분할하려합니다.

즉 위의 객체지향적 관점에서의 포인트는 class 이용인 것 같다. 기본적으로는 함수형 컴포넌트를 이용하지만, Domain Object를 View에서 분리하는 경우 아래 예시 코드를 보면 class를 통해 함수를 작성하고 있다.
[번역]잘 알려진 UI 패턴을 사용하여 리액트 애플리케이션 모듈화하기 - 로직을 캡슐화하는 데이터 모델링
함수형 컴포넌트를 사용하다보니, 강박적으로 모든 코드를 함수로 짜고 있었던 것 같은데 이런 고정관념을 좀 깰 수 있었달까… 확실히 class에서 강점을 가지는 부분들을 적용하면 좋은 것 같다.
또한 전반적인 흐름 자체가 지금까지 해오던 흐름과 크게 다르지 않아서 이해가 쉬웠다. 지금 딱 4단계 정도에 머물러 있는 느낌이랄까...

이 아키텍처의 경우, 레이어에 따라 분리한다. (FSD와 비슷한 느낌으로 생각하면 쉬울 듯!)
프레젠테이션
이 레이어는 기본적으로 UI 구성 요소로 구성됩니다. Vue의 경우 Vue SFcs입니다. React의 경우 React 구성 요소입니다. Svelte의 경우 Svelte SFC입니다. 등등. 프리젠테이션 계층은 애플리케이션 계층에 직접 연결됩니다.
애플리케이션
이 계층에는 애플리케이션 로직이 포함됩니다. 도메인 계층과 인프라 계층을 알고 있습니다. 이 아키텍처에서 이 계층은 React의 React Hooks 또는 Vue 3의 Vue "Hooks"를 통해 구현됩니다.
도메인
이 계층은 도메인/비즈니스 로직을 위한 것입니다. 비즈니스 로직만 도메인 계층에 있으므로 여기에는 프레임워크/라이브러리가 전혀 없는 순수한 JavaScript/TypeScript 코드만 있습니다.
인프라
이 계층은 외부 세계와의 통신(요청 전송/응답 수신) 및 로컬 데이터 저장을 담당합니다. 다음은 이 계층에 대한 실제 응용 프로그램에서 사용할 라이브러리의 예입니다.HTTP 요청/응답: Axios, Fetch API, Apollo Client 등Store(상태 관리): Vuex, Redux, MobX, Valtio 등
위의 객체지향 관점의 아키텍처 구조와 비슷하나, layer 구조를 별도로 가져가고 각각에 따라 구성했다는 점에서 보다 폴더구조를 작성하는 건 쉬워보인다. 또한 이 하나의 구조는 또 모듈화 시킬 수 있다는 점이 강점인 것 같다.

사실상 아키텍처 구조는 FSD를 따라 작성하고 있기에, 가장 주목한 부분은 모듈화를 하며 비즈니스 로직을 어떻게 분리하는가와 관련된 부분이었다. 이번 글을 작성하면서 느낀 점을 꼽자면,
정도가 있었다. 특히 단순 계산 로직 작성 시에, class를 도입하는 것도 충분히 고려해볼만한 사항인 것 같아 차차 적용해보려 한다. 역시 한 번 예제를 보는 것과 안 보는 건 차이가 크구나(ㅠㅠ)