๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

๐Ÿค 25๋…„ 4์›” ํšŒ๊ณ 

์•ฝ 23๋ถ„
Ju young Lee
A contribution-driven developer

์„œ๋ก โ€‹

2025๋…„ 4์›”, ๋น„์ „ ์‹œ์Šคํ…œ์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ธฐ์กด ๊ธฐ๋Šฅ์„ ์œ ์ง€ยท๊ฐœ์„ ํ•˜๋Š” ๊ณผ์ •์—์„œ ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์ง๊ฒฐ๋˜๋Š” ๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๋ฅผ ๋งˆ์ฃผํ•˜๊ฒŒ ๋๋‹ค.

  1. Axios ์ค‘๋ณต ํ˜ธ์ถœ ๋ฌธ์ œ โ€“ ์ž๋™ ๋กœ๊ทธ์ธ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•œ ๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ๋กœ ์ธํ•ด ๋„คํŠธ์›Œํฌ ๋น„์šฉ ์ฆ๊ฐ€
  2. AUM ๊ทธ๋ž˜ํ”„ ์„ฑ๋Šฅ ์ €ํ•˜ โ€“ ๋Œ€์šฉ๋Ÿ‰ ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋กœ ์ธํ•ด ํŽ˜์ด์ง€ ์ง„์ž…๊ณผ ํ•„ํ„ฐ๋ง ์‹œ ๋ฐœ์ƒํ•˜๋Š” ๋ Œ๋”๋ง ์ง€์—ฐ

์ด๋ฒˆ ํšŒ๊ณ ์—์„œ๋Š” ๊ฐ๊ฐ์˜ ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ์ธ์‹ํ•˜๊ณ  ๋ถ„์„ํ–ˆ์œผ๋ฉฐ, ์–ด๋–ค ์ ‘๊ทผ์„ ํ†ตํ•ด ํ•ด๊ฒฐํ–ˆ๋Š”์ง€ ๊ธฐ๋กํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

(๋” ๋‚˜์€ ์ ‘๊ทผ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€์„ ํ†ตํ•ด ์•Œ๋ ค์ฃผ์‹œ๋ฉด ์ง„์‹ฌ์œผ๋กœ ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!!) ๐Ÿ˜Š

4์›”์—๋Š” ์‚ฌ์šฉ์ž ๊ถŒํ•œ๊ณผ ๊ด€๋ จํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ์ง‘์ค‘ํ–ˆ์ง€๋งŒ, ๊ทธ ๊ธฐ๋Šฅ๊ณผ ๊ด€๋ จ๋œ ๊ฒƒ์€ 5์›” ํšŒ๊ณ ์— ์ž์„ธํžˆ ๋‹ค๋ค„๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

4์›” Action Pointโ€‹

1๋ถ„๊ธฐ๋ฅผ ๋Œ์•„๋ณด๋ฉฐ 4์›” Action Point๋ฅผ ์ž‘์„ฑํ•ด๋ณด์•˜๋Š”๋ฐ ์ด ์ค‘ 4๊ฐ€์ง€๋Š” ์‹ค์ฒœํ–ˆ๋‹ค. ๊ทธ ์™ธ์˜ ๋‚ด์šฉ๋“ค์€ ํ˜ธ๊ธฐ์‹ฌ ๋…ธํŠธ์— ์ €์žฅํ•ด๋†จ๊ณ  5์›”์˜ ํœด์ผ์— ๊ณต๋ถ€ํ•˜๋ฉฐ ๋‚˜์•„๊ฐ€๋ ค๊ณ  ํ•œ๋‹ค.

  • ์‹ค๋ฌด์—์„œ ๋งŒ๋‚œ ๋ฌธ์ œ ๊ทผ๋ณธ์ ์œผ๋กœ 2๊ฐœ ์ด์ƒ ํ•ด๊ฒฐ
  • Git ๋ฌธ์„œ ์ •๋ฆฌํ•˜๊ณ  Merge์™€ rebase ์„ธ์…˜ ์ •๋ฆฌ ๋ฐ ๊ณต์œ 
  • ๋ฐฑ์—”๋“œ CI/CD ํ™œ์šฉํ•˜์—ฌ EB์— ์ปจํ…Œ์ด๋„ˆ ์ž๋™ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•
  • FSD์˜ Public API ๋ฐฉ์‹์™€ ๋ฒˆ๋“ค๋Ÿฌ ์—ฐ๊ด€์ง€์–ด ์ธ์‚ฌ์ดํŠธ ์ •๋ฆฌ
  • HTTP ์ „์†ก ๊ณ„์ธต์— ๋Œ€ํ•ด ์ •ํ™•ํžˆ ์ •๋ฆฌ
  • ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ณด์•ˆ ๋ฌธ์„œํ™”ํ•˜๊ธฐ (XSS ๋ฐ CSRF ๊ณต๊ฒฉ)
  • FSD ์ ์šฉ๊ธฐ ํฌ์ŠคํŠธ 2ํŽธ ์ž‘์„ฑํ•˜๊ธฐ
  • ๊ฐ์ฒด ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋Œ€ํ•ด ํ•™์Šต
  • ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋Œ€ํ•ด ํ•™์Šต ์‹œ์ž‘ํ•˜๊ธฐ

๋ณธ๋ก โ€‹

Axios ์ค‘๋ณต ํ˜ธ์ถœ ๋ฌธ์ œ ๊ฐœ์„ โ€‹

๋ฌธ์ œ ๊ฐœ์š”โ€‹

์ž๋™ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ ํ›„, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ๋๋‹ค.

  • ๋ฌธ์ œ ํ˜„์ƒ: ์—‘์„ธ์Šค ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ์—ฌ๋Ÿฌ API์—์„œ ๋™์‹œ์— 401 ์—๋Ÿฌ ๋ฐœ์ƒ
  • ๊ฒฐ๊ณผ: ๊ฐ๊ฐ์˜ ์š”์ฒญ์ด ๋™์‹œ์— ๋ฆฌํ”„๋ ˆ์‹œ API๋ฅผ ํ˜ธ์ถœ โ†’ ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐœ์ƒ
  • ์‹ค์ œ ์˜ํ–ฅ: ์„œ๋ฒ„ ๋ถ€ํ•˜ ์ฆ๊ฐ€, ์‚ฌ์šฉ์ž ๋กœ๊ทธ์•„์›ƒ ๋ฆฌ์Šคํฌ ์ฆ๊ฐ€, UX ์ €ํ•˜

๊ธฐ์กด ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์•˜๋‹ค.

// axiosConfig.ts
...
if (error.response.status === STATUS.UNATHORIZED) {
if (error.response.data.message === ERROR_RESPONSES.accessExpired) {
if (!session?.refresh_token) {
resetSession();
return Promise.reject(new Error('No refresh token available'));
}

try {
const reissuedResult = await authApi.reissueToken(session.refresh_token);
return instance({
...config,
headers: {
...config?.headers,
Authorization: `Bearer ${reissuedResult.access_token}`,
},
});
} catch {
...
}

}
}
...

์ฝ˜์†”์„ ํ™•์ธํ•ด๋ณด๋‹ˆ ์•„๋ž˜์™€ ๊ฐ™์•˜๋‹ค.

3๊ฐœ์˜ 401์„ ๋ฐ˜ํ™˜ํ•œ API์š”์ฒญ์— ๋Œ€ํ•ด 3๋ฒˆ refresh ์š”์ฒญ์ด ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ž„์‹œ ํ•ด๊ฒฐ์ฑ…: react-router-dom + tanstack-query, loader ํ™œ์šฉโ€‹

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ฐพ์•„๋ณด๋‹ค ์ƒ๊ฐ๋‚œ ๊ฒƒ์€ react-router-dom๊ณผ tanstack-query๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋‹ค.

react-router-dom์˜ createBrowserRouter API์—์„œ ์‚ฌ์šฉ๋˜๋Š” loader ๋ฉ”์„œ๋“œ๋Š” ๋ผ์šฐํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜์ด๋ฉฐ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด์ ์„ ์ด์šฉํ•˜๋ฉด ํŽ˜์ด์ง€ ๋‹จ์—์„œ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ๋Š” ์—ฌ๋Ÿฌ API ์ด์ „์— API๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค.

๊ทธ๋ž˜์„œ AuthLayoutLoader ํด๋ž˜์Šค์— ์ž์‹ ์˜ ์ •๋ณด๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ง€๊ณ  ์˜ค๋Š” API๋ฅผ ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž„์‹œ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค.

//route.tsx
export const router: ReturnType<typeof createBrowserRouter> = createBrowserRouter([
{
path: publicPathKeys.root,
element: <DefaultLayout />,
loader: DefaultLayoutLoader.AuthLayoutPage,
errorElement: <GlobalErrorFallback />,
children:[
...
]
},
{
path: privatePathKeys.root,
element: <AuthLayout />,
loader: AuthLayoutLoader.AuthLayoutPage,
errorElement: <GlobalErrorFallback />,
children:[
...
]
}])



// auth-layout.model.ts

export class AuthLayoutLoader {
const { session, setSession, resetSession } = useSessionStore.getState();
static async AuthLayoutPage(args: LoaderFunctionArgs) {
const session = AuthLayoutLoader.getSession();
if (!session) {
return redirect(publicPathKeys.login());
}
await AuthLayoutLoader.fetchMyInfo();
return args;
}

private static async fetchMyInfo() {
const userInfoQuery = {
queryKey: ['me'],
queryFn: () => authApi.getMyInfo(),
};

const result = await queryClient.fetchQuery(userInfoQuery);
return result;
}

private static getSession() {
return useSessionStore.getState().session;
}
}

์œ„์™€ ๊ฐ™์ด Loader ๋‚ด๋ถ€์— ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋Š” fetchQuery๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ž์‹ ์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์˜ค๋Š” Promise ์š”์ฒญ์„ ์ถ”๊ฐ€ํ•˜์˜€๊ณ , ๊ทธ ๊ฒฐ๊ณผ

n๋ฒˆ์˜ ์š”์ฒญ ์ด์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์˜ค๋Š” ๋ ˆ์ด์–ด (loader)์—์„œ ํ•œ ๋ฒˆ ์ฒดํฌํ•˜๋„๋ก ํ•˜์—ฌ ํ•ด๋‹น ์ค‘๋ณต ํ˜ธ์ถœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ 3์ผ ์ •๋„ ์ง€๋‚˜, ์ด ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค.

  • ๋ฌธ์ œ 1: ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ๋งˆ๋‹ค ๋ถˆํ•„์š”ํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ๋ฐœ์ƒ
  • ๋ฌธ์ œ 2: loader์— ์˜์กดํ•˜๋ฉด, ์ปดํฌ๋„ŒํŠธ ๋‹จ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ํ† ํฐ ๋งŒ๋ฃŒ๋Š” ๋Œ€์‘ํ•˜์ง€ ๋ชปํ•จ

Loader๋Š” ๋ผ์šฐํŠธ ๊ธฐ๋ฐ˜ ๋™์ž‘ํ•˜๋Š” ๋ฉ”์†Œ๋“œ์ด๊ธฐ์— ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ์—ฌ๋Ÿฌ API๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋Œ€์‘ํ•˜์ง€ ๋ชปํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋” ๋ณธ์งˆ์ ์œผ๋กœ ํ•ด๊ฒฐํ•ด์•ผ ํ–ˆ๋‹ค.

๊ทผ๋ณธ ํ•ด๊ฒฐ: RefreshManager๋ฅผ ํ†ตํ•œ ์š”์ฒญ ํ ๊ด€๋ฆฌโ€‹

๊ตฌ๊ธ€ ๊ฒ€์ƒ‰์„ ํ†ตํ•ด ์‚ดํŽด๋ณด๋‹ˆ, ๋Œ€์ฒด์ ์œผ๋กœ ํ๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๋Š”๋ฐ ๊น”๋”ํ•œ ์ฝ”๋“œ๊ฐ€ ์—†์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ RefreshManager ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์•˜๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ „๋žต์œผ๋กœ ํ•ด๋‹น ํด๋ž˜์Šค๋Š” ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค.

  1. isRefreshing ์ƒํƒœ๋กœ ์ฒซ ์š”์ฒญ ์ดํ›„๋ถ€ํ„ฐ๋Š” ๋Œ€๊ธฐ ํ์— ๋„ฃ๊ธฐ
  2. ํ† ํฐ ๊ฐฑ์‹  ์™„๋ฃŒ ํ›„ ํ์— ์Œ“์ธ ์š”์ฒญ์— ์ƒˆ๋กœ์šด ํ† ํฐ์œผ๋กœ ์žฌ์š”์ฒญ
  3. ํด๋ž˜์Šค๋กœ ์ถ”์ƒํ™”ํ•˜์—ฌ ์ธํ„ฐ์…‰ํ„ฐ ๋‚ด์—์„œ ๋ฐ˜๋ณต๋˜๋Š” ๋กœ์ง ์ œ๊ฑฐ

RefreshManage Class ๊ตฌํ˜„

import { InternalAxiosRequestConfig } from 'axios'

import { CustomInstance } from '@/shared/lib'

type RequestCallback = (token: string) => void

class RefreshManager {
private isRefreshing = false
private queue: RequestCallback[] = []

async handle401(
config: InternalAxiosRequestConfig,
refreshFn: () => Promise<string>,
api: CustomInstance
): Promise<InternalAxiosRequestConfig> {
if (this.isRefreshing) {
return new Promise((resolve) => {
this.queue.push((token: string) => {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${token}`
resolve(api(config))
})
})
}

this.isRefreshing = true

try {
const newToken = await refreshFn()
this.queue.forEach((cb) => cb(newToken))
this.queue = []
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${newToken}`
return api(config)
} catch (err) {
this.queue = []
throw err
} finally {
this.isRefreshing = false
}
}
}

export const refreshManager = new RefreshManager()


// axiosConfig.tsx (ํด๋ž˜์Šค ์ธ์Šคํ„ดํŠธ ์ƒ์„ฑ๋ถ€)

if (error.response.status === STATUS.UNATHORIZED) {
if (error.response.data.message === ERROR_RESPONSES.accessExpired) {
if (!session?.refresh_token) {
resetSession();
return Promise.reject(new Error('No refresh token available'));
}

try {
const retryResponse = await refreshManager.handle401(
config,
async () => {
const reissued = await authApi.reissueToken();
setSession({
access_token: reissued.access_token,
role: reissued.role,
token_type: reissued.token_type,
});
return reissued.access_token;
},
instance,
);

return retryResponse;
} catch {
...
}

}
}

์•„๋ž˜ ์ฝ˜์†” ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด๋ณด๋‹ˆ ๊ทผ๋ณธ์ ์œผ๋กœ ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋จ์„ ํ™•์ธํ–ˆ๋‹ค.

์ด์ œ, ๋™์‹œ์— ์—ฌ๋Ÿฌ API 401์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ด๋„ ์ตœ์ดˆ ์š”์ฒญ์— ๋Œ€ํ•ด์„œ๋งŒ ๋ฆฌํ”„๋ ˆ์‹œ ์š”์ฒญ์„ ์ง„ํ–‰ํ•œ๋‹ค.

๊ฒฐ๊ณผ ๋น„๊ตโ€‹

์ƒํ™ฉ์ด์ „ ๋ฐฉ์‹๊ฐœ์„  ๋ฐฉ์‹
401 ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ์š”์ฒญ๋งˆ๋‹ค ๊ฐœ๋ณ„ ๋ฆฌํ”„๋ ˆ์‹œ ์‹คํ–‰๋‹จ 1ํšŒ ๋ฆฌํ”„๋ ˆ์‹œ ํ›„ ํ์— ์žˆ๋˜ ์š”์ฒญ ์žฌ์‹œ๋„
๋ฆฌํ”„๋ ˆ์‹œ API ํ˜ธ์ถœ ์ˆ˜NํšŒ (API ์š”์ฒญ ์ˆ˜๋งŒํผ)1ํšŒ
์ฝ”๋“œ ๋ณต์žก๋„์ธํ„ฐ์…‰ํ„ฐ ๋‚ด ๋ณต์žกํ•œ ๋ถ„๊ธฐํด๋ž˜์Šค ๋‚ด๋ถ€๋กœ ์ถ”์ƒํ™”ํ•˜์—ฌ ๊ฐ„๊ฒฐ

ํšŒ๊ณ โ€‹

  • ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ฉฐ ๋น„๋™๊ธฐ ์ƒํƒœ ๊ณต์œ ์™€ Axios ์ธํ„ฐ์…‰ํ„ฐ์˜ ํ•œ๊ณ„๋ฅผ ๋‹ค์‹œ ์ฒด๊ฐํ–ˆ๋‹ค. (5์›”์—๋Š” HttpClient๋ฅผ ๋งŒ๋“ค์–ด์„œ fetch๋ฅผ ์‚ฌ์šฉํ•˜๋˜ axios ํ˜น์€ ky์— ์ƒ๊ด€์—†์ด ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋งŒ๋“ค์–ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.)
  • React ์•ฑ์—์„œ ์ธ์ฆ ํ† ํฐ์„ ๊ด€๋ฆฌํ•  ๋•Œ๋Š” ํด๋ผ์ด์–ธํŠธ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ๋ฐ˜๋“œ์‹œ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.
  • ๊ณต์œ  ์ž์› ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์ธ ๋ฎคํ…์Šค์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•ด ๋ณด์ž.

AUM ๊ทธ๋ž˜ํ”„ ์„ฑ๋Šฅ ๊ฐœ์„ โ€‹

AUM (Assets Under Management) ์€ ๊ธˆ์œต ๊ธฐ๊ด€์ด ๊ณ ๊ฐ ์ž์‚ฐ์„ ๋Œ€์‹  ๊ด€๋ฆฌํ•˜๋ฉฐ ์šด์šฉํ•˜๋Š” ์ด ์‹œ์žฅ ๊ฐ€์น˜๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ์ด๋ฒˆ ๊ฐœ์„  ์ž‘์—…์€, ์ „์ฒด ๊ณ ๊ฐ์˜ AUM ์‹œ๊ณ„์—ด ๊ทธ๋ž˜ํ”„ ์„ฑ๋Šฅ ์ตœ์ ํ™”์— ์ค‘์ ์„ ๋‘”๋‹ค.

๋ฌธ์ œ ๊ฐœ์š”โ€‹

์ „์ฒด ๊ณ ๊ฐ์˜ AUM ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ํŽ˜์ด์ง€์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค:

  • ๊ทธ๋ž˜ํ”„๊ฐ€ ์กด์žฌํ•˜๋Š” ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ 1~2์ดˆ์˜ ๋”œ๋ ˆ์ด

  • ํ•„ํ„ฐ๋ง(์ „๋žต/๊ธฐ๊ด€/๊ธฐ๊ฐ„) ์ ์šฉ ์‹œ ๋ Œ๋”๋ง์ด ๋ฒ„๋ฒ…์ž„

  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX) ์ €ํ•˜, ํŽ˜์ด์ง€ ๋ฐ˜์‘ ์†๋„ ๋ถˆ๋งŒ

์‹œ๊ฐ์ ์œผ๋กœ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๊ทธ๋ž˜ํ”„์—์„œ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์ฒด๊ฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค:

์–ด๋А ๋‚ ๋ถ€ํ„ฐ ํ•ด๋‹น ๊ทธ๋ž˜ํ”„๊ฐ€ ์žˆ๋Š” ํŽ˜์ด์ง€๋กœ ๋“ค์–ด๊ฐˆ ๋•Œ์™€ ํ••ํ„ฐ๋ง์„ ์กฐ์ž‘ํ•  ๋•Œ 1์ดˆ์—์„œ 2์ดˆ์ •๋„์˜ ๋ธ”๋กœํ‚น์ด ๋˜๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค.

์›์ธ ๋ถ„์„โ€‹

Permance, Network Panel์„ ์‚ดํŽด๋ณธ ๊ฒฐ๊ณผ

1. API ๋„คํŠธ์›Œํฌ ์ง€์—ฐ์‹œ๊ฐ„

2. JavaScript ๋ธ”๋กœํ‚น

์›์ธ์„ ์•Œ๊ฒŒ ๋๊ณ  ํ•˜๋‚˜์”ฉ ๊ฐœ์„ ํ•ด๋ณด์•˜๋‹ค.

1. API ๋ณ€๊ฒฝํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ์ง€์—ฐ์‹œ๊ฐ„ 1์ดˆ ๋‹จ์ถ•โ€‹

์šฐ์„  ๊ฐ„๋‹จํ•œ ๋ฐฐ๊ฒฝ์„ ์„ค๋ช…ํ•˜๊ณ ์ž ํ•œ๋‹ค. ์ „์ฒด AUM ๊ทธ๋ž˜ํ”„ ์ƒํƒœ๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•ด์„œ ๋งŒ๋“ ๋‹ค. ๊ทธ๋ž˜ํ”„๋ฅผ ์œ„ํ•œ API๋Š” ์กด์žฌํ•  ํ•„์š”๋„ ์—†๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๊ณ  ์ด๋ฏธ ๋ชจ๋“  ๊ณ„์ขŒ List๋ฅผ ๋‚ด๋ ค์ฃผ๋Š” API๊ฐ€ ์žˆ๊ธฐ์— ํ•ด๋‹น API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋์—ˆ๋‹ค.

์ด์ „์—๋Š” ์ „์ฒด ๊ณ„์ขŒ ๋ชฉ๋ก์„ ์กฐํšŒํ•œ ๋’ค, ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹ฑํ•ด์„œ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆฌ๊ณ  ์žˆ์—ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๊ฐ ๊ณ„์ขŒ๋Š” ๋งค์ผ ์Œ“์ด๋Š” ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด์„œ, ์‹œ๊ฐ„์ด ์ง€๋‚ ์ˆ˜๋ก ๋ฐ์ดํ„ฐ ์–‘์ด ๊พธ์ค€ํžˆ ๋Š˜์–ด๋‚˜๋Š” ํŠน์ง•์ด ์žˆ๋‹ค. ๋˜ํ•œ ํ•ด๋‹น API๋Š” ์„œ๋ฒ„์—์„œ ๋‹ค์–‘ํ•œ ๋กœ์ง์ด ์กด์žฌํ•˜๊ณ  ์žˆ๋‹ค.

์ฒ˜์Œ์—” ํฐ ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ์ง€๋งŒ, ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉฐ ๋ฐ์ดํ„ฐ ์–‘์ด ๋งŽ์•„์ง€๊ณ , ํ•„ํ„ฐ๋ง์ด๋‚˜ ๋ Œ๋”๋ง ์‹œ์ ์— ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜๋ณต ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์ด ์ ์  ์„ฑ๋Šฅ ๋ณ‘๋ชฉ์œผ๋กœ ๋“œ๋Ÿฌ๋‚˜๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค.

Swagger๋ฅผ ์‚ดํŽด๋ณด๋‹ˆ ํ•œ๋‹ฌ ์ „, ๊ณ ๊ฐ์˜ Equity(์ž์‚ฐ)์„ ์ˆ˜์ •ํ•˜๋Š” ๊ธฐ๋Šฅ์— ํ•„์š”ํ•œ API๋ฅผ ๋งŒ๋“ค์—ˆ๊ณ  ์ด API๋Š” ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆด ๋•Œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์Œ์„ ํŒŒ์•…ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋Š” ๋‹จ์ˆœํ•œ List ํ˜•ํƒœ์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ๊ฐ€์ง€๋Š” ํ˜•ํƒœ์˜€๋‹ค. ๋‹ค๋งŒ ๊ฐ ๊ฐ์ฒด์˜ Interface๋Š” ๋‹ค์†Œ ๋ณต์žกํ–ˆ๋‹ค. ๋˜ํ•œ ๊ทธ๋ž˜ํ”„์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ๋งŒ์กฑํ•˜์ง„ ์•Š์•˜์ง€๋งŒ ํ•ต์‹ฌ์ ์ธ ๋ฐ์ดํ„ฐ๋Š” ์žˆ์–ด์„œ ํ•ด๋‹น API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ฐœ์„ ์„ ์‹œ๋„ํ–ˆ๋‹ค. ์‹œ๋„ํ•˜๋Š” ๊ณผ์ •์—์„œ ์ƒํƒœ์˜ ์ž๋ฃŒ๊ตฌ์กฐ๊ฐ€ ๋‹ฌ๋ผ์ง€๊ฒŒ ๋ผ ์–ด๋ ค์›€์ด ์žˆ์—ˆ์ง€๋งŒ, ๊ทธ๋กœ ์ธํ•ด ํ•จ์ˆ˜๋ฅผ ๋” ์ˆœ์ˆ˜ํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์—ฌ ๊ณ„์‚ฐ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ ์•ฝ 1์ดˆ์˜ ๋„คํŠธ์›Œํฌ ์ง€์—ฐ์‹œ๊ฐ„์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ด์ „ (์ „์ฒด ๊ณ„์ขŒ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ API)

  • ๋””์ฝ”๋”ฉ๋œ ๋ณธ๋ฌธ 1.3MB
  • content-length === 1321701
  • ์š”์ฒญ ์ „์†ก ์™„๋ฃŒ ๋ฐ ๋Œ€๊ธฐ ์ค‘: 1.8s

์ดํ›„ (historical Snapshot ์กฐํšŒ API)

  • ๋””์ฝ”๋”ฉ๋œ ๋ณธ๋ฌธ :5.0 MB
  • content-length === 5049472
  • ์š”์ฒญ ์ „์†ก ์™„๋ฃŒ ๋ฐ ๋Œ€๊ธฐ ์ค‘ : 836.46 ms
ํ•ญ๋ชฉ๊ฐœ์„  ์ „ ๋ฐฉ์‹๊ฐœ์„  ํ›„ ๋ฐฉ์‹
์‚ฌ์šฉ API์ „์ฒด ๊ณ„์ขŒ ๋ชฉ๋ก API/historicalSnapshot/all API
๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ณ„์ขŒ๋ณ„ ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ ํฌํ•จ (์ค‘์ฒฉ ๊ตฌ์กฐ)ํ‰ํƒ„ํ•œ List ํ˜•ํƒœ
์„ฑ๋Šฅ ๊ฐœ์„ ๋„คํŠธ์›Œํฌ ์ง€์—ฐ์‹œ๊ฐ„ (1~2์ดˆ)๋„คํŠธ์›Œํฌ ์ง€์—ฐ ์‹œ๊ฐ„ 0.8์ดˆ (์•ฝ 1์ดˆ ๋‹จ์ถ•)

๋” ์ ์ ˆํ•œ API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ•ด๋‹น ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋‹ค. ๊ทธ๋Ÿผ ์ด์ œ ๋‚˜๋จธ์ง€์ธ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ธ”๋กœํ‚น ํ˜„์ƒ์„ ์‚ดํŽด๋ณด๋ฉด

2. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ธ”๋กœํ‚น CPU ๋ธ”๋กœํ‚น ์ตœ์†Œํ™”โ€‹

Performance ํŒจ๋„์„ ๋ถ„์„ํ•ด๋ณด๋‹ˆ findMarketInfo() ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์‹œ์ ์—์„œ CPU ๋ธ”๋กœํ‚น์ด ์ง‘์ค‘์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค.

AUM ๊ทธ๋ž˜ํ”„์—๋Š” ๋‚ ์งœ๋ณ„๋กœ ๋งˆ์ผ“ ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ๋ณด์—ฌ์ค˜์•ผ ํ–ˆ๋Š”๋ฐ, ์ด๋ฅผ ์œ„ํ•ด ํŠน์ • ๋‚ ์งœ์— ํ•ด๋‹นํ•˜๋Š” ๋งˆ์ผ“ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์•„์˜ค๋Š” ๋กœ์ง์ด ํ•„์š”ํ–ˆ๋‹ค.

๊ธฐ์กด์—๋Š” findMarketInfo ํ•จ์ˆ˜๊ฐ€ markets ๋ฐฐ์—ด์—์„œ Array.prototype.find()๋ฅผ ์‚ฌ์šฉํ•ด ๋‚ ์งœ๊ฐ€ ์ผ์น˜ํ•˜๋Š” ํ•ญ๋ชฉ์„ ๋งค๋ฒˆ ์ˆœํšŒํ•˜๋ฉฐ ํƒ์ƒ‰ํ•˜๊ณ  ์žˆ์—ˆ๊ณ , ์ด ๋ฐ˜๋ณต ํƒ์ƒ‰์ด ์„ฑ๋Šฅ ๋ณ‘๋ชฉ์˜ ์ฃผ์š” ์›์ธ ์ค‘ ํ•˜๋‚˜๋กœ ๋“œ๋Ÿฌ๋‚ฌ๋‹ค.

export const findMarketInfo = ({
dailiesRowUpdatedTimestamp,
markets,
}: MarketProp): MarketDto | null => {
const marketInfo = markets.find((market) => {
const parseMarketDate = dayjs.unix(market.createdTimestamp).format(DATE_FORMAT_YYYY_MM_DD)
const parseDailiesDate = dayjs.unix(dailiesRowUpdatedTimestamp).format(DATE_FORMAT_YYYY_MM_DD)

return parseMarketDate === parseDailiesDate
})
if (!marketInfo) return null
return marketInfo
}

const processAum = () => {
...
const market = findMarketInfo({
dailiesRowUpdatedTimestamp: snapshot.updatedTimestamp,
markets,
});
...
}

์ด๋Ÿฐ ์‹์œผ๋กœ... AUM ์ƒํƒœ๋ฅผ ๊ณ„์‚ฐํ•˜๊ธฐ ์ˆœํšŒํ•˜๋Š” ๋กœ์ง ๋‚ด๋ถ€์—์„œ ํ•ด๋‹น ์ˆœํšŒ ๋กœ์ง์ด ์‹คํ–‰๋˜๊ณ  ์žˆ์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋ถ€๋ถ„์„ ํ•ด์‹œ ๊ธฐ๋ฐ˜ Map์„ ํ™œ์šฉํ•ด๋ณด์•˜๊ณ  ๋˜ํ•œ ํ•ด๋‹น ํ•จ์ˆ˜์˜ ์ˆ˜์ค€์„ ๋†’์—ฌ๋ณด์•˜๋‹ค.

const createMarketMap = (markets: MarketDto[]): Record<string, MarketDto> => {
return markets.reduce(
(map, market) => {
const marketDate = dayjs
.unix(market.createdTimestamp)
.format(DATE_FORMAT_YYYY_MM_DD);
map[marketDate] = market;
return map;
},
{} as Record<string, MarketDto>,
);
};

์•„๋ž˜์˜ ํ›…์ด ์‹คํ–‰๋ ๋•Œ ํ•œ๋ฒˆ์— ํ•ด๋‹น Map์„ ๊ฐ€์ง€๊ณ  ์˜ค๋„๋ก ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค. O(1)๋กœ ๋™์ผํ•œ ๋‚ ์งœ์˜ ๋งˆ์ผ“ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

export const useHistoricalAum = () => {
const { data: historicalSnapshotList } = useSuspenseQuery(
SnapShotQueries.getHistoricalAccountSnapshots(),
);
const { data: markets } = useSuspenseQuery(MarketQueries.getMarketData());
const { filters } = useTimeSeriesStore();

const aumWithMarkets = useMemo(() => {
const marketMap = createMarketMap(markets);
const aum = processAumData(historicalSnapshotList, marketMap);
return enrichAumWithMarkets(aum, marketMap, filters.asset);
}, [historicalSnapshotList, markets, filters.asset]);

return {
aumWithMarkets,
};
};

์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•˜๋ฉด ProcessAumData ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „, ํ•œ๋ฒˆ๋งŒ List๋ฅผ Map์— ๋งž๊ฒŒ ๋ฐ”๊พธ๋Š” ์ž‘์—…๋งŒ ํ•˜๋ฉด ๋˜๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค.

์ด ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•˜์ž ํŽ˜์ด์ง€ ์ง„์ž… ๋ฐ ์ƒˆ๋กœ๊ณ ์นจ์‹œ 0.6์ดˆ๋กœ INP๊ฐ€ ๊ฐœ์„ ๋๋‹ค. ์•ฝ 1์ดˆ ์ •๋„ ๊ฐœ์„ ์ด ๋๋‹ค.

3. zustand + react-query๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ•„ํ„ฐ๋ง ์ตœ์ ํ™”โ€‹

๋งˆ์ง€๋ง‰ ๋ณ‘๋ชฉ์€ ํ•„ํ„ฐ๋ง ์‹œ ๋งค๋ฒˆ AUM ๊ณ„์‚ฐ ํ•จ์ˆ˜๊ฐ€ ์žฌ์‹คํ–‰๋œ๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค. ์œ„์—์„œ ๋„คํŠธ์›Œํฌ API ๋ณ€๊ฒฝ ๋ฐ js blocking์„ ์œ ๋ฐœํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ตœ์ ํ™”ํ–ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํ•„ํ„ฐ๋งํ•  ๊ฒฝ์šฐ ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜๊ธฐ์— ์•ฝ๊ฐ„์˜ ๋ฒ„๋ฒ…๊ฑฐ๋ฆผ์ด ์กด์žฌํ–ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ํ•„ํ„ฐ ๋กœ์ง์„ zustand ๋‚ด๋ถ€๋กœ ์˜ฎ๊ฒจ๋ณด์•˜๋‹ค.

export const useTimeSeriesStore = create<TimeSeriesState>((set, get) => ({
aums: {},
filters: {
strategy: "all", // ์ดˆ๊ธฐ๊ฐ’: ํ•„ํ„ฐ ์—†์Œ
organizationId: "all", // ์ดˆ๊ธฐ๊ฐ’: ํ•„ํ„ฐ ์—†์Œ,
asset: "krw",
},
setAums: (aumObj) => set({ aums: aumObj }),
setOrganizationFilter: (organizationId: string) => {
set((state) => ({
filters: {
...state.filters,
organizationId,
},
}));
},
setAssetFiler: (asset) =>
set((state) => ({
filters: {
...state.filters,
asset,
},
})),
getFilteredAums: () => {
// TODO : ํšŒ๊ณ ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ๋ณด์ด๋Š” ๊ฐœ์„  ํฌ์ธํŠธ... ๋ถ„๊ธฐ๋ฌธ ๋”ฐ๋กœ ๋ถ„๋ฆฌํ•˜๊ธฐ
const { aums, filters } = get();
const { strategy, organizationId } = filters;

if (
(strategy === "all" || !strategy) &&
(organizationId === "all" || !organizationId)
) {
return aums;
}

const filteredAums: AUM = {};
Object.entries(aums).forEach(([date, entries]) => {
const filteredEntries = entries.filter(
(entry) =>
(strategy === "all" || entry.strategy === strategy) &&
(organizationId === "all" || entry.organizationId === organizationId),
);
if (filteredEntries.length > 0) {
filteredAums[date] = filteredEntries;
}
});
return filteredAums;
},
setStrategyFilter: (strategy) =>
set((state) => ({
filters: {
...state.filters,
strategy,
},
})),
}));

react-query๋ฅผ ํ†ตํ•ด ๋ฐ›์€ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ zustand ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ์ €์žฅ์†Œ์— ์ €์žฅํ•˜๋Š”๊ฒŒ ๋งž๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ๋Ÿฌ ์ด์œ ๋กœ ์ด๋Ÿฐ์‹์œผ๋กœ ์‹œ๋„ํ•ด๋ดค๋‹ค.

  1. ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ณต์œ ํ•˜๊ธฐ ์šฉ์ดํ•˜๋‹ค.
  2. ์ƒํƒœ์™€ ๋กœ์ง์„ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ์ผ๊ด€์„ฑ์ด ์ข‹์•„์ง„๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

์ตœ์ข… ๊ฒฐ๊ณผโ€‹

  • โœ… ํ•„ํ„ฐ ์ ์šฉ ์‹œ ๋ฐ˜์‘ ์†๋„ ๊ฐœ์„ 
  • โœ… LCP 200ms ๋ฏธ๋งŒ ์œ ์ง€ (Web Vitals ๊ธฐ์ค€ โ€˜Goodโ€™)
  • โœ… ์ „์ฒด ๋กœ์ง ๋‹จ 3๊ฐœ์˜ ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌ

์ฐธ๊ณ  : Google INP ์„ฑ๋Šฅ ๊ฐ€์ด๋“œ

๋น„์ ผ์‹œ์Šคํ…œ ์œ ์ง€๋ณด์ˆ˜ ๊ฐœ์„ โ€‹

  • ๊ณ ๋ฏผํ•˜๊ณ  ์žˆ๋Š” ํฌ์ธํŠธ
  1. ์œ ์ง€ ๋ณด์ˆ˜๋ž€ ๋ฌด์—‡์ด๋ฉฐ ์–ด๋–ป๊ฒŒ ์œ ์ง€ ๋ณด์ˆ˜ํ•˜๊ธฐ ์ข‹์€ ํ”„๋กœ๊ทธ๋žจ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š”๊ฑธ๊นŒ?
  • ํ•ด๋‹น ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์ง€์‹์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ ํฌ์ธํŠธ
  1. OOP
  2. FP
  3. Adapter Pattern

๋‹ค๋“ค ์–ด๋–ป๊ฒŒ ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์ข‹๊ฒŒ ํ”„๋ก ํŠธ์—”๋“œ ์„ค๊ณ„ํ•˜์‹œ๋‚˜์š”?

์„œ๋ฒ„ ์‘๋‹ต์— ๋Œ€ํ•œ Mapper๋ฅผ ๋‘์‹œ๋‚˜์š”?

๊ฒฐ๋ก โ€‹

ํ™•์‹คํžˆ 1๋ถ„๊ธฐ ํšŒ๊ณ ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ๋ฝ‘์€ Action point๋กœ ์ธํ•ด ๋ฐ”์œ ์™€์ค‘์—๋„ ์กฐ๊ธˆ์”ฉ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค.
๋ˆ„๊ตฐ๊ฐ€ ์‹œ์ผœ์„œ ํ•˜๋Š” ๊ฒƒ๋„ ์ž˜ํ•ด์•ผ ํ•˜์ง€๋งŒ, ์Šค์Šค๋กœ ๊ฐœ์„ ์ ์„ ์ฐพ์•„ ๊ฐœ์„ ํ•˜๋Š” ์žฌ๋ฏธ๋„ ์ ์ ํ•œ ๊ฒƒ ๊ฐ™๋‹ค.
์ด ๊ณผ์ •์—์„œ ๋‚ด๊ฐ€ ์–ผ๋งˆ๋‚˜ ์‹ค๋ ฅ์ด ๋ถ€์กฑํ•œ์ง€ ๊นจ๋‹ซ๋Š” ๊ฒƒ ๊ฐ™๋‹ค.
AI๋กœ ๋น ๋ฅด๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ฝ”๋“œ์˜ ํ•œ ์ค„์— ๋Œ€ํ•œ ์ด์œ ๋ฅผ ๊ณ ๋ฏผํ•˜๋ฉฐ ๊ทธ๋กœ ์ธํ•ด ๊ธฐ์—ฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๊ธฐ๋กœ ์˜ค๋Š˜๋„ ๋‹ค์งํ•ด๋ณธ๋‹ค.

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋‘ ๊ฐ์ž ์žˆ๋Š” ์ž๋ฆฌ์—์„œ ํŒŒ์ดํŒ…!

5์›” Action Pointโ€‹

  • FSD ๋ธ”๋กœ๊ทธ 2ํŽธ ์ž‘์„ฑ
  • ๋‹จ์œ„/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ ์šฉ ๋ฐ ํ•™์Šต
  • ๊ฐ์ฒด ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋Œ€ํ•ด ํ•™์Šต ์‹œ์ž‘ํ•˜๊ธฐ
  • ์บก์Аํ™”ํ•˜์—ฌ ํ”„๋กœ๊ทธ๋žจ ์œ ์ง€๋ณด์ˆ˜ ์‰ฝ๊ฒŒ ๋ฆฌํŒฉํ„ฐ๋ง
  • ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋Œ€ํ•ด ํ•™์Šต ์‹œ์ž‘ํ•˜๊ธฐ
  • Next.js ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ (fillsLog ์ง„ํ–‰ ์ค‘)
  • ์‚ฌ์šฉ์ž ๊ถŒํ•œ ์ •์ฑ… ๋„์ž… ์ •๋ฆฌ
  • ๋ฐฑ์—”๋“œ CI/CD ํ™œ์šฉํ•˜์—ฌ EB์— ์ปจํ…Œ์ด๋„ˆ ์ž๋™ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•