본문 바로가기

WINK-(Web & App)/Express.js (Node.js) 스터디

[2024-2 Node.js 스터디] 김민재 #5주차

반응형

프로젝트 구조 갖추기


npm init
npm i sequelize mysql2 sequelize-cli
npx sequelize init
템플릿을 넣을 views
라우터를 넣을 routes
정적파일을 넣은 public 

위 코드와 파일들을 생성하며 폴더 구조를 형성한다

routes/page.js

const express = require('express');
const { renderProfile, renderJoin, renderMain } = require('../controllers/page');

const router = express.Router();

router.use((req, res, next) => {
    res.locals.user = null;
    res.locals.followerCount = 0;
    res.locals.followingCount = 0;
    res.locals.followingIdList = [];
    next();
});

router.get('/profile', renderProfile);

router.get('/join', renderJoin);

router.get('/', renderMain);

module.exports = router;

get /profile, get /join, get / 으로 페이지 세 개로 구성했고
router.use로 템플릿 엔진에서 사용할 user, followingCount, followerCount, followingidList 변수를 res.locals로 설정했다. 
res.locals로 설정하는 이유는 user와 followingcount, followercount, followingidlist 변수는 모든 템플릿 엔진에서 공통으로 사용되기 때문이다.
controllers/page.js

exports.renderProfile = (req, res) => {
    res.render('profile', { title: '내 정보 - NodeBird' });
};

exports.renderJoin = (req, res) => {
    res.render('join', { title: '회원가입 - NodeBird' });
};

exports.renderMain = (req, res, next) => {
    const twits = [];
    res.render('main', {
        title: 'NodeBird',
        twits,
    });
};

이전과 다르게 controllers에서 따로 미들웨어를 다른 곳에서 불러와 사용한다.
일단은 코드를 관리하기 편하기 위해서라고 알고 있자

  • 기본적인 프론트 구성을 다 하면 아래와 같이 잘 작동하는 것을 볼 수 있다.

데이터베이스 세팅하기


  • MySQL과 시퀄라이즈로 데이터 베이스를 설정한다
  • 로그인 기능이 있으므로 사용자 테이블이 필요하고, 게시글을 저장할 게시글 테이블도 필요하다. 해시태그를 사용하므로 해시태크 테이블도 필요하다
  • models 폴더
    • user.js: 사용자 정보를 저장하고, 로컬과 카카오 로그인 에 따른 설정이 있다
    • post.js: 게시글 내용과 이미지 경로를 저장한다
    • hashtsg.js: 태그 이름을 저장한다.
    • index.js: 객체를 만들지만, 많아지는 모델을 고려해 새로운 구조를 통해 디렉토리에 있는 모든 파일들을 조회하도록 수정했다.
    • associate 함수를 수정해 모델간의 관계를 정했다
      • 같은 테이블 간에서도 관계를 정할 수 있다.
user.js
static associate(db) {
        db.User.hasMany(db.Post);
        db.User.belongsToMany(db.User, {
            foreignKey: 'followingId',
            as: 'Followers',
            through: 'Follow',
        });
        db.User.belongsToMany(db.User, {
            foreignKey: 'followerId',
            as: 'Followings',
            through: 'Follow',
        });
    }
    
post.js
static associate(db) {
        db.Post.belongsTo(db.User);
        db.Post.belongsToMany(db.Hashtag, {through: 'PostHashtag'});
    }

hashtag.js
static associate(db) {
        db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
    }
};
"development": {
    "username": "root",
    "password": "비번",
    "database": "nodebird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
으로 config.json을 수정하고 원래는 데이터 베이스를 따로 만들어야하지만
시퀄라이즈는 config.json을 읽어 데이터베이스를 생성해주는 기능이 있다.
npx sequelize db:create 를 쳐서 데이터베이스를 생성해보자

  • 마지막으로 app.js를 수정해 모델과 서버를 연결해 데이터베이스 세팅을 완료했다.

Passprot 모듈로 로그인 구현하기


npm i passport passport-local passport-kakao bcrypt
를 입력해 passport 관련 패키지들을 설치하자
const passport = require('passport');

const passportConfig = require('./passport');

passportConfig(); // 패스포트 설정

app.use(passport.initialize());
app.use(passport.session());

app.js를 수정해 조금뒤 만들 passport 모듈과 연결해주자
passport/index.js

const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');

module.exports = () => {
  passport.serializeUser((user, done) => {
    done(null, user.id);
  });

  passport.deserializeUser((id, done) => {
    User.findOne({ where: { id } })
      .then(user => done(null, user))
      .catch(err => done(err));
  });

  local();
  kakao();
};
  • 로그인의 전체적인 과정
    • /auto/login 라우터를 통해 로그인 요청이 들어옴
    • 라우터에서 passport.authenticate 메서드 호출
    • 로그인 전략(LocalStrategy) 수행
    • 로그인 성공 시 사용자 정보 객체와 함께 req.login 호출
    • req.login 메서드가 passport.seializeUser 호출
    • req.session에 사용자 아이디만 저장해서 세션 생성
    • express-session에 설정한 대로 브라우저에 connect.sid 세션 쿠키 전송
    • 로그인 완료

로컬 로그인 구현

  • 로컬 로그인이랑 다른 SNS 서비스를 통해 로그인하지 않고 자체적으로 회원가입 후 로그인하는 것
  • passport 모듈은 이미 설치했으므로 로컬 로그인 전략만 세우면 된다. 회원가입, 로그인, 로그아웃 라우터를 먼저 만들어보자.
middlewares/index.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}`);
  }
};

req 객체에 isAuthenticated 메서드를 추가하고, 로그인 중이면 true, 아니면 false이다.

  • routes, controllers 디렉토리에 둘다 auth.js를 만들어 회원 가입, 로그인, 로그아웃을 할 수 있도록 작성했다.
passport/localStrategy.js

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');

const User = require('../models/user');

module.exports = () => {
  passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'password',
    passReqToCallback: false,
  }, 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);
        } else {
          done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
        }
      } else {
        done(null, false, { message: '가입되지 않은 회원입니다.' });
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};

주어진 인수를 받고 이메일과 비밀번호를 받은 뒤, 일치하는지 비교해 리턴한다

카카오 로그인 구현

  • 카카오 로그인이란 로그인 인증 과정을 카카오에 맡기는 것을 뜻한다.
  • 회원가입 절차가 따로 없기 때문에, 처음 로그인할 때는 회원 가입 처리, 두번째 로그인할 때는 로그인 처리를 해야한다.
passport/kakaoStrategy

const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;

const User = require('../models/user');

module.exports = () => {
  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: { snsId: profile.id, provider: 'kakao' },
      });
      if (exUser) {
        done(null, exUser);
      } else {
        const newUser = await User.create({
          email: profile._json?.kakao_account?.email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser);
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};

카카오 로그인 구현을 위해 카카오 로그인에 대한 전략을 구성한다

  • 카카오 개발자 사이트에 들어가 API 키를 발급받아 .env 파일에 넣는다
  • 여기에서 닉네임과 이메일 정보를 받기 위해 동의를 선택해야한다
    • 하지만 이메일은 권한이 없어 동의를 얻지 못했다…
  • 그래도 일단 뭐 로그인은 어찌저찌 구현이 됐다

multer 패키지로 이미지 업로드 구현하기


  • 이미지 업로드를 위해 multer 모듈을 사용해 멀티 파트 형식의 이미지를 업로드 해보자
const { Post, Hashtag } = require('../models');

exports.afterUploadImage = (req, res) => {
  console.log(req.file);
  res.json({ url: `/img/${req.file.filename}` });
};

exports.uploadPost = async (req, res, next) => {
  try {
    const post = await Post.create({
      content: req.body.content,
      img: req.body.url,
      UserId: req.user.id,
    });
    const hashtags = req.body.content.match(/#[^\\s#]*/g);
    if (hashtags) {
      const result = await Promise.all(
        hashtags.map(tag => {
          return Hashtag.findOrCreate({
            where: { title: tag.slice(1).toLowerCase() },
          })
        }),
      );
      await post.addHashtags(result.map(r => r[0]));
    }
    res.redirect('/');
  } catch (error) {
    console.error(error);
    next(error);
  }
};

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const { afterUploadImage, uploadPost } = require('../controllers/post');
const { isLoggedIn } = require('../middlewares');

const router = express.Router();

try {
    fs.readdirSync('uploads');
} catch (error) {
    console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
    fs.mkdirSync('uploads');
}

const upload = multer({
    storage: multer.diskStorage({
        destination(req, file, cb) {
            cb(null, 'uploads/');
        },
        filename(req, file, cb) {
            const ext = path.extname(file.originalname);
            cb(null, path.basename(file.originalname, ext) + Date.now() + ext);
        },
    }),
    limits: { fileSize: 5 * 1024 * 1024 },
});

// POST /post/img
router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);

// POST /post
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost);

module.exports = router;

라우트와 컨트롤러에 post.js를 만들어 게시글을 업로드하는 방식을 구현했다.

또한 page.js를 수정해 조회할 때 게시글 작성자의 아이디와 닉네임을 join해서 제공하고, 게시글 순서를 정렬했다.
exports.renderMain = async (req, res, next) => {
    try {
        const posts = await Post.findAll({
            include: {
                model: User,
                attributes: ['id', 'nick'],
            },
            order: [['createdAt', 'DESC']],
        });
        res.render('main', {
            title: 'NodeBird',
            twits: posts,
        });
    } catch (err) {
        console.error(err);
        next(err);
    }
}

프로젝트 마무리


const User = require('../models/user');

exports.follow = async (req, res, next) => {
  try {
    const user = await User.findOne({ where: { id: req.user.id } });
    if (user) { // req.user.id가 followerId, req.params.id가 followingId
      await user.addFollowing(parseInt(req.params.id, 10));
      res.send('success');
    } else {
      res.status(404).send('no user');
    }
  } catch (error) {
    console.error(error);
    next(error);
  }
};

const express = require('express');

const { isLoggedIn } = require('../middlewares');
const { follow } = require('../controllers/user');

const router = express.Router();

// POST /user/:id/follow
router.post('/:id/follow', isLoggedIn, follow);

module.exports = router;

팔로우 기능을 위해 라우트와 컨트롤러에 user.js를 작성했다.

그리고 팔로잉/팔로워 숫자와 팔로우 버튼을 표시하기 위해 라우트와 컨트롤러 부분을 수정했다.
const { User, Post, Hashtag } = require('../models');

exports.renderProfile = (req, res) => {
    res.render('profile', { title: '내 정보 - NodeBird' });
};

exports.renderJoin = (req, res) => {
    res.render('join', { title: '회원가입 - NodeBird' });
};

exports.renderMain = async (req, res, next) => {
    try {
        const posts = await Post.findAll({
            include: {
                model: User,
                attributes: ['id', 'nick'],
            },
            order: [['createdAt', 'DESC']],
        });
        res.render('main', {
            title: 'NodeBird',
            twits: posts,
        });
    } catch (err) {
        console.error(err);
        next(err);
    }
}

exports.renderHashtag = async (req, res, next) => {
    const query = req.query.hashtag;
    if (!query) {
        return res.redirect('/');
    }
    try {
        const hashtag = await Hashtag.findOne({ where: { title: query } });
        let posts = [];
        if (hashtag) {
            posts = await hashtag.getPosts({ include: [{ model: User }] });
        }

        return res.render('main', {
            title: `${query} | NodeBird`,
            twits: posts,
        });
    } catch (error) {
        console.error(error);
        return next(error);
    }
};

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const {
    renderProfile, renderJoin, renderMain, renderHashtag,
} = require('../controllers/page');

const router = express.Router();

router.use((req, res, next) => {
    res.locals.user = req.user;
    res.locals.followerCount = req.user?.Followers?.length || 0;
    res.locals.followingCount = req.user?.Followings?.length || 0;
    res.locals.followingIdList = req.user?.Followings?.map(f => f.id) || [];
    next();
});

router.get('/profile', isLoggedIn, renderProfile);

router.get('/join', isNotLoggedIn, renderJoin);

router.get('/', renderMain);

router.get('/hashtag', renderHashtag);

module.exports = router;
그리고 이제 모듈들을 다 만들었다면 제일 중요한 연결 차례이다
app.js를 수정해 라우트의 post.js와 user.js를 연결했다

const postRouter = require('./routes/post');
const userRouter = require('./routes/user');

app.use('/post', postRouter);
app.use('/user', userRouter);

  • 터미널에서도 잘 작동하는 것을 볼 수 있다!!!
반응형