GraphQL과 Apollo 여섯번째 이야기 - GraphQL Module화에 대해 알아보아요 😀

2023. 11. 20. 15:05Back-End 작업실/기타 참고 자료

728x90
반응형

 

 

카카오페이 | 마음 놓고 금융하다

여기를 눌러 링크를 확인하세요.

qr.kakaopay.com

 

 

 

 

 

 

GraphQL과 타입스크립트로 개발하는 웹 서비스:설계부터 개발·배포까지 따라 하며 완성하는 웹 풀

COUPANG

www.coupang.com

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

 




🗂 목차

✅ GraphQL과 Apollo 첫번째 이야기 - 개념 익히기
 GraphQL과 Apollo 두번째 이야기 - REST API란?
 GraphQL과 Apollo 세번째 이야기 - GraphQL의 정보 주고 받는 방식

 GraphQL과 Apollo 네번째 이야기 - Apollo란?
✅ GraphQL과 Apollo 다섯번째 이야기 - GraphQL을 간단하게 구현해 보아요 😀
 GraphQL과 Apollo 여섯번째 이야기 - GraphQL Module화에 대해 알아보아요 😀
 GraphQL과 Apollo 일곱번째 이야기 - GraphQL Data Type에 대해 알아보아요 😀
 GraphQL과 Apollo 여덟번째 이야기 - GraphQL Union과 Interface 그리고 인자와 인풋 타입에 대해 알아보아요 😀
 GraphQL과 Apollo 아홉번째 이야기 - Java + Spring Boot에서 GraphQL 사용해 보기 - 실습 환경 구성
 GraphQL과 Apollo 열번째 이야기 - Java + Spring Boot에서 GraphQL 사용해 보기 - 실습 해보기
✅ GraphQL과 Apollo 열 한번째 이야기 - TypeScript + Nest.js에서 GraphQL 사용해 보기 - 실습 환경 구성
✅ 
GraphQL과 Apollo 열 두번째 이야기 - TypeScript + Nest.js에서 GraphQL 사용해 보기 - 실습 환경 테스트
 GraphQL과 Apollo 열 세번째 이야기 - TypeScript + Nest에서 GraphQL 사용해 보기 - 실습 해보기
 
GraphQL과 Apollo 열 네번째 이야기 - React와 Apollo Client
 GraphQL과 Apollo 열다섯번째 이야기 - React와 Apollo Client - Query와 Mutation 사용하여 웹 페이지 만들기
 GraphQL과 Apollo 열 여섯번째 이야기 - Kotlin + Spring Boot에서 GraphQL 사용해 보기 - 실습 환경 구성
 GraphQL과 Apollo 열 일곱번째 이야기 - Kotlin + Spring Boot에서 GraphQL 사용해 보기 - 실습 해보기


🤔 내가 만난 문제

⚠️ [Nest.js] TypeORM Table 관계가 맺어졌을 때, Seeding (feat. Migration)
⚠️ [Spring Boot 3.0] Could not resolve org.springframework.boot:spring-boot-gradle-plugin
⚠️ [Spring Boot 3] Spring Doc(Swagger) White Label Error


📋 부록

🔍 [Nest.js] 초기 환경 구성 (feat. TypeORM, QueryBuilder, GraphQL, Apollo)
🔍 [SOLID][Nest.js][Java + Spring] Interface를 활용한 결합도 분리 (Interface를 이용한 Dependency Injection - DI)

 

 

 

 

 

🚀 GraphQL과 Apollo 여섯번째 이야기

    🔽 GraphQL Module화에 대해 알아보아요 😀

        📦 개요

지난 다섯번째 이야기에서 GraphQL을 이용하여 서버를 구성할 때, 기본적인 QueryMutation을 어떻게 구성해야 하는지에 대해 알아보았어요.

이번 여섯번째 이야기에서는 GraphQL을 이용하여 서버 구성 시 보다 자세한 기능과 사용법에 대해 공부해 보려고 해요.

 

728x90

 

 

 

 

 

    🔽  서버 구성 요소 모듈화

        📦 살펴보기

const database = require('./database');
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
  type Query {
    teams: [Team],
    team(id: Int): Team,
    equipments: [Equipment],
    supplies: [Supply]
  }
  
  type Mutation {
    insertEquipment (
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    
    updateEquipment (
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    
    deleteEquipment(id: String): Equipment 
  }
   
  type Team {
    id: Int,
    manager: String,
    office: String,
    extension_number: String,
    mascot: String,
    cleaning_duty: String,
    project: String,
    supplies: [Supply]
  }
  
  type Equipment {
    id: String
    used_by: String
    count: Int
    new_or_used: String
  }
  
  type Supply {
    id: String,
    team: Int
  }
`;
const resolvers = {
    Query: {
        teams: () => database.teams.map((team) => {
            team.supplies = database.supplies.filter((supply) => {
                return supply.team === team.id;
            });

            return team;
        }),

        team: (parent, args, context, info) =>
            database.teams.filter((team) => {
                return team.id === args.id;
            }) [0],

        equipments: () => database.equipments,
        supplies: () => database.supplies
    },

    Mutation: {
        insertEquipment: (parent, args, context, info) => {
            database.equipments.push(args);
            return args;
        },

        updateEquipment: (parent, args, context, info) => {
            return database.equipments.filter((equipment) => {
                return equipment.id === args.id;
            }).map((equipment) => {
                Object.assign(equipment, args);
                return equipment;
            }) [0];
        },

        deleteEquipment: (parent, args, context, info) => {
            const deleted = database.equipments.filter((equipment) => {
                return equipment.id === args.id
            })[0]

            database.equipments = database.equipments.filter((equipment) => {
                return equipment.id !== args.id
            })

            return deleted
        }
    }
}

const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
    console.log(`🚀 주니의 Apollo 실습 Server 준비 완료! ${url}`);
})

 

위의 코드는 지난 시간까지 실습해 보았던 index.js에요.
보시다시피 GraphQL의 모든 Type 즉, Schema와 Resolver가 모두 한 파일에 있다는 문제가 있어요.

간단한 실습이라면 크게 문제될 건 없지만, 실제 서비스를 구동하는 서버라면 이처럼 구성하면 유지보수 하기 매우 힘들게 될거에요.

그래서 이번에는 이 내용들을 적절하게 나누어 Module화 하는 방법에 대해 공부해 보려고 해요.

실습에서 사용할 코드는 이 곳에 있어요.

 

3-1-server-modularized · master · Channel Yalco / yalco-inflearn-graphql-apollo · GitLab

GitLab.com

gitlab.com

 

해당 코드는 기본적인 Module화가 이미 진행된 프로젝트에요.

index.js

반응형


최초 해당 프로젝트의 index.js를 살펴 볼게요.

이 프로젝트에서는 18번째 줄에 TypeDef와 Resolver를 받아 new 연산자를 통해 생성자의 매개 변수로 받아 server 객체를 만들게 돼요.
 

const typeDefs = gql`
  type Query {
    teams: [Team],
    team(id: Int): Team,
    equipments: [Equipment],
    supplies: [Supply]
  }
  
  type Mutation {
    insertEquipment (
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    
    updateEquipment (
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    
    deleteEquipment(id: String): Equipment 
  }
   
  type Team {
    id: Int,
    manager: String,
    office: String,
    extension_number: String,
    mascot: String,
    cleaning_duty: String,
    project: String,
    supplies: [Supply]
  }
  
  type Equipment {
    id: String
    used_by: String
    count: Int
    new_or_used: String
  }
  
  type Supply {
    id: String,
    team: Int
  }
`;


지난 시간에 실습했던 index.js를 보면 위와 같이 typeDef 변수가 이렇게 구성된 걸 확인할 수 있고,

const resolvers = {
    Query: {
        teams: () => database.teams.map((team) => {
            team.supplies = database.supplies.filter((supply) => {
                return supply.team === team.id;
            });

            return team;
        }),

        team: (parent, args, context, info) =>
            database.teams.filter((team) => {
                return team.id === args.id;
            }) [0],

        equipments: () => database.equipments,
        supplies: () => database.supplies
    },

    Mutation: {
        insertEquipment: (parent, args, context, info) => {
            database.equipments.push(args);
            return args;
        },

        updateEquipment: (parent, args, context, info) => {
            return database.equipments.filter((equipment) => {
                return equipment.id === args.id;
            }).map((equipment) => {
                Object.assign(equipment, args);
                return equipment;
            }) [0];
        },

        deleteEquipment: (parent, args, context, info) => {
            const deleted = database.equipments.filter((equipment) => {
                return equipment.id === args.id
            })[0]

            database.equipments = database.equipments.filter((equipment) => {
                return equipment.id !== args.id
            })

            return deleted
        }
    }
}


resolver는 위와 같이 만들어진걸 확인할 수 있어요.

이렇게 만든 것은 서버 객체를 만들 때, new 연산자를 이용하여 생성자 매개 변수로 넣어주고, 서버 객체를 만들게 돼요.

출처: https://www.apollographql.com/docs/apollo-server/api/apollo-server/


Apollo 공식 문서를 보면 위와 같이 Server에 대한 메뉴얼을 확인할 수 있고, constructor(생성자)에 대한 내용을 확인할 수 있어요.

출처: https://www.apollographql.com/docs/apollo-server/api/apollo-server/


해당 메뉴얼을 보면 위와 같이 typeDefs와 resolvers를 필수 Option(옵션)으로 받아야 하는 것을 확인할 수 있어요.

먼저 typeDefs 설명을 확인해보면 단수의 DocumentNode 또는 Array<DocumentNode>라고 되어 있는데, 

const typeDefs = gql`
  type Query {
    teams: [Team],
    team(id: Int): Team,
    equipments: [Equipment],
    supplies: [Supply]
  }
  
  type Mutation {
    insertEquipment (
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    
    updateEquipment (
        id: String,
        used_by: String,
        count: Int,
        new_or_used: String
    ): Equipment
    
    deleteEquipment(id: String): Equipment 
  }
   
  type Team {
    id: Int,
    manager: String,
    office: String,
    extension_number: String,
    mascot: String,
    cleaning_duty: String,
    project: String,
    supplies: [Supply]
  }
  
  type Equipment {
    id: String
    used_by: String
    count: Int
    new_or_used: String
  }
  
  type Supply {
    id: String,
    team: Int
  }
`;


위와 같이 작성한 것이 단수형인 DocumentNode 형태이고, 이를 배열로도 만들어줄 수 있는 걸 확인할 수 있어요.

Resolvers 역시 마찬가지에요.
Object 즉, 객체 하나 혹은 Array (배열)을 이용할 수 있다고 나와 있어요.


최초 프로젝트의 typedefs-resolvers Directory(디렉터리)를 살펴보면 _queries.js, _mutaions.js, equipments.js를 볼 수 있는데, 이 프로젝트는 equipments에 대한 Query와 Mutaion만 작성된 프로젝트에요.

equipments.js


위 코드를 분석해 볼게요.
equipment에 대한 Schema(스키마)가 4 ~ 11번째 줄에 명시되어 typeDefs 변수에 들어간 걸 확인할 수 있어요.

12 ~ 19번째 줄에는 resolvers 변수에 이 두 기능을 수행하는 Query와 Mutaion이 한 객체로 선언된 걸 확인할 수 있어요.

그리고, typeDefs와 resovers 변수를 21 ~ 24번째 줄에 Module로 exports(내보내기) 하고 있어요.

index.js


index.js의 6번째 줄에 보면 equipments.js 21번째 줄에 내보내기한 Module을 6번째 줄에서 불러와 equipments 상수 변수에 넣고 있어요.

index.js


그런 뒤 11번째 줄에 해당 변수에서 typeDefs에 대한 값을 8번째 줄에 선언된 typeDefs 상수 배열 변수에 담게 되는 것이에요.

그리고, 14번째 줄 resolvers 상수 배열 변수에는 equipments.js 21번째 줄에 내보내기한 Module 중 resolvers를 불러와 담게 되는 것이에요.

공식 문서에서 확인했듯 배열로 넣어준 것을 확인할 수 있어요.



_queries.js


위 코드를 분석해 보면 equipments에 Root Query Type이 들어가고,

_mutaion.js


Mutation에는 Root Mutaion Type들이 Document Node로 지정되어 있고, 이들 역시 Module로 내보내기 되어져서 

 

index.js


이 곳에 4번째 줄과 5번째 줄에 각각 상수 변수에 담아준걸 확인할 수 있어요.

해당 프로젝트를 구동하기 위해 아래 명령어를 먼저 터미널에 입력합니다.

npm install


그런 뒤 아래 명령어로 기동 시켜줄게요.

npm start

 

Playgroud


http://localhost:4000/ 을 브라우저에 입력하면 위와 같이 Playgroud가 열리게 되고, 위와 같이 Query를 날려보면 정상적으로 결과값을 받는걸 확인할 수 있어요.

Playgroud


또한, Playgroud에서 오른쪽에 SCHEMA Tab을 클릭하면 위와 같이 스키마를 확인할 수 있고,

Playgroud


DOCS Tab을 통해 관련 내용을 확인할 수도 있어요.

공부를 하면서 느낀점은 Postman 보다는 Swagger와 유사하다고 느껴졌어요.

 

 

        📦 dbWorks.js 살펴보기

const database = require('./database.js')

const dataFiltered = (which, args) => {
    let result = database[which].filter((item) => {
        // 조건인자가 없거나, 페이징 관련 인자거나
        // 모든 요소가 아이템과 모두 일치하면 통과
        return !args || Object.keys(args).reduce((a, b) => {
            return a && (
                ['page', 'per_page'].includes(b) ||
                item[b] == args[b]
            )
        }, true)
    })

    // 페이징
    if (args.page && args.per_page) {
        result = result.slice(
            (args.page - 1) * args.per_page, 
            args.page * args.per_page)
    }

    return result
}

const dbWorks = {
    deleteItem: (which, args) => {
        const deleted = database[which].filter((item) => {
            return item.id == args.id
        })[0]
        database[which] = database[which].filter((item) => {
            return item.id != args.id
        })
        return deleted
    },

    getTeams: (args) => dataFiltered('teams', args)
        .map((team) => {
            team.members = dbWorks.getPeople({team: team.id})
            return team
        }),
    postTeam: (args) => {
        const newTeam = {
            id: database.teams.map((team) => {
                return Number(team.id)
            }).reduce((a, b) => {
                return Math.max(a, b)
            }, 0) + 1,
            ...args.input
        }
        database.teams.push(newTeam)
        return newTeam
    },
    editTeam: (args) => {
        return database.teams.filter((team) => {
            return team.id == args.id
        }).map((team) => {
            Object.assign(team, args.input)
            return team 
        })[0]
    },

    getPeople: (args) => dataFiltered('people', args) 
        .map((person) => {
            person.tools = [
                ...dbWorks.getEquipments({used_by: person.role}),
                ...dbWorks.getSoftwares({used_by: person.role})
            ]
            person.givens = [
                ...dbWorks.getEquipments({used_by: person.role}),
                ...dbWorks.getSupplies({team: person.team})
            ]
            return person
        }),
    postPerson: (args) => {
        const newPerson = {
            id: database.people.map((person) => {
                return Number(person.id)
            }).reduce((a, b) => {
                return Math.max(a, b)
            }, 0) + 1,
            ...args.input
        }
        database.people.push(newPerson)
        return newPerson
    },
    editPerson: (args) => {
        return database.people.filter((person) => {
            return person.id == args.id
        }).map((person) => {
            Object.assign(person, args.input)
            return person 
        })[0]
    },

    getRoles: (args) => dataFiltered('roles', args)
        .map((role) => {
            role.members = dbWorks.getPeople({role: role.id})
            role.equipments = dbWorks.getEquipments({used_by: role.id})
            role.softwares = dbWorks.getSoftwares({used_by: role.id})
            return role
        }),

    getEquipments: (args) => dataFiltered('equipments', args),
    postEquipment: (args) => {
        database.equipments.push(args)
        return args
    },
    increaseEquipment: (args) =>{
        return database.equipments.filter((equipment) => {
            return equipment.id == args.id
        }).map((equipment) => {
            equipment.count += 1 
            return equipment
        })[0]

    },

    getSoftwares: (args) => dataFiltered('softwares', args),

    getSupplies: (args) => dataFiltered('supplies', args),
}

module.exports = dbWorks


dbWorks.js는 위와 같이 이뤄져 있는데, 한번 살펴보려고 해요.
일단 해당 코드는 Resolver에 사용할 기능들을 Module화 하는 코드에요.

GraphQL을 사용하면서 사용할 Resolver를 여러가지 Mock 데이터베이스 기능을 함수들로 구현해 놓은 것이에요.
이 내용은 실습을 위한 내용이기 때문에 실제 서비스에서는 데이터베이스를 다루는 코드들이 들어가야 한다는 것을 잊으면 안되는 것이에요.




 

 

        📦 supply Module 추가

프로젝트에 대해 알아보았으니 실제 Module을 구현해 볼게요.
equipments.js를 따로 만든 것처럼 supplies.js 파일을 만들어 볼거에요.

supplies.js


위와 같이 만들어 줄거에요.

supplies.js

 

const { gql } = require('apollo-server')
const dbWorks = require('../dbWorks')

const typeDefs = gql`
    type Supply {
        id: String
        team: Int
    }
`
const resolvers = {
    Query: {
        supplies: (parent, args) => dbWorks.getSupplies(args),
    },
    Mutation: {
        deleteSupply: (parent, args) => dbWorks.deleteItem('supplies', args),
    }
}

module.exports = {
    typeDefs: typeDefs,
    resolvers: resolvers
}


코드는 위와 같이 작성해 주었어요.

이제 위 코드에서 작성한 resolver를 Root Query TypeRoot Mutaion Type에도 작성해 주어야 해요.

_queries.js


위와 같이 6번째 줄에 Root Query Type에 supplies는 Supply 객체 배열을 반환할 수 있게 선언해 주었어요.


여기까지만 하면 터미널은 또 난리가 날 거에요.
이유는 supplies.js File이 Import 되지 않아 정보가 없기 때문이에요.

_mutaions.js


일단, 위와 같이 6번째 줄에 Root Mutaion Type에 Supply를 삭제할 수 있는 함수를 선언해 주었어요.


index.js


index.js에 위와 같이 7번째 줄에 supplies를 받아오도록 처리하고, typeDefs와 resolvers를 받아 배열에 저장할 수 있도록 해 주었어요.


이렇게 해 주면 터미널은 다시 평화를 찾은 걸 확인할 수 있어요.

Playgroud - SCHEMA

 

Playgroud - DOCS


Playgroud에서 확인하면 위와 같이 위에서 추가한 내용이 확인되는 걸 볼 수 있어요.



Query를 날려보니 정상적으로 결과값을 반환 받는걸 확인할 수 있어요.

 

 

 

GraphQL과 타입스크립트로 개발하는 웹 서비스:설계부터 개발·배포까지 따라 하며 완성하는 웹 풀

COUPANG

www.coupang.com

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

 

 

 

🧐 참고 자료

해당 내용은 아래 강의를 공부하면서 정리한 내용임을 알려 드립니다.

 

[무료] 얄팍한 GraphQL과 Apollo - 인프런 | 강의

⚡ 짧고 굵은 전체 90분 강좌! 사이트의 코드들을 복붙하며 빠르게 GraphQL을 배우고 아폴로 사용법을 익히세요., ⏱ 여러분의 시간은 소중합니다. [사진] 🎢  GraphQL이 뭔가요? 서비스를 구성하는

www.inflearn.com

 

 

 

 

 

카카오페이 | 마음 놓고 금융하다

여기를 눌러 링크를 확인하세요.

qr.kakaopay.com

 

 

 

 

728x90
반응형