상세 컨텐츠

본문 제목

[Node.js] 9장 익스프레스로 SNS 서비스 만들기

21-22/21-22 Node.js

by Kimpeep 2021. 12. 27. 13:00

본문

728x90

1. 프로젝트 구조 갖추기

(1) package.json 생성

- 프로젝트를 생성할 폴더로 이동 후 npm init(5장) 명령어 실행

package.json

(2) 관계형 데이터베이스 MySQL 사용을 위해 시퀄라이즈 설치

npm i sequelize mysql2 sequelize-cli
npx sequelize init

설치 후 폴더

(3) 추가로 필요한 views / routes / public / passport 폴더 생성 + app.js / .env 파일 생성

  • views : 템플릿 파일 넣음
  • routes : 라우터 넣음
  • public : 정적 파일 넣음
  • passport : passport 패키지
  • .env  : 설정값 담음

생성 후

- npm 패키지 설치

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

- 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.COOKE_SECRET,
    cookie: {
        httpOnly: true, 
        secure: false,
    }
}));

app.use('/', pageRouter);

app.use((req, res, next) => {
    const error = new Error(`${req.mothod} ${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

COOKE_SECRET=cookiesecret

(4) routes/page.js , html 파일과 css 파일 생성

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, next) => {
    const twits = [];
    res.render('main', {
        title: 'NodeBird',
        twits,
    });
});

module.exports = router;

- views 폴더에 layout.html / main.html / profile.html / join.html / error.html 생성

https://github.com/ZeroCho/nodejs-book/tree/master/ch9/9.1/nodebird/views

 

GitHub - ZeroCho/nodejs-book

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

github.com

- public/main.css 생성

https://github.com/ZeroCho/nodejs-book/blob/master/ch9/9.1/nodebird/public/main.css

 

GitHub - ZeroCho/nodejs-book

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

github.com

 

2. 데이터베이스 세팅하기

(1) models 폴더 안에 user.js / post.js / hashtag.js 생성 

 

- models/user.js

: 사용자 정보 저장 모델, {이메일, 닉네임, 비밀번호} 저장

: SNS 로그인 시 {provider, snsId} 추가 저장(provider=local이면 로컬 / provider=kakao이면 카카오 로그인)

: timestamp / paranoid -> createAt, updateAt, deleteAt 컬럼 생성됨

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', // 기본 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) {}
}

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

- models/hashtag.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' });
  }
};

- models/index.js

: 7장에서 학습한 시퀄라이즈 생성하는 코드. 자동으로 생성된 코드를 아래와 같이 변경.

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;

 

- models/user.js

: static associate(db) {} 안에 각 모델 간의 관계 정의

  • 1:N 관계: hasMany 키워드 사용. User, Post 모델이 이에 해당함. user.getPosts, user.addPosts 같은 메서드 생성.
  • N:M 관계: 팔로잉 기능처럼 한명이 여러명을 팔로워를 가지고, 한 사람이 여러명을 팔로잉할 수도 있는 관계.
    • 한 테이블 내 조인(N:M)을 위해 foreigKey 옵션으로 같은 칼럼명에 다른 이름을 붙여줌. 
    • 조인으로 새로 생긴 모델의 이름 through 옵션으로 생성
    • as옵션 : foreignKey와 반대되는 모델을 가리킴

출처: Node.js 교과서 개정2판 ebook p.408(조현영 저)

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

 

 

(2) models/post.js

: User와 Post 모델은 1:N관계 - belongsTo로 연결

: Post와 Hashtag 모델은 N:M 관계 - belongsToMany, 모델명 PostHashtag

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

 

(3) 데이터베이스-서버와 연결하기 위해 데이터베이스 생성

 

- config/config.json

{
  "development": {
    "username": "root",
    "password": "nodejsbook",
    "database": "nodebird",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "operatorAliases": false
  },
}

- DB 생성

npx sequelize db:create

db 생성 성공

- 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 { sequelize } = require('./models');

const app = express();
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'));

.
.
.

});

+ 데이터베이스 연결 성공 이라고 뜸

+ 테이블이 없을 때 테이블을 자동으로 생성함

 

3. Passport 모듈로 로그인 구현하기

세션과 쿠키 처리 등 복잡한 작업 없이 검증된 passport모듈을 이용해

 

- app.js

  • passport.initialize 미들웨어 : 요청에 passport 설정 심음
  • passport.session 미들웨어 : req.session 객체에 passport 정보 저장
  • req.session 객체는 express-session에서 생성하므로 express-session 뒤에 연결해야 함
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 { 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(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());
.
.
.
});

 

- passport/index.js

: Passport 관련 코드

  • passport.serializeUser : 로그인시 실행, req.session(세션) 객체에 어떤 데이터를 저장할지 정하는 메서드
    • 매개변수로 user를 받음
    • done 함수에 두번째 인수로 user.id를 넘김(원하는 데이터 넣으면 됨)
    • done 함수의 첫 번째 인수는 에러 발생 시 사용, 두 번째 인수에는 저장하고 싶은 데이터 넣음
  • passport.deserializeUser : 매요청시 실행됨, passport.session 미들웨어가 이 메소드를 호출
    • done의 두 번째 인수가 deserializeUser의 매개변수가 됨
    • user.id를 받아 데이터베이스에서 사용자 정보를 조회함
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();
};
  • serializeUser : 사용자 정보 객체를 세션에 아이디로 저장하는 것
  • deserializeUser : 세션에 저장한 아이디를 통해 사용자 정보 객체를 불러옴

로그인 과정

  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 객체 사용 가능

 

(1) 로컬 로그인 구현하기

 

- 회원가입 구현하기

  • routes/middlewares.js
    • 로그인한 사용자 : 회원가입과 로그인 라우터에 접근 X -> 라우터에 접근 권한을 제어하는 미들웨어 추가                                            (req.isAuthenticated)
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
    • isLoggedIn / isNotLoggedIn 사용
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');

const router = express.Router();

router.use((req, res, next) => {
    // locals로 선언하여 모든 템플릿 엔진에서 공통 사용
    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('/', (req, res, next) => {
    const twits = [];
    res.render('main', {
      title: 'NodeBird',
      twits,
    });
  });
  
  module.exports = router;
  • 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;
  • localStrategy.js
    • LocalStrategy(전략 설정값, 실제 전략을 수행하는 async 함수)
    • 사용자가 데이터베이스에서 일치하는 이메일이 있는지 찾음 - 있다면 비밀번호 비교 - 일치한다면 done 함수의 두번째 인수로 사용자 정보를 넣어 보냄
      • 로그인에 실패했을 때만 done 함수의 두 번째 인수를 사용하지 않음
      • done함수의 첫 번째 인수를 사용하는 경우 : 서버쪽에서 에러 발생
      • done함수의 세 번째 인수를 사용하는 경우 : 로그인 처리 과정에서 비밀번호 일치X or 존재X 등의 사용자 정의 에러 발생
    • done이 호출된 후 다시 passport.authenticate의 콜백 함수 호출
    • 로그인 성공 시 리다이렉트되며 회원정보 뜸
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');

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

module.exports = () => {

	// LocalStrategy 생성자
  passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'password',
    // 실제 Strategy 수행
  }, 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);
    }
  }));
};

(2) 카카오 로그인 구현하기

  • 로그인 과정에서 검증을 카카오가 대신 해 줌
  • 회원가입 절차가 따로 없음
  • 처음 로그인시 회원가입 처리, 그 다음부터는 로그인 처리
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,//카카오에서 발급하는 ID, process.env를 통해 노출 방지
    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.kakao_account_email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser);
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};
  • routes/auth.js
    • 카카오 로그인 라우터
    • 카카오 로그인은 로그인 성공 시 내부적으로 req.login을 호출하므로 callback함수를 실행할 필요가 없음
    • 콜백 함수 대신 로그인에 실패했을 때 어디로 이동할지를 failureRedirect 속성에 적음, 그 다음 미들웨어에 성공 시 갈 곳을 적음
.
.
.
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;
  • app.js

위에서 이미 작업함 - auth라우터 추가

  • REST API 키를 복사하여 .env파일에 넣음
KAKAO_ID = RESTAPI키
  • 내 애플리케이션 > 앱설정 > 플랫폼 > Web > Web 플랫폼 등록 > http://localhost:8001

  • 제품 설정 > 카카오 로그인 > Redirect URI에 http://localhost:8001/oauth 지우고 http://localhost:8001/auth/kakao/callback(kakaoStrategy.js의 callback과 일치)

  • 제품설정 > 카카오 로그인 > 동의항목 을 아래와 같이 수정

 

 

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

  • multer 모듈 설치
npm i multer
  • routes/post.js
    • /post/img 라우터 : 이미지 하나를 업로드받은 뒤 이미지의 저장 경로를 클라이언트로 응답
    • /post 라우터 : 게시글 업로드를 처리하는 라우터, 이미지가 업로드됐다면 이미지 주소는 req.body.url로 전송
    • 해시태그 추출 -> 데이터베이스에 저장 -> 해시태그 모델을 post.addHashtags 메서드로 게시글과 연결
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;
  • routes/page.js
    • 메인 페이지 로딩 시 메인 페이지와 게시글을 함께 로딩
    • 데이터베이스에서 post 조회 -> twits에 넣어 렌더링
. .. 
const { Post, User } = require('../models');

const router = express.Router();
 .
 .
 .

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;

 

5. 프로젝트 마무리

  • routes/user.js - 팔로우 기능
    • /user/:id/follow 라우터 : :id 부분이 req.params.id가 됨
    • 팔로우할 사용자 db에서 조회 -> addFollowing 메서드로 현재 로그인한 사용자와의 관계 지정
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;
  • passport/index.js
    • 세션에 저장된 아이디로 사용자 정보 조회 시 팔로잉 목록 + 팔로워 목록 함께 조회
    • include -> attributes는 실수로 비밀번호 조회하는 것을 방지하기 위함
  .
  .
  .
  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();
};
  • routes/page.js
    • 팔로잉/팔로워 숫자와 팔로우 버튼 표시
    • /hashtag 라우터 : 쿼리스트링으로 해시태그 이름 받고 해시태그 값이 없는 경우 메인페이지로 redirect
    • 해시태그 검색 -> 있다면 getPosts 메서드로 모든 게시글 가져옴(User 정보를 합쳐서) -> twits에 넣어 렌더링
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User, Hashtag } = require('../models');

const router = express.Router();

router.use((req, res, next) => {
	// user 정보 가져와서 팔로잉 / 팔로워 수 저장
  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;
  • app.js
    • routes/post.js와 routes/user.js를 app.js에 연결
    • 업로드한 이미지를 제공할 라우터(/img)를 express.static 미들웨어로 uploads 폴더와 연결
    • express.static은 여러번 사용 가능
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');

.
.
.

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('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);

.
.
.
728x90

관련글 더보기