GraphQL? 한 줄로 말하자면 API용 쿼리 언어입니다. 그러니까 REST API처럼 endpoint를 이용하지 않고 더 유연하게 그냥 원하는 데이터를 쿼리로 가져온다는 거죠. GraphQL의 장점과 단점은 명확합니다. 물론 2012년에 개발되어 많은 상용 서비스에서 사용되고 있는 만큼 신뢰성은 충분하다고 생각됩니다. 비슷한 netflix에서 개발한 Falcor라는 것도 있는데 GraphQL에 비해 사용자 층이 비교할 수없이 적어서… (안습..) 먼저 GraphQL의 장점부터 알아보자면.

  1. 더 이상 endpoint 디자인에 스트레스 안 받아도 된다. => 개발 시간 단축
  2. 여러 endpoint에서 정보를 가져올 필요가 없다. => outbound traffic 줄일 수 있음.
  3. 필요 없는 정보를 전달받지 않을 수 있다. => outbound traffic 줄일 수 있음. => 돈 절ㅇ..
  4. 느슨한 frontend 결합. => 쉬운 확장. 개발 시간 단축
  5. Null 안정성, api versioning 불필요 (ex /v1/call)

그러니까 쉽게 이야기 해서, 내가 원하는 데이터만, 쿼리로 요청한 데이터만 얻을 수 있다는 거에요. 단점도 명확한데, endpoint의 수가 적어지거나 한 개라서 공격에 취약하다는 거죠. 물론 단점보다 장점이 많아서 고려할 필요도 없을 것 같습니다. 추후 서비스가 확장되면 마이크로 서비스 분리하기도 편하고요.

아무튼 캐릭터 보드는 기본적으로 SNS서비스이기 때문에, 전달되는 데이터의 양이 상당합니다. 캐릭터 보드의 기존 리턴 JSON을 예로 들어보죠.

{
	id: "id",
	name: "name",
	bio: "bio",
	followCount: 0,
  	followerCount: 0,
	community: [
		{
			communityName: "communityName",
			communityImg: "imgLink",
		},
		...
	],
	hotTag: [
		{
			tagName: "tagdata",
			value: "",
			isDefault: true
		},
		...
	]
	profileImg: "imgLink",
	backgroundImg: "imgLink",
	relation: [
		{
			characterUuid: "uuid",
			relationType: "type"
		}
	],
	tendency: "tendency"
	...
}

와우 전달되는 데이터의 양이 많네요. 더 놀라운 건 저 많은 정보를 사용자 정보를 가져올 때마다 불러와야 한다는 겁니다..;;

그러니까 예를 들어서 나는 사용자 이름만 알고 싶을 뿐인데 요청을 하면 endpoint가 이것저것 다른 지저분한 데이터를 추가로 전달하는 것이죠. 클라이언트 입장에선 쓸데없는 데이터를 전달받는 거라서 비효율적입니다. 나중에 마이그레이션 하면 감당 못할 것 같아서. 그냥 지금이라도 다 갈아엎고 GraphQL로 교체하기로 했습니다.

아까 언급하지 않은 GraphQL의 단점이 하나 더 있는데, 캐싱이 힘들다는 겁니다. 특정 URL로 요청하는 것이 아닌 한 URL에서 모든 요청을 처리해서 그렇죠. 이것도 나중에 서비스가 커지면 단점이 될 수 있는데, 예를 들어 글 같은 경우에는 캐싱을 해 주는 게 더 이득이거든요. 변경될 일이 적으니까요. 변경이 돼도 특정 캐시만 퍼시하면 되는거라서… 물론 너무 미래의 일, 또는 일어나지 않을 일이니까 생각을 멈추죠.

테스트!

아무튼 캐릭터 보드 개발은 1인 개발인 것도 있고 거의 무 자본 개발이라서 QA를 따로 구할 돈도 없기에 테스트 자동화가 필수입니다. JS테스팅 라이브러리는 처음인데, 요즘은 jest라는 게 유행한다고 해서 테스트 프레임워크는 jest로 서버 mocking 작업은 mswjs로 조금 더 쉽게 테스팅하기 위해 @testing-library/react을 추가로 사용해서 진행했습니다. jenkins를 이용한 jest 테스트는 여기에 설명되어 있으니 참고해 주세요!

저는 개인적으로 테스팅 관련한 코드는 메인 src 디텍토리에 넣지 않고 루트 디텍토리에 test폴더를 만들어서 따로 관리하는 편입니다. 소스 디텍토리에 뭐 .test.js이런식으로 다 모아버리면 초기엔 찾기 쉬울지 몰라도 나중 가면 관리가 무진장 어렵거든요. 아무리 IDE에 리펙토링 기능이 있다지만 경로 바뀌면 또 무슨 버그가 생길지 모르기에 처음부터 따로 관리해줍시다!

간략한 GraphQL 소개

일단 GraphQL에 대한 감을 잡아보죠. 공식 사이트에서 발췌한 스니핏중 하나입니다. 아래와 같이 쿼리를 하면

{
  me {
    name
  }
}
or
{
  hero {
    name
  	friends {
      name
    }
  }
}

요렇게 반환됩니다.

{
  "me": {
    "name": "Luke Skywalker"
  }
}
or
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

와! 너무나 직관적이네요. 조금 더 자세히 알아보죠. 자세한 내용은 문서를 보시면 금방 이해하실 수 있을 거에요!

#쿼리에 인자 전달 가능.
{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
#별칭 사용
{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
#재사용을 위한 fragment
{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}
fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
#변수 등등...

msw를 사용해보자!

이제 서버 mock을 만들어보죠! GraphQL 클라이언트는 원하는 걸 쓰시면 됩니다. 저는 urql을 사용했습니다. (오픈소스기도 하고 아폴로보다 성능이 좋다는 말이 많아서…)

어자피 클라이언트는 테스트 뿐만 아니라 프로덕션 빌드에도 사용해야 해서 –dev-save옵션은 필요가 없습니다.
npm install urql graphql

mswjs의 사용법은 너무나 쉬워서 1분이면 전부 이해하실 수 있습니다. 먼저 import를 해야 합니다.

import { graphql } from 'msw'
graphql.mutation('AddPizza', null)
graphql.query('GetPizza', null)

mutation은 POST요청이라고 생각하시면 됩니다. 서버에 데이터를 전송할 때 mutation(변화)을 명시적으로 표시해 쓰기를 유발하는 작업을 알릴 수 있습니다. query는 일반적인 GET요청이라고 생각하시면 됩니다 🙂 query를 먼저 살펴볼까요? 다음과 같은 query를 요청한다고 가정해 봅시다.

query GetPizza($pizzaName: Name!) {
  info(pizzaName: $pizzaName) {
    price
    taste
  }
}

해당 query에 대한 mock은 다음과 같이 만들 수 있습니다. req.variables를 이용해서 query의 변수에 접근할 수 있고, query 이름은 정규식도 사용할 수 있습니다. ctx.errors와 ctx.data를 적절히 사용해서 res을 이용해 리턴해 주면 됩니다!

graphql.query('GetPizza', (req, res, ctx) => {
  const { pizzaName } = req.variables
  return res(
    ctx.data({
      info: {
        price: '1000원',
        taste: '맛없음',
      },
    }),
  )
})

다음으로 mutation에 대해서 알아봅시다! 다음과 같은 mutation를 요청한다고 가정해 봅시다.

mutation AddPizza($pizzaName: String!, $price: String!, $taste: String!) {
  pizza(pizzaName: $pizzaName, price: $price, taste:$taste) {
    name
  }
}

해당 mutation에 대한 mock은 다음과 같이 만들 수 있습니다. 마찬가지로 정규식 사용 가능합니다.

graphql.mutation('AddPizza', (req, res, ctx) => {
  const { pizzaName, price, taste } = req.variables
  
  return res(
    ctx.data({
      pizza: {
        pizzaName: pizzaName,
      },
    }),
  )
})

localStorage.setItem같은걸 사용해서 더 복잡한 mock을 만들 수도 있겠지만 Don’t mock what you don’t own이라는 유명한 프로그래밍 격언처럼 특별한 경우가 아니라면 별로 좋지 않습니다.

mswjs의 몇 가지 유용한 추가 가능을 알아보도록 하죠. 먼저 link입니다. 이건 GraphQL에 여러 endpoint가 있을 경우 이름 충돌을 방지해 줍니다.

const pizza = graphql.link('https://api.pizza.com/v1');
pizza.query
OR
pizza.mutation

이름에 관계없이 모든 요청을 처리하는 operation도 있는데 잘 안 쓰이니 넘어가도록 할게요.

아무튼 mock을 만들었으니 테스트 코드를 짜봅시다. 공식 문서에도 여러 사례들에 대해 매우 잘 설명되어있습니다.

import { setupServer } from 'msw/node'
const server = setupServer(
    graphql.query('GetPizza', (req, res, ctx) => {
        const { pizzaName } = req.variables
        return res(
            ctx.data({
                info: {
                    price: '1000원',
                    taste: '맛없음',
                },
            }),
        )
    }),
    graphql.mutation('AddPizza', (req, res, ctx) => {
        const { pizzaName, price, taste } = req.variables
        return res(
            ctx.data({
                pizza: {
                    pizzaName: pizzaName,
                },
            }),
        )
    })
);
beforeAll(() => server.listen()) //설정
afterEach(() => server.resetHandlers()) // 테스트마다 초기화
afterAll(() => server.close()) // 해제

마지막으로 @testing-library/react를 사용해서 test를 만들 수 있습니다. 더 자세한 내용들은 여기서 찾으실 수 있습니다!

test('Pizza', async () => {
  server.use(
    ...
  )
})

urql을 사용해보자!

이번에는 urql을 한번 보죠. urql은 먼저 createClient를 사용해서 GraphQL endpoint url을 지정해 줘야 합니다.

import { createClient } from 'urql';

const client = createClient({
  url: 'url',
});

이런 식으로 말이죠. 그리고 지정했으면 그 url을 msw의 link()에 넣어줘야 합니다.

const pizza = graphql.link('url');

이제 ‘pizza’에 아까 전 코드처럼 query나 mutation를 등록한 후에 setupServer를 사용해주고, .use()를 통해 요청을 intercept 할 수 있습니다.

urql으로 요청하는 법 등은 해당 문서에 매우 자세하게 나와있으니 참고해주세요! 아까 만든 GetPizza와 AddPizza를 테스트하는 것은 여러분들의 몫으로 남겨두겠습니다.

글쓴이

phruse

쉬운 길보다는 어려운 길을 즐깁니다. 다양한 분야에 관심이 많으며 언젠가 많은 사람이 사용하는 기반 기술을 개발하는 것이 목표입니다.