반응형

# 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

  • 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
 

GitHub - ZeroCho/nodejs-book

Contribute to ZeroCho/nodejs-book development by creating an account on GitHub.

github.com

  • 각 파일 생성 완료 후 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) 리타이렉트할 경로를 지정.

### 카카오 로그인 앱 만들기

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

  • 내 애플리케이션에서 애플리케이션 추가하기 진행.
  • 생성된 항목 클릭하여 진입 후 플랫폼으로 이동. Web 플랫폼 등록.
  • 아래와 같이 로컬 등록, 추후 실제 도메인 있는 경우 해당 도메인 등록 (http://localhost:8081)

  • 카카오 로그인 항목으로 이동. 활성화 진행. 

  • 밑에 Redirect URI 등록 클릭 후 로컬 등록. (http://localhost:8001/auth/kakao/callback)

  • 동의항목으로 이동. 받을것들 선택 진행. (닉네임, 프로필 사진, 이메일 등)

  • 앱 키로 이동. REST API 키 복사 진행 후 .env에 아래와 같이 추가.
KAKAO_ID=복사한 REST_API 키 등록

 

반응형

+ Recent posts