I am going to make some blog project with Typescript, GraphQL(Apollo-Client) and Next.js. The purpose of this project is that,
-
Studying and Using Typescript
-
Learn GraphQL and Get the gist of difference between GraphQL and REST API.
-
Get the experience with design -> develope -> deploy
This architecture from Bulletproof node.js project architecture ๐ก๏ธ written by Sam Quinn
| Directory | Description |
|---|---|
| app.ts | App entry point |
| api | Express route controllers for all the endpoints of the app |
| graphql | Apollo-GraphQL Resolver |
| config | Environment variables and configuration related stuff |
| jobs | Jobs definitions for agenda.js (Scheduler modules) |
| loaders | Split the startup process into modules |
| models | Database models |
| services | All the business logic is here |
| subscribers | Event handlers for async task |
| types | Type declaration files (d.ts) for Typescript |
I'm going to apply "The principle of separation of concerns" with Controller - Service Layer - Data Access Layer (3 Layer Architecture).
$mkdir <serverName>
$cd <serverName>
$touch index.js or index.tsx
$npm init -y $npm i apollo-server graphql- apollo-serve๋ Apollo์์ ์ ๊ณตํ๋ GaphQL ์๋ฒ ํจํค์ง์ ๋๋ค.
- graphql์ Facebook์์ ์ ์ํ GraphQL ์คํ์ JS์ธ์ด๋ก ๊ตฌํํ ํจํค์ง์ ๋๋ค.
Apollo-server and Apollo-Client depends on graphql package, so you're supposed to install this package together.
First of all, We write index.ts for using Apollo-Server.
const { ApolloServer, gql } = require("apollo-server");- ApolloServer๋ GraphQL ์๋ฒ ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด์ฃผ๋ ์์ฑ์์ ๋๋ค.
- gql์ ์๋ฐ์คํฌ๋ฆฝํธ๋ก GraphQL ์คํค๋ง๋ฅผ ์ ์ํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด ํ๊ทธ์ ๋๋ค.
๊ทธ ํ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
const typeDefs = gql`
type Query {
ping : String
}
`;
const resolvers = {
Query: {
ping() => "pong"
}
};- typeDefs ๋ณ์๋ gql์ ์ด์ฉํด GraphQL ์คํค๋ง ํ์ ์ ์ ์ํฉ๋๋ค.
- resolvers ๋ณ์๋ GraphQL ์คํค๋ง๋ฅผ ํตํด ์ ๊ณตํ ๋ฐ์ดํฐ๋ฅผ ์ ์ํ๋ ํจ์๋ฅผ ๋ด์ ๊ฐ์ฒด๋ฅผ ํ ๋นํฉ๋๋ค.
- ์๋ String ๋ฐ์ดํฐ์ ์๋ตํ ์ ์๋ ping์ด๋ผ๋ ์ฟผ๋ฆฌ๋ฅผ ์ ์ํ ํ, ping์ด๋ผ๋ ์ฟผ๋ฆฌ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ํญ์ pong์ด๋ผ๋ ๋ฌธ์์ด์ ์๋ตํ๋๋ก ํ ๊ฒ์ ๋๋ค.
๋ง์ง๋ง์ผ๋ก ๋ค์๊ณผ ๊ฐ์ด ์์ฑํฉ๋๋ค.
const server = new ApolloServer({
typeDefs,
resolvers
});
server.listen().then(({ url }) => {
console.log(`Listening at ${url}`);
});- ์ ์ฝ๋๋ typeDefs์ resolvers๋ฅผ ApolloServer ์์ฑ์์ ๋๊ฒจ GraphQL ์๋ฒ ์ธ์คํด์ค๋ฅผ ์์ฑํ๊ณ ๊ทธ ์๋ฒ๋ฅผ ์์ํด์ฃผ๋ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
When run Apollo-Server with below command, You can see "Apollo Server ready at http/graphql"
$node .
$node index.js์ฝ์์ ๋ค์๊ณผ ๊ฐ์ด ์๋ฒ์ ์๋ต ๊ฒฐ๊ณผ๋ฅผ ํ ์คํธํ ์ ์์ต๋๋ค.
$curl -X POST "http;//localhost:4000" -H "content-type: application/json" -d '{"query":"{ping}"}'- curl์ ๋ค์ํ ํ๋กํ ์ฝ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ๋ช
๋ น์ค ๋๊ตฌ์
๋๋ค.
- ์๊ฒฉ ์๋ฒ(FTP, HTTP๋ฑ)์์ ํ์ผ์ ๋ฐ์ ๋ณด์ฌ์ฃผ๋ ๋๊ตฌ์ ๋๋ค.
- ๋ํ Graph QL ์๋ฒ๋ Playground๋ผ๊ณ ํ๋ ์น ๊ธฐ๋ฐ ํด์ด ์์ด์ ๋ธ๋ผ์ฐ์ ์์๋ ์ฟผ๋ฆฌ๋ฅผ ํ์ธํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
$npm i apollo-server-express
$npm i express๊ทธ ํ index.js ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํํฉ๋๋ค.
import expess = require('express');
import { ApolloServer } = require('apollo-server-express');
const app = express();
...
const server = new ApolloServer({
typeDefs,
resolvers
});
server.applyMiddleware({app, path: '/graphql'});
app.listen({port: 8000}, ()=> {
console.log('Apollo Server on http://localhost:8000/graphql');
});CORS ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด CORS ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ต์คํ๋ ์ค์ ์ถ๊ฐํด์ค๋๋ค.
$npm i corsimport cors = require('cors');
import expess = require('express');
import { ApolloServer } = require('apollo-server-express');
const app = express();
app.use(cors());
...$npm i mysql2 sequelize
$npm i nodemon -dev
$npm i sequelize-cli -g๊ทธ ํ ๋ค์ sequelize-cli๋ฅผ ์ด์ฉํด sequelize๋ฅผ ์ด๊ธฐํ ํฉ๋๋ค.
$sequelize init์ด๋ฌ๋ฉด config, models, migration, seeders๋ผ๋ ํด๋๊ฐ ์๋์ผ๋ก ์์ฑ๋ฉ๋๋ค.
modelํด๋ ์์ ๋ค์๊ณผ ๊ฐ์ด ๊ฐ ํ
์ด๋ธ์ ๋ํ ์คํค๋ง๋ฅผ ๋ง๋ค์ด ์ฃผ๋ฉด ๋ฉ๋๋ค.
๋ค์์ posts ํ ์ด๋ธ์ ๋ํด ๊ธฐ์ ํ ๋ถ๋ถ์ ๋๋ค.
module.exports = (sequelize, DataTypes) => {
// TABLE NAME : posts
const Post = sequelize.define(
"Post",
{
title: {
type: DataTypes.STRING(20),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
}
},
{
charset: "utf8",
collate: "utf8_general_ci",
tableName: "posts"
}
);
Post.associate = db => {
db.Post.belongsTo(db.User);
db.Post.hasMany(db.Comment);
db.Post.hasMany(db.Image);
db.Post.belongsToMany(db.Tag, { through: "PostTag" });
db.Post.belongsToMany(db.User, { through: "Like", as: "Liker" });
};
return Post;
};const express = require("express");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const expressSession = require("express-session");
const { ApolloServer, gql } = require("apollo-server-express");
const typeDefs = require("./graphql/schema");
const resolvers = require("./graphql/resolvers");
const db = require("./models");
const dotenv = require("dotenv");
const morgan = require("morgan");
// Express App Init
const app = express();
dotenv.config();
// Apollo Server Init
const server = new ApolloServer({
typeDefs: gql(typeDefs),
resolvers,
context: { db }
});
// Express Environment Setting
app.use(morgan("dev"));
app.use(express.json());
// JSONํํ์ ๋ณธ๋ฌธ์ ์ฒ๋ฆฌํ๋ express ๋ฏธ๋ค์จ์ด
app.use(express.urlencoded({ extended: true }));
// FORM์ ์ฒ๋ฆฌํด์ฃผ๋ express ๋ฏธ๋ค์จ์ด
app.use(
cors({
origin: true,
credentials: true
})
);
app.use("/", express.static("public"));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
expressSession({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false
},
name: "..."
})
);
//Database Init
db.sequelize.sync();
// Express API
server.applyMiddleware({ app, path: "/graphql" });
//Starting Express App
app.listen({ port: 8000 }, () => {
console.log("Apollo Server on ...");
});apollo-server-testing ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ JEST๋ฅผ ์ด์ฉํด Apollo-Server๋ฅผ ํ ์คํธ ํ๋ ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ๋ค.
๋จผ์ , GraphQL์ typeDefs์ schema๋ฅผ ๋ฃ์ testServer๋ฅผ ์์ฑ
const { ApolloServer } = require("apollo-server");
const graphqlConfig = require("../graphql");
const db = require("../models");
const baseContext = {
req: {
user: null,
login: (user, cb) => {
this.user = user;
baseContext.user = user;
cb();
},
logout: () => {
baseContext.user = null;
this.user = null;
}
},
db,
user: null
};
module.exports = {
testServer: new ApolloServer({
...graphqlConfig,
context: baseContext
}),
baseContext
};context์ express์ ์ฐ๋๋ ๋ถ๋ถ์ ๋ฃ๋ ๊ฒ ๋ณด๋จ Mock์ ๊ตฌํํด๋ณผ ๊ฒธ passport.js ๊ธฐ๋ฅ๊ณผ req๋ฅผ ๊ตฌํํ์ฌ basecontext์ ๋ฃ์ด์ฃผ์๋ค.
์ด ํ ๋ค์๊ณผ ๊ฐ์ด testClient๋ฅผ ์์ฑํด์ค๋๋ค.
const { testServer } = require("./mockTestServer");
const { gql } = require("apollo-server-express");
const { createTestClient } = require("apollo-server-testing");
const dotenv = require("dotenv");
dotenv.config();
const { query, mutate } = createTestClient(testServer);์์์ ์์ฑ ๋ query์ mutate์ JEST์ ํจ์๋ฅผ ์ด์ฉํด ๋ค์๊ณผ ๊ฐ์ด Apollo-Server๋ฅผ ํ ์คํธ ํ ์ ์์ต๋๋ค.
describe('ํ
์คํธ ๊ทธ๋ฃน ๋จ์', () => {
it('์ ์ ๋ฅผ ์์ฑํ๋ ํ
์คํธ', async () => {
const CREATE_USER = gql`
mutation($userId: String!, $password: String!, $grant: Int!, $nickname: String!) {
createUser(userId: $userId, password: $password, grant: $grant, nickname: $nickname) {
id
userId
nickname
grant
}
}
`;
const { data: { createUser } } = await mutate({
mutation: CREATE_USER,
variables: testUser
});
const { userId, grant, nickname } = testUser;
testUser['id'] = createUser.id;
expect(createUser).toEqual({ id: createUser.id, userId, grant, nickname });
});
...2019๋
๋ ์น ๊ฐ๋ฐ ํธ๋ ๋์ธ Graph QL์ ์๋ฒ์ธก๊ณผ ํด๋ผ์ด์ธํธ ์ธก ๋ชจ๋์์ ์ฌ์ฉํด ๋ณด์๋ค.
์ด๋ฅผ ์ฌ์ฉํด ๋ณธ ํ ๋๋์ ์ GraphQL์ด ๋ค์ํ ํ๋ซํผ๊ณผ ๋ค์ํ ์๋ฒ ํ๊ฒฝ์์์ ๋ฐ์ดํฐ ํต์ ์ ์ง์ํ๋ฉฐ API ํธ์ถ๊ฐ ๋ถํ์ ํ ์ค๋ฒ ํจ์น์ ์ธ๋ ํจ์น๋ฅผ ๋ฐฉ์งํด์ฃผ๋ ์ ์ฉํ ๊ธฐ์ ์คํ์ด๋ผ๋ ๊ฒ์ ์์๋ค.
์ผ๋จ ๊ทธ๋ํ ํ์์ ํฐ ์ฅ์ ๋ฐ ํน์ง์ ๋ค์๊ณผ ๊ฐ๋ค.
- ๋ค์ํ ํ๊ฒฝ์ ๋ฐ์ดํฐ๋ฅผ ์ง์ฝํ ์ ์๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ ๊ฑฐ์ API, ํ์ผ ์์คํ ๋ฑ ๋ค์ํ ํ๊ฒฝ์ ์ ๊ทผํ ์ ์๋ค.
- ๋ค์ํ ํ๋ซํผ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ์ ์๋ค. ๋ชจ๋ฐ์ผ(IOS, Android), ์น, IOT ์ ์๊ธฐ๊ธฐ ๋ฑ ๋ค์ํ ํ๋ซํผ์ ๋์ ๊ฐ๋ฅํ๋ค
- ๋ฐ์ดํฐ๋ฅผ ์ง์์ด ํ์์ผ๋ก ๊ตํํ์ฌ ๋ถํ์ํ ์ธ๋ ํจ์น์ ์ค๋ฒ ํจ์น๋ฅผ ๋ฐฉ์งํ ์ ์๋ค.
๋ค๋ง, GraphQL์ ํตํ ์ธ์ฆ(Authentication) ๋ฑ ๊ธฐํ ๋ช๋ช ๋ถ๋ถ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๊ณ ํ๋ค. ๋ฐ๋ผ์ ํ๋์ ๊ธฐ์ ์คํ์ผ๋ก ์๊ฐํ๊ณ ๊ธฐ์กด์ REST API์ ๊ฐ์ด ํผํฉํด์ ์ฌ์ฉํ๋ฉด ๋ฐฑ์๋์ ํ๋ก ํธ์๋๊ฐ์ ์์ ํจ์จ์ ํฅ์์ํค๊ณ ๋ค์ํ ํ๋ซํผ์ ์์ฝ๊ฒ ์ ์ฉํ ์๋ฃจ์ ์ผ ๊ฒ์ด๋ผ ์๊ฐํ๋ค.
ํด๋น ํ๋ก์ ํธ์์ Server๋ฅผ ๋ง๋ค๊ณ ํ
์คํธ ํ ๋๋ง๋ค ๋ก๊ทธ์ธ ํ๊ณ , Graph QL ์ฟผ๋ฆฌ๋ฅผ ํ๋ ์ด ๊ทธ๋ผ์ด๋์์ ์ง์ ์์ฑํ๋ ๊ฒ์ด ๋ฒ๊ฑฐ๋ก์์ ๋๊ผ๋ค.
์ด๋ฐ ๋ถํธํจ์ ํด๊ฒฐํ๊ธฐ ์ํด Apollo-Server๋ฅผ ํ
์คํ
ํ๋ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด์๊ณ 2019๋
ํ
์คํ
ํ๋ ์์คํฌ๋ก ํ๋๊ฐ ๋๊ณ ์๋ JEST๋ฅผ ์ฌ์ฉํด๋ณด๊ธฐ๋กํ๋ค.
์ด๋ฅผ ํตํด ํ
์คํธ์ ๋ํ 3๊ฐ์ง ํน์ฑ์ ์์๋ค.
- ํ ์คํธ๋ฅผ ์ํ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์๋ ๋ฐ๋์ ์๊ฐ(๋น์ฉ)์ด ์๋ชจ๋๋ค.
- ํตํฉ ํ ์คํธ ์ฝ๋(Integration Testing)์ ์ ์ฒด ๊ธฐ๋ฅ์ ๋น ๋ฅด๊ฒ ํ ์คํธ ํ ์ ์์ง๋ง ์ฐ๊ฒฐ์ด ๋ง์ ๋งํผ ์์ฑ์ ๋งค๋๋ฝ๊ฒ ํ๊ธฐ ์ด๋ ต๋ค.
- ๋จ์ ํ ์คํธ(Unit Testing)์ ๋ค๋ฅธ ์ฐ๊ฒฐ๊ณผ์ ๋ฌธ์ ๋ก ํ ์คํธ๊ฐ ์ด๋ ค์ธ ๋ Mocking์ด ํ์ํ๋ค.
ํ ์คํธ์ ๋ํ ์์ ํน์ฑ์ ํตํด ๋๋ ๋ค์๊ณผ ๊ฐ์ด ํ ์คํธ์ ์ฅ๋จ์ ์ ๋ํด ์๊ฒ ๋์๋ค.
| ๊ตฌ๋ถ | ์ค๋ช |
|---|---|
| ์ฅ์ | - ์์ฑ๋ ํ
์คํธ ์ฝ๋๋ฅผ ํตํด ๊ฐ๋ฐ๋ ์ฝ๋๋ฅผ ์์ ํด์ผํ ๋ ๋ฐ๋ณต๋ ํ
์คํธ ์ ์๋ฅผ ์๋ํ ํ ์ ์๋ค.(๋น์ฉ์ ๊ฐ) - ํ์ํ ๋์์ ๋ํ ๊ธฐ๋ฅ ์ฌ์ ์ ๋ฐ ์์ ์ ์ธ ์ฝ๋ ์์ฑ(์์ ์ฑ) |
| ๋จ์ | - ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ํด ์๋ชจ๋๋ ์๊ฐ์ด ๋ง๋ค.(์์ฐ์ฑ ํ๋ฝ) - ํ ์คํธ ์ฝ๋๋ฅผ ํ ์คํธ ํ๊ธฐ ์ํ ๋ณต์กํ ๊ณผ์ ์ด ์๊ธด๋ค. |
์ด๋ฅผ ํตํด ํ ์คํธ์ ํ์์ฑ์ ๋๊ผ์ง๋ง, ์ํฉ์ ๋ฐ๋ผ ํ ์คํธ๋ฅผ ๋์ ํ ์ง ๋ง์ง์ ๋ํ ์๊ฐ์ ํ๊ฒ๋์๋ค.
GraphQL Codegen is the paser which is translate between schema.graphql and native language. We can get some types for tpyescript to use graphql-codegen.
$yarn add -D @graphql-codegen/cli
$yarn add -D @graphql-codegen/typescriptAnd we need to write some script code like this.
{
"scripts": {
"codegen:start": "graphql-codegen graphql-codegen --config codegen.yml",
"codegen:init": "graphql-codegen init"
}
}Also, we need configuration for graphql-codegne codegen.yml
overwrite: true
schema: "./graphql/schema/schema.graphql"
documents: null
generates:
src/types/graphql.ts:
config:
contextType: ../context #MyContext
plugins:
- "typescript"
- "typescript-resolvers"