From f174b70c57dd546ca0e871ef7e4169ab0c6a045e Mon Sep 17 00:00:00 2001 From: Minsu <52266597+Gaic4o@users.noreply.github.com> Date: Wed, 30 Oct 2024 07:06:48 +0900 Subject: [PATCH] docs: authentication guide translated into Korean (#736) --- .../current/guides/examples/auth.md | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 i18n/kr/docusaurus-plugin-content-docs/current/guides/examples/auth.md diff --git a/i18n/kr/docusaurus-plugin-content-docs/current/guides/examples/auth.md b/i18n/kr/docusaurus-plugin-content-docs/current/guides/examples/auth.md new file mode 100644 index 000000000..60fe0d201 --- /dev/null +++ b/i18n/kr/docusaurus-plugin-content-docs/current/guides/examples/auth.md @@ -0,0 +1,225 @@ +--- +sidebar_position: 1 +--- + +# Authentication + +보통 인증은 세 가지 주요 단계로 이루어집니다: + +1. 사용자로부터 로그인 정보(아이디, 비밀번호 등)을 수집합니다. +2. 백엔드 서버로 해당 로그인 정보을 전송합니다. +3. 인증 후 발급받은 토큰을 저장하여 이후 요청에 사용합니다. + +## 사용자 로그인 정보 수집 방법 + +앱에서 사용자로부터 로그인 정보를 수집하는 방법을 알아보겠습니다. 만약에 OAuth를 사용하는 경우, OAuth 제공자의 로그인 페이지를 사용하여 [3단계](#how-to-store-the-token-for-authenticated-requests)로 바로 넘어갈 수 있습니다. + +### 전용 로그인 페이지 만들기 + +웹사이트에서 사용자 이름과 비밀번호를 입력하는 로그인 페이지를 제공하는 것이 일반적입니다. 이러한 페이지들은 구조가 단순하여 별도의 복잡한 분해 작업이 필요하지 않습니다. 다만, 로그인과 회원가입 양식은 외형이 비슷하기 때문에, 경우에 따라 두 양식을 하나의 페이지에서 통합하여 제공하기도 합니다. + +- 📂 pages + - 📂 login + - 📂 ui + - 📄 LoginPage.tsx (or your framework's component file format) + - 📄 RegisterPage.tsx + - 📄 index.ts + - other pages… + +로그인과 회원가입 컴포넌트를 별도로 만들고, 필요에 따라 index 파일에서 export 할 수 있습니다. 이 컴포넌트들은 사용자로부터 로그인 정보을 입력받는 폼을 포함합니다. + +### 로그인 다이얼로그 만들기 + +앱의 어디서나 사용할 수 있는 로그인 다이얼로그가 필요하다면, 이 다이얼로그를 재사용 가능한 위젯으로 만드는 것이 좋습니다. 이렇게 하면 불필요한 세분화를 피하면서도 어떤 페이지에서나 쉽게 로그인 다이얼로그를 띄울 수 있습니다. + +- 📂 widgets + - 📂 login-dialog + - 📂 ui + - 📄 LoginDialog.tsx + - 📄 index.ts + - other widgets… + +가이드 나머지 부분은 전용 페이지 방식에 대해 설명하고 있지만, 동일한 원칙을 로그인 다이얼로그에도 적용할 수 있습니다. + +### 클라이언트 측 검증 + +특히 회원가입의 경우, 사용자가 입력한 내용에 문제가 있을 때 빠르게 피드백을 제공하기 위해 클라이언트 측 검증을 수행하는 것이 좋습니다. 이를 위해 로그인 페이지의 `model` 세그먼트에서 검증 로직을 구현할 수 있습니다. 예를 들어 JS/TS에서는 [Zod][ext-zod]와 같은 스키마 검증 라이브러리를 사용할 수 있습니다: + +```ts title="pages/login/model/registration-schema.ts" +import { z } from "zod"; + +export const registrationData = z.object({ + email: z.string().email(), + password: z.string().min(6), + confirmPassword: z.string(), +}).refine((data) => data.password === data.confirmPassword, { + message: "비밀번호가 일치하지 않습니다", + path: ["confirmPassword"], +}); +``` + +그런 다음, ui 세그먼트에서 이 스키마를 사용하여 사용자 입력을 검증할 수 있습니다: + +```tsx title="pages/login/ui/RegisterPage.tsx" +import { registrationData } from "../model/registration-schema"; + +function validate(formData: FormData) { + const data = Object.fromEntries(formData.entries()); + try { + registrationData.parse(data); + } catch (error) { + // TODO: Show error message to the user + } +} + +export function RegisterPage() { + return ( +
validate(new FormData(e.target))}> + + + + + + + + +
+ ) +} +``` + +## 로그인 정보 전송 방법 + +로그인 정보를 백엔드 서버로 전송하기 위한 요청 함수를 작성하세요. 이 함수는 상태 관리 라이브러리나 뮤테이션 라이브러리(예: TanStack Query)를 사용하여 호출할 수 있습니다. + +### 요청 함수 저장 위치 + +이 요청 함수를 저장할 수 있는 위치는 크게 두 가지입니다: `shared/api` 또는 페이지의 `api` 세그먼트입니다. + +#### `shared/api`에 저장하기 + +모든 API 요청을 `shared/api`에 모아서 관리하고, 엔드포인트별로 그룹화하는 접근 방식입니다. 파일 구조는 다음과 같습니다: + +- 📂 shared + - 📂 api + - 📂 endpoints + - 📄 login.ts + - other endpoint functions… + - 📄 client.ts + - 📄 index.ts + +`📄 client.ts` 파일은 요청을 수행하는 원시 함수(예: `fetch()`)에 대한 래퍼를 포함합니다. 이 래퍼는 백엔드의 기본 URL 설정, 헤더 설정, 데이터 직렬화 등을 처리합니다. + +```ts title="shared/api/endpoints/login.ts" +import { POST } from "../client"; + +export function login({ email, password }: { email: string, password: string }) { + return POST("/login", { email, password }); +} +``` + +```ts title="shared/api/index.ts" +export { login } from "./endpoints/login"; +``` + +#### 페이지의 `api` 세그먼트에 저장하기 + +로그인 요청이 특정 페이지에만 필요한 경우, 로그인 페이지의 `api` 세그먼트에 함수를 저장할 수 있습니다: + +- 📂 pages + - 📂 login + - 📂 api + - 📄 login.ts + - 📂 ui + - 📄 LoginPage.tsx + - 📄 index.ts + - other pages… + +```ts title="pages/login/api/login.ts" +import { POST } from "shared/api"; + +export function login({ email, password }: { email: string, password: string }) { + return POST("/login", { email, password }); +} +``` + +이 함수는 페이지의 공개 API에서 내보낼 필요가 없습니다. 로그인 요청이 다른 곳에서 필요할 가능성이 낮기 때문입니다. + +### 이중 인증(2FA) + +앱이 이중 인증(2FA)을 지원하는 경우, 사용자가 일회용 비밀번호(OTP)를 입력할 수 있는 별도의 페이지로 이동해야 할 수 있습니다. 일반적으로 `POST /login` 요청은 사용자가 2FA를 활성화했음을 나타내는 플래그가 포함된 사용자 객체를 반환합니다. 이 플래그가 설정되면 사용자를 2FA 페이지로 리디렉션해야 합니다. + +2FA 페이지는 로그인과 밀접하게 연관되어 있으므로 Pages 레이어의 `login` 슬라이스에 함께 저장하는 것이 좋습니다.
+ +이중 인증을 처리하기 위해서는 `login()` 함수와 유사한 또 다른 요청 함수가 필요할 것입니다. 이러한 함수들은 `Shared`나 로그인 페이지의 `api` 세그먼트에 함께 배치할 수 있습니다. + +## 인증된 요청의 토큰 저장 방법 {#how-to-store-the-token-for-authenticated-requests} + +인증 방식이 로그인/비밀번호, OAuth, 2단계 인증 등 어떤 것이든, 결국 토큰이 발급됩니다. 이 토큰은 이후 요청에서 사용자 식별을 위해 저장되어야 합니다. + +웹 애플리케이션에서는 **쿠키**를 사용해 토큰을 저장하는 것이 가장 일반적이고 이상적인 방법입니다. 쿠키를 사용하면 토큰을 수동으로 관리할 필요가 없으며, 복잡한 처리를 줄일 수 있습니다. 만약 서버 사이드 렌더링을 지원하는 프레임워크(예: [Remix][ext-remix])를 사용 중이라면, 서버 사이드 쿠키 인프라를 `shared/api`에 저장하는 것이 좋습니다. Remix를 사용하는 예시는 튜토리얼의 [인증 섹션][tutorial-authentication]에서 확인할 수 있습니다. + +그러나 쿠키를 사용할 수 없는 상황에서는, 토큰을 직접 관리해야 합니다. 이 경우, 토큰 만료 시 갱신 로직을 함께 구현해야 할 수도 있습니다. 이 경우, 토큰 만료 시 갱신 로직을 함께 구현해야 합니다. FSD에서는 토큰을 저장할 수 있는 다양한 방법이 있습니다. + +### Shared에 저장하기 + +`shared/api`에 저장하는 접근 방식은 API 클라이언트와 잘 맞아떨어집니다. 인증이 필요한 다른 요청 함수에서 이 토큰을 쉽게 사용할 수 있기 때문입니다. API 클라이언트에서 반응형 스토어나 모듈 수준 변수를 사용해 토큰을 저장하고, `login()/logout()` 함수에서 해당 상태를 업데이트할 수 있습니다. + +토큰 자동 갱신은 API 클라이언트에서 미들웨어 형태로 구현할 수 있습니다. 모든 요청마다 실행되며, 아래와 같은 방식으로 동작합니다: + +- 사용자가 로그인하면 액세스 토큰과 갱신 토큰을 저장합니다. +- 인증이 필요한 요청을 수행합니다. +- 토큰이 만료되어 요청이 실패하면, 갱신 토큰을 사용해 새로운 토큰을 요청하고 저장한 후, 원래 요청을 다시 시도합니다. + +이 방법의 단점 중 하나는 토큰 관리 로직이 요청 로직과 같은 위치에 있어, 복잡해질 수 있다는 점입니다. 간단한 경우에는 문제가 없겠지만, 토큰 관리 로직이 복잡한 경우에는 요청과 관리 로직을 분리하는 것이 좋습니다. 요청 및 API 클라이언트는 `shared/api`에 두고, 토큰 관리 로직은 `shared/auth`에 두는 방식으로 나눌 수 있습니다. + +또 다른 단점은 백엔드가 토큰과 함께 현재 사용자 정보를 반환하는 경우, 이 정보를 별도로 저장하거나 `/me` 또는 `/users/current`와 같은 엔드포인트에서 다시 요청해야 한다는 점입니다. + +### Entities에 저장하기 + +FSD 프로젝트에서는 사용자 엔티티 또는 현재 사용자 엔티티를 사용하는 것이 일반적입니다. 두 엔티티는 같은 것을 가리킬 수도 있습니다. + +:::note + +**현재 사용자**는 "viewer" 또는 "me"라고도 합니다. 이는 권한과 개인 정보를 가진 단일 인증 사용자와 공개적으로 접근 가능한 정보로 구성된 모든 사용자 목록을 구별하기 위해 사용됩니다. + +::: + +User 엔티티에 토큰을 저장하려면 `model` 세그먼트에 반응형 스토어를 생성해야 합니다. 이 스토어는 토큰과 사용자 객체를 모두 포함할 수 있습니다. + +API 클라이언트는 일반적으로 `shared/api` 정의되거나 엔티티 전체에 분산되어 있습니다. 따라서 주요 과제는 레이어의 임포트 규칙([import rule on layers][import-rule-on-layers])을 위반하지 않으면서 다른 요청에서도 토큰을 사용할 수 있도록 하는 것입니다. + +> 레이어 규칙: 슬라이스의 모듈은 자기보다 낮은 레이어에 위치한 다른 슬라이스만 임포트할 수 있습니다. + +이 문제를 해결하기 위한 몇 가지 방법은 다음과 같습니다: + +1. **요청 시마다 토큰 수동 전달** + 이 방법은 가장 간단하지만, 번거롭고 타입 안전성이 보장되지 않으면 실수가 발생할 가능성이 큽니다. 또한 Shared의 API 클라이언트에 미들웨어 패턴을 적용하기 어렵습니다. +2. **앱 전역에서 글로벌 스토어로 토큰 관리** + 토큰을 context나 `localStorage`에 저장하고, `shared/api`에 토큰 접근 키를 보관합니다. 토큰의 반응형 저장소는 User 엔터티에서 내보내며, 필요한 경우 context Provider는 App 레이어에서 설정합니다. 이 방법은 API 클라이언트 설계를 유연하게 만들지만, 상위 레이어에 context 제공이 필요하다는 암묵적인 의존성을 발생시킵니다. 따라서 context나 `localStorage`가 제대로 설정되지 않았을 경우, 유용한 오류 메시지를 제공하는 것이 좋습니다. +3. **토큰 변경 시 API 클라이언트 업데이트** + 반응형 스토어를 활용해 엔티티의 스토어가 변경될 때마다 API 클라이언트의 토큰 스토어를 업데이트하는 구독(subscribe)을 생성할 수 있습니다. 이 방법은 상위 계층에 암묵적인 의존성을 만든다는 점에서는 이전 해결책과 비슷하지만, 이 방법은 더 "명령형(push)" 접근이고, 이전 방법은 더 "선언형(pull)" 접근입니다. + +엔티티의 `model`에 토큰을 저장하여 문제를 해결하면, 토큰 관리와 관련된 더 많은 비즈니스 로직을 추가할 수 있습니다. 예를 들어, `model` 세그먼트에 토큰 만료 시 갱신하는 로직을 추가하거나, 일정 시간이 지나면 토큰을 무효화하는 로직을 포함할 수 있습니다. +백엔드에 요청을 보내야 하는 경우에는 User 엔티티의 api 세그먼트나 `shared/api`를 사용할 수 있습니다. + +### Pages/Widgets에 저장하기 (권장하지 않음) + +애플리케이션 전역에 적용되는 상태(예: 액세스 토큰)를 페이지나 위젯에 저장하는 것은 권장되지 않습니다. 예를 들어, 로그인 페이지의 `model` 세그먼트에 토큰 스토어를 배치하는 대신, 이 아티클에서 제시한 처음 두 해결책인 Shared나 Entities를 사용하는 것이 권장됩니다. + +## 로그아웃 및 토큰 무효화 + +로그아웃 기능은 애플리케이션에서 중요한 기능이지만, 이를 위한 별도의 페이지는 없는 경우가 많습니다. 이 기능은 백엔드에 인증된 요청을 보내고, 토큰 스토어를 업데이트하는 작업으로 구성됩니다. + +모든 요청을 `shared/api`에 보관했다면, 로그인 함수 근처에 로그아웃 요청 함수를 두는 것이 좋습니다. 그렇지 않은 경우, 로그아웃 버튼이 있는 위치 근처에 로그아웃 요청 함수를 배치할 수 있습니다. 예를 들어, 모든 페이지에 나타나는 헤더 위젯에 로그아웃 링크가 있다면, 해당 요청을 그 위젯의 `api` 세그먼트에 배치하는 것이 좋습니다. + +토큰 스토어에 대한 업데이트는 로그아웃 버튼이 위치한 곳(예: 헤더 위젯)에서 트리거되어야 합니다. 이 요청과 스토어 업데이트를 해당 위젯의 `model` 세그먼트에서 결합할 수 있습니다. + +### 자동 로그아웃 + +로그아웃 요청 실패나 로그인 토큰 갱신 실패 시를 대비해 안전장치를 마련하는 것도 중요합니다. 이 두 경우 모두 토큰 스토어를 비워야 합니다. 토큰을 Entities에 저장하는 경우, 이 로직은 `model` 세그먼트에 배치할 수 있습니다. 토큰을 Shared에 저장하는 경우, 이 로직을 `shared/api`에 포함하면 세그먼트가 너무 복잡해질 수 있습니다. 따라서 토큰 관리 로직을 별도의 세그먼트(예: `shared/auth`)로 분리하는 것도 고려해볼 만합니다. + +[tutorial-authentication]: /docs/get-started/tutorial#authentication +[import-rule-on-layers]: /docs/reference/layers#import-rule-on-layers +[ext-remix]: https://remix.run +[ext-zod]: https://zod.dev