React, GraphQL 그리고 Declarative Pattern

기술이나 프레임워크를 올바르게 혹은 효율적으로 사용하기 위해선 여러가지 방법이 있겠지만, 그 중 하나는 기술이 갖고 있는 철학을 이해하고 그에 맞게 사용하는 것이라고 생각합니다.

그렇다면 React를 "React"답게, GraphQL을 "GraphQL" 답게 사용하려면 어떻게 해야 할까요?

React와 GraphQL에 대한 기본 개념을 알고 있다고 가정하에, 2년동안 React와 GraphQL을 사용하면서 느낀점을 Declarative Pattern 관점에서 설명합니다. 결론은 React는 UI의 How를 가져갔고, GraphQL은 데이터 처리의 How를 가져감으로써 What에 집중하는 Declarative UI가 가능합니다.

Declarative Pattern

먼저 Declarative Programming을 위키피디아에선 아래와 같이 설명합니다.

... what the program must accomplish in terms of the problem domain, rather than describe how to accomplish it as a sequence of the programming language primitives

여기서 집중해서 볼 키워드는 whathow 입니다. Declarative Programming은 how 보다는 what에 더 집중합니다. howwhat을 UI 관점에서 정리해보면 아래 표와 같습니다.

How

What

어떻게

무엇을

어떤 방식으로

어디에

어떤 순서로

어떤 모양으로

howwhat을 우리가 사용하는 코드와 어떻게 연결 지을 수 있을까요?

아래 예제 코드는 express의 미들웨어에서 request 객체에 user 가 없으면 login 페이지로 이동하는 JavaScript 코드입니다. (출처 : 그래서 DECLARATIVE UI가 뭔데? - 원지혁)

import express from 'express'

const app = express();

app.use((req, res, next) => {
    if (!req.user) {
        res.redirect("/login");
    } else {
        next();
    }
});

위 코드를 refactoring한다면 보통 어떻게 할까요?

app.use(redirectIfNotLoggedIn);

const redirectIFNotLoggedIn = (req, res, next) => {
    if(!req.user) {
        res.redirect("/login");
    } else {
        next();
    }
}

혹시 위와 같은 코드를 생각했나요? 여기서 만약 whathow를 분리해서 refactoring 한다면 다음처럼 변경할 수 있습니다.

app.use(redirectIf((req) => !req.user, "/login"));

const redirectIf = (compare, to) => {
  return (req, res, next) => {
    if (compare(req)) {
      res.redirect(to);
    } else {
      next();
    }
   }
} 

기존 코드에서 how를 잘 분리해서 redirectIfhow를 만들고 읽어야 하는 코드에 what을 뭉쳤습니다. 두 코드를 읽을 때 어떤 코드가 더 명확한가요? Declarative Pattern은 이렇게 what 에 더 집중해서 코드가 명확해지고 재사용하기 쉽도록 만들 수 있습니다.

Separation of Concerns

다들 아시겠지만, 컴퓨터 과학에는 관심사의 분리(separation of concerns) 라는 디자인 원칙이 있습니다. 관심이 있는 부분만 따로 뭉쳐서 구현을 하면 이해하기 쉽고, 재사용할 수 있으며, 유지 보수시 복잡성을 줄일 수 있습니다. UI에서 what에 대한 관심사의 분리 원칙을 적용할 수 있을까요? 그럼요 :)

Declarative UI

먼저 UI는 화면에 데이터를 그리는 역할을 합니다. 화면에 데이터를 그리기 위해서 무슨 데이터를, 어디에, 어떤 모양으로 그릴지 정해야 합니다. 날씨 정보를 보여줘야 한다면, 날씨 정보를, 최상단에, 네모 박스에 넣어서 화면을 그리게 됩니다. 즉 UI와 Declarative Pattern의 관계는 굉장히 자연스럽습니다.

그럼 무엇이 문제였을까요?

웹에서 데이터에 관한 처리는 JavaScript, 어디는 HTML, 어떤 모양은 CSS가 담당합니다. 이 세 언어는 너무나도 달라서 섞이기 쉽지 않았고, 대부분 Technology에 따라서(separation of technology) 정리하게 됩니다. JavaScript는 js 폴더에, HTML 파일은 html 폴더에, CSS 파일은 css 폴더에 말이죠. 관심사는 같아도 너무나 다른 모습이라서 뭉치기에 쉽지 않습니다.

그리고 이 관계는 React 가 등장하면서 허물어지게 됩니다.

React와 Declarative

React의 홈페이지의 첫 화면에 들어가면 아래와 같은 문구를 확인할 수 있습니다.

Declarative

React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.

Declarative views make your code more predictable and easier to debug.

말 그대로 JSX라는 문법을 통해서 JavaScript 코드에서 선언형 뷰를 제공합니다. 특정 이유가 있지 않은 한 DOM을 건들 필요 없이 JavaScript 파일내에서 뷰를 선언할 수 있습니다. 또한 CSS-in-JS를 통해서 JavaScript 코드 내에서 스타일을 선언 할 수 있게 되었습니다. (ex styled-compoents)

이제 JavaScript 코드에서 관심이 있는 것, 즉 "어디에, 무슨 모양으로" 를 함께 선언할 수 있습니다.

마지막 "무엇을" 은 GraphQL이 선언할 수 있도록 도와줍니다.

GraphQL Fragment와 Declarative Data Fetching

GraphQL은 표면상 드러나는 특징 (one endpoint, SDL, Type System) 외에도 좀 더 깊이 들어가면 GraphQL이 저 특징들 하여금 어떤 문제들을 해결하고 싶어 했는지 알 수 있습니다. UI에서 보여지는 모든 데이터는 Graph 관계에 가까우며, UI에서 필요한 필드를 선언함으로써 더욱 what 에 집중하게 됩니다.

GraphQL에는 Fragment라는 GraphQL 조각이 있습니다. Fragment에는 GraphQL 타입에 따라 필드를 선언 할 수 있습니다.

아래와 같이 Post 라는 타입에 대해서 필요한 필드만 Fragment로 선언할 수 있습니다.

type Post {
    name : String;
    author : Author;
}

type Author {
    name: Strong;
    age: Int;
}

fragment Post_Author_Fragment on Post {
  name
  author {
     name
     Int
  }
}

Fragment는 컴포넌트 마다 선언할 수 있으며, fragment 내에선 필드 중복이 가능합니다. (꼭 컴포넌트의 필드일 필요는 없습니다)

// PostHeader.jsx
const PostHeader = (post:Post_Header_Fragment) => <>...</>;

PostHeader.fragments = {
   post : gql`
      fragment Post_Header_Fragment on Post {
         name // Post 타입의 이름
      }
   `
}

// PostAuthor.jsx
const PostAuthor = (post:Post_Author_Fragment) => <>...</>;

PostAuthor.fragments = {
   post : gql`
      fragment Post_Author_Fragment on Post {
         name // Post 타입의 이름
         author {
            name
            age
         }
      }
   `
}

// Post.jsx
const Post = (post:Post_Fragment) => {
   return (
   <>
      <PostHeader post={post}/>
      <PostAuthor post={post}/>
   </>
   );
};

Post.fragments = {
   post : gql`
      fragment Post_Fragment on Post {
         ...Post_Header_Fragment
         ...Post_Author_Fragment
      }
      ${PostHeader.fragments.post}
      ${PostAuthor.fragments.post}
   `
};

// PostContainer.jsx
const GET_POST_QUERY = gql`
   query get_post_query($postId:Int!) {
      getPost(postId:$postId) {
         ...Post_Fragment
      }
   }
   ${Post.fragments.post}
`

const PostContainer = () => {
   const {data} = useQuery(GET_POST_QUERY, {variables : {postId:1}});
   
   return <Post post={data.getPost.data}/>
}

"관심사" 위주의 응집도를 높이기 위해 컴포넌트 마다 한 개 혹은 여러 개의 fragment를 같이 선언함으로써 컴포넌트와 그 컴포넌트가 가지는 데이터 의존성같이 배치(co-location) 할 수 있습니다. 즉 UI의 의존성과 데이터 의존성같은 위계에 놓이면서 Declarative Data Fetching이 가능해집니다.

이로써 데이터가 필요한 그 위치에서 관리할 수 있습니다.

Declarative Data Fetching"선언하고 잊어버린다" 그리고 "실수"가 없음을 보장합니다. 결국 각 컴포넌트에서 어떤 데이터가 필요한지(what)을 적으면, 라이브러리가 한 개의 GraphQL 요청으로 만들고(how) 응답을 받으면 응답의 결과가 자동으로 반영된 상태를 만듭니다(how). Fetch 하는 데이터를 결정하는 흐름이 Top-down 이 아닌 Bottom-up을 형성하게 됩니다. (유지보수에 강력합니다)

위의 코드처럼 각 컴포넌트들의 fragment들은 계속 합성되면서 최상단에서 data를 fetch하는 컴포넌트는 자신이 가지고 있는 컴포넌트(children)의 fragment 만 나열해서 선언하게 됩니다.

이렇 GraphQL은 UI에서 데이터에 대한 what은 남기고 how를 가져갑니다.

GraphQL이 데이터에 대한 선언까지 처리함으로써 React와 GraphQL을 사용하면 무엇을, 어디에, 어떤 모양과 같은 what 에 대한 정보를 같은 컴포넌트에(colocation) 선언하여 관심사를 분리하고 선언형 UI로 개발할 수 있었습니다.

GraphQL Mutation과 Side Effect

UI에서 데이터를 조회 뿐만 아니라 변경도 합니다. GraphQL에선 데이터에 대한 변경을 Mutation이라고 합니다. Mutation은 데이터에 대한 수정(Command)를 하며 그에 대한 부수효과(Side Effect)가 발생합니다.

예를 들어, 게시판에서 게시글을 추가하면(Command), 게시판 목록에는 새로운 게시글이 표시되는 것 처럼 말이죠(Side Effect). State Reflection 에만 관심이 있는 React는 이런 부수효과(how)를 좀 더 편리하게 처리하기 위해서 Redux, Mobx, Apollo Cache와 같은 전역으로 데이터를 관리해주는 스토어를 사용합니다.

여기서도 Fragment는 더욱 강력한 힘을 발휘합니다. Mutation의 Schema가 잘 짜여 있다면 Client 입장에서는 해당 Mutation으로 발생하는 결과물을 서버로부터 리턴받으면 됩니다. 이때 발생하는 결과물은 결국 컴포넌트와 일치하며, 뮤테이션의 반환 필드로 Fragment를 사용할 수 있습니다.

const UPDATE_POST = gql`
   mutation update_mutation(postId:3) {
      ...Post_Framgent
   }
   ${Post.fragments.post}
`

해당 Mutation을 서버에 요청하면 서버는 변경된 정보를 Fragment 필드에 따라 리턴합니다.

그럼 Apollo Cache는 해당 데이터를 가지고 스토어(Cache)를 업데이트 하게 되고 해당 데이터를 보고 있던 컴포넌트들의 상태에도 자동 반영합니다.(how)

이로써 Mutation에 따른 부수효과(how)를 Client 입장에서 안전하고 간단하게 다룰 수 있습니다.

더 나아가 GraphQL

GraphQL을 사용하면서 REST API와 견주기에 굉장히 매력이 있다고 생각합니다. 특히 하나의 뷰에 데이터 들이 복잡한 관계와 상태를 가질 수록 그 장점은 더 두드러집니다. (ex. Facebook)

GraphQL을 처음 접했을땐 앞에서 말한 표면적인 특징들이 크게 다가왔습니다.

서버에 타입이 이미 있다면 추가적인 요청 없이 클라이언트에서 필요한 데이터를 가지고 올 수 있고(오버페칭, 언더페칭을 해결), 서버의 Schema로부터 타입을 generate하여 타입에 대한 안전성을 확보할 수 있습니다. (참고로 nest.js를 사용하면 DTO에서 Schema를 가져올 수 있습니다. 즉 DTO에서 선언한 타입이 Client까지 연결됩니다!)

하지만 사용하면서 이런 표면적인 특징보다는 GraphQL이 Graph라는 특성이 더 강하게 와닿았습니다.

예를 들면, GraphQL은 타입에 따라 Resolver(타입을 처리하는 함수)가 호출이 됩니다. 즉 애초에 구현할때부터 JOIN(RDB라면)을 사용하지 않게 되고, 이건 추후에 DB를 분리해야 할때 큰 장점으로 다가옵니다(실제로 서비스 호출이 많은 IT 회사는 성능 이슈때문에 JOIN 사용을 지양한다고 합니다)

모델이 Graph로 연결되어있다라는 점이 애플리케이션을 개발함에 있어 얼마나 자연스러운지 생각하게 되었습니다.

또한 GraphQL을 사용하면서 백엔드와 의사소통에 불필요한 요소가 많이 사라졌습니다. API를 생성할때 물론 완벽하게 문서를 준비하고 회의를 하면 REST 또한 문제가 되지 않겠지만, GraphQL을 사용하면 UI 에서 필요한 스키마를 클라이언트에서 먼저 작성하고 백엔드 개발자와 UI와 함께 Schema를 검토한 후에 바로 개발할 수 있습니다(A라는 데이터가 필요합니다. API를 만들어주세요 => UI에 필요한 Schema를 공유한 후 개발). UI의 데이터들은 Graph 관계로 이루어져 있고 이러한 데이터 관계를 통해 요구하는 바를 GraphQL schema를 통해 좀 더 명확하게 소통할 수 있습니다. 여기서 Schema는 결국 Client와 Server 간의 추상화된 인터페이스 역할을 하게됩니다.(그래서 저는 Schema-Frist Approach를 선호합니다)

마치 GraphQL은 REST로 만들때보다 프론트와 백엔드 개발자가 한 곳을 바라보고 개발하는 느낌을 받았습니다.

마지막으로 Thinking in Graphs 문서의 마지막 부분입니다.

One Step at a time

Get validation and feedback more frequently

Don't try to model your entire business domain in one sitting. Rather, build only the part of the schema that you need for one scenario at a time. By gradually expanding the schema, you will get validation and feedback more frequently to steer you toward building the right solution.

무엇이든 처음부터 잘 만들 수 는 없습니다. 한번에 전체 비즈니스 도메인을 모델링하지 말고 한번에 하나의 시나리오, 필요한 부분만 시나리오를 만듭니다 GraphQL은 기존의 타입을 수정하는걸 권장하지 않습니다. 필드를 새로 추가하거나 타입을 선언해서 하위 호환성을 가져가도록 권장합니다.

이는 점진적으로 스키마를 확장함으로써 올바른 솔루션을 구축하기 위한 검증과 피드백을 더 자주 받을 수 있습니다.

GraphQL 사용 1년차, 2년차의 생각이 다르듯, 시간이 흐르면 저 또한 계속해서 생각이 바뀔거라고 장담합니다.

글을 읽고 궁금하거나, 이해가 안가거나, 잘못 된 정보가 있다면 알려주세요!

읽어주셔서 감사합니다.

Ref :

Last updated