09. Express로 SNS 서비스 만들기

Kim Sang Yeob·2023년 1월 26일
1

NodeJS Book 3th

목록 보기
9/9
post-thumbnail

9.1 프로젝트 구조 갖추기

1) package.json 작성하기

  • npm init 이용
{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "익스프레스로 만드는 SNS 서비스",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "yeobi01",
  "license": "MIT"
}

2) 모듈 다운받기

  • sequelize는 mysql의 ORM
  • mysql2는 드라이버 역할을 함
  • npx 명령어를 통해 sequelize 기본설정
$ npm i sequelize mysql2 sequelize-cli
$ npx sequelize init

3) 폴더 구조

4) 나머지 모듈 다운받기

  • cooike-parser는 브라우저에서 받아온 쿠키를 객체화 해줌
  • express-session은 로그인 구현할 때 필요한 모듈
  • morgan은 브라우저와 통신할 때마다 로깅해줌
  • multer는 이미지, 동영상을 업로드할 때 필요한 모듈
  • dotenv는 .env 파일을 읽어와서 환경변수 설정해줌
  • nunjucks는 강의에서 제공하는 html 파일이 nunjucks로 써져있어서..
$ npm i express cookie-parser express-session morgan multer dotenv nunjucks
$ npm i -D nodemon

5) app.js 구성하기

  • app.js는 우주선의 관제실 같은 곳
  • 다운받은 모듈과, exports한 모듈들을 미들웨어로 창작해주는 곳
  • 미들웨어 장착순서가 중요하기 때문에, 논리적인 사고로 순서 잘 정해주기
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', {
    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.COKKIE_SECRET,
    cookie: {
        httpOnly: true,
        secure: false,
    }
}))

app.use('/', pageRouter);
app.use((req, res, next) => { // 404 NOT FOUND
    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'), '번 포트에서 대기 중');
});

6) routes/page.js 구성하기

  • res.locals. 를 통해서 전역변수처럼 사용할 변수 설정하기
  • router.get의 두 번째 인자로 콜백함수를 넣지 않고, 함수 명(컨트롤러라고 부름)을 넣어줘서 분리하기.
  • 따라서 제일 위헤 컨트롤러로 분리해둔 함수들을 구조분해할당으로 불러온다.
  • 당연하게도 controllers/page.js도 구성해야함

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

router.get('/profile', renderProfile);
router.get('/join', renderJoin);
router.get('/', renderMain);

module.exports = router;

controllers/page.js

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: [],
    });
};

// 컨트롤러 : 서비스를 호출함
// 라우터 -> 컨트롤러(요청, 응답 안다) -> 서비스(요청, 응답 모른다)

7) 프론트코드 복사해오기..

  • nunjucks 문법에 맞게 구현되어 있는 html
  • 강의에서는 백엔드 강좌이기 때문에 중요하지 않다고 말함
  • nunjucks 문법을 잘 모르지만, 실무에서는 react나 vue를 많이 쓰기 때문에 실제 프로젝트를 한다면 react나 vue를 사용하자.
  • view폴더 아래 html 파일들 구성
  • public폴더 아래 css 파일 구성

9.2 데이터베이스 세팅하기

1) models 폴더 아래 테이블 생성하기

  • 시퀄라이즈를 통해 만들어진 테이블 클래스는 기본적으로 내부 메소드로 init과 associate를 가진다.
  • init은 생성되는 테이블(스키마)의 컬럼과 로우, 그리고 속성을 정의한다.
  • associate는 다른 테이블과의 관계(1:1, 1:N, N:M)을 정의한다.
  • 현재 만들고자 하는 프로젝트는 트위터와 비슷한 기능을 하는 노드버드이기 때문에, user, post, hashtag 테이블을 만든다.

models/user.js

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,
            }
        }, {
            sequelize,
            timestamps: true, // createAt, updateAt
            underscored: false,
            modelName: 'User',
            tableName: 'users',
            paranoid: true, // deleteAt 유저 삭제일 -> soft delete
            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'
        })
    }
}

module.exports = User;
  • 위 코드는 사용자의 정보를 저장하는 user 테이블이다.
  • 컬럼으로 email, nick, password, provider, snsId를 가진다.
  • provider는 local를 통해 회원가입 했는지, kakao를 통해 회원가입 했는지 저장하는 컬럼이다.
  • 게시글을 저장하는 테이블인 post와 1:N 관계를 가진다.
  • 다른 유저와 팔로우 팔로잉 관계를 가지기 때문에 user 테이블 끼리 N:M 관계를 가진다.

models/post.js

const Sequelize = require('sequelize');

class Post extends Sequelize.Model {
    static initiate(sequelize){
        Post.init({
            content: {
                type: Sequelize.STRING(40),
                allowNull: false,
            },
            img: {
                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){
        db.Post.belongsTo(db.User);
        db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
    }
}

module.exports = Post;
  • 위 코드는 게시글의 정보를 저장하는 post 테이블이다.
  • 컬럼으로 content와 img를 가진다.
  • 게시글은 유저에 속해 있으므로 user 테이블과 1:N 관계를 가진다.
  • 게시글은 여러 해시태그를 가질 수 있으므로 hashtag 테이블과 N:M 관계를 가진다.

models/hashtag.js

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){
        db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag'});
    }
}

module.exports = Hashtag;
  • 위 코드는 해시태그의 정보를 저장하는 hashtag 테이블이다.
  • 컬럼으로 title을 가진다.
  • 해시태그들은 여러 게시글에 포함되어 있으므로 post 테이블과 N:M 관계를 지닌다.

2) models/index.js 파일 자동화하기

  • index.js는 자바스크립트로 다룰 데이터베이스 객체인 db에 생성한 테이블들을 삽입해주는 코드라고 할 수 있다.
  • 기존 코드는 모든 model들을 require로 불러온 후, initate를 다하고 associate를 다 했음
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');
const db = {};
...
db.User = User;
db.Post = Post;
db.Hashtag = Hashtag;

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

module.exports = db;
  • 하지만 model들이 너무 많은 경우 코드가 쓸 데 없이 길어지기 때문에 이를 자동화하는 방법을 사용함.
  • Node의 내장 모듈인 fs와 path를 사용해서 구현
const fs = require('fs');
const path = require('path');
const db = {};
...
const basename = path.basename(__filename);
fs.readdirSync(__dirname)
  .filter(file => {
    return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
  })
  .forEach((file) => {
    const modle = require(path.join(__dirname, file));
    console.log(file, model.name);
    db[model.name] = model;
    model.initiate(sequelize);
  });
Object.keys(db).forEach(modelName => {
  if(db[modelName].associate){
    db[modelName].associate(db);
  }
})

module.exports = db;

3) app.js에서 데이터베이스 연결하기

  • sequelize.sync()를 이용해서 MySQL과 연결한다.
  • 아래코드를 app.js의 적절한 위치에 삽입한다.
sequelize.sync()
    .then(() => {
        console.log('데이터베이스 연결 성공')
    })
    .catch((err) => {
        console.error(err);
    })

9.3 passport 모듈 사용하기

1) passport.js

  • strategy(전략)에 따른 요청에 인증하기 위한 목적
  • strategy의 종류
    • Local Strategy (passport-local)
    • Social Authentication (passport-kakao, passport-naver 등) 존재
$ npm install passport passport-local passport-kakao express-session

2) structure (구조)

  • passport 폴더를 만들어 index.js와 strategy 작성
    • index.js : serialize, deserialize 작성, strategy 호출
    • strategy : local 및 외부 인증 전략 작성
  • middlewares 폴더를 만들어 isLoggedIn와 isNotLoggedIn 작성
    • 로그인 했는지에 대한 체크 미들웨어
  • controllers/auth.js에 join, login, logout 컨트롤러 작성
    • join : 회원가입 콜백함수
    • login : 로그인 콜백함수
    • logout : 로그아웃 콜백함수
  • app.js에 passport 등록
    • app.use(passport.initialize());
    • app.use(passport.session());
    • passport 폴더의 index 호출 passportConfig();
  • done
    • 첫 번째 인자 : DB조회시 발생하는 서버 에러. 무조건 실패하는 경우에만 사용
    • 두 번째 인자 : 성공했을 때 return할 값
    • 세 번째 인자 : 사용자가 임의로 실패를 만들고 싶을 때 사용
    • 호출되면 controllers/auth.js의 login부분의 콜백함수로 가짐

3) app.js 추가

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

const app = express();
passportConfig(); // 패스포트 설정
...
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()); // session 미들웨어 코드 뒤에 적용
  • passport를 app.js에 등록
  • passport 미들웨어는 세션 설정 후 등록 (initialize, session)

4) /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) => { // user === exUser
        done(null, user.id); // 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
    • strategy에서 로그인 성공 시 실행되는 done(null, user)에서 user 객체를 전달받아 세션(req.session.passport.user)에 저장
    • 보통 세션의 용량을 줄이기 위해 user의 id만 세션에 저장
  • deserializeUser
    • 서버로 들어오는 요청마다 세션 정보를 실제 DB의 데이터와 비교.
    • DB에 해당 유저 정보가 있으면 done(null, user)을 통해 req.user에 저장
    • serializeUser에서 done으로 넘겨주는 user가 deserializeUser의 첫 번째 매개변수로 전달되기 때문에 둘의 타입은 항상 일치해야함

/passport/localStrategy.js

const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const bcrypt = require('bcrypt');
const User = require('../models/user');

module.exports = () => {
    passport.use(new LocalStrategy({
        usernameField: 'email', // req.body.email
        passwordField: 'password', // req.body.password
        // session: true, // 세션에 저장 여부
        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);  
        }
    }));
};
  • local 로그인을 위한 전략
  • usernameField, passwordField : form 필드 내의 input의 name 태그.
  • passReqToCallback : express의 req 객체에 접근 가능 여부. true일 때, 뒤의 콜백 함수의 첫 번째 인자로 req가 붙음
  • body에 데이터가 { id: 'zerocho', pw: 'pswd' }로 전송되면, 콜백 함수의 id와 password 값이 각각 zerocho, pswd가 됨
  • DB에서 조건에 맞게 검색하여 done 함수를 이용해 exUser 객체 전송, 또는 에러 리턴

/passport/kakaoStrategy.js

const passport = require('passport');
const { Strategy: KakaoStrategy } = require('passport-kakao');
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('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);
        }
    }));
};
  • kakao 로그인을 위한 전략
  • clientID : 카카오 로그인 API를 사용하기 위한 ID
  • 카카오 API는 카카오 개발자 페이지에서 회원가입 후 받기.
  • callbackURL : 카카오 로그인시 요청을 보내줄 Redirect URL
  • DB에서 조건에 맞게 검색하여 done 함수를 이용해 exUser 객체 전송
  • DB에 없다면 newUser 만들어서 리턴

5) 라우터 및 미들웨어, 콜백함수 작성

/routes/auth.js

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

// POST /auth/join
router.post('/join', isNotLoggedIn, join);
// POST /auth/login
router.post('/login', isNotLoggedIn, login);
// GET /auth/logout
router.get('/logout', isLoggedIn, logout);
// /auth/kakao
router.get('/kakao', passport.authenticate('kakao'));
// /auth/kakao/callback
router.get('/kakao/callback', passport.authenticate('kakao', {
    failureRedirect: '/?loginError=카카오로그인 실패',
}), (req, res) => {
    res.redirect('/');
});

module.exports = router;
  • 회원가입, 로그인, 로그아웃 요청에 대한 라우터
  • kakao 로그인과 kakao 로그인 과정에서 발생하는 callbackURL에 대한 라우터
  • isLoggedIn과 isNotLoggedIn은 middlewares/index.js에 분리하여 작성
  • join, login, logout은 controllers/auth.js에 컨트롤러 분리하여 작성

/controllers/auth.js

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

exports.join = async (req, res, body) => {
    const { nick, email, 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);
        next(error);
    }
}
exports.login = (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);
}
exports.logout = (req, res, next) => {
    req.logout(() => {
        res.redirect('/');
    })
}
  • join
    • req.body로부터 nick, email, password 받음
    • password는 암호화하여 user 객체를 하나 생성한 후 DB에 저장
    • 만약 이미 존재하는 사용자라면 에러 반환
  • login의 passport.authenticate()
    • 사용할 strategy를 설정하고, 인증 절차에 대한 콜백 작성
    • 콜백함수의 인자들은 done 함수로부터 넘어옴
    • 첫 번재 인자가 true라면 서버 에러이므로 에러 반환
    • 두 번째 인자가 false라면 로그인 과정에서 에러 발생이므로 세 번째 인자를 참고하여 에러 반환
    • 에러가 없는 경우 로그인
  • logout
    • req.logout을 실행
    • 콜백함수로 '/'로 redirect 해주기

/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() 함수를 이용해 passport를 통해서 로그인 인증여부 확인

6) 로그인 과정 정리하기

  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. 로그인 완료

7) 로그인 이후의 과정 정리하기

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

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

1) 업로드 과정

    1. 사진이 입력되면 uploads 파일에 사진을 업로드하고, 사진파일의 이름을 HTML 속성으로 저장한다.
    1. 짹짹 버튼을 누르면 DB의 post 테이블에 저장한다.
    1. 업로드가 완료되면 메인 화면으로 보내준다.

2) 라우터 및 컨트롤러, 콜백함수 작성

routes/post.js

const express = require('express');
const router = express.Router();
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const { afterUploadImage, uploadPost } = require('../controllers/post');

try{
    fs.readdirSync('uploads');
} catch(error){
    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); // 이미지.png -> 이미지1234321.png
            cb(null, path.basename(file.originalname, ext) + Date.now() + ext);
        }
    }),
    limits: { fileSize: 5 * 1024 * 1024 },
});
router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);

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

module.exports = router;
  • upload는 이미지를 업로드 해주는 multer
  • upload2는 실제 게시물을 업로드 해주는 multer. 이때 이미지는 위치 정보를 포함하여 업로드 해줌.
  • 업로드 되는 사진들을 저장할 폴더 uploads 생성
  • multer 세팅하기
    • storage : 어디에 어떻게 저장할지 세팅
      • destination : 저장할 폴더 지정
      • filename : 저장할 이미지의 이름 지정. 이름뒤에 Date.now()를 추가해서 중복되는 이름의 파일이 올라오더라도 덮어씌우기 방지하기
    • limits : 파일 용량 한계 설정하기

controllers/post.js

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

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,
        });
        // 해쉬태그를 뽑아내기 위해선 정규표현식을 공부해야함
        // /#[^\s#]*/g 이 해쉬태그의 정규표현식 | 샵과 공백이 아닌 나머지를 뜻함
        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);
    }
};
  • afterUploadImage
    • 이미지 업로드 요청이 왔을 때 이미지의 url을 JSON으로 반환
  • uploadPost
    • 입력 받은 정보들로 DB에 post 생성하기
    • 정규 표현식을 통해 hashtags 추출하기
    • findOrCreate함수를 이용해서 hash태그가 있으면 result에 저장, 없으면 생성한 후 result에 저장하기
    • post와 hash태그 addHashtags를 이용해 N:M 관계 정의해주기

controllers/page.js의 renderMain

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(error){
        console.error(error);
    }
};
  • 메인화면에서 모든 게시물들 보여주기
  • 최신 순으로 정렬하기

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

1) 팔로우 기능 라우터 및 컨트롤러, 콜백함수 작성

routes/user.js

const express = require('express');
const router = express.Router();
const { isLoggedIn } = require('../middlewares');
const { follow, unfollow } = require('../controllers/user');

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

module.exports = router;

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);
    }
}
  • 유저를 찾은 후 addFollowing 메서드를 이용해서 유저끼리 관계 맺어주기

2) 해쉬태그 검색 기능 라우터 및 컨트롤러, 콜백함수 작성

routes/page.js

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

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?.Following?.map(f => f.id) || [];
    res.locals.likeCount = req.user?.Likes?.length || 0;
    next();
});
...
router.get('/hashtag', renderHashtag); // hashtag?hashtag=고양이

module.exports = router;
  • 기존의 page.js에 router.get('/hashtag', renderHashtag);추가하기

controllers/page.js의 renderHashtag

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, attributes: ['id', 'nick']}],
                order: [['createdAt', 'DESC']]
            });
        }
        res.render('main', {
            title: `${query} | NodeBird`,
            twits: posts, // 프론트에서 받아서 처리
        })
    } catch(error){
        console.error(error);
        next(error);
    }
}
  • req.query.hashtag로 부터 검색할 hashtag 저장
  • DB에 hashtag가 있는지 확인
  • getPosts를 통해 hashtag를 가지고 있는 post들을 가져오기
  • twits를 posts로 전달해서 검색된 게시물들을 화면에 보여주기

9.6 스스로 기능추가하기

1) 팔로잉 끊기

  • views/main.html 수정
    • 팔로우취소 버튼 만들기
    • click 이벤트 리스너 등록하기
    • 이벤트 발생시 axois.post('/user/${userId}/unfollow')로 post 요청 보내기
  • routes/user.js에 router.post('/:id/unfollow', isLoggedIn, unfollow);추가하기
  • controllers/user.js에 unfollow 컨트롤러 추가하기

controllers/user.js

exports.unfollow = async (req, res, next) => {
    try {
        const user = await User.findOne({ where: { id: req.user.id }});
        if(user){
            await user.removeFollowing(parseInt(req.params.id, 10));
            res.send('success');
        } else{
            res.status(404).send('no user');
        }
    } catch(error){
        console.error(error);
        next(error);
    }
}
  • user를 DB에서 찾았을 경우 아래 동작 실행
  • removeFollowing 함수를 통해 팔로잉 끊기

2) 프로필 정보 변경하기

  • views/layout.html에 <a id="profile-update" href="/update" class="btn">프로필 수정</a> 추가하기
  • routes/page.js에 router.get('/update', isLoggedIn, updateProfile); 추가하기
  • controllers/page.js에 exports.updateProfile = (req, res, next) => { res.render('update', { title: '정보 수정 - NodeBird'}); } 추가하기
  • 이제 프로필 수정 버튼을 누르면 views의 update.html 파일이 화면에 띄어짐

views/update.html

{% extends 'layout.html' %}

{% block content %}
  <div class="timeline">
    <form id="update-form" action="/profile/update" method="post">
        <div class="input-group">
          <label for="update-email">이메일</label>
          <input id="update-email" type="email" name="email"></div>
        <div class="input-group">
          <label for="update-nick">닉네임</label>
          <input id="update-nick" type="text" name="nick"></div>
        <div class="input-group">
          <label for="update-password">비밀번호</label>
          <input id="update-password" type="password" name="password">
        </div>
        <button id="update-btn" type="submit" class="btn">정보수정</button>
    </form>
  </div>
{% endblock %}
  • 수정할 이메일, 닉네임, 비밀번호를 입력하고 정보수정 버튼을 누를 수 있는 정적 화면
  • 정보수정 버튼을 누르면 입력된 데이터를 가지고 /profile/update를 요청한다.

routes/profile.js

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const { update } = require('../controllers/profile');
const router = express.Router();

// POST /profile/update
router.post('/update', isLoggedIn, update);

module.exports = router;
  • 정보수정 버튼이 눌려졌을 때의 요청을 받을 수 있는 라우터를 구성한다.

controllers/profile.js

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

exports.update = async (req, res, body) => {
    const { nick, email, password } = req.body;
    try {
        const exUser = await User.findOne({ where: { email }});
        if(exUser) {
            const hash = await bcrypt.hash(password,12);
            await exUser.update({
                email: email,
                nick: nick,
                password: hash,
            })
            return res.redirect('/');
        } else {
            
        }
    } catch(error) {
        console.error(error);
        next(error);
    }
}
  • req.body로부터 수정할 데이터를 받는다.
  • email은 수정되지 않는다고 가정한다.
  • DB에서 수정할 유저를 찾는다.
  • 비밀번호는 암호화하고 update 메소드를 통해 DB를 업데이트한다.

3) 게시글 삭제하기

  • views/main.html 수정
    • 게시물 삭제 버튼 만들기
    • click 이벤트 리스너 등록하기
    • 이벤트 발생시 axios.post('/post/${postId}/delete')로 post 요청 보내기
  • routes/post.js에 router.post('/:id/delete', isLoggedIn, deletePost); 추가하기
  • controllers/post.js에 deletePost 컨트롤러 추가하기

controllers/post.js

exports.deletePost = async (req, res, next) => {
    try {
        console.log(req.url);
        let idx = req.url.indexOf('/', 1);
        let postId = req.url.substring(1, idx);
        await Post.destroy({ where: { id: postId }});
        res.redirect('/');
    } catch(error){
        console.error(error);
        next(error);
    }
}
  • req.url에서 삭제할 게시글의 id값 추출하기
  • 추출한 id를 통해 DB에서 게시글 삭제하기

4) 사용자 이름을 누르면 그 사용자의 게시글만 보여주기

  • views/main.html 수정
    • 게시글의 사용자 이름에 click 이벤트 리스너 등록하기
    • 이벤트 발생시 window.location.replace('/content?id=${userId}');로 사용자 아이디를 쿼리스트링으로 포함하여 redirect 해주기
  • routes/page.js에 router.get('/content', renderContent); 추가하기
  • controllers/page.js에 renderContent 컨트롤러 추가하기

controllers/page.js

exports.renderContent = async (req, res, next) => {
    const query = req.query.id;
    if(!query) {
        return res.redirect('/');
    }
    try{
        const user = await User.findOne({ where: { id: query }});
        let posts = [];
        if(user) {
            posts = await user.getPosts({
                include: [{ model: User, attributes: ['id', 'nick']}],
                order: [['createdAt', 'DESC']]
            });
        }
        res.render('main', {
            title: `${user.nick} | NodeBird`,
            twits: posts,
        })
    } catch(error){
        console.error(error);
        next(error);
    }
}
  • query의 id값을 가지고 있는 유저 찾기
  • posts 배열에 유저의 게시글들을 모두 찾아서 최신순으로 저장하기
  • views/main.html에 twits를 posts로 넘겨 금방 찾은 게시글들만 화면에 띄우기
profile
Studying NodeJS...

0개의 댓글