ApolloClient onError๋ฅผ ํ์ฉํ Token Error Handling ๐ง๐ปโโ๏ธ
ApolloClient์
onError
๋ฉ์๋๋ฅผ ํตํด graphql ์๋ฒ, ๋๋ network์์ ๋ฐ์ํ ์๋ฌ๋ฅผ ์บ์นํ์ฌ ๊ฐ๊ฐ์ ์๋ฌ ์ํฉ์ ๋ง๋ ์ ์ ํ ํธ๋ค๋ง์ ํ ์ ์๋ค.
์ด๋ฒ ํฌ์คํ ์์๋ ํ ํฐ์ด ์ ํจํ์ง ์์ ๊ฒฝ์ฐ ๋ฐ์ํ๋ Unauthorized ์๋ฌ๋ฅผ ํธ๋ค๋งํ๋ ๋ฐฉ๋ฒ์ ์์๋ณธ๋ค.
์ฐ์ , Apollo Client ์ธ์คํด์ค๋ฅผ ๋ง๋ ๋ค.
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
const token = localStorage.getItem("token");
const httpLink = createHttpLink({
uri: "http://localhost:4000/graphql",
});
export const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
});
์๋ฒ๋ก ํด๋ผ์ด์ธํธ๊ฐ ๊ฐ์ง๊ณ ์๋ token์ authLink๋ฅผ ์ถ๊ฐํ์ฌ ํค๋์ ์ค์ด ๋ณด๋ด๋ ๋ก์ง์ ์์ฑํด๋ณด์.
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
const httpLink = createHttpLink({
uri: "http://localhost:4000/graphql",
});
const authLink: ApolloLink = setContext(async (_, { headers }) => {
const token = localStorage.getItem("token");
return {
headers: {
...headers,
token: token,
},
};
});
export const client = new ApolloClient({
link: from(authLink, httpLink),
cache: new InMemoryCache(),
});
authLink๋ฅผ ํตํด localStorage์ ๋ด๊ธด token์ด header์ ๋ด๊ฒจ ์๋ฒ๋ก ์ ์ก๋ ๊ฒ์ด๋ค.
์๋ฒ์์ ๊ตฌํ๋ ์ฌ์ฉ์ ์ธ์ฆ ์๋๋ฆฌ์ค์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ง๋ง,
- accessToken๊ณผ refreshToken์ ์ด์ฉํด ์ฌ์ฉ์์ ํ ํฐ์ ๊ฒ์ฆํ๋ ์ํฉ์์ ์ฌ์ฉ์์ accessToken์ด ๋ง๋ฃ๋ ๊ฒฝ์ฐ,
- ๋๋ ๋ก๊ทธ์ธ์ ํ๊ธฐ ์ ์๋ ๋ฏธ๋ก๊ทธ์ธ ์ฌ์ฉ์(anonymous user)๋ก์ ํ ํฐ ๋ฐ๊ธ์ด ํ์ํ ๊ฒฝ์ฐ
์ฌ์ฉ์๊ฐ ์น์ฌ์ดํธ์ ์ง์
ํ๋ฉด ์๋ฒ๋ก๋ถํฐ Unauthorized
์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค.
์ด๋ฌํ ๊ฒฝ์ฐ apollo client์ onError ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด `Unauthorized` ์๋ฌ ๋ฐ์ ์ ํ ํฐ ์ฌ๋ฐ๊ธ์ ํตํด ์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
// ...
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let error of graphQLErrors) {
switch (error.extensions.code) {
case "UNAUTHENTICATED":
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
}
}
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
}
);
export const client = new ApolloClient({
link: from(errorLink, authLink, httpLink),
cache: new InMemoryCache(),
});
graphQL ์์ฒญ ์ ๋ฐ์ํ๋ ์๋ฌ๋ ํฌ๊ฒ graphQL error, network error๋ก ๋๋ ์ ์๋ค.
apollo client์์๋ ์๋ฌ๋ฅผ ํธ๋ค๋งํ๊ธฐ ์ํ link๋ก onError์ RetryLink ๊ฐ ์๋๋ฐ ๊ณต์๋ฌธ์์์๋ graphQL error๋ onError ๋ก, network error๋ RetryLink๋ก ๊ฐ๊ฐ ํธ๋ค๋งํ ๊ฒ์ ๊ถ์ฅํ๊ณ ์๋ค.
์๋ฒ์์์ ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ์์ ํต๊ณผํ์ง ๋ชปํ ์์ฒญ์์ ๋ฐ์ํ๋ ์๋ฌ๋ graphQL์๋ฒ์์ ๋ฐ์์ํค๋ graphQL Error์ด๋ฏ๋ก onError๋ฅผ ์ด์ฉํด ์๋ฌ๋ฅผ ์ฒ๋ฆฌํด์ฃผ์๋ค.
onError๋
- onError๋ฅผ ์ด์ฉํด graphQL ์๋ฒ๋ก ๋ฐ์ดํฐ ์์ฒญ ์ค์ ๋ฐ์ํ ์๋ฌ๋ฅผ ์บ์นํ์ฌ ์ ์ ํ ํธ๋ค๋ง ๋ก์ง์ ์ถ๊ฐํด์ค ์ ์๋ค.
- ์์ฒญ์ ๋ค์ ๋ณด๋ด๊ธฐ ์ํด forwardํจ์๋ฅผ ํธ์ถํด์ผํ๋ค.
- ๋ง์ฝ ์ฌ์์ฒญ์์๋ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ, onError๋ ๋๋ค์ ์์ฒญ์ ๋ณด๋ด์ง๋ ์๋๋ค. (์ด ๊ฒฝ์ฐ ๋ฌดํ ๋ฃจํ์ ๋น ์ง ์ ์๊ธฐ ๋๋ฌธ) ์ฆ, ์คํจํ operation ๋น ํ ๋ฒ๋ง ์ฌ์์ฒญํ๋ค.
โ๏ธProblem
getNewToken()์ ๋น๋๊ธฐ์ ์ผ๋ก ์ํ๋๋ ํจ์์ด๋ค.(์๋ง ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ๊ทธ๋ด ๊ฒ์ด๋ผ ์๊ฐํ๋ค.)
๊ทธ๋์ async ๋ promise์ ๊ฐ์ ๋ฌธ๋ฒ์ ์ถ๊ฐํ์ฌ ํด๊ฒฐํ๊ณ ์ํ์๋๋ฐ, onError์ ์ฝ๋ฐฑํจ์์ async ํค์๋๋ฅผ ์ถ๊ฐํ๋๋ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
ErrorHandler์ ๋ฐํ๊ฐ์ Observable ๊ฐ์ฒด์ด๊ฑฐ๋ ์์ด์ผ ํ๋ค(void | Observable).
๊ทธ๋ฐ๋ฐ async ํค์๋๊ฐ ๋ถ์ ํจ์๋ Promise๋ฅผ ๋ฐํํ๊ฒ ๋๋ฏ๋ก ์์๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
..Observable์ ๋ฌด์์ธ๊ฐ๐ค
๋จผ์ RxJS๋ถํฐ ์ดํด๋ณด์.
RxJS
- RxJS๋ ํ์ผ ์ฝ๊ธฐ, data fetching, ํค ์ ๋ ฅ ๋ฑ์ ์ด๋ฒคํธ ์์ค๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํ JS์ ํ์ฅ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ์ฝ๋ฐฑ์ด๋ ํ๋ก๋ฏธ์ค๋ฅผ ๋์ฒดํ ์ ์๋ค.
- RxJS๋ ์ด๋ฒคํธ ์คํธ๋ฆผ์ observable ์ด๋ผ๋ ๊ฐ์ฒด๋ก ํํํ ํ ๋น๋๊ธฐ๋ก ๋์ํ๋ ๋ก์ง์ ๊ตฌํํ๋ค.
- observable ๊ฐ์ฒด๋ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๋ง๋๋ ๋ฐ์ดํฐ ์์ฐ์์ด๋ค.
- Apollo ๋ Observable ๊ตฌํ์ ์ํด zen-observable์ด๋ผ๋ ํจํค์ง๋ฅผ ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ๊ณ ์๋ค.
์ฐ์ Observable์ apollo link์์ ๋น๋๊ธฐ์ ์ธ ๋์์ ์ฒ๋ฆฌํ๋ ๊ฐ์ฒด๋ก, getNewToken()์ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์ํ ๊ฐ์ฒด๋ผ๊ณ ์ดํดํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.
๐กSolution
apollo-link ํจํค์ง์์๋ promise๋ฅผ ์ด์ฉํด observable ๊ฐ์ฒด๋ฅผ ์์ฑํด์ฃผ๋ fromPromise
๋ผ๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค.
fromPromise
- from promise to observable์ ์๋ฏธ์ธ ๊ฒ ๊ฐ๋ค.(์๋ง๋..?)
- promise ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ์ ๋ฌํ๋ฉด observable ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let error of graphQLErrors) {
switch (error.extensions.code) {
case "UNAUTHENTICATED":
return fromPromise(getNewToken())
.filter((value) => {
return Boolean(value);
})
.flatMap((token) => {
setUserToken({ isKeepLogin: false, token });
const oldHeaders = operation.getContext().headers;
// ์ด์ header๋ฅผ ํ์ฉํด ์ด์ ์์ฒญ์ context๋ฅผ ์ฌ์์ฑ
operation.setContext({
headers: {
...oldHeaders,
token: token || "",
},
});
return forward(operation); // ์คํจํ ์ด์ ์์ฒญ ๋ค์ ๋ณด๋ด๊ธฐ
});
}
}
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
}
);
filter: ๊ฒฐ๊ณผ๊ฐ ์ค ์ฝ๋ฐฑํจ์๋ฅผ ๋ง์กฑํ๋ ๊ฒฐ๊ณผ๊ฐ๋ง ํํฐ๋ง
flatMap(mergeMap): ๋ฐํ๋ observable ๊ฐ์ฒด๋ค์ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๋ณํฉํ์ฌ ๊ฒฐ๊ณผ๊ฐ์ ๋ด๋ณด๋ธ๋ค.
์ต์ข ApolloClient Provider ์ฝ๋
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
const httpLink = createHttpLink({
uri: "http://localhost:4000/graphql",
});
const authLink: ApolloLink = setContext(async (_, { headers }) => {
const token = localStorage.getItem("token");
return {
headers: {
...headers,
token: token,
},
};
});
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let error of graphQLErrors) {
switch (error.extensions.code) {
case "UNAUTHENTICATED":
return fromPromise(getNewToken())
.filter((value) => {
return Boolean(value);
})
.flatMap((token) => {
setUserToken({ isKeepLogin: false, token });
const oldHeaders = operation.getContext().headers;
// ์ด์ header๋ฅผ ํ์ฉํด ์ด์ ์์ฒญ์ context๋ฅผ ์ฌ์์ฑ
operation.setContext({
headers: {
...oldHeaders,
token: token || "",
},
});
return forward(operation); // ์คํจํ ์ด์ ์์ฒญ ๋ค์ ๋ณด๋ด๊ธฐ
});
}
}
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
}
);
export const client = new ApolloClient({
link: from(errorLink, authLink, httpLink),
cache: new InMemoryCache(),
});
ApolloClient์์ link๋ฅผ ์์ ๊ฐ์ด from์ผ๋ก ์ฐ๊ฒฐํด์ฃผ๋ฉด link chain์ด ํ์ฑ๋๋ค.
Link Chain
from
๋ฉ์๋๋ ์ฌ๋ฌ ๊ฐ์ ๋งํฌ๋ฅผ ๊ฒฐํฉํ์ฌ ํ๋์ link chain์ ํ์ฑํด์ฃผ๋ ๋ฉ์๋์ด๋ค. httpLink๋ ์ข ๋ฃ ๋งํฌ(terminating link)๋ก์ ๋งํฌ ์ฒด์ด๋์ ๊ฐ์ฅ ์ข ๋จ์ ์์นํ์ฌ graphQL ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด๊ณ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋ link์ด๋ค.- link chain์ผ๋ก ์ฐ๊ฒฐ๋์ด์๋ link๋ค์
forward
๋ฅผ ํตํด ์ฒด์ธ ์์ ๋ค์ ๋งํฌ๋ฅผ ํธ์ถํ๋ค.
๊ฒฐ๋ก
๊ธฐ์กด์๋ ApolloClientProvider๊ฐ ์ฌ์ฉ๋๋ ์ปดํฌ๋ํธ์์ token์ ๊ฒ์ฌํด token์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ง ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ์๋ค. ๊ทธ๋ฌ๋ onError ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด์ ๋ ๋ช
ํํ๊ฒ ํ ํฐ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๊ฒฝ์ฐ์ ๋ํ ํธ๋ค๋ง์ ํด์ค ์ ์๊ฒ ๋์๋ค. ๋ํ ํ ํฐ ์๋ฌ ๋ง๊ณ ๋ ๋ฐ์ดํฐ ์์ฒญ ์ค ๋ฐ์ํ๋ graphQL error, network error๋ค๋ ํ ๊ณณ์์ ํธ๋ค๋งํด์ค ์ ์๊ฒ ๋์ด ์ถํ์ ์๋ฌ ๋ชจ๋ํฐ๋ง ์์คํ
์ ๋์
ํ๊ฒ ๋ ๊ฒฝ์ฐ ๋ฌธ์ ๋ฅผ ์ฝ๊ฒ ํด๊ฒฐํ ์ ์์ ๊ฒ ๊ฐ๋ค.
apollo link๊ฐ link chain ๋ฐฉ์์ผ๋ก ๊ตฌํ๋์ด์๋ค๋ ๊ฒ ๋๋ฌด ์ ๊ธฐํ๋ค. ์ฌ๊ธฐ์ ๋น๋๊ธฐ ๋์ ์ฒ๋ฆฌ๋ฅผ ์ํด ์ฌ์ฉ๋ observable๊ฐ์ฒด์ ๋ํด์๋ ์ข ๋ ์กฐ์ฌํด๋ณด๊ณ rxJS์ ๋ํด ์ข ๋ ์์๋ณด๊ณ ์ถ๋ค.