오늘의 삽질

RogueLike TextGame

애니메이션 효과 넣기

  • 텍스트 게임으로써 한 번에 모든 문장이 나오는게 몰입감이 떨어지는 느낌이라 약간의 애니메이션 효과가 있으면 좋겠다 생각을 했다.
  1. 스켈레톤 코드를 뜯어보았을 때, logs[] 에 값을 저장하고 while을 돌려 console.log()를 쓰는 것을 발견헀다.

     while(player.hp > 0) {
         console.clear();
         displayStatus(stage, player, monster);
    
         logs.forEach((log) => console.log(log));
    
         console.log(
         chalk.green(
             `\n1. 공격한다 2. 아무것도 하지않는다.`,
         ),
         );
         const choice = readlineSync.question('당신의 선택은? ');
    
         // 플레이어의 선택에 따라 다음 행동 처리
         logs.push(chalk.green(`${choice}를 선택하셨습니다.`));
     }
    
  2. 그렇기에 logs 내부를 전부 돌릴 때, timer를 중간에 넣어주는 형식이 좋겠다고 생각했다.

  3. forEach의 콜백함수에 async를 붙이고 사용해도 비동기 작업을 기다리지 않는다..
    => 여기서 동기 관련 for 함수들을 찾아볼 때 for await of을 발견했다!

  4. timer 기능을 하여 한번만 실행해주는 setTimeout()함수를 이용했다.

         for await (const log of logs) {
         console.log(log)
         // 타이머로 Promise 의 resolve를 반환하게 하여 동기적 표현!
         await new Promise(resolve => setTimeout(resolve, 300));
     }
    

파일 분리 및 관리하기(+Github연결)

  • 스켈레톤 코드에선 코드들이 game.js에 뭉쳐있어 코드 수정 중 스크롤을 너무 왔다갔다 하게 된다..
    => 파일들을 분리해서 각각의 코드들의 접근성을 높이자!
  1. 구현 예정 기능들을 생각해서 event 폴더와 class 폴더를 만들어 분리하기로 하였다!

    • event 폴더에는 battle(전투) / reward(보상) / gameclear,over(엔딩)을 넣어줄 예정이다.

    • class 폴더에는 player / monster / reward / weapon 등의 구조들을 생각했다!

  2. 저번에 첫 프로젝트 경험으로 파일들을 분리하는건 import와 export로 쉽게 구현 하였다.

파일구조

전투 이벤트

  • 플레이어의 공격 / 회복 및 몬스터의 공격 / 회복 등의 기능을 구현하기 위해 class 폴더에 파일들을 나눠주어 메서드를 만드는게 좋다고 생각했다!
    => 보통 객체의 행동이나 상태를 제어하는 건 그 객체의 메서드를 이용하는게 좋다고 본적이 있다! (수정용이)
  1. player의 필요한 값들과 행동제어 메서드들을 구상하며 기본적인 틀을 짯다!

     class Player {
         //기본 구조
         constructor() {
             // 정신력
             this.maxHp = 100;
             this.hp = this.maxHp;
             // 무기
             this.weapon = Weapons[0];
             // 몰입도
             this.minDmg = 20 + this.weapon.damage;
             this.maxDmg = 30 + this.weapon.damage;
             // 이해력
             this.lev = 1;
             // 수면의 질
             this.minHeal = 100 + this.weapon.heal;
             this.maxHeal = 100 + this.weapon.heal;
             // 통계 값
             this.kills = 0;
             this.totalDmg = 0;
             this.totalHeal = 0;
             this.maxLev = this.lev;
         }
         // 문제 풀기(공격)
         attack(monster, logs) {
             //랜덤 값 추출
             let playerDmg =
                 Math.floor(Math.random() * (this.maxDmg - this.minDmg)) +
                 this.minDmg;
             monster.hp -= playerDmg;
             if (monster.hp > 0) {
                 this.totalDmg += playerDmg;
                 logs.push(
                     chalk.green(`${playerDmg} Page 만큼의 문제를 풀었습니다!`),
                 );
             } else {
                 this.totalDmg += playerDmg + monster.hp;
                 logs.push(
                     chalk.green(
                         `${playerDmg + monster.hp} Page 만큼의 문제를 풀었습니다!`,
                     ),
                 );
             }
             // 제출기한 업데이트 메서드 
             monster.Day(logs);
         }
             // 회복
         sleep(monster, logs) {
             const playerHeal =
                 Math.floor(Math.random() * (this.maxHeal - this.minHeal)) +
                 this.minHeal;
             if (this.hp === this.maxHp) {
                 logs.push(chalk.green(`정신이 온전합니다! 열심히 공부 하세요!!`));
             } else if (this.hp + playerHeal >= this.maxHp) {
                 logs.push(chalk.green(`정신이 매우 말끔해졌습니다!!`));
                 this.hp = this.maxHp;
             } else {
                 this.hp += playerHeal;
                 logs.push(
                     chalk.green(`${playerHeal} 만큼의 정신력을 회복했습니다!!`),
                 );
             }
             // 제출기한 업데이트
             monster.Day(logs);
         }
    
     }
    
  2. monster는 좀 더 다양성을 위해 랜덤으로 과목을 정하게 구상했다.

     class Monster {
         constructor(stage) {
             const typeRand = Math.floor(Math.random() * 100);
             if (typeRand < 5) {
                 this.type = '철학';
                 this.value = 10;
             } else if (typeRand < 20) {
                 this.type = '수학';
                 this.value = 7;
             } else if (typeRand < 55) {
                 this.type = '영어';
                 this.value = 5;
             } else if (typeRand < 80) {
                 this.type = '국어';
                 this.value = 3;
             } else {
                 this.type = '체육';
                 this.value = 1;
             } // 문제집 과목 설정
             this.name = `${this.type} 문제집`;
             // 남은 제출 기한
             this.maxDay = this.value * 5;
             this.day = this.maxDay;
             // 페이지 수
             this.maxHp = (this.value + stage) * 30;
             this.hp = this.maxHp;
             // 학습 피로도 day가 지날수록 강해짐 + 과목에따라 달라짐
             this.minDmg = Math.round(this.value + stage) + 5;
             this.maxDmg = Math.round(this.value + stage) + 10;
             // 난이도
             this.lev = stage + Math.floor(this.value / 2);
             // 망각 Page 수
             this.minHeal = 5;
             this.maxHeal = 20;
         }
             attack(player, logs) {
             //랜덤 값 추출
             const monsterDmg =
                 Math.floor(Math.random() * (this.maxDmg - this.minDmg)) +
                 this.minDmg;
             player.hp -= monsterDmg;
             logs.push(
                 chalk.redBright(`정신력이 ${monsterDmg}만큼 소모되었습니다! `),
             );
             if (player.hp <= 0) {
                 player.die(logs);
             }
         }
     }
    
  3. event 폴더에 있는 battle(전투) 파일의 선택지와 연결을 해준다.

     //battle 함수에서 매개변수로 stage / player / monster를 받아온다!
     const battle = async (stage, player, monster) => {
         let logs = [];
         let run = false;
    
         while (player.hp > 0) {
             //콘솔 로그 정리
             console.clear();
                
             console.log(
                 chalk.green(
                     `\n1. 문제풀기(공격) 2. 수면(회복) 3. 포기(도망) 4. 복습(버프)`,
                 ),
             );
    
             // readlinSync는 추가 package로 가져온 함수로 입력을 동기적으로 받는다.
             const choice = readlineSync.question('Choice? ');
    
             // 플레이어의 선택에 따라 다음 행동 처리
             logs.push(chalk.green(`${choice}번을 선택하셨습니다.`));
    
             switch (choice) {
                 case '1':
                     logs.push(chalk.blue('문제를 풀기시작합니다!'));
                     //받아온 매개변수 player의 메서드를 이용하여 공격을 실행!
                     player.attack(monster, logs);
                     //몬스터가 죽었는데 공격 당하는 일을 방지!
                     if (monster.hp > 0) {
                         monster.attack(player, logs);
                     } else {
                         logs.push(
                             chalk.green('문제집을 전부 풀어 정신이 멀쩡합니다!'),
                         );
                     }
                     break;
                 case '2':
                     console.log(chalk.blue('구현 준비중입니다.. '));
                     handleUserInput();
                     break;
                 case '3':
                     console.log(chalk.blue('구현 준비중입니다.. '));
                     handleUserInput();
                     break;
                 case '4':
                     console.log(chalk.blue('구현 준비중입니다.. '));
                     handleUserInput();
                     break;
                 default:
                     console.log(chalk.red('올바르지 않은 접근입니다.'));
                     handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음
             }
         }
     }
    

전투화면

보상 이벤트

  • 스테이지를 클리어 한 이후 랜덤한 보상을 얻는 것보단, 선택지가 어느정도 정해져있었으면 해서 구현을 하게 되었다!
  1. event 파일에 reward 파일을 만들고, 보상을 정해주기 위해 class에도 reward를 만들어준다!

  2. reward(event)를 battle과 비슷하게 구성을 하고, 매개변수로 reward(class)를 받아 reward에 메서드를 구성해주어 불러오게 만든다. => event에 함수들을 전부 넣어 관리하기에는 조금 불편해 보여서 그래따..

  3. 전투 이벤트와 동일하게 reward(event)에 class를 이어주며, battle이 끝난뒤에 몬스터를 잡았을 경우 이동하게 구성을 해준다!

    • reward 이벤트 구현
     // reward(event).js
     import chalk from 'chalk';
     import readlineSync from 'readline-sync';
     import figlet from 'figlet';
     import endgame from './gameover.js';
    
     const rewardEvent = async (stage, player, reward) => {
         let logs = [];
         let exit = false;
    
         logs.push(
             chalk.magentaBright(
                 `============================= 보상 정보 =============================`,
             ),
         );
         logs.push(
             chalk.greenBright(
                 `| 기본보상 | 회복 : ${reward.heal} | 이해력 증가 : ${reward.levUp} |`,
             ),
         );
         1;
    
         //player에 회복 및 레벨업 메서드를 이용해준다! (보상 이벤트를 구현하며 만든)
         player.heal(reward.heal, logs);
         player.levelSet(reward.levUp, logs);
    
         while (!exit) {
             console.clear();
             //단순 큰 글자를 그려주는 함수
             console.log(
                 chalk.green(
                     figlet.textSync('Reward Time', {
                         font: 'Standard',
                         horizontalLayout: 'default',
                         verticalLayout: 'default',
                     }),
                 ),
             );
    
             for await (const log of logs) {
                 console.log(log);
                 // 애니메이션 효과 딜레이
                 await new Promise((resolve) => setTimeout(resolve, 200));
             }
    
             displayReward(stage, player);
    
             logs = [];
    
             console.log(
                 chalk.green(
                     `\n선택은 한 번만 가능하며, 변경사항을 확인하고 취소할 수 있습니다(되돌아오기)`,
                     `\n1. 휴식 2. 필기구 강화 3. 뽑기 4. 포기`,
                 ),
             );
             const choice = readlineSync.question('Choice? ');
    
             // 플레이어의 선택에 따라 다음 행동 처리
             logs.push(chalk.green(`${choice}를 선택하셨습니다.`));
    
             switch (choice) {
                 case '1':
                     //reward의 메서드를 이용하여 진행 exit의 리턴값으로 while문 제어
                     exit = await reward.rest(player, stage, displayReward);
                     break;
                 case '2':
                     exit = await reward.upgrade(player, stage);
                     break;
                 case '3':
                     exit = await reward.gamble();
                     break;
                 case '4':
                     console.log(chalk.red('게임을 마무리 합니다.'));
                     return await endgame(stage, player);
                 default:
                     console.log(chalk.red('올바르지 않은 접근입니다.'));
                     handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음
             }
         }
     }
    
    • reward 구조/메서드 구현
     //reward(class).js
     class Rewards {
         constructor(player, monster, stage) {
             //회복량
             this.heal =
                 (Math.floor(Math.random() * (player.maxHeal - player.minHeal)) +
                     player.minHeal) *
                 2;
             //이해력 레벌업 수치
             this.levUp =
                 Math.round(Math.random() * monster.value) +
                 Math.round(monster.value / 2) +
                 stage;
             //수면효과 증가
             this.healUp =
                 Math.round(Math.random() * (monster.value * 2)) +
                 Math.round(monster.value) +
                 stage;
             //최대 정신력 증가
             this.hpUp =
                 Math.round(Math.random() * (monster.value * 5)) +
                 monster.value * 2 +
                 stage;
         }
    
         // 휴식 기능
         async rest(player, stage, displayReward) {
             let logs = [];
             let results = false;
             let exit = false;
             let choice;
    
             console.clear();
    
             console.log(
                 chalk.green(
                     figlet.textSync('Rest', {
                         font: 'Standard',
                         horizontalLayout: 'default',
                         verticalLayout: 'default',
                     }),
                 ),
             );
    
             logs.push(
                 chalk.magentaBright(
                     `============================= 보상 정보 =============================`,
                 ),
             );
             logs.push(
                 chalk.greenBright(
                     `| 휴식보상 | 최대 정신력 : ${this.hpUp} | 최대 수면효과 증가 : ${this.healUp} |`,
                 ),
             );
    
             while (!exit) {
                 for await (const log of logs) {
                     console.log(log);
                     // 애니메이션 효과 딜레이
                     await new Promise((resolve) => setTimeout(resolve, 200));
                 }
    
                 if (choice) {
                     await new Promise((resolve) => setTimeout(resolve, 1000));
                     return results;
                 }
    
                 displayReward(stage, player);
    
                 logs = [];
    
                 console.log(chalk.green(`\n1. 수락 2. 취소(뒤로가기)`));
                 choice = readlineSync.question('Choice? ');
    
                 // 플레이어의 선택에 따라 다음 행동 처리
                 logs.push(chalk.green(`${choice}를 선택하셨습니다.`));
    
                 switch (choice) {
                     case '1':
                         player.maxHpSet(this.hpUp, logs);
                         player.heal(this.hpUp, logs);
                         results = true;
                         break;
                     case '2':
                         results = false;
                         logs.push(
                             chalk.redBright(
                                 `선택을 취소했습니다! 선택지로 다시 이동합니다..`,
                             ),
                         );
                         break;
                     default:
                         console.log(chalk.red('올바르지 않은 접근입니다.'));
                         handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음
                 }
             }
         }
     }
    
    • 주요 파일들과 연결해주기
     // battle.js
     // while 문 내부
     if (monster.hp <= 0) {
         // 몬스터를 처치 시
         await new Promise((resolve) => setTimeout(resolve, 1000));
         // 글자를 1초 동안 유지되도록 하구
         player.kills += 1;
         // 통계값에 쓸 값 업데이트
         return true;
     }
    
     // game.js 
     // status 를 이용하여 리턴되는 값에 따라 다음 event로 이동을 하게 한다!
     let status;
     while (stage <= 10) {
         const monster = new Monster(stage);
         status = await battle(stage, player, monster);
         if (status === 'run') {
         // 도주 시
             player.hp = player.maxHp;
             continue;
         } else if (status) {
         // 스테이지 클리어 시
             const reward = new Rewards(player, monster, stage);
             await rewardEvent(stage, player, reward);
             stage++;
         } else {
         // 게임 오버!
             return await endgame(stage, player);
         }
     }
    

보상화면

무기 시스템

  • 스테이지를 통과할 때마다 선형적인 성장보다 로그라이크류 답게 여러 랜덤요소가 있었으면 해서 무기 시스템을 만들어두면 좋을 것 같다 생각했다!

  • 추가로 강화 시스템도 보상 이벤트에 추가하면 좋을 것 같아 만들었다.

  1. class에 weapon 파일을 추가하고 무기들을 저장할 수 있는 저장 파일’weapons’를 만들어준다

     //weapon class
     class Weapon {
         constructor(name, damage, heal, plus, prob) {
             //강화된 횟수
             this.plus = plus ? plus : 0;
             //강화 성공 확률
             this.plusProb = prob ? prob : 75;
             // 이름
             this.name = name;
             //기본 데미지
             this.damage = damage;
             //회복량
             this.heal = heal;
         }
     }
     export default Weapon;
    
     //weapons storage
     import Weapon from '../class/weapon.js';
    
     //무기 저장 변수
     const Weapons = [];
    
     //기본 (테스트용 무기)
     Weapons.push(new Weapon('평범한 노트', 1000, 0));
    
     export default Weapons;
    
  2. 무기를 player에 저장할 수 있는 변수를 만들어주고, 무기 교환 메서드도 만들어 준다!

     class Player {
         constructor() {
             // 정신력
             this.maxHp = 100;
             this.hp = this.maxHp;
             // 기본 무기로 설정해두기
             this.weapon = Weapons[0];
             // 몰입도
             this.minDmg = 20 + this.weapon.damage;
             this.maxDmg = 30 + this.weapon.damage;
             // 이해력
             this.lev = 1;
             // 수면의 질
             this.minHeal = 100 + this.weapon.heal;
             this.maxHeal = 100 + this.weapon.heal;
             // 통계 값
             this.kills = 0;
             this.totalDmg = 0;
             this.totalHeal = 0;
             this.maxLev = this.lev;
         }
         // 무기교체 메서드
         changeUpdate(weapon) {
             // 현재 값과 받아온 값들을 저장
             const preDamage = this.weapon.damage;
             const preHeal = this.weapon.heal;
             const aftDamage = weapon.damage;
             const aftHeal = weapon.heal;
    
             //데미지 변경
             this.minDmg -= preDamage;
             this.maxDmg -= preDamage;
             this.minDmg += aftDamage;
             this.maxDmg += aftDamage;
             //회복력 변경
             this.minHeal -= preHeal;
             this.maxHeal -= preHeal;
             this.minHeal += aftHeal;
             this.maxHeal += aftHeal;
    
             //weapon 업데이트 (교체)
             this.weapon = weapon;
         }
     }
    
  3. 무기 강화를 위해 reward(event / class)에 메서드와 연결구조를 구성해준다!
    (연결 구조는 맨위의 보상 이벤트란을 참고해주세요!)

     //무기 강화 메서드
     async upgrade(player, stage) {
         let logs = [];
         let results = false;
         let exit = false;
         let choice;
         const plusWpn = player.weapon.plusWeapon(stage);
    
         console.clear();
    
         console.log(
             chalk.green(
                 figlet.textSync('Rest', {
                     font: 'Standard',
                     horizontalLayout: 'default',
                     verticalLayout: 'default',
                 }),
             ),
         );
    
         logs.push(
             chalk.magentaBright(
                 `============================= 강화 정보 =============================`,
             ),
         );
         logs.push(
             chalk.cyanBright(`| 장착한 필기구 : ${player.weapon.name} |`),
         );
         logs.push(
             chalk.yellowBright(
                 `| 강화 이전 | 이름 : ${player.weapon.name}  | 몰입도 상승 : ${player.weapon.damage} Page  | 수면효과 상승 : ${player.weapon.heal} |`,
             ),
         );
         logs.push(
             chalk.greenBright(
                 `| 강화 이후 | 이름 : ${plusWpn.name}  | 몰입도 상승 : ${plusWpn.damage} Page  | 수면효과 상승 : ${plusWpn.heal} |`,
             ),
         );
         logs.push(
             chalk.redBright(
                 `| 강화 비용 | 이해력 : ${player.lev}/${player.weapon.plus * 2 + 1} | 확률 : ${player.weapon.plusProb}% | `,
             ),
         );
    
         while (!exit) {
             for await (const log of logs) {
                 console.log(log);
                 // 애니메이션 효과 딜레이
                 await new Promise((resolve) => setTimeout(resolve, 200));
             }
    
             logs = [];
    
             if (choice) {
                 await new Promise((resolve) => setTimeout(resolve, 1000));
                 return results;
             }
    
             console.log(chalk.green(`\n1. 수락 2. 취소(뒤로가기)`));
             choice = readlineSync.question('Choice? ');
    
             // 플레이어의 선택에 따라 다음 행동 처리
             logs.push(chalk.green(`${choice}를 선택하셨습니다.`));
    
             switch (choice) {
                 case '1':
                     if (Math.random() * 100 <= player.weapon.plusProb) {
                         logs.push(
                             chalk.greenBright(`무기 강화에 성공했습니다!!`),
                         );
                         player.levelSet(-(player.weapon.plus * 2 + 1), logs);
                         player.changeUpdate(plusWpn, logs);
                     } else {
                         logs.push(
                             chalk.redBright(`무기 강화에 실패했습니다..`),
                         );
                         player.levelSet(-(player.weapon.plus * 2 + 1), logs);
                     }
                     results = true;
                     break;
                 case '2':
                     logs.push(
                         chalk.redBright(
                             `선택을 취소했습니다! 선택지로 다시 이동합니다..`,
                         ),
                     );
                     results = false;
                     break;
                 default:
                     console.log(chalk.red('올바르지 않은 접근입니다.'));
                     handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음
             }
         }
     }
    

무기강화

게임 종료 및 재시작 + 통계확인 시스템

  • 게임이 승리했을 때나 패배했을 때를 가정한 이벤트를 구현할 필요성을 느껴 event폴더에 파일을 추가해주었다!
  1. 게임 종료 시, 여러 통계값들이 보이면 재밌을 꺼 같아 player에 통계전용 값들을 만들어 준다!
    (전투 이벤트 참조)

  2. 통계값들을 저장해주는 시스템을 메서드 곳곳에 넣어준다!
    (전투 이벤트 참조)

  3. 엔딩 이벤트들을 구성해주어 통계값들을 불러와준다

     //gameover 
     import chalk from 'chalk';
     import readlineSync from 'readline-sync';
     import { startGame } from '../game.js';
    
     const endgame = async (stage, player) => {
         let logs = [];
         let exit = true;
         while (exit) {
             console.clear();
             logs.push(chalk.magentaBright(`==== 결과 ====`));
             logs.push(chalk.cyanBright(`| Stage: ${stage} |`));
             logs.push(chalk.cyanBright(`| Ending : 0 | 나는 바보다 |`));
             logs.push(chalk.cyanBright(`| 최대 이해력 : ${player.maxLev} |`));
             logs.push(chalk.yellowBright(`| 푼 문제집 수 : ${player.kills} |`));
             logs.push(chalk.redBright(`| 풀었던 Page들 : ${player.totalDmg} |`));
             logs.push(chalk.greenBright(`| 회복한 정신력 : ${player.totalHeal} |`));
             logs.push(chalk.magentaBright(`=============`));
             for await (const log of logs) {
                 console.log(log);
                 // 애니메이션 효과 딜레이
                 await new Promise((resolve) => setTimeout(resolve, 300));
             }
             console.log(chalk.green(`\n1. 새로운 인생 2. 현타와서 종료`));
             const choice = readlineSync.question('Choice? ');
             switch (choice) {
                 case '1':
                     logs.push(chalk.green('게임을 다시 시작합니다!'));
                     //함수를 빠져나간 뒤 실행!
                     return startGame();
                 case '2':
                     console.log(chalk.green('게임이 종료됩니다!'));
                     process.exit(0);
                 default:
                     console.log(chalk.red('올바르지 않은 접근입니다.'));
                     handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음
             }
         }
     };
     export default endgame;
    
  4. 엔딩 이벤트를 기존에 있는 시스템과 연결해준다!
    (보상 이벤트 란의 game.js 참고)

  5. 게임이 끝나고, 재시작하기 위해 게임시작 기능인 game.js/startGame()을 재사용 하려는데.. 기존의 게임인 startGame() 함수 에서 빠져나오지 못해 console.log들이 겹쳐져 나오는 오류가 생겼다!

    오류

    => 그래서 함수 내부를 빠져나갈 수 있도록 return을 이용하여 endgame()를 나오고 startGame()을 실행할 수 있도록 해줬다!
    => 또한 endgame() 자체도 return으로 만들어 기존의 startGame()함수 내부에 있기에 나올 수 있도록 해줬다.

개선점 분석

  • 여러 시스템들을 구상하는 과정에서 너무 여러 요소를 고려하려 해서 시작할 엄두가 안났던 시간이 있다..
    => 우선순위를 정하여 구현을 마치고, 이후 과정에서 리팩토링/확장 하는 형식으로 작업을 해야겠다!

지식창고

RogueLike 과제

for await of

  • Array의 요소들을 돌아가며 순차적으로 비동기 함수를 동기적으로 실행한다!

  • await은 Promise 함수의 resolve 값을 기다리기 때문에 타이머를 기다리게 된다!

const Array = [1,2,3,4];
for await (const value of Array) {
    //콘솔에 값 로그 올리기
    console.log(value);
    // 비동기 함수인 setTimeout을 for await of과 Promise를 이용하여 타이머기능을 구현했다.
    await new Promise(resolve => setTimeout(resolve, 1000));
}