앞서 1편에서 backend부분의 type, query, mutation정의를 완료하였다!
RestAPI같이 각각의 endpoint도 필요하지 않고, uri의 설정 및 세팅도 필요없고 매우 빠르게 작성할 수 있었다 (물론 더 숙련이 된다면...)
이번에는 client-side에서 graphql을 어떻게 사용했는지 다뤄보려고 한다.
프로젝트를 제작한 뒤 찾아보니 graphql의 캐시를 이용해 local state로 token을 관리하며 로그인을 구현하는 방법이 따로 있었다... 하지만 그걸 몰랐기에 전에 사용했던 redux를 통해 로그인한 유저 정보 및 token을 관리하였다.
const initialState: AuthState = {
user: null,
}
const reducer = handleActions<AuthState, UserType>(
{
[LOGIN]: (state, action) => {
TokenService.setToken(action.payload.token)
return { user: action.payload }
},
[LOGOUT]: () => {
TokenService.removeToken()
return { user: null }
},
},
initialState
)
initialState와 reducer는 보는 코드 그대로, TokenService class를 통해 구현 로그인 시 localStorage에 토큰을 생성해주고 로그아웃 시 토큰을 제거해주었다.
(graphql에 더 초점을 맞추고 싶으니 redux는 패스...)
그러면, 먼저 로그인 부분을 보자
- 참고로 해당 프로젝트는 redux를 사용하였기 때문에 Container-Presenter패턴을 적용하였고, 로그인 토큰의 존재 여부를 판단하여 Container컴포넌트에서 Redirect를 곧바로 진행해주었다.
if (token !== null) {
return <Navigate to="/" />
}
항상 Container에서는 상태만 받아오고 판단은 Presenter부분에서 했는데, Container에서 상태판단까지 할 경우 Presenter가 훨~씬 깔끔해 질 수 있었다... 이걸 왜 생각하지 못했을까
- 또 참고로, react-router-dom이 v6으로 업데이트 되면서 Redirect가 없어졌다고 해서 Navigate를 사용해보았다. Navigate가 Redirect를 대체한다,,, 는 아닌거같지만 정상적으로 작동이 되길래 적용했다.
그 후 gql을 불러와야 하는데, 컴포넌트가 길어지는 게 딱 질색이라 gql폴더를 만들어서 export하기로 했다.
import { gql } from "@apollo/client"
export const REGISTER_USER = gql`
mutation register(
$username: String!
$email: String!
$password: String!
$confirmPassword: String!
) {
register(
registerInput: {
username: $username
email: $email
password: $password
confirmPassword: $confirmPassword
}
) {
id
email
username
createdAt
token
}
}
`
export const LOGIN_USER = gql`
mutation login($username: String!, $password: String!) {
login(username: $username, password: $password) {
id
email
username
createdAt
token
}
}
`
register와 login을 gql user폴더에 밀어넣기
$(달러)표시를 통해 변수를 지정할 수 있다. 이러한 Query와 변수들을 이제 진짜 client-side에서 사용해보자
mutation을 불러오기 위해 useMutation이라는 React hook을 사용할 수 있다.
const [loginUser, { loading, error }] = useMutation(LOGIN_USER, {
variables: { username, password },
})
이 때 loginUser는 데이터를 fetch하고 싶은 부분에 넣으면 된다.
loading, error은 말그대로 로딩과 에러라서 로딩 부분 스타일링 및 에러 핸들링을 아래와 같이 본인이 편한 코드로 구현해 주면 된다.
if(error){ return <p>Error</p> } if(loading){ return <p>Loading...</p> }
다음, variables부분은 변수이고 앞서 선언했던 mutation에
mutation login($username: String!, $password: String!)
$부분으로 들어가게 된다. 퍼즐 끼워맞추는 느낌으로 간단하게 구현해 줄 수 있다!
그렇다면 회원가입은 총 4개의 변수를 받기 때문에 아래와 같이 variables부분에 4개의 변수를 넣어주면 되겠죠?
const [RegisterUser, { loading }] = useMutation(REGISTER_USER, {
variables: { username, email, password, confirmPassword },
})
(참 쉽죠)
또 한 가지 방법은 variables를 fetch함수 부분에 곧바로 넣어주는 것이다.
const [loginUser, { loading, error }] = useMutation(LOGIN_USER)
...
<div
onClick={() => loginUser({variables: { username, password }})}>
로그인
</div>
본인이 더 편한 방법을 사용하면 될 거 같다
이렇게 끝낼 수 있는 useMutation이라면 참 좋겠지만 아무리 그래도 데이터 fetching하는게 이렇게 간단하진 않겠지... 이 과정에는 치명적인 결함이 있다. 만약 우리가 mutation을 통해 데이터를 추가한다고 해도 화면에 바로 나타나지는 않고, 새로고침을 해주면 나타난다. 추가한 데이터를 바로 화면에 랜더링 해줘야 하는 경우 useQuery에서 사용할 수 있는 refetch함수를 이용하여 해결할 수 있다.
useQuery 사용은 다음 일지에...!