상세 컨텐츠

본문 제목

프로젝트#1. 로그라이크 게임개발 JavaScript(3)

내일배움캠프 학습/진행 프로젝트

by 남민우_ 2024. 11. 18. 00:24

본문

이번 글에서는 이 로그라이크 게임 프로젝트를 진행하면서 느꼈던 점, 코드 버그 상황 등 트러블 슈팅을 진행한다.

 

트러블 슈팅

사실 모든 기능 구현자체는 크게 어렵지 않았다.

다른 언어라고는 해도 게임 개발 경험이 있었고 여러 프로젝트를 완성시켜봤기에 비교적 간단한 기능들로 구성된 이번 '로그소울' 은 수월하게 진행되었다.

그 안에서 내가 목표로 하고자 했던 것은 크게 두가지인데,

1. 자바스크립트 언어의 활용 이해

2. 데이터 추가 및 연동

이라고 볼 수 있겠다.

 

1번은 내용이 조금 길어질 듯 하여 2번에 대해서 먼저 진행한다.

 

2. 데이터 추가 및 연동

기획 단계에서부터 여러 몬스터 종류와 그에 맞는 설정을 주기로 했었고, 스토리를 위한 엔딩 스크립트와 같은 데이터들이 있었기에 데이터 파일의 활용은 필수적이었다.

해서 초기에 생각했던 것은 엑셀 파일 작성 및 연동을 통한 데이터 불러오기였으나, 부적합하다고 판단하게 된 두가지 계기가 있었다.

먼저 불러야와야 하는 데이터가 두 종류였다. 몬스터 데이터, 엔딩 스크립트 이 두가지 종류를 불러와야 했는데, 이를 하나의 엑셀에 담기에는 구조적으로 옳지 않다고 생각했고, 두 개의 엑셀 파일로 분리하여 불러오기에는 필요한 데이터 크기에 비해 많은 동작을 요구하는 것이 아닌가 라고 생각했다.

 

또 다른 이유로는 숙련도와 일정의 문제가 있었다.

부끄럽게도 엑셀 파일의 프로젝트 연결을 진행해본 경험이 없어서 새롭게 알아가면서 진행하기에는 약 일주일이라는 시간이 길지 않다고 느껴졌다.

이 또한 시도하고자 한다면 좋은 기회가 되었겠지만 결과적으로 json 파일의 데이터 연동 또한 처음 경험해보는 기술이었기에 이번 회차에서는 json 파일의 연동을 학습하고 다음 회차에 엑셀 파일 연동에 대해서 학습하고자 정리하였다.

 

FireBase 의 연동 또한 고려해보았다.

데이터 테이블 연동을 통해 관리 및 호출에 있어서 좋은 방법이 될 수 있지만, 위에서 언급했듯 불러와야 하는 데이터 자체가 크게 많지 않았다.

또한 프로젝트의 특성 상 데이터 값들은 한번 정해지고 나면 동적으로는 바뀔 일이 없기에 필요한 과정에 비해 오버되는 동작이라고 여겨졌고, json 파일 연동과는 달리 이전 '방명록 만들기' 시간에 학습을 진행한 적이 있기에 마찬가지로 패스하였다.

 

1. 자바스크립트 언어의 활용 이해

사실 이 1번 항목이 캠프에서 프로젝트를 제시한 목적이자, 내가 도달하고자 했던 목표일 것이다.

캠프의 강의에서 자바스크립트의 기초 문법에 대해서 학습을 진행하긴 했지만, 이는 대부분 이론적인 개념에 대해서 학습했을 뿐 언어를 진정으로 이해하기 위해서는 직접 프로젝트를 진행하면서 사용법과 버그 사항, 그 대처법에 대해서 스스로 깨우쳐야 한다고 생각하는 편이다.

따라서 이번 프로젝트에서 최대한 많은 시도를 하면서 버그를 일으켜보고자 했고, 안타깝다고 해야할지 다행이라고 해야 할지 대부분의 상황에서는 크게 문제 없기 진행되었다.

 

그나마 있었던 어려움의 한가지 예시를 들어보자면 메서드의 객체 반환을 들 수 있겠다.

먼저 코드를 보여주고 설명을 진행한다.

//플레이어의 내부 메서드 - 공격 기능
Attack(isParring) 
{
	if((this._DoubleATKRate/100) > Math.random())
	{
		//연속 공격 진행
		return { 
			damage : isParring ? (this._ATK*2) *2 : this._ATK * 2,
			message : chalk.blue(`연속 공격 발동!`)
		};
	}
	else
	{
		//일반 공격 진행
		return { 
			damage : isParring ? (this._ATK*2) : this._ATK,
			message : chalk.blue(`${isParring ? (this._ATK*2) : this._ATK} 의 공격을 합니다.`)
		};
	}
}

//사용 코드 - BattleInput 함수
case "1":
	//플레이어 공격 처리
	pDamage = player.Attack(false);
	logs.push(chalk.green(`공격 : ${pDamage.message}`));
	logs.push(monster.Mon_Hitted(pDamage.damage));

 

당시 상황

플레이어와 몬스터의 공방 주고받기를 구현하기 위한 과정이었다.

플레이어의 공격 메서드에서 초기에는 damage 값만을 반환하도록 하고 내부적으로 console.log 를 통해 전투 로그를 출력하려고 했으나 battle 함수 내부에서 logs 객체를 출력하기 전에 console.clear() 로 화면을 전체 지우는 동작이 있었다.

하지만 이 console.clear() 과정을 삭제하자니 전투 로그가 너무 많이 쌓일 경우 유저 경험에 불편함이 있다고 판단되어 고민하던 중, 여러 타입의 값을 동시에 반환하는 '객체 반환'에 대해서 떠올릴 수 있었다.

 

해결 : 객체 반환

logs 를 공격 메서드에 또 매개변수로 전달하고 이를 담아서 반환하는 등의 동작은 분명히 번거롭고 서로가 서로에게 얽혀들어가는 객체지향적이지 않은 코드가 될 것이 분명했다.

해서 공격 메서드 자체에서 반환을 하여 battle 함수에서 처리하되, 반환하는 값을 객체로 설정하여 전투 로그를 담당하는 message, 공격 수치를 담당하는 damage 두 개의 변수를 담았고, battle 함수 내부에서는 이에 따른 각자의 후처리를 진행할 수 있었다.

 

이후 몬스터의 공격 메서드 쪽에서도 동일한 구성을 통해 잘 처리할 수 있었다.

 

사실 이번 프로젝트에서 유일한 실패라고 여기는 부분은 이제 설명할 내용이다.

 

실패 : ReadLineSync

당시 상황

이 readlineSync 는 초기 스켈레톤 코드에서부터 사용자의 입력을 받도록 하기 위해 주어진 코드였다.

모듈 'readline-sync' 를 통해 임포트하고 내부 코드에서는 readlinSync.question()을 통해 입력을 받는 과정을 지니는 코드인데, 초기에는 오류 없이 동작하던 이 코드에서 문제가 발생했다.

 

발견 자체는 사실 사소한, 하지만 알고보니 심각한 상황이었다.

바로 처음 프로세스 실행 시 입력이 한번 씹히는 듯한 현상이 나타난 것이다.

사진으로 설명하면 다음과 같은데, 처음 1을 입력했음에도 불구하고 메인 화면이 다시 나타나는, 입력값이 사라진 현상이 나타난 것이다.

초기에는 입력이 정상적으로 처리되지 않았거나, 내가 QA과정에서 실수가 있었겠지 라고 생각하고 넘겼지만, 이 readlineSync 로 인해 여러 버그 상황이 나타나기 시작했다.

 

발생한 버그

1. 위에서 언급한 메인화면 반복 출력 현상

2. 전투 로그 딜레이 출력 버그 - 모든 로그가 나타나지 않고 다음 로그로 이월되어 나타나는 현상

- 그에 따라 전투 공방 수치 적용 및 스테이지 클리어가 정상적으로 동작하지 않음

3. 스테이지 1에서 몬스터 첫 처치 시, displayLobby() 로 돌아가는 현상

4. 플레이어의 EXP가 제대로 적용되지 않는 현상

- 얻은 경험치가 정상적으로 추가되지 않고, 두번의 반복 시 한번만 적용되는 버그 발생

 

이 모든 일이 readlinSync 단 하나 때문에 나타난 버그 현상이다.

초기에 이 버그들을 발견했을 때는 각자 다른 원인으로 일어나는 버그라고 판단되어 하나씩 해결하려고 했었다.

 

먼저 시도했던 것은 비교적 간단하다고 생각했던 4번이었는데, 간단한 만큼 골치를 썩혔다.

이 경험치 관련 코드는 사실 굉장히 간단한데,

1. battle 함수에서 몬스터 생성, return 시 이 몬스터의 EXP 값 반환 - exp 에 할당

2. startGame 함수에서 player.EXP += exp; 적용

3. 플레이어 내부 EXP setter 에서 수치 적용, 100 초과 시 레벨업 및 100만큼 마이너스(오버된 경험치 값을 지우기 위해)

이렇게 3단계의 동작이다.

 

문제가 있다면 빠르게 발견될 수 있는 매우 간단한 동작임에도 버그가 해결되지 않고, 다양한 시도를 해보았으나 모두 동일한 현상이 지속되었다.

 

버그 상황 중 1번의 해결을 위해 캠프의 튜터님께 질문드린 결과, 이 모든 상황이 readlineSync 의 자체 에러에 의해서였음을 알게 되었다.

readlineSync 자체가 개발되고 업데이트 된지 몇 년의 시간이 지난 상황이었고, 최신 OS에서는 정상적으로 동작하지 않을 수 있다는 것이었다.

사실 이 원인을 알게 되었을 때 좀 허탈하면서도 어이가 없었다고 말할 수 있겠다.

자바스크립트와 ES6 자체가 개발된 것이 최근임에도 불구하고 최신 OS에서 버그가 발생하는 상황이라니, 심지어 저 버그들을 해결하기 위해 코드를 계속 살피고 디버깅 했던 것이 모두 애초부터 잘 짜여진 코드였다는 것이 조금 억울하다시피 한 심정이었다.

 

해결 시도

튜터님의 조언을 빌려, 시도해본 방법은 두가지였다.

1. readlineSync 에 내부 함수를 구현하여 입력 처리 실패에 대한 처리

2. readlineSync 를 readline 으로 대체

 

1번의 경우 코드는 다음과 같다.

export const getInput = (options = {isInitial : true}) => {
    const {isInitial } = options;
    const question = isInitial ? "Enter : " : "Repeat : ";

    const input = readlineSync.question("Enter : ");
    if(!input) return getInput({isInitial : false});
    else return input;
};

 

이렇게 내부 구현 후 외부에서는 getInput 을 사용하여 처리하는 과정이었다.

하지만 이는 다소 아쉬운 점으로 내가 언어에 대해 완벽히 숙달하고 사용한 것이 아니라 그런지, 이 자체적으로도 여러 버그가 발생했었고 정상적으로 시행된 경우에도 기존의 버그가 수정되지 않았다.

 

함수의 기능 자체는 초기 실행 시 'Enter : ' 가 출력, 입력이 정상적으로 처리되지 않았을 경우 isInitial 을 false 로 변경하여 'Repeat : ' 가 출력되어야 하지만 실행 결과 Enter 만 반복 출력되며 입력이 처리되지 않은 현상이 수정되지 않았었다.

 

해서 이 1번은 기각, 2번을 시도하였다.

기존에 입력 및 처리는 다음과 같이 진행했었다.

async function handleUserInput() {
    const choice = readlineSync.question('Enter : ');
    switch (choice) {
        case '1':
            console.log(chalk.green('게임을 시작합니다.'));
            await startGame();
            break;
        case '2':
            console.log(chalk.yellow('구현 준비중입니다.. 게임을 시작하세요'));
            handleUserInput();
            break;
        case '3':
            console.log(chalk.blue('구현 준비중입니다.. 게임을 시작하세요'));
            handleUserInput();
            break;
        case '4':
            console.log(chalk.red('게임을 종료합니다.'));
            process.exit(0); 
        default:
            console.log(chalk.red('올바른 선택을 하세요.'));
            handleUserInput(); 
    }
}

 

이 코드를 다음과 같이 수정하는 것이다.

import readline from 'readline';
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

async function handleUserInput() {
rl.question('Enter : ', (choice) => {
    switch (choice) {
        case '1':
            console.log(chalk.green('게임을 시작합니다.'));
            await startGame();
            break;
        case '2':
            console.log(chalk.yellow('구현 준비중입니다.. 게임을 시작하세요'));
            handleUserInput();
            break;
        case '3':
            console.log(chalk.blue('구현 준비중입니다.. 게임을 시작하세요'));
            handleUserInput();
            break;
        case '4':
            console.log(chalk.red('게임을 종료합니다.'));
            process.exit(0); 
        default:
            console.log(chalk.red('올바른 선택을 하세요.'));
            handleUserInput(); 
    }
    rl.close();
  });
}

 

이렇게 수정할 경우 입력이 처리되지 않는 현상 자체는 해결이 되었다.

하지만 또 다른 버그가 나타나는데, 입력이 두번씩 나타나는 것이다. 말로는 설명이 어려워 사진을 첨부한다.

잘 보이지 않지만, 1을 한번만 타이핑했음에도 화면 상에서는 11 로 두번 나타나는 버그가 발생한 것이다.

이는 입력 호출 부분에서 중복 호출된 것으로 판단하고, 다른 버그들이 해결이 되는지 먼저 시도해보려고 했으나, 다른 쪽에서도 새로운 버그가 발생했다.

 

이는 코드를 수정한 현재 재현하기가 어려워 말로만 설명하자면, displayLobby() 함수가 무한 호출되어 console.clear() 가 계속되는 바람에 사용자의 입력을 받을 수 없는 현상이 발생한 것이다.

원인을 짐작하자면 입력을 받는 코드와 관련해서 displayLobby() 함수가 연결되어 무한 루프에 빠진 것이라고 판단되기는 하지만, 호출 부분은 수정하지 않고 입력 처리 코드만 수정했음에도 이런 현상이 발생하자 더이상 해결할 수 없다고 판단하였다.

 

사실 모든 상황에서 초기에 설명했던 버그들이 발생한다면 반드시 수정해야 했겠지만, 해결할 수 없다 라고 판단하게 된 강력한 계기가 저 초기 버그 4가지가 최신 OS에서만 발생한다는 것이었다.

Win10, Mac 을 사용하는 튜터님 혹은 다른 학생들의 컴퓨터에서 실행했을 때는 모든 코드가 문제없이 의도대로 동작하는 것을 보았고, 이 readlineSync 의 버그와 관련해서 여러 자료들을 찾아봤지만 비슷한 내용을 찾을 수 없었다.

 

최종, 그리고 심정

결국 버그가 발생했지만, 다른 OS에서는 모든 코드가 정상적으로 동작되는 상태로 코드를 초기화 시키고 프로젝트를 마무리짓게 되었다.

 

개발자를 지망함에 있어서 발생한 버그를 방치하는 것이 매우 바람직하지 않다는 것을 알고 있다.

시간이 걸리더라도 이를 해결하고자 하는 것이 맞지만, 그렇지 않았다는 점에서 아직 많이 부족함을 느끼고 있다.

단순히 상황탓을 하자면 코드의 자체 버그라 디버깅 자체도 어렵다는 핑계를 댈 수 있겠지만 이는 어디까지나 핑계일 뿐 분명히 해결할 수 있는 방법이 있었을 것이다.

 

프로젝트를 완성했음에도, 기능 구현 자체는 성공적으로 해냈다고 생각하면서도 찝찝함을 감출 수 없는 점이 계속 아쉽고 다음 프로젝트를 진행할 때는 이 경험을 토대로 기존 스켈레톤 코드에서부터 해석을 시작하여 버그 발생을 미연에 방지하는 단계를 추가적으로 밟아야겠다고 생각하게 된 계기가 될 것 같다.

관련글 더보기