Tutorial

여기 따라하면 기본적인건 해볼 수 있다.

 

설치

서버사이드(Java/Maven)

maven의존성

springframework에서 지원하는 java버전이 있고

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-graphql</artifactId>
        </dependency>

 

expediagroup에서 지원하는 kotlin버전이 있다.

        <dependency>
            <groupId>com.expediagroup</groupId>
            <artifactId>graphql-kotlin-spring-server</artifactId>
            <version>7.0.1</version>
        </dependency>

후자가 Resolver(리졸버)만 구현하면 스키마를 자동생성해주고 kotlin 타입과 연동되는등 kotlin을 사용한다면 더 편리한 측면이 있지만, 여기서는 전자를 사용한 기준으로 서술한다.

expediagroup 아티펙트의 경우, 내 경우엔 /graphql 404문제가 해결이 안돼서 springframework를 쓰기로 했다.
추가로 조사해보니 webmvc대신 webflux로 교체해야 호환되는 이슈도 있었다(여기)
spring-boot-starter-web 대신 spring-boot-starter-webflux의존성으로 바꿔야 하는데 WebConfig.kt등 여러곳에서 코딩방식을 바꿔야 하는 걸로 보인다.

 

클라이언트 사이드(React/Next.js)

관련 library설치

npm install @apollo/client graphql

 

아폴로?

Apollo Client는 JavaScript 어플리케이션에서 GraphQL API와 통신할 수 있게 해주는 라이브러리입니다.

Facebook에서는 GraphQL을 발명했으며, Relay라는 GraphQL 클라이언트를 만들어 공개했습니다. 그러나 Apollo가 Relay보다 더 널리 사용되는 이유는
사용자 친화적: Apollo는 사용자 친화적이고, 초보자에게 친숙하며, 설정이 상대적으로 간단합니다. 문서화도 잘 되어 있어, 개발자들이 쉽게 접근하고 사용할 수 있습니다.
커뮤니티 지원: Apollo는 강력한 커뮤니티 지원을 받고 있으며, 다양한 추가 기능과 툴이 개발되고 있습니다. 또한 꾸준한 업데이트와 개선이 이루어지고 있어, 더 많은 개발자들이 Apollo를 선호하게 되었습니다.

 

코드

이번 문서 에서는 특정문제에 대해서 제출된 횟수를 리턴하는 예제를 해보기로 하자.

프로젝트 구성에 대해서는 여기를 보고오자.

 

서버사이드

Query

1. GraphQL 스키마 정의

src/main/resources/graphql/schema.graphqls 파일에 쿼리를 추가

type Query {
  submissionCountByProblem(problemId: ID!): Int
}


2. 서비스 수정

SubmissionService에 getSubmissionCountByProblem매서드를 추가하여 문제별 제출 수를 가져옴 

@Service
class SubmissionService(private val submissionRepository: SubmissionRepository) {

    // 아래는 기존에 존재하던 매서드
    fun submitProblem(userId: Long, problemId: Int, code: String): Submission {
        val submission = Submission(
            userId = userId,
            problemId = problemId,
            code = code,
            status = "PENDING" // 초기 상태
        )
        return submissionRepository.save(submission)
    }

    // 아래 매서드 추가
    fun getSubmissionCountByProblem(problemId: Int): Int {
        return submissionRepository.countByProblemId(problemId)
    }
}

 

3. 컨트롤러에 로직 추가

컨트롤러에 @QueryMapping 어노테이션을 사용하여 submissionCountByProblem GraphQL쿼리를 처리하는 메서드를 추가(엔드포인트 추가는 아니지만 약간 유사)

package com.sevity.problemservice.controller

import ...

@RestController
class SubmissionController(private val submissionService: SubmissionService) {

    // 기존 코드
    // ...

    @QueryMapping
    fun submissionCountByProblem(@Argument problemId: Int): Int {
        return submissionService.getSubmissionCountByProblem(problemId)
    }
}

 

Mutation

//TBD

 

클라이언트사이드

본격적으로 클라이언트 사이드 코딩을 하기전에

https://localhost:9993/graphiql 

위 url에 접속해서 아래 처럼 날려볼 수 있다.

query  {submissionCountByProblem(problemId: 13)}

그럼 결과가 다음처럼 보일 것이다.

 

apollo library 사용을 위해 아래 코드를 기본적으로 넣어준다.

// src/apolloClient.js
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://sevity.com:9993/graphql',  // GraphQL 서버 URL
  cache: new InMemoryCache(),
});

export default client;

 

그다음 _app.js를 수정해서 각 페이지에서 해당 기능을 사용할 수 있게 해준다.

import ...
import { ApolloProvider } from '@apollo/client';
import client from '../apolloClient';  // Adjust the path if necessary

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  )
}

export default MyApp;

 

실제 조회하는 코드를 [id].js에 넣어준다.(제출 횟수 및 관련 부분)

...
import { useQuery } from '@apollo/client';
import gql from 'graphql-tag';

// 아래 스트링 방식이 후진적으로 보일 수 있는데, 간접적으로 문법검사나, 자동생성 툴이 존재한다.
// 하지만, 결국 서버측에 문자열로 graphQL을 보내야한다. 현재 graphQL의 한계점.
const GET_PROBLEM_SUBMISSION_COUNT = gql`
  query GetProblemSubmissionCount($problemId: Int!) {
    submissionCountByProblem(problemId: $problemId)
  }
`;


const Problem = () => {
  ...
  const { data, loading, error } = useQuery(GET_PROBLEM_SUBMISSION_COUNT, {
    variables: { problemId: Number(id) },
    skip: !id  // id가 없는 경우 쿼리를 건너뜁니다.
  });

  // JavaScript의 옵셔널 체이닝(Optional Chaining)사용('?.'부분들)
  const submissionCount = data?.submissionCountByProblem;
  if(data)
    console.log('GraphQL response:', JSON.stringify(data, null, 2));

...
  
  
  return (
    <div className="container">
      <div className="alert alert-success mt-3">username: {username} </div>
      {problem ? (
        <>
          <h1>{problem.title}</h1>
          <p>{problem.description}</p>
          <p><strong>예제 입력:</strong> {problem.exampleInput}</p>
          <p><strong>예제 출력:</strong> {problem.exampleOutput}</p>
          <p><strong>실제 입력:</strong> {problem.realInput}</p>
          <p><strong>실제 출력:</strong> {problem.realOutput}</p>
          {submissionCount && (
            <p><strong>제출 횟수:</strong> {submissionCount}</p>
          )}
          <textarea
            className="form-control"
            rows="10"
            value={sourceCode}
            onChange={(e) => setSourceCode(e.target.value)}
          ></textarea>
          <button className="btn btn-primary" onClick={handleSubmit}>
            제출
          </button>
        </>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
};

export default Problem;

 

 

트러블슈팅

/graphql 404

src/main/resources/graphql 폴더와 그안에 anyname.graphqls 파일을 생성안하면 /graphql 경로접근시 404뜨는 문제가 있어서 반나절 이상 소모했다 ㅠ

여기에도 기록함

 

expediagroup 아티펙트의 경우, 위의 해결책을 적용해도 여전히 404가 떴고, 추가로 조사해보니 webmvc대신 webflux로 교체해야 하는 이슈가 있었다(여기)
spring-boot-starter-web 대신 spring-boot-starter-webflux의존성으로 바꿔야 하는데 WebConfig.kt등 여러곳에서 코딩방식을 바꿔야 하는 걸로 보인다.

반응형

+ Recent posts