๐ค 25๋ 5์ ํ๊ณ
์๋ก โ
์ง๋๋ฌ, ์ ์ง๋ณด์ ํ๊ธฐ ์ฌ์ด ํ๋ก ํธ์๋ ์ค๊ณ๋ฅผ ๊ณ ๋ฏผํด ๋ณด๊ณ 5์์ ์ ์ฉํด๋ณด๋ ค๊ณ ํ๋ค. '์ด๋ป๊ฒ ์ ์ง๋ณด์ ํ๊ธฐ ์ข์ ์ํํธ์จ์ด๋ฅผ ๋ง๋ค ์ ์์๊น?'์ ๋ํ ์๊ฐ๊ณผ ๊ณ ๋ฏผ์ OOP๋ก ์๋ ด๋๊ณ ์๋ค. ํน์๋ ํ๋ก ํธ์๋์๋ ํฐ ๊ด๋ จ์ด ์๋ ๊ฒ์ ์๋์ง ์์๊ฐ ์ถ์ ์ ์๋ค. ํ์ง๋ง ๊ณต๋ถํ๋ฉด ๊ณต๋ถํ ์๋ก ๋ฆฌ์กํธ์ ์ ์ฉํ ๋ถ๋ถ์ด ๋ง๋ค๋ ๊ฒ์ ๋๋๋ค. ์ฃผ์ฅ๊ฐ์ ๋ด์ฉ๋ค์ด ๋ง์๊ณ ์ด๋ฅผ ์ํํด์ ์ฒด๋ํ๋ ์๊ฐ์ ๊ฐ์ง๊ณ ์๋ค. ๊นจ๋ํ ์ฝ๋๋ฅผ ์์ฑํ๋ ์ด์์ ๋ค๋ฌ์ ์ ์๊ฒ ๋๋ค!
5์ Action Pointโ
- FSD ๋ธ๋ก๊ทธ 2ํธ ์์ฑ
- ๋จ์/ํตํฉ ํ ์คํธ ์ ์ฉ ๋ฐ ํ์ต
- ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ (ํด๋ฆฐ ์ฝ๋์ค 1ํ๋ )
- ์บก์ํํ์ฌ ํ๋ก๊ทธ๋จ ์ ์ง๋ณด์ ์ฝ๊ฒ ๋ฆฌํฉํฐ๋ง (current)
- Next.js ์ฌ์ด๋ ํ๋ก์ ํธ ์งํ (fillsLog ์งํ ์ค) -> ์น ๋ทฐ๋ฅผ ๊ฒฝํํด๋ณด๊ณ ์ถ์ด์ ๊ด๋ จ๋ ์ฌ์ด๋ ํ๋ก์ ํธ ์งํํด๋ณผ ์์
- ์ฌ์ฉ์ ๊ถํ ์ ์ฑ ๋์ ์ ๋ฆฌ
- ๋ฐฑ์๋ CI/CD ํ์ฉํ์ฌ EB์ ์ปจํ ์ด๋ ์๋ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ
๊ณํํ์ง ์์์ง๋ง ํด๋ธ ๊ฒ
- ๋ฐฑ์๋ API ์ฒซ ์์ ๋ฐ ๋ฐฐํฌ
- ๋ฐฑ์๋ ๋ก๊ทธ์ธ ๊ธฐ๋ก ํน์ ๊ธฐ๊ฐ ์ธ ๋ฐ์ดํฐ๋ ์๋ ์ญ์ ๊ธฐ๋ฅ ์ถ๊ฐ!
- ๋ฐฑ์๋ ๊ถํ๋ช ๊ธฐ์กด User -> Client๋ก ๋ณ๊ฒฝ ์ฑ๊ณต! (์ ์ง๋ณด์ํ๊ธฐ ์ข์ ์ฝ๋๋ก ๋ง๋ค์ด๋ผ!)
5์์ ๊ฐ์ ์ ๋ฌ์ด๋ ๋งํผ ๋ง์ ํด์ผ์ด ์์๊ณ ๊ณต๋ถํ ์ ์๋ ์๊ฐ์ด ๋ค๋ฅธ ๋ฌ๋ณด๋ค ์๋์ ์ผ๋ก ๋ง์๋ค. ๊ทธ๋์ ์๊ฐ์ ํ๋ณดํด์ github action์ ๋ํ ๊ฐ์, docker์ ๋ํ ๊ฐ์, OOP๋ฅผ ์ค๋ช ํ๋ ๊ฐ์, ๋จ์ ํ ์คํธ์ ๊ธฐ์ ์ด๋ผ๋ ์ฑ ์ ๋ฐ์ ์ฝ์ผ๋ฉด์ ๊ณต๋ถํ๋ค. ์ด๋ฅผ ์ค๋ฌด์ ์ด๋ป๊ฒ ๋ น์ฌ๋ผ ์ ์์๊น๋ฅผ ๊ณ ๋ฏผํ ์ ์๋ ์๊ฐ์ ๊ฐ์ก๋ค. 5์ ๋ด๋ด ๋ค์ด๊ฐ Input์ด 6์์ ๋๋ฌ๋๊ธธ ์๋งํด ๋ณธ๋ค.
ํ๋ ๊นจ๋ฌ์ ๊ฒ์ "ํ๋ก ํธ์๋๋ OOP๋ฅผ ์๊ณ ์์ด์ผ ํ๋ค."์ด๋ค. ๊ทธ ์ด์ ๋ ์ฐจ์ฐจ ์ดํด๋ณด์.
๋ณธ๋ก โ
์ด๋ฒ ๋ฌ์ ๋น์ ผ ์์คํ ์ฃผ์ ๊ธฐ๋ฅ ๊ตฌํ๊ณผ ๊ฐ๋ฐ์ ๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํ๋ ์๊ฐ์ ๊ฐ์ก๋ค. ์๋ ์ง์๋ค์ ํ์ฉํด์ ์ฌ๋ด ๊ฐ๋ฐ์ ๊ฒฝํ์ ํฅ์ํ๊ณ ํ์ฌ์ ๋์์ด ๋๋ ๊ฒ๋ค์ ๋ง๋๋๋ฐ ๋ ธ๋ ฅํ๋ค. ๊ทธ๋ฌ๋ ์์ค ๊นจ๋ฌ์ ๊ฒ์ ๋ด๊ฐ ์ข์ํ๋ ๊ฑด ๋ฆฌ์กํธ ๊ฐ๋ฐ์ด ์๋๋ผ ๊ฒฐ๊ตญ ๋๋ฃ๋ค์ ๋๊ณ ๊ณ ๊ฐ๋ค์ ๋๋ ๊ฒ ๊ธฐ์๋ค๋ ๊ฒ์ด๋ค. ๋ฆฌ์กํธ๊ฐ ์๋์ด๋ ๋ ์ง ๋ชจ๋ฅธ๋ค๋ ์์ผ๊ฐ ์๊ฒผ๋ค.
์ค์ํ ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ์ - ์ฌ์ฉ์์ ์ญํ ๊ณผ ์ฑ ์ ๊ธฐ๋ฅ ๊ตฌํโ
๊ธฐ์กด ์ดํ๋ฆฌ์ผ์ด์
์ ๊ถํ์ ADMIN
๊ณผ USER
, 2๊ฐ์ง๋ง ์กด์ฌํ๋ค.
์ฌ๋ด ๋ชจ๋ ์ธ์๋ค์ด ๋ชจ๋ ์ด๋๋ฏผ ๊ถํ์ ๊ฐ์ง ๊ฐ ๊ณ์ ์ ์์ ํ๊ณ ์์๊ณ ์ดํ๋ฆฌ์ผ์ด์
๋ด๋ถ์๋ ๊ณ ๊ฐ์ ์ค์ํ ๋ฐ์ดํฐ ๋ฐ ๋ฏผ๊ฐํ ์ ๋ณด๋ค์ด ์กด์ฌํ๋ค.
What if
์ด ๋ฐ์ดํฐ๋ฅผ ํ์ฉํด์ ์ ์์ ์ธ ์ผ์ ์๋ ํ๋ค๋ฉด...?
์ต๊ทผ ๋ค์ด ์๋ ๋ง์ ํดํน ์ฌ๊ฑด๋ค์ด ๋ง์์... ๊ฐ๋งํ ์์ผ๋ฉด ์ ๋๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์ด ๊ธฐ๋ฅ์ ๋์ ํ์๊ณ ์ ์ํ๊ณ ๋ฐ์๋ค์ฌ์ก๋ค.
๊ทธ๋์ ์ด๋๋ฏผ์ ๊ถํ์ ๋ํ์์๊ฒ๋ง ๋ถ์ฌํ๊ณ ์ฌ๋ด ์ง์์ Staff๋ก ๊ถํ์ ๋ฐ๋ก ๋ง๋ค์ด, ์ด๋๋ฏผ์ด ์คํํ๊ฐ ์ฌ์ฉ ๊ฐ๋ฅํ ๊ถํ์ ๋ถ์ฌํ๋ ๋ฐฉ์์ผ๋ก ๋ณด์์ ๊ฐํํ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
์ด๋ป๊ฒ ๊ตฌํํ ๊ฒ์ธ๊ฐ?โ
๋ฐ์ดํฐ ์๋ฃ ๊ตฌ์กฐ ํ์์?
AWS IAM ์ ์ฑ
์ ์ฐธ๊ณ ํ์ฌ ์์ด๋์ด๋ฅผ ์ ์ํ๋ค. ๊ฒฐ๋ก ์ ์ผ๋ก RBAC (Role-Based Access Control) ๋ฐฉ์์ผ๋ก ๊ตฌํํ๊ธฐ๋ก ๊ฒฐ์ ๋๊ณ ๋ฐ์ดํฐ๋ ์๋์ ๊ฐ์ ํํ์ด๋ค.
{
"role": "admin",
"privileges": ["read:domain", "write:domain", "delete:domain",...]
}
์์ ๊ฐ์ ํํ๋ก ๊ฐ ์ญํ ์ ๋ฐ๋ฅธ privileges๋ฅผ ๋ฃ์ด์ฃผ๋ ๋ฐฉ์์ผ๋ก ํฉ์ํ๋ค.
์ฌ์ฉ์ ๊ถํ ํ์ธ ์ปค์คํ ํ ๊ตฌํ
๊ถํ์ ํ์ธํ๊ณ ํด๋น ๊ถํ์ด ์ ์ ์๊ฒ ์๋์ง ํ์ธํ๋ ๋ก์ง์ ํ ๊ตฐ๋ฐ์์ ๊ด๋ฆฌํด์ผํ๋๋ฐ ํด๋น ๊ธฐ๋ฅ์ ์ํํ๋ ํ ์ ๋ง๋ค์ด๋ณด์๋ค.
// usePrivilege.ts
import { useQuery } from "@tanstack/react-query";
import { AuthQueries } from "@/entities/auths";
import { Privilege } from "../constants/privileges";
import { hasPrivilege } from "../utils";
export type Mode = "or" | "and";
export type PrivilegeProps = {
required: Privilege | Privilege[];
mode?: Mode;
};
export function usePrivilege({ required, mode }: PrivilegeProps): boolean {
const { data: user } = useQuery(AuthQueries.getMyInfo());
const privileges = user?.privileges || [];
return hasPrivilege(privileges, required, mode);
}
// utils > hasPrivilege
export function hasPrivilege(
userPrivileges: string[] = [],
requiredPrivilege: Privilege | Privilege[],
mode: Mode = "or",
): boolean {
const requiredList = Array.isArray(requiredPrivilege)
? requiredPrivilege
: [requiredPrivilege];
if (mode === "and") {
return requiredList.every((requiredPrivilege) =>
userPrivileges.includes(requiredPrivilege),
);
}
return requiredList.some((requiredPrivilege) =>
userPrivileges.includes(requiredPrivilege),
);
}
๋จผ์ ๋ usePrivilege ํ ์ ๋ง๋ค์๋ค. ๊ทธ ์์์ ์ ์ ์ ์ํ๋ฅผ ๊ฐ์ง๊ณ ์จ ํ, privilege ๋ฐฐ์ด์ ํ์ธํ ํ, ์ธ์๋ก ๋ฐ์ ๊ถํ์ด ์ ์ ๊ฐ ๊ฐ์ง๊ณ ์๋ ๊ถํ ์ค์ ์๋์ง ๋น๊ตํ๋ ๋ก์ง์ด ํ์ํ๋ค. ๊ทธ๋์ ์ฒ์์ useprivilege ํ ๋ด๋ถ์ ๋ก์ง์ ๋์๋๋ฐ ์๊ฐํด ๋ณด๋ ๋ณต์กํ ๊ฒฝ์ฐ๊ฐ ์์ ์ ์์๋ค.
๊ถํ์ ๋ฐ๋ฅธ UI๋ฅผ ์ ์ดํ ๋ ๋ ๊ฐ์ ๊ถํ์ค ํ๋๋ง ์์ ๊ฒฝ์ฐ, 5๊ฐ์ ๊ถํ์ด ๋ชจ๋ ์์ ๊ฒฝ์ฐ ๋ฑ AND, OR์ ๊ฐ์ ๋ ผ๋ฆฌ ์ฐ์ฐ์ ํ๋ ๊ฒฝ์ฐ๊ฐ ์๋ค๋ ๊ฒ์ ์๊ฒ ๋๋ค.
๊ทธ๋์ hasPrivilege๋ผ๋ ์ ํธํจ์๋ก ๋ถ๋ฆฌํ๊ณ every์ some ๋ฉ์๋๋ฅผ ํ์ฉํด์ ๊ตฌํํ๋ค. ๋ ๋์ ๊ตฌ์กฐ๊ฐ ์๊ฑฐ๋ ์๋ฌธ์ด ๋๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ ๋ง ํธํ๊ฒ ๋ง์ํด ์ฃผ์ธ์~ ๋ ์ข์ ๋ฐฉ๋ฒ์ด ์๋์ง ๊ถ๊ธํด์ ๊ณต์ ํด์!
ํ๋ก ํธ UI ์ ์ด๋ ์ด๋ ๊ฒ ํด๋ ๋์ง ์์๊น?
๊ทธ๋ผ ๋ชจ๋ ํ๋ฉด ๊ณณ๊ณณ์ ์กด์ฌํ๋ UI์ ๋ฐ๋ฅธ ๊ธฐ๋ฅ์ ์ด๋ป๊ฒ ์ ์ดํ๋ ๊ฒ ์ข์๊น? ์ฐ์ UI ์ ์ด๋ฅผ ๋ด๋นํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด์ผ๊ฒ ๋ค๊ณ ์๊ฐํ๋ค.
import { ReactElement, ReactNode, cloneElement, isValidElement } from 'react';
import { useToast } from '../lib';
import { PrivilegeProps, usePrivilege } from '../lib/hooks/usePrivilege';
import NoPrivilegeOverlay from './NoPrivilegeOverlay';
type PrivilegeGateProps = PrivilegeProps & {
children: ReactNode;
render?: 'hide' | 'disabled' | 'custom';
variant?: 'page' | 'card' | 'section' | 'inline';
};
const PrivilegeGate = ({
required,
children,
mode,
render = 'hide',
variant = 'page',
}: PrivilegeGateProps) => {
const hasAccess = usePrivilege({ required, mode });
const { toast } = useToast();
if (hasAccess) return <>{children}</>;
if (render === 'disabled') {
if (isValidElement(children)) {
const onClick = () => {
toast({
title: 'You donโt have permission to perform this action.',
});
};
return cloneElement(children as ReactElement, {
onClick,
style: { pointerEvents: 'auto', opacity: 0.5, cursor: 'not-allowed' },
title: '๊ถํ์ด ์์ต๋๋ค',
});
}
return null;
}
if (render === 'custom') {
return <NoPrivilegeOverlay variant={variant} />;
}
return null;
};
export default PrivilegeGate;
ํ์์ PrivilegeGate ์ปดํฌ๋ํธ์ธ๋ฐ ์๊ตฌ ์ฌํญ์ด ์กฐ๊ธ์ฉ ์ถ๊ฐ๋๋ฉด์ if ๋ถ๊ธฐ๊ฐ ๋ง์์ง๋๋ฐ... ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๋ ๋ฐฉ๋ฒ์ ๊ฐ๊ตฌํด๋ณด๋ ค๊ณ ํ๋ค.
์ฐ์ ์์ ์ปดํฌ๋ํธ๋ฅผ ๋ฐ๊ณ ์กฐ๊ฑด์ ํตํด return ๊ฐ์ ๋ฐ๊ฟ์ฃผ๋ ์ญํ ์ ๋ด๋นํ๋ ์ปดํฌ๋ํธ์ด๋ค.
์๋ฅผ ๋ค์ด ํน์ ๋ฒํผ์ด ๊ถํ์ ๋ฐ๋ผ ์ ์ด๋์ด์ผ ํ๋ค๋ฉด ์๋์ ๊ฐ์ด ๊ตฌํํ ์ ์๋ค.
import ...
export const CreateHistoricalSnapshotButton = ({
dashboardItem,
openModal,
closeModal,
}: { dashboardItem: Dashboard } & ModalHandlerProps) => {
const handleCreateClick = () => {
openModal(
<CreateSnapshotForm
accountId={dashboardItem.accountId}
onClose={closeModal}
/>,
);
};
return (
<PrivilegeGate
render="hide"
required={PRIVILEGES.CREATE_HISTORICAL_ACCOUNT_SNAPSHOT}
>
<Button onClick={handleCreateClick} variant="auth">
Create Snapshot Records
</Button>
</PrivilegeGate>
);
};
์ปดํฌ๋ํธ ๋ด๋ถ ์ต์์์ PrivilegeGate ์ปดํฌ๋ํธ๋ฅผ ํตํด ์ ์ดํ๋ ๊ฒ ์ฒ์์๋ ์ข๋ค๊ณ ์๊ฐํ๋ค. ํ์ง๋ง ๊ณฐ๊ณฐ์ด ์๊ฐํด ๋ณด๋ฉด CreateHistoricalSnapshotButton๋ผ๋ ์ปดํฌ๋ํธ ์ ์ฅ์์๋ ๊ถํ์ ๋ํด ์ ํ์๊ฐ ์์์ ์๊ฐํ๋ค. ๊ทธ๋์ ๋ถ๋ฆฌํด์ ์ปดํฌ๋ํธ ์ธ๋ถ๋ก ์ฎ๊ฒผ๋ค.
import ...
export const CreateHistoricalSnapshotForm = ({...}) => {
return (
<>
...
<PrivilegeGate
render="hide"
required={PRIVILEGES.CREATE_HISTORICAL_ACCOUNT_SNAPSHOT}
>
<CreateHistoricalSnapshotButton .../>
</PrivilegeGate>
</>
);
};
์ด๋ฐ ์์ผ๋ก UI๋ฅผ ์ ์ดํ๋ ์ปดํฌ๋ํธ๋ ๋์ ์ปดํฌ๋ํธ ์์์์ ํ์ธํ ์ ์๋๋ก ํ๋ ๊ฒ ๋ง๋ ๊ฒ ๊ฐ๋ค. ๋ณด๊ธฐ์๋ ์ง์ ๋ถํ๋ฐ, ์ปดํฌ๋ํธ ์ ์ฅ์์ ์๊ฐํด ๋ณด๋ฉด ์ด๊ฒ ๋ง๋ ๊ฒ ๊ฐ๋ค.
๊ทธ๋ฐ๋ฐ... ์๋ฒ์์ ๊ถํ์ด ์ ๋ฐ์ดํธ๋๋ฉด ์ผ์ผ์ด ํ๋ฐํธ์๋ ๊ฐ๋ฐ์์๊ฒ ๋งํด์ค์ผ ํ๋?!
์ด ๋ถ๋ถ๋ ๊ณ ๋ฏผ์ด์๋ค. ๊ฐ ๊ถํ์ ๋ฐ๋ฅธ ์ฑ ์ ๋ฌธ์์ด ๋ฐฐ์ด์ด ์๋๋ฐ ์ด๋ฅผ ์๋ฒ์์ CRUD ํ ๊ฒฝ์ฐ, ํด๋ผ์ด์ธํธ์์ ์ด๋ป๊ฒ ๋๊ธฐํํ ์ ์์์ง๊ฐ ๊ณ ๋ฏผ์ด์๋ค. ํ์ฌ๋ ๋ณ๊ฒฝ๋ ๋ ๋งํด์ฃผ๋ฉด ๋ฐ๋ก ์์ ํ ์ ์๋ค๋ง...
์คํฌ๋ฆฝํธ์ CI/CD๋ฅผ ํ์ฉํด ๋ณด๋ฉด ํด๊ฒฐํ ์ ์๋ค๊ณ ์๊ฐํ๋ค.
import ...
// dotenv ๋ก๋
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const API_BASE = "๊ฐ๋ฐ_์๋ฒ_URL";
const LOGIN_EMAIL = "๊ฐ๋ฐ_์๋ฒ_์ด๋ฉ์ผ ";
const LOGIN_PASSWORD = "๊ฐ๋ฐ_์๋ฒ_๋น๋ฐ๋ฒํธ";
const OUTPUT_FILE = path.resolve(
__dirname,
"../src/shared/lib/constants/privileges.ts",
);
// ๊ถํ ๋ฌธ์์ด์ ์์ ํค ํํ๋ก ๋ณํ
function toConstKey(privilege: string): string {
return privilege.toUpperCase().replace(/[^A-Z0-9]/g, "_");
}
async function loginAndGetToken(): Promise<string> {
const res = await axios.post(
`${API_BASE}/v1/login`,
{
username: LOGIN_EMAIL,
password: LOGIN_PASSWORD,
},
{
headers: {
"Content-Type": "multipart/form-data", // FormData ์ ์ก ํ์ ์ค์
},
},
);
const token = res.data.access_token;
if (!token) {
throw new Error("โ ๋ก๊ทธ์ธ ์ฑ๊ณตํ์ง๋ง ํ ํฐ์ด ์์ต๋๋ค.");
}
return token;
}
async function fetchPrivileges(token: string): Promise<string[]> {
const res = await axios.get(`${API_BASE}/v1/administrators/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return res.data.data.privileges;
}
async function generate() {
try {
const token = await loginAndGetToken();
const privileges = await fetchPrivileges(token);
const entries = privileges
.map((p) => `${toConstKey(p)}: '${p}',`)
.join("\n ");
const content = `// โ ๏ธ This file is auto-generated for role ADMIN. Do not edit manually.
export const PRIVILEGES = {
${entries}
} as const;
export type Privilege = (typeof PRIVILEGES)[keyof typeof PRIVILEGES];
`;
fs.writeFileSync(OUTPUT_FILE, content, "utf-8");
console.log(
`โ
privileges.ts generated for role ADMIN with ${privileges.length} privileges.`,
);
} catch (err) {
console.error("โ Failed to generate privileges.ts");
console.error(err);
}
}
generate();
๋ช ์๋์ ํด๋ฆฐ ์ฝ๋์ฐ๋ฅผ 3์ฃผ๊ฐ ํ์ตํ๊ณ ๋ด ์ฝ๋๋ฅผ ๋ณด๋๋ฐ ๊ณ ์น ๊ฒ๋ค์ด ๋ง์ด ๋ณด์ธ๋ค... ๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ ๊ณต์ ํ๋ค. ํผ๋๋ฐฑ์ ๊ธฐ๋ค๋ฆฝ๋๋ค!!
์ฐ์ ๋ก๊ทธ์ธ์ ํ๊ณ privilege๋ฅผ ๊ฐ์ง๊ณ ์ค๊ณ ์ด๋ฅผ ์์ ํ์ผ๋ก generateํด์ฃผ๋ ์ญํ ์ ์ํํ๋ ์คํฌ๋ฆฝํธ์ธ๋ฐ ์ด๋ฅผ ๋น๋ํ๊ธฐ ์ ์์ ์ ์คํ์ํค๋๋ก ํ๋ค. ์ฐพ์๋ณด๋ prebuild๋ผ๋ ๊ฒ ์์๋ค.
//package.json
{
...
"scripts": {
"dev": "vite --host 0.0.0.0",
"generate:privileges": "node --loader ts-node/esm scripts/generate-privileges.ts",
"prebuild": "npm run generate:privileges",
"build": "vite build --debug",
"preview": "vite preview --port 8080",
"prettier": "prettier --write .",
"test": "vitest",
"prepare": "husky",
"lint-staged": "lint-staged",
"steiger": "npx steiger ./src --watch"
},
}
์ด๋ ๊ฒ ํ๋ฉด ๋น๋๋๊ธฐ ์ ํด๋น ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋๋ฉด์ ๊ถํ์ ๋๊ธฐํ๊ฐ ๋๋ค. ๊ทธ๋ผ Privilege ์ฒดํฌ ๋ฐ์ค๊ฐ ๋ง๋ค์ด์ง ๋ฐฐ์ด์ mapํ๋๋ก ํ๋ฉด ์๋ฒ์์ Privilege๊ฐ ๋ณ๊ฒฝ๋์ด๋ ํด๋ผ์ด์ธํธ์ ๋งํด์ฃผ์ง ์์๋ ๋์ง ์์๊น ์ถ๋ค.
๋ ์๋, ์์ ์ฝ๋๋ ์ด๋ค ๋ฌธ์ ๊ฐ ์์ ๊ฒ ๊ฐ์์? ๋ ์ข์ ๋ฐฉ๋ฒ์ด ์์๊น์?
2. ๋ถํธํ๊ฑด ๊ณ ์ณ์ผ์ง!โ
๊ฒฐ๋ก ๋ถํฐ ์ ๋ฆฌํด ๋ณด๋ฉด ๋ฐฑ์ค๋ API๋ฅผ ์์ ํ์๋ค.
1๋ฒ์์ ๋ดค๋ฏ ๊ถํ์ ๋ฐ๋ฅธ ์ฑ ์์ ๋ถ์ฌํ๋ ๊ธฐ๋ฅ ๊ตฌํํ๋ ๊ณผ์ ์์ ์ฒ์์ API์ end point๊ฐ ๊ฐ๊ฐ ๋๋ ์ ธ ์์๋ค.
์๋ฅผ ๋ค์ด
GET / ์ด๋๋ฏผ ๋ชฉ๋ก : /members/administrators/all
GET / ์คํํ ๋ชฉ๋ก : /members/staff/all
GET / ์ ์ ๋ชฉ๋ก : /members/users/all
GET / ์ด๋๋ฏผ ๋จ์ผ : /members/administrator
GET / ์คํํ ๋จ์ธ : /members/staff
GET / ์ ์ ๋จ์ผ : /members/user
์ด๋ฐ์์ผ๋ก ๋๋ ์ ธ์์๋ค.
์ด๋ก ์ธํด ๊ถํ์ ๋ฐ๋ผ ๋ค๋ฅธ API๋ฅผ ํธ์ถํด์ผ ํ๋ ๋ถ๊ธฐ๊ฐ ์๊ฒจ ๋ถํ์ํ๊ฒ UI ๋ฐ ๊ธฐ๋ฅ์ ๊ตฌํํ๋๋ฐ ๋ณต์กํด์ ธ ๋ฒ๋ ธ๋ค.
GET / ๋งด๋ฒ ๋ชฉ๋ก : /members/all?role=${role}
GET / ๋งด๋ฒ ๋จ์ผ : /members?role=${role}&id=${id}
๊ทธ๋์ ์๋์ ๊ฐ์ด ๋ฐ๊พธ๋ ค๊ณ ํ๋ค.
์๋ฌด๋ฆฌ ์๊ฐํด๋ ์ด๋๋ฏผ์์๋ง ์ฌ์ฉํ๋ ๊ธฐ๋ฅ์ด๊ณ ๋ฉค๋ฒ๋ฅผ ์กฐํํ๊ธฐ ๋๋ฌธ์ API์ Endpoint๊ฐ ๋ค๋ฅผ ํ์๊ฐ ์๋ค๊ณ ๋๊ผ๋ค. ์ฐจ๋ผ๋ฆฌ ๋ถ๊ธฐ ์ฒ๋ฆฌ๊ฐ ์๋ฒ์ ์๋ ๊ฒ ์ข์ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ๋ค. ๊ทธ๋์ ์๋์ ๊ฐ์ด ๋ฐฑ์๋ ์๋ฒ๋ฅผ ์์ ํ๋ค.
๋ฐฑ์๋๋ fast API๋ก ๊ตฌ์ฑ๋์ด ์๊ณ repository ๋์์ธ ํจํด์ด ์ ์ฉ๋์ด ์๋ค. ์๋ฒฝํ์ง ์์ง๋ง ๊ณต์ ํ๋ค. ๋ณด๊ณ ์ด์ํ ๋ถ๋ถ์ด ์์ผ๋ฉด ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ ๋ค๋ฉด ๋ง์ ๋์์ด ๋ ๊ฒ ๊ฐ์ต๋๋ค!!
// ์ด๋๋ฏผ ์ปจํธ๋กค๋ฌ
@router.get(
"/members/all",
response_model=CommonResponse,
tags=[Tag],
responses={...}
)
async def get_all_member_async(
role: Role,
administrator_token_data: AdministratorTokenData = Depends(JWTHandler.is_administrator())) -> JSONResponse:
if role == Role.Administrator:
return await administrator_service.get_all_administrators_async(administrator_token_data=administrator_token_data)
if role == Role.Staff:
return await administrator_service.get_all_staff_async(administrator_token_data=administrator_token_data)
if role == Role.Client:
return await administrator_service.get_all_users_async(administrator_token_data=administrator_token_data)
์ด๋ฐ ์์ผ๋ก ๊ตฌํํ๋ค. ๋ฐ๋ role์ ๋ฐ๋ผ ๋ค๋ฅธ ์๋น์ค๋ฅผ ๋ถ๋ฌ์ค๋๋ก ํ๋ค. ์ด๋ฌ๋ฉด ํ๋ก ํธ์์์ ๋ณต์กํ๋ ๋ฌธ์ ๋ฅผ ๋จ๋ฐฉ์ ํด๊ฒฐํ ์ ์์ ๊ฒ ๊ฐ์๋ค! ํ์ง๋ง ์ด ์ฝ๋๋ฅผ ์์ ํ๊ณ ๋ฐฐํฌํ๋ ๊ณผ์ ์ด ํ๋ํ๋ค...
๋ฐฑ์๋ ์์ค ์ฝ๋ ์์ ๋ฐ ๋ฐ์ํ๋ ๊ณผ์ ์์ ์ด๋ ค์ ์ง๋ง ์๋ CI/CD๋ฅผ ๊ตฌ์ถํ์ฌ ํ์ ํ ์ ์๋ ํ๊ฒฝ์ ๊ตฌํํ๋ค. (3์ผ๊ฐ ์ ๋ฌด ์ด์ธ์ ์๊ฐ์ ๋ชจ๋ ํฌ์ํ๋ค.)
AS-IS
ํ์ฌ ๋๋ฃ OS๋ Window์ด๋ฉฐ ๋ Mac OS์ด๋ค. ๋ฐฑ์๋ ๊ฐ๋ฐ ํ๊ฒฝ์ Python, Fast API ์ด๋ฉฐ ๋ฐฐํฌ ํ๋ซํผ์ AWS Elastic Bean stalk์ ํ์ฉํ๊ณ ์๊ณ ๋ฐฐํฌ๋ ์๋์ผ๋ก ์งํํ๊ณ ์์๋ค. 3๊ฐ์ ์ , git branch ์ธ์ ์ ์งํํ๊ณ ๊ทธ ๊ฒฐ๊ณผ ๋ธ๋์น๋ฅผ ๋๋ ์ ๊ด๋ฆฌํ๊ณ ์์์ง๋ง ์๋์ผ๋ก ๊ฐ ๋ธ๋์น์ ์์ค ์ฝ๋๋ฅผ zip ๋ณํํ์ฌ ์๋์ผ๋ก ๋ฐฐํฌํ๊ณ ์์๋ค.
TO-BE ๊ฒฐ๊ณผ์ ์ผ๋ก ์ฌ๋ฌ ์ํ ์ฐฉ์ค ๋์ ์๋์ yml์ผ๋ก ์ด๋ ํ๊ฒฝ์์๋ ์๋ ๋ฐฐํฌ๊ฐ ๋๋๋ก ๊ตฌํํ์๋ค.
name: Deploy to the Development, AWS Elastic Beanstalk
on:
pull_request:
types: [closed]
branches:
- develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python3 -m pip install pipreqs
pipreqs . --force
pip install -r requirements.txt
- name: Zip the application
run: |
zip -r vision-system.zip .
- name: Deploy to AWS Elastic Beanstalk
uses: einaregilsson/beanstalk-deploy@v22
with:
application_name: Vision-System
environment_name: Vision-System-development-backend-server
version_label: ${{ github.sha }}
region: ap-northeast-2
aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deployment_package: vision-system.zip
use_existing_version_if_available: true
name: Deploy to the Production, AWS Elastic Beanstalk
on:
push:
branches:
- main
pull_request:
types: [closed]
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Zip the application
run: |
zip -r vision-system.zip .
- name: Deploy to AWS Elastic Beanstalk
uses: einaregilsson/beanstalk-deploy@v22
with:
application_name: Vision-System
environment_name: Vision-System-production-backend-server
version_label: ${{ github.sha }}
region: ap-northeast-2
aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deployment_package: vision-system.zip
use_existing_version_if_available: true
์ฐ์ develop ๋ธ๋์น์ merge ๋์ ๋์ main ๋ธ๋์น์ merge ๋์ ๋ ํธ๋ฆฌ๊ฑฐ ๋๋๋ก ๊ตฌํํ๋ค. ์ค์ ์ฝ๋๊ฐ ๋ค๋ฅธ ๊ฒ ๊ฑฐ์ ์๋ค. ์ด๋ฒคํธ๊ฐ ๋ฐ๋๋๋ ์กฐ๊ฑด๊ณผ AWS EB์ ์๋ฒ ์ด๋ฆ์ ๋๋ค. ์ค๋ณต์ ์์จ ์ ์์ ๊ฒ ๊ฐ์๋ฐ ์ฐ์ !! 5์์๋ ์ด๋ ๊ฒ ํ๋ค. 6์์ ๊ฐ์ ํด ๋ณด์!
3. ๋๋ฃ์ ์ ์ ์ด๋ป๊ฒ ํ๋ณดํ ์ ์์๊น?โ
5์ 21์ผ, ์ ์ฒด ๋ฏธํ ์์ ๊ณ ๊ฐ๋ค์ด ๋ง์์ ธ, ๊ณ ๊ฐ ์๋์ ์๋์ง๊ฐ ๋๋ฌด ๋ง์ด ๋ค์ด ์ก์ฒด์ ์ผ๋ก ๋ฒํฐ๊ธฐ๊ฐ ์ด๋ ต๋ค๋ ์ด์ผ๊ธฐ๊ฐ ๋์๋ค. ๊ทธ ์ด์ ๋ ์ฐ๋ฆฌ์ ๊ณ ๊ฐ๋ค์ ๋๋ถ๋ถ ํด์ธ์ ์๊ณ ์์ค์ด ์์ ๊ฒฝ์ฐ ํ ๋ ๊ทธ๋จ์ผ๋ก ๋ฌธ์๊ฐ ๋ค์ด์ค๋ ๊ตฌ์กฐ์ธ๋ฐ... ๊ทธ ์๊ฐ๋๊ฐ ์ ํด์ ธ ์์ง ์์ ๊ฒ์ ๋ฌธ์ ๋ผ๋ ๊ฒ. ๋ฐค๋ฎ์์ด ๋์ํด์ผ ํ๊ธฐ์ ์๋ฏผํด์ง๊ณ ์ ์ ์ค์น๊ฒ ๋๋ค๊ณ ํ๋ค. ์คํํธ์ ์ผ๋ก์จ ์ฐ๋ฆฌ์ ๊ฒฝ์๋ ฅ์ ๋น ๋ฅธ ์๋์ธ๋ฐ ๊ทธ๋ผ ์ด๋ป๊ฒ ํ ๊ฒ์ธ๊ฐ?
๊ฐ๋ฐ๋ก ๋์์ค ์ ์๋ ๊ฒ ์์๊น?
๊ทธ๋์ ๋์จ ์ด์ผ๊ธฐ๊ฐ ์๊ฐ ๋ฐ ์ฃผ๊ฐ ์์ต๊ณผ ์์ค์ ์๋ ค์ฃผ๊ณ ์ด์ ์ ์ ๋ณด๋ค์ ์ ์ ์ ์ผ๋ก ์ฃผ๋ ๊ฒ ์ด๋ค์ง ํ์ ์๊ฐ์ด ์ด์ผ๊ธฐ๊ฐ ๋์๋ค.
๊ทธ๋ ๋ ์ ์ ๊ณ ๊ฐ๋ค์ด ๋ง์์ง๋๋ฐ ์ผ์ผ์ด ์๋์ผ๋ก ๊ฐ๋ฅํ ๊น๋ผ๋ ์๋ฌธ์ด ๋ค์๋ค. ๋ฆฌํฌํธ ์์๋ ๊ฐ ๊ณ ๊ฐ์ ์์ต๋ฅ ๊ณผ ๊ด๋ จ๋ ์ ๋ณด๋ค์ด ๋ง์ด ์๋ค. ๊ทธ๋์ ํด๋น ๊ธฐ๋ฅ์ ์ ์ํ๋ค.

๋๋ฃ๊ฐ ๋๋ฌด ์ข์ํ๋ค.
๊ทธ๋ ๋ ์๊ฐ์ ์ด๋ฐ ๊ฒ ์ข์ ๊ฑฐ ๊ตฌ๋ ์ถ์๋ค. ์ด๋ฐ ๊ฒ ์ฌ๋ฐ๋ค. ๋์์ค ์ ์๋ ๊ฒ ์๋ค๋ ๊ฒ!!
๊ทธ๋ผ ์ด์ ์ด๋ป๊ฒ ํ ๊ฒ์ธ๊ฐ๋ฅผ ๊ณ ๋ฏผํด๋ด์ผ ํ๋ค. ์ผ์ ๋ฒ์๋๋ฐ 6์์ ์ฌ๋ฐ์ ๊ฒ ๊ฐ๊ณ ๋ง๋ ๋ฌธ์ ๊ฐ ์ฐ์ ํ๋ค. ๊ฐ๋ณด์!
4. OOP์ ๋์ ๋จ๋ค.โ
4์๋ถํฐ ์ ์ง๋ณด์ํ๊ธฐ ์ข์ ์ํํธ์จ์ด๋ฅผ ๋ง๋ค๊ธฐ ์ํด ํ์ตํ๋ฉด์ ์๋ ด๋ ๊ณณ์ OOP์๋ค. OOP์ ๊ด๋ จ๋ ๋ถ๋ถ์ ๋ฐ๋ก ํฌ์คํ ์ ์์ฑํด๋ณด๋ ค๊ณ ํ๋ค. ๋ช ์๋์ ๊ฐ์๊ฐ ์ธํ๋ฐ์ ๋์์ ํด๋ฆฐ ์ฝ๋์ฐ๋ผ๋ ๊ฐ์๋ฅผ ๋จผ์ 2 ํ๋ ์ ์งํํ๊ณ ํ๋ ์ ๊ฑฐ๋ญํ ์๋ก ๊นจ๋ซ๋ ๊ฒ ๋ง์์ก๋ค. ์ ๋ง๋ก ์ฒ์๋ถํฐ 60%๊น์ง๋ ์ง์์ด ๋ฌ๊ฒ ๋๊ปด์ก๊ณ ๊ทธ ์ดํ๋ TDD์ ๊ด๋ จ๋ ๊ฒ์ด์ด์ ๋ชจ๋ ๋ด์ฉ์ ์น์ด๋จน์ง ๋ชปํ๋ค. ๋ช ๋ฒ ๋ ๋ณด๋ฉด ๋ ์ดํด๋๋ ๊ฒ๋ค์ด ๋ง์ ๊ฒ ๊ฐ๋ค.
5. Secure Coding, ์ปค๋ฆฌ์ด ๋ฐฉํฅ์ฑ ์ก๋ค.โ
์ต๊ทผ ๊ฐ์ ์์ฐ ์ ๊ณ์ ํดํน ์ฌ๊ฑด์ด ๋ง์ด ๋ฐ์ํ๋ค. ์ต๊ทผ ์ธ๋ก ์ ์๋ ค์ง ๋ ๊ฐ์ง ์ด์ธ์๋ ๋ง์ด๋ค.
-
Bybit ๊ฑฐ๋์ ํดํน (2025๋ 2์)
๋๋ฐ์ด์ ๋ณธ์ฌ๋ฅผ ๋ ๊ฐ์์์ฐ ๊ฑฐ๋์ Bybit์์ ์ฝ 15์ต ๋ฌ๋ฌ ์๋น์ ์ด๋๋ฆฌ์์ด ํ์ทจ๋์์ต๋๋ค. ๋ฏธ๊ตญ FBI๋ ์ด ์ฌ๊ฑด์ ๋ฐฐํ๋ก ๋ถํ์ ๋ผ์๋ฃจ์ค ๊ทธ๋ฃน์ ์ง๋ชฉํ์ต๋๋ค.
-
WazirX ๊ฑฐ๋์ ํดํน (2024๋ 7์)
์ธ๋ ๊ธฐ๋ฐ์ WazirX ๊ฑฐ๋์์์ ์ฝ 2์ต 3,490๋ง ๋ฌ๋ฌ ์๋น์ ๊ฐ์์์ฐ์ด ํ์ทจ๋์์ต๋๋ค. ์ด ์ฌ๊ฑด ์ญ์ ๋ผ์๋ฃจ์ค ๊ทธ๋ฃน๊ณผ ์ฐ๊ด๋์ด ์์ต๋๋ค.
๊ทธ ์ธ๋ ์๋ง์ ํดํน์ด ํฌ๊ณ ์๊ฒ ์ผ์ด๋๊ณ ์๋ค. ๋ณด๊ณ ๋ง ์์ ์๊ฐ ์๋ค. ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ก์ ์น ๋ธ๋ผ์ฐ์ ์ ๋ํด ๋๊ตฌ๋ณด๋ค ์ ์์์ผ ํ๋ค๊ณ ๋๋๋ค. ๊ทธ๋์ ๊ณต๋ถ๋ฅผ ์์ํ๋ ค๊ณ ํ๋ค. ์ด๋ค ๋ถ๋ถ์ ์ทจ์ฝ์ ์ด ์๋์ง ์์์ผ ํ๋ค. ๊ทธ๋์ ์ฐ์ ๋ธ๋ผ์ฐ์ ์ ์ด๋ค ์ทจ์ฝ์ ์ด ์๋์ง ์์์ผ ํ๊ณ ์๋ฒ ์ฌ์ด๋์์ ์ด๋ค ์ทจ์ฝ์ ์ด ์๋์ง ์์๋ณด๋ ค๊ณ ํ๋ค.
์ด ์ง์์ด ๋น์ฅ์ ๋ณด์ด๋ ์ฑ๊ณผ๋ ์์ ์ ์์ผ๋ ์น ๋ธ๋ผ์ฐ์ ๊ฐ ์ผ๋ง๋ ๋ณด์์ ๊ณ ๋ฏผํ๊ณ ์๋์ง๋ฅผ ๋ฆฌ๋ฒ์ค ์์ง๋์ด๋งํ ์ ์๋ ๊ธฐํ์ผ ๋ฟ ์๋๋ผ ๋ธ๋ผ์ฐ์ ์์ฒด์ ๋ํ ์ดํด๋๋ ์ฆ๊ฐํ ๊ฒ์ผ๋ก ์์๋๋ค.
๊ทธ๋์ 5์์๋ ์ค์ง์ ์ผ๋ก ๋ณด์๊ณผ ๊ด๋ จํ์ฌ ๋ฌด์์ ๊ฐ์ ํ๋๊ฐ?
๋น์ ผ ์์คํ ํ๋ก์ ํธ์ ์ธ์ฆ/์ธ๊ฐ ๋ถ๋ถ์ ๊ฐ์ ํ์๋ค. ๋ฆฌํ๋ ์ ํ ํฐ์ js๋ก ์ ๊ทผํ์ง ๋ชปํ๊ฒ ํ๊ณ . ์๋ฒ์์ HTTP ํค๋๋ฅผ ์ ์ดํจ์ผ๋ก ๋ณด์์ ๊ฐํํ์๋ค. ๊ทธ๋ฆฌ๊ณ 1๋ฒ์์ ๊ถํ ์์คํ ์ ๋์ ํ์๋ค.
๊ฒฐ๋ก โ
5์ ํ ๋ฌ์ ์ ์ง๋ณด์ํ๊ธฐ ์ข์ ์ฝ๋๋ฅผ ์ํด OOP, ๋จ์ ํ ์คํธ๋ฑ์ ๊ณต๋ถํ๋ฉฐ ์ค๋ฌด์์ ๋ณด์๊ณผ ๊ด๋ จํ ์์ ๋ค์ ๋ง์ด ์งํํ๋ค. ๊ทธ์ ๋ฐ๋ผ ๋ณด์์ ๊ด์ฌ์ด ๋ง์ด ๊ฐ๊ฒ ๋๊ณ ์๊ฐ๋ณด๋ค ์ดํ๋ฆฌ์ผ์ด์ , ํ์ฌ, ์ฌํ๋ฅผ ์ ํ ๊ณต๊ฒฉ์์๋ก๋ถํฐ ๋ณดํธํ๋๋ฐ ํฅ๋ฏธ๊ฐ ์์ ์๋ ์๋ค๊ณ ์๊ฐ์ด ๋ค์๋ค. ๋จผ์ ๋ ์น์ ๋ํด ์ ๋ฌธ๊ฐ๊ฐ ๋์ด์ผ๊ฒ ๋ค๊ณ ์๊ฐํ๋ฉฐ 5์์ ๋ง๋ฌด๋ฆฌํ๋ค. ๊ทธ๋์ 6์์๋ ์ ๋ฌด ์ด์ธ์ ํ๋ก ํธ์๋ ๋ถ์ผ์์ ์ผ์ด๋ฌ๋, ๊ทธ๋ฆฌ๊ณ ์ผ์ด๋ ์ ์๋ ํดํน์ ๋ํด ๊ธฐ๋ณธ ์๋ฃ๋ค๋ณด๋ค ํจ์ฌ ์์ธํ๊ณ ์ค์ง์ ์ผ๋ก ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค. ๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค. ๋ชจ๋ ๊ฐ์ ์๋ ์๋ฆฌ์์ ํ์ดํ !
6์ Action Pointโ
๊ฐ๋ฐ ๊ด๋ จโ
- ์๊ฐ ๋ฆฌํฌํธ ์์ฑ ๋ฐ ์ ์ฅ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ (๋๋ฃ์ ์ ์ ํ๋ณดํด๋ณด์!! ๊ฐ๋ณด์)
- ํจ๋ฉ ๋์์ธ ์์คํ ์ ๋ฌด ํ๋ก์ธ์ค ์ ์ฐฉ
- ๊ฐ์ฒด ์งํฅ์ ์คํด์ ์ง์ค ์ฝ๊ณ ์๊ฐ ์ ๋ฆฌ
- ๋จ์ ํ ์คํธ ๊ธฐ์ ์ฑ ์ฝ๊ณ ์๊ฐ ์ ๋ฆฌ -> ํ๋ก ํธ์๋ ๊ฐ์ฅ ๋ง๋งํ ๋ก์ง๋ถํฐ ๋จ์ ํ ์คํธ ์ ์ฉํ์!
- Next.js ์คํฐ๋๋ฅผ ํตํด Insight ์ ๋ฆฌํด์ ๊ณต์ ํ๊ธฐ (์๊ฐ์์ด ์คํฐ๋ ๊ธ์ง)
- ๋คํธ์ํฌ ๊ณต๋ถ๋ฅผ ํตํด ์ฌ๊ณ ๋ฅผ ํ์ฅ์์ผ๋ณด์
- DB ๊ณต๋ถํ์. postgresql
๋ณด์ ๊ด๋ จโ
- XSS์ ๋ํด ํ์ต ๋ฐ ๊ฒฐ๊ณผ๋ฌผ ์ ๋ฆฌ
- CSRF์ ๋ํด ํ์ต ๋ฐ ๊ฒฐ๊ณผ๋ฌผ ์ ๋ฆฌ
- OAuth์ ๋ํด ๊น์ด ํ์ต ๋ฐ ๊ฒฐ๊ณผ๋ฌผ ์ ๋ฆฌ