프로젝트를 진행하면서 자연스럽게 소셜 로그인 구현을 맡게 되었다. 처음에는 프론트엔드에서는 소셜 로그인이 비교적 간단하다는 말을 들었지만, 직접 구현해 보니 생각보다 복잡한 과정이 많았다. 특히 OAuth 2.0을 기반으로 한 로그인 프로세스를 처음부터 이해하고, 카카오, 네이버, 구글 등 서로 다른 방식의 API를 다뤄야 한다는 점에서 혼란이 있었다.
처음에는 카카오 로그인이 가장 쉬운 것 같아 이를 먼저 구현하기로 했다. 하지만 카카오 개발자 문서를 읽어보면서도 프론트엔드와 백엔드의 역할이 명확하게 잡히지 않아, 다른 사람들의 블로그와 공식 문서를 참고하며 하나씩 개념을 정리해 나갔다.
특히, 소셜 로그인은 인가 코드, 액세스 토큰, 사용자 정보 조회 등의 과정이 단계적으로 이루어지기 때문에 처음에는 이해하는 데 시간이 걸렸다. 하지만 직접 하나하나 해결해 나가면서 개념이 확실히 잡혔고, 로그인 플로우를 구축하는 경험을 쌓을 수 있었다.
이 글에서는 카카오 로그인을 기준으로 OAuth 2.0을 활용한 소셜 로그인 구현 과정을 정리하고, 직접 경험하면서 해결해 나간 방법을 공유하고자 한다.
OAuth란?
타사 애플리케이션이 사용자 정보를 직접 제공받지 않고, 사용자의 인증을 통해 제한된 범위 내에서 접근 권한을 위임받는 방식이다. 대표적인 OAuth 2.0 프로토콜을 사용하면, 카카오, 네이버, 구글과 같은 서비스에서 제공하는 인증 시스템을 활용해 사용자가 로그인할 수 있다. 이를 통해 사용자는 보안성을 유지하면서도 간편하게 여러 서비스에 로그인할 수 있다.
카카오 로그인 설정 과정
카카오 로그인을 사용하기 위해서는 먼저 카카오 개발자 사이트에서 애플리케이션을 등록하고, 필요한 설정을 완료해야 한다. 설정 과정은 다음과 같다.
1. 애플리케이션 추가하기
Kakao Developers 사이트에서 새로운 애플리케이션을 등록해야 한다. 앱 아이콘, 앱 이름 등 필수 정보를 입력하고 애플리케이션을 생성한다.
2. 카카오 로그인 활성화하기
애플리케이션 설정에서 카카오 로그인 기능을 활성화한다. 이 과정에서 클라이언트 및 서버에서 사용할 Redirect URI를 설정해야 한다.
3. Redirect URI 등록하기
OAuth 인증 과정에서 로그인 후 리디렉트될 서버 도메인을 등록해야 한다.
개발 단계에서는 http://localhost:3000과 같이 React 개발 서버의 도메인을 설정하면 되며, 배포 후에는 실제 운영 환경에서 사용할 https://yourdomain.com과 같은 도메인을 추가로 등록해야 한다.
4. 카카오에게 제공 받을 정보 설정하기
카카오 로그인 후 가져올 사용자 정보 항목을 설정할 수 있으며, 일부 항목은 사업자 등록이 필요하다.
기본적으로 닉네임과 프로필 사진은 제공되지만, 이메일과 같은 추가 정보는 별도 신청을 통해 사용할 수 있다.
필수동의, 선택 동의 등 원하는걸로 선택하고 동의 목적 입력 후 저장하면 된다.
5. 비즈앱 전환 신청
만약 이메일을 추가로 제공 받고 싶으면 개인 정보 동의항목 심사 신청을 누르고 비즈앱을 신청하면 된다.
개인 개발자 비즈 앱 전환을 선택한다.
비즈 앱으로 전환하는 목적을 선택하고 신청을 완료한다.
이후 동의 항목 설정 페이지로 돌아가면 카카오 계정(이메일) 제공 항목이 활성화되어 선택할 수 있다.
카카오 로그인 방식
카카오 공식 문서에서 제공하는 카카오 로그인의 흐름은 다음과 같다. 처음에 API 통신 자체를 잘 모르는 단계에서 공식 문서를 보니 하나도 이해가 가지 않았다. 앞서 말했듯이 다른 사람들이 작성한 카카오 로그인 관련 글을 많이 찾아서 읽으며 개념을 정리해 나갔다.
로그인 방식 정리
OAuth 로그인은 크게 두 단계로 나뉜다.
- 클라이언트(React)에서 OAuth 제공자의 로그인 페이지로 이동하여 인증을 받는다.
- 인증 후 받은 코드(Authorization Code)를 서버(Spring Boot)로 보내고, 서버에서 액세스 토큰을 받아 사용자 정보를 가져온다.
이 과정에서 중요한 것은 OAuth 제공자별로 API 요청 방식이 조금씩 다르다는 것이다. 또한, 로그인 후 JWT 토큰을 발급해 클라이언트에 전달해야 한다.
1. React에서 로그인 버튼 및 OAuth 요청
먼저 React에서 로그인 버튼을 만들었다. 환경변수 파일(.env)에 각 로그인 제공자의 API 키를 저장한 후, 이를 사용하여 로그인 URL을 생성하고 버튼 클릭 시 해당 URL로 이동하도록 구현했다.
const KAKAO_REST_API_KEY = process.env.REACT_APP_KAKAO_REST_API_KEY;
const KAKAO_REDIRECT_URI = process.env.REACT_APP_KAKAO_REDIRECT_URI;
const NAVER_REST_API_KEY = process.env.REACT_APP_NAVER_REST_API_KEY;
const NAVER_REDIRECT_URI = process.env.REACT_APP_NAVER_REDIRECT_URI;
const GOOGLE_REST_API_KEY = process.env.REACT_APP_GOOGLE_REST_API_KEY;
const GOOGLE_REDIRECT_URI = process.env.REACT_APP_GOOGLE_REDIRECT_URI;
const kakaoURL = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_REST_API_KEY}&redirect_uri=${KAKAO_REDIRECT_URI}&response_type=code`;
const naverURL = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${NAVER_REST_API_KEY}&redirect_uri=${NAVER_REDIRECT_URI}`;
const googleURL = `https://accounts.google.com/o/oauth2/auth?client_id=${GOOGLE_REST_API_KEY}&redirect_uri=${GOOGLE_REDIRECT_URI}&response_type=code&scope=openid email profile`;
그리고 각 버튼을 클릭하면 로그인 페이지로 이동하도록 했다.
const handleKakaoLogin = () => {
window.location.href = kakaoURL;
};
const handleNaverLogin = () => {
window.location.href = naverURL;
};
const handleGoogleLogin = () => {
window.location.href = googleURL;
};
2. React에서 OAuth 인증 후 처리
OAuth 인증을 마치면 code 값을 받아 Spring Boot 서버로 전달해야 한다. 이를 위해 useEffect를 활용하여 로그인 코드가 존재할 경우 자동으로 로그인 요청을 보내도록 설정했다.
1. useEffect를 활용한 로그인 코드 감지
useEffect(() => {
if (KAKAO_CODE) {
handleLogin(KAKAO_CODE);
}
}, [KAKAO_CODE]);
- KAKAO_CODE는 URL에서 code 값을 가져온다.
- useEffect는 KAKAO_CODE 값이 변경될 때마다 실행되며, handleLogin 함수를 호출하여 로그인 프로세스를 시작한다.
2. handleLogin 함수: 카카오 로그인 처리 흐름
const handleLogin = async (code) => {
try {
// 1. 카카오 API를 호출하여 액세스 토큰 요청
const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/auth/kakao`, { authorizeCode: code });
const accessToken = response.data.apiData;
- code 값을 Spring Boot 서버(/api/auth/kakao)로 전송하여 액세스 토큰을 요청한다.
- 서버에서는 인가 코드로 카카오 API를 호출하여 액세스 토큰을 받아 반환한다.
// 2. 액세스 토큰을 이용하여 사용자 정보 요청
const userResponse = await axios.get(`${process.env.REACT_APP_API_URL}/api/users/kakao/profile`, {
headers: { Authorization: `Bearer ${accessToken}` },
params: { provider: "카카오" }
});
- 받은 액세스 토큰을 사용하여 /api/users/kakao/profile 엔드포인트를 호출하고 사용자 정보를 요청한다.
- 요청 시, Authorization 헤더에 Bearer 액세스 토큰을 포함하여 전달한다.
// 3. 응답 데이터 구조 분해
const { userInfo, message, authUser } = userResponse.data.apiData;
서버에서 받은 사용자 정보(userInfo), 메시지(message), 기존 가입 여부(authUser)를 구조 분해 할당으로 변수에 저장한다.
3. 기존 회원 여부 확인 후 로그인 처리
// 4. 이미 가입된 이메일인지 확인 후 처리
if (message === "이미 가입된 이메일") {
if (authUser) {
// 5. 로그인 성공 시 JWT 토큰을 저장 후 홈페이지 이동
localStorage.setItem("token", userResponse.headers["authorization"].split(" ")[1]);
navigate("/");
} else {
alert("이미 가입된 이메일입니다. 가입한 계정으로 로그인하세요.");
navigate("/user/login");
}
- 기존 회원인지 확인: message === "이미 가입된 이메일"이면 이미 해당 이메일이 등록된 경우다.
- 로그인 성공 시 처리:
- authUser 객체가 존재하면 정상 로그인한 것이므로, 서버에서 받은 JWT 토큰을 localStorage에 저장하고 홈페이지로 이동한다.
- authUser가 존재하지 않으면, 사용자에게 이미 가입된 이메일이므로 기존 계정으로 로그인하라는 안내 메시지를 띄운다.
4. 신규 회원 가입 처리
} else if (message === "신규 회원") {
// 6. 신규 회원의 경우 회원가입 페이지로 이동
navigate("/user/social/signup", { state: { ...userInfo, provider: "카카오" } });
}
} catch (error) {
// 7. 로그인 실패 시 에러 메시지 출력
alert("카카오 로그인에 실패했습니다.");
}
};
- 신규 회원인 경우: message === "신규 회원"이면, navigate를 이용해 회원가입 페이지(/user/social/signup)로 이동한다.
- 에러 처리: 로그인 요청 중 오류가 발생하면 alert("카카오 로그인에 실패했습니다.") 메시지를 띄운다.
5. 코드 흐름 정리
- useEffect에서 KAKAO_CODE가 존재하면 handleLogin 호출.
- handleLogin 함수에서 인가 코드 → 액세스 토큰 → 사용자 정보 순서로 요청 진행.
- 서버로부터 받은 사용자 정보와 메시지를 기반으로 기존 회원 여부 확인.
- 기존 회원이면 JWT 저장 후 로그인 처리.
- 신규 회원이면 회원가입 페이지로 이동.
- 로그인 요청이 실패하면 에러 메시지 출력.
이 과정을 통해 카카오 로그인 후 사용자 정보를 가져와 인증 및 회원가입을 진행하는 로직을 완성할 수 있다.
6. 완성된 handleLogin 함수
useEffect(() => {
if (KAKAO_CODE) {
handleLogin(KAKAO_CODE);
}
}, [KAKAO_CODE]);
const handleLogin = async (code) => {
try {
const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/auth/kakao`, { authorizeCode: code });
const accessToken = response.data.apiData;
const userResponse = await axios.get(`${process.env.REACT_APP_API_URL}/api/users/kakao/profile`, {
headers: { Authorization: `Bearer ${accessToken}` },
params: { provider: "카카오" }
});
const { userInfo, message, authUser } = userResponse.data.apiData;
if (message === "이미 가입된 이메일") {
if (authUser) {
localStorage.setItem("token", userResponse.headers["authorization"].split(" ")[1]);
navigate("/");
} else {
alert("이미 가입된 이메일입니다. 가입한 계정으로 로그인하세요.");
navigate("/user/login");
}
} else if (message === "신규 회원") {
navigate("/user/social/signup", { state: { ...userInfo, provider: "카카오" } });
}
} catch (error) {
alert("카카오 로그인에 실패했습니다.");
}
};
3. Spring Boot에서 카카오 로그인 처리
Spring Boot에서는 인가 코드를 받아 카카오 API를 통해 액세스 토큰을 요청했다.
@PostMapping("/api/auth/kakao")
public JsonResult kakaLogin(@RequestParam String authorizeCode) {
try {
String accessToken = juKakaoService.getKakaoAccessToken(authorizeCode);
return JsonResult.success(accessToken);
} catch (Exception e) {
return JsonResult.fail("카카오 로그인 실패.");
}
}
이후 해당 토큰을 사용하여 사용자 정보를 가져왔다.
public JuUserVo getKakaoUserProfile(String authHeader) throws Exception {
String accessToken = authHeader.substring(7);
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
ResponseEntity<String> response = restTemplate.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
new HttpEntity(headers),
String.class
);
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(response.getBody());
String email = jsonNode.path("kakao_account").path("email").asText();
String nickname = jsonNode.path("properties").path("nickname").asText();
JuUserVo userInfo = new JuUserVo();
userInfo.setEmail(email);
userInfo.setUser_nickname(nickname);
return userInfo;
}
4. JWT 생성 및 로그인 처리
로그인이 완료되면 JWT를 생성하여 클라이언트에 전달했다.
@PostMapping("/api/users/login")
public JsonResult login(@RequestBody JuLoginVo juLoginVo, HttpServletResponse response) {
JuLoginVo authUser = juUserService.exeLogin(juLoginVo);
if (authUser != null) {
JwtUtil.createTokenAndSetHeader(response, "" + authUser.getMember_id());
return JsonResult.success(authUser);
} else {
return JsonResult.fail("아이디 또는 비밀번호가 잘못되었습니다.");
}
}
마무리
OAuth 로그인은 생각보다 복잡한 과정이 많았다. 특히, 각 플랫폼마다 인증 방식이 다르고, JWT를 활용한 인증 관리도 신경 써야 했다. 하지만 구현을 마치고 나니, 직접 로그인 플로우를 구축한 보람이 있었다. 이후 보안을 강화하기 위해 Refresh Token을 활용한 인증 유지 기능도 추가할 예정이다. 또한, 현재 API 요청을 동기적으로 처리하고 있으며 RestTemplate을 사용하고 있어서, 성능 최적화를 위해 비동기 처리 및 WebClient로 전환할 계획이다.
참고자료
'SpringBoot' 카테고리의 다른 글
[SpringBoot] Twilio를 활용한 기념일 알림 기능 개발 (0) | 2025.02.23 |
---|---|
[SpringBoot] Twilio를 활용한 React & Spring Boot 휴대폰 인증 기능 개발 (0) | 2025.02.21 |
[SpringBoot] jsp 만들기 (0) | 2025.02.10 |
[SpringBoot] SpringBoot 설치부터 프로젝트 생성, 의존성 추가까지 (0) | 2025.02.09 |
[SpringBoot] 의존성 주입(Dependency Injection)이란? (0) | 2025.01.27 |