본문 바로가기

Frontend

[번역] Widget Driven Development

 

Widget Driven Development | Alexey Antipov

Decomposition of UI applications into self-contained widgets leveraging streamlined Data Management

alexei.me

 

우리가 애플리케이션을 개발할 때 컴포넌트를 조합합니다. 각 UI 컴포넌트는 기본적으로 마크업, 범위가 지정된 스타일, 일부 UI 로직의 조합입니다. 데이터 관리는 빈번하게 컴포넌트 제어에서 제외되어 복잡한 데이터 흐름이 있는 복잡한 아키텍처를 초래합니다. 

 

이 글에서는 컴포넌트를 데이터 로직과 UI를 모두 완벽하게 제어할 수 있는 자율적으로 분리된 위젯으로 변환하는 방법을 보여드리겠습니다. 

The History of Components

제 생각에 위젯은 컴포넌트의 자연스러운 후속제품입니다. 이를 확인하기 위해 시간을 거슬러 올라가 UI 구축에 대한 접근 방식이 시간이 지남에 따라 어떻게 발전해 왔는지 살펴볼 것을 제안합니다.

 

많은 사람들이 모든 애플리케이션 스타일이 하나의 글로벌 CSS 파일에 정의되어 있던 시절을 기억합니다. 스타일 정의는 다양한 CSS 선택자의 복잡한 조합을 사용했습니다. 스타일 충돌은 앱에서 흔히 발생했습니다. 이러한 스타일의 복잡성은 애플리케이션의 성능에도 영향을 미쳤습니다.

 

2009년에 BEM이 탄생했습니다. BEM은 스타일을 정의하고 클래스를 명명하기 위한 일련의 지침을 제공했습니다. 이러한 규칙은 스타일 충돌과 비효율적인 선택자 문제를 해결하기 위한 것이었습니다. BEM은 UI를 Blocks, Elements, Modifiers 관점에서 생각하도록 권장했습니다.

 

2013~2015년은 컴포넌트 접근법이 부상한 시기입니다. 리액트는 UI를 마크업(HTML)과 UI로직(JS)의 조합인 컴포넌트로 간단하게 나눌 수 있게 했습니다. 이는 애플리케이션 개발의 판도를 바꾸었습니다. 곧 다른 프레임워크도 컴포넌트 기반 접근 방식을 채택하며 그 뒤를 따랐습니다. 

 

빌드 도구, CSS 전처리기, CSS-in-JS 및 CSS 모듈과 같은 기술이 등장하면서 스타일링을 컴포넌트의 일부로 만드는 것이 가능해졌습니다. 

 

스토리북은 개발자가 고립된 환경에서 컴포넌트를 빌드하고 적절한 스타일 범위를 지정할 수 있도록 돕기 위해서 등장했습니다. 컴포넌트 프로퍼티 값이 컴포넌트의 모양과 동작을 정의하는 등 개발자가 UI를 상태의 함수로 생각하도록 장려했습니다.

 

재사용 가능한 고품질 컴포넌트 컬렉션이 등장했습니다.

 

The unresolved hurdles

컴포넌트 중심의 접근 방식은 UI를 재사용 가능한 분리된 조각으로 나누고 사전 빌드된 컴포넌트 모음을 사용하여 대규모 애플리케이션을 구축하는데 도움이 되었습니다. 하지만 한가지 아쉬운 점은 UI 컴포넌트에 데이터를 공급하는 방법이 없었습니다. 

 

데이터 관리는 프론트엔드 엔지니어링에서 가장 어려운 작업 중 하나이자 UI 앱을 복잡하게 만드는 주요 원인이었습니다. 그래서 컴포넌트를 두가지 유형으로 나누는 방법을 배웠습니다.

 

- Presentational Components: UI 표현을 담당하며 일반적으로 상태가 없고 부작용이 없습니다. 컨테이너 컴포넌트는 데이터 관련 로직을 처리하고 프레젠테이션 컴포넌트에 데이터를 전달합니다. 

- Container Components: 컨테이너 컴포넌트는 데이터 관련 로직을 처리하고 데이터를 프레젠테이션 컴포넌트로 전달합니다. 

 

이제 남은 것은 컨테이너 컴포넌트가 데이터와 함께 작동하는 방식을 정의하는 것입니다.

 

The Naive approach

일반적인 접근 방식은 각 컨테이너 컴포넌트가 기본 프레젠테이션 컴포넌트가 필요로하는 데이터를 단순히 가져오는 것입니다.

 

일반적으로 여러 다른 컴포넌트에서 동일한 데이터가 필요하기 때문에 실제로 이러한 접근 방식을 구현하면 여러 가지 문제가 발생할 수 있습니다. 

 

- 중복된 요청과 데이터 과다 가져오기. 결과적으로 UI가 느려지고 서버에 과부하가 걸립니다.

- 동일한 엔드포인트에 대한 요청이 서로 다른 데이터를 가져올 때 컴포넌트 간에 데이터 불일치 가능성

- 복잡한 데이터 무효화(백엔드에서 데이터가 변경되어 모든 종속 컴포넌트가 데이터를 다시 가져와야 하는 경우)

 

The Common parent approach

데이터 가져오기(및 변경) 기능을 모든 기본 컴포넌트에 데이터를 전달하는 공통 상위 컴포넌트로 이동하여 문제를 극복하는 방법을 배웠습니다.

 

요청 중복 및 데이터 무효화 문제를 해결했습니다. 하지만 새로운 도전에 직면했습니다.

 

- 전체 애플리케이션 로직이 더 복잡해지고 더 많이 결합되었습니다.

- 여러 컴포넌트를 통해 데이터를 전달해야 했습니다. 이 문제는 악명이 높아져 prop drilling이라는 이름이 붙었습니다.

 

The State Management approach

prop drillling 문제를 우회하기 위해 상태 관리 라이브러리를 사용하는 방법을 배웠습니다. 데이터를 기본 컴포넌트로 전파하는 대신 트리 아래의 모든 컴포넌트가 액세스 할 수 있는 일부 스토어에 데이터를 배치하여 거기에서 직접 데이터를 가져올 수 있도록 했습니다. 컴포넌트는 스토어에서 변경 사항을 구독하여 데이터를 항상 최신 상태로 유지합니다. 

 

 

prop drilling 문제는 해결되었지만 꽁짜는 아닙니다.

 

- 이제 스토어라는 새로운 개념을 다루어야 하며 스토어 구조 설계 및 유지 관리, 스토어 내 데이터의 적절한 업데이트, 데이터 정규화, 변경 가능한 것과 변경 불가능한 것, 단일 스토어와 다중 스토어 여러 가지 새로운 것에 신경써야 합니다.

- 상태 관리 라이브러리에서는 새로운 어휘를 배워야합니다. 액션, 액션 크리에이터, 리듀서, 미들웨어, thunks등입니다.

- 도입된 복잡성과 명확성 부족으로 인해 개발자는 스토어 작성 방법, 해야 할 일과 피해야할 일에 대한 스타일 가이드를 만들어야 했습니다. 

- 그 결과 애플리케이션은 매우 복잡하게 얽히고 설키게 되었습니다. 불만을 품은 개발자들은 다른 구문을 사용하는 새로운 상태 관리 라이브러리를 개발하여 문제를 완화하려고 노력했습니다. 

 

The Naive approach reimagined

더 잘할 수 있을까요? 데이터 관리에 더 쉽게 접근할 수 있는 방법은 없을까요? 데이터 흐름을 투명하고 이해하기 쉽게 만들 수 있을까요? 앱의 얽힘을 풀고 직교성을 높일 수 있을까요? 마크업, 스타일, UI 로직과 같은 방식으로 데이터 로직을 컴포넌트의 제어 하에 둘 수 있을까요?

 

우리는 너무 깊이 들어가서 나무만 보고 숲을 보지 못하는 것 같습니다. 다시 출발점인 Naive 접근법으로 돌아와서 문제를 다른 방식으로 해결할 수 있는지 봅시다.

 

가장 아쉬운 점은 요청 중복과 데이터 불일치였습니다. 

컴포넌트와 백엔드 사이에 API 래퍼나 인터셉터와 같이 중간 플레이어가 있어서 내부적으로 이 모든 문제를 해결할 수 있다면 어떨까요? 

 

- 모든 중복된 요청 제거

- 데이터 일관성 보장: 모든 컴포넌트는 동일한 요청을 사용할 때 항상 동일한 데이터를 가져야 합니다. 

- 데이터 무효화 기능 제공: 컴포넌트가 서버에서 데이터를 변경하면 해당 데이터에 의존하는 다른 컴포넌트가 새 데이터를 수신해야 합니다. 

- 컴포넌트에 투명해야 하며 어떤 식으로든 로직에 영향을 주지 않아야 합니다.(컴포넌트가 백엔드와 직접 통신한다고 생각하게 해야 합니다.)

 

좋은 소식은 우리가 이미 가지고 있고 라이브러리가 있다는 것입니다.

일부 GraphQL 클라이언트, 예: Relay, React-Query, SWR, Redux Toolkit Query, Vue Query for RESTful API.

 

이러한 접근 방식의 가장 큰 장점은 애플리케이션의 데이터 로직을 풀고, 데이터 로직을 컴포넌트의 제어하에 두고, 모든 조각을 결합하여 더 나은 직교성을 달성할 수 있다는 것입니다.

 

저희 팀에서는 위에서 설명한 Naive한 접근 방식을 React Query와 함께 사용하기 시작했고 이 접근 방식이 마음에 들었습니다. 이를 통해 애플리케이션 구축에 다른 방식으로 접근할 수 있었습니다. 저는 이를 "위젯 중심 개발"이라고 부르고 싶습니다.

 

모든 페이지를 자율적으로 동작하고 독립적인 소위 위젯으로 분할하는 것이 핵심입니다.

 

모든 위젯은 다음에 대한 책임을 갖습니다.

 

- fethcing and providing all the required data to its UI

- mutating the related data on server if needed

- data representation in the UI

- UI for Loading State

- (optional) UI for Error State

 

코드 구성에 대해 말하자면, 모든 위젯 관련 파일을 한 곳에 배치합니다.

일반적으로 동일한 API 엔드포인트가 여러 위젯에 걸쳐 사용됩니다. 그래서 우리는 모든 위젯을 별도의 공유 폴더에 보관하기로 결정했습니다. 

 

이러한 쿼리 구성은 RESTful API와 React Query를 사용하기 때문에 우리에게 잘 맞습니다. GraphQL의 경우 의미가 없을 수 있습니다. 

 

우리는 리액트 쿼리 라이브러리를 사용하며 quereis/ 의 각 파일은 리액트 쿼리로 래핑된 fetch 및 mutation 메서드를 노출합니다. 모든 컨테이너 컴포넌트는 비슷한 구조를 갖습니다. 

 

import { useParams } from 'react-router-dom';
import { useBookQuery } from 'queries/useBookQuery';
import { useAuthorQuery } from 'queries/useAuthorQuery';
import Presentation from './Presentation';
import Loading from './Loading';
import Error from './Error';

export default BookDetailsContainer() {
  const { bookId } = useParams();
  const { data: book, isError: isBookError } = useBookQuery(bookId);
  const { data: author, isError: isAuthorError } = useAuthorQuery(book?.author);

  if (book && author) {
    return <Presentation book={book} author={author} />
  }

  if (isBookError || isAuthorError) {
    return <Error />
  }

  return <Loading />
}

 

선언적으로 종속된 쿼리가 얼마나 쉽고 간편하게 처리되는지 주목하세요. 또한 위젯의 유일한 종속성은 URL에 책 아이디가 있다는 것 뿐입니다.

 

대부분의 위젯 컨테이너 컴포넌트에는 프로퍼티가 없으며 URL 데이터를 제외한 외부 상태에 의존하지 않습니다. 이러한 접근 방식은 위젯이 어떤 API 쿼리에 의존하는지 투명하게 보여줍니다. 이러한 투명성과 거의 제로에 가까운 외부 종속성이 결합되어 위젯을 쉽게 테스트할 수 있고 코드에 대한 자신감을 얻을 수 있습니다. 

 

일반적으로 위젯에 대한 변경은 해당 위젯이 폴더 아래에 있는 파일에 대한 수정으로 제한됩니다. 이렇게 하면 애플리케이션의 다른 부분이 손상될 위험이 크게 줄어듭니다.

 

새 위젯을 추가하는 방법도 매우 간단합니다. 위젯에 필요한 모든 파일이 들어 있는 새 폴더를 만들고, 필요한 경우 /queries 폴더에 새 쿼리를 만들면 됩니다. 다시 말하지만, 애플리케이션의 다른 부분을 손상시킬 위험은 매우 제한적입니다. 

 

또한 컨텍스트에 대한 종속성이 제한되어 있기 때문에 모든 위젯을 다른 페이지에서 쉽게 재사용할 수 있습니다. 일반적으로 해당 페이지의 URL에 위젯에 필요한 데이터 식별자가 포함되어 있는지 확인하기만 하면 됩니다. 

 

Conclusion

컴포넌트 접근 방식을 사용하면 재사용 가능한 독립적인 UI 조각을 쉽고 간단하게 만들 수 있습니다. 하지만 모든 문제를 해결하지 못했고 프론트엔드 애플리케이션은 종종 복잡한 데이터 관리로 어려움을 겪었습니다.

 

데이터 관리에 다른 방식으로 접근하고 애플리케이션의 복잡성을 크게 줄일 수 있는 라이브러리가 있습니다. 이러한 라이브러리를 활용하여 데이터 로직을 컴포넌트의 제어하에 두고 애플리케이션을 재사용 가능한 독립형 위젯 집합으로 변환할 수 있습니다. 이를 통해 데이터 흐름이 투명해지고 아키텍처가 유연해지며 코드가 탄력적이고 테스트하기 쉬워집니다.