Node.js 교과서 : 9. 노드버드 SNS 만들기

전지연·2025년 11월 19일

GDGoC_2025

목록 보기
10/16
post-thumbnail

프로젝트 구조 갖추기

package.json 생성

=> 파일을 직접 만들거나 npm init 명령어를 사용

  • 형광펜 친 부분만 알아서 작성

버전은 최소로 -> 한번 올리면 내릴 수 없음

필요한 패키지 다운

npm i sequelize mysql2 sequelize-cli
npx sequelize init

public, passport, views, routes 라이브러리, .env 파일, app.js 파일 생성

익스프레스 라이브러리 설치

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();    // process.env 설정
const pageRouter = require('./routes/page');

const app = express();
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {    // views에서 error.html을 넌적스가 찾아서 응답으로 보내줌
    express: app,
    watch: true,
});

app.use(morgan('dev'));     // 배포 시 combined로 변경
app.use(express.static(path.join(__dirname, 'public')));
// static -> 프론트에서 자유롭게 public에 접근할 수 있도록
app.use(express.json());                            // json 요청 받을 수 있도록
app.use(express.urlencoded({ extended: false }));   // form 요청 받을 수 있도록
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
        httpOnly: true,   // 자바스크립트에서 접근 못하도록
        secure: false,    // http 적용 시 true로 변경
    }
}));

app.use('/', pageRouter);
app.use((req, res, next) => {     // 404에러
    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에 key를 따로 작성해 주어야 한다.

.env
COOKIE_SECRET=nodebirdsecret

= 앞뒤에 띄어쓰기 없도록, 맨 뒤에 세미콜론 없도록


routes/page.js

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

router.use((req, res, next) => {
    res.locals.user = null;
    res.locals.followerCount = 0;
    res.locals.followingCount = 0;
    res.locals.followingIdList = [];
    next();    // next 빼먹으면 밑의 라우터가 실행이 안됨
})

router.get('/profile', renderProfile);   // render-- : 컨트롤러, 폴더로 분리
router.get('/join', renderJoin);
router.get('/', renderMain);

module.exports = router;

controllers/page.js

controllers 디렉토리는 따로 생성해 주어야 함

exports.renderProfile = (req, res, next) => {
    // 서비스를 호출
    res.render('profile', { title: '내 정보 - NodeBird' });
};
exports.renderJoin = (req, res, next) => {
    res.render('join', { title: '회원 가입 - NodeBird' });
};
exports.renderMain = (req, res, next) => {
    res.render('main', {
        title: 'NodeBird',
        twits: [],
    });
};

호출 구조 : 라우터 -> 컨트롤러 -> 서비스 순서대로 호출

  • 컨트롤러 : 요청, 응답 뭔지 앎
  • 서비스 : 요청, 응답 뭔지 모름

HTML

views 디렉토리에 html 파일 생성

1. error.html

2. join.html

3. main.html

4. layout.html


데이터베이스 세팅하기

자바스크립트로 sql 사용하기 위해 시퀄라이즈 사용

model 기본 구조 :
클래스 {
. . . . initiate { 모델 정보들 }
. . . . associate { 테이블 관계 }
}

user.js - initiate

const Sequelize = require('sequelize');

class User extends Sequelize.Model {
  
    static initiate(sequelize) {
        User.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.ENUM('local', 'kakao'),    //이메일 로그인, 카카오 로그인 구분
                allowNull: false,
                defaultValue: 'local',
            },
            snsId: {
                type: Sequelize.STRING(30),
                allowNull: true,
            }
        }, {
            // validation : 이메일, snsId 둘다 null 가능 -> 이거 검사
            sequelize,
            timestamps: true,   // createdAt, updatedAt
            underscored: false,   // createdAt -> created_at
            modelName: 'User',
            tableName: 'users',   // 모델네임 : javascript, 테이블네임 : SQL(복수형)
            paranoid: true,   // deletedAt : 유저 삭제일
            charset: 'utf8',
            collate: 'utf8_general_ci',   // db에 한글 저장
        })
    }

    static associate(db) {

    }
}

module.exports = User;
  • User.init : 테이블 설정
  • provider - type: Sequelize.ENUM( . . ) : 이메일 로그인, 카카오 로그인 구분
  • sequelize 추가기능
    • timestamps : createAt, updatedAt 생성
    • underscored : createdAt을 created_at으로 바꿔줌
    • modelName, tableName : 모델 네임은 자바스크립트, 테이블네임은 SQL에서 사용
    • paranoid : deletedAt 생성
    • charset : 보통 utf8 사용(이모티콘까지 사용할 때엔 utf8mb4 사용)
    • collate : DB에 한글 저장

모델 코드 수정한다고 db가 수정되지 않음

수정 방법 :

  • workbench 등을 사용해서 직접 db를 수정
  • sql문을 사용
  • 시퀄라이즈의 migration을 사용해서 수정
  • 개발 시 테이블 삭제 후 서버 재실행(추천)

Post.js - initiate

const Sequelize = require('sequelize');

class Post extends Sequelize.Model {

    static initiate(sequelize) {
        Post.init({
            content: {
                type: Sequelize.TEXT,
                allowNull: false,
            },
            img: {    // 글 1개당 이미지 1개
                type: Sequelize.STRING(200),
                allowNull: true,
            },
        }, {
            sequelize,
            timestamps: true,
            underscored: false,
            paranoid: false,
            modelName: 'Post',
            tableName: 'posts',
            charset: 'utf8mb4',
            collate: 'utf8mb4_general_ci'
        })
    }

    static associate(db) {

    }
}


module.exports = Post;

Hashtag.js - initiate

const Sequelize = require('sequelize');

class Hashtag extends Sequelize.Model {

    static initiate(sequelize) {
        Hashtag.init({
            title: {
                type: Sequelize.STRING(15),
                allowNull: false,
                unique: true,
            },
        }, {
            sequelize,
            timestamps: true,
            underscored: false,
            paranoid: false,
            modelName: 'Hashtag',
            tableName: 'hashtags',
            charset: 'utf8mb4',
            collate: 'utf8mb4_general_ci'
        })
    }

    static associate(db) {

    }
}


module.exports = Hashtag;

models/index.js

위의 코드처럼 만들어진 모델을 불러와서 initiate, associate 해주면 됨

다만 아래 코드처럼 initiate와 associate를 자동화해 줄 수도 있다.

const Sequelize = require('sequelize');
const fs = require('fs');
const path = require('path');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];

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

const basename = path.basename(__filename);   // index.js
fs.readdirSync(__dirname)
  .filter((file) => {   // index.js 제외
    return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
  })
  .forEach((file) => {
    const model = require(path.join(__dirname, file));
    console.log(file, model.name);
    db[model.name] = model;
    model.initiate(sequelize);   // initiate 호출
  });
// associate는 initiate 이후에 호출해야 함
Object.keys(db).forEach((modelName) => {

  if (db[modelName].associate) {
    db[modelName].associate(db);   // associate 호출
  }
});

module.exports = db;

User.js - associate

const Sequelize = require('sequelize');

class User extends Sequelize.Model {

    static initiate(sequelize) { . . }

    static associate(db) {
        db.User.hasMany(db.Post);   // 1:N  유저:게시글
        
        // N:M  팔로잉, 팔로워
        db.User.belongsToMany(db.User, {   // 팔로워
            foreignKey: 'followingId',
            as: 'Followers',
            through: 'Follow',
        });
        db.User.belongsToMany(db.User, {   // 팔로잉
            foreignKey: 'followerId',
            as: 'Followings',
            through: 'Follow',
        });
    }
}

module.exports = User;
  • 팔로워의 foreignKey : followingId
    : 내가 팔로잉하는 사람 찾으려면-> 내 id를 먼저 찾아야함
  • 팔로잉의 foreignKey: followerId
    : 유명 연예인의 팔로워 찾으려면 -> 유명 연예인의 id를 먼저 찾아야함

만약 zero의 팔로잉을 찾는다
-> zero의 id 찾아야함
-> Follow 테이블에서 followerId가 1일때의 followingId를 확인
-> Following 테이블에서 id가 2, 3인 객체를 찾음


Post.js - associate

const Sequelize = require('sequelize');

class Post extends Sequelize.Model {

    static initiate(sequelize) { . . }

    static associate(db) {
        db.Post.belongsTo(db.User);   // N:1  게시글:유저
        
        // N:M  게시글:해시태그,
        db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
    }
}

module.exports = Post;
  • 게시글과 해시테그 테이블은 서로 이름이 달라 헷갈리지 x
    -> foreignKey 설정 안해줘도 됨

Hashtag.js - associate

const Sequelize = require('sequelize');

class Hashtag extends Sequelize.Model {

    static initiate(sequelize) { . . }

    static associate(db) {
        db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });   // N:M  해시태그:게시글
    }
}

module.exports = Hashtag;

실행

config.js에 username과 password를 mysql에 설정한 대로 입력한 후 터미널에 다음과 같은 명령어를 입력한다.

npx sequelize db:create

실행하면 DB가 생성된다.

그리고 app.js에서 sequelize를 연결해 준다.

app.js 일부

...
const { Sequelize, sequelize } = require('./models');

dotenv.config();    // process.env 설정

...

nunjucks.configure( ... );

sequelize.sync({ force: false })   // 서버 실행 시 테이블 생성
    .then(() => {
        console.log('데이터베이스 연결 성공');
    })
    .catch((err) => {
        console.error(err);
    });

app.use(morgan('dev'));
...
  • force: false
    : 오류 발생 시 기존 테이블 삭제 후 재생성
    개발 시에만 사용!!

이제 실행하기 위해 npm start를 해 준다.

실행 완료


세팅 및 회원가입 만들기

필요한 패키지 다운

npm i passport passport-local passport-kakao bcrypt

app.js에 passport 설정

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

dotenv.config();    // process.env 설정
...
const passportConfig = require('./passport');   // 패스포트 설정

const app = express();
passportConfig();   // 패스포트 설정 함수 호출
...

passport 미들웨어

app.js에 passport 미들웨어 작성 시 반드시 app.use(session( ... )) 밑에 작성

app.js 일부

app.use(session({ ... }));   // 세션

app.use(passport.initialize());
app.use(passport.session());
  • passport.session : 쿠키 생성해서 접속 가능하도록 도와줌

로그인 과정
1. /auth/login 라우터를 통해 로그인 요청이 들어옴
2. 라우터에서 passport.authenticate 메서드 호출
3. 로그인 전략(LocalStrategy) 수행
4. 로그인 성공 시 사용자 정보 객체와 함깨 req.login 호출
5. req.login 메서드가 passport.serializeUser 호출
6. req.session에 사용자 아이디만 저장해서 세션 생성
7. express-session에 설정한 대로 브라우저에 connect.sid 세션 쿠키 전송
8. 로그인 완료

passport/index.js

const passport = require('passport');
const local = require('./localStrategy');   // localStrategy, kakaoStrategy 호출
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();
};

middlewares/index.js

exports.isLoggedIn = (req, res, next) => { // 로그인 했는지 판단

  if (req.isAuthenticated()) {   //passport 통해서 로그인 했는지
        next();
    } else {
        res.status(403).send('로그인 필요');
  }
};

exports.isNotLoggedIn = (req, res, next) => { // 로그인 안 했는지 판단
  if (!req.isAuthenticated()) {//passport 통해서 로그인 안했는지
        next();
    } else {
        const message = '로그인한 상태입니다.';
        res.redirect(`/?error=${message}`);
        
  }
};

routes/page.js

  • /profile -> isLoggedIn인 상태일 때만 실행
  • /join -> isNotLoggedIn인 상태일 때만 실행

req.user, req.isAuthenticated, req.login()
-> 원래 있는 것들 아니고 app.js에서 passport를 initialize 할 때 생성된다.

routes/auth.js

const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { join, login, logout } = require('../controllers/auth');

const router = express.Router();

router.post('/join', isNotLoggedIn, async (req, res, next) => {
    
});

router.post('/login', isNotLoggedIn, (req, res, next) => {
    
});

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

module.exports = router;

다음으로 app.js로 가서 페이지라우터 밑에 auth 라우터를 설정한다.

app.use('/', pageRouter);
app.use('/auth', require('./routes/auth'));

이렇게 하면 auth.js로 가는 api는 /auth가 앞에 붙는 api만 있게 됨

controller에 또 auth.js를 생성한다.

controller/auth.js

const { User } = require('../models/user');
const bcrypt = require('bcrypt');   // 비밀번호 암호화
exports.join = (req, res, next) => {
    const { nick, email, password} = req.body;  // req.body -> form 데이터
    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({
            nick,
            email,
            password: hash,
        });
        return res.redirect('/');  //메인화면으로 - 회원가입 성공(passport랑 아무 상관 없음)
    } catch (err) {
        console.error(err);
        return next(err);
    }
}


exports.login = (req, res) => {
}

exports.logout = (req, res) => {
}

passport로 이메일 로그인 만들기

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

done(서버 실패, 성공 유지, 로직 실패)

  • done(null, exUser) : 서버 성공, user정보 전달
  • done(null, false, { message : ' . . ' }) : 서버 실패, 실패 이유 전달

done 호출 -> controllers/auth.js의 passport.authenticate로 전달

  • serializeUser에서 done의 두 번째 인자에 user의 id가 들어감
    -> deserializeUser에 done을 통해 user의 id가 전달
    -> findOne에서 id를 가지고 user의 정보를 리턴
    -> req.user 생성

  • serializeUser : 로그인할 때만 실행
    deserializeUser : 각 요청마다 실행

  • req.user가 생성될 때 req.session도 같이 생성됨

세션 : { 123456789 : 1} => { 세션쿠키: 유저아이디 }
app.js의 app.use에 connect.sid라는 이름으로 세션 쿠키가 브라우저로 전송

브라우저 connect.sid = 123456789
=> 클라이언트가 받고 나서 cookieParser로 객체 생성 : { connect.sid: 123456789 }

controllers/auth.js
localStrategy.js에서 호출된 done은 passport.authenticate로 전달

const { User } = require('../models/user');
const bcrypt = require('bcrypt');   // 비밀번호 암호화
exports.join = (req, res, next) => { ... }

// 이메일 로그인 컨트롤러
// POST /auth/login
exports.login = (req, res, next) => {
    passport.authenticate('local', (authError, user, info) => {    // localStrateygy 호출
        if (authError) {
            console.error(authError);
            return next(authError);
        }
        if (!user) {   // 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);
};

exports.logout = (req, res) => {
}
  • authError 없고 !user = false
    => 서버 로그인 성공(return)

로그인 이후의 과정
1. 요청이 들어옴
2. 라우터에 요청이 도달하기 전에 passport.session 미들웨어가 passport.deserialzeUser 메서드 호출
3. connect.sid 세션 쿠키를 읽고 세션 객체를 찾아서 req.session으로 만듦
4. req.session에 저장된 아이디로 데이터베이스에서 사용자 조회
5. 조회된 사용자 정보를 req.user에 저장
6. 라우터에서 req.user 객체 사용 가능

req.locals : 미들웨어간 공유되는 데이터
req.sessions : 사용자에게 묶여있는 데이터

logout 원리 : 세션 쿠키와 유저 id 연결 삭제


카카오 로그인

passport-kakao 모듈로부터 Strategy 생성자를 불러와 전략을 구현
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.kakao_account_email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser);
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};
  • clientId : 카카오에서 발급해주는 아이디
  • async에서 callbackURL을 통해 받은 엑세스 토큰, 리프레시 토큰, 사용자 프로필을 받음
    (profile은 카카오톡 정보 상태에 따라 바뀜 -> console로 확인 필요)
  • const exUser = await User.findOne( . . )
    : 기존 유저 확인
  • profile 에서 email, nick, snsId를 추출

routes/auth.js

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

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

router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/?error=카카오로그인 실패',
}), (req, res) => {
  res.redirect('/');
});

module.exports = router;

미들웨어 확장 패턴

  • app.use(passport.authenticate(...)
  • app.use((req, res, next) => passport.authenticate(...)(req, res, next));
    이 둘은 같음
    then 확장 이유 : req, res를 authenticate 안에서 사용하기 위해

카카오 로그인 흐름
/auth/kakao -> 카카오톡로그인화면 -> /auth/kakao/callback

카카오 로그인 설정

  1. 카카오 디벨로퍼스 페이지로 들어간 후 카카오계정 로그인을 진행한다.

  2. 새 애플리케이션 생성

  3. 항목 입력 후 앱 키 가져오기

  4. .env 파일에 rest api 키를 작성ㅌ

  5. 플랫폼 설정 - 웹 플랫폼으로 선택
    도메인 주소 작성

  6. Redirect URI 설정
    http://localhost:8001/auth/kakao/callback

  7. 카카오 로그인 활성화

  8. 개인정보 수집 동의 항목 설정


게시글, 이미지 업로드하기

필요한 패키지 다운

npm i multer

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.js

  • post 라우터 등록
    (app.js 일부)
...
const postRouter = require('./routes/post');
...

app.use('/post', require('./routes/post'));
...
  • 프론트로 사진 리턴
app.use(express.static(path.join(__dirname, 'public')));
// 추가
app.use('/img', express.static(path.join(__dirname, 'uploads')));

controllers/post.js

  • 게시글에서 해시태그 추출

문자열 : "어쩌구저쩌구. #노드 #express"
여기서 #만 가져와야 함
=> 정규표현식 사용: #[^\s#]#/g

const Post = require('../models/post');
const Hashtag = require('../models/hashtag');

exports.afterUploadImage = (req, res) => {
    console.log(req.file);
    res.json({ url: `/img/${req.file.filename}` });       // 프론트에 이미지 url 응답
}

// 게시글 업로드 컨트롤러
exports.uploadPost = async (req, res, next) => {
    // req.body.content, req.body.url
    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 없으면 db에 반영 안됨
            console.log('result', result);
            await post.addHashtags(result.map(r => r[0]));  // 다대다 관계 메서드
        }
        res.redirect('/');
   }
    catch (error) {
        console.error(error);
        next(error);
    }
};

controllers/page.js

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

exports.renderProfile = (req, res, next) => {
    // 서비스를 호출
    res.render('profile', { title: '내 정보 - NodeBird' });
};
exports.renderJoin = (req, res, next) => {
    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: [],
        });
    } catch (error) {
        console.error(error);
        next(error);
    }
};

팔로우, 해시태그 검색 기능 만들기

팔로우 기능 구현

  • routes/user.js
const express = require('express');
const { isLoggedIn } = require('./middlewares');
const router = express.Router();
const { follow } = require('../controllers/user');

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

module.exports = router;
  • app.js에서 라우터를 선언해 줘야 한다.
const userRouter = require('./routes/user');
  • controllers/user.js
const User = require('../models/user');

exports.follow = 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);
    }
}
  • passport/index.js

팔로우 기능을 실행했을 때 프론트에서 어떤 일이 벌어질 지도 체크해 주어야 한다.

req.user가 만들어 졌을 때 팔로잉 수와 팔로우 수를 가져올 수 있게 수정

...
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));
  });
...
  • routes/page.js
...
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();    // next 빼먹으면 밑의 라우터가 실행이 안됨
})
...
  • req.user에서 follower 수와 following 수를 가져옴
  • req.user는 로그인 해야 생김
    => 로그인 전에는 null, 에러 생길 수 있음
    => 옵셔널 체인징 처리(?붙임) -> null이어도 에러 x
    => '|| 0' 사용

해시태그 검색 기능

  • routes/page.js
...
const { renderJoin, renderMain, renderProfile, renderHashtag } = require('../controllers/page');
...
router.get('/hashtag', renderMain);
  • controllers/page.js
    해시태그 먼저 찾음 -> 그 다음 해시태그 포함된 게시글 불러오기

핵심 정리

0개의 댓글