๊ฐ๋ฐ ์ด๋ฐ์๋ ์ฃผ๋ก ๋ชฉ์ ๋ฐ์ดํฐ๋ก UI๋ง ๋น ๋ฅด๊ฒ ๊ตฌ์ฑํด์์ง๋ง,
์ด์ ๋ ์ค์ ์๋ฒ์ ํต์ ํด์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๊ณ , ์ํ์ ๋ฐ๋ผ ํ๋ฉด์ด ๋ฐ๋๋๋ก ๋ง๋ค์ด์ผ ํ๋ค.
์ฐ๋ฆฌ๋ Swagger๋ก ์ ์๋ REST API๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ฐ๋ฐ์ ์์ํ๊ณ ,
์ฒ์์ ๋น์ฐํ axios.get(), axios.post()๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ฉด ๋์ง ์์๊น ์๊ฐํ๋ค.
ํ์ง๋ง API๊ฐ ๋์ด๋๊ณ ํ๋ฉด์ด ๋ง์์ง๋ฉด์,
โ์ด๊ฑธ ์ผ์ผ์ด ๋ค ์ง๋ ๊ฑด ๋๋ฌด ๊ท์ฐฎ๊ณ , ์ ์ง๋ณด์๋ ์ด๋ ต๋คโ๋ ๊ฒฐ๋ก ์ ๋๋ฌํ๋ค.
๊ทธ๋์ ์ฐ๋ฆฌ๋ ์ง์ ๋ง๋ API ํต์ ๊ตฌ์กฐ์ธ kepler-http ๋ฅผ ๋์ ํ๋ค.
๊ฐ๋จํ ๋งํ๋ฉด, kepler-http๋
ํ๋์ ์์ฒญ ๋จ์๋ฅผ ๋ช
ํํ๊ฒ ์ ์ํ๊ณ ,
fetcher ํจ์ ํ๋๋ก ๋ชจ๋ ์์ฒญ์ ์ฒ๋ฆฌํ๋๋ก ๋ง๋ ํต์ ๋ฏธ๋ค์จ์ด๋ค.
์ฐ๋ฆฌ๋ ์์ฒญ ๋จ์๋ฅผ "Wing(์)"์ด๋ผ๊ณ ๋ถ๋ฅด๊ณ ,
fetcher ํจ์๋ "ShootingStar(์ํ
์คํ)"๋ผ๊ณ ๋ถ๋ฅธ๋ค.
์ด๋ฆ์ ๊ฝค ์ ์พํ์ง๋ง, ๊ธฐ๋ฅ์ ์์ฃผ ๊ตฌ์กฐ์ ์ด๋ค.
kepler-http๋ ๋ค์ ์ธ ๊ฐ์ง ํด๋์ค๋ฅผ ์ค์ฌ์ผ๋ก ๊ตฌ์ฑ๋๋ค:
Config โ ๊ณตํต axios ์ค์ (baseURL, timeout, ์ธ์ฆ ๋ฑ)
Auth โ ํ ํฐ ์ ์ฅ ๋ฐ ๋ฆฌํ๋ ์ ์ค์
Wing โ ์์ฒญ ๋จ์ (axios config ๊ฐ์ฒด ์ญํ )
์ด ์ธ ๊ฐ์ง๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฒญ ํ๋ฆ์ด ๋์ํ๋ฉฐ,
์ดํ์ ๋์ค๋ ShootingStar๋ ์ด ๊ตฌ์กฐ ์์์ ์ค์ ์์ฒญ์ ๋ ๋ฆฌ๋ fetcher๋ค.
export class Config {
baseUrl: string;
timeout: number;
headers: KeplerHeaders;
auth: Auth;
}
Config๋ axios ์ธ์คํด์ค๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉํ๋ ๊ณตํต ์ค์ ๊ฐ์ฒด๋ค.
baseUrl: ๋ชจ๋ API ์์ฒญ์ ๊ธฐ๋ณธ URL
timeout: ์์ฒญ ์ ํ ์๊ฐ
headers: ๊ณตํต ํค๋ (Authorization ๋ฑ)
auth: ์ธ์ฆ ์ฒ๋ฆฌ ๋ก์ง์ ๋ด์ Auth ์ธ์คํด์ค
์ด๊ฑด ๋ณดํต ์ฑ ์คํ ์ด๊ธฐ์ ํ ๋ฒ ์์ฑ๋๊ณ ,
ShootingStar์ ์์ฑ์์ ์ ๋ฌ๋๋ค:
const config = new Config('https://api.example.com', 10000, auth);
const shootingStar = new ShootingStar(config);
export class Auth {
refreshUrl: string;
}
๋ก๊ทธ์ธ ์ ๋ฐ์ accessToken, refreshToken์ localStorage์ ์ ์ฅ
๋ก๊ทธ์์ ์ localStorage ์ด๊ธฐํ
ํ ํฐ ๋ง๋ฃ ์ ์ฌ์ฉํ refresh URL์ ๊ด๋ฆฌ
const auth = new Auth('/v1/auth/refresh', accessToken, refreshToken);
ShootingStar๋ ์์ฒญ ์ค 401 ์๋ฌ๊ฐ ๋๋ฉด,
์ด Auth.refreshUrl์ ์ด์ฉํด ์๋์ผ๋ก ํ ํฐ์ ๊ฐฑ์ ํ๋ค.
export class Wing {
url: string;
method: Method;
needAuth: boolean;
headers: KeplerHeaders;
params: KeplerParam;
data: KeplerBody;
}
Wing์ ํ๋์ ์์ฒญ์ ํ์ํ ์ ๋ณด๋ฅผ ๋ชจ๋ ๋ด๊ณ ์๋ ํด๋์ค๋ค.
๋ง ๊ทธ๋๋ก axios์ config ๊ฐ์ฒด๋ฅผ ํด๋์ค๋ก ๊ฐ์ผ ๊ฒ์ด๋ค.
const wing = new Wing(
'/v1/users/123/example',
Method.GET,
false,
exampleParam, // param
{} // body
);
ShootingStar.launch()๋ ์ด wing์ ๋ฐ์์ axios ์์ฒญ์ ์ํํ๋ค.
ํด๋์ค ์ญํ ์ฌ์ฉ ์์น
Config axios ์ธ์คํด์ค ์ด๊ธฐํ์ฉ ์ค์ new ShootingStar(config)
Auth ํ ํฐ ์ ์ฅ ๋ฐ refresh ๋ก์ง config.auth, refresh ์ ์ฌ์ฉ
Wing ์ค์ ์์ฒญ ๋จ์ (url, method, param, body ํฌํจ) shootingStar.launch(wing)
์ฐ์ ์ ์ฒด ๊ตฌ์กฐ ์ค ๊ฐ์ฅ ํต์ฌ์ธ ShootingStar ํด๋์ค๋ฅผ ํ๋์ฉ ๋ฏ์ด๋ณด์.
์ด fetcher๋ ๊ฒฐ๊ตญ axios ๋ํผ์ง๋ง, ๋ค์๊ณผ ๊ฐ์ ์ญํ ์ ๋ ๊ฐ์ง๊ณ ์๋ค:
์์ฒญ ์ ์ฒ๋ฆฌ
ํ ํฐ ํ์ ์ฌ๋ถ ํ์ธ ๋ฐ ํค๋ ์ค์
์๋ต ์คํจ ์ ํ ํฐ ์ฌ๋ฐ๊ธ
๋ค์ ์ฌ์์ฒญ
async launch<KeplerData>(wing: Wing): Promise<KeplerResponse<KeplerData>> {
์ด ํจ์๋ ๋ชจ๋ ์์ฒญ์ ๋ค ์ด ํจ์ ํ๋๋ก ์ฒ๋ฆฌํ๋ค.
wing ๊ฐ์ฒด์ url, method, param, body, header ๋ฑ์ด ๋ค ๋ค์ด ์๊ณ ,
ํ์ํ๋ฉด ์๋์ผ๋ก accessToken๋ ๋ถ๋๋ค.
if (wing.needAuth) {
if (localStorage.getItem('accessToken') == '') throw new Error("UnAuthorizedRequest");
wing.headers = {
common: {
Authorization: `Bearer ${localStorage.getItem('accessToken')}`
},
post: {}
};
}
์ธ์ฆ์ด ํ์ํ ์์ฒญ์ด๋ฉด localStorage์์ accessToken์ ๊บผ๋ด๊ณ ,
๊ทธ๊ฑธ Authorization ํค๋์ ๋ถ์ธ๋ค.
return await this._instance.request(wing);
if (e.response.status === axios.HttpStatusCode.Unauthorized) {
let access_token = await this._refreshToken();
wing.headers.common.Authorization = `Bearer ${access_token}`;
return await this._instance.request(wing);
}
์ฒ์ ์์ฒญ์ ์คํจํ๋๋ผ๋,
401 Unauthorized๋ผ๋ฉด refreshToken์ ์ฌ์ฉํด์ accessToken์ ์๋ก ๋ฐ๊ณ ,
๋ค์ ๋์ผ ์์ฒญ์ ์ฌ์๋ํ๋ค.
private async _refreshToken(): Promise<string> {
let wing = new Wing(this._config.auth.refreshUrl, Method.POST, true, {}, {
refresh_token: localStorage.getItem('refreshToken')
});
...
localStorage.setItem('accessToken', res.data.access_token);
return res.data.access_token;
}
refresh token์ ์ฌ์ฉํด์ ์ ํ ํฐ์ ๋ฐ์์ค๊ณ ,
localStorage์ ์ ์ฅ ํ ๋ฐํ.
๋๋ถ์ ์ฐ๋ฆฌ๋ ํ ํฐ ๋ง๋ฃ๋ฅผ ์ ๊ฒฝ ์ธ ํ์ ์์ด launch()๋ง ํธ์ถํ๋ฉด ๋๋ค.
kepler-http๋ API ์์ฒญ์ ๊ตฌ์ฑํ ๋
๋ชจ๋ ์์(body, param, headers, response)๋ฅผ ๋ช
ํํ ํ์
์ผ๋ก ๋ถ๋ฆฌํด์ ์ ์ํ๋ค.
์ด๊ฑด ๋จ์ํ ํ์
์์ ์ฑ์ ์ํ ๊ฒ๋ง์ด ์๋๋ผ,
์์ฒญ์ ๋ชจ๋ํํ๊ณ ์ผ๊ด๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํ ์ค๊ณ๋ค.
export default interface KeplerHeaders {
common: { Authorization: string };
post: {};
}
์์ฒญ ํค๋๋ฅผ ๊ณตํต(common)๊ณผ POST ์ ์ฉ(post)์ผ๋ก ๋ถ๋ฆฌํด์ ํ์ฅ์ฑ ํ๋ณด
๋๋ถ๋ถ์ ์์ฒญ์์๋ common.Authorization๋ง ์ฌ์ฉํ์ง๋ง, ํ์ํ ๊ฒฝ์ฐ ๋ฉ์๋๋ณ ํค๋ ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅ
export default interface KeplerResponse<T extends KeplerData> {
status: number;
data: T;
}
๋ชจ๋ ์๋ต์ status์ data๋ก ๊ตฌ์ฑ
data๋ ์ฐ๋ฆฌ๊ฐ ์ง์ ํ ํ์ (ProfileData, PostData ๋ฑ)์ ์ ๋ค๋ฆญ์ผ๋ก ์ ๋ฌ
export default interface KeplerData {
[key: string]: any;
}
๋ชจ๋ ์๋ต ํ์ ์ ๋ฒ ์ด์ค ์ธํฐํ์ด์ค
์ค์ ๋ก ์ฌ์ฉํ ๋๋ ํ์ฅํด์ ๊ตฌ์ฒด์ ์ธ ์๋ต ๊ตฌ์กฐ๋ฅผ ์ ์
export interface ProfileData extends KeplerData {
id: string;
nickname: string;
profileImage: string;
level: number;
}
export default interface KeplerParam {
[key: string]: string | number;
}
URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ์ฉ ํ์
์์ฒญ๋ง๋ค ํ์ํ ํ๋ผ๋ฏธํฐ๋ ๋ฐ๋ก ์ ์ํด์ ๋๊ธด๋ค
export interface GetProfilesParam extends KeplerParam {
world_id: string;
game_id: string;
}
export default interface KeplerBody {
[key: string]: any;
}
์์ฒญ body ํ์ ์ ๋ฒ ์ด์ค
POST, PATCH ๋ฑ์์ ์ฌ์ฉํ๋ payload ์ ์
export interface EditProfileBody extends KeplerBody {
nickname: string;
profileImage: string;
}
โ
์์ฝ
ํ์ผ ์ค๋ช
์์
KeplerHeaders.ts ํค๋ ๊ตฌ์กฐ ์ ์ Authorization ๋ฑ
KeplerResponse.ts ๋ชจ๋ ์๋ต์ ๊ณตํต ํํ status, data
KeplerData.ts ์๋ต ํ์
์ base ProfileData, PostData
KeplerParam.ts URL ํ๋ผ๋ฏธํฐ ์ ์ world_id, game_id
KeplerBody.ts ์์ฒญ ๋ฐ๋ ์ ์ nickname, profileImage
์ด๋ฐ ํ์
๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก
๊ฐ ์์ฒญ์ ํ์ํ ๋ฐ์ดํฐ ์กฐ๊ฐ์ ์กฐ๋ฆฝํ ๋ค
Wing ๊ฐ์ฒด๋ก ์์ฒญ์ ์์ฑํ๊ณ ,
ShootingStar.launch()๋ก ์ ์กํ๋ ๊ตฌ์กฐ๋ค.