따꿍의 프로젝트
[2025.07.26] auth PR 확인하기 본문
app.ts
import express, { ErrorRequestHandler } from 'express';
import cookieParser from 'cookie-parser';
import morgan from 'morgan';
import session from 'express-session';
import passport from 'passport';
import sequelize from './sequelize';
import authRouter from './routes/auth';
import passportConfig from './passport';
import './models';
const app = express();
passportConfig();
export const syncDB = async () => {
try {
await sequelize.sync({ alter: true });
console.log('DB 동기화 완료');
} catch (err) {
console.error('DB 동기화 실패:', err);
}
};
app.use(morgan('dev'));
app.use((req, res, next) => {
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
express.json()(req, res, next);
} else {
next();
}
});
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET!,
cookie: {
httpOnly: true,
secure: false,
},
})
);
//passport 연결
app.use(passport.initialize());
app.use(passport.session()); //로그인 상태 세션 기반으로 유지
app.use('/auth', authRouter);
app.get('/', (req, res) => {
res.send('Hello TypeScript Backend!');
});
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
console.error(err);
res.status(err.status || 500);
};
app.use(errorHandler);
export default app;
1. if (['POST', 'PUT', 'PATCH'].includes(req.method)) 이해하기
app.use(morgan('dev'));
app.use((req, res, next) => {
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
express.json()(req, res, next);
} else {
next();
}
});
This middleware conditionally parses JSON only for certain HTTP methods.
- For POST, PUT, and PATCH requests — which usually contain JSON bodies — it runs the express.json() middleware.
- For other methods (like GET, DELETE), it skips JSON parsing to avoid unnecessary processing.
This avoids running express.json() on requests that almost never have a body (like GET), which can be a minor optimization, especially when handling large volumes of traffic or unusual edge cases (e.g., when parsing JSON on GET causes errors due to unexpected content type).
app.use(express.json()); // always apply (POST, PUT, PATCH, GET, DELETE 모두 파싱함)
This is what most projects do, unless you need fine-grained control like in your example.
2. session과 passport 미들웨어 완벽 정리
app.use(session())
- if authentication succeeds, a session(personal 'container' for that user) will be established
and maintained through a cookie set in the user's browser.
검정 부분이 session이 하는 역할이고,
파란 부분이 중간중간 passport가 하는 역할이다.

❓ 궁금점:
이러면 passport가 돌아갈때마다 express-session에 cookie라는 이름의 세션이 존재하는지 확인한다는 것인데,
passport관련 코드는 무조건 session이 다 세팅된 이후 시점에 적어줘야하는 것 아닌가?
어떻게 passportConfig()가 app.use(session())보다 이전에 나타나는 것일까?
-> It works because passportConfig() only defines the strategies, and not the middleware that depends on session().
passportConfig에는 local()와 kakao()가 들어가있는데
All this is doing is configuring Passport — defining how it should serialize users and what strategies to use.
⚠️ This part does not rely on sessions yet. It just registers logic.

passport>index.ts
- serializeUser은 로그인 직후, 해당 유저만의 상자(session)을 만들어주고,
세션에 무엇을 저장해줄지 정해줍니다. -> done이 해당 역할을 해줌
ex) done(null, user.user_id)이면 세션에 user_id만 저장함
만일 user_id보다 많은것을 session에 저장해주고 싶다면

- deserializeUser은 세션안에 저장한 user_id를 기준으로 전체 User 정보를 DB에서 데리고 오고
이 유저 전체 정보를 req.user에 저장해주는 역할을 한다.
import passport from 'passport';
import local from './localStrategy';
import kakao from './kakaoStrategy';
import User from '../models/user';
export default () => {
passport.serializeUser((user: any, done) => {
done(null, user.user_id); // user.user_id 가 PK
});
passport.deserializeUser(async (id: number, done) => {
try {
const user = await User.findByPk(id);
if (!user) return done(null, false);
return done(null, user);
} catch (err) {
console.error(err);
return done(err);
}
});
local();
kakao();
};
- done 안에 있는 null은 에러가 없다는 소리이다.
done(error, result);
passport>localStrategy.ts
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import bcrypt from 'bcrypt';
import User from '../models/user';
export default () => {
passport.use(
new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
passReqToCallback: false,
},
async (email, password, done) => {
try {
const exUser = await User.findOne({ where: { email } });
// 유저가 존재하고, 비밀번호도 존재할 때만 bcrypt 비교
if (exUser && exUser.password) {
const result = await bcrypt.compare(
password,
exUser.password
);
if (result) {
return done(null, exUser);
} else {
return done(null, false, {
message: '비밀번호가 일치하지 않습니다.',
});
}
}
// 유저가 없거나 비밀번호가 없을 때
return done(null, false, {
message:
'가입되지 않은 회원이거나 비밀번호가 없습니다.',
});
} catch (error) {
console.error(error);
return done(error);
}
}
)
);
};
1. async function은 email, password, done을 어떻게 받아올까?
async function은 LocalStrategy에서 verifyCallback 역할을 한다.
new LocalStrategy(options, verifyCallback)

=> passport.authenticate에서 알아서 email, password을 꺼내낸다.
Passport handles extracting the fields from req.body and passing them in.
2. options는 뭐하는 녀석일까?
{ usernameField: 'email', passwordField: 'password' }
These tell Passport:
- usernameField: 'email'
- Instead of looking for a field called username (default), use the field called email from req.body.
- passwordField: 'password'
- This one is actually default, but it’s explicitly saying: look for a field called password in req.body.
So with this, Passport will extract credentials like this:
const email = req.body.email; const password = req.body.password;
and pass them into your verify function:
async (email, password, done) => { ... }
Without These Options
If you didn’t include them, Passport would look for:
req.body.username
req.body.password
which is the default for passport-local.
passport>kakaoStrategy.ts
import passport from 'passport';
import { Strategy as KakaoStrategy } from 'passport-kakao';
import User from '../models/user';
export default () => {
passport.use(
new KakaoStrategy(
{
clientID: process.env.KAKAO_ID!,
callbackURL: '/auth/kakao/callback',
},
async (accessToken, refreshToken, profile, done) => {
console.log('Kakao profile:', profile);
try {
const exUser = await User.findOne({
where: { sns_id: profile.id, provider: 'kakao' },
});
if (exUser) {
return done(null, exUser);
} else {
const newUser = await User.create({
email: profile._json?.kakao_account?.email || null,
nickname: profile.displayName,
sns_id: profile.id,
provider: 'kakao',
});
return done(null, newUser);
}
} catch (error) {
console.error(error);
return done(error);
}
}
)
);
};
middlewares>index.ts
import { Request, Response, NextFunction } from 'express';
export const isLoggedIn = (req: Request, res: Response, next: NextFunction) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).send('로그인이 필요합니다.');
}
};
export const isNotLoggedIn = (
req: Request,
res: Response,
next: NextFunction
) => {
if (!req.isAuthenticated()) {
next();
} else {
res.redirect('/');
}
};
routes>auth.ts
import express from 'express';
import passport from 'passport';
import { join, login, logout } from '../controllers/auth';
import { isLoggedIn, isNotLoggedIn } from '../middlewares/index';
const router = express.Router();
router.post('/join', isNotLoggedIn, join);
router.post('/login', isNotLoggedIn, login);
router.get('/logout', isLoggedIn, logout);
// Kakao OAuth
router.get('/kakao', passport.authenticate('kakao'));
router.get(
'/kakao/callback',
passport.authenticate('kakao', {
failureRedirect: '/?loginError=카카오로그인 실패',
}),
(req, res) => {
res.redirect('/');
}
);
export default router;
controllers>auth.ts
import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcrypt';
import passport from 'passport';
import User from '../models/user';
export const join = async (req: Request, res: Response, next: NextFunction) => {
const { email, nick, password } = req.body;
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
return res.status(400).json({
success: false,
message: '이미 가입된 이메일입니다.',
}); //유저 있으면 에러
}
if (!password || password.length < 6) {
return res.status(400).json({
success: false,
message: '비밀번호는 6자 이상이어야 합니다.',
}); //비밀번호 최소 6자
}
const hash = await bcrypt.hash(password, 12); //비밀번호 암호화
await User.create({
email,
nickname: nick,
password: hash,
provider: 'local',
}); //회원정보 저장
return res.status(201).json({
success: true,
message: '회원가입이 완료되었습니다.',
});
} catch (error) {
console.error(error);
return next(error);
}
};
export const login = (req: Request, res: Response, next: NextFunction) => {
passport.authenticate(
'local',
(
authError: Error | null,
user: Express.User | false, //로그인 성공 시 req.user = user로 Express에 등록됨
info: { message: string }
) => {
if (authError) {
console.error(authError);
return next(authError);
}
if (!user) {
return res
.status(401)
.json({ success: false, message: info.message });
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
const { user_id, email, nickname } = user as User;
return res.status(200).json({
success: true,
message: '로그인 성공',
user: {
id: user_id,
email,
nickname,
},
});
});
}
)(req, res, next);
};
export const logout = (req: Request, res: Response) => {
req.logout(() => {
return res.status(200).json({
success: true,
message: '로그아웃 되었습니다.',
});
});
};'웹프로젝트 > 졸작' 카테고리의 다른 글
| [20205.08.25] UserBookProgress의 is_bookmarked column 삭제 후 UserBookBookmark 생성 (0) | 2025.08.29 |
|---|---|
| [2025.08.03] auth 이메일 인증, 이메일찾기, 아이디 찾기, 임시 비밀번호 전송, 비밀번호 재설정 기능 PR 확인 (0) | 2025.08.03 |
| [2025.07.23] Many to Many sequelize로 구축하기 (0) | 2025.07.23 |
| [2025.07.23] sequelize.sync({alter:true})의 문제 (0) | 2025.07.23 |
| [2025.07.23] dataValues만 꺼내내기 (+ res.send의 숨겨진 기능) (0) | 2025.07.23 |