들어가며
이번 글에서는 프론트엔드 개발자가 실무에서 접할 수 있는 권한 관리 방법론인 RBAC, ABAC, 그리고 최신 트렌드인 ReBAC을 설명하고, 각각의 장단점과 구현 예시를 통해 권한 관리에 대해 알아보도록 하자.
대상독자는 다음과 같음
- 권한 관리 방법론에 대해 고민하고 있는 프론트엔드 개발자
- 복잡한 사용자 권한 관리 체계를 구현하고자 하는 중급 이상의 개발자
- 면접을 앞둔 개발자
권한관리 왜 필요한가?
현재 나는 회사에서 제공하는 서비스와 API들에 대한 Saas 서비스를 만들고 있다. 현재 서비스들은 B2B로도 계약이 된 것도 있고 혹은 개인이 사용하는 B2C도 포함하고 있다. 따라서 이를 웹서비스로 제공하는 니즈에 대해서 응답하기 위해 서비스를 만들고 있다.
그래서 로그인과 권한 관리에 대한 체계적인 학습과 PoC가 필요했고 이 문서는 바로 그 고생길의 가이드문서이다.
우선 권한 관리에 대해 알아보기 전에 "인증"과 "인가"에 대해 알아야한다.
인증 (Authentication)
인증을 한마디로 설명하자면 "이 사용자가 누구인가"를 확인하는 과정이다. 즉 이 사용자가 시스템에 로그인할 수 있는 정당한 자격을 갖추었는지 확인하는 절차이며, 기본적으로 시스템에 접근할 때 가장 먼저 이루어지는 단계이다.
일반적으로 다음과 같은 방법으로 수행된다.
- 아이디와 비밀번호 : 사용자가 등록한 정보를 기반으로 신원을 확인
- OAuth : Google, Github, Naver 등 제3자 서비스를 통해 확인, 일반적으로 소셜로그인이라고도 한다.
- 다중 인증(Multi-Factor Auth) : 비밀번호 외에 추가 인증 요소를 통해 다중으로 확인, 대표적으로 OTP가 있다.
그리고 다음과 같은 방법으로 프론트엔드에서 구현한다.
- 로그인 폼을 제공하고 입력값을 검증
- OAuth 제공업체가 지원하는 라이브러리, 혹은 API 맞춰 구현
인가 (Authorization)
인가는 "이 사용자가 무엇을 할 수 있는가"를 판단하는 과정이다. 인증된 사용자에게 허용된 리소스와 권한을 제한하는 것을 말한다.
인가는 인증된 사용자가 어떤 리소스나 기능에 접근할 수 있는지 결정하는 과정이다. 인가가 바로 권한 관리의 핵심이며, 오늘 우리가 글을 쓰는 주제이기도 하다.
일반적으로 다음과 같은 방법으로 수행된다.
- 일반 유저는 관리자 페이지에 접근할 수 없음
- 공유받지 않은 프로젝트에는 접근할 수 없음
- 글을 작성한 유저는 관리자가 아님에도 해당 글을 삭제할 수 있음
그리고 다음과 같은 방법론을 가지고 있다.
- RBAC(Role-Based Access Control): 사용자의 역할에 따라 접근 가능한 리소스를 결정.
- ABAC(Attribute-Based Access Control): 사용자의 역할뿐 아니라 사용자 및 리소스의 속성에 따라 세부적인 접근 권한을 제어.
- ReBAC(Relationship-Based Access Control): 사용자와 리소스 간의 관계에 따라 접근 권한을 제어.
그리고 이러한 것들은 프론트엔드에서 아래와 같이 구현된다.
- 사용자 역할(Role) 또는 권한(Permission)에 따른 UI 제어
- 네비게이션 메뉴, 버튼, 페이지 접근 제어
- 잘못된 접근 시 403 Forbidden 페이지로 리디렉션
- 버로부터 받은 권한 정보를 클라이언트 상태로 관리
전통적인 방식에서는 인증과 인가는 백엔드에서만 처리했다. 프론트는 백엔드에서 인증과 인가를 수행하여 응답한 값을 그대로 화면에 보여주기만 하면 퇴근할 수 있었다. 하지만 현대 프론트엔드에서는 화면에 보여주는 것을 넘어, 사용자의 행동 흐름과 접근 제어를 적극적으로 판단하고 제어하기를 요구하고 있다. 그래서 슬프게도 우리도 인증, 인가를 공부해야한다. 그러니 이중에 오늘은 인가에 대해 알아보자.
권한 관리 방법론
다양한 권한 관리 방법론 중에 오늘 우리가 살펴볼 것은 전통적인 RBAC, ABAC 그리고 최신 트렌드인 ReBAC이다. 하지만 먼저 말해보자면 내가 회사 프로젝트에 적용한 방법은 RBAC + ABAC의 하이브리드버전이고, ReBAC를 적용하진 않았다. 왜 그런지 하나씩 살펴보면서 가보자.
RBAC
RBAC은 Role-Based Access Control으로, 사용자에게 역할(role)을 할당하고, 그 역할에 따라 접근 권한을 제어하는 방식이다. 가장 널리 쓰이는 방식이며, 거의 표준처럼 자리잡고 있다.
그리고 대다수의 프로젝트는 RBAC 수준으로 처리가 가능한 것도 사실이다.
RBAC은 하나의 코드단락으로 이해가 가능하다. 예시를 보자.
type User = {
role: 'admin' | 'editor' | 'viewer';
};
const user: User = { role: 'editor' };
function PostEditorPanel() {
const canEdit = user.role === 'admin' || user.role === 'editor';
if (!canEdit) {
return <p>Access Denied</p>;
}
return <div>게시글 편집 패널</div>;
}
User는 Role를 가지고 있고, 특정 리소스나 액션에 대한 권한을 얻는다.
그리고 이렇게 간단하게 이해되는 만큼, RBAC의 장점은 직관적이고 간단하다는 점이다.
하지만 간단하다는 점은 곧 복잡한 제어가 어렵다는 점이있다.
하나의 예시를 살펴보자
- "A"라는 User의 Role은 "viewer"인데 본인이 작성한 Post는 edit할수 있어야 한다라는 정책이 추가되었다.
이렇게 된다면 글을 작성한 유저는 "editor"가 되어야한다. 그렇다면 "viewer"가 필요가 있을까?
그래서 나온것이 바로 다음 ABAC이다
ABAC
ABAC은 Atrribute-Based Access Control로, 사용자, 리소스, 환경에 대한 속성(attribute) 을 기준으로 세밀하고 동적인 접근 제어를 수행하는 방식이다.
여기서부터는 약간 복잡하니 구성요소부터 살펴보도록 하자.
- 속성(Attribute): 사용자 속성, 리소스 속성, 요청 시간/위치 등
- 정책(Policy): 속성 기반 조건 정의 (예: 본인이 작성한 "Post"는 "edit"할 수 있다)
RBAC과 달리 ABAC은 역할(Role)은 "사용자 속성의 일부"로 보고 다른 속성들과 조합해 조건문으로 정책을 평가하는 것이 핵심이다. 그리고 속성과 정책은 프론트엔드 관점에서 정책 평가 함수(Policy Evaluator)로 구현된다.
그럼 기존의 'admin' | 'editor' | 'viewer' 의 역할 정보를 포함하여 정책을 정의해보자.
- Admin은 모든 것이 가능하다
- Editor, Viewer는 자신이 작성한 게시글을 수정할 수 있다.
- Editor는 타인이 작성한 게시글을 수정할 수 있다.
type User = {
id: string;
role: "admin" | "editor" | "viewer";
};
type Post = {
id: string;
authorId: string;
};
그리고 정책 평가 함수는 다음처럼 구현할 수 있다.
function canEditPost(user: User, post: Post): boolean {
if (user.role === "admin") return true;
if (user.role === "editor") return true;
return user.id === post.authorId;
}
여기서 혹자는 그럴수 있다. "RBAC의 canEdit 함수나 ABAC의 canEditPost 함수가 비슷한데요?"
// RBAC
const canEdit = user.role === 'admin' || user.role === 'editor';
그래보일수 있다. 하지만 이건 ABAC의 간단한 구현만 한 것이다. 실무의 가혹하고 쓸데없는 조건을 추가해보자.
복잡한 정책 추가
최신 트렌드 쓰레드의 조건에, 특이한 정책을 추가해보자.
- 게시글 수정은 평일에만 할 수 있다.
- 게시글 수정은 글 작성 1시간 후부터 가능하다.
이 기능을 구현하기 위해선 우선 타입부터 확장되어야한다.
type User = {
id: string;
role: "admin" | "editor" | "viewer";
};
type Post = {
id: string;
authorId: string;
createdAt: Date; // 추가된 타입(ABAC 기준으로 속성)
};
그리고 정책 평가 함수도 리펙토링해보자.
function canEditPost(user: User, post: Post, now = new Date()): boolean {
// 1. 관리자: 항상 가능
if (user.role === "admin") return true;
// 2. 에디터: 항상 가능
if (user.role === "editor") return true;
// 3. Viewer: 자신의 글만, 그리고 조건 만족 시
const isAuthor = user.id === post.authorId;
if (!isAuthor) return false;
// 4. 평일인지 체크 (0:일 ~ 6:토)
const day = now.getDay();
const isWeekday = day >= 1 && day <= 5;
if (!isWeekday) return false;
// 5. 글 작성 1시간 후인지 체크
const elapsedMs = now.getTime() - post.createdAt.getTime();
const oneHourMs = 60 * 60 * 1000;
const isAfterOneHour = elapsedMs >= oneHourMs;
if (!isAfterOneHour) return false;
return true;
}
사용 예시는 다음처럼 나오게 된다.
const currentUser: User = {
id: "user123",
role: "viewer",
};
const currentPost: Post = {
id: "post456",
authorId: "user123",
createdAt: new Date(Date.now() - 30 * 60 * 1000), // 작성한 지 30분
};
function PostEditorPanel() {
const canEdit = canEditPost(currentUser, currentPost);
if (!canEdit) {
return <p>수정 권한이 없습니다</p>;
}
return <div>게시글 편집 패널</div>;
}
그리고 생각해보자. 과연 "canEditPost"를 RBAC으로 구현하려면 룰이 몇개가 필요할 것인가?
이처럼 시간, 사용자 속성, 리소스 속성, 컨텍스트(요일) 등을 조합한 정책은 졍형화된 RBAC으로는 거의 불가능하다. 여기서 살펴본 ABAC + 정책 평가 함수 기반 구조를 통해서만 구현 가능하다고도 볼 수 있다.
DSL (Domain-Specific Language)
뜬금없는 용어가 등장했다. DSL은 특정 도메인 문제를 더 자연스럽고 선언적으로 표현할 수 있도록 만들어진 전용 언어 혹은 문법적 인터페이스이다. ( 최근에는 선언적이란 단어가 여기저기서 등장하는 경향이 있다)
여기저기서 설명이 복잡하지만 간단히 말하자면 정책 평가 함수를 가독성있게 수정한 것이라고 보면 된다. ( 이러한 해석은 나의 의견이다. 반박은 거절한다 )
기존의 "canEditPost"를 DSL에 맞춰 구현하면 다음처럼 나온다.
function can(user: User) {
return {
edit(post: Post) {
if (user.role === "admin") return true;
if (user.role === "editor") return true;
const isAuthor = user.id === post.authorId;
const isWeekday = [1, 2, 3, 4, 5].includes(new Date().getDay());
const isAfterOneHour = Date.now() - post.createdAt.getTime() >= 3600000;
return isAuthor && isWeekday && isAfterOneHour;
},
delete(post: Post) {
if (user.role === "admin") return true;
return user.id === post.authorId;
},
};
}
그리고 이러한 구현체는 다음처럼 사용된다.
if (can(user).edit(post)) {
// 수정 가능
}
흡사 하나의 문장처럼 자연스럽게 읽힌다.
이처럼 DSL 형태로 권한 평가 로직을 구성하면, 복잡한 조건을 하나의 함수에 몰아넣는 방식에서 벗어나, 정책을 비즈니스 행위 중심의 구조로 표현할 수 있어 유지보수성과 가독성이 크게 향상된다.
실제로 can(user).edit(post)와 같은 표현은 실제 도메인에서 사용되는 문장 구조와 유사하기 때문에, 개발자뿐 아니라 기획자나 보안 담당자와의 커뮤니케이션에도 이점이 있다. 그냥 코드 던지면 쉽게 이해하게 된다.
그리고 이를 고도화하면 다음과 같은 최종형태까지 갈 수 있다.
type PolicyContext = {
user: User;
resource: any;
now?: Date;
};
type PolicyEvaluator = (ctx: PolicyContext) => boolean;
const policies = {
post: {
/**
* 정책: post:edit
*
* - Admin: 무조건 편집 가능
* - Editor: 본인 또는 타인 게시글 상관없이 편집 가능
* - Viewer: 다음 조건을 모두 만족해야 편집 가능
* - 자신이 작성한 글이어야 함
* - 평일(월~금)이어야 함
* - 작성된 지 1시간 이상 경과해야 함
*/
edit: ({ user, resource: post, now = new Date() }: PolicyContext<Post>) => {
if (user.role === "admin") return true;
if (user.role === "editor") return true;
const isAuthor = user.id === post.authorId;
const isWeekday = [1, 2, 3, 4, 5].includes(now.getDay());
const isAfterOneHour =
now.getTime() - post.createdAt.getTime() >= 60 * 60 * 1000;
return isAuthor && isWeekday && isAfterOneHour;
},
/**
* 정책: post:delete
*
* - Admin: 무조건 삭제 가능
* - Editor, Viewer: 자신이 작성한 게시글만 삭제 가능
*/
delete: ({ user, resource: post }: PolicyContext<Post>) => {
if (user.role === "admin") return true;
return user.id === post.authorId;
},
},
};
주석으로 정책까지 친절하게 달아두면 금상첨화일 것이다.
그리고 DSL Wrapper를 만들어 한번 감싸면 된다.
function can(user: User) {
return {
edit(post: Post) {
return policies.post.edit({ user, resource: post });
},
delete(post: Post) {
return policies.post.delete({ user, resource: post });
},
};
}
// 사용 예시
if (can(user).edit(post)) {
// 사용자에게 수정 권한이 있음
}
왜 "post:edit" 형식을 쓰는가?
여기까지 읽어온 개발자라면 한가지 눈여겨볼 요소가 있다. 우리가 평소에 잘 쓰지 않는 키 값의 형태가 보인다.
왜 "postEdit"이 아닌 "post:edit" 형식인가?
그것은 바로 ":" 구분자를 사용하면 구조적 명확성, 표현력, 확장성 측면에서 이점이 있기 때문이다. "postEdit"이라고 써도 된다. 하지만 실무에선 "post:edit"를 권장한다고 한다.
솔직하게 말하자면 엄청나게 복잡한 권한 관리를 해보지 못한 나는 초반에 아무런 학습 없이 "postEdit"으로 구현했고 실제로 아무런 문제가 없었다. 하지만 권장되는 패턴은 그 이유가 있는 법이니 그냥 쓰자
권한 관리 뿐만 아니라 Redis key, IAM 정책(s3:GetObject), GraphQL schema 등에서도 사용되는 타입이기도 하다.
"post:edit" // 리소스 타입:행위
"billing:view" // 모듈:권한
"feature:enable"
ReBAC
솔직하게 말하겠다. 이건 써본적 없다. 그래서 이론적으로만 설명하겠다.
Relationship-Based Access Control로, 사용자와 리소스 간의 관계(Relationship)를 기반으로 권한을 판단하는 방식이다. 기존의 역할(RBAC)이나 속성(ABAC)이 아닌, "이 사용자가 이 리소스와 어떤 관계를 맺고 있는가?"를 기준으로 인가를 결정한다.
ReBAC의 핵심요소는 다음과 같다.
- 주체 (Subject)
- 보통 사용자 (예: Alice)
- 객체 (Object)
- 리소스 (예: 프로젝트, 게시글, 채널 등)
- 관계 (Relationship)•
- owner, member, manager, invited, viewer, parent, follower 등
- 관계는 그래프 형태로 모델링 가능 (그래프 DB 또는 관계 그래프 엔진 사용)
- 정책 (Policy)
- 특정 관계가 있을 때, 어떤 권한이 부여되는지 정의
뭔가 기존의 체계보다 많다. 복잡할때는 단순화한 예시를 통해 구체화해보자.
ReBAC으로 바라본 Google Drive
우선 들어가기에 앞서 찐으로 구글 드라이브가 ReBAC의 인증을 사용하는가에 대한 답은 "Yes"다.
내가 이전에 읽은 "구글 개발자는 이렇게 일한다"에 따르면 구글은 자체 권한 관리 시스템인"Zanzibar"를 사용한다. 이번에 학습하면서 알아보니 Zanzibar가 ReBAC 모델의 대표적인 구현체라고 한다.
Zanzibar: Google’s Consistent, Global Authorization System
Security, Privacy and Abuse Prevention
research.google
시간나면 읽어보시라.
이제 다시 구글 드라이브로 돌아가서 구글 드라이브는 이렇게 동작한다.
- 사용자가 어떤 파일이나 폴더와 어떤 관계를 맺고 있느냐에 따라, 접근 권한이 달라진다.
이것이 바로 ReBAC (관계 기반 접근 제어)의 핵심 아이디어다.
시나리오를 살펴보자. 우리에게 매우 익숙한 시나리오다.
- Alice는 문서 A의 소유자(owner)입니다.
- Bob은 문서 A의 편집자(editor)로 초대받았습니다.
- Carol은 문서 A를 보기만(viewer) 할 수 있습니다.
- Dave는 아무 관계도 없습니다.
이 시나리오는 관계 모델링해보면
[
{ subject: "user:alice", relation: "owner", object: "file:A" },
{ subject: "user:bob", relation: "editor", object: "file:A" },
{ subject: "user:carol", relation: "viewer", object: "file:A" }
]
이 모델링을 바탕으로 프론트엔드에서는 이렇게 사용할 수 있다.
const currentUser = "user:bob";
const fileId = "file:A";
if (hasPermission(currentUser, "edit", fileId)) {
return <EditToolbar />;
} else {
return <p>수정 권한이 없습니다</p>;
}
function hasPermission(userId: string, action: string, fileId: string) {
const relation = getUserRelation(userId, fileId); // 관계 DB or 캐시에서 조회
const permissionsByRelation = {
owner: ["read", "edit", "delete"],
editor: ["read", "edit"],
viewer: ["read"],
};
return permissionsByRelation[relation]?.includes(action) ?? false;
}
이 코드를 프론트엔드 개발자의 입장에서 다시 살펴보자.
관계(Relation) | owner, editor 등 | 백엔드가 리턴해주는 사용자-리소스 간 연결 상태 |
권한(Permission) | read, edit 등 | 버튼 활성화 여부, 라우팅 접근 제어 등에 사용 |
리소스(Resource) | file:A, folder:X | 문서, 폴더 등 구체적 객체 |
프론트엔드 개발자 입장에서는 권한을 직접적으로 판단하지 않고 관계를 기반으로 권한을 추론하여 관리하면 되는 것이다.
더 직관적으로 풀어쓰자면 다음과 같다.
- 프론트에서는 "특정 사용자가 특정 리소스와 어떤 관계인지"만 알면,
- 그 관계가 어떤 권한을 의미하는지는 서버를 통해 받아오고,
- 프론트는 그 결과만 받아서 UI를 제어하면 된다
ReBAC과 기존방식
코드로 비교해보자. 이게 더 이해하기가 수월하다.
// Bob이 파일의 editor인지, owner인지 직접 로직으로 판단해야 함
if (user.id === file.ownerId || file.editors.includes(user.id)) {
showEditButton();
}
이런 코드는 필연적으로 코드가 복잡하지고, 리소스마다 조건이 다르고, 정책이 바뀔때마다 프론트도 업데이트 되어야한다.
근데 ReBAC은 요런 귀찮음에서 해소된다. 어찌보면 백엔드로 복잡함을 던지는 구조라고도 볼 수 있다.
// 백엔드로부터 "edit 권한 있음/없음" 결과만 받음
const canEdit = await checkPermission(user.id, "edit", file.id);
if (canEdit) {
showEditButton();
}
코드를 해석해보자면 "나는 권한 있는지 없는지 모르겠고, canEdit 값만 가져와"이다.
프론트는 단지 "어떤 사용자", "어떤 리소스", "어떤 액션"을 가지고 서버에 콜하기만 하면 되는 것이다. 멋지게 표현해보자면
“관계로부터 도출된 권한”만 받아서 UI를 렌더링하면 되는 구조
근데 또 이렇게 생각할 수 있다.
- ABAC인 경우도 속성에 따른 권한 여부를 서버에서 받아오면 같은 것 아닌가?
아... 그렇다. 코드 구조 자체만 보면 ABAC이든 ReBAC이든 구분되지 않는다.
그래서 핸디는 뭘 썼나요?
당황스럽게도 ABAC이든 ReBAC이든 구분되지 않는다는 결론으로 들어오면서 나는 선택의 기로에 서게된다. 바로 어떤 권한 체계로 할 것인가?
나는 ABAC를 사용했다.
왜 ABAC를 사용했냐고 물어본다면 다음과 같은 이유가 있다.
- ABAC은 프론트 단독 구현이 가능하다
- ABAC은 실용적이고 구현이 쉽다.
이제 이 조건을 조금더 설명해보겠다.
ABAC은 프론트 단독 구현이 가능하다.
우선 회사의 컨텍스트부터 파악하자면 회사에서는 keycloak 기반의 인증시스템을 사용한다. 그리고 keycloak은 인증과 함께 인가에 대한 정보로 사용하고 있었으며 기존 인가 방법론은 "RBAC"이었다.
안그래도 바쁜 백엔드 형님한테 ReBAC할건데 관계테이블 함께 고민해보쉴? 하면 100이면 120로 혼난다. 물론 그 타당성을 입증하는 것은 필요하겠지만 내가 판단하기엔 우리 프로젝트는 ReBAC처럼 관계가 있는라는 물음에서 "없다"라고 결론지었다.
ABAC은 실용적이고 구현이 쉽다
기존 시스템은 이미 role 기반 인증 구조를 갖추고 있으며, 그 role은 토큰에 포함되어 프론트에서 쉽게 접근 가능하다. 이 정보를 기반으로 role === 'admin' 같은 판단을 하는 것은 사실상 ABAC 조건의 일부다.
즉, 나는 기존의 RBAC을 ABAC 관점에서 재해석하여 프론트에서 자체적으로 평가 가능한 조건문으로 변환했다.
// 타입 정의
type User = {
id: string;
role: "admin" | "manager" | "member";
teamId: string;
};
type Resource = {
id: string;
ownerId: string;
teamId: string;
};
// ABAC 조건 기반 정책 평가 함수
function canEditResource(user: User, resource: Resource): boolean {
// admin은 모든 리소스 수정 가능
if (user.role === "admin") return true;
// manager는 같은 팀의 리소스 수정 가능
if (user.role === "manager" && user.teamId === resource.teamId) return true;
// member는 본인이 소유한 리소스만 수정 가능
return user.role === "member" && user.id === resource.ownerId;
}
실제 사용 예시를 간략화하여 들고와봤다. 현재에는 정책 평가 함수를 can(user).edit(resource)처럼 DSL화를 해야할 것인가에 대한 고민하고 있다.
마무리
ABAC은 우리 조직과 프로젝트의 현실에 가장 잘 맞는 실용적인 선택이었다. 최신의 트렌드에 맞춰 ReBAC으로 가는 것도 좋은 일이지만 거의 대부분 "구관이 명관이다"라는 것은 들어맞는다.
"도메인의 복잡도는 곧 권한 모델의 복잡도다" 라는 말이 있다. 이 말로 이루어보건데, 지금 우리의 상황은 ABAC이면 충분하다. 현제 우리의 상황은 사용자와 리소스 간의 복잡한 관계 그래프도 없고, 상속되는 권한 체계도 없다.
하지만 ABAC를 선택했다고 ReBAC이나 RBAC으로의 확장이나 변화를 막는것은 아니다. 시스템이 더 커지고 관계가 더욱 정교해진다면 ABAC 기반의 구조 위에 ReBAC적 요소를 덧붙이거나 필요에 따라 ReBAC처럼 서버로 권한을 요청할 수도 있다.
실제로도 프론트에서의 API 요청은 백엔드형님의 자체적인 권한 체크로 2중방어를 한다. 프론트는 그저 백엔드로 들어갈 불필요한 API를 방지하거나 유저에게 "너 권한없으니 버튼도 누르지못함"이라고 버튼을 disabled할뿐이다.
추가
당근에서 ReBAC의 구현체인 graplix(https://github.com/daangn/graplix)를 오픈소스로 공개했다. 당근형님들은 stackframe부터 이런거 참 멋져(원지혁님이 이런 문화 주도한다고 들었는데 멋지신듯)
참고 자료
- https://medium.com/daangn/%EA%B5%AC%EA%B8%80%EC%B2%98%EB%9F%BC-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EA%B6%8C%ED%95%9C-%EC%89%BD%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-feat-graphql-9ce80d34d39b
- https://www.youtube.com/watch?v=5GG-VUvruzE&ab_channel=WebDevSimplified
끝.
'개발 > 개발지식' 카테고리의 다른 글
Web Worker에서 SharedArrayBuffer는 정말 성능을 개선할까? (0) | 2025.05.04 |
---|---|
[채용] 5년차 개발자의 신입 개발자 면접 회고 (32) | 2024.03.13 |
[개발잡담] 디아블로4와 함께 알아보는 스파게티 코드 (0) | 2023.07.27 |
[유튜브] url로 영상의 정보를 가져오는 기능 만들기 (0) | 2023.07.25 |
[포트폴리오] AI로 만드는 연차별 포트폴리오 (ChatGPT) (1) | 2023.01.27 |
댓글