TIL

[멋쟁이사자처럼 부트캠프 TIL회고] 오목 게임 만들기

Cadi 2025. 3. 18. 21:34

오늘한 것

 

1. 서버 IP 주소 확인

2. 멀티 플레이 구현 준비

3. 시행착오가 있었던 부분 ( 파싱 관련 )

 

 

01. 서버 IP 주소 확인

 

현재에는 프랑크푸르트 서버로 ( 무료 ) koyeb를 이용하고 있다. 

따라서 프랑크 프루트 IP 주소로 DB에 접속 가능하게 해 주었더니 오류가 떴다. 

 

일단은 외부 IP도 접속이 가능하게끔 모든 주소를 허용해 두었지만, 최소한의 보안을 위해

현재 서버의 IP주소를 확인하고, 이 주소에서만 접근할 수 있도록 조치할 예정이다. 

 

두 가지 방법이 있다, 외부 모듈을 설치(axios)하는 방법과, 기본적인 https 모듈을 사용하는 방법. 

 

const axios = require('axios');
const https = require('https');

 

 

  .then((res) => console.log('🟢 현재 Koyeb 서버의 외부 IP(axios):', res.data.ip))
  .catch((err) => console.error('IP 확인 실패:', err));


  https.get('https://api.ipify.org?format=json', (res) => {
    let data = '';
    res.on('data', chunk => { data += chunk; });
    res.on('end', () => {
      console.log('🟢 현재 Koyeb 서버의 외부 IP(https):', JSON.parse(data).ip);
    });

 

 

둘 모두 같은 결과가 나온다. 

 

위의 IP 주소를 몽고DB의 Network whiteList에 등록해주면 된다. 

 

* 주의 등록한 후 바로 재배포하면 안될 때가 있다. 3~5분의 시간이 지나고 재배포하자 .

 

 

02. 멀티플레이 구현 준비 

 

헤맸던 부분 ; JS는 문법이 달라서 ... 

 

1. 배열이 가변적이다. 

2. undefined하면 기본적으로 falsy 값이 나온다 .

 

const users = require('../models/User');

const { Server } = require('socket.io');
const { v4: uuidv4 } = require('uuid');

// rooms 안에 들어가는 배열 구조
// {roomId: String, userId1: String, userId2: STring,  rank: Number}
let rooms = [];

module.exports = function (server) {
    const io = new Server(server);

    io.on('connection', (socket) => {
        console.log("사용자가 연결되었습니다");
        socket.on('joinMatch', (grade, userId) => {
            console.log(`${userId}가 매칭을 시작했습니다.`);
            const matchedRoom = findMatchingRoom(grade);
            if (matchedRoom) {
                socket.join(matchedRoom.roomId);
                socket.emit('joinRoom', { roomId: matchedRoom.roomId });
                //먼저 들어와 있는 사람 id1 뒤에 들어온 사람 id2
                socket.to(matchedRoom.roomId).emit('startGame', { userId1: matchedRoom.userId1, userId2: userId });


                // 매칭된 방 제거
                rooms = rooms.filter(room => room.roomId != matchedRoom.roomId);
            }
            else {
                const roomId = uuidv4();
                socket.join(roomId);
                rooms.push({ roomId, userId1: userId, userId2: null, grade });
                socket.emit('createRoom', { roomId });
            }
        });

        socket.on('sendPlayerInfo', (data) => {
            const userData = data.userData;
            const roomId = data.roomId;
            socket.to(roomId).emit('getPlayerInfo', userData);
        });

        // 여기서부터는 멀티플레이 관련
        socket.on('doPlayer', (index, roomId) => {
            console.log(`${index}에 돌을 두었습니다.`);
            socket.to(roomId).emit('doOpponent', (index));
        });

        socket.on('sendGameResult', (userId, result) => {
            users.findOne({ userId: userId }).then(user => {
                if (result == 1) {
                    user.win += 1;
                }
                else if(result == 0){
                }
                else if(result == -1){
                    user.lose -=1;
                }
                user.save();
            });


        });

        //TODO: eixtRoom이랑 endGame emit하는 부분

        socket.on('disconnect', () => {
            console.log('사용자가 연결을 끊었습니다.');
        });
    });

}

function findMatchingRoom(userGrade) {
    const allowGradediff = 1;
    return rooms.find(room => Math.abs(room.rank - userGrade) <= allowGradediff);
}

 

 

일단 기본적으로 구현해본 코드. 

 

현재에는 grade( 급수 ) 관련 코드가 들어가 있지 않다.

조금 수정해서 급수 관련 코드를 넣었다.

function GradeUpdate(user, result) {
    var limit;
if(user.grade == 18 && user.gradeScore == -3 ) return;
if (user.grade == 1 && user.gradeScore == 10 ) return;

    if (user.grade <= 4) {
        limit = 10;
    }
    else if (user.grade <= 9) {
        limit = 5;
    }
    else {
        limit = 3;
    }
if( result == 1){
  user.gradeScore++;
  if (user.gradeScore > limit){
    user.grade++;
    user.gradeScore = 0;
  }
}
else if ( result == -1){
 user.gradeScore--;
 if (user.gradeScore < -limit){
    user.grade--;
    user.gradeScore = 0;
 }
}

}

 

 

급수를 업데이트하는 코드를 만들고, 이와 같이 넣어준다.

 

socket.on('sendGameResult', (userId, result) => {
            users.findOne({ userId: userId }).then(user => {
                if (result == 1) {
                    user.win += 1;
                }
                else if (result == 0) {
                    user.draw += 1;
                }
                else if (result == -1) {
                    user.lose -= 1;
                }
                GradeUpdate(user, result);
                user.save();
            });

 

 

일단은 여기까지... 

 

추가적으로 유저 스키마도 업데이트 해 주었다. 

lose와 draw, profileIndex를 넣어주었다. 

const userSchema = new mongoose.Schema({
    nickName: { type: String, default: '홍길동'},
    userId: { type: String, default: 'abcd' },
    userPassword: { type: String, default: '1234' },
    coin: { type: Number, default: 0 },
    win: { type: Number, default: 0 },
    draw: { type: Number, default: 0},
    lose: { type: Number, default: 0 },
    grade: { type: Number, default: 18},
    gradeScore: { type: Number, default: 0},
    profileIndex: { type: Number, default: 0}
});

 

 


 

이제 팀원 분들과 소통하며 필요한부분을 찾고 있었다.

 

1. 유저 정보 바꾸기 . ( 프로필 사진이나 닉네임 변경 ) 

2. 타임아웃 정보 전달 (상대방에게 승리 정보 전달 ) 

 

1. 유저 정보 바꾸기

exports.changeUserInfo = async (req, res) => {
    try {
        if(!req.session.isAuthenticated) res.stauts(404).send('로그인이 필요합니다.');
        var userId = req.body.userId;
        var nickName = req.body.nickName;
        var profileIndex = req.body.profileIndex;
        var user = await users.findOne({ userId: userId });
        if (user){
            if (nickName != null) {
                user.nickName = nickName;
            }
            if (profileIndex != null){
                user.profileIndex = profileIndex;
            }
            await user.save();
            res.status(200).send('사용자 정보가 성공적으로 변경되었습니다.');
        }
        else{
            res.status(404).send('없는 유저 ID입니다.');
        }
     
    }
    catch(err){
        console.log('cannot change the user Information '+ err);
        res.status(500).send('사용자 정보 변경 중 오류가 발생했습니다');
    }
}

 

세션도 추가했기에 세션을 통해서 했다.

사실 조금 더 정확하게 하려면 자신의 id의 정보만 바꿀 수 있게 하는 것이 좋겠지만, 일단은 패스

 

 

2. 타임 아웃 정보 전달

 socket.on('timeOut',(isTimeout,roomId)=>{
            console.log('상대방의 시간 초과로 승리했습니다.');
            socket.to(roomId).emit('timeOutOpponent', (isTimeout));
        })

 

 

03. 파싱 관련 문제 

 

처음에 데이터를 전달할 때에 

 

        _socket.Emit("joinMatch", new {grade});

이렇게 전달했다. 

그런데 계속 전달이 되지 않길래 로그를 찍어봤다. 

 io.on('connection', (socket) => {
        console.log("사용자가 연결되었습니다");
        socket.on('joinMatch', (data) => {
            console.log('사용자가 매칭을 시작했습니다'+ data);

 

사용자가 매칭을 시작했습니다 라는 로그도 뜨지 않았다, 그럼 joinMatch가 emit이 되지 않은 것인가 ? 

했지만, 사용자가 연결되었습니다. 라는 로그는 잘 뜨길래 데이터 쪽에서 문제가 있다고 판단했다. 

그래서 확실히 하기 위해 다음과 같이 바꿔 주었다.

 

_socket.Emit("joinMatch", JsonConvert.SerializeObject(new { grade = grade }));

 

이렇게 하면 정상적으로 정보를 잘 전달한다. 

 

Emit으로 joinMatch는 전달이 잘 되었지만, 파싱 실패로 인해서 ( 정상적인 JSON 객체가 전달되지 않아서) 이벤트 콜백이 호출되지 않았던 것이다. ( 비정상 구조/배열/문자열로 래핑된 JSON이면 이벤트를 수신해도 콜백을 실행하지 못함).

즉, 데이터 형식 오류로 인해서 이벤트 콜백 함수가 '무시'된 것 처럼 보인 것이다. 

 

이런 문제가 발생한 이유는 SocketIOUnity가 내부적으로 전달할 데이터를 직렬화 하는데, 이 떄 명확한 JSON Object가 아닌 Object[]로 감싼 구조가 되는 버그/제약이 자주 발생해서 그런 것 같다. 

 

* C#에서 익명 객체를 내부적으로 배열로 직렬화하거나, .NET 내부에서 JSON을 자동 감싸는 특성 때문

 

GPT의 예시를 빌려오자면, 

그렇기 떄문에 명확하게 올바른 객체를 전달하는 것이 좋은 것 같다.

 

 

결국 해결한 코드는 다음과 같다. 

 


    io.on('connection', (socket) => {
        console.log("사용자가 연결되었습니다");
        socket.on('joinMatch', (data) => {
            console.log('사용자가 매칭을 시작했습니다'+ data);
            let parsedData;
            if (typeof data === 'string') {
                try {
                    parsedData = JSON.parse(data);
                } catch (e) {
                    console.error('데이터 파싱 실패:', e);
                    parsedData = { grade: 0 };
                }
            } else {
                parsedData = data;
            }
           
            const grade = parsedData.grade; // 파싱된
            const matchedRoom = findMatchingRoom(grade);
// 이하 생략

 

 

public void RequestMatch(int grade)
{
    //_socket.Emit("joinMatch", new {grade});
   // _socket.Emit("joinMatch", JsonConvert.SerializeObject(new { grade = grade }));
   var matchData = new MatchData { grade = grade };
   _socket.Emit("joinMatch", JsonConvert.SerializeObject(matchData));


}

 

**** 추가 **** 

public void SendMessage(string roomId, string nickName, string message)
{
    _socket.Emit("sendMessage", new { roomId, nickName, message });
}

저번에 했던 것을 찾아봤는데, 익명 객체로 보낸다. 

 socket.on('sendMessage', function(message) {
     console.log('메시지를 받았습니다: ' + message.roomId + ' ' + message.nickName + ' : ' + message.message);
     socket.to(message.roomId).emit('receiveMessage', { nickName: message.nickName, message: message.message });
 });

 

 


중간에 실패했던 코드

 

 socket.on('joinMatch', (grade) => {
            console.log('사용자가 매칭을 시작했습니다'+ grade);

 

public void RequestMatch(int grade)
{
    //_socket.Emit("joinMatch", new {grade});
   // _socket.Emit("joinMatch", JsonConvert.SerializeObject(new { grade = grade }));
   // var matchData = new MatchData { grade = grade };
   // _socket.Emit("joinMatch", JsonConvert.SerializeObject(matchData));

   _socket.Emit("joinMatch", grade);


}

 

기본적으로 '객체'를 전달하는 방식이 기준이라 단일 int를 전달하는 것은 실행되지 않았다. 

 

또 위에서 익명객체를 보냈을 때 잘 되길래 한 번 실험해봤다. 

 

  io.on('connection', (socket) => {
        console.log("사용자가 연결되었습니다");
        socket.on('joinMatch', function(data) {
            console.log('사용자가 매칭을 시작했습니다'+ data);
             var grade = data.grade2;
             console.log('grade는 : ' + grade);

 

 

public void RequestMatch(int grade)
{
    string grade2 = grade.ToString();
    _socket.Emit("joinMatch", new {grade2});

 

int형이라 안되는건가 싶어 string으로 바꿔서 보내봤는데도 제대로 작동하지 않는다. 

 


시행 착오 부분에 새롭게 정리했다.

멘토님과 함께 많은 시간을 쏟아부어 정리했다. 내가 너무 데이터 형식에만 매몰되어 있어서 생긴 문제도 있었다. 

 

https://febelo0524.tistory.com/172

 

Unity < - > Node.js Socket.IO 데이터 교환

문제 : 데이터를 전하는 방식이 될 때도 있고 안될 때도 있음 다음은 내가 강사님에게 질문한 전문이다.  안녕하세요 ! 질문이 생겨 질문 드립니다. 강사님이 알려주셨던 코드 중, 메시

febelo0524.tistory.com

자세한 내용은 다음 글 참고 !