๐ค 25๋ 4์ ํ๊ณ
์๋ก โ
2025๋ 4์, ๋น์ ์์คํ ์ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ณ ๊ธฐ์กด ๊ธฐ๋ฅ์ ์ ์งยท๊ฐ์ ํ๋ ๊ณผ์ ์์ ์ค์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ง๊ฒฐ๋๋ ๋ ๊ฐ์ง ๋ฌธ์ ๋ฅผ ๋ง์ฃผํ๊ฒ ๋๋ค.
- Axios ์ค๋ณต ํธ์ถ ๋ฌธ์ โ ์๋ ๋ก๊ทธ์ธ ๊ณผ์ ์์ ๋ฐ์ํ ๋ถํ์ํ API ํธ์ถ๋ก ์ธํด ๋คํธ์ํฌ ๋น์ฉ ์ฆ๊ฐ
- 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 ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด์๋ค. ๋ค์๊ณผ ๊ฐ์ ์ ๋ต์ผ๋ก ํด๋น ํด๋์ค๋ ๊ตฌ์ฑ๋์ด ์๋ค.
- isRefreshing ์ํ๋ก ์ฒซ ์์ฒญ ์ดํ๋ถํฐ๋ ๋๊ธฐ ํ์ ๋ฃ๊ธฐ
- ํ ํฐ ๊ฐฑ์ ์๋ฃ ํ ํ์ ์์ธ ์์ฒญ์ ์๋ก์ด ํ ํฐ์ผ๋ก ์ฌ์์ฒญ
- ํด๋์ค๋ก ์ถ์ํํ์ฌ ์ธํฐ์ ํฐ ๋ด์์ ๋ฐ๋ณต๋๋ ๋ก์ง ์ ๊ฑฐ
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 ํด๋ผ์ด์ธํธ ์ํ ์ ์ฅ์์ ์ ์ฅํ๋๊ฒ ๋ง๋์ง ๋ชจ๋ฅด๊ฒ ๋ค. ํ์ง๋ง ์ฌ๋ฌ ์ด์ ๋ก ์ด๋ฐ์์ผ๋ก ์๋ํด๋ดค๋ค.
- ํํฐ๋ง๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๊ณต์ ํ๊ธฐ ์ฉ์ดํ๋ค.
- ์ํ์ ๋ก์ง์ ํ ๊ณณ์์ ๊ด๋ฆฌํ๋ฏ๋ก ์ฌ์ฌ์ฉ์ฑ๊ณผ ์ผ๊ด์ฑ์ด ์ข์์ง๋ค๊ณ ์๊ฐํ๋ค.
์ต์ข ๊ฒฐ๊ณผโ
- โ ํํฐ ์ ์ฉ ์ ๋ฐ์ ์๋ ๊ฐ์
- โ LCP 200ms ๋ฏธ๋ง ์ ์ง (Web Vitals ๊ธฐ์ค โGoodโ)
- โ ์ ์ฒด ๋ก์ง ๋จ 3๊ฐ์ ์์ ํจ์๋ก ๋ถ๋ฆฌ
์ฐธ๊ณ : Google INP ์ฑ๋ฅ ๊ฐ์ด๋
๋น์ ผ์์คํ ์ ์ง๋ณด์ ๊ฐ์ โ
- ๊ณ ๋ฏผํ๊ณ ์๋ ํฌ์ธํธ
- ์ ์ง ๋ณด์๋ ๋ฌด์์ด๋ฉฐ ์ด๋ป๊ฒ ์ ์ง ๋ณด์ํ๊ธฐ ์ข์ ํ๋ก๊ทธ๋จ์ ๋ง๋ค ์ ์๋๊ฑธ๊น?
- ํด๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ์ง์์ด๋ผ๊ณ ์๊ฐํ ํฌ์ธํธ
- OOP
- FP
- Adapter Pattern
๋ค๋ค ์ด๋ป๊ฒ ์ ์ง๋ณด์ํ๊ธฐ ์ข๊ฒ ํ๋ก ํธ์๋ ์ค๊ณํ์๋์?
์๋ฒ ์๋ต์ ๋ํ Mapper๋ฅผ ๋์๋์?
๊ฒฐ๋ก โ
ํ์คํ 1๋ถ๊ธฐ ํ๊ณ ๋ฅผ ์งํํ๋ฉฐ ๋ฝ์ Action point๋ก ์ธํด ๋ฐ์ ์์ค์๋ ์กฐ๊ธ์ฉ ์ ๋ฆฌํ ์ ์๊ฒ ๋๋ค.
๋๊ตฐ๊ฐ ์์ผ์ ํ๋ ๊ฒ๋ ์ํด์ผ ํ์ง๋ง, ์ค์ค๋ก ๊ฐ์ ์ ์ ์ฐพ์ ๊ฐ์ ํ๋ ์ฌ๋ฏธ๋ ์ ์ ํ ๊ฒ ๊ฐ๋ค.
์ด ๊ณผ์ ์์ ๋ด๊ฐ ์ผ๋ง๋ ์ค๋ ฅ์ด ๋ถ์กฑํ์ง ๊นจ๋ซ๋ ๊ฒ ๊ฐ๋ค.
AI๋ก ๋น ๋ฅด๊ฒ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ง๋ง, ์ฝ๋์ ํ ์ค์ ๋ํ ์ด์ ๋ฅผ ๊ณ ๋ฏผํ๋ฉฐ ๊ทธ๋ก ์ธํด ๊ธฐ์ฌํ๋ ๊ฐ๋ฐ์๊ฐ ๋๊ธฐ๋ก ์ค๋๋ ๋ค์งํด๋ณธ๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค. ๋ชจ๋ ๊ฐ์ ์๋ ์๋ฆฌ์์ ํ์ดํ !
5์ Action Pointโ
- FSD ๋ธ๋ก๊ทธ 2ํธ ์์ฑ
- ๋จ์/ํตํฉ ํ ์คํธ ์ ์ฉ ๋ฐ ํ์ต
- ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ์ ๋ํด ํ์ต ์์ํ๊ธฐ
- ์บก์ํํ์ฌ ํ๋ก๊ทธ๋จ ์ ์ง๋ณด์ ์ฝ๊ฒ ๋ฆฌํฉํฐ๋ง
- ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ ๋ํด ํ์ต ์์ํ๊ธฐ
- Next.js ์ฌ์ด๋ ํ๋ก์ ํธ ์งํ (fillsLog ์งํ ์ค)
- ์ฌ์ฉ์ ๊ถํ ์ ์ฑ ๋์ ์ ๋ฆฌ
- ๋ฐฑ์๋ CI/CD ํ์ฉํ์ฌ EB์ ์ปจํ ์ด๋ ์๋ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ