반응형
# SNS 서비스 만들기
1. 노드버드 프로젝트 구조 갖추기.
### NodeBird SNS 서비스
- 기능 : 로그인, 이미지 업로드, 게시글 작성, 해시태그 검색, 팔로잉
- express-generator 대신 직접 구조를 갖춤
- 프런트엔드 코드보다 노드 라우터 중심으로 볼 것.
- 관계형 데이터 베이스 MySQL 선택.
### 프로젝트 시작
- 프로젝트 폴더 생성.
- package.json 파일 생성 : 프로젝트의 기본.
npm init
- 시퀄라이즈 폴더 구조 생성
npm i sequelize mysql2 sequelize-cli
npx sequelize init
### 폴더 구조 설정
- views (템플릿 엔진), routes (라우터), public (정적 파일 : css 등), passport (패스포트) 폴더 생성
- app.js와 .env 파일도 생성하기.
### 패키지 설치와 nodemon
- npm 패키지 설치 후 nodemon도 설치
- nodemon은 서버 코드가 변경되었을때 자동으로 서버를 재시작해줌.
npm i -D nodemon
pm i express express-session nunjucks morgan cookie-parser dotenv multer
### app.js
- 노드 서버의 핵심.
- 소스 코드 참고 : https://github.com/zerocho/nodejs-book
- app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const pageRouter = require('./routes/page');
const app = express();
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
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,
},
}));
app.use('/', pageRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
- .env도 같이 추가.
COOKIE_SECRET=cookiesecret
### 라우터 생성
- routes/page.js : 템플릿 엔진을 렌더링하는 라우터
- views/layout.html : 프론트 엔드 화면 레이아웃 (로그인/유저 정보 화면)
- views/main.html : 메인 화면 (게시글들이 보임)
- views/profile.html : 프로필 화면 (팔로잉 관계가 보임)
- views/error.html : 에러 발생 시 에러가 표시될 화면
- public/main.css : 화면 css
- 소스 참고 : https://github.com/ZeroCho/nodejs-book/tree/master/ch9/9.1/nodebird
- 각 파일 생성 완료 후 npm start로 실행.
2. 데이터베이스 구조 갖추기
### 모델 생성
- models/user.js : 사용자 테이블과 연결됨.
provider : 카카오 로그인인 경우 kakao, 로컬 로그인(이메일/비밀번호)인 경우 local
snsId : 카카오 로그인인 경우 주어지는 id
- models/post.js : 게시글 내용과 이미지 경로를 저장 (이미지는 파일로 저장)
- models/hashtag.js : 해시태그 이름을 저장 (나중에 태그로 검색하기 위함)
3. 테이블 관계 정의하기
### models/index.js
- 시퀄라이즈가 자동으로 생성해주는 코드 대신 다음과 같이 변경.
모델들을 불러옴 (require)
모델 간 관계가 있는 경우 관계 설정
User(1) : Post(다)
Post(다) : Hashtag(다)
User(다) : User(다)
### associate 작성하기
- 모델간의 관계들 associate에 작성
1 대 다 : hasMany와 belongsTo
다 대 다 : belongsToMany
- foreignKey : 외래키
- as : 컬럼에 대한 별명
- through : 중간 테이블
### 팔로잉-팔로워 다대다 관계
- User(다) : User(다)
- 다대다 관계이므로 중간 테이블(Follow) 생성됨.
- 모델 이름이 같으므로 구분 필요함(as가 구분자 역할, foreignKey는 반대 테이블 컬럼의 프라이머리 키 컬럼)
- 시퀄라이즈는 as 이름을 바탕으로 자동으로 addFollower, getFollowers, addFollowing, getFollowings 메서드 생성
### 시퀄라이즈 설정하기
- 시퀄라이즈 설정은 config/config.json 에서, 개발 환경용 설정은 development 아래에.
{
"development": {
"username": "root",
"password": "root 비밀번호",
"database": "nodebird",
"host": "127.0.0.1",
"dialect": "mysql"
},
- 설정 파일 작성 후 nodebird 데이터베이스 생성.
npx sequelize db:create
### 모델과 서버 연결하기
- npm start로 서버 실행 시 콘솔에 SQL문이 표시됨.
npm start
> nodebird@1.0.0 start C:\study\study_1\project_sns_service
> nodemon app
[nodemon] 2.0.13
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
8001 번 포트에서 대기중
Executing (default): CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER NOT NULL auto_increment , `email` VARCHAR(40) UNIQUE, `nick` VARCHAR(15) NOT NULL, `password` VARCHAR(100), `provider` VARCHAR(10) NOT NULL DEFAULT 'local', `snsId` VARCHAR(30), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `deletedAt` DATETIME, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci;
Executing (default): SHOW INDEX FROM `users` FROM `nodebird`
Executing (default): CREATE TABLE IF NOT EXISTS `posts` (`id` INTEGER NOT NULL auto_increment , `content` VARCHAR(140) NOT NULL, `img` VARCHAR(200), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `UserId` INTEGER, PRIMARY KEY (`id`), FOREIGN KEY (`UserId`)
REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
Executing (default): SHOW INDEX FROM `posts` FROM `nodebird`
Executing (default): CREATE TABLE IF NOT EXISTS `hashtags` (`id` INTEGER NOT NULL auto_increment , `title` VARCHAR(15) NOT NULL UNIQUE,
`createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
Executing (default): SHOW INDEX FROM `hashtags` FROM `nodebird`
Executing (default): CREATE TABLE IF NOT EXISTS `Follow` (`createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `followingId` INTEGER , `followerId` INTEGER , PRIMARY KEY (`followingId`, `followerId`), FOREIGN KEY (`followingId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`followerId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci;
Executing (default): SHOW INDEX FROM `Follow` FROM `nodebird`
Executing (default): CREATE TABLE IF NOT EXISTS `PostHashtag` (`createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `PostId` INTEGER , `HashtagId` INTEGER , PRIMARY KEY (`PostId`, `HashtagId`), FOREIGN KEY (`PostId`) REFERENCES `posts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`HashtagId`) REFERENCES `hashtags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
Executing (default): SHOW INDEX FROM `PostHashtag` FROM `nodebird`
데이터베이스 연결 성공
4. 패스포트 사용하기
### 패스포트 설치
- 로그인 과정을 쉽게 처리할 수 있게 도와주는 Passport 설치하기
- 비밀번호 해시화를 위한 bcrypt도 같이 설치
- 설치 후 app.js와 연결
- passport.session(): req.session 객체에 passport 정보를 저장
- express-session 미들웨어에 의존하므로 이보다 더 뒤에 위치해야 함.
npm i passport passport-local passport-kakao bcrypt
### 패스포트 모듈 작성
- passport/index.js 작성
- passport.serializeUser : req.session 객체에 어떤 데이터를 저장할 지 선택, 사용자 정보를 다 들고 있으면 메모리를 많이 차지하기 때문에 사용자의 아이디만 저장
- passport.deserializeUser : req.session에 저장된 사용자 아이디를 바탕으로 DB 조회로 사용자 정보를 얻어낸 후 req.user에 저장.
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
// passport는 전략을 사용(로그인을 어떻게 할지 적어놓은 파일.)
module.exports = () => {
// 메모리의 효율성을 위해 serializeUser, deserializeUser 사용
// auth.js 에서 login 라우터 에서 req login에 넣은 유저가 넘어와서
passport.serializeUser((user, done) => {
done(null, user.id); // 유저에서 user의 id만 뽑아서 done 수행. -> 세션의 유저 id만 저장하는 것. (user 저장가능하지만, 서버 메모리가 한정적이므로 id만 저장./ 실무에서는 메모리에도 저장하면 안됨_실무에서 이러면 컴퓨터가 못버팀)
});
// 메모리에 저장된 id로 user.findOne 해서 전체 유저를 복구해줌.
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user)) // req.user로 접근할 수 있게됨.(로그인한 사용자 정보 확인가능)
.catch(err => done(err));
});
local();
kakao();
};
### 패스포트 처리 과정 (중요)
- 로그인 과정
1. 로그인 요청이 들어옴
2. passport.authenticate 메서드 호출
3. 로그인 전략 수행
4. 로그인 성공 시 사용자 정보 객체와 함께 req.login 호출
5. req.login 메서드가 passport.serializeUser 호출
6. req.session에 사용자 아이디만 저장
7. 로그인 완료
- 로그인 이후 과정
1. 모든 요청에 passport.session() 미들웨어가 passport.deserializeUser 메서드 호출
2. req.session에 저장된 아이디로 데이터베이스에서 사용자 조회
3. 조회된 사용자 정보를 req.user에 저장
4. 라우터에서 req.user 객체 사용 가능
### 로컬 로그인 구현
- passport-local 패키지 필요
- 로컬 로그인 전략 수립
- 로그인에만 해당하는 전략이므로 회원가입은 따로 만들어야 한다.
- 사용자가 로그인 했는지, 하지 않았는지 여부를 체크하는 미들웨어도 만듦.
- routes/middlewares.js
// 로그인 한지 여부
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).send('로그인 필요');
}
};
// 로그인 안 한지 여부
exports.isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
next();
} else {
const message = encodeURIComponent('로그인한 상태입니다.');
res.redirect(`/?error=${message}`);
}
};
- routes/page.js
const { application } = require('express');
const express = require('express');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = null;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followerIdList = [];
next();
});
router.get('/profile', (req, res) => {
res.render('profile', { title: '내 정보 - NodeBird' });
});
router.get('/join', (req, res) => {
res.render('join', { title: '회원가입 - NodeBird' });
});
router.get('/', (req, res) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
user: req.user,
});
});
module.exports = router;
### 회원가입 라우터
- routes/auth.js 작성
- bcrypt.hash로 비밀번호 암호화
- hash의 두 번째 인수는 암호화 라운드
- 라운드가 높을수록 안전하지만, 오래 걸림
- 적당한 라운드를 찾는 게 좋다.
- ?error 쿼리스트링으로 1회성 메시지
- routes/auth.js
const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');
const router = express.Router();
// 회원가입_ 로그인 안 한 사람만 회원가입 할 수 있도록 isNotLoggedIn
router.post('/join', isNotLoggedIn, async (req, res, next) => {
const { email, nick, password } = req.body;
try {
// 먼저 기존에 입력된 이메일로 가입된 대상 있는지 검사.
const exUser = await User.findOne({ where : { email } });
// 입력된 이메일로 가입된 대상이 존재하는 경우.
if (exUser) {
return res.redirect('/join?error=exist'); // 쿼리스트링사용 프론트로 exist 날려서 프론트에서 처리될 수 있도록 함.
}
// 입력된 이메일로 가입된 대상 없는경우.
const hash = await bcrypt.hash(password, 12); // hash(값, 12)에서 12는 얼마나 복잡하게 해시할건지 나타냄. (높을수록 복잡함, 소요시간 오래걸림)
// 유저 생성.
await User.create({
email,
nick,
password: hash,
});
return res.redirect('/');
} catch (error) {
console.error(error);
return next(error);
}
});
// 로그인 로그인 안 한 사람만 로그인 할 수 있도록 isNotLoggedIn
router.post('/login', isNotLoggedIn, (req, res, next) => {
// 미들웨어를 확장하는 패턴.
// login 시 아이디 비밀번호 입력하면 passport.authenticate('local', 부분 실행 -> localStrategy.js 로 가게됨.
passport.authenticate('local', (authError, user, info) => {
// 서버에 에러있는 경우.
if (authError) {
console.error(authError);
return next(authError);
}
// 로그인 실패한 경우
if (!user) {
return res.redirect(`/?loginError=${info.message}`); // 로그인 실패에 관한 메시지를 담아서 프론트로 전달.
}
// 로그인 성공한 경우 이때 passport의 index.js로 가게됨.
return req.logIn(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.redirect('/');
});
})(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙임.
});
// 로그아웃 로그인 한 사람만 로그아웃 할 수 있도록 isLoggedIn
router.get('/logout', isLoggedIn, (req, res) => {
req.logout();
req.session.destroy();
res.redirect('/');
});
// kakao로 로그인 하기 클릭 authenticate('kakao') -> kakaoStrategy로 가게됨 그래서 카카오 로그인을 하게됨. (kakao 사이트로 가게됨)
router.get('/kakao', passport.authenticate('kakao'));
// kakao 로그인 성공 시 kakao에서 /kakao/callback로 요청을 쏴줌. -> passport.authenticate('kakao', 실행 -> kakaoStrategy로로 가서 검사진행.
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/', // 실패 시 여기로.
}), (req, res) => { // 성공 시 여기 실행.
res.redirect('/');
});
module.exports = router;
### 로그인 라우터
- routes/auth.js 작성
- passport.authenticate('local'); 로컬 전략
- 전략을 수행하고 나면 authenticate의 콜백 함수 호출됨
- authError : 인증 과정 중 에러.
- user : 인증 성공 시 유저 정보
- info : 인증 오류에 대한 메시지
- 인증이 성공했다면 req.login으로 세션에 유저 정보 저장.
### 로컬 전략 작성
- passport/localStrategy.js 작성
- usernameField의 passwordField가 input 태그의 name(body-parser의 req.body)
- 사용자가 DB에 저장되어있는지 확인 후 있다면 비밀번호 비교(bcrypt.compare)
- 비밀번호까지 일치하면 로그인.
- passport/localStrategy.js
const passport = require('passport');
const localStrategy = require('passport-local');
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new localStrategy({
usernameField: 'email', // req.body.email
passwordField: 'password', // req.body.password
}, async (email, password, done) => {
try {
// 로그인 시 입력된 이메일을 가진 대상이 존재하는지 확인.
const exUser = await User.findOne({ where : { email } });
// 입력된 이메일 대상이 존재하는 경우.
if (exUser) {
// 비밀번호 비교(입력된 비밀번호와 해시화된 비밀번호 비교)
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser); // done의 경우 인수를 3개 받음. 여기서 1번 : 서버 / 2번 : 로그인가능여부 / 3번 로그인실패시 메시지
} else {
done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
}
// 입력된 이메일 대상이 존재하지 않는 경우.
} else {
done(null, false, { message: '가입되지 않은 회원입니다.' });
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
6. 카카오 로그인하기
### 카카오 로그인용 라우터 만들기
- 회원가입과 로그인이 전략에서 동시에 수행.
- passport.authenticate('kakao')만 하면 됨
- /kakao/callback 라우터에서는 인증 성공 시 (res.redirect)와 실패 시(failureRedirect) 리타이렉트할 경로를 지정.
### 카카오 로그인 앱 만들기
- 카카오 개발자 사이트 로그인 : https://developers.kakao.com/
- 내 애플리케이션에서 애플리케이션 추가하기 진행.
- 생성된 항목 클릭하여 진입 후 플랫폼으로 이동. Web 플랫폼 등록.
- 아래와 같이 로컬 등록, 추후 실제 도메인 있는 경우 해당 도메인 등록 (http://localhost:8081)
- 카카오 로그인 항목으로 이동. 활성화 진행.
- 밑에 Redirect URI 등록 클릭 후 로컬 등록. (http://localhost:8001/auth/kakao/callback)
- 동의항목으로 이동. 받을것들 선택 진행. (닉네임, 프로필 사진, 이메일 등)
- 앱 키로 이동. REST API 키 복사 진행 후 .env에 아래와 같이 추가.
KAKAO_ID=복사한 REST_API 키 등록
반응형
'인프런 강의 학습 > Node.js 교과서' 카테고리의 다른 글
Node.js학습_MySQL (0) | 2021.10.17 |
---|---|
Node.js학습_몽고디비 (0) | 2021.10.15 |
Node.js 학습_익스프레스 웹 서버 만들기 (0) | 2021.10.07 |
Node.js 학습_npm(패키지 매니저) (0) | 2021.10.05 |
Node.js 학습_http 모듈로 서버 만들기 (0) | 2021.10.04 |