TIL

[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 : 0304 서버 맛보기

Cadi 2025. 3. 4. 23:04

오늘 배운 것

1. 서버 관련 기본 지식

2. 서버 관련 설정

3. 서버 관련 기본 코드

01. 서버 관련 기본 지식

서버

 

종류

  • TCP 서버 

프로토콜 : TCP

연결 방식 : 지속적인 연결

데이터 송수신 방식 : 자유로운 데이터 교환

속도 : 느리지만 신뢰성 보장

사용 사례 : 게임 서버, 파일 전송, 원격 제어  

 

TCP 서버는 지속적인 연결을 통해 실시간 통신에 이용된다. 안정적인 데이터 전송을 보장하며 데이터를 순서대로 전달하고, 손실이 발생할 경우 재전송한다. 

 

TCP 서버의 특징으로는 

1. 데이터 무결성을 위해 상대적으로 속도가 느림.

2. 패킷 순서가 보장됨

3. 데이터가 손실되면 재전송을 요청하여 손실을 막음

4. 매번 연결되는 방식이 아닌 최초 연결 후 지속적인 연결 방식을 사용

  • HTTP 서버

프로토콜 : HTTP(TCP기반)

연결 방식: 요청 - 응답 후 연결 종료

데이터 송수신 방식 : 요청과 응답 형식의 데이터

속도 : 상대적으로 빠름

사용 사례 : 웹 서버,REST API

 

HTTP 서버는 비실시간 통신에 이용된다, 기반은 TCP이지만, 클라이언트의 요청이 있을 때만 응답한다.

그래서 주로 실시간성이 없는 웹 사이트를 제작하는데에 사용되며, 게임에서는 로그인, 업적, 랭킹 등을 구현하는데 사용된다. 

 

HTTP서버의 특징으로는

1. 비연결 지향 : 요청과 응답이 끝나면 연결이 끊어짐

2. 요청의 종류 : 서버에 GET, POST 등으로 구분해서 요청을 보냄

3. 서버 응답 : 클라이언트의 요청을 받아 처리하고 응답을 보냄

 

 

데이터베이스(DB)

 : 효과적으로 데이터를 저장하고 관리하는 시스템. 대량의 데이터를 효과적으로저장하고 필요시 빠르게 검색하여

   데이터를 불러오거나 값을 수정할 수 있음. 

 

데이터베이스의 특징으로는

1. 데이터의 정확성과 일관성

2. 접근 권한을 통해 데이터 보안을 유지

3. 여러 사용자가 동시 접근 가능

4. 장애 발생 시 데이터 복구 가능

 

종류

  • 관계형 데이터베이스 (Relational Database)

1. 테이블 구조(엑셀과 비슷한)로 데이터를 저장함

2. SQL 명령어를 사용하여 데이터를 관리함.

3. 데이터 중복을 최소화함.

4. MySQL, PostgreSQL, MariADB, Microsoft SQL Server, Oracle 등이 있음

 

  • NoSQL 데이터베이스

1. 비정형, 반정형 데이터를 저장하는 데이터베이스

2. 스키마가 없거나 유연한 구조를 가짐

3. 관계형 데이터베이스에 비해 빠른 성능을 제공

4. MongoDB,Redis,Cassandra,Firebase Firestore 등이 있다.

 

  • 인메모리 데이터베이스

1. 데이터를 디스크가 아닌 메모리에 저장하는 데이터베이스

2. 메모리를 사용해 빠른 성능이 제공됨

3. 휘발성이 있어 보통 캐싱 용도로 사용

4. Redis, Memcached 등이 있음

내 질문들 

1. 관계형 데이터베이스의 특징 중 중복을 최소화 한다는 것은 무슨 의미이고 어떻게 동작하는가 ? 
:  관계형 데이터베이스는 '정규화(Normalization)' 과정을 통해 데이터 중복을  최소화한다. 

   정규화는 데이터를 여러 테이블로 분산하고 관계를 설정하여 중복을 제거하고 데이터의 일관성을 유지하는 과정.
   예를 들어 고객 정보 테이블에 주문 정보까지 함께 저장하는 대신, '고객 정보' 테이블과 '주문 정보' 테이블을 분리하고
   고객 ID를 통해 두 테이브를 연결하는 것.

 

2. NoSQL 데이터베이스의 특징 중 비정형 반정형, 스키마가 없다는 것은 무엇을 의미하는가 ?

: 비정형 데이터는 미리 정의된 데이터 모델이나 스키마가 없는 데이터를 의미, 즉 텍스트 문서, 이미지, 오디오 등 다양한 형태를 가질 수 있다. 반정형 데이터는 일부 구조를 갖지만, 관계형 데이터베이스처럼 엄격한 스키마를 따르지 않는 데이터.
마지막으로 스키마는 데이터베이스의 구조와 제약 조건을 정의하는 것. 관계형 데이터베이스에서는 테이블의 열과 데이터 타입, 관계 등을 정의하지만 NoSQL 데이터베이스에서는 스키마가 없거나 유연하여 다양한 형태의 데이터를 저장가능하다. 예를 들어 사용자 정보를 저장할때 관계형에서는 이름,나이,주소 등의 항목으로 테이블을 만들어야 하지만, NoSQL에서는 이름만 있는 사용자, 이름과 주소만 있는 사용자 등 다양한 형태의 데이터를 저장할 수 있다. 

 

 

3. .NoSQL 데이터베이스의 성능이 빠른 이유는 무엇인가 ?  제약 조건 하에서 몇 번 row , col을 지목하는 것이 더 빨라 보이는데 왜 그런 것인가 ?

: 둘 다 나름의 장단점을 갖고 있다 (데이터의 특성과 연관된)

NoSQL 데이터베이스는 데이터를 분산하여 저장하고 처리하는데에 특화되어 있다. 

관계형 데이터베이스는 데이터의 일관성과 무결성을 유지하는 데 강점을 가지고, 정규화로 인해 테이블이 여러개로 나뉘어 있는 경우가 많아 데잍어를 한 번 읽어오기 위해서 여러 테이블을 조인해야 하는 경우가 생긴다. 이 과정에서 속도가 느려질수 있다.

어떤 데이터베이스가 항상 더 빠르다고 단정지을 수 는 없다. 

 

 

 

 

02. 서버 관련 기본 설정

 

서버도 내용이 유니티만큼이나 방대하지만, 주어진 시간이 1주일밖에 되지 않기 때문에 

기초적인 내용만 보고 진행하고, 나중에 핵앤슬레쉬 할 때 다른 방향으로 또 하겠다. 

 

우리는 다양하게 사용가능한 개발환경인 NodeJS를 사용할 것이다. 

 

https://nodejs.org/ko

 

Node.js — 어디서든 JavaScript를 실행하세요

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

정상적으로 다운로드하면 CMD 창에서 확인 할 수 있다.

 

추가로 Postman과 MongoDB를 다운받아 준다. 

 

Postman

  • API(Application Programming interface) 개발 및 테스트를 위한 도구
  • 웹 서버에 다양한 형태의 요청(GET, POST 등)을 보내고 서버로부터의 응답을 쉽게 확인 가능
  • 주로 API의 작동 방식과 응답 결과를 테스트하고 디버깅하는 데 사용

내 정리 : 데이터나 요청 등을 지정한 URL로 보내는 것을 쉽게 할 수 있는 테스트용 앱

 

MongoDB

  • NoSQL 데이터베이스의 한 종류
  • 데이터를 유연한 JSON 형태의 문서로 저장, 대량의 데이터를 효율적으로 관리, 저장, 검색

MongoDB의 화면

 

 

 

 

 

 

PayrrelSync : Unity는 2인 테스트가 안되니까 그걸 쉽게 해 주는 오픈소스(아직 사용 안함)

 

CMD 창에 다음을 입력

npx express-generator --view=pug tictactoe-server

 

 

질문 : 위의 명령어는 무엇을 의미할까 ?

 

npx : npm(Node Package Manager)에 포함된 도구, 전역으로 설치하지 않고도 npm 패키지를 실행할 수 있게 도와준다.

 

express-generator : Express.js 애플리케이션의 기본 구조를 빠르게 생성해주는 도구, 필요한 파일과 폴더 구조를 수동으로 만들 필요 없이, 미리 정의된 템플릿을 기반으로 프로젝트를 시작 가능

 

--view=pug : --view는 사용할 템플릿 엔진을 지정하는 것, pug는 HTML을 생성하는 엔진 중 하나.

 

tictactoe--server : 디렉토리 이름

 

즉, 이 명령어는 tictactoe라는 이름의 폴더에 Express.js 웹 애플리케이션의 기본 구조를 생성하며 , 뷰 엔진으로는 pug를 사용하도록 설정하는 것이다.

 

* 참고 : Express.js란 Node.js 환경에서 웹 애플리케이션과 API를 구축하기 위한 빠르고 유연한 웹 프레임워크.

그러니까 실제 서버 프로그램을 만드는 것이 아니라, 서버 프로그램을 만들기 위한 기본적인 틀을 제공해 주는 것.

 

 

서버란 것을 너무 어려운 개념으로 생각하고 있었다. 서버의 핵심 개념은 결국 클라이언트로부터 요청을 받고,

요청에 대한 응답을 보내는 것이다. 그래서 저런 기본 틀을 만들어서 생각보다 쉽게 만들 수 있다. 

 

package.json 파일을 열어준다.

이걸 기본적으로 설치해야 한다. 

 

 

기본적으로 VScode의 터미널을 쓰면 다음과 같은 오류가 뜰 텐데 삭제해주고 명령어 npx install을 해 주면된다.

 

올바르게 설치한 후 npm start하고 localhost:3000으로 서버에 접속 가능

 

물론 지금 만든 것은 개인이니 외부에서 접근할 수 없다.

테스트를 위해 클라우드에 올려서 테스트해 볼 예정

뭐 아마존이나 등등도 가능하겠지만

간단하게 테스트할 수 있는 헤로쿠(HEROKU)로 테스트 해 볼 것이다. 

(다른 서버에 비해서는 초기 세팅할게 없기 떄문)

bin 파일의 일부

 

우선, 우리가 만든 서버를 활성화 시켜주어야 하기 때문에 npm start를 해 준다. (터미널 창)

 우리의 port가 3000으로 설정되어 있기 때문에, 아래와 같이 주소창에 localhost:3000을 쓰면

다음과 같이 우리가 설정한 View 페이지로 연결된다.

 

 

* port 는 컴퓨터 네트워크에서 데이터를 주고받는 가상의 통로 

 

 

 

 

* app.js가 메인 파일 느낌이다. 

 

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var mongodb =require('mongodb'); //추가
var MongoClient = mongodb.MongoClient; //추가

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

async function connectDB() {
  var databaseURL = "mongodb://localhost:27017/tictactoe";

  try {
    const database = await MongoClient.connect(databaseURL, {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    console.log("DB 연결 완료: " + databaseURL);
    app.set('database',database.db('tictactoe'));

    // 연결 종료 처리
    process.on("SIGINT", async () => {
      await database.close();
      console.log("DB 연결 종료");
      process.exit(0);
    });
  } catch (err) {
    console.error("DB 연결 실패: " + err);
    process.exit(1);
  }
}

connectDB().catch(err => {
  console.error("초기 DB 연결 실패: " + err);
  process.exit(1);
});


// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 

* 참고 : 일반적으로 Https 들은 기본적으로 80포트를 사용하기 때문에 생략할 수 있다.

 

서버 테스트는 위와 같이 브라우저로 테스트 할 수도 있지만, PostMan이란 것을 이용할 수도 있다.

 

 

GET 메서드

  • 주로 서버에서 특정 리소스를 가져올 때 사용(데이터 조회)
  • 요청하는 데이터를 URL의 쿼리 문자열에 담아 전송, 따라서 보안이 약함
  • URL 길이 제한으로 전송할 수 있는 데이터의 양이 제한적
  • 멱등성을 가지므로 요청을 여러번 보내도 결과가 같음
  • 브라우저에서 캐싱 가능

POST 메서드

  • 서버에서 데이터를 생성하거나 수정할 때 사용, 주로 데이터를 서버에 제출하는 용도
  • HTTP 메세지의 본문(Body)에 담아 전송
  • GET보다 보안성이 높음
  • 멱등성이 없어, 동일한 요청을 여러번 보내면 결과가 다를 수 있음
  • 브라우저에서 캐싱 불가

 

 

이 부분은 요청을 받으면, 요청이 들어온 곳으로 'respond with a resource라는 메세지를 send한다는 의미다.

 

(사실 정확히 이해 못해서 밑에서 또 따로 정리할 것이다)

 

* 테스트를 위해서는 반드시 저장과 npm start를 통해 서버가 실행중인 상태 혹은 애플리케이션이 실행 중인 상태로 바꿔주어야 한다. ( 클라이언트 요청을 처리할 수 있는 상태)

 

상단의 +로 테스트 할 수 있다.

올바르게 하면 다음과 같이 respond with a resorce가 뜬다 .

 

 

 

 

이제 데이터베이스 (MongoDB)를 설치하기 위한 라이브러리들을 설치해줄 것이다.

npm install하면 기본적인 것을 설치하는 것이고, 뒤에  특정 패키지 명을 붙이면 해당 패키지를 설치한다. 

 

npm install : package.json 파일에 정의된 모든 의존성 패키지를 node_modules 폴더에 설치

특정 패키지 명 붙임 : node_modules 폴더에 설치하고, package.json 파일의 의존성 목록에 해당 패키지 추가

 

질문 : 특정 패키지들은 어떻게 다운로드 되는 것인가 ? 

 

npm 레지스트리라는 온라인 저장소에서 다운로드 된다. 

공식 웹사이트인 npmjs.com 에서 여러 가지 패키지들을 찾아볼 수 있다. 

https://www.npmjs.com/

 

npm | Home

Bring the best of open source to you, your team, and your company Relied upon by more than 17 million developers worldwide, npm is committed to making JavaScript development elegant, productive, and safe. The free npm Registry has become the center of Java

www.npmjs.com

 

 

 

npm install bcrypt, express-session, session-file-store, socket.io, uuid, mongodb

 

다른사람이 만든 것을 내려받았을 때도 npm istall해 줘야 한다. 

 

 

몽고디비를 이용하기 위해 다음과 같이 app.js파일에 추가해 주었다.. 

 


03. 서버 관련 기본 코드

 

완성된 코드들을 보고 차근차근 이해하는 것이 더 빠르다고 생각한다. 

다음은 완성된 코드들이다. 

우선 app.js 코드들이다. DB에 연결과 Session을 정의한다. 

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var mongodb = require('mongodb');       // 추가
var MongoClient = mongodb.MongoClient;  // 추가

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
const session = require('express-session');
var fileStore =require('session-file-store')(session);

var app = express();

app.use(session({
  secret: process.env.SESSION_SECRET || 'session-login',
  resave: false,
  saveUninitialized: false,
  store: new fileStore({
    path: './sessions',
    ttl: 24 * 60 * 60,
    reapInterval: 60 * 60
  }),
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000
  }
}))


// MongoDB 연결
async function connectDB() {
  var databaseURL = "mongodb://localhost:27017/tictactoe";

  try {
    const database = await MongoClient.connect(databaseURL, {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    console.log("DB 연결 완료: " + databaseURL);
    app.set('database',database.db('tictactoe'));

    // 연결 종료 처리
    process.on("SIGINT", async () => {
      await database.close();
      console.log("DB 연결 종료");
      process.exit(0);
    });
  } catch (err) {
    console.error("DB 연결 실패: " + err);
    process.exit(1);
  }
}

connectDB().catch(err => {
  console.error("초기 DB 연결 실패: " + err);
  process.exit(1);
});



// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 

 NodeJS는 C#과는 조금 다른 점이 있기 때문에 중간중간 이해가 가지 않는 부분들이 많았다.

 

우선 require('문자열') 함수는 외부 모듈을 현재 파일로 불러오는 역할을 한다. 

이 함수를 통해 우리가 설치했던 express, mongodb 등의 모듈을 가져와 사용할 것이다. 

 

추가적으로 express-session이란 외부 모듈을 가져와서 사용했는데, 이는 HTTP의 특성 때문이다. 

HTTP는 매 번 연결을 끊기 때문에 매 번 로그인에 대한 정보를 갖고 있지 않으면 내가 누군지를 계속해서 전달해야한다.

세션이란걸 이용해서 로그인된 정보를 알려줄 것이다.(세션이라는 공간에서 저장하고, 이후 요청이 올 때 추가로 자신이 누군지 알리지 않아도 처리할 수 있는)

세션은 로그인 상태 유지, 장바구니 관리, 사용자 설정 저장 등 다양한 용도로 사용된다. 

 

const session = require('express-session');
// 세션 관리를 위한 기능 제공이 가능한  미들웨어를 불러옴

var fileStore = require('session-file-store')(session);
// 세션 데이터를 파일 시스템에 저장하는데 사용되는 세션 저장소,
// 위 코드는 session-file-store를 불러와 session 미들웨어와 함께 사용할 수 있도록 설정

app.use(session({ ...})));
// app..use()는 Express.js 애플리케이션에 미들웨어를 등록하는 메서드
// session 미들웨어를 애플리케이션에 등록하고 세션설정을정의

* 미들웨어 : 소프트웨어 공학에서 애플리케이션, 서비스 ,시스템 간의 통신을 돕는 소프트웨어 계층

 

 

그 이후의 async function connectDB() {}는 MongoDB에 연결하는 비동기 함수이다. 

var databaseURL = " ~  " 
// 데이터베이스 연결 주소

const database =  await MongoClient.connect(databaseURL, {});
// MongoDB 서버에 연결 시도


app.set('database',database.db('tictactoe'));
// Express 애플리케이션에 데이터베이스 객체 저장


proecess.on() ~ 
// 애플리케이션이 종료될 때 데이터베이스 연결을 닫기 위한 이벤트 리스너 등록

 

 

 

 

다음은 우리가 사용할 users 스크립트다. 

 

 

var express = require('express');
var router = express.Router();
var bcrypt = require('bcrypt');
//const { response } = require('../app');
const { ObjectId } = require('mongodb')
var saltrounds = 10;

var ResponseType = {
  INVALID_USERNAME: 0,
  INVALID_PASSWORD: 1,
  SUCCESS: 2
}

/* GET users listing. */
router.get('/', function (req, res, next) {
  res.send('respond with a resource');
});

// 회원가입
router.post('/signup', async function (req, res, next) {
  try {
    var username = req.body.username;
    var password = req.body.password;
    var nickname = req.body.nickname;

    // 입력값 검증
    if (!username || !password || !nickname) {
      return res.status(400).send("모든 필드를 입력해주세요");
    }

    // 사용자 중복 체크
    var database = req.app.get('database');
    var users = database.collection('users');

    const existingUser = await users.findOne({ username: username });
    if (existingUser) {
      return res.status(409).send("이미 존재하는 사용자입니다");
    }
    // 비밀번호 암호화
    var salt = bcrypt.genSaltSync(saltrounds);
    var hash = bcrypt.hashSync(password, salt);
    // DB에 저장
    await users.insertOne({
      username: username,
      password: hash, // 해시된 비밀번호 저장
      nickname: nickname
    });
    res.status(201).send("사용자가 성공적으로 생성되었습니다");
  } catch (err) {
    console.error("사용자 추가 중 오류 발생:", err);
    res.status(500).send("서버 오류가 발생했습니다");
  }
});

// 로그인 기능
router.post('/signin', async function (req, res, next) {
  try {
    var username = req.body.username;
    var password = req.body.password;


    var database = req.app.get('database');
    var users = database.collection('users');

    //입력값 검증

    if (!username || !password) {
      return res.status(400).send("모든 필드를 입력해주세요.");

    }

    const existingUser = await users.findOne({ username: username });
    if (existingUser) {
      var compareResult = bcrypt.compareSync(password, existingUser.password);
      if (compareResult) {

        req.session.isAuthenticated = true;
        req.session.userId = existingUser._id.toString();
        req.session.username = existingUser.username;
        req.session.nickname = existingUser.nickname;

        res.json({ result: ResponseType.SUCCESS })
      } else {
        res.json({ result: ResponseType.INVALID_PASSWORD })
      }
    }
    else {
      res.json({ result: ResponseType.INVALID_USERNAME });
    }

  } catch (err) {
    console.error("로그인 중 오류 발생", err);
    res.status(500).send("서버 오류가 발생했습니다.");
  }
})


router.post('/signout', function (req, res, next) {
  req.session.destroy((err) => {
    if (err) {
      console.log("로그아웃 중 오류 발생");
      return res.status(500).send("서버 오류가 발생했습니다.");
    }
    res.status(200).send("로그아웃 되었습니다.");
  });
})

router.post('/addscore', async function(req, res, next) {
  try {
    if (!req.session.isAuthenticated) {
      return res.status(400).send("로그인이 필요합니다.");
    }
    var userId = req.session.userId;
    var score = req.body.score;

    // 점수 유효성 검사
    if (!score || isNaN(score)) {
      return res.status(400).send("유효한 점수를 입력해주세요.");
    }

    var database = req.app.get('database');
    var users = database.collection('users');

    const result = await users.updateOne(
      { _id: new ObjectId(userId) },
      {
        $set: {
          score: Number(score),
          updatedAt: new Date()
        }
      }
    );
    if (result.matchedCount === 0) {
      return res.status(400).send("사용자를 찾을 수 없습니다.");
    }
    res.status(200).json({ message: "점수가 성공적으로 업데이트 되었습니다." });  
  } catch (err) {
    console.error("점수 추가 중 오류 발생: ", err);
    res.status(500).send("서버 오류가 발생했습니다.");
  }
});


router.get('/score', async function(req,res,next){
  try{
    if (!req.session.isAuthenticated){
      return res.status(403).send("로그인이 필요합니다.");
    }
    var userId =req.session.userId;
    var database = req.app.get('database');
    var users = database.collection('users');


    const user = await users.findOne({ _id: new ObjectId(userId)});
    if(!user){
      return res.status(404).send("사용자를 찾을 수 없습니다.");
    }

    res.json({
      id: user._id.toString(),
      username: user.username,
      nickname: user.nickname,
      score: user.score || 0

    });
  } catch (err){
    console.error("점수 조회 중 오류 발생.", err);
    res.status(500).send("서버 오류가 발생했습니다.")
  }
});

module.exports = router;

 

 

마찬가지로 express 모듈을 가져온다

.

더해서,  라우팅을 처리하는 객체를 express.Router();로 생성하고 router에 저장한다. 

라우터는 HTTP의 요청을 특정 경로와 메서드에 따라 처리하는 역할을 한다.

예를 들어 클라이언트의 요청이 들어 왔을 때, HTTP 요청은 다음과 같은 요소들을 포함한다.

HTTP 메서드 (GET,POST 등), URL, 헤더, 본문 등을 포함한 HTTP 요청이 들어오면

Node.js 서버에 도달하고, Express.js가 요청 URL과 HTTP 메서드를 분석하여 어떤 라우팅 규칙을

적용할지 결정한다. 그 후 router 객체에 등록된 라우팅 규칙과 일치하는 핸들러 함수를 찾아 실행한다.

 

bcrypt도 모듈도 불러온다. (비밀번호 암호화를 위한 라이브러리)

 

질문 : ObjectId는 왜 ? 어떻게 사용되는 것인가 ?

const { ObjectId } = require('mongodb')
 

이 부분이 제일 헷갈렸다.

결국 mongodb 안에 있는 ObjectId 타입을 사용하기 위해 가져온다고 생각하면 된다. 

나중에 mongodb 에 있는 _id 를 비교하기 위해서인데, mongodb에 있는 _id는 string 타입이 아니라 ObjectId 타입이기 때문에 비교해 주기 위해서 미리 이렇게 가져온 것이다. ( C# 코드로 비교하면 using MongoDB.Bson;)

 

// 회원가입
router.post('/signup', async function (req, res, next) {
  try {
    var username = req.body.username;
    var password = req.body.password;
    var nickname = req.body.nickname;

    // 입력값 검증
    if (!username || !password || !nickname) {
      return res.status(400).send("모든 필드를 입력해주세요");
    }

    // 사용자 중복 체크
    var database = req.app.get('database');
    var users = database.collection('users');

    const existingUser = await users.findOne({ username: username });
    if (existingUser) {
      return res.status(409).send("이미 존재하는 사용자입니다");
    }
    // 비밀번호 암호화
    var salt = bcrypt.genSaltSync(saltrounds);
    var hash = bcrypt.hashSync(password, salt);
    // DB에 저장
    await users.insertOne({
      username: username,
      password: hash, // 해시된 비밀번호 저장
      nickname: nickname
    });
    res.status(201).send("사용자가 성공적으로 생성되었습니다");
  } catch (err) {
    console.error("사용자 추가 중 오류 발생:", err);
    res.status(500).send("서버 오류가 발생했습니다");
  }
});

 

회원 가입 코드이다.

 

router.post('/signup', ...) /signup으로 끝나는 URL 에 대한 POST 요청을 받아서 처리한다는 의미이다. 

뒤에 붙은 async function이 요청을 처리하는 핸들러 함수이다. 여기서는 요청 , 응답 ,next를 받을 수 있다.

 

req, 즉 요청의 body에서 받은 값들을 갖고, 모두 입력되어 있으면 중복을체크해서 만들 수 있으면 암호화 해서 만드는 것

여기서 비교하는 방식이 신기했다. findOne({usernae: username}) 에서 좌측은 데이터베이스의 필드 이름이고, 오른쪽은 검색할 값이 된다.  { } 는 MongoDB에서 검색할 조건 객체이다. 

 

 

위 사진처럼 필드 이름 : 검색할 값 으로 넣어주면 된다. 

 

      return res.status(400).send("모든 필드를 입력해주세요");
 

 

res.status(400)과 같은 코드는 응답의 상태 코드를 설정하는 메서드이다.

서버가 클라이언트의 요청을 처리하는 결과를 나타내는 세 자리 숫자를 통해 클라이언트는 이 코드를 통해 요청이 성공적으로처리되었는지, 오류가 발생했는지 등을 판단할 수 있다. 

 

 

 

다음은 로그인 기능이다.

router.post('/signin', async function (req, res, next) {
  try {
    var username = req.body.username;
    var password = req.body.password;


    var database = req.app.get('database');
    var users = database.collection('users');

    //입력값 검증

    if (!username || !password) {
      return res.status(400).send("모든 필드를 입력해주세요.");

    }

    const existingUser = await users.findOne({ username: username });
    if (existingUser) {
      var compareResult = bcrypt.compareSync(password, existingUser.password);
      if (compareResult) {

        req.session.isAuthenticated = true;
        req.session.userId = existingUser._id.toString();
        req.session.username = existingUser.username;
        req.session.nickname = existingUser.nickname;

        res.json({ result: ResponseType.SUCCESS })
      } else {
        res.json({ result: ResponseType.INVALID_PASSWORD })
      }
    }
    else {
      res.json({ result: ResponseType.INVALID_USERNAME });
    }

  } catch (err) {
    console.error("로그인 중 오류 발생", err);
    res.status(500).send("서버 오류가 발생했습니다.");
  }
})

 

위에서 추가적으로 설명하지 않은 것이 있다. 

  var database = req.app.get('database');
    var users = database.collection('users');

 

이 부분이다. 하나하나 설명해보자면, 

'req'는 클라이언트가 보낸 요청(Request) 객체이다. 

그런데 req.app은 현재 실행중인 Express 애플리케이션(app)을 참조하는 속성이다. 

즉 요청이 들어온 애플리케이션의 app 객체를 가져오는 것이다.

더 나아가 req.app.get('database')는  app에 저장된 데이터베이스 객체를 가져오는 것이다.

처음 시작할 때 connectDB함수에서 다음과 같이 저장했었다. 

 

async function connectDB() {
  const client = new MongoClient("mongodb://localhost:27017/");
  await client.connect();

  const db = client.db("tictactoe"); // 사용할 데이터베이스 선택
  app.set('database', db);  // 🚀 Express 애플리케이션(app)에 DB 객체 저장!
}

 

이렇게 저장한 객체를 가져오는 것이다. 

그리고 이렇게 저장한 객체에서 users 컬렉션을 가져오는 부분이 

database.collection('users')이다. 

 

*MongoDB 안에는 여러 개의 DB가 있고, 각 DB 안에 여러개의 Collection이 있고, Collection 안에 여러 개의 문서가 있다.

router.post('/signout', function (req, res, next) {
  req.session.destroy((err) => {
    if (err) {
      console.log("로그아웃 중 오류 발생");
      return res.status(500).send("서버 오류가 발생했습니다.");
    }
    res.status(200).send("로그아웃 되었습니다.");
  });
})

가장 간단한 로그아웃 함수

 

 

router.post('/addscore', async function(req, res, next) {
  try {
    if (!req.session.isAuthenticated) {
      return res.status(400).send("로그인이 필요합니다.");
    }
    var userId = req.session.userId;
    var score = req.body.score;

    // 점수 유효성 검사
    if (!score || isNaN(score)) {
      return res.status(400).send("유효한 점수를 입력해주세요.");
    }

    var database = req.app.get('database');
    var users = database.collection('users');

    const result = await users.updateOne(
      { _id: new ObjectId(userId) },
      {
        $set: {
          score: Number(score),
          updatedAt: new Date()
        }
      }
    );
    if (result.matchedCount === 0) {
      return res.status(400).send("사용자를 찾을 수 없습니다.");
    }
    res.status(200).json({ message: "점수가 성공적으로 업데이트 되었습니다." });  
  } catch (err) {
    console.error("점수 추가 중 오류 발생: ", err);
    res.status(500).send("서버 오류가 발생했습니다.");
  }
});
 

 

updateOne()은 조건에 맞는 첫 번째로 발견된 문서를 업데이트하는 함수다.

_id가 userId를 ObjectId로 변환한 것과 같은 것을 찾아서 점수와 날짜를 바꿔준다. 

 

router.get('/score', async function(req,res,next){
  try{
    if (!req.session.isAuthenticated){
      return res.status(403).send("로그인이 필요합니다.");
    }
    var userId =req.session.userId;
    var database = req.app.get('database');
    var users = database.collection('users');


    const user = await users.findOne({ _id: new ObjectId(userId)});
    if(!user){
      return res.status(404).send("사용자를 찾을 수 없습니다.");
    }

    res.json({
      id: user._id.toString(),
      username: user.username,
      nickname: user.nickname,
      score: user.score || 0

    });
  } catch (err){
    console.error("점수 조회 중 오류 발생.", err);
    res.status(500).send("서버 오류가 발생했습니다.")
  }
});

 

 

마지막 점수 함수이다. 

데이터베이스에서 필요한 값들을 찾아 json 형태로 전달한다. 

 

 

 

 

 

* 참고 : Body에 오타가 있는지 확인하려면 Json Parser 검색 - Json Parser Online
http://json.parser.online.fr/

 

Json Parser Online

 

json.parser.online.fr


오늘 전체적으로 모르는 것도 너무 많이 나왔고, 처음 배우는 것도 너무 많이 나왔지만 부족한 시간 탓에 훌쩍훌쩍 넘어가시는 느낌이 있었다. 딱 이 정도 속도나 조금 더 빠른게 좋은 것 같다. 조금 더 치열하게 공부하고 싶다. 

 

오늘의 목표

1. 코테 문제풀이

2. 기획(Clicker)