ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TIL - 얄팍한 GraphQL & Apollo 학습일지
    개발 2022. 3. 15. 22:01

    얄팍한 GraphQL 과 Apollo 강좌를 통한 학습

    얄팍한 GraphQL & Apollo 강좌

     

    학습목표

    GraphQL이 무엇인지, 왜 사용하는지에 대해 이해하고 실무 프로젝트에 도입을 결정할때 타당성을 찾기 위함.

     

    목차

    • 기존의 REST API 한계와 GraphQL 강점
    • Apollo 서버 구축
    • React와 Apollo Client
      • useQuery
      • useMutation
    • 학습 후기

    기존의 REST API 한계와 GraphQL 강점

    REST API는 데이터를 주고받는 주체들간 약속된 형식으로 클라이언트가 필요한 데이터 꾸러미를 서버에 요청 할 수 있다.

     

    요청 형식URI

    GET localhost:3000/api/team
    GET localhost:3000/api/team/{id 번호}
    GET localhost:3000/api/people
    GET localhost:3000/api/people?{변수}={값}&{변수}={값} ...
    GET localhost:3000/api/team/{id 번호}/people

    팀, 팀원의 목록을 받아오는 API 가 존재한다.

     

    Case 1: Overfetching

    각 팀의 매니저와 오피스 호수만 필요할 때 localhost:3000/api/team 를 호출해서 팀 정보를 모두 불러오면 쉽게 접근 할 수 있을 것이다.

    [
      {
        "manager": "Mandy Warren",
        "office": "101A",
      },
      {
        "manager": "Stewart Grant",
        "office": "101B",
      },
      {
        "manager": "Smantha Wheatly",
        "office": "102A",
      },
      // ...
    ]

    하지만 위 처럼 내가 필요한 팀의 매니저와 오피스 호수만 정확히 받아 올 수는 없을까?

     

     

    Case 2: Underfetching

    특정 팀의 매니저와 팀원들 명단이 필요할 때

     

    • localhost:3000/api/team/{id 번호}
    • localhost:3000/api/team/{id 번호}/people

    두 번의 호출을 통해서 팀 id 번호를 통한 데이터를 가져 올 수 있다.

    필요한 정보들만 한번의 요청을 통해서 가져올 수 있다면 좋을텐데 말이다.

     

    단편적인 이야기 이지만 이러한 REST API의 한계를 배경으로 GraphQL 니즈가 생겼다고 할 수 있겠다.

     

    GraphQL 에서는 어떻게 할까?

    REST API 에서 팀 정보를 모두 받아올 때의 경우 query 키워드를 통해서 모든 팀의 정보에 접근 할 수 있다.

    GraphQL에서 query를 작성하는 모양은 마치 우리가 잘 아는 json 형식에서 key 값만 사용하여 원하는 정보를 콕콕 찍어 가져오는 듯하다.

    query {
      teams {
        id
        manager
        office
        extension_number
        mascot
        cleaning_duty
        project
      }
    }

     

    그렇다면 팀의 필요한 정보만 받아오기 위해서는 원하는 key 값만 작성하면 될 것이다.

    아래에서는 Case 1 에서 팀의 매니저와 팀 호수만 불러 올 수 있도록 작성한 query 이다.

    query {
      teams {
        manager
        office
      }
    }

    보기에도 직관적이고 불필요한 데이터 없이 원하는 정보만 가져오기에 너무 편리하다.

    또 모든 팀의 정보가 아니라 원하는 팀의 정보만을 원 할 경우는 특정 id를 지정해서 가져 올 수도 있다.

    query {
      team(id: 1) {
        manager
        office
      }
    }

     

    이번엔 Case 2 에서 팀 정보와 해당 팀 멤버들의 정보를 받아올때 2번의 호출을 통해서 접근했었던 경우를 살펴보자.

    query {
      team(id: 1) {
        manager
        office
        members {
          first_name
          last_name
        }
      }
    }

    GraphQL에서는 json 구조처럼 조회 한 팀 정보안에 members 정보를 조회해서 팀원의 first_name과 last_name을 한번의 호출로 받아오고 있다.

     

    위에서 알아본 것 처럼 GraphQL에서는 

     

    • 필요한 정보들만 선택하여 받아올 수 있다.
      • Overfetching 문제 해결
      • 데이터 전송량 감소
    • 여러 계층의 정보를 한번에 받아 올 수 있다.
      • Underfetching 문제 해결
      • 요청 횟수 감소
    • 하나의 End-Point 에서 모든 요청을 처리

     

    와 같은 강점이 있다.

     


    Apollo

    GraphQL은 일반적으로 사용하는 REST API와 작성하는 명세가 다르고 동작하는 방식에도 차이가 있다.

    그 말은 단순히 프론트엔드에서 작성하는 코드가 GraphQL로 서버에 요청을 날린다해서 원하는데로 데이터를 받아오는것이 아니라

    프론트엔드에서 GraphQL로 요청한 정보를 이해 할 수 있는 백엔드 코드에도 GraphQL을 위한 코드가 준비되어 있어야 한다는 것이다.

     

    이러한 환경을 세팅하고 코드를 작성하는데 도움을 주는 가장 인기있는 라이브러리가 Apollo이다.

    Apollo는 프론트엔드와 백엔드에서 GraphQL을 쉽고 편리하게 작업 할 수 있게 도와주며 다양한 언어들의 호환이 가능하다고 한다.

     

    이번 학습에서는 

    • Apollo Server 를 사용한 백엔드 서버 제작
    • Apollo Client 와 React를 사용한 프론트엔드 웹 제작

     

    을 목표로 학습해 나간다.

     

    우선 Apollo 서버 환경을 구축해야하는데 본 학습에서는 프론트엔드 관점에서 학습함으로 자세한 설명은 얄코 강의본을 통해 간단히 세팅해보길 바란다.

     

    [Apollo 서버 구축 강의본 링크]

    https://www.yalco.kr/@graphql-apollo/2-1/

     

    Apollo 서버 구축하기

    어려운 프로그래밍 개념들을 쉽게 설명해주는 유튜브 채널 '얄팍한 코딩사전'. 영상에서 다 알려주지 못한 정보들이나 자주 묻는 질문들의 답변들, 예제 코드들을 얄코에서 확인하세요!

    www.yalco.kr

     


    Apollo 서버 구축 요약

     

    CSV 데이터 베이스

    실제 데이터베이스 구축까지 하기엔 많은 시간이 소요될 수 있기 때문에 간단하게 csv 문서를 통해 데이터 베이스로 활용한다.


    Query: 데이터 조회 대한 응답 설계

    Apollo 서버에서는 클라이언트에서 query 로 요청한 정보를 리턴하기 위한 명세를 작성하기 위해서 type Query 를 정의 한다.

    Query 안에서 정의한 teams는 [Team] 을 리턴하고 [Team] 은 teams.csv 데이터에 각 row에 접근하여 데이터를 반환한다.

     

    ​​

    아래 선언된 resolvers 는 Query에 해당하는 데이터를 리턴하기위해 csv 데이터에 접근하여 해당 key 맞는 정보를 출력 할 수 있게 한다.

     

    Apollo 서버는 server 생성자를 통해 typeDefs와 resolvers를 통해 클라이언트로 정보를 보내는 역할을 한다.

     

    equipments 데이터를 불러오는 Query를 추가하여 Postman 과 같은 API 테스트 도구를 사용해 결과를 확인 할 수 있다.


    Mutation: 데이터 수정/삭제 대한 응답 설계

    데이터 수정/삭제를 위해서 클라이언트에서 mutation 요청을 했을때 거치게 되는 로직이 바로 type Mutation 이다.

    Mutation 또한 명세를 정의하고 데이터에 접근하기 위한 로직인 resolvers를 만들어야 한다.

     

    아래 예시

    // 서버 type Mutation 정의
    type Mutation {
        insertEquipment(
            id: String,
            used_by: String,
            count: Int,
            new_or_used: String
        ): Equipment
        ...
    }
    // 서버 resolvers 선언
    const resolvers = {
        Query: {
        	//...
        }
    	Mutation: {
            insertEquipment: (parent, args, context, info) => {
                database.equipments.push(args)
                return args
            },
            //...
        }
    }
    // 클라이언트에서 요청 시
    mutation {
      insertEquipment (
        id: "laptop",
        used_by: "developer",
        count: 17,
        new_or_used: "new"
      ) {
        id
        used_by
        count
        new_or_used
      }
    }

     

    이러한 방식으로 서버에서 API 응답에 대한 설정을 하다보면 하나의 페이지에서 Query 와 Mutation, resolvers가 쌓이면서 코드가 길어지게 된다.

     

    프로젝트를 잘 유지, 관리하기 위해서는 코드를 모듈화하는 것이 필요하다.

     

    모듈화 된 폴더 구조

     

    모듈화 후 정리된 index.js 코드

    // index.js
    const { ApolloServer } = require('apollo-server')
    const _ = require('lodash')
    
    const queries = require('./typedefs-resolvers/_queries')
    const mutations = require('./typedefs-resolvers/_mutations')
    const equipments = require('./typedefs-resolvers/equipments')
    const supplies = require('./typedefs-resolvers/supplies')
    
    const typeDefs = [
        queries,
        mutations,
        equipments.typeDefs,
        supplies.typeDefs
    ]
    
    const resolvers = [
        equipments.resolvers,
        supplies.resolvers
    ]
    
    const server =  new ApolloServer({typeDefs, resolvers})
    
    server.listen().then(({url}) => {
        console.log(`🚀  Server ready at ${url}`)
    })

     

    이후 3-2 강의 ~ 3-4 강의까지는 지금까지 초안으로 작성된 코드들에 type 을 선언하여 더 안정적인 코드로 만드는 작업이다.

    typescript와 매우 유사하며 필요에 따라 깊이 있게 공부해보는것도 좋을것 같다.

     

    [Lesson 2. GraphQL의 기본 타입들] 링크

    https://www.yalco.kr/@graphql-apollo/3-2/


    React와 Apollo Client

    설치해야되는 모듈

    react 프로젝트에 Apollo Client 를 통한 데이터 요청을 하기 위해서 아래의 모듈을 설치해 준다.

    • @apollo/client
    • graphql
    npm i @apollo/client graphql

     

    화면 레이아웃

    강의에서 제공하는 기본 레이아웃이다.

    admin 페이지와 같은 레이아웃으로 왼쪽에는 상단에는 GNB, 좌측에는 LNB 우측에는 세부 Contents 가 뿌려지는 화면을 구성하는게 목표이다.

     

    Apollo 서버와 연결하기

    프로젝트 루트 컴포넌트인 app.js 파일에서 ApolloProvider를 사용해서 client 를 주입한다.

    여기서 client 는 ApolloClient 생성자를 통해 만들어진 인스턴스로 마치 Axios 처럼 해당 url 에 연결을 도와준다.

     

    const client = new ApolloClient({
      uri: 'http://localhost:4000',
      cache: new InMemoryCache()
    });

    client 인스턴스의 옵션값으로 들어간 cache를 살펴보면 InMemoryCache() 를 통해서 서버로 부터 한번 받아온 데이터를

    필요 이상으로 재요청하는것을 방지하게 도와주는 용도이다.

    마치 react-query의 캐싱 옵션같은 역할은 하는것이다.

     

    import './App.css';
    import React, { useState } from 'react';
    import { ApolloProvider } from '@apollo/client';
    import { ApolloClient, InMemoryCache } from '@apollo/client'
    
    import Roles from './components/roles'
    import Teams from './components/teams'
    import People from './components/people'
    
    const client = new ApolloClient({
      uri: 'http://localhost:4000',
      cache: new InMemoryCache()
    });
    
    function App() {
    
      const [menu, setMenu] = useState('Roles')
    
      let mainComp = {
        Roles: (<Roles />),
        Teams: (<Teams />),
        People: (<People />),
      }
    
      function NavMenus() {
        return [
          'Roles', 'Teams', 'People'
        ].map((_menu, key) => {
          return (
            <li key={key} className={menu === _menu ? 'on' : ''}
              onClick={() => { setMenu(_menu); }}>{_menu}</li>
          );
        });
      }
    
      return (
        <div className="App">
          <ApolloProvider client={client}>
            <header className="App-header">
              <h1>Company Management</h1>
              <nav>
                <ul>
                  {NavMenus()}
                </ul>
              </nav>
            </header>
            <main>
              {mainComp[menu]}
            </main>
          </ApolloProvider>
        </div>
      );
    }
    
    export default App;

     

    useQuery(query)

    데이터 요청하기

    본격적으로 데이터를 요청하기 위해서 화면 좌측에 Roles에 해당하는 데이터를 불러와 각각의 메뉴 버튼을 그려주는 작업을 하려고 한다.

    위에서 client 인스턴스를 Provider를 통해 연결하였기 때문에 이제 내가 필요한 데이터를 요청하는 작업을 해야한다.

    GraphQL에서는 데이터를 조회하기위해서 query 키워드를 사용했던것이 기억나는가?

    클라이언트에서도 마찬가지로 서버에서 명시한 Query를 불러줘야 한다. 이것을 도와주는 것이 apollo client에서 제공하는

    useQuery 함수다.

     

    roles.js 컴포넌트 코드이다.

    import './components.css';
    import { useState } from 'react';
    import { useQuery, gql } from '@apollo/client';
    
    const GET_ROLES = gql`
      query GetRoles {
        roles {
          id
          requirement
        }
      }
    `;
    
    function Roles() {
        const [contentId, setContentId] = useState('');
    
        function AsideItems () {
            const roleIcons = {
                developer: '💻',
                designer: '🎨',
                planner: '📝'
            }
            const { loading, error, data } = useQuery(GET_ROLES);
            if (loading) return <p className="loading">Loading...</p>
            if (error) return <p className="error">Error :(</p>
            return (
                <ul>
                    {data.roles.map(({id}) => {
                        return (
                            <li key={id} className={'roleItem ' +  (contentId === 'id' ? 'on' : '')}
                                onClick={() => {setContentId(id)}}>
                                <span>{contentId === id ? '🔲' : '⬛'}</span>
                                {roleIcons[id]} {id}
                            </li>
                        )
                    })}
                </ul>
            );
        }
    
        function MainContents () {
            return (<div></div>);
        }
    
        return (
            <div id="roles" className="component">
                <aside>
                    {AsideItems()}
                </aside>
                <section className="contents">
                    {MainContents()}
                </section>
            </div>
        )
    }
    
    export default Roles;

    우리가 사용하는 React 코드에 중간 중간 GraphQL과 관련된 코드들이 보인다.

     

    먼저 요청할 query를 상수로 선언한 것을 볼 수 있다.

    const GET_ROLES = gql`
      query GetRoles {
        roles {
          id
          requirement
        }
      }
    `;

     

    이렇게 선언한 GET_ROLES를 useQuery에 인자로 넘겨 해당 요청을 실행한다.

            const { loading, error, data } = useQuery(GET_ROLES);
            if (loading) return <p className="loading">Loading...</p>
            if (error) return <p className="error">Error :(</p>

    useQuery는 loading, error, data 값을 리턴하여 마치 react-query처럼 프론트 코드를 작성하는데 로딩 및 에러 핸들링도 편하게 쓸 수 있도록 인터페이스를 제공한다.

     

    요청 결과 GetRoles query 를 통해 각각의 id 정보를 담은 데이터가 넘어왔고

    요청한 데이터로 컴포넌트가 리턴한 UI를 그리게 된다.

     

    여기서 프론트 입장에서 굉장히 편리한 점은 요구사항에 따라서 데이터를 더 요청해야 하거나 필요없는 데이터가 있을 경우 query 문을 수정하여 프론트단에서 데이터를 요청 할 수 있다는 점이다.

     

    requirement 데이터를 요청하기 위한 query 수정

    기존 id 만 넘어오는 요청에 requirement 데이터를 추가로 불러야 하는 경우 간단하게 query 문에 값만 지정하면 된다.

     

    requirement 데이터를 추가로 요청한 결과

    이렇게 클라이언트에서 요청한 데이터가 넘어 올 수 있는 것은 apollo 서버에 이미 해당 query에 대한 명세가 작성되어 있기 때문이다.

     

    // roles csv 데이터를 조회하는 서버 코드
    const resolvers = {
        Query: {
            roles: (parent, args) => dbWorks.getRoles(args),
            role: (parent, args) => dbWorks.getRoles(args)[0]
        },
    }

     

    useQuery(query, options)

    데이터 요청하기: 특정 id값에 해당하는 컨텐츠만 가져오기

     

    여기서 특정 데이터만 요청한다는 것은 파라미터를 옵션으로 던져 해당하는 데이터를 조회하는것을 말한다.

    roles 데이터를 요청한 결과로 그려진 메뉴에서 마지막 메뉴인 "Planner" 버튼을 클릭했을때 Planner에 해당하는 데이터만 요청하려면 어떻게 해야할까?

     

    REST API 라면 요청 URL에 "/manage/newpost/16?type=post" -> ?type=post 처럼 파라미터 포함해 GET 방식으로 요청 할 것이다. GraphQL 경우 어떻게 요청 하는지 알아보자.

     

    위에서 알아봤듯이 요청을 위해서는 query문을 useQuery에 할당하는 것으로 요청했다. 

     

    우선 요청을 위한 query를 살펴보자.

    // ...
    const GET_ROLE = gql`
      query GetRole($id: ID!) {
        role(id: $id) {
          id
          requirement
          members {
            id
            last_name
            serve_years
          }
          equipments {
            id
          }
          softwares {
            id
          }
        }
      }
    `;
    // ...

    위와 같이 role 데이터를 요청할때 id 값을 인자로 받아 해당 id에 해당하는 데이터만 불러올 수 있도록 작성 한다.

    다음은 useQuery 를 사용하는 코드이다.

     

      function MainContents () {
    
        const { loading, error, data } = useQuery(GET_ROLE, {
          variables: {id: contentId}
        })
    
        if (loading) return <p className="loading">Loading...</p>
        if (error) return <p className="error">Error :(</p>
        if (contentId === '') return (<div className="roleWrapper">Select Role</div>)
    
        return (
          <div className="roleWrapper">
            <h2>{data.role.id}</h2>
            <div className="requirement"><span>{data.role.requirement}</span> required</div>
            <h3>Members</h3>
            <ul>
              {data.role.members.map((member) => {
                return (<li>{member.last_name}</li>)
              })}
            </ul>
            <h3>Equipments</h3>
            <ul>
              {data.role.equipments.map((equipment) => {
                return (<li>{equipment.id}</li>)
              })}
            </ul>
            <h3>Softwares</h3>
              {data.role.softwares.map((software) => {
                return (<li>{software.id}</li>)
              })}
            <ul>
            </ul>
          </div>
        );
      }

     

    전체코드 중 useQuery를 살펴보면

     

      const { loading, error, data } = useQuery(GET_ROLE, {
          variables: {id: contentId}
        })

     

    useQuery 두번째 인자로 variables 키 값에 객체 형태로 조회하고자하는 contentId 값을 넘기는 것을 확인 할 수 있다.

     

    contentId에 해당하는 요청으로부터 온 응답

    응답을 통해 얻은 데이터로 오른쪽 화면에 Contents를 그리게 되는 로직이다.

     

    각각 메뉴를 클릭해서 테스트하면서 확인된 점은 Client 인스턴스를 생성할때 cache 옵션을 통해 한번 시도한 요청에 대한 재요청을 방지할수 있다고 했는데 메뉴 선택을

     

    Planner -> Designer -> Planner 순으로 테스트 해봤을때 

     

    이미 Planner 의 정보를 받아온 cache 정보가 있기 때문에 다시 요청하지 않고 캐싱 된 데이터로 화면을 그려 불필요한 요청을 줄일 수 있었다.

     

     

    useMutation(query, options)

    데이터를 수정하거나 삭제하는 경우에 사용하는 useMutation이다.

    useMutation을 사용할때도 마찬가지고 첫번째 인자로 사용 할 쿼리를 넘기고, 두번째 인자로 옵션값을 넣을 수 있다.

    여기서 두번째 인자로 사용되는 옵션이 좀 특이한데 아래에서 코드를 나눠 살펴보자.

    더보기

    코드 전체를 보려면 "더보기" 클릭

    import './components.css';
    import { useState } from 'react';
    import { useQuery, useMutation, gql } from '@apollo/client'
    
    const GET_TEAMS = gql`
      query GetTeams {
        teams {
            id
            manager
            members {
              id
              first_name
              last_name
              role
            }
          }
      }
    `;
    
    const GET_TEAM = gql`
      query GetTeam($id: ID!) {
        team(id: $id) {
            id
            manager
            office
            extension_number
            mascot
            cleaning_duty
            project
          }
      }
    `;
    
    const DELETE_TEAM = gql`
      mutation DeleteTeam($id: ID!) {
        deleteTeam(id: $id) {
          id
        }
      }
    `
    // ...
    const EDIT_TEAM = gql`
      mutation EditTeam($id: ID!, $input: PostTeamInput!) {
        editTeam(id: $id, input: $input) {
          id,
          manager,
          office,
          extension_number,
          mascot,
          cleaning_duty,
          project
        }
      }
    `
    
    const POST_TEAM = gql`
      mutation PostTeam($input: PostTeamInput!) {
        postTeam(input: $input) {
          id
          manager
          office
          extension_number
          mascot
          cleaning_duty
          project
        }
      }
    `
    
    
    let refetchTeams
    
    
    function Teams() {
    
        function execPostTeam () {
            postTeam({
                variables: { input: inputs }})
        }
    
        const [postTeam] = useMutation(
            POST_TEAM, { onCompleted: postTeamCompleted })
    
        function postTeamCompleted (data) {
            console.log(data.postTeam)
            alert(`${data.postTeam.id} 항목이 생성되었습니다.`)
            refetchTeams()
            setContentId(0)
        }
    
        function execEditTeam () {
            editTeam({
                variables: {
                    id: contentId,
                    input: inputs }
            })
        }
    
        const [editTeam] = useMutation(
            EDIT_TEAM, { onCompleted: editTeamCompleted })
    
        function editTeamCompleted (data) {
            console.log(data.editTeam)
            alert(`${data.editTeam.id} 항목이 수정되었습니다.`)
            refetchTeams()
        }
    
        function execDeleteTeam () {
            if (window.confirm('이 항목을 삭제하시겠습니까?')) {
                deleteTeam({variables: {id: contentId}})
            }
        }
    
        const [deleteTeam] = useMutation(
            DELETE_TEAM, { onCompleted: deleteTeamCompleted })
    
        function deleteTeamCompleted (data) {
            console.log(data.deleteTeam)
            alert(`${data.deleteTeam.id} 항목이 삭제되었습니다.`)
            refetchTeams()
            setContentId(0)
        }
    
        const [contentId, setContentId] = useState(0)
        const [inputs, setInputs] = useState({
            manager: '',
            office: '',
            extension_number: '',
            mascot: '',
            cleaning_duty: '',
            project: ''
        })
    
        function AsideItems () {
            const roleIcons = {
                developer: '💻',
                designer: '🎨',
                planner: '📝'
            }
    
            const { loading, error, data, refetch } = useQuery(GET_TEAMS);
            refetchTeams = refetch
    
            if (loading) return <p className="loading">Loading...</p>
            if (error) return <p className="error">Error :(</p>
    
            return (
                <ul>
                    {data.teams.map(({id, manager, members}) => {
                        return (
                            <li key={id}>
                  <span className="teamItemTitle" onClick={() => {setContentId(id)}}>
                    Team {id} : {manager}'s
                  </span>
                                <ul className="teamMembers">
                                    {members.map(({id, first_name, last_name, role}) => {
                                        return (
                                            <li key={id}>
                                                {roleIcons[role]} {first_name} {last_name}
                                            </li>
                                        )
                                    })}
                                </ul>
                            </li>
                        )
                    })}
                </ul>
            )
        }
    
        function MainContents () {
    
            const { loading, error } = useQuery(GET_TEAM, {
                variables: {id: contentId},
                onCompleted: (data) => {
                    if (contentId === 0) {
                        setInputs({
                            manager: '',
                            office: '',
                            extension_number: '',
                            mascot: '',
                            cleaning_duty: '',
                            project: ''
                        })
                    } else {
                        setInputs({
                            manager: data.team.manager,
                            office: data.team.office,
                            extension_number: data.team.extension_number,
                            mascot: data.team.mascot,
                            cleaning_duty: data.team.cleaning_duty,
                            project: data.team.project
                        })
                    }
                }
            });
    
            if (loading) return <p className="loading">Loading...</p>
            if (error) return <p className="error">Error :(</p>
    
            function handleChange(e) {
                const { name, value } = e.target
                setInputs({
                    ...inputs,
                    [name]: value
                })
            }
    
            return (
                <div className="inputContainer">
                    <table>
                        <tbody>
                        {contentId !== 0 && (
                            <tr>
                                <td>Id</td>
                                <td>{contentId}</td>
                            </tr>
                        )}
                        <tr>
                            <td>Manager</td>
                            <td><input type="text" name="manager" value={inputs.manager} onChange={handleChange}/></td>
                        </tr>
                        <tr>
                            <td>Office</td>
                            <td><input type="text" name="office" value={inputs.office} onChange={handleChange}/></td>
                        </tr>
                        <tr>
                            <td>Extension Number</td>
                            <td><input type="text" name="extension_number" value={inputs.extension_number} onChange={handleChange}/></td>
                        </tr>
                        <tr>
                            <td>Mascot</td>
                            <td><input type="text" name="mascot" value={inputs.mascot} onChange={handleChange}/></td>
                        </tr>
                        <tr>
                            <td>Cleaning Duty</td>
                            <td><input type="text" name="cleaning_duty" value={inputs.cleaning_duty} onChange={handleChange}/></td>
                        </tr>
                        <tr>
                            <td>Project</td>
                            <td><input type="text" name="project" value={inputs.project} onChange={handleChange}/></td>
                        </tr>
                        </tbody>
                    </table>
                    {contentId === 0 ?
                        (<div className="buttons">
                                <button onClick={execPostTeam}>Submit</button>
                            </div>
                        ) : (
                            <div className="buttons">
                                <button onClick={execEditTeam}>Modify</button>
                                <button onClick={execDeleteTeam}>Delete</button>
                                <button onClick={() => {setContentId(0)}}>New</button>
                            </div>
                        )}
                </div>
            )
        }
        return (
            <div id="teams" className="component">
                <aside>
                    {AsideItems()}
                </aside>
                <section className="contents">
                    {MainContents()}
                </section>
            </div>
        )
    }
    
    export default Teams;

     

    먼저 삭제를 위해 execDeleteTeam 이벤트가 등록된 버튼이 보인다.

    <button onClick={execDeleteTeam}>Delete</button>

    이 버튼을 클릭하면 execDeleteTeam 함수가 실행되는데

     

        function execDeleteTeam () {
            if (window.confirm('이 항목을 삭제하시겠습니까?')) {
                deleteTeam({variables: {id: contentId}})
            }
        }

    confirm 메시지 창을 통해 사용자의 인터랙션을 받고 삭제를 확인 할 경우 실행되는 deleteTeam 함수는

    const [deleteTeam] = useMutation(
            DELETE_TEAM, 
            { onCompleted: deleteTeamCompleted }
    )

    useMutation 실행 후 리턴되는 값으로 함수이다.

    deleteTeam 함수를 통해 실제 데이터가 삭제되고 이때 넘겨받은 인자의 id값을 통해서 사용자가 삭제하고자 하는 데이터를 삭제한다.

    그리고 useMutation 두번째 인자로 할당된 옵션을 보면

    { onCompleted: deleteTeamCompleted }

    onCompleted 라는 키에 deleteTeamCompleted라는 값이 할당되어있다, deleteTeamCompleted는 코드 작성자가 정의한 함수로 해당 deleteTeam() 이 실행된 후에 작동한다.

    function deleteTeamCompleted (data) {
        console.log(data.deleteTeam)
        alert(`${data.deleteTeam.id} 항목이 삭제되었습니다.`)
        refetchTeams() // refetch 중요!
        setContentId(0)
    }

    deleteTeamCompleted 함수의 코드를 보면 삭제 후 alert 을 띄우고 마지막에 setContentId(0) 으로 화면을 초기화 한다.

    그런데 중간에 refetchTeams() 함수가 보이는가?

    이 함수가 없으면 데이터를 삭제한 후 화면이 최신 데이터를 기반으로 바뀌지 않는다.

     

    왜냐면? React는 컴포넌트의 상태값이 바뀌어야 화면을 리렌더링 하니까!

     

    그럼 team 정보를 받아 화면을 그리고 있는데 그 데이터는 어디에 있는가?

        function AsideItems () {
            const roleIcons = {
                developer: '💻',
                designer: '🎨',
                planner: '📝'
            }
    
            const { loading, error, data, refetch } = useQuery(GET_TEAMS);
            refetchTeams = refetch
    
            if (loading) return <p className="loading">Loading...</p>
            if (error) return <p className="error">Error :(</p>
    
            return (
            //...

    바로 useQuery(GET_TEAMS) 가 실행될때 받아온 data를 가르킨다.

    useQuery는 refetch라는 인터페이스를 제공하는데 refetch는 함수로 해당 useQuery를 다시 실행시켜 데이터를 갱신 할 수 있다.

    때문에 refetchTeams라는 컴포넌트 바깥쪽 스코프에 변수를 지정하여 컴포넌트에 데이터 갱신이 필요할때 마다 실행시켜

    화면을 리렌더링 할 수 있게 한다는게 포인트이다.

     

    // 문서 상단..
    import './components.css';
    import { useState } from 'react';
    import { useQuery, useMutation, gql } from '@apollo/client'
    
    const GET_TEAMS = gql`
      query GetTeams {
        teams {
            id
            manager
            members {
              id
              first_name
              last_name
              role
            }
          }
      }
    `;
    
    // refetch 함수를 담기 위한 변수
    let refetchTeams
    
    // 컴포넌트 시작..
    function Teams() {...}

     

    블로그에 다 담지 못한 Edit, Post 동작들도 있는데 Delete와 마찬가지로 useMutation을 사용하여 구현하기때문에 자세한 내용은

    강의를 참고하면 좋을것 같다...

     


    학습을 마치며..

    GraphQL 을 공부하게 된 이유는 현재 진행하고 있는 모각코 스터디 때문에 시작하게 되었다.

    스터디에서 GraphQL 이 좋다.. 라는 말만 듣는게 전혀 체감이되지 않아 이런 저런 영상들을 찾아보다.

    실습하면서 할 수 있는 무료 강의가 있어서 듣게 되었다.

     

    갓얄코님이 제공하는 GraphQL 강의는 정말 유료로 팔아도 될 정도로 유익한거같다.

    강의 총 시간이 1시간 30분인데 프론트엔드 작업의 강의 분량은 20분 남짓이다.

    (강의를 모두 들으면서 블로그까지 정리하는데 6시간 걸린건 안비밀..)

     

    거의 Apollo 서버 구축과 type 정의하는데 모든 강의가 흘러간다.

     

    나는 GraphQL이 프론트에서만 사용하는 스택인줄 알고있었는데

    강의를 듣고나니 프론트에서

     

    "GraphQL 좋데! 쓰자!"

     

    해봤자 백엔드 개발자의 서버 작업이 없이는 못쓰는거 아닌가..?

    내가 프론트랑 백엔드 다해야하는건가?

    사실상 GraphQL도 프로젝트에 정말 핏하고 효율적으로 쓰기 위해선 진행하는 프로젝트의 성격과 사용방식에 따라서

    많은 상의를 거쳐서 어떻게 API 설계를 해야할지 많이 고민해야할거같다.

     

    무작정 편하게 쓴다고 모든 데이터를 하나의 End-Point에 다 때려박는것도 좀 아니지 않나..?

     

    우선 확실한건 react-query처럼 데이터 패칭시에 loading, error 등의 인터페이스가 제공되어 프론트 개발시 굉장히 편할거같고

    데이터 캐싱이 된다는 점도 큰 장점인것같다.

     

    그리고 요청하는 입장에 따라 편하게 데이터의 구조를 변경할수있다는 점도 큰 매리트다.

     

    앞으로 GraphQL을 도입하게 될지 모르지만 기회가 되면 실무에서도 써보고 싶다.

     

     

    댓글

onul-hoi front-end developer limchansoo