구글 로그인을 구현하고 있었습니다.
아무 런타임 에러 없이 정상적으로 유저의 인증이 완료되고,
지정한 redirect uri로 리다이렉팅까지 정상 동작하고 있었습니다.
하지만, 인증된 유저에게 발급된 토큰을 정상적으로 받아오지 못하는 logical error가 발생하고 있었습니다.
저의 경우, 구글 로그인이 정상적으로 이루어지면 프론트엔드 단에서 아래의 코드가 수행되도록 프로그램을 작성하였습니다.
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const AuthCallback = () => {
const navigate = useNavigate();
useEffect(() => {
// URL 쿼리에서 인증 정보 추출
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("access_token"); // 액세스 토큰
console.log(token);
if (token) {
localStorage.setItem("authToken", token);
navigate("/home/");
} else {
console.error("No token found");
}
}, [navigate]);
return (
<div>
<h1>로그인 완료! 페이지는 곧 작성할 예정</h1>
</div>
);
};
export default AuthCallback;
그런데 계속하여 아래와 같이 토큰을 url 쿼리에서 찾아오지 못하고 있었습니다.
"no token found" 는 아래와 같이 url 쿼리의 access_token
매개변수에 저장된 값이 비어있을 경우 출력되는 문자열이기 때문입니다.
아래는 해당 부분에 대한 코드 스니펫으로, 위에 첨부한 코드의 일부분입니다.
if (token) {
localStorage.setItem("authToken", token);
navigate("/home/");
} else {
console.error("No token found");
}
이렇게, 로그인이 정상적으로 완료되면 보여줄 페이지가 잘 로드되는 것을 보면
구글 계정을 이용한 OAuth작업은 정상적으로 수행되었다는 것을 알 수 있습니다.
하지만 계속해서 토큰을 찾을 수 없다고 합니다.
로컬 스토리지에도 아무런 엑세스 토큰이 저장되지 않은 것을 확인할 수 있습니다.
구글 로그인이 완료된 이후의 url은 아래와 같은 형식이었습니다.
http://localhost:3000/auth/callback/#state=TSCiJOQTjFhtXbgm&access_token=ya29.a0AXooCgvDk5kuaXensOloKAQ1QPVsJGCD1RW0kaJ4F1 이후 생략
중요한 것은, &access_token=
매개변수가 url에 분명히 존재하고 있다는 것입니다.
그럼에도 아래 코드에서 token
의 값은 계속하여 null
이었습니다.
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("access_token");
console.log(token); //null
문제는 해당 access_token
매개변수가 url의 해시 부분에 속해있었기 때문입니다(fragment identifier 라는 이름으로 불리기도 합니다).
URLSearchParams
는 url의 쿼리 부분의 매개변수들을 받아오지만,
해쉬 부분에 있는 매개변수들은 가져오지 않습니다.
여기서, URL의 해시 부분이란 무엇일까요?
URL에서의 해시란, #
뒤에 나와있는 모든 부분을 의미합니다.
위에서 확인했던 리다이렉팅 된 후의 url을 다시 살펴보면,
http://localhost:3000/auth/callback/#
로컬호스트 주소 뒤에, ?
가 아닌 #
이 나와있습니다.
그렇기에, 로컬호스트 주소 뒤의 모든 매개변수들은 URL해시에 속해있는 것이고,
URLSearchParams
로 해당 값에 접근할 수 없었던 것입니다.
URLSearchParams
는 ?
뒤에 이어지는 쿼리 매개변수들에 접근할 수 있지만, 해시 부분엔 접근할 수 없습니다.
이것이 .get("access_token");
의 값이 null
인 이유입니다.
URLSearchParams
로 access_token
매개변수에 접근하기 위해서는, #
기호를 제거해주면 됩니다.
아래와 같은 코드를 통해 URL의 해쉬부분에 접근하여 #
문자를 제거할 수 있습니다.
const hash = window.location.hash.substring(1); // '#'을 제거
const urlParams = new URLSearchParams(hash);
const token = urlParams.get("access_token"); // 액세스 토큰
console.log(token); //값 정상 저장
window.location.hash
를 통해 URL의 해시부분에 접근할 수 있습니다.
URL해시부분은 #문자부터 시작되기에, #에 접근하기 위해선 window.location.search
가 아닌 .hash
로 접근해야 합니다.
이후 substring(1)
메소드를 통해 해시부분의 0번 문자열(#)을 지워줍니다.
이제 #이 지워졌기에, 로컬호스트 주소 뒤의 부분은 더이상 해시가 아니며,
URLSearchParams
로 자유로이 접근하여 매개변수를 조작할 수 있습니다.
아래는 수정된 전체 코드입니다.
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const AuthCallback = () => {
const navigate = useNavigate();
useEffect(() => {
// URL에서 인증 정보 추출
const hash = window.location.hash.substring(1); // '#'을 제거한 부분
const urlParams = new URLSearchParams(hash);
const token = urlParams.get("access_token"); // 액세스 토큰
console.log(token);
if (token) {
localStorage.setItem("authToken", token);
navigate("/home/");
} else {
console.error("No token found");
}
}, [navigate]);
return (
<div>
<h1>로그인 완료! 페이지는 곧 작성할 예정</h1>
</div>
);
};
export default AuthCallback;
URL hash(fragment identifier)
에 대해 조금 자세히 알아보겠습니다.
fragment identifier
는 HTML에서 문서내부를 이동하기 위해 사용됩니다.
하이퍼링크의 경로에 #원하는 id
를 적어주면, 해당 id값을 가진 tag로 문서 내 화면을 이동시킵니다.
<p id="1">
동해물과 백두산이 마르고 닳도록<br />
하느님이 보우하사 우리나라 만세<br />
무궁화 삼천리 화려강산<br />
대한사람 대한으로 길이 보전하세<br />
</p>
<p id="2">
남산 위에 저 소나무 철갑을 두른 듯<br />
바람서리 불변함은 우리 기상일세<br />
무궁화 삼천리 화려강산<br />
대한사람 대한으로 길이 보전하세<br />
</p>
<p id="3">
가을 하늘 공활한데 높고 구름 없이<br />
밝은 달이 우리 가슴 일편단심일세<br />
무궁화 삼천리 화려강산<br />
대한사람 대한으로 길이 보전하세<br />
</p>
<p id="4">
이 기상과 이 맘으로 충성을 다하여<br />
괴로우나 즐거우나 나라 사랑하세<br />
무궁화 삼천리 화려강산<br />
대한사람 대한으로 길이 보전하세<br />
</p>
위와 같이 1, 2, 3, 4
각각을 id
로 가진 p tag들이 있습니다.
<ul>
<li><a href="#1">1절</a></li>
<li><a href="#2">2절</a></li>
<li><a href="#3">3절</a></li>
<li><a href="#4">4절</a></li>
</ul>
a 태그
의 href
속성에 #id
를 주면, a태그 클릭시 해당 아이디를 가진 html tag가 존재하는 영역으로 화면이 이동될 것입니다.
제가 작업중인 React는 SPA(Single Page Application)입니다.
즉, 화면을 전환하더라도 페이지는 새로고침되지 않고,
필요한 컴포넌트들만 변경되는 방식입니다.
이런 SPA라이브러리에서 navigating 작업을 진행할 때에도,
해쉬(#)를 사용하여 히스토리 관리를 할 수 있습니다.
저의 경우도 리액트를 사용하여 개발하고,
react-router-dom
라이브러리의 useNavigate
를 이용해 화면을 전환하였으니, 리다이렉팅된 url의 로컬호스트 주소 뒤에 ?가 아닌 #가 존재하였던 것입니다.
아무런 런타임 혹은 컴파일에러 없이 코드가 돌아가는데,
URLSearchParams의 메소드들이 의도한대로 동작하지 않는다면,
URL내에 fragment identifier가 존재하고 있는 것은 아닌지 확인해보아야 합니다.
특히나 프론트엔드를 React와 같은 SPA라이브러리로 개발한다면,
페이지 전환시 URL에 해쉬부분이 생성될 수 있으니 더욱 신경쓸 필요가 있습니다.