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

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

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

์„œ๋ก โ€‹

์ง€๋‚œ๋‹ฌ, ์œ ์ง€๋ณด์ˆ˜ ํ•˜๊ธฐ ์‰ฌ์šด ํ”„๋ก ํŠธ์—”๋“œ ์„ค๊ณ„๋ฅผ ๊ณ ๋ฏผํ•ด ๋ณด๊ณ  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, ์ปค๋ฆฌ์–ด ๋ฐฉํ–ฅ์„ฑ ์žก๋‹ค.โ€‹

์ตœ๊ทผ ๊ฐ€์ƒ ์ž์‚ฐ ์—…๊ณ„์— ํ•ดํ‚น ์‚ฌ๊ฑด์ด ๋งŽ์ด ๋ฐœ์ƒํ–ˆ๋‹ค. ์ตœ๊ทผ ์–ธ๋ก ์— ์•Œ๋ ค์ง„ ๋‘ ๊ฐ€์ง€ ์ด์™ธ์—๋„ ๋ง์ด๋‹ค.

  1. Bybit ๊ฑฐ๋ž˜์†Œ ํ•ดํ‚น (2025๋…„ 2์›”)

    ๋‘๋ฐ”์ด์— ๋ณธ์‚ฌ๋ฅผ ๋‘” ๊ฐ€์ƒ์ž์‚ฐ ๊ฑฐ๋ž˜์†Œ Bybit์—์„œ ์•ฝ 15์–ต ๋‹ฌ๋Ÿฌ ์ƒ๋‹น์˜ ์ด๋”๋ฆฌ์›€์ด ํƒˆ์ทจ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฏธ๊ตญ FBI๋Š” ์ด ์‚ฌ๊ฑด์˜ ๋ฐฐํ›„๋กœ ๋ถํ•œ์˜ ๋ผ์ž๋ฃจ์Šค ๊ทธ๋ฃน์„ ์ง€๋ชฉํ–ˆ์Šต๋‹ˆ๋‹ค.

  2. 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์— ๋Œ€ํ•ด ๊นŠ์ด ํ•™์Šต ๋ฐ ๊ฒฐ๊ณผ๋ฌผ ์ •๋ฆฌ