Chapter 9. 로그인 및 회원 가입 기능 구현
실습 인증☑️
Google Login 구현해보기
👉 Passport 라이브러리
다음과 같은 명령어를 사용해서 라이브러리 설치를 진행했습니다.
npm install \\
express-session \\
passport \\
passport-google-oauth20

👉 Google 로그인 인증 키 발급
1️⃣ Google Cloud Platform 접속하기

2️⃣ Project 생성하기

3️⃣ OAuth 2.0 Client ID 생성하기
APIs and services 로 이동 후 설정 완료
OAuth2 정보를 등록하기 위해서 다음 절차 진행

좌측의 Credentials 탭을 클릭한 후, 상단 Create Credentials 버튼을 클릭한 후, OAuth client ID 항목을 선택 후 내용을 입력해줬습니다.

4️⃣ Client ID, Client secret 조회하기
생성을 하고 나면 팝업이 뜨면서 Client ID와 Client secret을 조회 가능!

해당 내용을 .env에 반영
👉 인증을 위한 코드 추가
src/auth.config.js 파일

이 내용을 추가해줘서 먼저 Google 로그인 후 전달 받은 사용자의 프로필 정보에 이메일이 포함되어 있는지 확인하고, 이메일을 이용해서 사용자를 조회하게 한다.
const googleVerify = async (profile) => {
const email = profile.emails?.[0]?.value;
if (!email) {
throw new Error(`profile.email was not found: ${profile}`);
}
const user = await prisma.user.findFirst({ where: { email } });
if (user !== null) {
return { id: user.id, email: user.email, name: user.name };
}
const created = await prisma.user.create({
data: {
email,
name: profile.displayName,
gender: "추후 수정",
birth: new Date(1970, 0, 1),
address: "추후 수정",
detailAddress: "추후 수정",
phoneNumber: "추후 수정",
},
});
return { id: created.id, email: created.email, name: created.name };
};
👉 Passport 사용하기 - 세션 (Session)
다음 명령어로 라이브러리를 설치해주었습니다.
npm install @quixo3/prisma-session-store

그리고 Prisma Schema를 수정해주었습니다.
model Session {
id String @id
sid String @unique
data String @db.VarChar(512)
expiresAt DateTime @map("expires_at")
@@map("session")
}

그 다음 index.js코드를 수정해주었습니다

👉 Passport 사용하기 - Route
app.get("/oauth2/login/google", passport.authenticate("google"));
app.get(
"/oauth2/callback/google",
passport.authenticate("google", {
failureRedirect: "/oauth2/login/google",
failureMessage: true,
}),
(req, res) => res.redirect("/")
);
👉 Passport 사용하기 - 미들웨어
index.js 코드를 수정한 다음 env 파일도 수정해줍니다
// src/index.js
import express from "express";
import cors from "cors";
import swaggerAutogen from "swagger-autogen";
import swaggerUiExpress from "swagger-ui-express";
import { handleStoreSignUp,handleListStoreReviews} from "./controller/store.controller.js";
import { handleReviewSignUp } from "./controller/review.controller.js";
import { handleMissionSignUp,handleListStoreMissions,handleListUserInProgressMissions,handleCompleteUserMission } from "./controller/mission.controller.js";
import { handleChallengeSignUp } from "./controller/challenge.controller.js";
import { handleListUserReviews, handleUserLogin,handleUserSignUp } from './controller/user.controller.js';
import { PrismaSessionStore } from "@quixo3/prisma-session-store";
import session from "express-session";
import passport from "passport";
import { googleStrategy } from "./auth.config.js";
import { prisma } from "./db.config.js";
dotenv.config();
passport.use(googleStrategy);
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(cors());
app.use(express.static("public")); // 정적 파일 접근
app.use(express.json()); // request의 본문을 json으로 해석할 수 있도록 함 (JSON 형태의 요청 body를 파싱하기 위함)
app.use(express.urlencoded({ extended: false })); // 단순 객체 문자열 형태로 본문 데이터 해석
app.use(
session({
cookie: {
maxAge: 7 * 24 * 60 * 60 * 1000, // ms
},
resave: false,
saveUninitialized: false,
secret: process.env.EXPRESS_SESSION_SECRET,
store: new PrismaSessionStore(prisma, {
checkPeriod: 2 * 60 * 1000, // ms
dbRecordIdIsSessionId: true,
dbRecordIdFunction: undefined,
}),
})
);
app.use((req, res, next) => {
res.success = (success) => {
return res.json({ resultType: "SUCCESS", error: null, success });
};
res.error = ({ errorCode = "unknown", reason = null, data = null }) => {
return res.json({
resultType: "FAIL",
error: { errorCode, reason, data },
success: null,
});
};
next();
});
app.use(
"/docs",
swaggerUiExpress.serve,
swaggerUiExpress.setup({}, {
swaggerOptions: {
url: "/openapi.json",
},
})
);
app.get("/openapi.json", async (req, res, next) => {
// #swagger.ignore = true
const options = {
openapi: "3.0.0",
disableLogs: true,
writeOutputFile: false,
};
const outputFile = "/dev/null"; // 파일 출력은 사용하지 않습니다.
const routes = ["./src/index.js"];
const doc = {
info: {
title: "UMC 7th",
description: "UMC 7th Node.js 테스트 프로젝트입니다.",
},
host: "localhost:3000",
};
const result = await swaggerAutogen(options)(outputFile, routes, doc);
res.json(result ? result.data : null);
});
// 가게 추가 API
app.post("/api/stores", handleStoreSignUp);
// 가게 리뷰 추가 API
app.post("/api/reviews", handleReviewSignUp);
// 가게 미션 추가 API
app.post("/api/missions", handleMissionSignUp);
// 가게의 미션 도전하기 API
app.post("/api/challenges", handleChallengeSignUp);
// 리뷰 목록 확인
app.get("/api/stores/:storeId/reviews", handleListStoreReviews)
// 내가 작성한 리뷰 목록 확인
app.get('/api/users/:userId/reviews', handleListUserReviews);
// 특정 가게의 미션 목록 조회 API
app.get('/api/stores/:storeId/missions', handleListStoreMissions);
// 특정 사용자의 진행 중인 미션 목록 조회 API
app.get('/api/users/:userId/missions/in-progress', handleListUserInProgressMissions);
// 특정 사용자의 진행 중인 미션 완료로 업데이트 API
app.put('/api/users/:userId/missions/:missionId/complete', handleCompleteUserMission);
//유저 로그인 API
app.post("/api/users/login", handleUserLogin);
// 유저 회원가입 API
app.post("/api/users/signup", handleUserSignUp);
app.get("/oauth2/login/google", passport.authenticate("google"));
app.get(
"/oauth2/callback/google",
passport.authenticate("google", {
failureRedirect: "/oauth2/login/google",
failureMessage: true,
}),
(req, res) => res.redirect("/")
);
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
res.status(err.statusCode || 500).error({
errorCode: err.errorCode || "unknown",
reason: err.reason || err.message || null,
data: err.data || null,
});
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
app.use(passport.session());
EXPRESS_SESSION_SECRET="secretvaluerandom"
👉 테스트 해보기
처음에는 undefined가 출력 되는 것을 확인했습니다.

✅ http://localhost:3000/oauth2/login/google 로 이동 후 로그인 후 /으로 이동하는 것을 확인
🎯핵심 키워드 정리
💠OAuth 2.0 이란?
OAuth 2.0은 인터넷 사용자들이 비밀번호를 공유하지 않고도 다른 애플리케이션이나 서비스에 자신의 정보를 안전하게 제공할 수 있도록 해주는 인증 및 권한 부여 프레임워크을 말한다.
- 인증(Authentication):
- 사용자가 누구인지 확인하는 과정.
- 예: Google 계정을 통해 로그인.
- 권한 부여(Authorization):
- 사용자가 특정 서비스나 리소스에 접근할 수 있도록 허가하는 과정.
- 예: Google Drive에서 파일 접근 권한을 제3자 애플리케이션에 부여.
- 사용자가 애플리케이션에 로그인 요청:
- 애플리케이션은 사용자를 대신하여 OAuth 제공자(예: Google, Facebook)에 인증 요청을 보냄.
- 사용자가 제공자에서 로그인:
- 사용자는 자신의 자격 증명(예: 이메일, 비밀번호)으로 제공자에 로그인.
- 사용자 권한 부여:
- 사용자는 애플리케이션이 특정 리소스(예: 이메일, 프로필 정보)에 접근할 수 있는 권한을 부여.
- 토큰 발급:
- 제공자는 애플리케이션에 엑세스 토큰을 발급.
- 리소스 접근:
- 애플리케이션은 엑세스 토큰을 사용해 제공자의 API를 호출하여 사용자 리소스에 접근.
💠HTTP Cookie란?
- HTTP Cookie는 웹 브라우저와 웹 서버 간에 상태 정보를 저장하고 교환하기 위해 사용되는 작은 데이터 조각
- 주로 사용자 세션을 관리하고, 사용자 환경을 맞춤화하며, 추적 목적으로 사용
Cookie의 주요 특징
- 키-값 쌍:
- 쿠키는 key=value 형태로 저장됩니다.
- 예: sessionId=abc123;.
- 저장 위치:
- 쿠키는 브라우저에 저장되며, 동일한 웹 서버에 요청을 보낼 때 자동으로 포함됩니다.
- 전송:
- HTTP 요청 헤더의 Set-Cookie 필드로 서버가 브라우저에 전달.
- 브라우저는 이후 요청의 Cookie 필드로 서버에 쿠키를 전송.
✏️ 9주차 필기
🔥 미션 인증
🔏 로그인 및 회원 가입 기능 구현
먼저 네이버 개발자 센터에서 애플리케이션을 등록하고 클라이언트 아이이디와 클라이언트 시크릿을 발급받아야합니다.
1️⃣ 네이버 개발자 센터 등록

오픈 API 이용신청을 누르고 들어간다.
2️⃣Application 등록


이후 등록을 한 뒤에 애플리케이션 정보를 확인합니다.

3️⃣설정 및 코드 추가
npm install passport-naver를 입력하여 라이브러리를 설치하였습니다.

.env 파일에 아까 받은 Client ID랑 Client Secret과 REDIRECT URL를 입력해줍니다.
그 다음 db.config.js 를 다음과 같이 입력해줍니다.
export const naverStrategy = new NaverStrategy(
{
clientID: process.env.PASSPORT_NAVER_CLIENT_ID,
clientSecret: process.env.PASSPORT_NAVER_CLIENT_SECRET,
callbackURL: process.env.PASSPORT_NAVER_REDIRECT_URI,
},
async (accessToken, refreshToken, profile, done) => {
console.log("Naver profile:", profile);
const email = profile.emails?.[0]?.value || null; // 이메일이 없을 경우 null 처리
if (!email) {
throw new Error(`profile.email was not found: ${JSON.stringify(profile)}`);
}
try {
// 기존 사용자 검색
const user = await prisma.users.findFirst({ where: { email } });
console.log("User found:", user);
if (user !== null) {
return { id: user.id, email: user.email, name: user.user_name };
}
// 새로운 사용자 생성
const created = await prisma.users.create({
data: {
email,
user_name: profile.displayName || "네이버 사용자",
gender: "추후 수정",
birth: new Date(1970, 0, 1),
address: "추후 수정",
detailAddress: "추후 수정",
phoneNumber: "추후 수정",
naverId: profile.id, // 네이버 사용자 고유 ID 저장
password: null, // 명시적으로 null 설정
},
});
console.log("User created:", created);
return { id: created.id, email: created.email, name: created.user_name };
} catch (error) {
console.error("Error in naverVerify:", error);
throw error;
}
}
);
// Passport Configuration
passport.use(naverStrategy);
export default passport;
다음 라우터를 따로 등록해줍니다.
import express from "express";
import passport from "passport";
const router = express.Router();
router.get("/auth/naver", passport.authenticate("naver"));//로그인 메인화면
router.get(
"/auth/naver/callback",
passport.authenticate("naver", { failureRedirect: "/login" }),
(req, res) => {
res.redirect("/");
}
);
export default router;
index.js 코드에선 다음과 같이 입력해줍니다.
//네이버 로그인 확인
app.use(naverAuthRouter);
그런 다음 http://localhost:3000/auth/naver에 접속하면 다음과 같은 화면이 뜨면서 잘 실행되는 것을 확인할 수 있습니다.

'🏃♀️ 대외활동 > UMC 7기 - Node.js' 카테고리의 다른 글
[UMC] 7th Ne(o)rdinary 해커톤 회고록 (1) | 2024.11.27 |
---|---|
[UMC 7th Server] 9주차 트러블 슈팅 (1) | 2024.11.25 |
[UMC 7th Server] 6, 7주차 트러블 슈팅 기록 (0) | 2024.11.19 |
[UMC 7th Server] Chapter 8. 프론트엔드 연동과 Swagger (2) | 2024.11.18 |
[UMC 7th Server] Chapter 7. Express 미들웨어 & API 응답 통일 & 에러 핸들링 (3) | 2024.11.12 |