본문 바로가기
SPA/React

[React] Apollo를 이용하여 GraphQL 사용하기

by J4J 2021. 12. 26.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Apollo를 이용하여 GraphQL 사용하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

들어가기에 앞서 GraphQL을 이용하여 데이터를 가져오는 서버 및 설명은 여기를 참고해주시면 됩니다.

 

 또한 타입 스크립트를 이용하여 설정 및 구현해보도록 하겠습니다.

 

 

 

Apollo 환경 설정

 

[ 1. 패키지 설치 ]

 

$ npm install @apollo/client graphql

 

 

 

[ 2. App.tsx에 Provider 등록 ]

 

import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import * as React from 'react';
import Person from './person';

const App = (): JSX.Element => {

    const client = new ApolloClient({
        uri: 'http://localhost:8080/graphql', // GraphQL 서버 URL
        cache: new InMemoryCache() // fetching이 이루어진 query 캐싱 처리
    });

    return (
        <ApolloProvider client={client}>
            <Person /> {/* 아래에서 구현 예정 */}
        </ApolloProvider>
    )
}

export default App;

 

 

 

Apollo로 GraphQL 사용하기

 

위에 링크를 걸어둔 서버는 다음과 같은 스키마로 구성되어 있습니다.

 

# This directive allows results to be deferred during execution
directive @defer on FIELD

# Query root
type Query {
  persons: [PersonEntity]
  person(id: Int!): PersonEntity
}

#
type PersonEntity {
  age: Int!
  id: Int!
  name: String
  pets: [PetEntity]
  phone: String
}

#
type PetEntity {
  id: Int!
  name: String
  person_id: Int!
}

# Mutation root
type Mutation {
  deletePerson(person: PersonEntityInput): Boolean!
  savePerson(person: PersonEntityInput): PersonEntity
}

#
input PersonEntityInput {
  phone: String
  name: String
  age: Int!
  id: Int!
  pets: [PetEntityInput]
}

#
input PetEntityInput {
  id: Int!
  person_id: Int!
  name: String
}

 

 

 

리액트를 이용하여 해당 스키마들을 호출하여 특정 행동들을 해보겠습니다.

 

App.tsx과 동일한 위치에 person.tsx파일을 만들고 다음과 같이 코드를 작성해보겠습니다.

 

 

반응형

 

 

import * as React from 'react';
import { useQuery, gql, useMutation, ApolloQueryResult } from '@apollo/client';

// interface
interface Iperson {
    id: number;
    name: string;
    phone: string;
    age: number;
}

// query
const GET_PERSONS = gql`
    query GetPersons {
        persons {
            id
            name
            phone
        }
    }
`

const GET_PERSON = gql`
    query GetPerson($id: Int!) { 
        person(id: $id) {
            id
            name
            phone
        }
    }
`

const SAVE_PERSON = gql`
    mutation SavePerson($person: PersonEntityInput) {
        savePerson(person: $person) {
            id
        }
    }
`

const DELETE_PERSON = gql`
    mutation DeletePerson($person: PersonEntityInput) {
        deletePerson(person: $person)
    }
`

// component
const Person = (): JSX.Element => {

    // state
    const [person, setPerson] = React.useState<Iperson>({
        id: 0,
        name: '',
        phone: '',
        age: 0,
    });

    // getPersons refetch를 위한 ref
    const getPersonsRefetch = React.useRef<(variables?: Partial<{ id: number; }> | undefined) => Promise<ApolloQueryResult<any>>>();

    const getPersons = () => {
        const { loading, error, data, refetch } = useQuery(GET_PERSONS);

        // save나 delete가 수행되었을 때 변경 내역을 다시 가져오기 위해 refetch 저장
        getPersonsRefetch.current = refetch;

        if(loading) return <p>Loading...</p>
        if(error) return <p>Error...</p>

        return (
            <div style={ListContainer}>
                {data.persons.map((person: Iperson) => {
                    return (
                        <div style={PersonsBox}>
                            <div style={BoxLeft}>
                                <h2>{person.id}</h2>
                                <h3>{person.name}</h3>
                                <h3>{person.phone}</h3>
                            </div>
                            <div style={BoxRight}>
                                <button onClick={() => {
                                    // 삭제 쿼리 전달
                                    deletePerson({
                                        variables: { person: { id: person.id, name: '', phone: '', age: 0 } }
                                    })
                                }}>삭제</button>
                            </div>
                        </div>
                    )
                })}
            </div>
        )
    }

    const getPerson = (id: number) => {
        const { loading, error, data } = useQuery(GET_PERSON, {
            variables: {
                id,
            }
        })

        if(loading) return <p>Loading...</p>
        if(error) return (<><p>Error...</p></>)

        return (
            <div style={ListContainer}>
                <div style={PersonBox}>
                    <div style={BoxLeft}>
                        <h2>{data.person.id}</h2>
                        <h3>{data.person.name}</h3>
                        <h3>{data.person.phone}</h3>
                    </div>
                </div>
            </div>
        )
    }

    const [savePerson] = useMutation(SAVE_PERSON, {
        onCompleted: (data) => {
            // 저장이 성공적으로 수행되었을 경우
            alert(`${data.savePerson.id}이(가) 생성되었습니다.`);
            if(getPersonsRefetch.current) {
                // getPersons refetch 수행
                getPersonsRefetch.current();
            }
        }
    })

    const [deletePerson] = useMutation(DELETE_PERSON, {
        onCompleted: () => {
            // 삭제가 성공적으로 수행되었을 경우
            alert('삭제되었습니다.');
            if(getPersonsRefetch.current) {
                // getPersons refetch 수행
                getPersonsRefetch.current();
            }
        }
    })

    return (
        <div style={Wrapper}>
            <div style={PersonContainer}>
                {getPersons()}
                {getPerson(1)}
            </div>

            <div style={CommandContainer}>
                <div style={CommandHeader}>
                    <table>
                        <tbody>
                            <tr>
                                <td>이름</td>
                                <td>
                                    <input type='text' onChange={(e) => setPerson((curPerson) => {
                                                            return {
                                                                ...curPerson,
                                                                name: e.target.value
                                                            }
                                                        })} />
                                </td>
                            </tr>
                            <tr>
                                <td>전화번호</td>
                                <input type='text' onChange={(e) => setPerson((curPerson) => {
                                                            return {
                                                                ...curPerson,
                                                                phone: e.target.value
                                                            }
                                                        })} />
                            </tr>
                            <tr>
                                <td>나이</td>
                                <input type='number' onChange={(e) => setPerson((curPerson) => {
                                                            return {
                                                                ...curPerson,
                                                                age: Number(e.target.value)
                                                            }
                                                        })} />
                            </tr>
                        </tbody>
                    </table>
                </div>
                <div style={CommandFooter}>
                    <button onClick={() => {
                        // 생성 쿼리 전달
                        savePerson({
                            variables: {
                                person: person
                            }
                        })
                    }}>추가</button>
                </div>
            </div>
        </div>
    )
}

export default Person;

// style
const Wrapper = {
    width: '840px',
    
    margin: '0 auto',

    display: 'flex',
    justifyContent: 'center',
}

const PersonContainer = {
    margin: '0 auto'
}

const ListContainer = {
    margin: '0 auto'
}

const PersonsBox = {
    border: '1px solid olive',
    borderRadius: '8px',

    margin: '18px 0',

    display: 'flex'
}

const PersonBox = {
    border: '3px solid red',
    borderRadius: '8px',

    margin: '18px 0',

    display: 'flex'
}

const BoxLeft = {
    display: 'flex',
    flexDirection: 'column' as 'column',
    justifyContent: 'center',
    alignItems: 'center',

    width: '180px'
}

const BoxRight = {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',

    width: '180px'
}

const CommandContainer = {
    margin: '18px 0'
}

const CommandHeader = {
    margin: '14px 0'
}

const CommandFooter = {
    textAlign: 'center' as 'center'
}

 

 

728x90

 

 

그리고 코드를 실행해보면 다음과 같은 화면을 볼 수 있습니다.

 

실행 결과

 

 

 

왼쪽에서 각 박스들이 가지고 있는 삭제 버튼을 누르면 해당 데이터는 삭제처리가 이루어집니다.

 

또한 오른쪽에서는 값을 입력한 뒤 추가 버튼을 누르면 입력된 값을 기반으로 데이터 생성이 됩니다.

 

전체적으로 구현된 내용은 위와 같고 이번엔 각 쿼리들마다 구분해서 살펴보겠습니다.

 

 

 

query: persons

 

persons 실행을 위한 쿼리는 다음과 같이 되어있습니다.

 

const GET_PERSONS = gql`
    query GetPersons {
        persons {
            id
            name
            phone
        }
    }
`

 

 

 

서버에 구성되어 있는 스키마를 참고해보면 persons를 이용하여 PersonEntity에 포함되어 있는 데이터들을 가져올 수가 있습니다.

 

그중 age값은 화면에서 사용하지 않을 거기 때문에 id, name, phone만 불러오고 있습니다.

 

 

 

또한 GetPersons라는 query 명을 볼 수 있습니다.

 

query 명은 사용하지 않아도 문제가 될 것은 없지만 어떤 목적을 위한 요청인지 명시하기 위해 사용될 수 있습니다.

 

 

 

해당 쿼리 요청은 다음 코드에서 수행되고 있습니다.

 

// getPersons refetch를 위한 ref
const getPersonsRefetch = React.useRef<(variables?: Partial<{ id: number; }> | undefined) => Promise<ApolloQueryResult<any>>>();

const getPersons = () => {
    const { loading, error, data, refetch } = useQuery(GET_PERSONS);

    // save나 delete가 수행되었을 때 변경 내역을 다시 가져오기 위해 refetch 저장
    getPersonsRefetch.current = refetch;

    if(loading) return <p>Loading...</p>
    if(error) return <p>Error...</p>

    return (
        <div style={ListContainer}>
            {data.persons.map((person: Iperson) => {
                return (
                    <div style={PersonsBox}>
                        <div style={BoxLeft}>
                            <h2>{person.id}</h2>
                            <h3>{person.name}</h3>
                            <h3>{person.phone}</h3>
                        </div>
                        <div style={BoxRight}>
                            <button onClick={() => {
                                // 삭제 쿼리 전달
                                deletePerson({
                                    variables: { person: { id: person.id, name: '', phone: '', age: 0 } }
                                })
                            }}>삭제</button>
                        </div>
                    </div>
                )
            })}
        </div>
    )
}

 

 

 

쿼리 요청에 대한 리턴 값으로 loading, error, data, refetch를 볼 수 있고 각각 다음과 같습니다.

 

  • loading → 데이터를 불러오기 전 로딩 상태
  • error → query 요청에 error가 발생
  • data → query 요청에 대한 결괏값
  • refetch → query 요청을 다시 수행하는 함수

 

 

 

query: person

 

person은 persons와 크게 다를 것이 없습니다.

 

차이가 있다면 query에 변수를 사용한다는 것입니다.

 

const GET_PERSON = gql`
    query GetPerson($id: Int!) { 
        person(id: $id) {
            id
            name
            phone
        }
    }
`

 

 

 

 

person은 id에 맞는 값을 찾아서 return 해주기 때문에 id값을 명시해서 전달해줘야 합니다.

 

항상 동일한 id를 보내줄 경우에는 변수를 사용할 필요가 없지만 상황에 따라 다른 id를 불러올 경우에는 변수를 이용해줘야 합니다.

 

변수를 사용하기 위해서는 query 명을 지정한 뒤 $를 사용해주면 됩니다.

 

변수로 id가 전달되면 $id로 전달받을 수 있고 그에 맞는 타입을 지정한 뒤 query에 사용해주면 됩니다.

 

그리고 이런 변수 값은 다음과 같이 전달하여 사용해줄 수 있습니다.

 

const getPerson = (id: number) => {
    const { loading, error, data } = useQuery(GET_PERSON, {
        variables: {
            id, // id: id,
        }
    })

    if(loading) return <p>Loading...</p>
    if(error) return (<><p>Error...</p></>)

    return (
        <div style={ListContainer}>
            <div style={PersonBox}>
                <div style={BoxLeft}>
                    <h2>{data.person.id}</h2>
                    <h3>{data.person.name}</h3>
                    <h3>{data.person.phone}</h3>
                </div>
            </div>
        </div>
    )
}

 

 

 

mutation: savePerson

 

savePerson은 다음과 같은 쿼리로 사용할 수 있습니다.

 

const SAVE_PERSON = gql`
    mutation SavePerson($person: PersonEntityInput) {
        savePerson(person: $person) {
            id
        }
    }
`

 

 

 

PersonEntityInput 타입에 맞는 변수를 전달받은 뒤 query에 사용해주면 되고 또한 스키마 구성되어 있는 것처럼 return값으로 저장된 데이터 값에 대한 정보를 확인할 수 있습니다.

 

해당 쿼리를 실행하는 함수는 다음과 같이 구현되어 있습니다.

 

const [savePerson] = useMutation(SAVE_PERSON, {
    onCompleted: (data) => {
        // 저장이 성공적으로 수행되었을 경우
        alert(`${data.savePerson.id}이(가) 생성되었습니다.`);
        if(getPersonsRefetch.current) {
            // getPersons refetch 수행
            getPersonsRefetch.current();
        }
    }
})

 

 

 

이렇게 만들어진 함수는 화면에서 버튼을 클릭할 경우 변수 전달과 함께 실행되게 함으로 써 person을 새롭게 생성해줄 수 있습니다.

 

<div style={CommandFooter}>
    <button onClick={() => {
        // 생성 쿼리 전달
        savePerson({
            variables: {
                person: person
            }
        })
    }}>추가</button>
</div>

 

 

 

mutation: deletePerson

 

deletePerson도 savePerson과 유사합니다.

 

query는 다음과 같이 PersonEntityInput 타입에 맞는 변수를 전달받아 사용되도록 되어 있습니다.

 

const DELETE_PERSON = gql`
    mutation DeletePerson($person: PersonEntityInput) {
        deletePerson(person: $person)
    }
`

 

 

 

또한 해당 query를 실행하는 함수는 다음과 같이 구현되어 있습니다.

 

const [deletePerson] = useMutation(DELETE_PERSON, {
    onCompleted: () => {
        // 삭제가 성공적으로 수행되었을 경우
        alert('삭제되었습니다.');
        if(getPersonsRefetch.current) {
            // getPersons refetch 수행
            getPersonsRefetch.current();
        }
    }
})

 

 

 

그리고 함수 실행은 각 box들마다 가지고 있는 버튼을 이용하여 삭제될 수 있도록 되어 있습니다.

 

$person에 맞는 변수값도 전달해줘야 되기 때문에 person과 관련된 정보도 같이 전달하는 것을 확인할 수 있습니다.

 

<div style={BoxRight}>
    <button onClick={() => {
        // 삭제 쿼리 전달
        deletePerson({
            variables: { person: { id: person.id, name: '', phone: '', age: 0 } }
        })
    }}>삭제</button>
</div>

 

 

 

 

 

 

이상으로 Apollo를 이용하여 GraphQL 사용하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글