목표 : React 에서 무한 스크롤 구현하기 (Infinity Scroll in React)

여정 : scroll Event Listener -> Throttle 로 최적화 -> Intersection Observer API 사용

 

1. 단순 구현

  • 스크롤 이벤트를 참조하여 다음 데이터 API 호출

코드 구현

  • 가정
    • fetchData() 는 API 를 요청하는 함수
    • 예제에서는 console.log('fetch API 요청') 찍히는 걸로 API 요청을 대신함
useEffect(() => {
	const handleScroll = () => {
		const { scrollTop, offsetHeight } = document.documentElement;
		if (window.innerHeight + scrollTop >= offsetHeight) {
			// 스크롤 위치 바닥일때 데이터 페치
			fetchData() // -> API 요청하는 함수
			console.log('fetch API 요청');
		}
	};
	// 화면 진입시 데이터 페치
	fetchData() // -> API 요청하는 함수
	console.log('fetch API 요청');
	window.addEventListener('scroll', handleScroll);
	return () => window.removeEventListener('scroll', handleScroll);
}, []);
  • 개선 포인트
    • documentElement.scrollTop과 documentElement.offsetHeight는 리플로우(Reflow)가 발생하는 참조이므로 API를 과다로 요청하는 경우 발생 가능

 

2. 쓰로틀 (Throttle) 적용

  • 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
  • 동일 이벤트가 반복적으로 시행되는 경우 이벤트의 실제 반복 주기와 상관없이 임의로 설정한 일정 시간 간격(밀리세컨드)으로 콜백 함수의 실행을 보장

코드 구현

  1. 기본 Throttle 함수(ver1)
function throttle(callback, delay = 300) {
	let waiting = false

	return (...args) => {
		if (waiting) return

		callback(...args)
		waiting = true
		setTimeout(() => {
			waiting = false
		}, delay)
	}
}
  • callback: 실행 대상이 되는 콜백 함수
  • delay : 얼마 간격으로 함수를 실행할 지 결정하며 millisecond 단위, 기본값으로 0.3초를 주었음
  • waiting 상태가 true 인 경우 실행 없음
  • 최초 함수가 호출되었을 때 waiting은 false
  • 이 때 콜백 함수를 실행한 뒤 다시 waiting은 true가 됩니다.
  • waiting이 true가 되었을 때 delay 밀리초 후에는 waiting이 강제로 false가 되고, 다시 콜백 함수가 실행이 됩니다.

1-2. 기본 Throttle 함수(ver2)

function throttle(callback, delay = 3000) {
	let timer;
	return (...args) => {
		if (timer) return;
		timer = setTimeout(() => {
			callback(...args);
			timer = null;
		}, delay);
	};
}
  • timer 를 생성하는 역할
  • 생성해둔 timer 가 있으면, 콜백함수 실행 없이 return
  • timer 가 없으면, timer 를 만듦
  • 만든 timer 는 delay 시간 이후에 콜백 함수를 실행하는 타이머

⇒ 두가지 throttle 함수를 각각 적용해봤을 때 동작에 차이가 발생했음

 

2. handleScroll 함수에 throttle 함수를 적용

useEffect(() => {
	const handleScroll = throttle(() => {
		const { scrollTop, offsetHeight } = document.documentElement;
		if (window.innerHeight + scrollTop >= offsetHeight) {
			// 스크롤 위치 바닥일때 데이터 페치
			console.log('fetch API 요청');
		}
	});
	// 화면 진입시 데이터 페치
	console.log('fetch API 요청');
	window.addEventListener('scroll', handleScroll);
	return () => window.removeEventListener('scroll', handleScroll);
}, []);
  • ver2 throttle 함수의 경우 : 정상 동작
    1. 스크롤 바닥
    2. 3000ms 대기
    3. timer (setTimeout) 생성
    4. callback 함수 실행
    5. timer 해제
    6. 스크롤 위로 갔다가 다시 바닥
    7. 3000ms 대기
  • ver1 throttle 함수의 경우 이상 동작
    • 함수의 코드 순서를 확인해 보자
      • throttle 함수 ver1
      function throttle(callback, delay = 3000) {
      	let wait = false;
      
      	return (...args) => {
      		if (wait) {
      			return;
      		}
      
      		callback(...args);
      		wait = true;
      		setTimeout(() => {
      			wait = false;
      		}, delay);
      	};
      }
      
      • 이상동작 원인
        • 일단 스크롤 이벤트가 생길 때 마다 callback 함수가 실행되는데,
        • 스크롤 이벤트가 트리거 되는 시작점의 스크롤 위치 값을 들고 감
        • if (window.innerHeight + scrollTop >= offsetHeight) {
        • 그래서 스크롤 바닥 조건에 걸리지 않음
        • fetch 함수를 실행하지 않음
  • 화면 예시
    • 한번에 끝까지 스크롤 한 상태
    • callback 함수가 호출됨

  • scrollTop 위치를 “3” 으로 물고 있음
  • fetch 할 조건 (scrollTop이 아래에 위치해 있는가?) 에 해당되지 않아서 fetch 함수가 실행되지 않는다
  • delay 시간 이후에 callback 함수를 실행해야 원하는 scrollTop 값으로 조건체크를 할 수 있음

  • 함수 변형
function throttle(callback, delay = 3000) {
		let wait = false;

		return (...args) => {
			if (wait) {
				return;
			}
			wait = true;
			setTimeout(() => {
				callback(...args);
				wait = false;
			}, delay);
		};
	}

  • delay 시간 이후 callback 함수가 호출 되어서 최신 scrollTop 위치값을 가져올 수 있

 

⇒ 스크롤 이벤트 특성상 delay 이후에 콜백함수가 실행되는 것이 정상 동작이었으나, 케이스에 따라 콜백을 먼저 실행하고, delay 가 있어야 하는 경우도 있을 것

 

3. Intersection Observer API (체신 문법 적용^^)

  • Intersection Observer API 적용
  • 교차 관찰자 API
  • 브라우저에서 제공하는 API
  • 크롬 51버전부터 사용가능
  • 위에서 확인했듯이, 스크롤 이벤트는 일단 스크롤이 발생하면 트리거됨
  • 우리가 원하는 것은 스크롤이 아래에 도달했을 때만 트리거 시키고 싶음

코드 구현

  • new IntersectionObserver() 생성자 표현식을 사용하여, observer 객체를 생성하여 사용
  1. 생성
    • 교차될때 실행할 콜백함수를 제공해야 한다 → fetchData 함수
    • options 로 root / rootMargin / threshold 값을 커스텀 할 수 있음
  2. 설정
    • options 값 커스텀
      • root : 대상이 되는 객체의 가시성을 확인 할 때 사용하는 뷰포트 요소, 기본값은 브라우저 뷰포트

      • rootMargin : root 가 가진 여백, 교차성을 개산하기 전에 적용됨

      • threshold : observer의 콜백함수가 실행될 대상 요소의 가시성 퍼센테이지, 10%만 보여도 수행하는가?, 100% 다보였을때 실행하는가

 

3. 관찰대상 지정하기

  • 요소를 observer.observe 메소드에 등록해주면 된다
let target = document.querySelector('#listItem'); 
observer.observe(target);

4. 콜백함수 구현하기

  • 콜백함수 기본 args
let callback = (entries, observer) => {
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};
  • IntersectonObserverEntry 리스트를 첫번째 인자로 받고, observer 자체를 두번째 인자로 받는다
  • React 용 useIntersect hook
const { useRef, useCallback, useEffect } = require('react');

const useIntersect = (onIntersectFn, options) => {
	const ref = useRef(null); // 1
	const callback = useCallback(
		(entries, observer) => {
			entries.forEach((entry) => {
				if (entry.isIntersecting) onIntersectFn(entry, observer);
			});
		},
		[onIntersectFn]
	);

	useEffect(() => {
		if (!ref.current) return;
		const observer = new IntersectionObserver(callback, options); // 2
		observer.observe(ref.current); // 3
		return () => observer.disconnect();
	}, [ref, options, callback]);

	return ref;
};

export default useIntersect;
  1. 관찰 타겟 ref 생성
  2. observer 생성
  3. observer.observe 에 관찰 타겟 ref 연결
  4. ref unmount 시 observer disconnect 시키기 (삭제)
  5. callback 함수 선언
    • entry 객체 내부 key

    • entry 가 isIntersecting true 인터섹트 트리거 되면,
    • onIntersectFn 함수를 실행
    • onIntersectFn은 useIntersect 훅 선언시 전달 → fetchData 함수
  • useIntersect 훅 사용부분
    • 핵심 내용은 fetchData 함수를 실행해야함
    • 타겟에 대한 관찰을 멈출지는 상황에 따라 판단
const ref = useIntersect(async (entry, observer) => {
	// observer.unobserve(entry.target);
	// if(hasNextPage && !isFetching) {
	// 	console.log('fetchNextPage');
	// }
	console.log('fetchNextPage');
});

 

4. 폴리필

  • safari 에서 지원하고 있지 않음 폴리필 필요

https://github.com/GoogleChromeLabs/intersection-observer

https://github.com/w3c/IntersectionObserver

$ npm i intersection-observer
import 'intersection-observer'

const io = new IntersectionObserver(콜백함수, 옵션)

const 관찰대상 = document.querySelectorAll(관찰대상)

// IE에서 사용하기
Array.prototype.slice.call(관찰대상).forEach(elim => {
  io.observe(elim)
})

 

참고

 

 

로그인 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 돌려놓기

 

1. npm 설치

npm install sass -g

2. 버전 정보 확인

설치 완료 됐으면 버전확인하면서 잘 설치되었는지 확인합니다.

npm show sass version

3. Vs code에서 확장 프로그램 설치

SASS 컴파일을 자동으로 해주는 플러그인

  1. live Sass compiler - 만든 이 : Glenn Marks

4.  sass complier 경로 설정

1. vscode 좌측 톱니바퀴 - 설정 클릭

2. 설정 json 파일 수정을 위해 우측 상단 설정 열기 클릭

3. 아래 구문 추가

{
  "liveSassCompile.settings.formats": [
    // This is Default.
    {
      "format": "expanded",
      "extensionName": ".css",
      "savePath": "~/../../../public/css"
    },
    {
      "format": "compressed",
      "extensionName": ".min.css",
      "savePath": "~/../../../public/css"
    }
  ],
  // "liveSassCompile.settings.includeItems": [
  //   "www/assets/scss/**/*.scss",
  // ],
  "liveSassCompile.settings.generateMap": false,
  "liveSassCompile.settings.autoprefix": [
    "> 0.5%",
    "last 2 versions",
    "Firefox ESR",
    "not dead"
  ]
}

가장 중요한 것은 savePath 이다

본인의 프로젝트 경로에 맞게 설정을 해주어야 한다

 

나의 경우 리액트로 작업을 하고 있고

/

public/css/ : 여기가 sass 가 컴파일된 css 파일이 모이는 곳이다 (결과물)

src/assets/scss/user.scss : 여기서 내가 필요한 scss 코드를 작성 (원본)

 

savePath 를 현재 경로에 맞게 잘 설정을 해준다

  • 프론트 : localhost:3000 띄워서 로컬에서 개발
  • API서버(백) : http://~.com dev서버를 올려서 해당 dev 서버에 요청

 

  • 브라우저에서 보내는 요청 → CORS 발생
    • 브라우저는 localhost:3000을 달고 있는데, 다른 주소(API서버)로 부터 온 응답값을 허용하지 않음 (동일출처가 아니기 때문)
  • 해결책 : 브라우저에서 나가는 요청을 API 서버 주소로 둔갑시킨다
    • proxy 설정

 

  • http-proxy-middleware 패키지 설치
  • src/setupProxy.js
    • api/ 로 시작하는 요청 url 인 경우 설정한 target 주소에서 요청하는 것으로 둔갑해줌
    • /api 부분은 빠져야 하므로 pathRewrite 도 시켜준다
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = (app) => {
    app.use(createProxyMiddleware('/api', {
        target: 'http://myservices.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
    })
    );
}
  • api 요청 부
import axios from 'axios';

export const postLogin = ({ id, password }) => {
	return axios.post('/api/auth/access_token', {
		id,
		password,
	});
};

https://12716.tistory.com/entry/ReactJS-CORS이슈-해결을-위한-Proxy-설정

 

[ReactJS] CORS이슈 해결을 위한 Proxy 설정

앞선 포스팅에서 프론트서버의 3000번포트에서 백엔드서버인 5000번포트로 요청을 보낼때 CORS policy에 의해서 막혔다라는 에러가 발생했었는데요. 서버는 포트가 5000번이고 클라이언트는 3000번으

12716.tistory.com

 

 

운영에 프론트를 배포하고 나서 동일한 코드가 어떻게 동작하는 지 확인

일단 운영에서는 프록시 설정이 작동하지 않는다

http-proxy-middleware 패키지는 자동으로 개발환경에서만 작동하도록 세팅되어있다

 

운영환경에서는 그럼 CORS 에러가 발생하지 않는가?

 

기본적으로 알고 있어야할 개념이 있다

서브 도메인(Sub Domain)

서브 도메인은 보조 도메인으로써, URL로 전송하거나 계정 내의 IP 주소나 디렉토리로 포워딩되는 도메인 이름의 확장자이다.

예를들어 네이버는 여러 서비스들을 아래와 같은 서브도메인을 통해 사용자가 접근할 수 있도록 한다.

  • 네이버 블로그 : blog.naver.com
  • 네이버 메일 : mail.naver.com
  • 네이버 금융 : finance.naver.com
  • https://app.jakearchibald.com
  • https://other-app.jakearchibald.com

프론트 주소가 myapp.com 

API 서버 주소가 api-myapp.com

 

위와 같으면 CORS 발생하지 않음

 

당연히 이런 개념을 알고, 주소를 팠겠거니,,, 한다면,

운영에서는 

 

 

Proxy 설정

  • 로컬 개발에서 설정
    1. 프록시 (/api) 로 설정
      • login.js : axios 요청부
      import axios from 'axios';
      
      export const postLogin = ({ id, password }) => {
      	return axios.post('/api/auth/access_token', {
      		id,
      		password,
      	});
      };
      
      • setupProxy.js
        • 프록시 설정에서 target 값을 주면 axios 에서 baseURL 설정을 안해도 됨
      const { createProxyMiddleware } = require('http-proxy-middleware');
      
      module.exports = function (app) {
      	app.use(
      		'/api',
      		createProxyMiddleware({
      			target: 'http://myapp-api.myapp.com/>',
      			changeOrigin: true,
      			pathRewrite: { '^/api': '' },
      		})
      	);
      };
      • 결과
        • 서버로 요청이 정상적으로 갔고, 서버로부터 응답을 받음
        • Request URL : localhost:3000/api/auth/access_token

 => 로컬) 요청 → localhost:3000 → 프록시 서버 → 프록시에 설정한 target 서버로 요청

 

 

 

 

2. 프록시('/api/~') 설정 + axios baseURL 변경

  • login.js
export const postLogin = ({ id, password }) => {
	return axiosInstance.post('/auth/access_token', {
		id,
		password,
	});
};
  • axiosInstanse.js
const axiosInstance = axios.create({
	baseURL: 'http://myapp-api.myapp.com',
	headers: {
		'Content-Type': 'application/json',
	},
});
  • setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
	app.use(
		'/api',
		createProxyMiddleware({
			target: 'http://myapp-api.myapp.com/>',
			changeOrigin: true,
			pathRewrite: { '^/api': '' },
		})
	);
};
  • 결과
    • 서버로부터 정상(여기서는 401이 정상임, 잘못된 계정을 입력했기 때문에 unauthorized, 내가 받고싶었던 응답) 응답을 받음

  • 의문점 : 근데 /api~로 시작을 안했는데.. CORS 가 어떻게 통과 되었는가?
  • 로컬에서 프록시 설정을 빼보자-> 잘됨... 백엔드에서 전체 받아주는거로 처리했나..?

 

 

백에서 처리를 해주었다면, 프론트에서 프록시 처리하지말고, 그냥 axios baseURL 만 바꺼서 보내면 된다

 

 

  •  

 

  • 로그인시 받는 유저 데이터 recoil state 는 새로고침시 날아감
    1. 새로고침시 마다 유저 정보를 API 요청 → XXXXXX
    2. recoil state 를 persist
      • localStorage를 사용하는 방법이 있음
        • 새로고침시 localStorage에 저장해둔 값을 state에 다시 입력
        • localStorageEffect.js
        function localStorageEffect(key) {
        	return ({ setSelf, onSet }) => {
        		const savedValue = localStorage.getItem(key);
        		if (savedValue != null) {
        			setSelf(JSON.parse(savedValue));
        		} else {
        			// 로컬 스토리지에 저장된 값이 없으면 로그아웃 처리
        			localStorage.removeItem(key);
        			localStorage.removeItem('acccessToken');
        			localStorage.removeItem('refreshToken');
        			if (window.location.pathname !== '/login') {
        				window.location.href = '/login';
        			}
        		}
        
        		onSet((newValue, _, isReset) => {
        			isReset
        				? localStorage.removeItem(key)
        				: localStorage.setItem(key, JSON.stringify(newValue));
        		});
        	};
        }
        
        export default localStorageEffect;
        
        • store/recoil/index.js
        export const userInfoState = atom({
        	key: 'userInfoState',
        	default: {
        		id: 'M12341234',
        		name: '김철수',
        		accessLv: 3,
        	},
        	effects: [localStorageEffect('user')],
        });
        

 

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

 

 

구글로 로그인 시키는 방법은 잘 정리된 블로그들 많으니 참고

(필요 선행작업 : 구글 클라우드 플랫폼에서 클라이언트 ID 발급받기)

 

잘 쓰고 있던 구글 로그인 js 라이브러리가 deprecated 된다고 한다.

바꿔줘야한다.

 

바꿔주는 김에 OAuth 개념 정리

 

 

정리할 내용

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

 

Google Identity

  • OAuth 2.0 개념
    • 인증을 위한 개방형 표준 프로토콜(약속)
    • 여러 서비스들(카카오, 구글, 네이버 등) 에서 통일된 Identity 체계를 사용하기 위해 모색된 개념
  • 사전에 꼭 정리해야하는 용어
    • Authentication
      • 유저의 신원 인증
      • id_token : 유저의 정보가 담긴 토큰
    • Authorization
      • 유저가 요청한 권한 부여
      • access_token : 이 토큰으로 필요한 서비스에 데이터 요청을 할 수 있다
  • UseCase
    1. 구글 계정으로 회원가입 및 로그인
    2. 구글 계정으로 회원가입 및 로그인 + 구글 API 사용(ex) 구글 캘린더, 구글 포토, 구글 드라이브 접근 권한 등)

 

[UseCase.2 의 플로우]

 

 

최대한 간단한 플로우를 먼저 개념잡고, 디테일하게 들어가는게 좋다

 

  1. 유저는 구글계정으로 로그인하여  authentization(신원확인)을 받는다. 그 결과로 authorization_code를 받는다.
  2. 받았던 authorization_code를 가지고, authorization(권한부여)를 받는다. 그 결과로 access_token을 받는다
  3. access_token을 가지고, 필요한 서비스의 데이터를 요청

+ Recent posts