본문 바로가기

FrontEnd

아폴로 클라이언트로 알아보는 클라이언트 아키텍처 [Apollo Client & Client-side Architecture Basics]

반응형

https://www.apollographql.com/blog/apollo-client/architecture/client-side-architecture-basics/

 

Apollo Client & Client-side Architecture Basics

Architecture is the foundation, the skeleton, that sets your project up for success. It’s the stuff that we wish we got right from the start because making drastic architectural changes, later on, can be challenging and time-consuming. Today, we React de

www.apollographql.com

위 글의 번역이다.

아폴로 클라이언트 - 클라이언트 아키텍처 베이직

아키텍처는 프로젝트를 성공으로 이끄는 토대이자 뼈대입니다.
나중에 아키텍처를 크게 변경하는 것은 어렵고 시간이 많이 소요되기에 처음부터 제대로 설계해야 합니다.
React 개발자는 Context, Hooks, Redux 및 xState와 같은 도구를 가지고 있습니다.
우리는 4~5가지 다른 종류의 상태를 처리하는 코드를 작성하고
인증, 상태 관리, 네트워킹, 애플리케이션 로직 및 subscription(실시간 업데이트)과 같은 것을 처리하는 데 시간을 자주 소비합니다.
"[개발자의] 전문적인 평판과 생계에 대한 위험을 최소화하면서 일관되고 일관성 있게 React 앱을 빌드하기 위해 널리 인정되는 표준"이 많지 않기 때문에 종종 스스로 결정을 내려야 합니다.(Joel Hooks , 2020년 6월 17일)
 
이 기사에서는 몇 가지 클라이언트 측 아키텍처 기본 사항에 대해 설명합니다.
우리는 모던 아키텍처에에 영향을 미치는 원칙, 해결해야 할 문제, 처리해야 하는 상태 유형 및 Apollo Client가 수행하는 역할에 대해 논의할 것입니다.
면책 조항: 애플리케이션을 디자인하고, 상태를 다루고, 관심사를 분리하는 다른 방법이 있지만
이것이 우리(team-apollo)가 좋아하는 개념적 모델입니다.
당신에게 효과가 없더라도, 당신의 팀과 당신의 프로젝트에 의미가 있는 것으로 대화를 안내하는 데 도움이 되기를 바랍니다.

Client-side architecture basics

Structure vs. Developer experience

이상적인 디자인은 문제를 해결하지만 개발자 경험을 희생시키지 않는 것입니다.
즉, 작동하는 디자인을 고려할 때 논리(logic)와 감정(emphaty)을 모두 사용해야 합니다.

논리적으로 우리는 당면한 문제를 해결할 수 있는 도구를 선택합니다. 어떤 도구가 효과적일까요?
감정적으로 쓰기에 편하고, 사용하면 기분좋은 도구를 선택합니다.
도구가 일을 잘 수행할 수 있을 뿐만 아니라 도구에 대한 통제감, 숙달감, 만족감, 자부심을 줄 수 있다면(Don Norman의 'Design of Everyday Things'에서) 우리가 잘한 일이라고 생각합니다.
예를 들어 피아노, 검술같은게 있겠네요.
가파른 학습 곡선, 강제적인 규칙 또는 겉보기에 불필요한 복잡성과 구조를 가진 도구를 강제로 사용하는 것은 기분이 좋지 않습니다.
많은 경우 꽤 부정적인 감정을 유발할 수 있습니다.
이것이 일부 개발자가 최소 학습 곡선, 슬림 API, 자유도가 있는 React.js를 선호하는 이유일 수 있습니다.
Angular와 비교하여 학습 곡선은 더 가파르지만 앱 구성 방법에 대한 지침은 훨씬 더 많습니다.
두 도구 모두 훌륭합니다. 그리고 둘 다 동일한 작업을 수행할 수 있습니다. 일부는 React가 더 나은 개발자 경험을 제공하고
Angular가 더 많은 Structure를 제공한다고 말합니다. 이 두 가지 속성, 즉 Structure와 개발자 경험이 필수적입니다.
React와 Angular의 예시를 보니 둘은 충돌하는것 같네요?
소프트웨어 디자인 = 구조 대 개발자 경험
임의의 규칙을 강제하는 대신, 현재 React 앱의 클라이언트 측 아키텍처에 대해 알고 있는 것으로 시작하여 쾌적하지만 구조화된 출발점에서 시작할수 있는지 봅시다.

Model-View-Presenter

Model-View-Presenter(https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter)에 대해 들어보셨나요?
사용자 인터페이스를 구축하는 방법을 설명하는 데 사용되는 기존의 아키텍처 패턴 중 하나입니다.
MVC(Model-View-Controller)가 서버에서 클라이언트를 분리하는 방법을 설명했다면
MVP는 클라이언트 부분의 구조를 분해했습니다.

Model-View-Presenter는 일반적으로 클라이언트 애플리케이션 내에서 사용되는 아키텍처 패턴입니다. MVC 패턴의 파생물입니다.

MVP 패턴에서...

  • view는 사용자 이벤트를 생성합니다.
  • 이러한 사용자 이벤트는 모델의 update 또는 change로 전환됩니다.
  • model이 변경되면 새 데이터로 view를 업데이트합니다.

네, 옵저버 패턴입니다.

자세히 들여다보면 대부분의 클라이언트 측 아키텍처가 관찰자 패턴의 구현이라는 것을 알 수 있습니다.

The need for a better client-side architecture standard

솔직히 말해서, MVP는 훌륭하지만 오늘날 프로젝트의 요구 사항에는 너무 일반적입니다.
MVC와 MVP 모두에서 View와 Controller/Presenter가 React 앱에서 무엇을 참조하는지 명확합니다.
View는 프레젠테이션 컴포넌트
P/C는 컨테이너 컴포넌트입니다.
 
하지만 모델은 뭘까요?

가장 큰 문제는 M이 너무 많은 책임을 진다는 것입니다.

결과적으로 적절한 작업에 적절한 도구를 일치시키는 것이 때때로 어려울 수 있습니다.

Tasks of the model

React 앱의 모델이 해결해야 하는 주요 문제 중 일부는 다음과 같습니다.
  • 네트워킹 및 데이터 가져오기 - 전체 data fetching 레이어를 구축해야 하나요? 비동기 상태를 수동으로 셋업하나요?(isLoading)
  • 모델의 동작 — mutation 전에 실행해야 하는 의사 결정 논리가 있으면 어떻게 되나요? 어디로 가나요? 게임 로직이 많은 체스 게임을 만들고 있다면 어떨까요? 코드의 구조를 어떻게 구성합니까? 특정 routes와 actions에 대해 인증을 처리하려면 어떻게 해야 합니까? 유효성 검사 논리를 어떻게 처리합니까?
  • 상태 관리(모델 데이터) - 단일 컴포넌트에 속한 상태를 어떻게 처리하나요? 둘 이상의 구성 요소에 속한 상태는 어떻습니까? 상태 관리 라이브러리에 무엇을 사용합니까? 컨텍스트를 사용할 수 있습니까? 데이터가 변경될 때 보기가 다시 렌더링되도록 반응성을 설정하려면 어떻게 해야 하나요?
  • … 등

그리고 2020년에는 모호한 모델 문제에 대한 솔루션을 찾기 위해 개발자 도구 상자를 열 때 이것이 보입니다.

  • React hooks
  • Redux
  • Context API
  • Apollo Client
  • xState
  • react-query
클린 아키텍처를 통해 백엔드 프로그래밍에서 모호한 모델 문제를 해결하는 데 사용된 것과 동일한 영향력 있는 디자인 원칙을 적용함으로써
클라이언트 측에서도 합리적인 원칙에 도달합니다.

Principle #1 – CQS (Command Query Separation)

상태를 변경하지 않는 메소드와 상태를 변경하는 메소드 분리
명령 쿼리 분리는 작업이 명령 또는 쿼리임을 나타내는 설계 원칙입니다.
  • commands는 상태를 변경하지만 데이터를 반환하지 않으며,
  • query는 데이터를 반환하지만 상태를 변경하지는 않습니다.
CQRS

이 패턴의 주요 이점은 코드를 더 쉽게 추론할 수 있다는 것입니다.

궁극적으로 두 가지 코드 경로를 만들 것을 촉구합니다. 하나는 읽기용이고 다른 하나는 쓰기용입니다.

이 원칙을 여러 설계 수준에 적용할 수 있지만 실제로 적용되는 가장 간단한 방법은 메서드 수준입니다.

Commands

createUser 및 toggleTodo 메소드를 고려하십시오. 둘 다 command 작업입니다.
function createUser (props: UserDetails): Promise<void> { ... }
function toggleTodo (todoId: number): void { ... }
이 두 메서드 모두 아무 것도 반환하지 않습니다. 둘 다 void를 리턴합니다.
이는 다음 메서드가 유효한 커맨드가 아님을 의미합니다.
function createUser (): Promise<User> { ... }
function toggleTodo (): Todo { ... }
이것은 유용한 패턴이지만 예외가 있습니다.
두 가지 좋은 예는
스택 맨 위에서 항목을 팝하거나
돌연변이 응답 내에서 변경된 데이터 또는 최소한 변경된 데이터의 ID를 반환하는 것입니다
(이 필요성에 대해서는 "캐시 정규화 설명하기"에서 논의할 것입니다).
일반적으로 우리는 이 접근 방식으로 시작한 다음 적절하다고 생각되면 해당 방식을 파괴합니다.
 

Queries

쿼리는 데이터를 반환하고 부작용을 수행하지 않는 작업입니다. 예를 들면 다음과 같습니다.
function getCurrentUser (): Promise<User> { ... }
function getUserById (userId: UserId): Promise<User> { ... }

Principle #2 – Separation of Concerns

앱의 아키텍처 관심사 사이에 논리적 경계를 의식적으로 적용
할 일 목록이 있다고 가정합니다.
사용자가 할 일에서 삭제를 클릭하면 무슨 일이 일어나나요?

view는 이벤트를 컨테이너(container / presenter)로 전달합니다.
그러면 사용자 이벤트를 React Hook 또는 Redux 썽크의 메서드에 연결할 수 있습니다.
거기에서 일부 로직을 실행하고, 네트워크 요청을 호출해야 하는지 결정하고, 로컬에 저장된 상태를 업데이트하고, 업데이트해야 함을 UI에 알리고 싶을 수 있습니다.
단순한 앱인데도 할 일이 많네요.
그리고 조금 전에 로직을 실행하고 싶다고 말했을 때 정확히 어떤 종류의 로직이 될 수 있는지 명확하지 않았습니다.
권한 부여 논리, 유효성 검사 논리, 상호 작용/도메인 논리 등이 될 수 있습니다. 이 모든 것은 해결해야 할 유효한 문제입니다.
 
관심사의 분리는 내가 가장 좋아하는 디자인 원칙 중 하나입니다.
앱에서 수행할 작업에 대해 생각하고 특정 계층에 위임하고 해당 계층이 자신의 작업만 수행하도록 하는 것입니다.

관심사의 분리 - 각 레이어가 분리된 관심사를 다룸

관심사의 분리가 반드시 물리적으로 코드를 다른 폴더와 파일로 분리하는 것을 의미하지는 않습니다.
코드가 처리하고 있는 문제를 파악하고 올바른 폴더 구조를 통해 이를 반영하는지 확인하는 것이 더 중요합니다.
물리적 분리가 아닌 논리적 분리입니다.
그러나 자주 함께 변경되고 가능한 한 서로 가까운 코드와 파일을 찾는 것이 좋습니다.

이것이 왜 중요할까요?

  • 분해. 관심사를 분리하면 수행해야 하는 작업, 해당 작업이 속한 계층 및 이러한 문제를 해결할 수 있는 도구에 대한 가시성을 높일 수 있습니다.
  • 기능 구조화. 앱의 모든 기능을 수직 슬라이스 세트로 생각할 수 있습니다. 즉 앱은 "특정 기능을 작동시키는 데 관련된 모든 계층에서 수행해야 하는 작업의 합계"입니다. 구성 요소, 상태 및 동작의 수직 슬라이스는 앱의 기능입니다.
  • 위임. 이 레이어를 직접 만들어야 합니까? 아니면 라이브러리나 프레임워크에 작업을 위임해야 합니다. 예를 들어, 대부분의 개발자는 프리젠테이션 구성 요소를 위한 사용자 정의 뷰 레이어 라이브러리를 빌드하지 않습니다. React 또는 Vue.js를 사용합니다. 하지만 많은 사용자가 맞춤 상태 관리 시스템을 처음부터 구축합니다.

A better client-side architecture starting point

CQS와 SoC를 사용하여 우리는 MVP 패턴이 어떻게 현대적인 React 앱으로 나타나는지 보다 명확하게 이해하게 되었습니다.

요약하면 관심사는다음과 같습니다.

  • Presentation components — UI를 렌더링하고 사용자 이벤트를 만듭니다.
  • UI logic — view의 동작과 로컬 컴포넌트 상태를 포함합니다.
  • Interaction layer — 모델 동작 & 공유 상태. 하나의 앱에 다양한 상태 모델이 존재할 수 있습니다. (모달 등). 리액트 훅이나 xState 사용.
  • Container/controller —접착제 층. 사용자 이벤트를 올바른 상호 작용/응용 프로그램 계층 구조에 연결하고 반응 데이터를 전역 저장소에서 프레젠테이션 구성 요소로 전달합니다. 이를 페이지 컴포넌트로 생각할 수도 있습니다.

 

  • 🌟 Networking & data fetching — Performs API calls, fetches data, and signals the state of network requests (meta state).
  • 🌟 State management & storage — Shared global storage, provides APIs to update data, and configure reactivity.

 

더 깊이 들어가려면 최신 클라이언트 측 아키텍처의 각 문제에 대한 예와 주장을 확인하고
클라이언트 측 아키텍처 기본 에세이 전체를 읽어보세요.
- 내생각에는 꼭 읽어볼 필요는 없음 -
https://khalilstemmler.com/articles/client-side-architecture/introduction/

The different types of state in client-side apps

아키텍처를 제외하고 상태는 클라이언트 측 웹 앱을 디자인할 때 혼란스러운 또 다른 부분입니다.
Jed Watson은 GraphQL Summit 2019에서 "A Treatise on State"라는 제목의 강연에서
로컬(구성 요소), 공유 로컬(글로벌), 원격(글로벌), 메타,  라우터 총 5가지 다른 유형의 상태를 설명합니다.
 
  • local (component):
    • 단일 컴포넌트에 속하는 상태입니다.React hook으로 UI 상태를 관리합니다. ex) useState, useRef
  • shared local (global):
    • 둘 이상의 컴포넌트에 속하는 상태입니다. ex) recoil, context API
  • remote (global):
    • 백엔드 서비스와 관련한 상태입니다. ex) react-query, useEffect
  • meta:
    • 상태에 대한 상태를 나타냅니다. 좋은 예는 네트워크 요청의 진행 상황을 알려주는 비동기 상태 loading 입니다. ex) react-query, suspense
  • router state:
    • 브라우저의 현재 URL, history 관련 정보입니다. ex) react-router

States and concerns of Apollo Client

다양한 상태 유형과 해결해야 할 문제를 이해했으므로 이제 Apollo Client가 귀하의 해결을 돕기 위해 긴밀히 관여하고 있는 몇 가지 문제에 대해 논의할 준비가 되었습니다.

Concern — Presentation components

유아이를 그리고 사용자 이벤트를 생성함

프레젠테이션 구성요소는 Model-View-Presenter의 View에 해당합니다.
  • UI에 데이터를 표시하고
  • 사용자 이벤트 생성(키 누르기, 버튼 클릭, 호버 상태 등)

 

프레젠테이션 컴포넌트는 구현 세부 사항입니다.

구현 세부정보는 주요 목표가 아닌 목표를 달성하는 데 도움이 되는 하위 수준의 관심사입니다.
주요 목표 중 하나가 할 일 추가 기능을 연결하는 것이라면
UI의 버튼, 스타일 및 텍스트는 해당 기능을 구현하는 세부 사항입니다.

프레젠테이션 구성 요소는 휘발성(volatile)일 수 있습니다.

자주 변경되는 모든 것을 휘발성(volatile)이라고 합니다.
구성 요소의 모양과 느낌을 지속적으로 변경하는 것이 구성 요소를 그렇게 만드는 이유입니다.
이 현상을 수용하는 한 가지 방법은 안정적인 재사용 가능한 구성요소 세트(구성요소 라이브러리에서 작성하거나 가져온 것)를 결정한 다음, 이를 바탕으로 view를 만드는 것입니다. 재사용 가능한 구성 요소를 사용할 수 있지만 데이터 요구 사항은 자주 변경됩니다.
GraphQL 쿼리를 사용하여 카드 설명을 표시하는 이 간단한 CardDescription 구성 요소를 살펴보겠습니다.
const CARD_DESCRIPTION_QUERY = gql`
  query CardDescription($cardId: ID!) {
    card(id: $cardId) {
      description
    }
  }
`;

const CardDescription = ({ cardId }) => {
  const { data, loading } = useQuery({ 
		query: CARD_DESCRIPTION_QUERY, 
    variables: { cardId }
  });

  if (loading) {
    return null;
  }

  return <span>{data.card.description}</span>
}
스타일을 변경해야 할 가능성은 얼마나 됩니까? 옆에 lastChanged 날짜와 같은 것을 표시하는 것은 어떻습니까?
가능성이 꽤 높습니다.
프레젠테이션 컴포넌트에 GraphQL 쿼리를 포함해야 하나요? 네!
동일한 원인에 의해 변하는 것은 가능한 한 가깝게 둡니다.
GraphQL 쿼리는 가능한 한 프레젠테이션 컴포넌트에 가깝게 하는 것이 좋습니다.
쿼리는 데이터 요구 사항을 정의합니다. 그리고 요구 사항이 변경되면 함께 변경해야 할 가능성이 높기 때문에 파일을 서로 가깝게 두면
파일 간에 앞뒤로 이동하로써 발생하는 불필요한 인지 부하를 줄일 수 있습니다.
 
구성 요소에 쿼리를 넣는 것의 한 가지 잠재적인 단점은 이제 GraphQL에서 전환하려는 경우 구성 요소가 순수하지 않고
GraphQL에 연결된다는 것입니다. 전송 계층(transport-layer) 기술을 전환하려면 모든 구성 요소를 리팩토링해야 합니다.
또 다른 잠재적인 단점은 이러한 구성요소를 테스트하려면 mocked Apollo Client provider에 감싸져 있는지 확인해야 한다는 것입니다. 저는 그것을 장점으로 본다. mockedProvider API는 매우 강력한 테스트 도구입니다.

내 권장 사항은 쿼리를 컴포넌트에 연결하는 것입니다.
놀라운 개발자 경험을 얻기 위해 GraphQL을 완벽하게 사용하고 나중에 변경하기로 결정하는 위험을 감수할 가치가 있다는 것입니다.

(주 : 개인적으로 mocking을 안좋아해서 별로~)

쿼리 성능에 대한 참고 사항: 위에 표시된 것처럼 특정 데이터 청크에 대한 쿼리가 많아도 괜찮습니다. Apollo 클라이언트를 사용하여 Apollo는 데이터가 이미 캐시되었는지 여부를 확인하는 복잡한 논리를 처리합니다. 그렇지 않은 경우에는 가져오기 요청을 내보냅니다.
 

Queries subscribe to state changes

일반적으로 상태가 변경되어 props를 프레젠테이션 구성 요소로 전달할 수 있도록 알림을 받아야 하는 것은 컨트롤러/컨테이너 구성 요소입니다. 최소한 Redux를 Connect와 함께 사용했을 때 이것이 작동한 방식입니다.

그러나 Apollo 클라이언트 쿼리는 데이터가 변경될 때 자동으로 알림을 받기 때문에 컨테이너 구성요소를 통해 props를 제공할 필요가 없습니다.
 

그렇다면 컨테이너 구성요소는 무엇을 위해 필요할까요? 두가지.

  • 여러 경로가 있는 앱에서 최상위 페이지 구성요소 역할을 합니다. 페이지 컴포넌트는 해당 페이지의 모든 프레젠테이션 구성요소와 기능을 로드합니다.
  • 의사결정 로직을 수행하기 위해 프레젠테이션 구성요소에서 모델(상호작용/애플리케이션) 레이어로 명령을 전달합니다. 때때로, 그 의사 결정 논리는 우리가 돌연변이를 호출해야 한다고 결정할 수 있습니다.

프레젠테이션 구성 요소에 GraphQL mutation를 포함해야 합니까?

프레젠테이션 구성 요소 내부에 GraphQL mutation을 넣는 것은 권장하지 않습니다.
프레젠테이션 구성 요소에 의사 결정 논리가 없도록 하고 앱의 model 부분이 그렇게 하도록 하세요.
 
CQS는 읽기 및 쓰기 경로를 분리하는 것이었습니다. 읽기 작업은 읽기 전용이어야 하며 부작용을 수행하지 않아야 한다고 말했습니다.
우리는 이것이 코드를 더 쉽게 추론할 수 있게 해주기 때문에 이것을 좋아합니다. 이전에 메서드 수준에서 이에 대한 예를 보았습니다.

CQS가 구성요소에도 적용되는 방식에 대해 생각해 보세요.

프리젠테이션 구성요소가 읽기용인 경우 상태 변경 시기를 결정하는 데 관련해서는 안 된다는 의미가 아닐까요?

프레젠테이션이 쓰기에 대한 책임이 없어야 한다는 것이 아닌가요?
 

네, 즉 의사 결정 논리가 발생해야 하는 경우 이는 view가 아니라 모델의 관심사 입니다.

 

예를 들어, 체스 게임을 고려하십시오. ChessPiece 컴포넌트가 있는 경우
ChessPiece가 이동 돌연변이를 실행하는 것을 원하지 않을 것입니다.
이동을 수행할 게임 로직이 있기 때문입니다.
아마도 그 조각은 대각선이나 L자 모양으로만 움직일 수 있을 것입니다.
상호 작용 레이어에 게임 로직 및 모든 의사 결정 로직을 위임합니다.
상호 작용 논리, 데이터 가져오기, 상태 관리, 클라이언트 측 캐시(또는 저장소)까지 포함하는 최신 클라이언트 측 아키텍처의 전체 모델 부분입니다.

React에서는 React 후크를 사용하여 상호작용(애플리케이션이라고도 함) 레이어 로직을 구현합니다.

다음은 잠재적으로 mutation을 호출하기 전에 먼저 일부 의사 결정 논리를 수행하는 Apollo Client 및 React Hooks를 사용하는 createTodoIfNotExists 작업의 예입니다.

function useTodos (todos) {

  const createTodoIfNotExists = (text: string) => {
    const alreadyExists = todos.find((t) => t === text);
    
    if (alreadyExists) {
      return;
    }
    
    ...
		// Validate text
    // Perform mutation
  }

  return { createTodoIfNotExists }
}

// Container
function Main () {
  const { data: todos } = useQuery(GET_ALL_TODOS);
  const { createTodoIfNotExists } = useTodos(todos);

  ...
}

상태 머신이 필요한 경우 xState와 같은 도구를 사용하여 대신 상호 작용 논리 모델을 빌드할 수도 있습니다.

Concern — State management and storage

 Storage, updates, and reactivity
Apollo Client는 애플리케이션의 전체 상태 관리 부분을 처리하는 상태 관리 라이브러리입니다.
(https://www.apollographql.com/blog/dispatch-this-using-apollo-client-3-as-a-state-management-solution)에서 우리는 상태 관리 솔루션의 세 가지 기본 책임에 대해 배웠습니다.
  • 스토리지 - 글로벌 상태를 유지하기 위해 백엔드 서비스에서 검색된 데이터의 원격(전역) 상태, 공유 로컬 상태 또는 이 둘의 혼합일 수 있습니다.
  • 상태 업데이트 - 대부분의 경우 작업을 호출한 후 캐시된 상태를 부작용으로 업데이트해야 합니다.
  • 반응성 - 상태가 변경되면 새 데이터로 다시 렌더링해야 함을 UI 조각에 알려야 합니다.

Apollo Client에서는 정규화된 캐시를 저장소로 사용하고 캐시 API를 사용하여 상태를 업데이트하고

상태가 변경되면 앱 전체에서 쿼리에 변경 사항을 (자동) 브로드캐스트합니다.

Apollo Client는 상태 관리 및 데이터 fetch 문제를 모두 처리합니다.

Concern — Networking & data fetching

API 호출 수행 및 메타데이터 상태 보고

많은 상태 관리 라이브러리에서는 데이터 가져오기 및 네트워킹 문제도 처리하는 것이 편리하다고 생각합니다.

Apollo Client에서 이것은 당신을 위해 처리됩니다.

네트워킹 및 데이터 가져오기 계층의 책임은 다음과 같습니다.

  • 백엔드 서비스 위치(주소) 알기
  • 응답 계산하기
  • 응답, 오류 마샬링(직렬화/역직렬화)
  • 비동기 상태 Report — this is a form of meta state.

State — Local (component) state

단일 구성 요소에 속하는 상태 자체 비공개 구성 요소 로컬 상태를 유지하려는 구성 요소에 Apollo Client를 사용할 수 있지만
대부분의 경우 React의 useState와 같은 도구를 대신 사용할 것입니다.


개별 컴포넌트에 대한 데이터 그래프를 모델링하지 마십시오.

GraphQL은 데이터 그래프를 모델링하고 활용하여 필요한 데이터로 작업하는 것입니다.
그러나 GraphQL은 개별 컴포넌트를 초월하는 데이터와 상호작용하는 것입니다.
 
예를 들어 UI 구성 요소의 일부로 각 Todo 항목에 대해 isToggled 상태 조각을 갖는 것이 합리적일 수 있지만
데이터 그래프에서 isToggled에 대해 알아야 합니까?
 
아니요, 이는 UI 구성 요소에 매우 특정한 것입니다.
그래프에 UI 컴포넌트에 고유한(또는 이름을 따서 명명된) 개체가 포함되어 있으면 그래프를 잘못된 방향으로 디자인하고 있을 수 있습니다.

State — Remote state

백엔드 서비스에서 검색된 상태

Apollo Client는 원격 데이터 그래프의 캐시된 청크인 원격 상태에 대해 단독으로 책임이 있습니다.

useQuery 훅으로 쿼리를 호출하면 필요한 데이터를 가져와 호출자에게 제공하지만 정규화하고 캐시하기도 합니다.

// `data` holds the remote state of our data
const { data, loading, error } = useQuery(GET_ALL_TODOS);

데이터 그래프의 청크를 캐싱함으로써 다음에 그래프의 동일한 부분을 요청할 때

Apollo Client는 다른 네트워크 요청에서 캐시를 가져오는 대신 캐시로 바로 이동할 수 있을 만큼 똑똑합니다.
 

State — Meta state

 상태에 대한 상태
이러한 5가지 유형의 상태 중에서 Apollo 클라이언트가 구체적으로 다룰 수 있는 3가지 상태가 있습니다.
첫 번째는 메타 상태입니다. 상태에 대한 상태입니다.
useQuery 및 useMutation 훅을 사용하여 쿼리 또는 mutation을 수행하면 요청의 현재 상태에 대한 세부 정보가 포함된 객체를 얻습니다.
 
// The 'loading' variable holds the meta state of the network request.
const { data, error, loading } = useQuery(GET_ALL_TODOS);
Apollo Client를 사용하면 이 문제를 해결할 수 있습니다.
Axios 및 Redux와 같은 보다 기본적인 접근 방식을 사용하려면 썽크 내에서 이 신호 코드를 직접 작성해야 합니다.
export function createTodoIfNotExists (text: string) {
  return async (dispatch, getState) => {
    const { todos } = getState();

    const alreadyExists = todos.find((t) => t === text);
    
    if (alreadyExists) {
      return;
    }
     
    // Signaling start
    dispatch({ type: actions.CREATING_TODO })

    try {
      const result = await todoAPI.create(...)
      
      // Signaling success
      dispatch({ 
        type: actions.CREATING_TODO_SUCCESS, 
        todo: result.data.todo 
      })
    } catch (err) {
  
      // Signaling Failure
			dispatch({ type: actions.CREATING_TODO_FAILURE, error: err })
    }

  }
}

State — Shared local state

여러 구성 요소에서 사용되는 상태
상태는 둘 이상의 구성 요소에서 사용할 때 공유됩니다.
원격 상태는 일반적으로 앱의 여러 구성 요소에서 사용되므로 공유 상태의 한 형태로 간주합니다.
공유 상태는 다음 중 하나일 수 있습니다.
  • remote state,
  • client-side only local state,
  • or combination of both
원격 및 클라이언트-only 로컬 상태 결합
원격 상태에 추가 데이터를 추가해야 하는 경우 가능합니다. 이를 위해 Apollo Client를 사용할 수 있습니다.
Redux에서 온 개발자를 위한 예는 다음과 같습니다.
할 일 앱에서는 스토어에 병합되기 전에 각 할일에 클라이언트 측 전용 공유 로컬 상태를 추가할 수 있습니다.
이 경우 isSelected 속성을 추가합니다.
switch (action.type) {
  ...
  case actions.GET_TODOS_SUCCESS:
    return {
      ...state,
      // Add some local state to the remote state before merging it
      // to the store
      todos: action.todos.map((t) => { ...t, isSelected: false })
    }
}
일반적인 사용 사례는 아니지만 Apollo Client 3에서 이 작업을 수행해야 하는 경우 캐시 정책 및 반응 변수와 동일합니다.
import { makeVar } from "@apollo/client";

export const currentSelectedTodoIds = makeVar<number[]>([]);

export const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {
    Todo: {
      fields: {
        isSelected: {
          read (value, opts) {
            const todoId = opts.readField('id');
            const isSelected = !!currentSelectedTodoIds()
              .find((id) => id === todoId)
              
            return isSelected;
          }
        }
      }
    }
  }
});

 

마지막으로 동일한 쿼리에서 remote 및 클라이언트 전용 local 상태를 모두 요청하기 위해 @client 지시문을 사용하여 클라이언트 측에만 존재하는 것과 데이터 그래프에서 원격으로 해결할 수 있는 것을 캐시에 알릴 수 있습니다.
export const GET_ALL_TODOS = gql`
  query GetAllTodos {
    todos { 
      id  
      text  
      completed
      isSelected @client
    }
  }
`

 

다음 가이드인 Reactive Variables를 사용한 로컬 상태 관리에서는 원격 상태 외에 로컬 상태로 작업하는 것과 관련된 일반적인 시나리오를 해결하는 방법을 탐구합니다. 링크 : Local State Management with Reactive Variables

Conclusion

강력하고 테스트 가능하며 유연하고 유지 관리 가능한 React 애플리케이션을 빌드하는 것이 쉬운 작업은 아닙니다.
그러나 우리가 처리해야 하는 여러 가지 우려 사항과 상태 유형에 대한 명확한 이해는 훌륭한 출발점입니다.
여기에서 클라이언트 측 아키텍처에서 가장 일반적인 문제를 처리하는 방법을 설계할 수 있습니다.
권장 사항을 요약하면 다음과 같습니다.
  • UI 레이어에서 GraphQL 쿼리를 프레젠테이션 구성 요소와 가깝게 유지하세요.
    • 보통 쿼리는 페이지 단위로 배치를 많이 함.
  • 공유되지 않는 로컬(구성요소) 상태의 경우 React의 useState 훅이 작업에 가장 적합한 도구입니다.
    • 그래프에 UI 컴포넌트에 고유한(또는 이름을 따서 명명된) 개체가 포함되어 있으면 그래프를 잘못된 방향으로 디자인하고 있을 수 있습니다.
  • 공유(전역) 상태의 경우 Apollo Client 3의 로컬 상태 관리 API를 사용하거나 다시 React hooks API를 사용할 수 있습니다.
  • 컴포넌트 대신 인터렉션 레이어에 mutation(및 모든 모델 동작)을 배치하는 것을 선호합니다.
  • Apollo Client가 모든 상태 관리, 네트워킹 및 데이터 가져오기 문제를 처리하도록 하세요.
  • Apollo Client는 후속 요청을 위해 데이터를 효율적으로 가져오고, 정규화하고, 캐시할 수 있으므로 원격(전역) 상태를 처리하도록 합니다.

클린 아키텍처 예시 :

https://github.com/apollographql/odyssey-lift-off-part5-client

 

GitHub - apollographql/odyssey-lift-off-part5-client: Odyssey Lift-off V - Client - Course Companion App

Odyssey Lift-off V - Client - Course Companion App - GitHub - apollographql/odyssey-lift-off-part5-client: Odyssey Lift-off V - Client - Course Companion App

github.com

반응형