
websocket이라는 protocol을 사용하면 실시간 데이터 송수신을 구현할 수 있다.
그래서 보통 메신저를 websocket을 사용해서 개발한다.
종강 기념으로 방학동안 websocket을 사용해서 facebook messenger를 클론 코딩해보려 한다.
그런데 강의 영상 없이!
우선 database는 MongoDB를 사용할 것이다. MongoDB Atlas에서 500MB까지는 무료로 지원해주기 때문이다
프론트는 당연 React, 백엔드는 Express.js를 활용할 예정이다.
언어는 TypeScript를 사용한다.
폴더 구조는 다음과 같다.
root
├─ client
│ └─ src
│ ├─ assets
│ ├─ components
│ │ ├─ layouts
│ │ ├─ elements
│ │ └─ ui
│ ├─ contexts
│ ├─ hooks
│ ├─ interfaces
│ ├─ pages
│ ├─ services
│ ├─ shared
│ ├─ styles
│ └─ utils
└─ server
└─ src
├─ controllers
├─ daos
├─ lib
├─ middlewares
├─ models
├─ routes
└─ services
지금까지 React 앱을 만들 때 계속 CRA를 이용해서 프로젝트를 생성했었는데, 이번에는 CRA 없이 해보게 되었다.
모듈을 import 할 때 상대 경로를 사용하면 헷갈리기 때문에 절대 경로를 사용할 생각이었는데, 절대 경로를 사용하려면 CRA 기본 세팅 때문에 복잡해지는 부분이 많았다. 새로운 라이브러리를 설치하는 등.
그래서 아예 CRA 없이 리액트 프로젝트를 생성해보게 되었다.
참고 블로그: https://yogjin.tistory.com/59
이 분이 올려놓으신 대로 쭉 따라한 다음, package.json에 스크립트를 추가해주면 완성이다.
<package.json>
{
"name": "client",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"html-webpack-plugin": "^5.6.3",
"ts-loader": "^9.5.1",
"typescript": "^5.7.2",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
},
"scripts": {
"dev": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"start": "webpack serve --config webpack.dev.js"
}
}
개발 시에는 yarn dev 명령어를 입력해주면 서버가 실행된다.
<tsconfig.json>
{
"compilerOptions": {
"target": "es2021",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"noEmit": false,
"baseUrl": "./src",
"paths": {
"@components/*": ["components/*"],
"@pages/*": ["pages/*"],
"@utils/*": ["utils/*"],
"@styles/*": ["styles/*"],
"@hooks/*": ["hooks/*"],
"@contexts/*": ["contexts/*"],
"@services/*": ["services/*"],
"@types/*": ["types/*"]
},
"outDir": "./dist"
},
"include": ["src"],
"exclude": ["node_modules"]
}
이건 tsconfig.json 파일인데, 보이는 것과 같이 paths에 원하는 경로를 추가해줄 수 있다.
이제 다른 파일에서 저 폴더의 하위 파일을 import 하고 싶으면 @를 붙이고 절대경로로 표기해줄 수 있다.
일단 생각나는대로 폴더를 구성해주었는데, 나중에 더 늘어날 것 같다.
이제 Client 설정이 완료되었으니 Server 설정으로 넘어가보자.
먼저 yarn init -y 를 이용해 server 폴더에 package.json을 추가해준다.
차례대로 아래 명령어를 입력해준다.
yarn add express
tsc --init
yarn add -D express typescript tsx nodemon @types/node @types/express
yarn add -D @types/express-serve-static-core
이제 package.json에 script를 추가해준다.
<package.json>
{
"name": "server",
"type": "module",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"express": "^4.21.2",
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/express-serve-static-core": "^5.0.2",
"@types/node": "^22.10.2",
"nodemon": "^3.1.9",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
},
"scripts": {
"start": "node dist/app.js",
"build": "tsc -p .",
"dev": "nodemon --watch \"src/**/*.ts\" --exec \"tsx\" src/app.ts"
}
}
마찬가지로 yarn dev 명령어를 사용해 개발 서버를 실행할 수 있다.
express도 상대경로 문제가 동일하게 있기 때문에 절대경로로 바꾸는 세팅이 필요하다.
아까처럼 tsconfig.json에 경로를 등록해주면 된다.
<tsconfig.json>
{
"compilerOptions": {
"target": "es2021",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"baseUrl": "./src",
"paths": {
"@controllers/*": ["src/controllers/*"],
"@models/*": ["src/models/*"],
"@routes/*": ["src/routes/*"],
"@services/*": ["src/services/*"],
"@daos/*": ["src/daos/*"],
"@middlewares/*": ["src/middlewares/*"],
"@lib/*": ["src/lib/*"]
},
"outDir": "./dist"
},
"include": ["src"],
"exclude": ["node_modules"]
}
이렇게 필요한 폴더들을 모두 추가해주었다.
그런데 이렇게 하고 개발하는 것 자체는 문제 없지만, 배포시에 상대경로로 변환되지 않아서 실행이 안 되는 문제가 있다.
해당 문제는 아래와 같이 해결이 가능하다.
yarn add tsc-alias
위 명령어를 활용해서 tsc-alias를 설치해준다.
tsconfig.json에 아래를 추가해준다.
"tsc-alias": {
"resolveFullPaths": true,
}
마지막으로 package.json script 부분에 path를 수정해줄 명령어를 추가한다.
"scripts": {
"start": "node dist/main.js",
"build": "tsc -p .",
"clean-path": "tsc-alias",
"dev": "nodemon --watch \"src/**/*.ts\" --exec \"tsx\" ./main.ts"
}
나는 이런식으로 작성했다.
이제 아래의 순서대로 커맨드를 실행하면 정상적으로 프로덕션 환경의 웹서버가 실행될것이다.
yarn build
yarn clean-path
yarn start
Express 서버에서 MongoDB를 이용하기 위해 Mongoose를 설치해야 한다.
yarn add mongoose dotenv
yarn add -D @types/mongoose
명령어를 입력하여 mongoose와 dotenv를 설치해준다.
다음은 MongoDB 세팅이다.
MongoDB Atlas
링크를 클릭해서 회원가입/로그인을 해준다.

아마 이런 창으로 넘어갈텐데, 이제 New Project 버튼을 눌러 Project를 새로 만들어주면 된다.

프로젝트 이름을 입력해주고, 태그는 필요 없어서 그냥 넘겼다.
그 다음에는 유저를 추가하는 창이 뜨는데, 어차피 나 혼자 하는 프로젝트니 그것도 넘겼다.

이제 이런 창이 나올텐데, DB를 만들어야 하기 때문에 Cluster를 새로 만들어준다.

create 버튼 누르면 이렇게 창이 새로 뜨는데, 나는 당연히 free를 선택한다.
db 클러스터 이름을 적고, 어느 클라우드에 배포할지 고른다. 나는 AWS로 골랐다.
리전은 API 서버랑 가까운 지역 고르면 된다.
나중에 하겠지만 API 서버도 서드파티 플랫폼에 배포할거라 리전을 서울로 선택하는 의미가 별로 없다.
quick setup 항목에 preload sample dataset 옵션이 있는데, 선택할시 DB를 더미데이터로 채워주기 때문에 선택하지 않았다.

이제 클러스터를 생성하면 이런 창이 뜨는데, MongoDB Compass와 연결해주어야 한다.
Compass는 MySQL Workbench처럼 데이터베이스에 직접 접근해서 데이터를 확인하고 관리할 수 있는 소프트웨어다.
Database user를 생성한 뒤, choose connection method를 누르면

이런 창이 나오는데, 우리는 Compass를 사용할 것이기 때문에 먼저 compass를 눌러준다.

Compass가 설치되어있지 않다면 설치해주고, 설치가 되어있다면 2번으로 넘어간다.
MongoDB 클러스터의 URI을 복사하고 Compass를 실행한다.

add new connection을 누른다.

그러면 이런 창이 나오는데, 아까 복사했던 URI를 붙여넣어준다.
아까 생성했던 계정의 비밀번호가 입력되어있지 않다면, <db_password> 부분을 변경해준다.
Name에서 연결명을 설정해주고 저장하면 클라우드에 올라간 Cluster에 연결된다.
이제 Compass에도 연결했으니 Express 서버에서도 연결해주도록 하자.

여기서 Drivers를 눌러준다.

full code example 옵션을 선택해주면 이렇게 mongoose에 연결하는 코드를 띄워주는데, 우리는 uri를 dotenv 파일에 저장할 것이니 약간 변형할 필요가 있다. require 대신 import 문을 사용할 것이기도 하고.
MONGO_URI = <본인의 uri>
.env 파일에 이렇게 작성해준 뒤 app.ts에 샘플코드를 약간 변형해서 작성해준다.
import express, { Request, Response, NextFunction } from "express";
import dotenv from "dotenv";
import cors from "cors";
import mongoose, { ConnectOptions } from "mongoose";
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
app.get("/", (req: Request, res: Response, next: NextFunction) => {
res.send("API Running");
});
async function run() {
try {
const clientOptions = {
serverApi: { version: "1", strict: true, deprecationErrors: true },
};
await mongoose.connect(
process.env.MONGO_URI as string,
clientOptions as ConnectOptions
);
await mongoose.connection.db?.admin().command({ ping: 1 });
console.log(
"Pinged your deployment. You successfully connected to MongoDB!"
);
app.listen("8080", () => {
console.log(`
🛡️ Server listening on port: 8080 🛡️
`);
});
} finally {
await mongoose.disconnect();
}
}
run().catch(console.dir);
이렇게 /src 하위에 app.ts를 구성해주면 된다.

잘된다
이후 작업을 하다가 발생한 몇 가지 오류를 기록한다.
이렇게 하게 되면, 리액트 서버를 실행할 때, webpack이 tsconfig에 써있는 절대경로를 인식하지 못하는 문제가 발생한다.
그래서 삽질을 꽤 오래 했는데, 해결 방법을 찾았다.
// webpack.common.js
...
resolve: {
alias: {
"@assets": path.resolve(__dirname, "src/assets"),
"@components": path.resolve(__dirname, "src/components"),
"@pages": path.resolve(__dirname, "src/pages"),
"@utils": path.resolve(__dirname, "src/utils"),
"@styles": path.resolve(__dirname, "src/styles"),
"@hooks": path.resolve(__dirname, "src/hooks"),
"@contexts": path.resolve(__dirname, "src/contexts"),
"@services": path.resolve(__dirname, "src/services"),
"@shared": path.resolve(__dirname, "src/shared"),
"@interfaces": path.resolve(__dirname, "src/interfaces"),
},
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
...
이렇게 webpack.common.js의 resolve 필드에 alias를 추가해주어 경로를 하나 하나 tsconfig에서 했던 것과 비슷하게 등록해야 한다.
이 오류도 아마 webpack에서 문제가 기인하는 것으로 보인다.
CRA로 프로젝트를 세팅하게 되면 개발에 필요한 모든 라이브러리를 자동으로 다운로드해줘서 편리하게 사용할 수 있지만, CRA 없이 직접 프로젝트를 세팅한다면 추가로 다운해야 하는 라이브러리가 꽤 있다.
첫째로 svg다.
일반적으로 CRA를 통해 리액트 프로젝트를 시작할 경우, svg 파일은 하나의 모듈로써 코드에 삽입이 가능하다.
예를 들자면,
import logo from "./logo.svg";
이런 식으로 import가 가능하고,
return <>{logo}</>;
이렇게 반환하는 것도 가능하다.
반면에 webpack을 통해서 직접 보일러 플레이트를 세팅하니 저런 식으로 사용할 수 없었다.
이에 알아본 결과, svg url loader라는 라이브러리를 사용해서 비슷하게 할 수 있었다.
yarn add -D svg-url-loader 커맨드를 사용하여 dev dependency에 svg url loader를 추가해주고,
// declarations.d.ts
declare module "*.svg" {
const content: any;
export default content;
}
이렇게 declaration 파일을 만든 뒤에, tsconfig 파일에 include 해주면 된다.
...
"include": ["src", "declarations.d.ts"],
...
추가로 webpack.common.js 파일도 살짝 손봐야 한다.
// webpack.common.js
rules: [
{
test: /\.(js|ts|tsx)$/i,
exclude: /node_modules/,
use: {
loader: "ts-loader",
},
},
{
test: /\.svg$/,
use: [
{
loader: "svg-url-loader",
options: {
limit: 10000,
},
},
],
},
],
rules 필드에 svg를 로드할 때 사용할 라이브러리를 입력해주면 완성이다.
이렇게 하고 난 뒤에는,
import hessenger from "@assets/hessenger.logo.svg";
...
<img src={hessenger} alt="logo" />
...
이런 식으로 사용이 가능해진다. CRA 때와 완전히 똑같지는 않은데, 아마 다른 라이브러리를 사용하면 똑같이 만들 수 있을 것 같다. 지금도 충분히 편리하기 때문에 더 찾아보지는 않았다.
비슷한 맥락에서 컴포넌트에 css 파일을 import 하는 것도 인식 되지 않았다...!
이것을 해결하기 위해서도 svg 처럼 loader 설정을 해주어야 하는데,
다운로드 해야 하는 라이브러리가 두 가지다.
yarn add -D css-loader style-loader 이 커맨드를 사용해서 두 라이브러리 모두 dev dependency에 추가해준다.
이후 아까 한 것 처럼 webpack.common.js의 rules 필드에 로더들을 추가한다.
rules: [
{
test: /\.(js|ts|tsx)$/i,
exclude: /node_modules/,
use: {
loader: "ts-loader",
},
},
{
test: /\.svg$/,
use: [
{
loader: "svg-url-loader",
options: {
limit: 10000,
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
그러면 정상적으로 css 파일을 import할 수 있다.
세 가지 에러를 모두 해결한 뒤의 최종 webpack.common.js 파일은 다음과 같다:
// webpack.common.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/index.tsx",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
resolve: {
alias: {
"@assets": path.resolve(__dirname, "src/assets"),
"@components": path.resolve(__dirname, "src/components"),
"@pages": path.resolve(__dirname, "src/pages"),
"@utils": path.resolve(__dirname, "src/utils"),
"@styles": path.resolve(__dirname, "src/styles"),
"@hooks": path.resolve(__dirname, "src/hooks"),
"@contexts": path.resolve(__dirname, "src/contexts"),
"@services": path.resolve(__dirname, "src/services"),
"@shared": path.resolve(__dirname, "src/shared"),
"@interfaces": path.resolve(__dirname, "src/interfaces"),
},
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
module: {
rules: [
{
test: /\.(js|ts|tsx)$/i,
exclude: /node_modules/,
use: {
loader: "ts-loader",
},
},
{
test: /\.svg$/,
use: [
{
loader: "svg-url-loader",
options: {
limit: 10000,
},
},
],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
devtool: "inline-source-map",
devServer: {
static: "./dist",
hot: true,
open: true,
},
};
삽질 끄읏