208 lines
14 KiB
Markdown
208 lines
14 KiB
Markdown
# Facebook 로그인 연동 - 프론트엔드 개발계획서
|
|
|
|
## 1. 개요
|
|
|
|
CastAD 서비스에 Facebook 계정 연동 기능을 구현한다. 백엔드에 OAuth 2.0 Authorization Code Flow가 이미 구현되어 있으며, 프론트엔드는 이 플로우의 시작(연동 요청)과 끝(결과 수신)을 담당한다.
|
|
|
|
기술 스택: React 기반 SPA를 전제로 한다.
|
|
|
|
## 2. OAuth 2.0 전체 플로우
|
|
|
|
Facebook OAuth 연동은 총 8단계로 이루어진다. 프론트엔드가 직접 관여하는 단계는 1, 2, 8번이며, 3~7번은 Facebook과 백엔드 사이에서 자동으로 처리된다.
|
|
|
|
**1단계 (프론트 → 백엔드)**: 프론트엔드가 `GET /sns/facebook/connect`를 호출한다. Authorization 헤더에 사용자의 Bearer 토큰을 포함해야 한다. 백엔드는 `auth_url`(Facebook 인증 페이지 URL)과 `state`(CSRF 방지 토큰)를 JSON으로 응답한다.
|
|
|
|
**2단계 (프론트 → Facebook)**: 프론트엔드가 응답받은 `auth_url`을 팝업 윈도우로 연다. 사용자는 이 팝업에서 Facebook 로그인과 권한 승인을 수행한다.
|
|
|
|
**3단계 (Facebook → 백엔드)**: 사용자가 승인을 완료하면 Facebook이 백엔드의 `redirect_uri`(`/sns/facebook/callback`)로 리다이렉트한다. 이때 query parameter로 `code`(인가 코드)와 `state`를 전달한다. 프론트엔드는 이 단계에 관여하지 않는다.
|
|
|
|
**4~6단계 (백엔드 ↔ Facebook)**: 백엔드가 `code`를 Facebook API에 전달하여 access token을 획득하고, 장기 토큰으로 교환한 뒤, 사용자 정보를 조회하여 DB에 저장한다. 프론트엔드는 이 단계에 관여하지 않는다.
|
|
|
|
**7단계 (백엔드 → 프론트)**: 백엔드가 처리를 완료한 뒤 302 리다이렉트로 프론트엔드 URL로 보낸다. 성공 시 `/social/connect/success`로, 실패 시 `/social/connect/error`로 리다이렉트하며 결과 정보를 query parameter에 포함한다.
|
|
|
|
**8단계 (프론트)**: 리다이렉트된 페이지(팝업 내부)에서 query parameter를 파싱하고, `window.opener.postMessage()`로 부모 창에 결과를 전달한 뒤 팝업을 닫는다. 부모 창은 메시지를 수신하여 UI를 업데이트한다.
|
|
|
|
## 3. 백엔드 API 명세
|
|
|
|
### 3-1. Facebook 연동 시작
|
|
|
|
- 엔드포인트: `GET /sns/facebook/connect`
|
|
- 인증: 필수. `Authorization: Bearer <access_token>` 헤더를 포함해야 한다.
|
|
- 성공 응답 (200): `auth_url` 필드에 Facebook 인증 페이지 URL, `state` 필드에 CSRF 방지 토큰이 포함된 JSON 객체를 반환한다.
|
|
|
|
```json
|
|
{
|
|
"auth_url": "https://www.facebook.com/v21.0/dialog/oauth?client_id=...&redirect_uri=...&state=...&scope=...&response_type=code",
|
|
"state": "5PcHtcvJdQskHw5aG-MIOmv5CCX3L9v3eqQOqyZlKsc"
|
|
}
|
|
```
|
|
|
|
- 에러 응답: 401 (인증 실패, 토큰 없음 또는 만료)
|
|
|
|
### 3-2. Facebook OAuth 콜백
|
|
|
|
- 엔드포인트: `GET /sns/facebook/callback`
|
|
- 인증: 불필요. 이 엔드포인트는 Facebook이 직접 호출하며, 프론트엔드가 직접 호출하지 않는다.
|
|
- query parameter: `code` (Facebook 인가 코드), `state` (CSRF 토큰), `error` (선택, OAuth 에러 코드), `error_description` (선택, 에러 설명)
|
|
- 응답: JSON이 아닌 302 리다이렉트로 응답한다. 프론트엔드의 성공 또는 에러 페이지로 리다이렉트한다.
|
|
|
|
성공 시 리다이렉트 URL: `{OAUTH_FRONTEND_URL}/social/connect/success?platform=facebook&account_id={id}&channel_name={name}`
|
|
|
|
실패 또는 취소 시 리다이렉트 URL: `{OAUTH_FRONTEND_URL}/social/connect/error?platform=facebook&error={message}&cancelled=true|false`
|
|
|
|
`OAUTH_FRONTEND_URL`은 백엔드 환경변수로 설정되며, 개발 환경 기본값은 `http://localhost:3000`이다. 프론트엔드 배포 도메인과 일치해야 하므로 백엔드 팀과 사전 확인이 필요하다.
|
|
|
|
### 3-3. Facebook 연동 해제
|
|
|
|
- 엔드포인트: `DELETE /sns/facebook/disconnect`
|
|
- 인증: 필수. `Authorization: Bearer <access_token>` 헤더를 포함해야 한다.
|
|
- 성공 응답 (200): `{"success": true, "message": "Facebook 계정 연동이 해제되었습니다."}`
|
|
- 에러 응답: 401 (인증 실패), 404 (연동된 Facebook 계정 없음, 에러 코드 `FACEBOOK_ACCOUNT_NOT_FOUND`)
|
|
|
|
## 4. 프론트엔드 구현 항목
|
|
|
|
### 4-1. 라우트 등록
|
|
|
|
React Router에 다음 2개의 라우트를 추가해야 한다. 이 페이지들은 백엔드 콜백의 302 리다이렉트 대상이므로 반드시 존재해야 한다.
|
|
|
|
- `/social/connect/success`: OAuth 연동 성공 처리 페이지
|
|
- `/social/connect/error`: OAuth 연동 실패/취소 처리 페이지
|
|
|
|
이 두 페이지는 팝업 윈도우 안에서 렌더링되며, 독립적으로 접근 가능해야 한다(SPA 라우팅이 아닌 직접 URL 접근으로도 동작해야 한다).
|
|
|
|
### 4-2. Facebook 연동 버튼 컴포넌트
|
|
|
|
소셜 계정 관리 페이지 또는 설정 페이지에 배치한다.
|
|
|
|
동작 흐름:
|
|
1. 사용자가 "Facebook 연동하기" 버튼을 클릭한다.
|
|
2. 버튼을 로딩(비활성) 상태로 전환한다.
|
|
3. `GET /sns/facebook/connect`를 호출하여 `auth_url`과 `state`를 받는다.
|
|
4. `window.open(auth_url, 'facebook-oauth', 'width=600,height=700,scrollbars=yes')`로 팝업을 연다.
|
|
5. `window.open()`의 반환값이 `null`이면 브라우저가 팝업을 차단한 것이다. 이 경우 `window.location.href = auth_url`로 현재 창에서 리다이렉트하는 폴백을 적용한다.
|
|
6. 팝업에서 `postMessage`를 수신할 `message` 이벤트 리스너를 등록한다.
|
|
|
|
팝업 방식을 권장하는 이유: 현재 페이지의 상태(폼 입력값, 스크롤 위치 등)를 유지하면서 OAuth 인증을 진행할 수 있다.
|
|
|
|
### 4-3. OAuth 성공 페이지 (`/social/connect/success`)
|
|
|
|
이 페이지는 팝업 윈도우 안에서 렌더링된다.
|
|
|
|
동작 흐름:
|
|
1. `URLSearchParams`로 query parameter를 파싱한다. 사용 가능한 파라미터는 `platform`("facebook"), `account_id`(연동된 계정 ID, 숫자), `channel_name`(Facebook 사용자 이름)이다.
|
|
2. `window.opener`가 존재하는지 확인한다(팝업으로 열렸는지 판별).
|
|
3. 팝업인 경우: `window.opener.postMessage()`로 부모 창에 성공 결과를 전달하고, `window.close()`로 팝업을 닫는다. postMessage의 `targetOrigin`은 보안을 위해 `window.location.origin`으로 지정한다.
|
|
4. 팝업이 아닌 경우(폴백 리다이렉트): 소셜 계정 관리 페이지로 `navigate()`한다.
|
|
|
|
postMessage 데이터 구조:
|
|
```json
|
|
{
|
|
"type": "FACEBOOK_OAUTH_RESULT",
|
|
"success": true,
|
|
"platform": "facebook",
|
|
"accountId": "연동된 계정 ID",
|
|
"channelName": "Facebook 사용자 이름"
|
|
}
|
|
```
|
|
|
|
### 4-4. OAuth 에러 페이지 (`/social/connect/error`)
|
|
|
|
이 페이지도 팝업 윈도우 안에서 렌더링된다.
|
|
|
|
동작 흐름:
|
|
1. `URLSearchParams`로 query parameter를 파싱한다. 사용 가능한 파라미터는 `platform`("facebook"), `error`(에러 메시지 문자열), `cancelled`("true" 또는 "false")이다.
|
|
2. `cancelled`가 "true"이면 사용자가 Facebook에서 직접 취소한 것이다. "false"이면 시스템 에러이다.
|
|
3. 팝업인 경우: `window.opener.postMessage()`로 부모 창에 에러 결과를 전달하고, `window.close()`로 팝업을 닫는다.
|
|
4. 팝업이 아닌 경우: 에러 메시지를 화면에 표시하고 "다시 시도" 버튼과 소셜 관리 페이지로 돌아가는 링크를 제공한다.
|
|
|
|
postMessage 데이터 구조:
|
|
```json
|
|
{
|
|
"type": "FACEBOOK_OAUTH_RESULT",
|
|
"success": false,
|
|
"platform": "facebook",
|
|
"error": "에러 메시지",
|
|
"cancelled": true
|
|
}
|
|
```
|
|
|
|
### 4-5. 부모 창 메시지 수신 처리
|
|
|
|
연동 버튼이 있는 컴포넌트에서 `window.addEventListener('message', handler)`로 메시지를 수신한다.
|
|
|
|
처리 로직:
|
|
1. `event.origin`이 현재 페이지의 origin과 일치하는지 검증한다(보안).
|
|
2. `event.data.type`이 `'FACEBOOK_OAUTH_RESULT'`인지 확인한다.
|
|
3. `event.data.success`가 `true`이면 연동 성공 UI를 표시한다(토스트 메시지, 연동된 계정 정보 갱신 등).
|
|
4. `event.data.success`가 `false`이면 `event.data.cancelled` 여부에 따라 취소 메시지 또는 에러 메시지를 표시한다.
|
|
5. 컴포넌트 언마운트 시 이벤트 리스너를 반드시 해제한다.
|
|
|
|
### 4-6. 연동 해제 기능
|
|
|
|
동작 흐름:
|
|
1. 연동된 계정 정보 옆에 "연동 해제" 버튼을 배치한다.
|
|
2. 클릭 시 확인 다이얼로그를 표시한다("Facebook 연동을 해제하시겠습니까?").
|
|
3. 확인 시 `DELETE /sns/facebook/disconnect`를 호출한다. Authorization 헤더를 포함해야 한다.
|
|
4. 성공 응답(200) 수신 시 UI를 미연동 상태로 업데이트한다.
|
|
5. 404 응답 시 이미 연동 해제된 상태이므로 UI를 미연동 상태로 업데이트한다.
|
|
|
|
### 4-7. 연동 상태 표시
|
|
|
|
연동 전 상태: Facebook 아이콘과 "연동하기" 버튼을 표시한다.
|
|
|
|
연동 후 상태: Facebook 아이콘, 연동된 사용자 이름(channel_name), "연동 해제" 버튼을 표시한다.
|
|
|
|
참고: 현재 백엔드에 사용자의 Facebook 연동 상태를 조회하는 전용 API가 없다. 페이지 진입 시 연동 여부를 표시하려면, 기존 사용자 프로필 API에 소셜 연동 정보가 포함되어 있는지 확인하거나, 백엔드에 상태 조회 API 추가를 요청해야 한다. 연동 직후에는 postMessage로 받은 `accountId`와 `channelName`을 컴포넌트 상태에 저장하여 표시할 수 있다.
|
|
|
|
## 5. 에러 처리
|
|
|
|
프론트엔드에서 처리해야 하는 에러 시나리오와 대응 방법은 다음과 같다.
|
|
|
|
**사용자 취소 (`cancelled=true`)**: 사용자가 Facebook 인증 화면에서 "취소"를 눌렀다. "Facebook 연동이 취소되었습니다." 메시지를 표시한다. 재시도 가능하므로 연동 버튼을 다시 활성화한다.
|
|
|
|
**state 만료 (`FACEBOOK_STATE_EXPIRED`)**: 사용자가 Facebook 로그인을 너무 오래 지체하여 백엔드의 state 토큰이 만료되었다(기본 TTL: 300초, 약 5분). "인증 세션이 만료되었습니다. 다시 시도해주세요." 메시지를 표시한다.
|
|
|
|
**토큰 교환 실패 (`FACEBOOK_AUTH_FAILED`)**: Facebook에서 받은 인가 코드로 access token 교환이 실패했다. "Facebook 인증에 실패했습니다. 다시 시도해주세요." 메시지를 표시한다.
|
|
|
|
**Facebook API 오류 (`FACEBOOK_API_ERROR`)**: Facebook Graph API 호출 중 서버 오류가 발생했다. "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요." 메시지를 표시한다.
|
|
|
|
**연동 해제 시 계정 없음 (`FACEBOOK_ACCOUNT_NOT_FOUND`)**: 이미 연동 해제된 상태에서 다시 해제를 시도했다. UI를 미연동 상태로 갱신한다.
|
|
|
|
**인증 실패 (401)**: 서비스 로그인 토큰이 만료되었다. 로그인 페이지로 이동시킨다.
|
|
|
|
## 6. 폴백 처리 (팝업 차단 시)
|
|
|
|
브라우저가 팝업을 차단하는 경우 `window.open()`이 `null`을 반환한다. 이때는 현재 창에서 `window.location.href = auth_url`로 직접 이동시키는 폴백을 적용한다.
|
|
|
|
폴백 모드에서는 OAuth 완료 후 `/social/connect/success` 또는 `/social/connect/error` 페이지로 돌아왔을 때 `window.opener`가 `null`이다. 이 경우 postMessage 대신 다음과 같이 처리한다:
|
|
1. query parameter에서 결과를 파싱한다.
|
|
2. 결과를 `sessionStorage`에 저장한다(키 예시: `facebook_oauth_result`).
|
|
3. 소셜 계정 관리 페이지로 `navigate()`한다.
|
|
4. 소셜 계정 관리 페이지 마운트 시 `sessionStorage`에서 결과를 확인하고, 있으면 UI에 반영한 뒤 `sessionStorage`에서 삭제한다.
|
|
|
|
이 폴백이 필요한 이유: 현재 창 리다이렉트 방식에서는 OAuth 전에 있던 페이지 상태가 사라지므로, 결과를 임시 저장하여 원래 페이지로 복귀 후 처리해야 한다.
|
|
|
|
## 7. 환경 설정 확인 사항
|
|
|
|
프론트엔드 개발 시작 전에 백엔드 팀과 다음 값들을 확인해야 한다.
|
|
|
|
1. `OAUTH_FRONTEND_URL`: 백엔드가 콜백 처리 후 프론트엔드로 302 리다이렉트할 때 사용하는 URL이다. 개발 환경에서는 `http://localhost:3000`, 운영 환경에서는 실제 프론트엔드 도메인이 설정되어야 한다. 이 값이 잘못되면 OAuth 완료 후 엉뚱한 URL로 리다이렉트된다.
|
|
|
|
2. `OAUTH_SUCCESS_PATH`: 성공 시 리다이렉트 경로. 기본값은 `/social/connect/success`이다.
|
|
|
|
3. `OAUTH_ERROR_PATH`: 실패 시 리다이렉트 경로. 기본값은 `/social/connect/error`이다.
|
|
|
|
4. `FACEBOOK_REDIRECT_URI`: Facebook이 인증 완료 후 호출하는 백엔드 콜백 URL이다. 프론트엔드가 변경할 수 없으며, Facebook 개발자 콘솔에 등록된 URI와 정확히 일치해야 한다. 현재 설정값은 `http://dev.castad.net/sns/facebook/callback`이다.
|
|
|
|
## 8. 구현 순서
|
|
|
|
다음 순서로 구현을 권장한다.
|
|
|
|
1. React Router에 `/social/connect/success`와 `/social/connect/error` 라우트를 등록한다.
|
|
2. 성공 페이지를 구현한다. query parameter 파싱과 postMessage 전송 로직을 작성한다.
|
|
3. 에러 페이지를 구현한다. 취소와 에러를 분기 처리하고 postMessage 전송 로직을 작성한다.
|
|
4. Facebook 연동 버튼 컴포넌트를 구현한다. API 호출, 팝업 열기, postMessage 수신을 작성한다.
|
|
5. 팝업 차단 시 폴백 로직을 구현한다(sessionStorage 기반).
|
|
6. 연동 해제 기능을 구현한다.
|
|
7. 연동 상태 표시 UI를 구현한다(상태 조회 API 확인 후).
|