제품 품질 저하 없이 개발팀을 확장하는 방법
개발팀을 확장하고 계신가요? 제품 품질을 저하시키지 않고 성장하는 방법을 알아보세요. 이 가이드에서는 확장할 시기의 징후, 팀 구조, 채용, 리더십 및 도구와 더불어 The Codest가 어떻게...
목표 초기 설정 종속성 설치 시작하기 먼저 메인 디렉터리에 tsconfig.json을 추가합니다: 이제 서버 구현을 위한 src/server.ts를 만들어 보겠습니다. 그런 다음 로컬 서버용 함수와 람다용 함수 두 개를 추가합니다. 이제 리졸버나 타입 정의가 없으므로 몇 가지를 만들어야 합니다. 처음에는 [...]를 원한다고 가정해 봅시다.
npm init -ynpm 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!다시 배포하고 확인합니다.

마음에 드셨기를 바랍니다! 자유롭게 개선하고 변경해 주세요.
자세히 읽어보세요: