AccessToken์ ๋ง๋ฃ๊ธฐํ์ด ์ ํด์ ธ ์์ด์ ๋ง๋ฃ๋๊ณ ๋๋ฉด ์๋ก์ด AccessToken์ ๋ฐ์์์ผ ํ๋ค. ์ด๋ ์ฌ์ฉ๋๋ ํ ํฐ์ด RefreshToken
์ด๋ค.
์ ์ ๊ฐ ๋ก๊ทธ์ธ์ ํ๋ฉด ๋ฐฑ์๋๋ก๋ถํฐ AccessToken๊ณผ RefreshToken์ ๋ฐ์์ค๊ฒ ๋๋ค. AccessToken
์ 1-2์๊ฐ ์ ๋์ ์งง์ ๋ง๋ฃ ๊ธฐํ์ ๊ฐ๊ณ ์๊ณ , RefreshToken
์ 2์ฃผ~1๊ฐ์ ์ ๋์ ๊ธด ๋ง๋ฃ ๊ธฐํ์ ๊ฐ๊ณ ์๋ค.
๋ฐ๋ผ์ ๋ฐ๊ธ๋ AccessToken์ ์ ํจ ๊ธฐ๊ฐ์ด ์ง๋ ๋ง๋ฃ ๋๋ ์์ ์์ RefreshToken
์ ํตํด์ ๋ก๊ทธ์ธ ์์ด ์๋ก์ด AccessToken์ ๋ฐ์์ฌ ์ ์๊ฒ ๋๋ค.
๋ค๋ง, RefreshToken
์ญ์ ๋ง๋ฃ ๊ธฐ๊ฐ์ด ์๊ธฐ ๋๋ฌธ์ RefreshToken์ ๋ง๋ฃ ๊ธฐ๊ฐ์ด ์ง๋๊ฒ ๋๋ค๋ฉด ์ด๋๋ ๋ค์ ๋ก๊ทธ์ธ์ ์งํํด ์๋ก์ด RefreshToken์ ๊ฐ์ ธ์ค๋ ๊ณผ์ ์ด ํ์ํ๋ค.
๋ณ์์๋ accesstoken์ ๋ฃ๊ณ , ์ฟ ํค์ refreshtoken์ ๋ฃ์ด์ accesstoken์ ํตํด ๊ถํ ๋ถ๊ธฐ ๋ฑ์ ํด์ค ์ ์๋ค.
์ฟ ํค๋ secure / httpOnly์ ๊ฐ์ ์ต์ ์ด ์์ด์ ๋ณด์์ด ๋ ์ ๋์ด ์๋ค. ๋ฐฑ์๋์์ ์ต์ ์ ๊ฑด ์ฑ๋ก ๋ธ๋ผ์ฐ์ ๋ก ๋ณด๋ด๋ฉด, ๋ธ๋ผ์ฐ์ ์์๋ recoil์ ํ ํฐ์ ๋ด๊ฑฐ๋ ํ ์ ์๋ค.
httpOnly: ๋ธ๋ผ์ฐ์ ์์ Javascript๋ฅผ ์ด์ฉํด ์ฟ ํค์ ์ ๊ทผํ ์ ์๊ณ , ํต์ ์ผ๋ก๋ง ํด๋น ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋ค.
secure: https ํต์ ์์๋ง ํด๋น ์ฟ ํค๋ฅผ ๋ฐ์์ฌ ์ ์๋ค.
fetchUser api ์์ฒญ์ ํด์ (unauthenticated error)ํ ํฐ๋ง๋ฃ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค๋ฉด, restoreAccessToken api๋ฅผ ํตํด ํ ํฐ์ ์ฌ๋ฐ๊ธ ๋ฐ๊ณ , ํ ํฐ(accesstoken) ์ฌ๋ฐ๊ธ ํ์ ์คํจํ ์ฟผ๋ฆฌ์ accesstoken๋ง ๋ฐ๊ฟ์ api ์์ฒญ์ ์ฌ์๋ํ๋ฉด, ์ธ๊ฐ์ ์ฑ๊ณตํ๊ฒ ๋๋ค.
์ ๊ณผ์ ์ silent authentication
์ด๋ผ๊ณ ํ๋ค. ์ ์ ๋ ํ ํฐ์ด ์ฌ๋ฐ๊ธ๋๋ ๊ฒ์ ์์ง ๋ชปํ๊ณ , ๋ท๋จ์์ ์กฐ์ฉํ ์ด ๊ณผ์ ์ด ๋น ๋ฅด๊ฒ ์งํ๋๋ ๊ฒ์ด๋ค.
- AccessToken ๋ง๋ฃ ํ ์ธ๊ฐ ์์ฒญ
- ํด๋น ์ค๋ฅ๋ฅผ ํฌ์ฐฉํด์ ์ธ๊ฐ ์๋ฌ์ธ์ง ์ฒดํฌ
- RefreshToken์ผ๋ก AccessToken ์ฌ๋ฐ๊ธ ์์ฒญ
- ๋ฐ๊ธ ๋ฐ์ AccessToken์ state์ ์ฌ์ ์ฅ
- ๋ฐฉ๊ธ ์คํจํ๋(error) API๋ฅผ ์ฌ์์ฒญ
์์ฆ ๋ฐฑ์๋์์๋ ๋ก๊ทธ์ธ ๊ด๋ จ API๋ค์ AuthService๋ก, ๋ก๊ทธ์ธ์ ์ ์ธํ API๋ค์ ResourceService๋ก ๋๋๋ ์ถ์ธ๋ค. ResourceService๋ UserService, BoardService ๋ฑ์ผ๋ก ์์ํ๊ฒ ์ชผ๊ฐ๊ธฐ๋ ํ๋ค(MicroServiceArchitecture
).
๋ํ, ์์
๋ก๊ทธ์ธ(๊ตฌ๊ธ ๋ก๊ทธ์ธ, ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฑ)์ ํ ๋์๋ Open Authentication(OAuth
) ๋ฅผ ์ฌ์ฉํ๋ค.
์ฌ๋ฌ๊ฐ์ง ์ฟผ๋ฆฌ ๋ฐฉ์
1. useQuery: ํ์ด์ง ์ ์ํ๋ฉด ์๋์ผ๋ก data์ ๋ฐ์์ง๊ณ (data๋ ๊ธ๋ก๋ฒ์คํ ์ดํธ ์ ์ฅ) ๋ฆฌ๋ ๋๋ง๋จ
2. useLazyQuery: ๋ฒํผ ํด๋ฆญ์ data์ ๋ฐ์์ง๊ณ (data๋ ๊ธ๋ก๋ฒ์คํ ์ดํธ ์ ์ฅ) ๋ฆฌ๋ ๋๋ง๋จ
3. useApolloClient:axios
์ฒ๋ผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ(data๋ ๊ธ๋ก๋ฒ์คํ ์ดํธ ์ ์ฅ). ์ํ๋ ์์ ์ ์คํ ํ ์ํ๋ ๋ณ์์ ๋ด๋ ๋ฐฉ์.
useApolloClient
๋ฅผ ์ฌ์ฉํด์ ๋ฒํผ์ ๋๋ ์ ๋ fetchUserLoggedIn์ ๋ฐ์์๋ค.const FETCH_USER_LOGGED_IN = gql`
query fetchUserLoggedIn {
fetchUserLoggedIn {
email
name
}
}
`;
export default function LoginSuccessPage() {
const client = useApolloClient();
const onClickButton = async () => {
const result = await client.query({
query: FETCH_USER_LOGGED_IN,
});
console.log(result);
};
return (
<button onClick={onClickButton}>ํด๋ฆญํ์ธ์</button>
);
}
apollo setting
ํ์ผ ์์ export default function ApolloSetting(props: IApolloSettingProps) {
const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
const [userInfo, setUserInfo] = useRecoilState(userInfoState);
if (!accessToken || !userInfo) return;
setUserInfo(JSON.parse(userInfo));
}, []);
const errorLink = onError(({ graphQLErrors, operation, forward })=>{
// 1-1. ์๋ฌ๋ฅผ ์บ์น
// 1-2. ํด๋น์๋ฌ๊ฐ ํ ํฐ๋ง๋ฃ ์๋ฌ์ธ์ง ์ฒดํฌ(UNAUTHENTICATED)
// 2-1. refreshToken์ผ๋ก accessToken์ ์ฌ๋ฐ๊ธ ๋ฐ๊ธฐ
// 2-2. ์ฌ๋ฐ๊ธ ๋ฐ์ accessToken ์ ์ฅํ๊ธฐ
// 3-1. ์ฌ๋ฐ๊ธ ๋ฐ์ accessToken์ผ๋ก ๋ฐฉ๊ธ ์คํจํ ์ฟผ๋ฆฌ์ ๋ณด ์์ ํ๊ธฐ
// 3-2. ์ฌ๋ฐ๊ธ ๋ฐ์ accessToken์ผ๋ก ๋ฐฉ๊ธ ์์ ํ ์ฟผ๋ฆฌ ์ฌ์์ฒญํ๊ธฐ
})
const uploadLink = createUploadLink({
uri: "http://backend08.codebootcamp.co.kr/graphql",
headers: { Authorization: `Bearer ${accessToken}` },
credentials: "include",
});
const client = new ApolloClient({
link: ApolloLink.from([uploadLink]),
cache: APOLLO_CACHE,
connectToDevTools: true,
});
return (
<ApolloProvider client={client}>
{props.children}
</ApolloProvider>
)
}
graphQLErrors : ์๋ฌ๋ค์ ์บ์นํด์ค
operation : ๋ฐฉ๊ธ์ ์ ์คํจํ๋ ์ฟผ๋ฆฌ๊ฐ ๋ญ์๋์ง ์์๋
forward : ์คํจํ๋ ์ฟผ๋ฆฌ๋ค์ ์ฌ์ ์ก
errorLink
์์ฑ ๋ฐ ๊ตฌ์กฐ ๊ธฐ๋ฐ ์ค์ // apollo setting ํ์ผ์ errorLink๋ถ๋ถ
import { onError } from "@apollo/client/link/error";
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
// 1. ์๋ฌ๋ฅผ ์บ์น
if (graphQLErrors) {
for (const err of graphQLErrors) {
// 2. ํด๋น ์๋ฌ๊ฐ ํ ํฐ ๋ง๋ฃ ์๋ฌ์ธ์ง ์ฒดํฌ(UNAUTHENTICATED)
if (err.extensions.code === "UNAUTHENTICATED") {
// 3. refreshToken์ผ๋ก accessToken์ ์ฌ๋ฐ๊ธ ๋ฐ๊ธฐ
}
}
}
});
์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ์๊ธด๋ค. refreshToken์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ graphQL ์์ฒญ์ ๋ณด๋ด์ผ ํ๋๋ฐ, errorLink๋ฅผ ์์ฑํ๋ ์ฝ๋๋ ApolloProvider ๋ฐ๊นฅ์ ์๊ธฐ ๋๋ฌธ์ useQuery๋ useApolloClient๋ฑ์ ์ด์ฉํด graphQL ์์ฒญ์ ๋ณด๋ผ ์ ์๋ค.
๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ graphql-request๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ๋ณผ ๊ฒ์ด๋ค.
graphql-request ์ค์น: yarn add graphql-request
graphql-request ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ๋ค์๊ณผ ๊ฐ์ด error๋ฅผ ์บ์นํ ๋ค accessToken์ ์ฌ๋ฐ๊ธ ๋ฐ๋ ์ฝ๋๋ฅผ ์ ์ด์ค๋ค.
import { GraphQLClient } from "graphql-request";
export default function ApolloSetting(props: IApolloSettingProps) {
const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
const [userInfo, setUserInfo] = useRecoilState(userInfoState);
const RESTORE_ACCESS_TOKEN = gql`
mutation restoreAccessToken {
restoreAccessToken {
accessToken
}
}
`;
if (!accessToken || !userInfo) return;
setUserInfo(JSON.parse(userInfo));
}, []);
// ๋ฆฌํ๋ ์ ํ ํฐ ๋ง๋ฃ ์๋ฌ ์บ์น & ๋ฐ๊ธ
const errorLink = onError(({ graphQLErrors, operation, forward })=>{
// 1-1. ์๋ฌ๋ฅผ ์บ์น
if(graphQLErrors){
for(const err of graphQLErrors){
// 1-2. ํด๋น ์๋ฌ๊ฐ ํ ํฐ๋ง๋ฃ ์๋ฌ์ธ์ง ์ฒดํฌ(UNAUTHENTICATED)
if (err.extensions.code === "UNAUTHENTICATED") {
// 2-1. refreshToken์ผ๋ก accessToken์ ์ฌ๋ฐ๊ธ ๋ฐ๊ธฐ
const graphqlClient = new GraphQLClient(
"https://backend-practice.codebootcamp.co.kr/graphql",
{ credentials: "include" }
);
const result = await graphqlClient.request(RESTORE_ACCESS_TOKEN);
// RESTORE_ACCESS_TOKEM์ด๋ผ๋ gql์ ์์ฒญํ ๋ค ๋ฐํ๋๋ ๊ฒฐ๊ณผ๊ฐ์ result์ ๋ด๋๋ค.
const newAccessToken = result.restoreAccessToken.accessToken;
// 2-2. ์ฌ๋ฐ๊ธ ๋ฐ์ accessToken ์ ์ฅํ๊ธฐ
setAccessToken(newAccessToken);
//3-1. ์ฌ๋ฐ๊ธ ๋ฐ์ accessToken์ผ๋ก ๋ฐฉ๊ธ ์คํจํ ์ฟผ๋ฆฌ์ ๋ณด ์์ ํ๊ธฐ
if(typeof newAcessToken !== "string") return
operation.setContext({
headers: {
...operation.getContext().headers,
Authorization: `Bearer ${newAccessToken}`, // accessToken๋ง ์๊ฑธ๋ก ๋ฐ๊ฟ์น๊ธฐ
},
});
//3-2. ์ฌ๋ฐ๊ธ ๋ฐ์ accessToken์ผ๋ก ๋ฐฉ๊ธ ์์ ํ ์ฟผ๋ฆฌ ์ฌ์์ฒญํ๊ธฐ
forward(operation)
}
}
}
})
const uploadLink = createUploadLink({
uri: "http://backend08.codebootcamp.co.kr/graphql",
headers: { Authorization: `Bearer ${accessToken}` },
credentials: "include",
});
const client = new ApolloClient({
link: ApolloLink.from([uploadLink]),
cache: APOLLO_CACHE,
connectToDevTools: true,
});
return (
<ApolloProvider client={client}>
{props.children}
</ApolloProvider>
)
}
graphql-request
๋ฅผ ์ด์ฉํ์ฌ accessToken์ ์ฌ๋ฐ๊ธ ๋ฐ๋ ์ฝ๋๋ฅผ ๋ณ๋ ํจ์๋ก ๋ถ๋ฆฌํ์ฌ ์
๋ ฅํด ์ค๋ค.import { gql } from "@apollo/client";
import { GraphQLClient } from "graphql-request";
const RESTORE_ACCESS_TOKEN = gql`
mutation restoreAccessToken {
restoreAccessToken {
accessToken
}
}
`;
export async function getAccessToken() {
try {
const graphqlClient = new GraphQLClient(
"https://backend-practice.codebootcamp.co.kr/graphql",
{
credentials: "include",
}
);
const result = await graphqlClient.request(RESTORE_ACCESS_TOKEN);
const newAccessToken = result.restoreAccessToken.accessToken;
return newAccessToken;
} catch (error) {
console.log(error.message);
}
}
์ฝ๋๋ฅผ ๋ถ๋ฆฌํ ํ์ errorLink ๋ด๋ถ ๋ก์ง์ ์์ ํด์ค๋ค. ํจ์์ return ๊ฐ์ด promise ์ด๋ฏ๋ก .then()
์ ์ด์ฉํด์ ์ดํ์ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
// 2-1. refreshToken์ผ๋ก accessToken์ ์ฌ๋ฐ๊ธ ๋ฐ๊ธฐ
return fromPromise(
getAccessToken().then((newAccessToken) => {
// ํ ํฐ์ ๋ฃ์ด๋๋ global state - recoilState
const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
useEffect(() => {
// 1. ๊ธฐ์กด๋ฐฉ์(refreshToken ์ด์ )
// console.log("์ง๊ธ์ ๋ธ๋ผ์ฐ์ ๋ค!!!!!");
// const result = localStorage.getItem("accessToken");
// console.log(result);
// if (result) setAccessToken(result);
// 2. ์๋ก์ด๋ฐฉ์(refreshToken ์ดํ) - ์๋ก๊ณ ์นจ ์ดํ์๋ ํ ํฐ ์ ์งํ ์ ์๋๋ก
void getAccessToken().then((newAccessToken) => {
setAccessToken(newAccessToken);
});
}, []);
์ฐ์์ ์ธ ๋น๋๊ธฐ ์์
(์ฐ์์ ์ธ ํ์ด์ง ํด๋ฆญ, ์ฐ์์ ์ธ ๊ฒ์์ด ๋ณ๊ฒฝ ๋ฑ)์ ์ํด observable ์ฌ์ฉํ๋ค.
์๋ฅผ ๋ค์ด, 3๋ฒ ํ์ด์ง๋ฅผ ์์ฒญํ๋ค๊ฐ ๋น ๋ฅด๊ฒ 5๋ฒ ํ์ด์ง๋ฅผ ์์ฒญํ์ ๊ฒฝ์ฐ 3๋ฒ ํ์ด์ง ์์ฒญ์ ์ทจ์ ํ 5๋ฒ ํ์ด์ง๋ฅผ ๋ณด๋ด์ค์ผ ํ๋๋ฐ, ๋ฐฑ์๋์์๋ 3๋ฒ ํ์ด์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ฒ ๋๋ค.
์ด๋ฐ ๊ฒฝ์ฐ์๋ 3๋ฒ ํ์ด์ง ์์ฒญ์ ์ทจ์ํด์ผ ํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ์ฌ์ฉ์์ ๋ถํธํ ๊ฒฝํ์ ์ด๋ํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
ํ์ง๋ง, ์ด๋ฐ๊ฒฝ์ฐ๋ promise๋ก ์ฒ๋ฆฌํ๋๊ฒ ์ฝ์ง ์๋ค. ์ด๋ด ๋ observable
์ ์ฌ์ฉํ๋ฉด ์ข๋ค!
์๋ฐ์คํฌ๋ฆฝํธ ๋งค์๋์๋ ๋ค๋ฅธ flatMap์ด๋ค. ์ฌ๊ธฐ์ ์ฌ์ฉํ๋ flatMap์ apollo-client์์ ์ง์ํ๋ flatMap
์ด๋ค!
์ค์ต์ apollo-client
์์ ์ง์ํ๋ observable์ ์ฌ์ฉํ๋ค.
์ค์น๋ชฉ๋ก
yarn add zen-observable
yarn add @types/zen-observable --dev
yarn add graphql-request
์ค์ต ๋ด์ฉ์ ๋ค์๊ณผ ๊ฐ๋ค.
import {from} from 'zen-observable'
export default function ObservableFlatmapPage(): JSX.Element{
const onClickButton = (): void => {
// new Promise((resolve, reject) => {})
// new Observable((observer) => {})
// from์ hoverํ๋ฉด observable์ด ๋์ด
from(["1๋ฒ useQuery", "2๋ฒ useQuery", "3๋ฒ useQuery"]) // fromPromise
.flatMap((el: string)=> from([`${el} ๊ฒฐ๊ณผ์ qqq ์ ์ฉ`,`${el} ๊ฒฐ๊ณผ์ zzz ์ ์ฉ`]))
.subscribe((el)=>(console.log(el)))
} // subscribe -> ๋น๋๊ธฐ์ ์ธ ์์ ์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์ง์์ ์ผ๋ก ํธ์ถ ๊ฐ๋ฅ
return <button onClick={onClickButton}> ํด๋ฆญ</button>
}
fromPromise?
promise๋ฅผ observableํ์ ์ผ๋ก ๋ฐ๊ฟ์ฃผ๋ ๋๊ตฌ.
onError ํจ์๋ return ํ์ ์ผ๋ก observable์ ๋ฐ๊ณ ์์ง๋ง, ์ฐ๋ฆฌ๊ฐ return ํด์ฃผ๋ ๊ฐ์ promise์ด๊ธฐ ๋๋ฌธ์ fromPromise๋ฅผ ์ฌ์ฉํด์ observable ํ์ ์ผ๋ก ๋ฐ๊ฟ์ค์ผ ํจ.