본문 바로가기
프로젝트

[Node.js] 채팅 앱 만들기 (backend)

by 개발자신입 2024. 6. 5.
반응형

채팅 앱 만들어보자!

Node.js

Vite + React

MongoDB


react 설치

터미널 frontend 폴더에서 설치하기

  1. npm create vite@latest .
  2. React / JavaScript 선택

패키지 설치

  1. 루트 폴더에서 npm init -y 실행
  2. npm i express dotenv cookie-parser bcryptjs mongoose socket.io jsonwebtoken
  3. 전부 설치 : express, dotenv, cookie-parser, bcryptjs, mongoose, socket.io, jsonwebtoken

nodemon 설치

npm install nodemon --save-dev -g

 

Nodemon은 Node.js 애플리케이션 개발을 보다 편리하게 해주는 도구로, 소스 코드가 변경될 때마다 자동으로 서버를 재시작해주는 역할을 합니다. 이를 통해 개발자는 서버를 수동으로 재시작할 필요 없이 코드 변경 사항을 즉시 반영할 수 있어 개발 효율성이 크게 향상됩니다.

 

몽고DB 연결

connectToMongoDB.js

위치 : /backend/db/---.js

import mongoose from "mongoose";   


const connectToMongoDB = async () => {
  try {
    const uri = process.env.MONGO_URI;
    if (!uri) {
      throw new Error('MONGO_URI is not defined in .env file');
    }
    await mongoose.connect(uri, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB connected successfully');
  } catch (error) {
    console.error('Error connecting to MongoDB', error);
    process.exit(1); // Exit process with failure
  }
};

export default connectToMongoDB;

.env

PORT = 5000
MONGO_URI=mongodb+srv://아이디:비밀번호@cluster0.-----.mongodb.net/chat-app?retryWrites=true&w=majority

기본 기능

auth.routes.js

import express from "express";
import { login, logout, signup } from "../controllers/auth.controller.js";

const router = express.Router();

router.post("/signup", signup);

router.post("/login", login);

router.post("/logout", logout);


export default router;

user.model.js

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
    fullname: {
        type: String,
        required: true
    },
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true,
        minlength:6
    },
    confirmPassword: {
        type: String,
        required: true
    },
    gender: {
        type: String,
        required: true,
        enum: ["male", "female"]
    },
    profilePic:{
        type: String,
        default: "",
    },
});

const User = mongoose.model("User", userSchema);

export default User;

아바타 가져오기

https://avatar-placeholder.iran.liara.run/

auth.controller.js

import User from '../models/user.model.js';
import bcrypt from 'bcryptjs';


export const signup = async (req, res) => {
    try {
        const { fullname, username, password, confirmPassword, gender } = req.body;
        
        if(password !== confirmPassword){
            return res.status(400).json({ message: "Passwords do not match" });
        }

        const user = await User.findOne({ username });
        
        if(user){
            return res.status(400).json({ message: "User already exists" });
        }

        // hash password HERE
        // https:avater-placeholder.iran.liara.run

        const boyProfilePic = `https://avatar.iran.liara.run/public/boy?username=${username}`
        const girlProfilePic = `htps://avatar.iran.liara.run/public/girl?username=${username}`

    const newUser = new User({
        fullname,
        username,
        password,
        confirmPassword,
        gender,
        profilePic: gender === "male" ? boyProfilePic : girlProfilePic
    });

    await newUser.save();

    res.status(201).json({
        _id: newUser._id,
        fullname: newUser.fullname,
        username: newUser.username,
        profilePic: newUser.profilePic  
    })

    } catch(error){
        console.log("Error in signup", error);
        res.status(500).json({ message: error.message });
    }
};

 

postman(thunder client)으로 post 보내서 확인하기 -> 유저가 1명 만들어짐.

 

mongoDB 확인

아까 post로 보낸 정보가 db에 저장되어있다.

비밀번호가 바로 보이니까, 이거를 암호화해야함 (bcryptjs)

 

bcrypt

컨트롤러에서 bcryptjs 코드 추가

import 후 salt 선언 후, password: hashedPassword로 설정.

import bcrypt from 'bcryptjs';

.
.
.


        // hash password HERE
        const salt = await bcrypt.genSalt(10);
        const hashedPassword = await bcrypt.hash(password, salt);

.
.

    const newUser = new User({
        fullname,
        username,
        password: hashedPassword,
        confirmPassword,
        gender,
        profilePic: gender === "male" ? boyProfilePic : girlProfilePic
    });

 

다시 post로 유저 정보 보내면 비번이 암호화가 되어서 저장된다.

 

VScode 에서 git bash 사용하기

좌측 3번째 메뉴에서 Windows용 GIT 다운로드 한 후, vscode 재시작하면 됨.

bash 시크릿 토큰 생성


JWT

generateToken.js

import jwt from "jsonwebtoken";

const generateTokenAndSetCookie = (userId, res) => {
    const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
        expiresIn: "15d",
    })
    
    res.cookie("jwt", token, {
        maxAge: 15 * 24 * 60 * 60 * 1000,
        httpOnly: true, // prevent XSS attacks cross-site scripting attacks
        sameSite:"strict", // prevent CSRF attacks
    });
};

export default generateTokenAndSetCookie;

auth.controller.js

generateToken.js를 import 해주고, newUser에서 jwt 토큰 설정해줌.

import generateTokenAndSetCookie from '../utils/generateToken.js';


    if(newUser){
        // generate JWT token
        generateTokenAndSetCookie(newUser._id, res);
        await newUser.save();
    }

 

새로운 user 생성(send)하면, cookies에 jwt value가 생성됨.


로그아웃

auth.controller.js

 

export const logout = (req, res) => {
    try {
        res.cookie("jwt","",{maxAge:0})
        res.status(200).json({message:"Logged out successfully"})
    } catch (error) {
        console.log("Error in login", error);
        res.status(500).json({ error: error.message });
    }

 

생성, 수정 날짜 추가

    // createAt, updateAt
}, {timestamps: true});

 

메세지 보내기

  1. 아까 생성한 유저로 로그인 send 보내고 cookie 값을 복사함.  (쿠키값이 필요한 이유는 아래에 기입함.)
  2. 메세지 보낼 때, headers에 cookie를 추가해서 보내야 한다.
  3. 성공적으로 메세지가 전송되었다!

 

  1. 몽고DB에 저장됨

 

 

message.controller.js

  • 필수 : DB에 저장되게 하려면 save() 메소드를 작성해 주어야 함.
        // 순차적으로 처리됨
        // await newMessage.save();
        // await conversation.save();

        // 동시에 처리됨.
        await Promise.all([newMessage.save(), conversation.save()]);
export const getMessages = async (req, res) => {
    try {
        const {id:userToChatId} = req.params;
        const senderId = req.user._id; 
        const conversation = await Conversation.findOne({
            participants: {
                $all: [senderId, userToChatId]
            },
        }).populate("messages"); // messages array에 push한다.

        if(!conversation) return res.status(200).json([]);

        const messages = conversation.messages;

        res.status(200).json(messages);
        
    } catch (error) {
        console.log("Error in login", error);
        res.status(500).json({ error: error.message });
    }
}
populate는 Mongoose에서 참조된 문서들을 자동으로 조회하여 필드에 데이터를 채워주는 기능입니다. populate를 사용하면, 관련된 데이터의 ID만 저장된 필드를 실제 데이터로 치환해줍니다. 이를 통해 관련된 데이터에 쉽게 접근할 수 있습니다. 관련된 데이터를 자동으로 가져와서 개발자가 쉽게 사용할 수 있도록 해줍니다.

 

 

안 됐던 이유

현재 보호된 라우트(protectRoute 미들웨어)가 적용되어 있어 JWT 토큰이 없는 경우 401 에러를 반환합니다. 이 경우, 클라이언트 요청에서 JWT 토큰이 필요합니다.

 

=> send할 때 로그인 된 유저의 jwt 토큰까지 같이 보내야 했던 것.

 


User

user.routes.js

import express from "express";
import protectRoute from "../middleware/protectRoute.js";
import { getUsersForSidebar } from "../controllers/user.controller.js";

const router = express.Router();

router.get("/", protectRoute, getUsersForSidebar)

export default router;

user.controller.js

import User from "../models/user.model.js";

export const getUsersForSidebar = async (req, res) => {

    try {

        const loggedInUserId = req.user._id;

        const filteredUsers = await User.find({ _id: { $ne: loggedInUserId } });

        res.status(200).json(filteredUsers);


    } catch (error) {
        console.log("Error in getUsersForSidebar: ", error.message);
        res.status(500).json({ error: "Internal Server Error" });
    }
}

server.js

import userRoutes from "./routes/user.routes.js";

app.use("/api/users", userRoutes)
반응형

댓글