회원가입 창에서 새로운 유저를 생성하려는데,
회원가입하기 버튼을 누르니 다음과 같은 에러가 발생했습니다.
에러 문구를 천천히 살펴보니,
해독 되어야 할 문자열이 올바르게 인코딩 되지 않았다고 합니다.
에러가 발생한 지점의 코드를 클릭해보았더니,
제가 작성하지도 않은 파일인 bundle.js의 229번 라인에서 atob함수가 정상적으로 실행되지 못하고 있다고 합니다.
제가 작성하지도 않은 파일에서 에러가 발생한다니 당황스러웠으나,
한 가지 힌트는 얻을 수 있었습니다.
atob함수의 인자에 리프레시 토큰이 전달되고 있으니, JWT와 관련하여 문제가 발생하였을 것이라 짐작해볼 수 있습니다.
문제는 axios 요청 헤더의 Authorization 설정이었습니다.
아래는 유저가 폼에 입력값을 모두 입력한 뒤,
"회원가입 하기" 버튼을 클릭했을 때 벡엔드 서버로 POST 요청을 보내는 이벤트 핸들러의 일부분입니다.
const handleSubmit = (e) => {
e.preventDefault();
axiosInstance
.post(`user/register/`, {
email: formData.email.trim(),
user_name: formData.user_name.trim(),
password: formData.password.trim(),
})
//------- 코드 생략 -----------
저의 경우, axios의 설정을 미리 포함시킨 axiosInstance를 만들어 export하고,
axios를 사용해야 하는 상황에서 axiosInstance를 사용하고 있었습니다.
axios.js라는 별도의 파일 안에 생성한 axiosInstance 객체의 모습은 다음과 같습니다.
import axios from "axios";
const baseURL = "http://127.0.0.1:8000/api/";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
//django 설정 파일에서 simple jwt header type에 JWT주었음
Authorization: localStorage.getItem("access_token")
? "JWT " + localStorage.getItem("access_token")
: null,
"Content-Type": "application/json",
accept: "application/json",
// responseType: "json",
},
});
// --------코드 생략 ------------
엑시오스 요청 헤더의 Authorization 설정 부분만 자세히 살펴보겠습니다.
Authorization: localStorage.getItem("access_token")
? "JWT " + localStorage.getItem("access_token")
: null,
삼항 연산을 활용하여, 로컬 스토리지에 엑세스 토큰이 있으면 JWT접두사를 해당 토큰에 첨부하고, 엑세스 토큰이 존재하지 않으면 null값을 Authorization에 전달하고 있습니다.
로그인이 완료된 유저라면 로컬스토리지에 토큰이 존재하겠지만,
회원가입을 진행하는 시점에선 당연히 토큰이 존재하지 않으니,
회원가입 요청을 보내던 이벤트 핸들러의 권한 부분에 null 값이 들어가
문제가 발생했던 것입니다.
JWT의 Authorization 키에 null value가 주어지면,
atob에러가 발생합니다.
권한(여기선 토큰)이 없는 상황에선 아예 Authorization key자체를 전달하지 않아야 합니다. Authorization: null
의 형태는 에러를 발생시킵니다.
해결 방법은 간단합니다.
크게 두 가지를 생각해볼 수 있겠습니다.
- axiosInstance 파일 내부에서 Authorization 로직 수정
- 회원가입 로직 파일 내에선 axiosInstance대신 axios.post 사용
저의 경우, 회원가입 로직을 제외한 경우 모두 토큰이 브라우저에 저장되어 있기에,
효율성의 측면에서 2번 방법을 사용하기로 하였습니다.
수정된 회원가입 요청 코드는 다음과 같습니다.
const handleSubmit = (e) => {
e.preventDefault();
axios
.post(`user/register/`, {
email: formData.email.trim(),
user_name: formData.user_name.trim(),
password: formData.password.trim(),
})
너무 간단하지만, axiosInstance를 axios로 변경해주면 됩니다.
이렇게 함으로서 헤더에 Authorization 객체 자체가 존재하지 않게 됩니다.
회원가입의 경우, 당연히 누구나 요청할 수 있는 것이기에,
요청 헤더에 Authorization 값 자체가 존재하지 않아도 아무 상관이 없는 것입니다.
프로그램을 다시 실행해보니..
이제 atob에러는 해결되었지만, 404에러가 발생하고 있네요.
400번대 에러는 간단히 해결이 가능합니다.
브라우저 측의 에러이니, 높은 확률로 axios의 요청 엔드포인트가 잘못 되었을 것입니다.
axios.post의 요청 엔드포인트만 다시 살펴볼까요?
axios
.post(`user/register/`,{});
현재, 로컬호스트 주소가 모두 생략되어 있습니다.
처음엔 axiosInstance의 baseURL에 로컬호스트/api/ 를 지정해두었기에, user를 엔드포인트의 진입점으로 잡아도 문제가 없었지만,
현재는 axiosInstance를 사용하지 않고 있기에, axiosInstance의 baseURL을 axios.post의 요청 엔드포인트 앞에 첨부해주어야 합니다.
수정된 엔드포인트는 아래와 같습니다.
http://127.0.0.1:8000/api/user/register/
해당 사항을 반영한 코드는 아래와 같습니다.
const handleSubmit = (e) => {
e.preventDefault();
axios
.post(`http://127.0.0.1:8000/api/user/register/`, {
email: formData.email.trim(),
user_name: formData.user_name.trim(),
password: formData.password.trim(),
})
이후 다시 실행해보면..!
정상적으로 유저가 생성되었습니다!
pgAdmin에 접속하여 유저 데이터베이스에도 정상적으로 사용자가 등록되었는지 확인해보겠습니다.
설레는 첫 유저가 정상적으로 생성되었습니다!
혹시 궁금하신 독자분들을 위해, 전체 코드를 첨부하겠습니다.
디자인보다는 로직에 집중해주세요.
import axiosInstance from "../axios.js";
import { Link, useNavigate } from "react-router-dom";
import React, { useState } from "react";
import styled from "styled-components";
import axios from "axios";
//커스텀 스타일 컴포넌트 정의
const Wrapper = styled.form`
background-color: black;
color: white;
display: flex;
flex-direction: column;
gap: 15px;
//배경색 화면 전체에 차게
min-height: 100vh;
justify-content: center;
align-items: center;
padding-top: 30px;
`;
const Input = styled.input`
padding: 5px 20px;
border: 2px solid white;
border-radius: 20px;
background-color: black;
color: white;
font-size: 28px;
`;
//회원가입
export default function Register() {
const navigate = useNavigate();
const initialFormData = Object.freeze({
email: "",
user_name: "",
password: "",
});
const [formData, setFormData] = useState(initialFormData);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value.trim(),
});
};
const handleSubmit = (e) => {
e.preventDefault();
axios
.post(`http://127.0.0.1:8000/api/user/register/`, {
email: formData.email.trim(),
user_name: formData.user_name.trim(),
password: formData.password.trim(),
})
.then((res) => {
navigate("/login");
console.log(res);
})
.catch((err) => {
console.log(err);
alert("문제가 발생했습니다. 다시 시도해주세요");
navigate("");
});
};
return (
<Wrapper onSubmit={handleSubmit}>
<h1 style={{ fontSize: "80px", marginBottom: "10px" }}>회원가입</h1>
<Input
placeholder="이메일을 입력해주세요"
type="email"
name="email"
required
onChange={handleChange}
/>
<Input
placeholder="사용자 이름을 입력해주세요"
type="text"
name="user_name"
required
onChange={handleChange}
/>
<Input
placeholder="비밀번호를 입력해주세요"
type="password"
name="password"
required
onChange={handleChange}
/>
<Input type="button" onClick={handleSubmit} value={"회원가입 하기"} />
<Link to={"/login"}>
<button>이미 회원이신가요? 로그인 하기</button>
</Link>
</Wrapper>
);
}
atob에러는 제가 현 시점까지 마주한 에러 중,
가장 해결방법을 찾기 모호했습니다.
발생 원인의 경우의 수가 하나로 확정되는 에러도 아닌 데다,
구글링을 해도 발생 원인이 개발자마다 너무 다르니 큰 도움이 되지도 않았습니다.
하지만, JWT토큰을 프로젝트에 사용하고 있는 경우,
토큰과 관련하여 문제가 발생할 확률이 높다고 합니다.
로직에 이상이 없는지 잘 살펴보고,
제가 문제를 해결한 방법처럼
개발자도구의 콘솔에서 제공하는 에러 발생지점을 클릭하여
해당 코드를 분석해 root cause를 역추적하는 방법도 고려해볼 수 있겠습니다.
감사합니다.