GraphQL에 대해 알기 위해선 먼저 REST API에 대해 알아야 한다.
REST API란?
API또는 애플리케이션 프로그래밍 인터페이스는 애플리케이션이나 디바이스가 서로 간에 연결하여 통신할 수 있는 방법을 정의하는 규칙세트이다. REST(REpresentational State Transfer) 아키텍쳐 스타일의 디자인 원칙을 준수하는 API이다. 출처
그런데 이때 REST는 뭘까?
REST(REpresentational State Transfer)란?
자원을 이름(자원의 표현)으로 구분하여 자원의 상태를 주고 받는 것으로
- 자원 : 해당 소프트웨어가 관리하는 모든 것을 말한다.
- 자원의 표현 : 자원을 표현하기 위한 이름을 말한다.
문서나 그림, 데이터 등(자원)에서 음식 정보라는 자원을 REST방식으로 사용한다고 가정했을 때, 'FOOD'를 자원의 표현으로 정한다.
즉, HTTP URI를 통해 자원을 명시하고 GET, POST, PUT, DELETE를 통해 CRUD를 적용하는 것을 의미한다.
REST API의 작동 방식
HTTP 요청을 통해 통신함으로써 리소스 내에서 레코드(CRUD)의 작성(create), 읽기(read), 업데이트(update), 삭제(delete) 등의 표준 데이터베이스 기능을 수행한다.
- GET : 레코드를 검색한 후 결과 리턴
- POST : 레코드를 작성
- PUT : 작성한 레코드를 전체 업데이트
- PATCH : 작성한 레코드를 부분 수정
- DELETE : 작성한 레코드를 삭제
간단하게 이해하면 소프트웨어간 정보를 주고받는 일종의 방식으로 이러한 REST API를 사용하는 프로그램을 RESTFUL이라한다.
그런데 이런 REST API방식에는 몇 가지 단점이 있다.
- 요청을 보내면 그에 해당하는 모든 정보들을 리턴받는다.
- REST API를 작성하는 표준이 존재하지 않아 제각각인 경우가 존재한다.(확장성이 높다라고 할 수 있긴하다.)
- 이러한 방식을 지원하지 않는 옛날 브라우저들이 존재한다.
이러한 REST API의 몇 가지 단점들을 보완하기 위해 만들어진 것이 GraphQL이다.
SQL과 같은 쿼리 언어로 REST API의 단점을 보완하여 페이스북에서 만들어냈다. REST API의 POST방식으로 엔드포인트가 무조건 /graphql인 URI로 요청을 보낸다. 이때 body안에 어떠한 함수를 실행시키고 어떠한 값을 받을 지 작성한다.
장점
- 엔드포인트를 하나로 통합할 수 있다.
- 여러가지 요청을 한 번에 묶어 요청할 수 있고, 원하는 정보만 출력받을 수 있다.
Apollo Server에서 작업을 처리할 때 사용하는 도구로 4가지의 인수를 받을 수 있다.
const resolvers = {
Query: {
user(parent, args, context, info) {
return users.find(user => user.id === args.id);
}
}
}
위 인수에 대한 설명
ARGUMENT | DESCRIPTION |
---|---|
parent | 이 필드의 부모 resolver의 반환 값, 만약에 필드가 존재하지 않는다면 rootValue에서 전달된 값 반환 |
args | 이 필드에 제공된 모든 GraphQL 인수를 포함한다. EX) query{user(id: "4"}라는 쿼리를 보낼 때 args는 user{ "id" : "4"}라는 정보를 반환 |
context | resolver에서 전역으로 사용 가능한 변수를 말한다. |
info | 필드 이름, 루트에서 필드까지의 경로 등을 포함하여 작업의 실행 상태에 대한 정보를 포함하여 리턴한다. |
apollo server resolver에서 전역으로 사용가능한 변수를 말하며, 주로 유저가 로그인을 하였는지 확인할 때 사용하는 변수로 apollo server에 context를 추가하여 어떠한 정보를 사용할지 정할 수 있다.
공식 문서에서 req안에 header안에 담긴 authorization의 정보를 context에 담아 전역으로 사용할 수 있게한 코드이다.
// Constructor
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
authScope: getScope(req.headers.authorization)
})
}));
// Example resolver
(parent, args, context, info) => {
if(context.authScope !== ADMIN) throw new AuthenticationError('not admin');
// Proceed
}
login할 때 생성된 토큰을 context에 담아 사용하면 로그인된 유저만 사용할 수 있는 기능등을 구현할 수 있다.
- GraphQL로 작성된 백엔드 서버에서 유저가 로그인을 했을 때 유저 정보를 암호화하여 토큰으로 만든다.
- 만들어진 토큰을 브라우저의 header에 저장하여 로그인이 필요한 서비스 이용 시 Resolver에서 복호화하여 유저가 로그인을 하였는지 확인할 수 있다.
간단하게 로그인을 구현해보았다.
login(
@Args('email') email: string,
@Args('password') password: string,
@Context() context: any
){
// 1. 로그인(이메일과 비밀번호가 일치하는 유저 찾기)
const user = await this.userService.findOne({ email })
// 2. 일치하는 유저가 없으면? 에러 던지기!
if(!user) throw new UnprocessableEntityException('이메일이 존재하지 않습니다.')
// 3. 일차하는 유저가 있지만, 암호가 틀렸다면 에러던지기!!
const isAuth = await bcrypt.compare(password, user.password)
if(!isAuth) throw new UnprocessableEntityException('암호가 틀렸습니다.')
// 4. refreshToken(=JWT)을 만들어서 프론트엔드(쿠키)에 보내주기
// res, req는 context에 저장되어있음
this.authService.setRefreshToken({user, res: context.res})
// 5. 일치하는 유저가 있으면? accessToken(JWT)을 만들어서 프론트엔드에 보내주기
return this.authService.getAccessToken({user})
}
그동안 코드를 작성하며 왜 이러한 방식을 사용했고, context를 통해 로그인 정보를 어떻게 받아왔는지 명확하게 이해하지 못했었는데 이번 공부를 통해 조금이나마 이해할 수 있었다.