- 프론트엔드: React, TanStack Query, Google Identity Services (GIS)
- 백엔드: Node.js, Express, Drizzle ORM, google-auth-library
프론트엔드: 사용자와의 첫 만남, React와 GIS
사용자가 가장 먼저 마주하는 화면이죠. 프론트엔드에서는 구글 로그인 버튼을 보여주고, 사용자가 로그인을 완료하면 받게 되는 토큰을 백엔드로 넘겨주는 역할을 합니다.
1. 구글 로그인 버튼, 어떻게 띄우지?
Google Identity Services (GIS) 라이브러리를 사용하면 생각보다 간단하게 로그인 버튼을 만들 수 있습니다. AuthPage.tsx 컴포넌트에서 useEffect를 사용해 컴포넌트가 마운트될 때 GIS를 초기화하도록 설정했습니다.
// client/src/pages/auth-page.tsx
useEffect(() => {
const initializeGoogleSignIn = () => {
if (window.google && googleButtonRef.current) {
// GIS 초기화
window.google.accounts.id.initialize({
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID as string, // .env 파일에서 클라이언트 ID 가져오기
callback: handleGoogleLogin, // 로그인 성공 시 실행될 콜백 함수
});
// 구글 로그인 버튼 렌더링
window.google.accounts.id.renderButton(
googleButtonRef.current,
{ theme: "outline", size: "large", type: "standard", text: "signin_with" }
);
} else {
// 구글 스크립트 로딩이 늦어질 경우를 대비한 재시도
setTimeout(initializeGoogleSignIn, 100);
}
};
initializeGoogleSignIn();
}, []); // 컴포넌트 마운트 시 한 번만 실행
여기서 client_id는 당연히 Google Cloud Console에서 발급받은 ID를 사용해야 하고요, 가장 중요한 부분은 callback 함수인 handleGoogleLogin 입니다.
2. 로그인 성공! 백엔드로 토큰 전송
사용자가 구글 계정을 선택하고 인증을 마치면, 위에서 지정한 handleGoogleLogin 함수가 구글로부터 받은 응답과 함께 호출됩니다. 이 응답 객체의 credential 필드에 바로 그 중요한 ID 토큰(JWT)이 들어있습니다.
// client/src/pages/auth-page.tsx
const handleGoogleLogin = (response: any) => {
// TanStack Query의 mutation을 사용해 백엔드로 ID 토큰 전송
googleLoginMutation.mutate({ token: response.credential });
};
이제 이 토큰을 백엔드로 보내 "이 사용자 진짜 구글로 로그인한 거 맞아요!" 하고 검증을 요청해야 합니다. 이 비동기 요청은 TanStack Query의 useMutation을 사용해서 아주 깔끔하게 처리했습니다.
3. TanStack Query로 비동기 로직 관리하기
useMutation을 사용하면 API 요청의 로딩, 성공, 실패 상태를 쉽게 관리할 수 있죠.
// client/src/hooks/use-auth.tsx
const googleLoginMutation = useMutation({
mutationFn: async ({ token }: { token: string }) => {
// 백엔드 API로 ID 토큰 전송
const res = await apiRequest("POST", "/api/auth/google/verify", { token });
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Backend error: ${res.status} - ${errorText}`);
}
return res.json();
},
onSuccess: (user: SelectUser) => {
// 로그인 성공 시!
queryClient.clear(); // 기존 캐시 초기화
queryClient.setQueryData(["/api/user"], user); // 새 사용자 정보 캐시에 저장
// ★★★ 가장 중요했던 부분 ★★★
// 사용자 정보 쿼리를 '무효화'해서 UI를 즉시 업데이트!
queryClient.invalidateQueries({ queryKey: ["/api/user"] });
queryClient.invalidateQueries({ queryKey: ["/api/channels"] });
},
onError: (error: Error) => {
// 로그인 실패 시 토스트 메시지 표시
toast({
title: "Google 로그인 실패",
description: error.message,
variant: "destructive",
});
},
});
📌 프론트엔드 핵심 교훈: invalidateQueries의 중요성
"로그인은 됐는데, 왜 화면 상단에 사용자 이름이 바로 안 바뀌지? 🤔"
처음에 겪었던 문제입니다. onSuccess 콜백에서 setQueryData로 캐시를 직접 업데이트했는데도 UI가 꿈쩍도 안 하더군요. 해결책은 queryClient.invalidateQueries를 호출하는 것이었습니다. setQueryData는 단순히 캐시 안의 데이터를 바꾸는 역할만 하지만, invalidateQueries는 TanStack Query에게 "이 데이터는 이제 옛날 거니까, 다음에 필요할 때 새로 가져와!"라고 알려주는 역할을 합니다. 이 한 줄을 추가하고 나서야 로그인 직후 사용자 정보가 화면에 바로 반영되었습니다.
백엔드: 보이지 않는 곳에서의 검증, Node.js와 Drizzle ORM
프론트에서 토큰을 받았으니, 이제 서버의 시간입니다. 백엔드는 토큰이 유효한지 검증하고, 우리 서비스의 사용자인지 확인(또는 생성)한 뒤, 세션을 만들어주는 역할을 합니다.
1. ID 토큰, 진짜인지 가짜인지 확인하기
프론트에서 받은 토큰을 그대로 믿으면 절대 안 되죠. 반드시 백엔드에서 google-auth-library를 사용해 다시 검증해야 합니다.
// server/routes/google.ts
import { OAuth2Client } from 'google-auth-library';
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
router.post('/verify', async (req, res) => {
const { token } = req.body;
try {
// 구글 서버에 직접 토큰 유효성 검증 요청
const ticket = await client.verifyIdToken({
idToken: token,
audience: process.env.GOOGLE_CLIENT_ID, // 이 토큰이 우리 앱을 위한게 맞는지 확인
});
const payload = ticket.getPayload();
if (!payload) {
return res.status(400).json({ message: 'Invalid token' });
}
const { sub: googleId, email, name: username } = payload;
// ... 사용자 조회/생성 로직으로 ...
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
});
verifyIdToken 함수가 성공적으로 실행되면, payload에는 구글이 보증하는 사용자의 이메일, 이름, 고유 ID(sub) 등의 정보가 담겨 있습니다.
2. 우리 회원 맞으세요? (feat. Drizzle ORM)
이제 검증된 이메일로 우리 데이터베이스에 사용자가 있는지 찾아봅니다. 없으면 새로 만들어줘야겠죠? 이 과정에서 Drizzle ORM을 사용했는데, 몇 가지 삽질 포인트가 있었습니다.
// server/routes/google.ts
// ... 토큰 검증 후 ...
// let으로 선언해야 재할당이 가능!
let [user] = await db.select().from(users).where(eq(users.email, email)).execute();
if (!user) {
// 사용자가 없으면 새로 생성
console.log('Backend: User not found, creating new user.');
const [newUser] = await db.insert(users).values({
googleId,
email,
username,
authProvider: 'google',
}).returning().execute(); // .returning()으로 삽입된 레코드 반환
user = newUser; // 새로 만든 사용자를 user 변수에 할당
}
// ... 세션 로그인 로직으로 ...
📌 백엔드 핵심 교훈: Drizzle ORM 사용 시 겪었던 삽질들
1. TypeError: ...get is not a function 처음엔 ...get() 메서드로 단일 레코드를 가져오려 했는데 에러가 발생했습니다. 저희가 사용하는 PostgreSQL 드라이버(neon-serverless)에서는 get()을 지원하지 않더군요. 대신 execute()를 사용해야 했습니다. execute()는 항상 배열을 반환하기 때문에, const [user] = ... 처럼 배열 비구조화 할당으로 첫 번째 요소를 꺼내 써야 합니다.
2. TypeError: Assignment to constant variable user 변수를 const로 선언해놓고, 신규 유저 생성 시 user = newUser 코드로 재할당하려 하니 발생한 당연한 오류였습니다. let으로 변경해서 간단히 해결했습니다. 기본이지만 급하게 코딩하다 보면 놓치기 쉽죠. 😅
3. column "google_id" does not exist Drizzle 스키마 파일에는 googleId 필드를 분명히 추가했는데, 자꾸 DB에 해당 컬럼이 없다는 에러가 났습니다. 원인은 스키마 파일과 실제 DB의 싱크가 맞지 않았기 때문이었습니다. 터미널에서 npm run db:push 명령어로 스키마 변경사항을 DB에 적용해주니 바로 해결되었습니다. ORM 사용 시 스키마와 DB 상태를 항상 일치시키는 것이 정말 중요합니다.
3. 로그인 상태 유지: 세션 처리
사용자 정보까지 처리했다면, 마지막으로 req.login()을 호출해 세션을 만들어줍니다. 이렇게 해야 다음부터 이 사용자가 우리 서비스에 들어왔을 때, "아, 로그인된 사용자로구나!" 하고 알아볼 수 있습니다.
// server/routes/google.ts
req.login(user, (err) => {
if (err) {
return res.status(500).json({ message: 'Session login error' });
}
// 프론트엔드로 최종 사용자 정보 반환
return res.json({ user });
});
마무리하며
구글 로그인 연동은 단순히 라이브러리를 가져다 쓰는 것을 넘어, 프론트엔드의 상태 관리와 백엔드의 데이터베이스 처리, 그리고 둘 사이의 안전한 통신까지 고려해야 하는 제법 깊이 있는 작업이었습니다.
특히 TanStack Query의 캐시 무효화, Drizzle ORM의 쿼리 메서드 차이, DB 스키마 동기화 같은 문제들을 해결하며 풀스택 개발의 묘미를 느낄 수 있었네요.
'개발' 카테고리의 다른 글
| Expo 완전 마스터 가이드 - 개발부터 배포까지 (0) | 2025.08.20 |
|---|---|
| React 웹 프로젝트를 Expo React Native로 완전 마이그레이션하기: 실전 가이드 (4) | 2025.08.09 |
| Node.js 풀스택 개발시 로컬 개발 환경과 배포 환경의 괴리에서 발생하는 문제점에 대한 논의 (0) | 2025.08.02 |
| Vite와 Node.js 풀스택 개발에서 npm run dev를 통해 프론트와 백 동시에 실행하기(디버깅) (3) | 2025.08.02 |
| Node.js 앱 무중단 배포: GitHub Actions와 Docker로 EC2 자동화 파이프라인 구축하기 (5) | 2025.08.02 |