Next.js App Router 환경에서 removeChild 에러가 발생하는 현상을 분석하고, 근본 원인 및 실질적인 해결 방법을 안내합니다.


1. 에러 현상 개요

Next.js 기반의 서비스에 외부 라이브챗 서비스를 연동한 후, 페이지 이동시 global-error 페이지로 떨어지는 형상이 발생했다.

또한, Sentry와 같은 APM 도구에서는 다음과 같은 형태로 에러가 포착:

TypeError: Cannot read properties of null (reading 'removeChild')

incomplete-app-router-transaction

 

단순히 외부 라이브챗 스크립트를 적용했을 때만 발생하는 현상이었으므로, 외부스크립트의 동작이 원인일 것으로 예측하였다.

 

2. 현상 파악 과정

과거에도 동일한 에러를 본적이 있고, 그때 스크립트가 우리 서비스의 DOM 요소를 수정하여서 발생한 에러였다.
이 경험을 통해, 외부 스크립트가 DOM 변화를 발생시키는지 확인하기로 함.

 

2-1)실제 DOM 변화 관찰

크롬 개발자도구의 Elements 탭을 통해 다음과 같은 DOM 조작을 확인했다:

  • 외부 라이브챗 스크립트가 기존 <title>을 제거하고 새롭게 생성
  • favicon 용 <link> 태그 일부가 교체됨

이러한 조작이 App Router의 DOM 관리 방식과 충돌을 일으킨 것으로 추정.

 

3. 원인 분석 : 왜 removeChild 오류가 발생하는가?

🔧 Next.js App Router의 <head> 처리 방식

Next.js의 App Router(React 18 기반)는 <title>, <meta>, <link> 등의 태그를 "hoistable elements" 로 취급합니다.

  • 최초 렌더 시 해당 요소를 <head>에 추가
  • 이후 라우팅이 변경될 때 React 내부의 diff 알고리즘이 기존 요소와 비교하여 removeChild 또는 appendChild를 호출

이때 React는 이전 <head> 요소의 참조를 캐싱하고 있기 때문에, 외부 스크립트가 이를 임의로 삭제하거나 교체하면 parent.removeChild(node) 호출 시 다음과 같은 오류가 발생합니다:

 

📌 관련 이슈: Vercel Discussions #52625

 

Cannot read properties of null (reading 'removeChild') with App directory and Intercom · vercel next.js · Discussion #52625

Summary I am having a Next.js (App directory) project which uses Intercom. When I open a page on mobile, open/close the intercom, and then navigate to a new page. I am having Cannot read properties...

github.com

 

 

😱 외부 스크립트의 직접 DOM 조작

  • 라이브챗 위젯은 실행 중에 document.head 내부의 <title> 및 favicon <link> 태그를 직접 수정하거나 교체
  • React는 기존 요소에 대한 참조를 유지하고 있으므로, 라우팅 전환 시 parentNode가 null인 노드를 제거하려고 시도 → 예외 발생

 

4. 해결 전략 및 옵션

전략 장점 단점
A. 라이브챗이 <head>를 수정하지 않도록 설정<br/>예: disable-favicon-badge, hide_default_launcher 등의 옵션 활용 가장 단순하고 안정적<br/>Next.js의 DOM 관리 규칙을 준수 옵션이 없는 위젯에서는 불가능
B. 위젯을 React 컴포넌트 안에서 mount/unmount 관리<br/>전용 Provider 및 useEffect를 통해 상태 관리 라우팅 간 head diff 예측 가능<br/>제어 및 해제가 용이 초기화/해제 비용, 코드 복잡도
C. iframe 또는 Web Component로 위젯 격리 Next.js의 DOM과 완전히 독립적<br/>head 충돌 없음 도입 난이도 존재, 스타일·크기 조정 필요
D. MutationObserver로 head 조작 감시 후 롤백 위젯에 설정 옵션이 없는 경우 대처 가능 깜빡임 가능성, 유지보수 어려움

 

5. 주의사항 및 고려점

1. <head> 직접 조작은 금물

Next.js 14 이상에서는 Metadata API를 통해 <title>, <meta>, <link> 태그를 설정해야 하며, 직접 DOM을 조작하는 방식은 권장되지 않습니다.

외부 스크립트가 이를 위반하는 경우, 반드시 격리 또는 복원 처리 로직을 도입해야 합니다.

2. 스크립트 중복 로드 방지

cleanup 로직 없이 위젯이 중복 로드되면, 전역 객체 충돌이나 already initialized 오류가 발생할 수 있습니다. 항상 언마운트 처리 후 재로드하도록 구성해야 합니다.

3. 개발 환경(HMR)에서도 반드시 테스트

Next.js 13.4.10 버전에서는 개발 중 CSS HMR 갱신 도중에도 동일한 removeChild 오류가 발생한 사례가 있습니다. 반드시 개발 서버에서도 동일한 방식으로 테스트가 필요합니다.

 

 

🧩 결론 및 요약

문제 원인: 외부 스크립트가 Next.js App Router가 관리하는 <head> 요소를 변경하면서 React의 diff 알고리즘과 충돌

해결 방법:

  1. 라이브챗 위젯 옵션으로 <head> 조작 비활성화
  2. React 컴포넌트에서 위젯 mount/unmount 시점에 head 복원
  3. iframe 또는 Web Component로 위젯 격리
  4. 최후의 수단: MutationObserver로 head 복원 감시

 

💡 실무 팁

  • 라이브챗과 같은 외부 위젯은 필요한 페이지에서만 로드하고, 전역으로 항상 mount하지 마세요.
  • 위젯 초기화 시점과 해제 시점을 명확히 제어하여, head 충돌 및 전역 변수 중복 정의를 방지하세요.
  • iframe 방식이 가능하다면 가장 안전하게 DOM 격리를 보장할 수 있습니다.

 

세팅한 내용

 

[스트라이프 대시보드]

1. card 활성화

2. Apple pay 활성화

3. Payment domain 등록 

- 로컬 호스트 같은건 안됨, https 로 제공하는 도메인이어야 함,

- dev 서버가 https 로 돌고있어서, dev 도메인을 등록하였음

- verify 를 받기 위해선 아래 프론트에서 추가 작업이 필요함

 

 

[프론트 작업]

1. 스트라이프가 제공하는 도메인 인증용 파일을 다운로드 (참고)

2. Next.js 기준 /public/.well-known/apple-developer-merchantid-domain-association  파일 생성

3. 다운받은 도메인 인증용 파일 내용 붙여넣기

4. 미들웨어 설정에서 .well-known 으로 시작하는 라우팅은 미들웨어 로직 타지 않도록 설정

- middleware.ts

export const config = {
  // Matcher ignoring `/_next/` and `/api/`
  matcher: [
    "/", // Required when i18n is enabled, otherwise middleware won't be executed on index route
    "/((?!api/|google/|_next/|_static/|auth|_vercel|.well-known|fonts|icons|images/|[\\w-]+\\.\\w+).*)",
  ],
};

 

5. local-domain.com/.well-known/apple-developer-merchantid-domain-association 링크로 접근했을때, 해당 파일이 다운로드 됨

6. 다운로드 되지 않고, plain/text 로 제공하기 위해서 next.config.js 세팅

const nextConfig = {
  async headers() {
    return [
      {
        source: "/.well-known/apple-developer-merchantid-domain-association",
        headers: [
          {
            key: "Content-Type",
            value: "text/plain",
          },
        ],
      },
    ];
  },
  ...
}

 

 

7. dev 서버 배포

8. https://dev-mydomain.com/.well-known/apple-developer-merchantid-domain-association 로 접근시 텍스트 제공

 

 

[백엔드 설정]

* 사실 이 세팅을 왜 해줘야하는지 모르겠음..

* 백엔드에서 테스트를 직접 충분히 해볼 수 없어서 그냥 세팅 되어있는 대로 기재한다

 

1. intent 를 생성하는 로직에서 domain 을 등록 (참고)

// 백엔드 코드 로직
createPaymentIntent

1. customer create
2. setDomainName('https://dev-mydomain.com').build()
3. domain dreate
4. domain retrieve
5. domain setEnabled(true).build()
6. domain update

 

 

 

[기대값]

- 스트라이프가 알아서 판단해서 유저가 apple pay 사용 가능한 상태면 apple pay 를 노출 시켜준다고 한다.

- <PaymentElement> 내부 apple pay 탭 노출 됨

 

 

[문제상황]

1. apple pay 노출 안됨 

2. 1번을 해결하여 결제로 넘어갔을때 apple session 생성에서 에러 발생

{
  "error": {
    "message": "Could not create Apple Pay session. Please make sure you have registered this Stripe account. For more information, see https://stripe.com/docs/apple-pay#web.",
    "type": "invalid_request_error"
  }
}

 

 

[구글링...]

1. safari 개인정보보호 모드에서는 노출되고, 일반 모드에서는 애플페이 탭이 노출 안되었음.

- 비슷한 내용들의 질문글들이 있음.

- 사파리 세팅 값을 변경하니 (Check Apple Pay 를 disable 처리) 일반 모드에서도 애플페이 탭이 노출되었다

- 근데 애플페이 세팅이 잘 되어있는 사람한테도 Check Apple Pay 켜져있으면 애플페이 탭이 안 보이는게 이상하네(?)

 

https://github.com/woocommerce/woocommerce-gateway-stripe/issues/1548

 

Safari "Allow websites to check for Apple Pay" setting have to be disabled in order Apple Pay button appears. · Issue #1548 ·

Describe the bug See #1111. The issue is still there: p1619724791069000-slack-C7U3Y3VMY (see the thread for sample URLs) To Reproduce Use Safari with the setting enabled Open the sample URLs in pri...

github.com

 

https://wordpress.org/support/topic/apple-pay-not-displaying-when-safari-set-to-show/

 

Apple Pay not displaying when Safari set to show

Apple Pay not displaying when Safari set to show Resolved joolsf1 (@joolsf1) 2 years, 8 months ago I have a WooCommerce shop with Stripe Plugin. The Apple Pay button only displays on my desktop and…

wordpress.org

 

https://support.apple.com/en-al/guide/safari/ibrw1075/mac

 

Change Advanced settings in Safari on Mac

In Safari on your Mac, show full website addresses, set accessibility options, set advanced privacy options, and show features for web developers.

support.apple.com

 

[테스트 했던 세팅값들]

사파리 설정 켜짐 + 테스트 카드 애플페이 등록 : 일반 X, 시크릿 O

사파리 설정 켜짐 + 애플페이 없음 : 일반 X, 시크릿 O

사파리 설정 꺼짐 + 애플페이 없음 : 일반 O

 

 

2. apple session 생성에서 에러

사파리 세팅을 변경한 뒤 애플페이 탭이 잘 노출되고, 애플페이를 선택해서 결제로 넘어가면 session 을 생성하는 부분에서 에러가 발생한다.

 

URL: https://api.stripe.com/v1/apple_pay/session

status : 400

{
  "error": {
    "message": "Could not create Apple Pay session. Please make sure you have registered this Stripe account. For more information, see https://stripe.com/docs/apple-pay#web.",
    "type": "invalid_request_error"
  }
}

 

도메인도 등록하고 다 했는데, 왜 register 하라는 건가.. 도메인 관련된 에러가 맞는가..

비슷한 에러가 스택오버 플로우에 있는데, 명쾌한 답은 없고, 테스트 키라서 그런가 해서 라이브 키로 테스트 해보았다.

https://stackoverflow.com/questions/70809397/stripe-connect-enable-apple-pay

 

Stripe Connect – enable Apple Pay?

I'm trying to make Stripe Connect accounts accept payments in my app with the use of PaymentElement. So far the payments are going through with the regular credit card input, but now I want to enable

stackoverflow.com

 

라이브 키로 바꿔서 테스트 하니까.. 잘 됨

 

(머지?)

 

 

[(번외) 애플페이 테스트]

애플 테스트 카드 추가 방법

https://developer.apple.com/apple-pay/sandbox-testing/

 

Sandbox Testing - Apple Pay

The Apple Pay Sandbox environment allows merchants and developers to test their implementation of Apple Pay with test credit and debit cards.

developer.apple.com

 

1. 애플 개발자 센터에서 샌드박스 테스팅 계정 추가

2. 맥북 애플 id 테스트 계정으로 변경

3. 지갑에서 테스트 카드 등록

4. https dev 서버에서 스트라이프 라이브 키로 바꾸고 테스트 카드로 결제 진행하면, live 모드에서 왜 테스트 카드 쓰냐고 에러남

 

 

 

 

  • aws s3 버킷에 에셋을 올리고 나서 url 을 리턴 받는데,
  • 비디오의 경우 일정 시간 동안 해당 url 이 not Found 를 리턴한다
  • 아마도 인코딩 시간동안은 접근이 안되는 것으로 추정(카카오 스토리지도 동일한 현상)
  • 해결방법으로 not Found 를 받는 경우 비디오 요소를 리랜더링 해서 해당 url 에 접근 재시도
    • 일정 주기 & 제한된 횟수 만큼

 

solution1)

  • 비디오 자체를 리랜더링
  • 내부 state 를 <video> 태그의 key 로 사용하여 리랜더링 될 수 있도록 한다
  • <video> 태그의 onError 속성 사용

component/VideoWithErrorCheck.tsx

import { useState } from "react";

export default function VideoWithErrorCheck({ ...props }) {
  const [errorCount, setErrorCount] = useState(0);
  const onVideoError = () => {
    // S3 URL 최초 몇 초 동안은 not Found 발생
    // 3초마다 1번씩 최대 10번만 시도
    if (errorCount < 10) {
      setTimeout(() => {
        setErrorCount((prevErrorCount) => prevErrorCount + 1);
      }, 3000);
    }
  };
  return <video key={errorCount} onError={onVideoError} {...props}></video>;
}

 

 

컴포넌트 사용부
myPage.tsx

<VideoWithErrorCheck
    width={"100%"}
    height={"100%"}
    style={{
      objectFit: "cover",
    }}
    src={`${el.mediaPath}`}
    controls
    autoPlay
    muted
    loop
/>

 

별건 아니고... 초창기에 흔히 하기 쉬운 실수..(?)

 

 

Access to fetch at 'https://accounts.kakao.com/login?continue=https%3A%2F%2Fkauth.kakao.com%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A3000%252Flogin%26through_account%3Dtrue%26client_id%3Dff30835bba2350bfe5fcf200c1eb4891' (redirected from 'https://kauth.kakao.com/oauth/authorize?client_id=ff30835bba2350bfe5fcf200c1eb4891&redirect_uri=http://localhost:3000/login&response_type=code') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

 

로그인 요청 보낼 때 fetch 나 axios, ajax 비동기 요청함수로 보내면 CORS 에러난다

 

<a>, <Link> 로 리다이렉트 시켜야함

 

코드 요청부터 백엔드에서 다 한다고 해도, 백엔드로 요청하는 것부터 리다이렉트로 요청해야함

 

첫 시작은 url 로 get 요청

 

"use client";

import Link from "../../../node_modules/next/link";

export default function KakaoLoginButton() {
  return (
    <Link
      href={"http://localhost:5500/members/signin/kakao"}
      style={{ color: "black", backgroundColor: "yellow", padding: "10px" }}
    >
      카카오 로그인
    </Link>
  );
}

 

 

* 케이스를 마주할 때마다 업데이트 할 예정

 

Frontend 에서 처리하기 용이하면서도 RESTful API 관점을 잃지 않고 HTTP 응답 상태 코드를 어떻게 약속할지에 대한 생각과 의견이다.

 


상태 코드 Case

1. GET 조회 시 빈 데이터

- customer/order/{userId} : userId 에 해당하는 order 리스트를 조회했는데, 해당 유저의 오더 건이 없는 경우

- product?word={text} : 쿼리로 조회시 검색 결과가 없는 경우

[응답]

status : 200
response : {
	result : true,
	data: []
}

 

[추가 고려사항]

- result : true / false 의 용도를 어떻게 정의해야 하는가?

- 사실상 result : false 인 케이스는 없지 않을까? 필요하긴 한가? -> 일단 현재는 필요없다고 생각함

 

[UI]


2. GET 조회 시 존재하지 않는 데이터 (자원이 없음)

- product/{productId} : productId 로 조회하는 페이지에서 해당 ProductId 가 없는 경우

[응답]

status: 404
message : "존재하지 않는 제품입니다."

 

[UI]

 

[Other Case]

- 500 에러로 리턴 : 에러코드의 의미가 맞지 않음

status: 500
message : "존재하지 않는 제품입니다."

 

- 200에 result: false 로 리턴 : 해당 케이스는 성공이 아님

status: 200
response : {
	result: false,
    status: 404,
    message: "존재하지 않는 제품입니다."
    subCode: "NOT_FOUND"
}

3. PUT/DELETE 수정/삭제시 없는 데이터를 수정/삭제 하려는 경우 (자원이 없음)

[응답]

status: 404
message : "존재하지 않는 제품입니다."

 

[UI]

- 사이트 내부적으로 사용하는 alert 를 띄우고, 요청내용이 처리되지 않도록 처리, 이전 페이지로 이동

 

[Other Case]

- 500 에러로 리턴 : 에러코드의 의미가 맞지 않음

status: 500
message : "존재하지 않는 제품입니다."

 

- 200에 result: false 로 리턴 : 해당 케이스는 성공이 아님

status: 200
response : {
	result: false,
    status: 404,
    message: "존재하지 않는 제품입니다."
    subCode: "NOT_FOUND"
}

4. Client 의 잘못된 요청 - Bad Request

- 요청을 보내는 파라미터 값 (path, query, body) 이 유효하지 않은 경우

- Type 불일치, 필수값 누락 등

[응답]

status : 400
message: "[channelSapCode] : 공백일 수 없습니다"

 

[UI]

1. GET & 잘못된 queryKey : 메인 페이지로 리다이렉트


 

2. GET & 잘못된 queryKey : 빈 데이터와 동일하게 처리

- 사실상 올바른 queryKey 에는 "" 빈 스트링이 들어온 것으로 보고, 빈 데이터 결과를 리턴함

 


 

3. GET & queryKey 데이터 타입 불일치 : 에러 페이지로 리턴

https://www.airbnb.co.kr/?monthly_start_date=2024-03-01

 

- 아래와 같이 date 형식을 다르게 보냈을 때

https://www.airbnb.co.kr/?monthly_start_date=20240301


 

4. PUT/POST 에서 전달 하는 데이터의 형식이 올바르지 않는 경우

- 사실상 프론트와 백이 동일한 로직으로 걸러주어야 함

- 만에하나 백으로 넘어왔을 경우, 사이트 내부적으로 사용하는 alert 를 띄우고, 요청내용이 처리되지 않도록 처리

 


 

로그인 JWT(accessToken, refreshToken)

 

플로우

  • websequencediagrams 스크립트
title JWT login process (evaluation-iic)

frontend->backend: login (/auth/access_token)
backend -> backend : 유저 확인 (access_token, refresh_token 발급)
backend->frontend: login 응답 (access_token, refresh_token)
frontend -> frontend : localStorage에 저장(access_token, refresh_token)
frontend-> backend: 데이터 요청 (ex) GET /evaluation/group) with access_token
backend -> backend : access_token 검증
alt if access_token valid
backend -> frontend : 데이터 응답
else else access_token invalid
backend -> frontend : 401 Error 응답
frontend -> backend : 토큰 재발급 요청(/auth/refresh_token) with refresh_token
backend -> backend : refresh_token 검증
alt if refresh_token valid
backend -> frontend : 토큰 재발급 응답(access_token, refresh_token)
frontend -> frontend : localStroage 저장(access_token, refresh_token)
else else refresh_token invalid
backend -> frontend : 401 Error 응답
frontend -> frontend : 로그아웃 처리 및 redirect to login 페이지

 

사전준비

  • msw 를 사용하여 401 응답을 가정
  • axios 로 비동기 요청 처리
    • axios instance 를 생성하여, interceptor 정의 하여 사용
    • src/axiosInstance.js
    import axios from 'axios';
    
    const axiosInstance = axios.create({
    	baseURL: 'https://myapp-dev-api.myapp.com',
    	headers: {
    		'Content-Type': 'application/json',
    	},
    });

 

  • 로그인 로직에서는 pure axios를 사용 → 로그인 요청에는 access_token을 체크하지 않기 때문
import axios from 'axios';

axios.defaults.baseURL = 'https://myapp-dev-api.myapp.com';

// login 시는 accessToken 체크 안함
export const postLogin = ({ id, password }) => {
	return axios.post('/auth/access_token', {
		id,
		password,
	});
};

 

  • 로그인 성공시 localStorage에 응답받은 토큰(access_token, refresh_token) 담기
const loginMutation = useMutation({
	mutationFn: postLogin,
	onSuccess: ({ data }) => {
		setUserInfoState({
			...data,
			userId: formData.id,
			accessLv: getAccessLv(data.scope),
		});
		localStorage.setItem('accessToken', data.access_token);
		localStorage.setItem('refreshToken', data.refresh_token);

		toast.success(`Logged in as ${formData.id}`, {
			theme: 'colored',
		});

		navigate('/main');
	},
	onError: () => {
		console.log('실패');
	},
});

 

토큰 플로우 테스트

1. 토큰 테스트를 위한 임시 테스트 버튼 생성

  • 버튼 온클릭 함수
    • 데이터 get 요청 보내는 함수
    • 로그인 이후 모든 request 에서는 axiosInstance 를 사용
const tokenTest = async () => {
		const result = await axiosInstance.get('/mydata/question/apply/');
		console.log(result);
	};

 

2. axiosInstance.js 의 baseURL 을 잠시 localhost:3000으로 변경

  • msw 사용하기 위해 변경
const axiosInstance = axios.create({
	// baseURL: 'https://myapp-dev-api.myapp.com',
	baseURL: 'http://localhost:3000',
	headers: {
		'Content-Type': 'application/json',
	},
});
  • axios 의 baseURL 도 localhost:3000으로 변경 → msw 사용하기 위해
axios.defaults.baseURL = '<http://localhost:3000>'
  • 현재는 request에 interceptor를 통해서 access_token 을 헤더에 담기 처리
axiosInstance.interceptors.request.use(
	function (config) {
		// 요청시 AccessToken 을 헤더에 담기
		const accessToken = localStorage.getItem('accessToken');

		if (accessToken) {
			config.headers.Authorization = `Bearer ${accessToken}`;
		} else {
			localStorage.removeItem('user');
			localStorage.removeItem('accessToken');
			localStorage.removeItem('refreshToken');
			window.location.href = '/login';
		}
		return config;
	},
	function (error) {
		// return request error
		return Promise.reject(error);
	}
);

 

3. msw handlers.js 에 작업

rest.get('/mydata/question/apply/', async (req, res, ctx) => {
	return res(
		ctx.status(200),
		ctx.json({
			result: false,
		})
	);
}),
  • 일반적인 get 요청을 하는데, axiosInstance 로 요청을 하면, request.interceptor를 거쳐서 로컬 스토리지에 있는 access_token 을 헤더에 담아서 요청하게 됨

 

4. 버튼을 눌러서 정상 동작 확인

  • header 에 access_token 이 담겨 있는 것을 확인

 

5. access_token이 만료되었다는 fake 응답을 리턴

  • msw handlers.js 에 작업
    • 이후 /auth/refresh_token 요청에서 msw 로 새로운 토큰을 ‘test-access-token’ 으로 발급해줄 예정
    • 현재 토큰은 만료되었다고 가정, 이후 ‘test-access-token’이 유효한 토큰이라고 가정
    • 일단 현재시점의 요청으로는 401을 리턴
rest.get('/mydata/question/apply/', async (req, res, ctx) => {
	console.log('msw-get-request', req.headers.get('authorization'));
	const authCheck = req.headers.get('authorization');
	if (authCheck === 'Bearer test-access-token') {
		return res(
			ctx.status(200),
			ctx.json({
				result: false,
			})
		);
	}

	return res(ctx.status(401));
}),

 

6. response.interceptor 지정

  • 401 받으면 바로 이어서 refresh_token으로 토큰 재발행 요청
  • 200 성공시 localStorage 새로운 토큰으로 업데이트
  • 기존에 요청했던 작업 재요청 → failedQueue에 담겨있는 작업 처리
let isRefreshing = false; // 중복 발행 방지 flag
let failedQueue = []; // 할 일 리스트

// 할 일 처리하는 큐
const processQueue = (error, token = null) => {
	failedQueue.forEach((taskPromise) => {
		if (error) {
			taskPromise.reject(error);
		} else {
			taskPromise.resolve(token);
		}
	});
	failedQueue = [];
};

axiosInstance.interceptors.response.use(
	function (response) {
		return response;
	},
	async (error) => {
		const {
			config,
			response: { status },
		} = error;

		const originalRequest = config;

		// 401 access 권한 에러 발생시
		if (status === 401 && !originalRequest._retry) {
			if (isRefreshing) {
				return new Promise((resolve, reject) => {
					failedQueue.push({ resolve, reject });
				})
					.then((token) => {
						originalRequest.headers.Authorization = `Bearer ${token}`;
						return axios(originalRequest);
					})
					.catch((err) => {
						return err;
					});
			}

			originalRequest._retry = true;
			isRefreshing = true;

			const refreshToken = localStorage.getItem('refreshToken');

			return new Promise((resolve, reject) => {
				axios
					.post('/auth/refresh_token', { refreshToken: refreshToken })
					.then(({ data }) => {
						localStorage.setItem('accessToken', data.access_token);
						localStorage.setItem('refreshToken', data.refresh_token);
						originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
						resolve(axios(originalRequest));
						processQueue(null, data.accessToken);
					})
					.catch((err) => {
						processQueue(err, null);
						reject(err);
						localStorage.removeItem('accessToken');
						localStorage.removeItem('refreshToken');
						window.location.href = '/login';
					})
					.then(() => {
						isRefreshing = false;
					});
			});
		}
		// return other error
		return Promise.reject(error);
	}
);
  • 로컬 스토리지 변화 확인 : accessToken / refreshToken 값 확인

  • 요청 콘솔 로그
    • 데이터 get 요청 (/evaluation/question/apply/) Failed 401
    • 토큰 재발행 요청 (/auth/refresh_token) 200 성공
    • 새로 발급받은 access_token 으로 데이터 get 재요청(/evaluation/question/apply/) 200 성공

 

7. 만약 refresh_token 도 만료라면?

  • /auth/refresh_token 에서 401 을 또 주는 상황
  • 그럼 axiosInstance.interceptors.response의 catch 에서 잡히기 때문에, 로컬 스토리지의 모든 토큰을 지우고 login 페이지로 리다이렉트
rest.post('/auth/refresh_token', async (req, res, ctx) => {
		console.log('msw-refresh-token-request', req);
		return res(ctx.status(401));
	})
  • 로컬 스토리지 확인 : accessToken, refreshToken 키값 삭제됨

 

8. 테스트 완료후 baseUrl 돌려놓기

 

npx create-react-app

create-react-app으로 설치한 리액트 앱 레포지토리는 빌드 설정을 webpack 을 따로 설정해 주지 않아도 된다.

 

AWS amplify는 풀스택 애플리케이션 구축, 배송 및 호스팅 지원하는 솔루션이다

 

그동안 클라우드 컴퓨터 생성을 위해 리눅스 서버를 하나 생성하고, 서버를 관리하는 방식으로 배포를 했다면,

 

그냥 github page 나 vercel 등 과 같이 레포지토리만 지정하고, 빌드 스크립트만 등록하면 알아서 배포를 다해준다

 

작업 방식은 공식문서나 제야에 널려있으니 참고

https://aws.amazon.com/ko/amplify/

 

풀 스택 개발 - 웹 및 모바일 앱 - AWS Amplify

실시간 및 오프라인 기능이 포함된 iOS, Android, Flutter, 웹 또는 React Native 앱용 크로스 플랫폼 백엔드를 클릭 몇 번으로 생성합니다.

aws.amazon.com

 

 

배포를 잘~ 해놓고 나서 내가 직면한 문제는,

 

모든 스크립트도 다 잘 가져왔는데, 빈 화면만 뜨는 것이었다.

 

관련해서 구글링을 좀 해보았다.

 

여러가지 에러 케이스 중 나와 동일한 케이스를 찾았고, 그에 따른 해결 방법을 찾았다.

https://medium.com/@siddhantahire98/how-to-fix-the-blank-screen-after-a-static-deployment-with-create-react-app-2e76983a5d5d

 

How to fix the Blank screen After a Static Deployment with create-react-app

It’s one of the most annoying situations after deploying a static React app: After all that work, you finally deployed your app for…

medium.com

 

 

 

Network 탭과 Source 탭을 보면 소스는 정상적으로 받아져 왔다

 

CheckPoint

빌드 결과물에

 

✅ index.html도 경로는 절대경로로 설정 되어야 있어야함

<Link 태그의 href="/static/~">

 

 

 

 package.json에서 homepage 값 수정

amplify 에서 받은 host주소를 작성해야함

"homepage" : "https://myapp.test.com"

 

 

 


 

25.08.20 추가 이슈

이번에는 Next.js 어플리케이션을 Amplify 로 배포하는 상황

 

(Amplify 도 개선이 많이 되어서 UI 가 좀 달라졌음)

 

핵심 문제는 .env 를 읽어오지 못하고 있는 것

 

특히 api/route.ts 라우트 함수에서 (즉 Next.js 서버단에서) 사용하는 env 를 읽어오지 못하고 있음

 

 

1. 환경변수에 필요한 env 를 세팅합니다.

 

 

2. 호스팅 - 빌드설정에서 amplify.yml 에서 직접 .env 를 주입하는 것으로 수정

 

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci --cache .npm --prefer-offline
    build:
      commands:
        - echo "MY_ACCESS_KEY=$MY_ACCESS_KEY" >> .env
        - echo "MY_ENDPOINT=$MY_ENDPOINT" >> .env
        
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - .next/cache/**/*
      - .npm/**/*

 

정리할 내용

  • Google Identity 간단 개념 (+ oauth) 👉 링크
  • 구글 라이브러리에서 변경되는 내용 👉 링크
  • authorization 에서 flow  선택 (gsi/client 라이브러리 사용)
  • 라이브러리를 사용하지 않고 google oauth api로 direct 요청하기
  • 토이플젝을 통해서 code → token 얻어내는 과정 및 유의사항
  • 기존 로그인 플로우 & 변경되는 로그인 플로우 설명

 

Direct Using Google OAuth 2.0

  • 다시 한 번 목적을 생각해보자
  • 나는 access_token 과 refresh_token을 사용하여 “로그인”을 이용할 것이다
    • refresh_token을 따로 관리해서 자동로그인 시켜주고 로그인이 거의 영원히 풀리지 않도록 하기 위해서
  • 그리고 지금 라이브러리가 deprecated 되어서 새로운 라이브러리로 대체를 시켜줘야하는 상황
  • 한가지 더 옵션이 있다
  • 신규 라이브러리도 사용하지 않고 “Using OAuth 2.0” → google oauth api 로 direct 로 요청하는 방식 (링크)

 

  • 로그인 flow using google oauth 2.0

 

 

 

Step 1: Configure the client object 

  • If you are directly accessing the OAuth 2.0 endpoints, you can proceed to the next step.
  • 라이브러리를 사용하는경우 client 객체를 init 해주어야 하지만, 다이렉트로 OAuth 2.0 엔드포인트에 접근하는 경우는 필요없다

Step 2: Redirect to Google's OAuth 2.0 server

  • To request permission to access a user's data, redirect the user to Google's OAuth 2.0 server.
  • 이제 직접 authorization(권한 부여) 를 요청한다
  • 핵심
    • oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth";
    •  파라미터 값
      • redirect_uri: "http://localhost:5000/direct-callback"  -> google oauth 서버로 부터 code를 받을 곳 지정
      • response_type: "code" -> token으로 요청하면 바로 access_token 을 준다
      • scope: "email profile" -> 사용할 google api 범위, 이때 지정한 범위 안에서만 access_token으로 요청가능
      • access_type: "offline" -> 이 값을 반드시 지정해야 authorization_code를 token으로 교환할 때 access_token과 함께 refresh_token 도 얻을 수 있다
      • prompt: "consent" -> 이 값을 반드시 지정해야 authorization_code를 token으로 교환할 때 access_token과 함께 refresh_token 도 얻을 수 있다
  • OAuth 에 GET 요청을 하기 위한 코드 구현방법은 두가지가 있다 (더 있을 수도) 둘 중에 마음에 드는것으로 선택하면 된다

 

[Option 1]

Oauth 문서에는 프론트에서 oauth 엔드포인트로 요청을 보낼 때 CORS를 허용하지 않아서 js로 동적으로 form을 만들어서 요청해야한다고, JS Sample을 아래와 같이 제공하고 있다

 

  • direct.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Using Google OAuth 2.0</h1>
    <button onclick="oauthSignIn()">코드 요청</button>
    <script>
      /*
       * Create form to request access token from Google's OAuth 2.0 server.
       */
      function oauthSignIn() {
        var oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth";

        // Create <form> element to submit parameters to OAuth 2.0 endpoint.
        var form = document.createElement("form");
        form.setAttribute("method", "GET"); // Send as a GET request.
        form.setAttribute("action", oauth2Endpoint);

        // Parameters to pass to OAuth 2.0 endpoint.
        var params = {
          client_id:
            "YOUR_CLIENT_ID",
          redirect_uri: "http://localhost:5000/direct-callback",
          response_type: "code",
          scope: "email profile",
          access_type: "offline",
          prompt: "consent",
        };

        // Add form parameters as hidden input values.
        for (var p in params) {
          var input = document.createElement("input");
          input.setAttribute("type", "hidden");
          input.setAttribute("name", p);
          input.setAttribute("value", params[p]);
          form.appendChild(input);
        }

        // Add form to page and submit it to open the OAuth 2.0 endpoint.
        document.body.appendChild(form);
        form.submit();
      }
    </script>
  </body>
</html>

 

[Option 2]

window.location.href 를 통해서 url을 변경시켜주는 것또한 방법이다

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Using Google OAuth 2.0</h1>
    <button onclick="goToGoogleOAuth()">코드 요청</button>
    <script>
      function goToGoogleOAuth() {
        window.location.href = makeGoogleOauthUrl();
      }

      function makeGoogleOauthUrl() {
        var oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth";

        var params =
          "client_id=" +
          encodeURI(
            "YOUR_CLIENT_ID"
          ) +
          "&redirect_uri=" +
          encodeURI("http://localhost:5000/direct-callback") +
          "&response_type=" +
          encodeURI("code") +
          "&scope=" +
          encodeURI("email profile") +
          "&access_type=" +
          encodeURI("offline") +
          "&prompt=" +
          encodeURI("consent");

        return oauth2Endpoint + "?" + params;
      }

    </script>
  </body>
</html>

 

 

Step 3: Google prompts user for consent (생략) 👉 (링크)

 

 

Step 4: Handle the OAuth 2.0 server response

  • 요청을 보내고 나면 redirect_uri: /direct-callback 으로 code 값을 받는다
  • direct-callback.html 에서는 받은 param으로 받은 authorization_code를 localStorage에 저장하고, 다시 루트 (/direct) 로 페이지를 이동시킨다.

 

  • direct-callback.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      const urlParams = new URL(location.href).searchParams;
      const code = urlParams.get("code");
      window.localStorage.setItem("authCode", code);
      window.location.href = "http://localhost:5000/direct";
    </script>
  </body>
</html>

 

 

  • 다시 /direcct로 이동했을 때 로컬스토리지의 authCode 값을 확인하여, 백엔드 서버로 authCode를 보내주어야 한다.
  • direct.html 안에 authCode 로컬 스토리지를 체크하는 코드가 필요하다. 아래 코드를 추가한다.
  • 페이지 로딩시 바로 로컬 스토리지의 authCode를 체크하고, 값을 읽어서 백엔드 /locate API로 authCode를 전송한다
checkLocalStorage();
function checkLocalStorage() {
  const authCode = window.localStorage.getItem("authCode");
  if (authCode !== null) {
    // do server request
    fetch("<http://localhost:5000/locate>", {
      method: "POST",
      body: JSON.stringify({ code: authCode }),
      headers: new Headers({
        "Content-Type": "application/json",
      }),
    })
      .then((res) => res.json())
      .then((data) => {
        console.log(data);
        window.localStorage.setItem(
          "login_token",
          data.login_token
        );
        console.log("do login with login_token");
      });
  }
}

 

Step 5: Exchange for access_token with authorization_code

  • 백엔드 코드 (app.js)
// direct & authorization code flow : get tokens with authorization_code
app.post("/locate", async function (req, res) {
  const options = {
    uri: "https://oauth2.googleapis.com/token",
    method: "POST",
    qs: {
      client_id:
        "YOUR_CLIENT_ID",
      client_secret: "YOUR_CLIENT_SECRET",
      code: req.body.code,
      grant_type: "authorization_code",
      redirect_uri: "http://localhost:5000/direct-callback",
    },
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  };
  let token;
  request.post(options, function (error, response, body) {
    token = JSON.parse(body);
    console.log('token', token)
  });
  res.status(200).json({ login_token: "hello" });
});
  • 프론트로부터 받은 authorization_code를 가지고 oauth2.google 서버에 요청한다
  • redirect_uri 는 최초 요청했을 때 redirect_url 값과 동일하게 적어야한다
  • 받은 token (access_token, refresh_token, id_token)을 통해 유저의 가입유무를 검증하거나, 별도 처리를 하고, 자체적으로 login_token을 만들어 프론트에 응답한다
  • response

정리할 내용

  • Google Identity 간단 개념 (+ oauth) 👉 링크
  • 구글 라이브러리에서 변경되는 내용
  • authorization 에서 flow 선택
  • 라이브러리를 사용하지 않고 googl oauth api로 direct 요청하기
  • 토이플젝을 통해서 code → token 얻어내는 과정 및 유의사항
  • 기존 로그인 플로우 & 변경되는 로그인 플로우 설명

 

변경되는 내용

  • library의 deprecate
  • AS-IS
  • TO-BE
    • authorization 과 authentication 을 분리
      • authentication
        • id_token
        • 구글 Sign In - 단순 로그인이 필요한 경우 사용
        • google 에서 제공하는 로그인 버튼, 자동로그인, One-Tap 키트를 제공 → 어떤 사이트든 유저가 동일한 UX를 경험할 수 있다
        • credentialResponse → JWT (id_token)
        {
        	"clientId": "538344653255-758c5h5is6to.apps.googleusercontent.com",
        	"credential": "eyJhbGciOizA1Y2wiZW1haWIjoiNTM4MzQ0_UO6OCRQ",
        	"select_by": "user"
        }
          
      • authorization
        • access_token
        • 구글 서비스 API 가 필요한 경우 사용 (ex) 구글 드라이브, 구글 메일 등)
        • 또는 access_token & refresh_token 을 사용한 로그인 방식을 사용하고 싶은 경우
        {
          "access_token": "ya29.A0ARrdaM_LWSO-hEr_FAVqf92sZZHphr0g",
          "token_type": "Bearer",
          "expires_in": 3599,
          "scope": "<https://www.googleapis.com/auth/calendar.readonly>"
        }

 

  • library 정리
    • implicit flow? authorization_code flow? 일단은 몰라도 되고, 그저 하나의 라이브러리로 통합되었다는 것만 확인하면 된다
library (deprecated) New library  Note
apis.google.com/js/platform.js accounts.google.com/gsi/client Add new library for authentication(id_token)
apis.google.com/js/api.js accounts.google.com/gsi/client Add new library for authorization(access_token) and follow the implicit flow.
apis.google.com/js/client.js accounts.google.com/gsi/client Add new library for authorization(access_token) and the authorization code flow.

정리할 내용

  • Google Identity 간단 개념 (+ oauth) 👉 링크
  • 구글 라이브러리에서 변경되는 내용 👉 링크
  • authorization 에서 flow  선택 (gsi/client 라이브러리 사용)
  • 라이브러리를 사용하지 않고 googl oauth api로 direct 요청하기
  • 토이플젝을 통해서 code → token 얻어내는 과정 및 유의사항
  • 기존 로그인 플로우 & 변경되는 로그인 플로우 설명

 

Google Identity Options(선택지)

이번 포스팅에서는 Sigin + API (목적은 access_token으로 구글 API 사용하기) -> 새로운 라이브러리 gsi/client 를 사용하는 구현방법에 대해서 정리해보려 한다.

 

authorization 의 flow

  • 지금 파트가 어디(authentication / authorization) 인지 다시 한 번 확인하는게 좋다 (자꾸 헷갈림)
  • 지금은 authorization : 권한 부여 파트이다
  • authorization 의 목적은 access_token을 얻어내는것
  • 여기까지 하고, authorization 에는 2가지 구현 방식이 있다 (링크)
    • implicit flow
    • authorization code flow
  • 각 flow의 특징, 장단점을 고려하여 본인에게 맞는걸 고른다

 

1. implicit flow

  • initTokenClient 메소드를 사용한다
  • 최종 목적인 access_token을 바로 프론트에서 callback으로 받는다
const client = google.accounts.oauth2.initTokenClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: '<https://www.googleapis.com/auth/calendar.readonly>',
  callback: (response) => {
    ...
  },
});

 

  • 실제 response
{
	access_token : "ya29.a0AX9GBOb_lSF_zh------WGisUOHP52EkyhtLRIyMOA0163",
	authuser : "0", 
	expires_in : 3599,
	hd : "kr.hello.com",
	prompt : "none",
	scope : "email profile <https://www.googleapis.com/auth/contacts.readonly>",
	token_type : "Bearer"
}

 

전체 코드 보기👇

더보기

implicit.html

<!DOCTYPE html>
<html>
  <head>
    <script
      src="https://accounts.google.com/gsi/client"
      onload="initClient()"
      async
      defer
    ></script>
  </head>
  <body>
    <script>
      var client;
      var access_token;

      function initClient() {
        client = google.accounts.oauth2.initTokenClient({
          client_id:
            "174709277496-j4------------8i8g6agnffjj.apps.googleusercontent.com",
          scope: "email profile",
          callback: (response) => {
            console.log("google oauth", response);
            access_token = response.access_token;
            fetch("http://localhost:5000/implicit-callback", {
              method: "POST",
              body: JSON.stringify(response),
              headers: new Headers({
                "Content-Type": "application/json",
              }),
            }).then((res) => console.log(res));
          },
        });
      }
      function getToken() {
        client.requestAccessToken({ prompt: "" });
      }
      function revokeToken() {
        google.accounts.oauth2.revoke(access_token, () => {
          console.log("access token revoked");
        });
      }

      function loadPeople() {
        var xhr = new XMLHttpRequest();
        xhr.open(
          "GET",
          "https://people.googleapis.com/v1/people/me?personFields=emailAddresses,names"
        );
        xhr.setRequestHeader("Authorization", "Bearer " + access_token);
        xhr.send();
      }
    </script>
    <h1>Google Identity Services Authorization Token model</h1>
    <button onclick="getToken();">Get access token</button><br /><br />
    <button onclick="revokeToken();">Revoke token</button>

    <button onclick="loadPeople();">Use access token</button>
  </body>
</html>
  • requestAccessToken() 메소드를 사용하여 access_token 요청
  • requestAccessToken() 메소드에서 특정 params 값을 재설정 할 수 있다
    • prompt 를 빈 스트링(””) 값을 넣으면 동의항목에 대해서 한번만 물어본다
  • 브라우저로 받은 access_token을 브라우저에서 바로 사용할 수 있다
  • loadPeople() 함수에서 헤더에 access_token을 담아서 people api를 사용할 수 있다

app.js

// node_modules 에 있는 express 관련 파일을 가져온다.
var express = require("express");
var bodyParser = require("body-parser");
const { google } = require("googleapis");
const url = require("url");
var request = require("request");

// express 는 함수이므로, 반환값을 변수에 저장한다.
var app = express();
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());

// 5000 포트로 서버 오픈
app.listen(5000, function () {
  console.log("start! express server on port 5000");
});

// localhost:5000/implicit 브라우저에 res.sendFile() 내부의 파일이 띄워진다.
app.get("/implicit", function (req, res) {
  res.sendFile(__dirname + "/public/implicit.html");
});

// implicit flow : get user info with access_token
app.post("/implicit-callback", async function (req, res) {
  console.log("implicit-callback");
  const options = {
    uri: "https://people.googleapis.com/v1/people/me?personFields=emailAddresses,names",
    method: "GET",
    headers: {
      Authorization: "Bearer " + req.body.access_token,
    },
  };
  request.get(options, function (error, response, body) {
    if (error) console.log(error);
    console.log(response.statusCode);
    console.log(body);
  });
  res.status(200).send("ok");
});
  • 프론트에서 /implicit-callback 으로 넘긴 access_token을 사용하여 people api를 이용할 수도 있다
  • 백엔드 콘솔 response

 

2. authorization code flow

  • initCodeClient 메소드를 사용한다
  • 구글 계정을 선택하는 UX를 Popup 창을 띄우거나, 페이지를 redirect 시킬 수 있다
  • authorization code를 먼저 받고 → 그 코드를 가지고 한번 더 google 에 요청하여 최종 목적인 access_token을 받는 방식이다
  • popup의 경우 callback 함수의 response로 authorization_code를 받는다
const client = google.accounts.oauth2.initCodeClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: '<https://www.googleapis.com/auth/calendar.readonly>',
  ux_mode: 'popup',
  callback: (response) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', code_receiver_uri, true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    // Set custom header for CRSF
    xhr.setRequestHeader('X-Requested-With', 'XmlHttpRequest');
    xhr.onload = function() {
      console.log('Auth code response: ' + xhr.responseText);
    };
    xhr.send('code=' + response.code);
  },
});

 

  • redirect의 경우 미리 지정해둔 redirect uri로 response 로 authorization_code 를 받는다
const client = google.accounts.oauth2.initCodeClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: '<https://www.googleapis.com/auth/calendar.readonly>',
  ux_mode: 'redirect',
  redirect_uri: "<https://your.domain/code_callback_endpoint>",
  state: "YOUR_BINDING_VALUE"
});

 

  • 실제 response
{
  code: '4/0AWgavdf--------ucnY14Ud_qNu9A37g',
  scope: 'email profile <https://www.googleapis.com/auth/calendar.readonly> <https://www.googleapis.com/auth/contacts.readonly> <https://www.googleapis.com/auth/drive.metadata.readonly> <https://www.googleapis.com/auth/calendar.events.readonly> openid <https://www.googleapis.com/auth/userinfo.profile> <https://www.googleapis.com/auth/userinfo.email>',
  authuser: '0',
  hd: 'kr.hello.com',
  prompt: 'consent'
}

 

  • 받은 authorization_code를 이용하여 access_token을 발급받아야한다
POST /token HTTP/1.1
Host: oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

code=4/P7q7W9---------IaQm6bTrgtp7&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=https%3A//mysite.example.com/code&
grant_type=authorization_code

 

  • response 를 확인하면 access_token, refresh_token, id_token 가 있다

 

전체 코드 보기👇

 

더보기

authorizationCode.html (popup 모드)

<!DOCTYPE html>
<html>
  <head> </head>
  <body>
    <script>
      var client;
      function initClient() {
        client = google.accounts.oauth2.initCodeClient({
          client_id:
            "174709277496-j4nblfbu-------agnffjj.apps.googleusercontent.com",
          scope: "email profile",
          ux_mode: "popup",
          callback: (response) => {
            console.log(response);
            fetch("http://localhost:5000/callback", {
              method: "POST",
              body: JSON.stringify(response),
              headers: new Headers({
                "Content-Type": "application/json",
              }),
            }).then((res) => console.log(res));
          },
        });
      }
      // Request an access token
      function getAuthCode() {
        // Request authorization code and obtain user consent
        client.requestCode();
      }
    </script>
    <button onclick="getAuthCode();">requestCode</button>
    <!-- //구글 api 사용을 위한 스크립트 -->
    <script
      src="https://accounts.google.com/gsi/client"
      onload="initClient()"
      async
      defer
    ></script>
  </body>
</html>
  • requestCode() 메소드를 사용하여 authorization_code를 요청
  • popup 모드로 한 경우 callback 함수의 response 로 받는다
    • 받은 response를 그대로 백엔드 /callback 으로 보낸다

app.js (popup 모드)

  • 프론트에서 post로 보냈기 때문에 post로 받는다
  • google oauth 로 보낼때 아래와 같은 쿼리스트링을 맞춰주어야 한다
  • Q) redirect_uri 값을 보내는 곳과 맞춰줘야하는지? 확실히 모르겠다
    • 백엔드에서 저기로 페이지를 이동시켜주는 것은 아님 확인
// authorizaion code using popup mode relay endpoint
app.post("/callback", async function (req, res) {
  console.log("body", req.body);
  const options = {
    uri: "https://oauth2.googleapis.com/token",
    method: "POST",
    qs: {
      code: req.body.code,
      client_id:
        "174709277496-j4n---------a8i8g6agnffjj.apps.googleusercontent.com",
      client_secret: "GOCSPX-lO8--------------_Atdn",
      redirect_uri: "http://localhost:5000",
      grant_type: "authorization_code",
    },
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  };
  let token;
  request.post(options, function (error, response, body) {
    if (error) console.log(error);
    token = JSON.parse(body);
    console.log(token);
    res.status(200).send(token.access_token);
  });
});
  • backend response
    • access_token, refresh_token, id_token 전체 받아옴 확인

 

authorizationCode.html (redirect 모드)

<!DOCTYPE html>
<html>
  <head> </head>
  <body>
    <script>
      var client;
      function initClient() {
        client = google.accounts.oauth2.initCodeClient({
          client_id:
            "174709277496-j4nb------------g6agnffjj.apps.googleusercontent.com",
          scope: "email profile",
          ux_mode: "redirect",
          redirect_uri: "http://localhost:5000/callback",
        });
      }
      // Request an access token
      function getAuthCode() {
        // Request authorization code and obtain user consent
        client.requestCode();
      }
    </script>
    <button onclick="getAuthCode();">requestCode</button>
    <!-- //구글 api 사용을 위한 스크립트 -->
    <script
      src="https://accounts.google.com/gsi/client"
      onload="initClient()"
      async
      defer
    ></script>
  </body>
</html>
  • requestCode() 메소드를 사용하여 authorization_code를 요청
  • redirect 모드로 한 경우 지정한 redirect_uri 로 구글 oauth 서버에서 바로 보내준다

app.js (redirect 모드)

// authorization code using redierct mode redirect callback
app.get("/callback", async function (req, res) {
  let q = url.parse(req.url, true).query;
  // Get access and refresh tokens (if access_type is offline)
  const options = {
    uri: "https://oauth2.googleapis.com/token",
    method: "POST",
    qs: {
      code: q.code,
      client_id:
        "174709277496-j4---------------agnffjj.apps.googleusercontent.com",
      client_secret: "GOCS------------------zaGRA_Atdn",
      redirect_uri: "http://localhost:5000/callback",
      grant_type: "authorization_code",
    },
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  };
  let token;
  request.post(options, function (error, response, body) {
    if (error) console.log(error);
    token = JSON.parse(body);
    console.log(token);
    res.status(200).send(token.access_token);
  });
});
  • 쿼리파라미터로 받기 때문에 url을 파씽하여 code(authorization_code) 값을 걸러낸다
  • google oauth 서버에서 “GET” 요청으로 보내준다
  • 걸러낸 코드를 oauth 서버에 access_token 으로 바꿔달라고 요청한다
  • redirect 모드에서 서버쪽 “redirect_uri” 는 반드시 최초 프론트에서 요청했던 redirect_uri와 같은 값이어야 한다 → popup 모드에서와는 다르게 동작하는 것으로 보임
  • 백엔드 response

 

 

+ Recent posts