미래 지향적인 웹 앱 구축: The Codest의 전문가 팀이 제공하는 인사이트
The Codest가 최첨단 기술로 확장 가능한 대화형 웹 애플리케이션을 제작하고 모든 플랫폼에서 원활한 사용자 경험을 제공하는 데 탁월한 성능을 발휘하는 방법을 알아보세요. Adobe의 전문성이 어떻게 디지털 혁신과 비즈니스를 촉진하는지 알아보세요...
목표 초기 설정 종속성 설치 시작하기 먼저 메인 디렉터리에 tsconfig.json을 추가합니다: 이제 서버 구현을 위한 src/server.ts를 만들어 보겠습니다. 그런 다음 로컬 서버용 함수와 람다용 함수 두 개를 추가합니다. 이제 리졸버나 타입 정의가 없으므로 몇 가지를 만들어야 합니다. 처음에는 [...]를 원한다고 가정해 봅시다.
npm init -y
npm install --save typescript graphql aws-lambda @types/aws-lambda
먼저 tsconfig.json
를 기본 디렉토리에 추가합니다:
{
"컴파일러옵션": {
"target": "es5",
"module": "commonjs",
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/*.ts", "src/**/*.ts", "src/**/*.js"],
"exclude": ["node_modules"]
}
이제 다음을 만들어 보겠습니다. src/server.ts
를 서버 구현에 추가합니다. 그런 다음 로컬 서버용 함수와 람다용 함수 두 개를 추가합니다.
// src/server.ts
"apollo-server-lambda"에서 { ApolloServer를 ApolloServerLambda로 }를 가져옵니다;
"apollo-server"에서 { ApolloServer }를 가져옵니다;
const createLambdaServer = () => =>.
new ApolloServerLambda({
typeDefs,
resolvers,
introspection: true,
playground: true,
},
});
const createLocalServer = () => =>.
new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
},
});
export { createLambdaServer, createLocalServer };
리졸버나 유형 정의가 없으므로 몇 가지를 만들어야 합니다. 먼저 사용자를 생성하고 사용자에 대한 정보를 수신한다고 가정해 보겠습니다.
// src/schemas.ts
const { gql } = require("apollo-server-lambda");
const userSchema = gql`
유형 사용자 {
id: ID!
email: String!
name: 문자열!
}
유형 Query {
user(id: ID!): User!
}
유형 Mutation {
createUser(이름: 문자열!, 이메일: 문자열!, 비밀번호: 문자열!): User!
}
`;
이 기능이 익숙하지 않으시다면 아폴로는 아주 멋진 튜토리얼을 준비했습니다.
이제 하나의 쿼리와 하나의 변형을 가진 사용자 확인자를 만들어 보겠습니다.
// src/resolvers.ts
const userResolver = {
Query: {
user: async (parent, args, context, info) => {
{...}
},
},
Mutation: {
createUser: async (parent, args, context, info) => {
{...}
},
},
};
하지만 데이터가 없네요... 해결해 봅시다 😉
유형 정의와 리졸버를 서버로 가져오는 것을 잊지 마세요.
// src/server.ts
"apollo-server-lambda"에서 { ApolloServer를 ApolloServerLambda로 }를 가져옵니다;
"apollo-server"에서 { ApolloServer }를 가져옵니다;
"./schemas"에서 { typeDefs }를 가져옵니다;
"./resolvers"에서 { resolvers }를 가져옵니다;
{...}
이제 데이터베이스와 연결할 수 있는 좋은 시기입니다. 이 특별한 경우에는 MongoDB를 사용하겠습니다. 무료이며 유지 관리가 쉽습니다. 하지만 그 전에 두 가지 종속성을 더 설치해 보겠습니다:
npm 설치 --save 몽구스 닷텐브 저장
첫 번째 단계는 사용자 모델을 만드는 것입니다.
// src/model.ts
"몽구스"에서 mongoose, { 문서, 오류, 스키마 }를 가져옵니다;
내보내기 유형 사용자 = 문서 & {
_id: 문자열,
이메일: 문자열
이름: 문자열
password: 문자열
};
삭제 몽구스.연결.모델["사용자"];
const UserSchema: Schema = new Schema({
email: {
type: String,
required: true,
unique: true,
},
이름: {
type: 문자열,
required: true,
minLength: 3,
maxLength: 32,
},
password: {
type: 문자열,
required: true,
},
});
export const userModel = mongoose.model ("User", UserSchema);
보안이 우선입니다! 이제 비밀번호를 해싱하여 보안을 강화해 보겠습니다.
npm install --save bcrypt @types/bcrypt
이제 미들웨어 전 콜백 내에서 비밀번호 보안을 구현합니다. 사전 미들웨어 함수는 각 미들웨어가 다음 미들웨어를 호출할 때 차례로 실행됩니다. 비밀번호를 안전하게 보호하기 위해 별도의 함수 호출에 대해 솔트와 해시를 생성하는 기술을 사용하고 있습니다.
// src/model.ts
"bcrypt"에서 bcrypt를 임포트합니다;
{...}
const SALT_WORK_FACTOR: number = 10;
UserSchema.pre("save", function (next) { {
const user = this as User;
if (!this.isModified("password")) return next();
bcrypt.genSalt(SALT_WORK_FACTOR, function (err, salt) {
if (err) return next(err);
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next();
});
});
});
{...}
그런 다음 UserSchema에 비교 비밀번호 메서드를 추가합니다:
// src/model.ts
{...}
UserSchema.methods.comparePasswords = 함수 (
후보 비밀번호: 문자열
cb: (err: 오류 | null, same: boolean | null) => void
) {
const user = this as User;
bcrypt.compare(candidatePassword, user.password, (err, isMatch) => {
if (err) {
반환 cb(err, null);
}
cb(null, isMatch);
});
};
{...}
물론 사용자 유형도 수정합니다.
유형 비교 비밀번호 함수 = (
후보 비밀번호: 문자열,
cb: (err: 오류, isMatch: boolean) => void
) => void;
내보내기 유형 사용자 = 문서 & {
_id: 문자열,
이메일: 문자열
이름: 문자열
password: 문자열,
비교 비밀번호: 비교 비밀번호 함수,
};
이제 서버와 데이터베이스 간의 연결을 준비할 수 있습니다. MONGODB_URI
는 연결을 만드는 데 필요한 연결 문자열이 포함된 환경 변수입니다. 몽고DB 아틀라스 계정에 로그인한 후 클러스터 패널에서 가져올 수 있습니다. 안에 넣기 .env
// .env
mongodb_uri = ...;
해당 파일을 항상 다음 위치에 추가하는 것을 잊지 마십시오. .gitignore
. 훌륭합니다! 이제 DB와 연결할 수 있는 함수를 추가해 보겠습니다.
// src/server.ts
"몽구스"에서 몽구스, { 연결 }을 가져옵니다;
{...}
let cachedDb: Connection;
const connectToDatabase = async () => {
if (cachedDb) return;
await mongoose.connect(process.env.MONGODB_URI || "", {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
useCreateIndex: true,
});
cachedDb = mongoose.connection;
};
{...}
컨텍스트는 모든 리졸버에서 공유되는 객체입니다. 이를 제공하려면 ApolloServer 생성자에 컨텍스트 초기화 함수를 추가하기만 하면 됩니다. 해봅시다.
// src/server.ts
"./models/user.model"에서 { userModel }을 임포트합니다;
{...}
const createLambdaServer = async () => =>.
new ApolloServerLambda({
typeDefs,
resolvers,
introspection: true,
playground: true,
context: async () => {
await connectToDatabase();
반환 {
models: {
userModel,
},
};
},
});
const createLocalServer = () => =>.
new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
context: async () => {
await connectToDatabase();
반환 {
models: {
userModel,
},
};
}
});
보시다시피, 저희는 또한 사용자 모델
통해 컨텍스트
. 이제 리졸버를 업데이트할 수 있습니다:
// resolvers.ts
const userResolver = {
Query: {
user: async (_, { email, name, password }, { models: { userModel } }) => {
const user = await userModel.findById({ _id: id }).exec();
return user;
},
},
Mutation: {
createUser: async (_, { id }, { models: { userModel } }) => {
const user = await userModel.create({ 이메일, 이름, 비밀번호 });
반환 사용자;
},
},
};
멋지네요! 이제 기본 서버 인스턴스를 생성합니다:
// src/index.ts
"./서버"에서 { createLocalServer }를 가져옵니다;
require("dotenv").config();
const port = process.env.PORT || 4000;
const server = createLocalServer();
server.listen(port).then(({ url }) => {
console.log(`${url}에서 실행 중인 서버`);
});
실행하기 전에 마지막으로 nodemon.json
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "ts-node --transpile-only ./src/index.ts"
}
다음에 스크립트 추가 package.json
"스크립트": {
"start": "nodemon"
},
그리고 달려!
npm 시작
터미널에서 이러한 정보를 얻을 수 있습니다:
http://localhost:4000/ 에서 실행 중인 서버
그렇다면 http://localhost:4000/
를 클릭합니다.
GraphQL 플레이그라운드가 나타납니다. 새 사용자를 만들어 보겠습니다!
데이터베이스에서 어떻게 보이는지 살펴보세요.
멋지다! 모든 것이 잘 작동합니다!
사용자 정보를 가져와 보겠습니다.
그리고 비밀번호 필드를 추가합니다...
Nice! 오류가 발생했습니다. "사용자" 유형에서 "비밀번호" 필드를 쿼리할 수 없습니다."
. 보시다시피 사용자 유형 정의 내에 이 필드를 추가하지 않았습니다. 일부러 넣은 것입니다. 비밀번호나 기타 민감한 데이터를 쿼리할 수 있도록 해서는 안 됩니다.
또 한 가지... 인증 없이 사용자 데이터를 얻을 수 있다는 것은 좋은 해결책이 아닙니다. 우리는 그것을 고쳐야 합니다.
하지만 그 전에는...
GraphQL을 사용해 보겠습니다. 코드 생성기를 사용하여 스키마에 따라 호환되는 기본 유형을 가져옵니다.
npm install --save @graphql-codegen/cli @graphql-codegen/introspection
그래프 작성 @그래프 작성 @그래프 작성 @그래프 작성 @그래프 작성 리졸버
만들기 codegen.yml
덮어쓰기: true
스키마: "http://localhost:4000"
생성합니다:
./src/generated/graphql.ts:
플러그인
- "typescript"
- "typescript-resolvers"
./graphql.schema.json:
플러그인
- "introspection"
추가 코드젠
스크립트를 package.json
"스크립트": {
"start": "노데몬",
"codegen": "graphql-codegen --config ./codegen.yml",
},
그런 다음 로컬 서버가 실행 중일 때 스크립트를 실행합니다:
npm 실행 코드젠
성공할 경우 메시지를 받게 됩니다:
√ 구문 분석 구성
√ 출력 생성
해당 정보를 받으면 두 개의 파일이 나타납니다:
graphql.schema.json
메인 디렉토리에graphql.ts
새로 생성된 경로에 src/generated
저희는 두 번째에 더 관심이 있습니다. 열면 멋진 유형 구조를 확인할 수 있습니다.
이제 리졸버를 개선할 수 있습니다:
// src/resolvers.ts
"./generated/graphql"에서 { 해석기, 토큰, 사용자 }를 임포트합니다;
const userResolver: Resolvers = {
Query: {
user: async (_, { id }, { models: { userModel }, auth }): Promise => {
const user = await userModel.findById({ _id: id }).exec();
return user;
},
},
Mutation: {
createUser: async (
_,
{ 이메일, 이름, 비밀번호 },
{ models: { userModel } }
): Promise => {
const user = await userModel.create({
이메일
name,
password,
});
반환 사용자;
},
},
};
다음으로 간단한 토큰 기반 인증을 설정해 보겠습니다.
npm install --save jsonwebtoken @types/jsonwebtoken
만들기 checkAuth
함수를 호출하여 토큰이 유효한지 확인합니다. 결과를 컨텍스트에 추가하여 리졸버 내부에서 액세스할 수 있도록 합니다.
// src/server.ts
import { IncomingHttpHeaders } from "http";
import {
APIGatewayProxyEvent,
APIGatewayProxyEventHeaders,
Context,
} from "aws-lambda";
import jwt from "jsonwebtoken";
{...}
const checkAuth = async ({ token } : APIGatewayProxyEventHeaders | IncomingHttpHeaders ) => {
if (typeof token === "string") {
try {
return await jwt.verify(token, "riddlemethis");
} catch (e) {
throw new AuthenticationError(`Your session expired. Sign in again.`);
}
}
};
{...}
const createLambdaServer = async (
{ headers }: APIGatewayProxyEvent,
context: Context
) => {
return new ApolloServerLambda({
typeDefs,
resolvers,
context: async () => {
await connectToDatabase();
const auth = await checkAuth(headers);
return {
auth,
models: {
userModel,
},
};
},
});
};
function createLocalServer() {
return new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
context: async ({ req: { headers } = {} }) => {
const auth = await checkAuth(headers);
await connectToDatabase();
return {
auth,
models: {
userModel,
},
};
},
});
}
또한 이러한 토큰을 생성하는 방법이 필요합니다. 가장 좋은 방법은 리졸버 내부에 로그인 쿼리를 구현하는 것입니다.
// resolvers.ts
"./generated/graphql"에서 { 해석기, 토큰, 사용자 }를 가져옵니다;
const userResolver: Resolvers = {
Query: {
user: async (_, { id }, { models: { userModel }, auth }): Promise => {
if (!auth) throw new AuthenticationError("인증되지 않았습니다");
const user = await userModel.findById({ _id: id }).exec();
return user;
},
login: async (
_,
{ 이메일, 비밀번호 },
{ models: { userModel } }
): Promise => {
const user = await userModel.findOne({ email }).exec();
if (!user) throw new AuthenticationError("유효하지 않은 자격 증명");
const matchPasswords = bcrypt.compareSync(password, user.password);
if (!matchPasswords) throw new AuthenticationError("잘못된 자격증명");
const token = jwt.sign({ id: user.id }, "riddlemethis", {
expiresIn: 60,
});
반환 { 토큰 };
},
},
Mutation: {
createUser: async (
_,
{ 이메일, 이름, 비밀번호 },
{ models: { userModel } }
): Promise => {
const user = await userModel.create({
이메일
name,
password,
});
반환 사용자;
},
},
};
토큰 유형 및 로그인 쿼리별로 사용자 유형 정의도 업데이트해야 합니다.
유형 토큰 {
토큰: 문자열!
}
타입 쿼리 {
user(id: ID!): User!
login(이메일: 문자열!, 비밀번호: 문자열!): Token!
}
이제 토큰 없이 사용자를 확보해 보겠습니다.
좋아요, 잘 작동합니다! 로그인을 시도해 보세요.
이번에는 헤더에 토큰을 추가하여 사용자를 다시 가져와 보겠습니다.
멋지네요! 자격 증명을 잘못 설정하면 어떻게 되나요?
멋지네요!
마지막 단계: Netlify로 서버리스 API 배포!
폴더 만들기 람다
를 메인 디렉토리에 넣고 그 안에 두 개의 파일을 넣습니다:
첫 번째는 AWS 핸들러를 포함합니다. ApolloServerLambda 서버 인스턴스를 생성하고
를 호출한 다음 해당 인스턴스의 createHandler를 사용하여 핸들러를 노출합니다.
// lambda/graphql.ts
"aws-lambda"에서 { APIGatewayProxyEvent, Context }를 가져옵니다;
"???"에서 { createLambdaServer }를 가져옵니다;
export const handler = async (
event: APIGatewayProxyEvent,
context: 컨텍스트
) => {
const server = await createLambdaServer(event, context);
return new Promise((res, rej) => {
const cb = (err: 오류, args: any) => (err ? rej(err) : res(args));
server.createHandler()(event, context, cb);
});
};
두 번째는 tsconfig입니다. 중요한 부분은 outDir
필드에 입력합니다.
// lambda/tsconfig.json
{
"컴파일러옵션": {
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es6",
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "노드",
"skipLibCheck": true,
"esModuleInterop": true,
"outDir": "./dist"
},
"include": ["./*.ts", "./**/*.ts", "./**/*.js"]
}
하지만 문제가 있습니다. 사용할 수 없습니다. /src
dir로 이동해야 합니다. 왜냐하면 Netlify는 람다 폴더 외부에 도달할 수 없기 때문입니다. 따라서 번들로 묶어야 합니다.
npm 설치 --save ncp
디렉토리를 복사할 수 있는 패키지입니다.
추가 번들
스크립트를 package.json
"스크립트": {
"번들": "ncp ./src ./lambda/bundle",
"codegen": "graphql-codegen --config ./codegen.yml",
"start": "nodemon",
},
이제 다음을 실행하면 npm 실행 번들
새로 생성된 람다/번들
디렉토리의 모든 파일이 있습니다. src/
.
내부에서 가져오기 경로 업데이트 lambda/graphql.ts
// lambda/graphql.ts
"aws-lambda"에서 { APIGatewayProxyEvent, Context }를 가져옵니다;
"./bundle/server"에서 { createLambdaServer }를 가져옵니다;
{...}
이제 람다/번들 디렉터리를 다음 위치에 추가할 수 있습니다. .gitignore
빌드 명령이 무엇인지, 함수가 어디에 있는지 Netlify에 알려줘야 합니다. 이를 위해 netlify.toml
file:
// netlify.toml
[빌드]
명령 = "npm 실행 빌드:람다"
함수 = "lambda/dist"
보시다시피, 이 디렉토리는 outDir
필드에 lambda/tsconfig.json
앱 구조는 다음과 같아야 합니다(음... 일부분 ;)).
앱
└───람다
│ └────번들
│ │ graphql.ts
│ tsconfig.json.ts
└───src
│ └───-generated
│ │ │ graphql.ts
│ │ index.ts
│ │ model.ts
│ │ resolvers.ts
│ │ schemas.ts
│ server.ts
│
│ codegen.yml
│ graphql.schema.json
│ netlify.toml
│ nodemon.json
│ tsconfig.json
추가 번들:람다
스크립트를 package.json
"스크립트": {
"빌드:람다": "npm 실행 번들 && tsc -p lambda/tsconfig.json",
"번들": "ncp ./src ./lambda/bundle",
"codegen": "graphql-codegen --config ./codegen.yml",
"start": "nodemon",
},
Netlify를 통해 앱을 배포해 보겠습니다.
그리고 TAAADAAAA...
이는 엔드포인트가 기본적으로 http://page/
하지만 http://page/.netlify/functions/graphql
.
어떻게 해결하나요? 매우 간단합니다. 그냥 _redirect
와 함께:
/ /.netlify/functions/graphql 200!
다시 배포하고 확인합니다.
마음에 드셨기를 바랍니다! 자유롭게 개선하고 변경해 주세요.
자세히 읽어보세요: