Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

미래학자

우버 클론 코딩 (nomad coders) #11 본문

nomad corders

우버 클론 코딩 (nomad coders) #11

미래학자 2019. 4. 28. 09:13

프로젝트를 시작할 때 tslint가 정상적으로 돌고 있지 않았는데, 원인이 tslint를 글로벌로 설치를 하고 typescript또한 글로벌로 설치를 해야 했었다. 이런 버그를 수정하고 나니 tslint가 import 순서를 지적 많이 해줘서 부득이하게 #1.62 부터 tslint에서 잡아준 import 순서대로 코드를 올리게 됐다.

이번에는 Place 정보를 추가하고 삭제하고 변경하는 과정을 진행할 예정이다.

#1.60 AddPlace Resolver

이번에는 장소를 추가하는 type과 Mutation을 정의할 차례다. 한 사람은 여러 장소를 가질 수 있다. 주소가 같은 장소래도 사용자마다 독립적으로 가지는 형태인거 같다. 기존에는 장소와 유저와의 관계가 없었기 때문에 추가한다.

  • src/api/User/shared/User.graphql 필드에 places를 추가 하자.

      ...
      ridesAsDriver: [Ride]
      places: [Place]
      isDriving: Boolean!
      ...
  • src/api/Place/shared/Place.graphql 필드에 user를 추가하자.

      ...
      isFav: Boolean!
      user: User!
      createAt: String!
      ...
  • src/entities/Place.ts

      ...
        Entity,
          ManyToOne,
        PrimaryGeneratedColumn,
        UpdateDateColumn,
       } from 'typeorm'
      import User from './User'
    
      ...
      @Column({ type: "boolean"})
      isFav: boolean;

      @ManyToOne(type => User, user => user.places)
      user: User

      @CreateDateColumn() createAt: string;
      @UpdateDateColumn() updateAt: string;
    }

     export default Place;
  • src/entities/User.ts

      ...
      import Ride from './Ride';
      import Place from './Place';
    
      ...
    
        @OneToMany(type => Ride, ride => ride.driver)
        ridesAsDriver: Ride[];
    
        @OneToMany(type => Place, place => place.user)
        places: Place[];
    
        @Column({ type: "boolean", default: false})
        isDriving: boolean;
      ...

entities에 관계를 추가했다.

  • src/api/Place/AddPlace/AddPlace.graphql

      type AddPlaceResponse {
        ok: Boolean!
        error: String
      }
    
      type Mutation {
        AddPlace(
          name: String!
          lat: Float!
          lng: Float!
          address: String!
          isFav: Boolean!
        ): AddPlaceResponse!
      }
  • src/api/Place/AddPlace/AddPlace.resolvers.ts

      import { Resolvers } from "src/types/resolvers";
      import { AddPlaceMutationArgs, AddPlaceResponse } from "src/types/graph";
      import privateResolver from "../../../utils/privateResolver";
      import Place from "../../../entities/Place";
      import User from "../../../entities/User";
    
      const resolvers: Resolvers = {
        Mutation: {
          AddPlace: privateResolver(async (
            _, 
            args: AddPlaceMutationArgs, 
            { req }
          ) : Promise<AddPlaceResponse> => {
            const user: User = req.user;
            try {
              await Place.create({ ...args, user }).save();
              return {
                ok: true,
                error: null
              }
            } catch(error) {
              return {
                ok: false,
                error: error.message
              }
            } 
          })
        }
      };
    
      export default resolvers;

장소에 대한 정보를 추가하는 것은 간단하게 끝났다.

#1.61 EditPlace Resolver

Place에 대한 정보를 변경할 필요가 있다.

  • src/api/Place/EditPlace/EditPlace.graphql

      type EditPlaceResponse {
        ok: Boolean!
        error: String
      }
    
      type Mutation {
        EditPlace(
          placeId: Int!,
          name: String,
          isFav: Boolean
        ): EditPlaceResponse!
      }
  • src/api/Place/EditPlace/EditPlace.resolvers.ts

      import { Resolvers } from "src/types/resolvers";
      import { EditPlaceMutationArgs, EditPlaceResponse } from "src/types/graph";
      import privateResolver from "../../../utils/privateResolver";
      import cleanNullArgs from "../../../utils/cleanNullArgs";
      import Place from "../../../entities/Place";
      import User from "../../../entities/User";
    
      const resolvers: Resolvers = {
        Mutation: {
          EditPlace: privateResolver(async (
            _, 
            args : EditPlaceMutationArgs, 
            { req }
          ) : Promise<EditPlaceResponse> => {
            const user: User = req.user;
            try {
              const place = await Place.findOne({id: args.placeId}, { relations: ["user"] });
              if(place) {
                if(place.user.id === user.id) {
                  const notNull: any = cleanNullArgs(args);
                              delete notNull.placeId;
                  await Place.update({ id: args.placeId }, { ...notNull });
                  return {
                    ok: true,
                    error: null
                  }
                } else {
                  return {
                    ok: false,
                    error: 'Not Authorized'
                  }
                }
              } else {
                return {
                  ok: false,
                  error: 'Place not found'
                }
              }
            } catch (error) {
              return {
                ok: true,
                error: error.message
              }
            }
          })
        }
      }
    
      export default resolvers;

작성한 코드를 살펴보자. 처음 보는 표현이 있다.

const place = await Place.findOne({id: args.placeId}, { relations: ["user"] });

place entity는 user에 관계되어 있다. 근데 조회할 때 필요하지 않은데 모두 가져와 버리면 DB의 성능 문제가 발생할 것이다. typeorm에서는 가져올 관계가 있는 데이터를relations에 배열 형태로 보내서 가져오게 된다. 이런 방식은 일반적인 관계형 DB의 방식이지만 내가 필요한 데이터는 [user.id](http://user.id) 하나 뿐인데 너무 큰 비용이 발생한다고 생각할 수 있다. 그래서 우리는 place entity에 userId 필드를 추가해서 손쉽게 가져올 것이다.

  • src/api/Place/shared/Place.graphql 에 userId 필드를 추가하자.

      ...
        isFav: Boolean!
        userId: Int
        user: User!
      ...
  • src/entities/Place.ts

      ...
          @Column({ type: "boolean"})
        isFav: boolean;
    
        @Column({nullable: true})
        userId: number;
    
        @ManyToOne(type => User, user => user.places)
        user: User
      ...

여기서 typeorm이 하는 멋진일이 있는데, Place를 저장할 때 따로 uesrId에 값을 채울 필요없이 user 에 값을 넣으면 user.id가 userId으로 자동으로 채워진다고 한다. 차이라면 type을 정의하지 않는 거??

이제 relations를 사용하지 않은 코드로 조금 변경하자

  • src/api/Place/EditPlace/EditPlace.resolvers.ts

      ...
            try {
              const place = await Place.findOne({id: args.placeId});
              if(place) {
                if(place.userId === user.id) {
                  const notNull = cleanNullArgs(args);
      ...

강의에서는 언급되지 않지만 내가 겪은 오류가 있었다.

const notNull: any = cleanNullArgs(args);
delete notNull.placeId;
await Place.update({ id: args.placeId }, { ...notNull });

args에는 placeId라는 프로퍼티가 있다. 이 값에 일치하는 id를 가진 Place를 찾는데, 이때

notNull 객체가 placeId를 프로퍼티로 갖는게 문제다. id 를 업데이트 하는 것 자체도 논리적으로 문제지만, Place entity에는 id는 정의되어 있지만 placeId가 정의되어 있지 않다. 그렇기 때문에 오류가 발생해서 placeId는 일치하는 값을 찾는데에만 쓰기 때문에 제거하는 로직을 추가 하였다.

#1.62 DeletePlace Resolver

place 삭제 관련 코드다. 특별히 설명할 것은 없다.

  • src/api/Place/DeletePlace/DeletePlace.graphql

      type DeletePlaceResponse {
        ok: Boolean!
        error: String
      }
    
      type Mutation {
        DeletePlace(placeId: Int!) : DeletePlaceResponse!
      }
  • src/api/Place/DeletePlace/DeletePlace.resolvers.ts

      import { DeletePlaceMutationArgs, DeletePlaceResponse } from "src/types/graph";
      import { Resolvers } from "src/types/resolvers";
      import Place from "../../../entities/Place";
      import User from "../../../entities/User";
      import privateResolver from "../../../utils/privateResolver";
    
      const resolvers: Resolvers = {
        Mutation: {
          DeletePlace: privateResolver(async (
            _, 
            args: DeletePlaceMutationArgs, 
            { req }
          ): Promise<DeletePlaceResponse> => {
            const user: User = req.user;
            try {
              const place = await Place.findOne({ id: args.placeId });
              if(place) {
                if(place.userId === user.id) {
                  place.remove();
                  return {
                    ok: true,
                    error: null
                  }
                } else {
                  return {
                    ok: false,
                    error: 'Not Authorized'
                  }
                }
              } else {
                return {
                  ok: false,
                  error: 'Place not found'
                }
              }
            } catch(error) {
              return {
                ok: false,
                error: error.message
              }
            }
          })
        }
      }
    
      export default resolvers;

#1.63 GetMyPlaces Resolver and Testing

  • src/api/Place/GetMyPlace/GetMyPlace.graphql

      type GetMyPlaceResponse {
        ok: Boolean!
        error: String
        places: [Place]
      }
    
      type Query {
        GetMyPlace: GetMyPlaceResponse!
      }
  • src/api/Place/GetMyPlace/GetMyPlace.resolvers.ts

      import { GetMyPlaceResponse } from "src/types/graph";
      import { Resolvers } from "src/types/resolvers";
      import User from "../../../entities/User";
      import privateResolver from "../../../utils/privateResolver";
    
      const resolvers: Resolvers = {
        Query: {
          GetMyPlace: privateResolver(async (_, __, { req }) : Promise<GetMyPlaceResponse> => {
            try {
              const user: any = await User.findOne(
                { id: req.user.id },
                { relations: ["places"]}
              );
              if(user) {
                return {
                  ok: true,
                  error: null,
                  places: user.places
                }
              } else {
                return {
                  ok: false,
                  error: "User not found",
                  places: null
                }
              }
            } catch(error) {
              return {
                ok: false,
                error: error.message,
                places: null
              }
            }
          })
        }
      }
    
      export default resolvers;

지금까지 작성한 mutation과 query를 테스트 해보자.

http://localhost:4000/playground 에 접근해서 다음의 쿼리를 날려보자. 물론 요청 헤더에 token을 포함하는 것을 잊지 말아야 한다.

query {
  GetMyPlace {
    ok
    error
    places {
      id
      name
      isFav
    }
  }
}

정상적으로 가져오지만 places가 빈 배열일 것이다. 아래에 장소 하나를 추가해보자.

mutation {
  AddPlace(name: "home", lat: 133.1, lng: 32.3, address: "대한민국 서울", isFav: true) {
    ok
    error
  }
}

이 후 다시 GetMyPlace쿼리를 보내면 장소값이 들어있는 것을 확인할 수 있다. 임의로 하나 더 추가 해보자.

mutation {
  AddPlace(name: "work", lat: 9.1, lng: 1.3, address: "대한민국 판교", isFav: false) {
    ok
    error
  }
}

이제 EditPlace를 호출하자. 제일 먼저 추가한 place가 변경될 것이다. GetMyPlace로 변경된 것을 확인하자.

mutation {
  EditPlace(placeId: 1, name: "즐거운 나의집") {
    ok
    error
  }
}

DeletePlace로 호출하자.

mutation {
  DeletePlace(placeId: 2) {
    ok
    error
  }
}

정상적으로 작동 한다.!

Comments