개발후기/node.js

[Node.js]sns 서비스 제작: Trip_Share 데모 버전

빨대도둑 2023. 9. 6. 21:44

해당 프로젝트는 Trip_Share를 제작하기 이전에 미리 제작한 데모 버전 입니다.

 

사용한 코드는 제일 하단에 링크를 첨부했습니다. 

 

이 밖에 사용한 기술에 대한 설명은 다음 링크를 참고해주시기 바랍니다. 

물론 바로 아래에서도 몇몇개의 코드에 대해서 설명을 할 예정입니다. 


 

해당 글에서 만든 프로그램을 바탕으로 다음의 프로젝트를 제작하였습니다. 

 

https://brilliant-star.tistory.com/166

 

[Node.js]Trip Share

자신이 여행을 다녀온 곳을 공유할 수 있는 사이트 입니다. 배포를 Heroku를 통해서 하려고 했지만 Procfile의 문제 때문에 배포는 하지 못했습니다. 사용한 코드의 링크는 아래 첨부했습니다. https:/

brilliant-star.tistory.com

 

 

 

각 코드에 대한 설명은 구현 이미지, 동영상 아래 첨부했습니다.  


 

sns 코드(api 제작까지 작성하였습니다)

https://github.com/Ysungho/node

 

GitHub - Ysungho/node: 노드를 사용해서 만든 블로그 입니다. Trip_share의 데모 버전 입니다.

노드를 사용해서 만든 블로그 입니다. Trip_share의 데모 버전 입니다. . Contribute to Ysungho/node development by creating an account on GitHub.

github.com

 

위의 주소에서 nodebird 폴더에 구성했습니다. 


 

추가로 api와 node test에 관한 설명은 다음 포스팅 글을 참고해 주시기 바랍니다. 

 

https://brilliant-star.tistory.com/175

 

[Node.js]node 게시판 api

다음 포스팅에서 사용한 게시판 api에 대한 자세한 설명 글 입니다. 해당 github에는 주석으로 설명을 하지 않았지만 이번 포스팅에서 따로 설명을 추가하겠습니다. https://github.com/Ysungho/node GitHub -

brilliant-star.tistory.com

 

https://brilliant-star.tistory.com/176

 

[Node.js]node sns 테스트코드

다음 포스팅에서 사용한 sns 테스트에 대한 자세한 설명 글 입니다. 해당 github에는 주석으로 설명을 하지 않았지만 이번 포스팅에서 따로 설명을 추가하겠습니다. https://github.com/Ysungho/node GitHub -

brilliant-star.tistory.com

 


구현 사진 및 동영상

sns 시작 화면

 


로그인 이후

 


사진 업로드


 

구현 동영상[sns]

기본 동작 구현: 로그인, 게시글 등록, 검색

 


 

회원가입, 팔로우, 프로필 조회

 


파일 구성

 


 

//node_module, package.json 모델 생성
npm i sequelize mysql2 sequelize-cli
npx sequelize init

npm i express cookie-parser express-session morgan multer dotenv nunjucks
npm i -D nodemon

/서버 시작
npm start

//데이터베이스 생성
npx sequelize db:create

//카카오톡 로그인 방법
npm i passport passport-local passport-kakao bcrypt

//이미지 업로드
npm i multer

 


 

package.json

{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "익스프레스로 만드는 SNS 서비스",
  "main": "app.js",
  "scripts": {
    "start": "nodemon server",
    "test": "jest",
    "coverage": "jest --coverage"
  },
  "author": "sungho",
  "license": "MIT",
  "dependencies": {
    "bcrypt": "^4.0.1",
    "cookie-parser": "^1.4.6",
    "dotenv": "^8.6.0",
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "morgan": "^1.10.0",
    "multer": "^1.4.4",
    "mysql2": "^2.3.3",
    "nunjucks": "^3.2.4",
    "passport": "^0.4.1",
    "passport-kakao": "^1.0.1",
    "passport-local": "^1.0.0",
    "sequelize": "^5.22.5",
    "sequelize-cli": "^5.5.1"
  },
  "devDependencies": {
    "artillery": "^2.0.0-34",
    "jest": "^29.6.1",
    "nodemon": "^2.0.22",
    "supertest": "^6.3.3"
  }
}

사용자와 게시물 간, 게시물과 해시태그 간의 관계가 중요하기 때문에 NoSQL 대신에 SQL을 데이터 베이스로 사용

 


 

models/user.js

const Sequelize = require('sequelize');

module.exports = class User extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      email: {
        type: Sequelize.STRING(40),
        allowNull: true,
        unique: true,
      },
      nick: {
        type: Sequelize.STRING(15),
        allowNull: false,
      },
      password: {
        type: Sequelize.STRING(100),
        allowNull: true,
      },
      provider: {
        type: Sequelize.STRING(10),
        allowNull: false,
        defaultValue: 'local',
      },
      snsId: {
        type: Sequelize.STRING(30),
        allowNull: true,
      },
    }, {
      sequelize,
      timestamps: true,
      underscored: false,
      modelName: 'User',
      tableName: 'users',
      paranoid: true,
      charset: 'utf8',
      collate: 'utf8_general_ci',
    });
  }

  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',
    });
  }
};

User 모델과 Post 모델은 1:N 관계에 있으므로 hasMany로 연결되어 있습니다.

user, getPosts, user.addPosts 같은 관계 메서드들이 생성됩니다.

같음 모델끼리도 N:M 관계를 가질 수 있습니다. 팔로잉 기능이 대표적으로 N:M 입니다.

사용자 한명이 팔로워를 여러명 가질 수 도 있고, 한 사람이 여러명을 팔로잉할 수도 있습니다.

 

같은 테이블 간에 N:M 관계에서는 모델 이름과 컬럼 이름을 따로 정해야 합니다.

‘through’ 옵션을 사용해 생성할 모델의 이름을 Follow로 정했습니다.\

Follow 모델에서 사용자 아이디를 저장하는 컬럼 이름이 둘 다 UserId이면 누가 팔로워고 누가 팔로잉 중인지 구분되지 않으므로 따로 설정해야 합니다.

 

foreignKey 옵션에 각각 followerId, followingId를 넣어줘서 두 사용자 아이디를 구분합니다.

같은 테이블 간의 N:M 관계에서는 as 옵션도 넣어야 합니다. 둘 다 User 모델이라 구분되지 않기 때문입니다.

주의할 점은 as는 foreignKey와 반대되는 모델을 가리킨다는 것입니다.

 

foreignKey가 follerId(팔로워 아이디)면 as는 following 이고, freignKey가 followingId면 as는 followers여야 합니다.

followers를 찾으려면 먼저 팔로잉 하는 사람의 아이디(followingId)를 찾아야 하는 것이라고 생각하면 됩니다.

 


 

models/post.js

const Sequelize = require('sequelize');

module.exports = class Post extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      content: {
        type: Sequelize.STRING(140),
        allowNull: false,
      },
      img: {
        type: Sequelize.STRING(200),
        allowNull: true,
      },
    }, {
      sequelize,
      timestamps: true,
      underscored: false,
      modelName: 'Post',
      tableName: 'posts',
      paranoid: false,
      charset: 'utf8mb4',
      collate: 'utf8mb4_general_ci',
    });
  }

  static associate(db) {
    db.Post.belongsTo(db.User);
    db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
  }
};

User 모델과Post 모델은 1:N 관계이므로 belongsTo로 연결되어 있습니다.

시퀼라이즈는 Post 모델에 User 모델의 id를 가리키는 UserId 컬럼을 추가합니다.

belongsTo는 게시글에 붙습니다. → post.getUser, post.addUser와 같은 관계 메서드가 생성됩니다.

 


 

models/hashtags.js

const Sequelize = require('sequelize');

module.exports = class Hashtag extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      title: {
        type: Sequelize.STRING(15),
        allowNull: false,
        unique: true,
      },
    }, {
      sequelize,
      timestamps: true,
      underscored: false,
      modelName: 'Hashtag',
      tableName: 'hashtags',
      paranoid: false,
      charset: 'utf8mb4',
      collate: 'utf8mb4_general_ci',
    });
  }

  static associate(db) {
    db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
  }
};

Hashtag 모델은 Post모델과 N:M 관계이므로 관계를 설정했습니다.

NodeBird의 모델은 총 다섯개→ User, Hashtag, Post, PostHashtag, Follow.

자동으로 생성된 모델도 다음과 같이 접근할 수 있습니다.

db.sequelize.models.PostHashtag
db.sequelize.models.Follow

 


 

models/index.js

const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');

const db = {};
const sequelize = new Sequelize(
  config.database, config.username, config.password, config,
);

db.sequelize = sequelize;
db.User = User;
db.Post = Post;
db.Hashtag = Hashtag;

User.init(sequelize);
Post.init(sequelize);
Hashtag.init(sequelize);

User.associate(db);
Post.associate(db);
Hashtag.associate(db);

module.exports = db;

 


 

passport/kakaoStrategy.js

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 && profile._json.kaccount_email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser);
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};

 


 

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

serializeUser 는 로그인 시 실행되며 req.session(세션) 객체에 어떤 데이터를 저장할지 정하는 메서드 입니다.

매개변수로 user을 받고 나서 done 함수에 두 번째 인수로 user.id를 넘기고 있습니다.

done 함수의 첫 번째 인수는 에러 발생 시 사용하는 것이고, 두 번째 인수에는 저장하고 싶은 데이터를 넣습니다.

로그인 시 사용자 데이터를 세션에 저장하는데 세션에 사용자 정보를 모두 저장하면 세션의 용량이 커지고 데이터 일관성에 문제가 발생하므로 사용자의 아이디만 저장하라고 명령한 것입니다.

 

serializeUser가 로그인 시에만 실행된다면 deserializeUser는 매 요청 시 실행됩니다.

paassport.session 미들웨어가 이 메서드를 호출합니다. serializeUser의 done의 두번째 인수로 넣었던 데이터가 deserializeUser의 매개변수가 됩니다.

 

조금 전에 serializeUser에서 세션에 저장했던 아이디를 받아 데이터베이스에서 사용자 정보를 조회합니다.

조회한 정보를 req.user에 저장하므로 앞으로 req.user를 통해 로그인한 사용자의 정보를 가져올 수 있습니다.

⇒ 즉 serialUser는 사용자 정보 객체를 세션에 아이디로 저장하는 것이고, deserializerUser는 세션에 저장한 아이디를 통해 사용자 정보 객체를 불러오는 것 입니다.

 

세션에 불필요한 데이터를 담아두지 않기 위한 과정입니다.

 

전체 과정

  1. 라우터를 통해 로그인 요청이 들어옴
  2. 라우터에서 passport.authenticate 메서드 호출
  3. 로그인 전략 수행
  4. 로그인 성공 시 사용자 정보 객체와 함께 req.login 호출
  5. req.login 메서드가 passport.serializeUser 호출
  6. req.session에 사용자 아이디만 저장
  7. 로그인 완료

 

로그인 이후 과정

  1. 요청이 들어옴
  2. 라우터에 요청이 도달하기 전에 passport.session 미들웨어가 passport.deserializeUser 메서드 호출
  3. req.session에 저장된 아이디로 데이터베이스에서 사용자 조회
  4. 조회된 사용자 정보를 req.user에 저장
  5. 라우터에서 req.user 객체 사용 가능

 


 

로컬 로그인 구현

로컬로그인: 다른 SNS 서비스를 통해 로그인 하지 않고 자체적으로 회원가입 후 로그인하는 것을 의미

 

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;

 


 

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 {
    console.log(req.user);
    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;

 


 

routes/user.js

const express = require('express');

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

const router = express.Router();

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

module.exports = router;

 


 

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}`);
  }
};

passport 는 req 객체에 isAUthenticated 메서들 추가합니다. 로그인 중이면 req.isAuthenticated 가 true이고, 그렇지 않으면 false입니다.

따라서 로그인 여부를 이 메서드로 파악할 수 있습니다. 라우터들 중에 로그아웃 라우터나 이미지 업로드 라우터 등은 로그인한 사람만 접근할 수 있게 해야 하고,

회원가입, 라우터나 로그인 라우터는 로그인 하지 않은 사람만 접근할 수 있게 해야 합니다.

→이럴 때 라우터에 로그인 여부를 검사하는 미들웨어를 넣어 걸러낼 수 있습니다.

 


 

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();

//==1==
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');
    }
    const hash = await bcrypt.hash(password, 12);
    await User.create({
      email,
      nick,
      password: hash,
    });
    return res.redirect('/');
  } catch (error) {
    console.error(error);
    return next(error);
  }
});

//==2==
router.post('/login', isNotLoggedIn, (req, res, next) => {
  passport.authenticate('local', (authError, user, info) => {
    if (authError) {
      console.error(authError);
      return next(authError);
    }
    if (!user) {
      return res.redirect(`/?loginError=${info.message}`);
    }
    return req.login(user, (loginError) => {
      if (loginError) {
        console.error(loginError);
        return next(loginError);
      }
      return res.redirect('/');
    });
  })(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙입니다.
});

//==3//
router.get('/logout', isLoggedIn, (req, res) => {
  req.logout();
  req.session.destroy();
  res.redirect('/');
});

router.get('/kakao', passport.authenticate('kakao'));

router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/',
}), (req, res) => {
  res.redirect('/');
});

module.exports = router;
  1. 회원가입 라우터 입니다. 기존에 같은 이메일로 가입한 사용자가 있는지 조회한 뒤, 있다면 회원가입 페이지로 되돌려 보냅니다. 단 주소 뒤에 에러 쿼리스트링으로 표시합니다. 같은 이메일로 가입한 사용자가 없다면 비밀번호를 암호화하고, 사용자 정보를 생성합니다.
  2. 로그인 라우터입니다. 로그인 요청이 들어오면 passport.authenticate(’local’) 미들웨어가 로컬 로그인 전략을 수행합니다. 미들웨어인데 라우터 미들웨어 안에 들어있습니다. 내부 미들웨어에서 (req, res, next)를 인수로 제공해서 호출하면 됩니다.
  3. 로그아웃 라우터입니다. req.logout 메서드는 req.user 객체를 제거하고, req.sesison.destroy는 req.session 객체의 내용을 제거합니다. 세션 정보를 지운 후 메인 페이지로 되돌아 가면 로그인은 해제되어 있습니다.

 


 

controllers/user.js

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

exports.addFollowing = 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);
  }
};

 


 

config/config.json

{
  "development": {
    "username": "root",
    "password": "0806",
    "database": "nodebird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": "0806",
    "database": "nodebird_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

 


 

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) => {
  console.error(err);
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 


 

server.js

const app = require('./app');

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기중');
});

 


 

핵심 내용

  • 서버는 요청에 응답하는 것이 핵심 임무이므로 요청을 수락하든 거절하든 상관없이 반드시 응답해야 합니다. 이때 한번만 응답해야 에러가 발생하지 않습니다.
  • 개발 시 서버를 매번 수동으로 재시작하지 않으려면 nodemon을 사용하는 것이 좋습니다.
  • detenv 패키지와 .env 파일로 유출되면 안 되는 비밀 키를 관리합니다
  • 라우터는 routes 폴더에, 데이터베이스는 models 폴더에, html 파일은 views폴더에 구분하여 저장하면 프로젝트 규모가 커져도 관리하기 편합니다.
  • 데이터 베이스를 구성하기 전에 1:1 1:N N:M 인지 관계를 잘 살펴야 합니다.
  • routes/middlewares.js 처럼 라우터 내에 미들웨어를 사용할 수 있습니다.
  • 프론트엔드 form 태그의 인코딩 방식이 multipart 일 때는 multer 같은 multipart 처리용 패키지를 사용하는 것이 좋습니다.