본문 바로가기

ComputerScience/NodeJs

node - 9.4 익스프레스로 SNS서비스 만들기 (이미지 업로드, 팔로잉, 해시태그 구현)

728x90

https://thebook.io/080229/

 

더북(TheBook): Node.js 교과서 개정 2판

 

thebook.io

* 위 내용을 정리하였음

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

- 멀티파트 형식의 이미지를 업로드 해보자.

npm i multer

- 이미지는 서버 디스크에 저장되고 그 경로만 데이터 베이스에 저장된다.

- post라우터를 작성해보자.

// routes/post.js

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

const { Post, Hashtag } = require('../models');
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 },
});

router.post('/img', isLoggedIn, upload.single('img'), (req, res) => {
  console.log(req.file);
  res.json({ url: `/img/${req.file.filename}` });
});

const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), 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);
  }
});

module.exports = router;

- app.use('/post')를 할것이기 때문에 POST /post/img 와 POST /post 라우터를 만든다.

- POST /post/img 라우터는 로그인 확인 -> 이미지 업로드 -> 이미지의 저장 경로 클라이언트로 응답

- static 미들웨어가 /img 경로의 정적 파일들을 제공하므로 클라이언트가 이미지에 접근할 수 있다.

 

- POST /post 라우터는 게시글 업로드를 처리한다. 

- 이전 라우터에서 이미지를 업로드하고 req.body.url에 이미지 주소가 전송되었다.

- 게시물은 이미지가 들어 있지 않으므로 none을 사용

- 정규표현식으로 해시태그 부분을 찾고나서 이미 있다면 가져오고 없으면 생성한 후 가져온다. 그리고 모델들을 추출하여 게시물과 해시테그 모델들을 연결한다.

 

- 이렇게 정적 파일 제공을 원한다면 multer-s3, multer-google-storage같은 패키지를 활용하여 클라우드 스토리지를 사용하는게 좋다.

// routes/page.js

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User } = require('../models');

const router = express.Router();

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

router.get('/profile', isLoggedIn, (req, res) => {
  res.render('profile', { title: '내 정보 - NodeBird' });
});

router.get('/join', isNotLoggedIn, (req, res) => {
  res.render('join', { title: '회원가입 - NodeBird' });
});

router.get('/', 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);
  }
});

module.exports = router;

- page.js를 손을 좀 봐서 메인페이지 로딩시 게시물들이 보이도록 한다.

- 게시물 조회시 사용자 모델에서 id, nick 속성을 join해서 게시물 들을 찾는다. 

- twits에 게시물 배열을 넣어 랜더링 한다.

2. 팔로잉, 해시태그 기능 구현

// routes/user.js

const express = require('express');

const { isLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

router.post('/:id/follow', isLoggedIn, async (req, res, next) => {
  try {
    const user = await User.findOne({ where: { id: req.user.id } });
    if (user) {
      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);
  }
});

module.exports = router;

- :id가 req.params.id가 된다. (현재 로그인한 사용자)

- 로그인 했다면 디비에서 팔로우할 사용자를 조회한 후

- 그 사용자의 팔로워를 추가해준다. 

 

- 팔로우 팔로잉 관계가 생성 되었으므로 req.user에도 팔로우 팔로잉 관계를 저장해준다.

- 사용자 정보를 불러올때 팔로우 팔로잉 정보도 함께 불러오기 때문에 deserializeUser를 수정한다.

// 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 },
      include: [{
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followers',
      }, {
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followings',
      }],
    })
      .then(user => done(null, user))
      .catch(err => done(err));
  });

  local();
  kakao();
};

- 라우터 들이 실행되기 전에 deserializeUser가 항상 먼저 실행된다. 따라서 모든 요청마다 사용자 정보를 조회한다.

- 따라서 캐싱을 활용할 필요가 있고 실제 서비스에서는 레디스 같은 데이터베이스에 사용자 정보를 캐싱한다.

- follow, follwer를 표시해야 하므로 page.js도 수정한다.

// routes/page.js

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User, Hashtag } = require('../models');

const router = express.Router();

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

router.get('/profile', isLoggedIn, (req, res) => {
  res.render('profile', { title: '내 정보 - NodeBird' });
});

router.get('/join', isNotLoggedIn, (req, res) => {
  res.render('join', { title: '회원가입 - NodeBird' });
});

router.get('/', 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);
  }
});

router.get('/hashtag', 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);
  }
});

module.exports = router;

- 로그인시에는 req.user가 존재하기 때문에 이것으로 로그인 여부를 확인한다.

- 팔로워 아이디들을 리스트로 저장하는 이유는 팔로워 리스트에 게시글 작성자의 아이디가 없으면 팔로우 버튼을 보여주기 위해서 이다.

 

- 이제 해시태그 검색 라우터를 만들어보자.

- 쿼리스트링으로 해시태그 이름을 받는다.

- 해시태그가 없으면 메인페이지로 가고 있다면 데이터베이스에서 작성자 정보를 Join해서 post들을 다 가져오자.

3. 최종 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');
const passport = require('passport');

dotenv.config();
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const postRouter = require('./routes/post');
const userRouter = require('./routes/user');
const { sequelize } = require('./models');
const passportConfig = require('./passport');

const app = express();
passportConfig(); // 패스포트 설정
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});
sequelize.sync({ force: false })
  .then(() => {
    console.log('데이터베이스 연결 성공');
  })
  .catch((err) => {
    console.error(err);
  });

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/img', express.static(path.join(__dirname, 'uploads')));
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(passport.initialize());
app.use(passport.session());

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

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'), '번 포트에서 대기중');
});

- 업로드한 이미지를 제공할 라우터(/img)도 express.static 미들웨어로 uploads 폴더와 연결한다.

- 이렇게 하면 uploads 폴더 내 사진들이 /img 주소로 제공된다.

app.use('/img', express.static(path.join(__dirname, 'uploads')));

728x90
반응형