트러블 슈팅 카카오 최초 로그인시 자동 회원가입(닉네임 중복)
프로젝트를 구성하던 도중 카카오 로그인을 구현하게 되었다.
우리 서비스에서 회원가입과 로그인 기능이 있기 떄문에 카카오 로그인을 하게되면 기존 회원과
중복된 이메일, 닉네임이 만들어지게 되면 당연히 안되기 때문에 에러가 생기면서 '중복된 이메일입니다.' '중복된 닉네임 입니다.' 라는 오류가 나오게 로직을 구성하였는데 카카오에서 로그인시 정보를 받아 오는 과정에서 문제가 생겼다.
이메일은 카카오에서 로그인 할때 이메일을 그대로 받아 오기 때문에 크게 상관없었지만. '닉네임의 경우'
name: profile.displayName 이렇게 구성해서 카카오 프로피에있는 네임을 가져오게 되어있는데 이 네임의 경우가
실명인 경우가 많고 또 실명을 닉네임으로 하게되면 '김민수' ,'이하늘' 등등 흔한 이름들이 닉네임으로 초기 설정이 된다.
닉네임의 경우 회원가입이 되고 나면 유저가 직접 회원정보 페이지에 들어가서 수정해야 하기때문에 이런 흔한 이름으로
구성된 닉네임이 생기게 되면 다른 회원이 가입 할 때 중복된 이름때문에 가입이 되지 않게 된다. 그래서 카카오로 부터 받아온 네임을 쓰지않고 닉네임을 별도의 로직을 추가로 만들어서 유저에게 랜덤으로 생성되는 닉네임을 지정해주는 방식으로 결정되었다.
1번째 시도
첫번째 시도에서는 랜덤 텍스트를 그냥 2글자에서 8글자사이로 만들어내는 로직을 구현하였다.
그랫더니 회원가입했을때 '샹뤽AF륙' 뭐 대충 이런식의 좀 알아보기 힘든 닉네임이 생성되었다.
랜덤한 단어도 아니고 이러한 알기 힘든 텍스트를 닉네임으로 만들어 준다면 서비스적으로 보기에 좋지 않기 때문에 수정이 필요했다.
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-kakao';
import * as bcrypt from 'bcrypt';
export class JwtKakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
constructor() {
super({
clientID: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_CLIENT_SECRET,
callbackURL: process.env.KAKAO_CALLBACK_URL,
scope: ['account_email', 'profile_nickname'],
});
}
async validate(accessToken: string, refreshToken: string, profile: any) {
console.log('카카오에서 주는 accessToken:' + accessToken); //이건 카카오에서 주는 엑세스토큰
console.log('카카오에서 주는 refreshToken:' + refreshToken); // 이건 카카오에서 주는 리프레시 토큰임
console.log(profile);
console.log(profile._json.kakao_account.email);
//비밀번호 암호화
const hashedPassword = await bcrypt.hash(profile.id.toString(), 10);
// 랜덤 nickname 생성
const nickname = this.generateRandomNickname();
return {
name: profile.displayName,
email: profile._json.kakao_account.email,
password: hashedPassword,
confirmPassword: hashedPassword,
nickname: nickname,
profileImg: '기본이미지 url',
accessToken: profile._json.kakao_account.access,
refreshToken: profile._json.kakao_account.refresh,
};
}
private generateRandomNickname() {
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz가갸거겨고교구규그기끼끄나냐너녀노뇨누뉴눼뉘뉴다댜더데도됴두듀뒤듸라랴러려로료루류뤼르리리끼끄마먀머며모묘무뮤뮈뮤나냐너녀노뇨누뉴눼뉘뉴다댜더데도됴두듀뒤듸바뱌버벼보뵤부뷔뷰뷸뷰사싸서셔소쇼수슈수아야어여오요우유윈유자작저져조주쥐쥬쥐키';
const minLength = 2; // 최소 길이
const maxLength = 8; // 최대 길이
const nicknameLength =
Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;
let nickname = '';
for (let i = 0; i < nicknameLength; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
nickname += characters.charAt(randomIndex);
}
return nickname;
}
}
2차 수정 그리고 트러블
2차 수정된 코드이다.
기존의 이상한 텍스트를 생성하는것을 삭제하고
익명+영문또는숫자로 구성해서 8글자의 랜덤한 닉네임을 생성시켜준다.
또한 중복을 확인하고 중복이라면 다시 랜덤한 닉네임을 만들어서 중복이 방지되도록 코드를 구현하였다.
여기서 문제없이 서버가 실행되고 당연히 카카오 로그인 정상적으로 작동하면서 최초로 카카오 로그인을 할경우
그 카카오아이디로 회원가입을 시켜줄 줄 알았는데.
ERROR [ExceptionsHandler] Cannot read properties of undefined (reading 'userDetail')
TypeError: Cannot read properties of undefined (reading 'userDetail')
... 유저 디테일에서 데이터를 읽어오지 못하고있다는것이었다.
뭐가 문제인지 몰라서 계속 콘솔로그를 여기저기 찍어보고 service쪽을 수정도해보고 했지만 문제가 해결되지 않았는데
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-kakao';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../prisma/prisma.service'; // 프리즈마 서비스 파일 경로를 사용하는 경로로 수정해야 합니다.
export class JwtKakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
constructor(private readonly prisma: PrismaService) {
super({
clientID: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_CLIENT_SECRET,
callbackURL: process.env.KAKAO_CALLBACK_URL,
scope: ['account_email', 'profile_nickname'],
});
}
async validate(accessToken: string, refreshToken: string, profile: any) {
//console.log('카카오에서 주는 accessToken:' + accessToken);
//console.log('카카오에서 주는 refreshToken:' + refreshToken);
//console.log(profile);
//console.log(profile._json.kakao_account.email);
// 비밀번호 암호화
const hashedPassword = await bcrypt.hash(profile.id.toString(), 10);
// 고유한 익명 nickname 생성
const nickname = await this.generateUniqueAnonymousName();
//console.log('닉네임 확인', nickname);
return {
name: profile.displayName,
email: profile._json.kakao_account.email,
password: hashedPassword,
confirmPassword: hashedPassword,
nickname: nickname,
profileImg: '기본이미지 url',
accessToken: profile._json.kakao_account.access,
refreshToken: profile._json.kakao_account.refresh,
};
}
private async generateUniqueAnonymousName(): Promise<string> {
const anonymousPrefix = '익명';
const randomLength = 6;
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
while (true) {
let randomString = '';
for (let i = 0; i < randomLength; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
const anonymousName = `${anonymousPrefix}${randomString}`;
return anonymousName;
// // 프리즈마를 사용하여 중복 확인
// const existingUser = await this.prisma.userDetail.findUnique({
// where: { nickname: anonymousName },
// });
// if (!existingUser) {
// return anonymousName; // 중복되지 않는 이름 반환
// }
}
}
}
문제해결
export class JwtKakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
constructor(
private readonly prisma: PrismaService,
@Inject(PrismaService) private readonly prismaService: PrismaService // 추가
) {
super({
clientID: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_CLIENT_SECRET,
callbackURL: process.env.KAKAO_CALLBACK_URL,
scope: ['account_email', 'profile_nickname'],
});
}
이코드로 의존성을 주입해주지 않아서 문제가 해결되지 않았다.
기존의 constructor(private readonly prisma: PrismaService) 이코드로 나는 prisma 의존성 주입이 된 줄 알았는데
추가적으로 @Inject(PrismaService) private readonly prismaService: PrismaService // 추가 이코드를 입력해야 의존성 주입이 제대로 되었던 것이었다. 앞으로 데이터를 못찾는다는 에러가 뜨게 된다면 가장먼저 의존성 주입부터 의심해봐야 겠다.
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-kakao';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../prisma/prisma.service'; // 프리즈마 서비스 파일 경로를 사용하는 경로로 수정해야 합니다.
import { Inject } from '@nestjs/common';
export class JwtKakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
constructor(
private readonly prisma: PrismaService,
@Inject(PrismaService) private readonly prismaService: PrismaService // 추가
) {
super({
clientID: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_CLIENT_SECRET,
callbackURL: process.env.KAKAO_CALLBACK_URL,
scope: ['account_email', 'profile_nickname'],
});
}
async validate(accessToken: string, refreshToken: string, profile: any) {
//console.log('카카오에서 주는 accessToken:' + accessToken);
//console.log('카카오에서 주는 refreshToken:' + refreshToken);
//console.log(profile);
//console.log(profile._json.kakao_account.email);
// 비밀번호 암호화
const hashedPassword = await bcrypt.hash(profile.id.toString(), 10);
// 고유한 익명 nickname 생성
const nickname = await this.generateUniqueAnonymousName();
//console.log('닉네임 확인', nickname);
return {
name: profile.displayName,
email: profile._json.kakao_account.email,
password: hashedPassword,
confirmPassword: hashedPassword,
nickname: nickname,
profileImg: '기본이미지 url',
accessToken: profile._json.kakao_account.access,
refreshToken: profile._json.kakao_account.refresh,
};
}
private async generateUniqueAnonymousName(): Promise<string> {
const anonymousPrefix = '익명';
const randomLength = 6;
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
while (true) {
let randomString = '';
for (let i = 0; i < randomLength; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
const anonymousName = `${anonymousPrefix}${randomString}`;
//return anonymousName; // 밑의 로직이 작동안하면 임시적으로 사용
// // 프리즈마를 사용하여 중복 확인
const existingUser = await this.prisma.userDetail.findUnique({
where: { nickname: anonymousName },
});
if (!existingUser) {
return anonymousName; // 중복되지 않는 이름 반환
}
}
}
}