Unity 본캠프 8일차 - C# / TRPG 제작

한예준·2025년 4월 16일

오늘도 어제에 이어서 C# 강의를 수강하고 TRPG 프로젝트를 마저 제작하도록 하겠다. 오늘 강의의 범위는 C#에서 잘 드러나는 객체지향 프로그래밍적인 문법이라고 정리할 수 있겠다. 또, 이후 시간에는 TRPG 프로젝트의 필수 구현 기능들을 구현해보고, 가능하다면 추가 구현 기능까지 건드려 볼 생각이다.


클래스와 객체

객체지향 프로그래밍의 특징

  • 캡슐화
    • 관련된 데이터와 기능을 하나의 단위로 묶는 것이다.
    • 클래스를 사용하여 데이터와 메서드를 캡슐화하여 정보를 은닉하고 직접적인 접근을 제한하여 안정성과 유지 보수성을 높인다.
  • 상속
    • 기존의 클래스를 확장하여 새로운 클래스를 만드는 메커니즘이다.
    • 부모 클래스(상위 클래스)의 특성과 동작을 자식 클래스(하위 클래스)가 상속받아 재사용할 수 있다.
    • 코드의 중복을 줄이고, 클래스 간 계층 구조를 구성하여 코드의 구조화와 유지보수를 용이하게 한다.
  • 다형성
    • 하나의 인터페이스나 기능을 다양한 방식으로 구현하거나 사용할 수 있는 능력을 의미한다.
    • 하나의 메서드 이름이 다양한 객체에서 다르게 동작할 수 있게 하는 것으로, 오버로딩/오버라이딩을 통해 구현된다.
    • 유연하고 확장 가능한 코드 작성을 가능하게 하고, 코드의 가독성과 재사용성을 높인다.
  • 추상화
    • 복잡한 시스템이나 개념을 단순화하여 필요한 기능에 집중하는 것을 의미한다.
    • 세부 구현 내용을 감추고 핵심 개념에 집중하여 코드의 이해와 유지보수를 용이하게 한다.
  • 객체
    • 클래스로부터 생성된 실체, 상태와 행동을 가지며, 실제 개체나 개념을 모델링한다.
    • 각 객체는 클래스로부터 생성되어 고유한 데이터를 가지고 있다.

클래스

객체를 생성하기 위한 템플릿, 속성과 동작을 가진다.

  • 필드
    • 클래스에서 사용되는 변수, 객체의 상태를 나타낸다.
  • 메서드
    • 클래스에서 수행되는 동작, 객체의 동작을 구현한다.
  • 생성자
    • 객체를 초기화한다. 객체가 생성될 때 자동으로 호출된다.
  • 소멸자
    • 객체가 소멸될 때 호출된다. 메모리나 리소스의 해제 등의 작업을 수행한다.

클래스 선언과 인스턴스

class Person
{
    public string Name;
    public int Age;

    public void PrintInfo()
    {
        Console.WriteLine("Name: " + Name);
        Console.WriteLine("Age: " + Age);
    }
}

Person p = new Person();
p.Name = "John";
p.Age = 30;
p.PrintInfo(); // 출력: Name: John, Age: 30

구조체와 클래스

  • 모두 사용자 정의 형식을 만드는 데 사용된다.
  • 구조체는 값 형식, 클래스는 참조 형식이다.
  • 구조체는 상속을 받을 수 없지만 클래스는 상속이 가능하다.
  • 클래스가 더 복잡한 객체를 표현하고 다양한 기능을 제공하기 위해 사용된다.

접근 제한자

접근제한자란?

클래스, 필드, 메서드 등의 접근 가능한 범위를 지정하는 키워드이다. 클래스의 캡슐화를 제어한다.

  • public: 외부에서 자유롭게 접근이 가능하다.
  • private: 같은 클래스 내부에서만 접근 가능하다.
  • protected: 같은 클래스 내부와 상속받은 클래스에서만 접근 가능하다.

필드와 메서드

  • 클래스는 필드와 메서드로 구성된다.
  • 필드는 클래스 내부에 선언되어 있는 변수, 클래스의 상태를 나타낸다.
  • 메서드는 클래스 내부에 선언되어 있는 함수, 클래스의 동작을 정의하고 실행한다.
  • 필드, 메서드 및 호출 예시
class Player
{
    // 필드
    private string name;
    private int level;

    // 메서드
    public void Attack()
    {
        // 공격 동작 구현
    }
}

Player player = new Player();  // Player 클래스의 인스턴스 생성
player.Attack();  // Attack 메서드 호출

즉, 클래스를 통해 정의한 객체는 생성 시 필드 의 속성을 가지고, 이후 메서드를 호출하여 통해 특정 작업을 수행한다.


생성자와 소멸자

생성자

  • 생성자란?
    • 객체가 생성될 때 호출되는 특별한 메서드.
    • 클래스의 인스턴스를 초기화하고, 필요한 초기값을 설정하는 역할을 수행.
    • 클래스와 동일한 이름을 가지며, 반환 타입이 없다.
    • new 키워드와 함께 호출
      예시) Player player = new Player();
  • 생성자의 특징
    • 객체를 초기화하는 과정에서 필요한 작업 수행

소멸자

  • 소멸자란?
    • 객체가 소멸되는 시점에서 호출되는 특별한 메서드.
    • 객체의 사용이 종료되고 메모리에서 해제될 때 자동으로 호출되어 필요한 정리작업을 수행한다.
    • 클래스와 동일한 이름을 가지며, 이름 앞에 ~기호를 붙여 표현한다.
    • 명시적으로 소멸자를 호출하는 것은 권장되지 않는다.
  • 소멸자의 역할
    • 자원 해제, 메모리 해제, 로깅 및 디버깅

프로퍼티

  • 프로퍼티란?
    • 클래스의 멤버로서, 객체의 필드 값을 읽거나 설정하는 데에 사용되는 접근자 메서드의 조합.
    • 객체의 필드값은 은닉화되어있기 때문에 직접 접근하지 않고, 간접적으로 값을 설정하거나 읽을 수 있도록 하는 중간매개역할을 한다.
  • 프로퍼티 구문
    • getset 접근자를 사용하여 값을 읽고 설정하는 동작을 정의한다.
    • get 접근자는 프로퍼티의 값을 반환하는 메서드로, 외부에서 읽을 때 호출된다.
    • set 접근자는 프로퍼티의 값을 설정하는 메서드로, 외부에서 값을 대입할 때 호출된다. value라는 암묵적 매개변수를 사용하여 전달된 값을 처리한다.
public class Player
{
    // 내부 저장용 필드
    private int score;

    // 프로퍼티 정의: 외부에서 score 값을 읽거나 쓸 수 있도록 제공
    public int Score
    {
        get { return score; }
        set { score = value; }
    }
}
// `score`라는 필드는 직접 접근하지 않고, `score`라는 프로퍼티를 통해서만 접근할 수 있다.

프로퍼티가 명확하게 이해가 되지 않아서, 조금 더 추가적인 내용을 찾아보았다.

  • 프로퍼티의 사용방법

    • 자동으로 구현되는 프로퍼티
      private제한자를 사용한 변수를 생성하지 않고 프로퍼티 자체를 사용한다.
      조건 없이 단순히 변수 값을 할당하거나 반환한다.
      get,set 접근자 안에 내용이 없으면 프로퍼티 그 자체를 사용할 수 있다.
      다음은 사용 예시이다.
    public class Player
    {
      // 자동 구현 프로퍼티를 사용, 외부에서 값을 읽고 수정도 가능
      public string Name { get; set; }
      public int Level { get; set; }
      
      // 읽기 전용 자동 구현 프로퍼티: 외부에서는 값을 읽기만 가능, 변경 불가능.
      public int Health { get; private set; }
    
      public Player(string name, int level, int health)
      {
          Name = name;
          Level = level;
          Health = health;
      }
    }
    • Player 클래스의 Name과 Level 프로퍼티는 public get, set 접근자를 가지고 있기 때문에, 동일한 스크립트 내에서 다른 클래스나 다른 스크립트에서도 자유롭게 읽고 쓸 수 있다.
    • Health 프로퍼티는 public get 과 private set을 가지고 있으므로, 외부(다른 스크립트나 클래스)에서는 이 값을 읽을 수만 있을 뿐, 값을 설정하거나 변경할 수는 없다.
    • 즉, Health는 외부에서는 읽기 전용으로만 접근할 수 있어서, 값 변경은 player 클래스 내부의 메서드나 생성자 내에서만 이루어지도록 보호된다.

오버라이딩과 오버로딩

오버라이딩(Overriding)

  • 부모 클래스에 정의된 메서드를 자식 클래스에서 재정의하는 기법이다.
  • 상속 관계 내에서 발생하며, 메서드 이름, 매개변수, 반환타입이 부모 클래스와 동일해야 한다.
  • 이를 통해 자식 클래스는 부모 클래스의 기본 동작을 자신만의 방식으로 변경할 수 있다.

오버로딩(Overloading)

  • 같은 클래스 내에서 동일한 메서드 이름을 사용하되, 매개변수의 개수나 타입, 순서가 다른 여러 메서드를 정의하는 기법이다.
  • 동일한 기능을 다양한 입력 방식으로 호출할 수 있게 하여, 코드의 가독성과 유연성을 향상시킨다.

제너릭

제너릭이란?

  • 데이터 형식에 독립적인 코드를 작성할 수 있도록 해주는 기능.
  • 즉, 특정 타입에 한정하지 않고 여러 타입에 대해 재사용 가능한 클래스, 메서드, 인터페이스 등을 작성할 수 있게 해준다.
  • 장점:
    • 타입 안전성: 컴파일 타임에 타입을 체크하여 잘못된 형 변환을 방지
    • 코드 재사용성: 동일한 로직을 다양한 데이터 타입에 대해 사용할 수 있으므로 중복 코드를 줄일 수 있다.
    • 성능: 박싱/언박싱 없이 타입에 맞게 동작하기 때문에 성능이 향상된다.

제너릭 사용법

  • 제너릭은 클래스나 메서드 선언 시 타입 매개변수를 지정하여 사용할 수 있다.
    • 제너릭 클래스:
      클래스 이름 뒤에 각괄호(<>)를 사용하여 타입 매개변수를 지정한다. 이 매개변수는 해당 클래스 내의 필드, 프로퍼티, 메서드에 사용되어 인스턴스 생성 시 구체적인 타입으로 대체된다.
    • 제너릭 메서드:
      메서드 선언 시 메서드 이름 앞이나 반환형 뒤에 타입 매개변수를 지정할 수 있으며, 메서드 내부에서 해당 타입에 맞는 연산이나 처리를 수행한다.

out, ref

out 키워드

  • 메서드에서 여러 결과 값을 반환하기 위해 매개변수를 사용하도록 하는 키워드
//예시 나누기 코드
using System;

public class Example
{
    // out 사용 예: 나누기 연산 후 몫과 나머지를 반환하는 메서드
    public static void Divide(int dividend, int divisor, out int quotient, out int remainder)
    {
        quotient = dividend / divisor;
        remainder = dividend % divisor;
    }

    public static void Main()
    {
        // out 예시 사용
        int dividend = 17;
        int divisor = 5;
        // out 변수는 초기화하지 않아도 됨
        int quotient, remainder;
        Divide(dividend, divisor, out quotient, out remainder);  // 메서드 내부에서 값을 할당
        Console.WriteLine($"{dividend} 나누기 {divisor} 의 몫은 {quotient}이고, 나머지는 {remainder}입니다");
    }
}
  • Divide 메서드
    Divide메서드는 나눗셈을 수행한 후, out 키워드를 사용하여 몫(quotient)과 나머지(remainder)를 반환한다.
  • out의 특징: 호출할 때 해당 변수들은 초기화할 필요가 없으며, 메서드 내부에서 반드시 값을 할당해야한다.
  • out 키워드를 사용하는 이유
    • 메서드 내부에서 여러 결과를 동시에 외부로 전달할 수 있다.
    • 별도의 변수 반환구조를 만들지 않아도 된다.
    • 복잡한 반환형을 정의하지 않고 간단하게 기존의 변수들을 연결하여 값을 전달할 수 있다.

    ref 키워드

  • 메서드에 인자로 전달된 변수롤 참조로 넘겨, 메서드 내부에서 변수의 값을 수정하면 호출한 곳의 변수 값에도 그 수정사항이 반영되게 한다.
public class Example
{
    // ref 사용 예: 두 변수의 값을 교환하는 메서드
    public static void Swap(ref int a, ref int b)
    {
        int temp = a;
        a = b;
        b = temp;
    }

    public static void Main()
    {
        // ref 예시 사용
        int x = 5; // 반드시 초기화되어 있어야 함
        int y = 10; // 반드시 초기화되어 있어야 함
        Console.WriteLine("Before Swap: x = " + x + ", y = " + y);
        Swap(ref x, ref y);   // ref 키워드로 x와 y 전달
        Console.WriteLine("After Swap: x = " + x + ", y = " + y);
    }
}
  • Swap 메서드
    Swap 메서드는 두 변수 abref키워드를 사용하여 인자로 받음.
    이 메서드는 전달받은 변수들의 값을 교환한다.
  • ref의 특징
    • 호출할 때 전달하는 변수는 반드시 미리 초기화되어 있어야 하며, 메서드 내부에서 수정하면 호출 후에도 그 변경이 반영된다.
    • 메서드 내에서 전달받은 변수의 값을 직접 변경함으로써, 별도의 반환값을 처리하지 않고도 외부 변수에 영향을 줄 수 있다.
    • 큰 데이터를 구조체 등으로 다룰 때, ref를 사용하면 값 복사를 피할 수 있어 성능 면에서 이점이 있다.
    • 어떤 메서드는 외부 변수를 인자로 받아, 그 변수의 상태를 조건에 따라 변경해야 하는 경우가 있는데, 이런 경우 ref를 사용하면, 메서드가 반환값으로 새 값을 전달하는 대신, 전달받은 변수를 직접 수정하여 호출한 쪽의 변수 상태를 업데이트할 수 있다.

두 키워드 모두 메서드 내부의 작업 결과를 외부 변수에 직접 효율적으로 반영하게 해준다. 즉, 메서드 내부에서의 변경이 외부에 그대로 적용된다.


TRPG 제작 (이어서)

필수 구현 기능 중 먼저, 보유중인 아이템을 전부 보여주고, 장착중인 아이템 앞에는 [E]표시를 붙여주는 기능을 만들어 줄 생각이다.

Item 관리 및 장착 기능

  • 먼저 Item.cs를 만들어서 아이템을 관리해보겠다.
  • Item은 이름, 공격력, 방어력, 설명, 장착여부를 표시하는 하나의 클래스로 묶어서 관리하겠다.
public class Item
{
    public string Name;
    public int Atk;
    public int Def;
    public string Description;
    public bool IsEquipped;

    public Item(string name, int atk, int def, string description, bool isEquipped = false)
    {
        Name = name;
        Atk = atk;
        Def = def;
        Description = description;
        IsEquipped = isEquipped;
    }
    //예시 아이템
    public static Item IronArmor = new Item("무쇠갑옷", 0, 5, "무쇠로 만들어져 튼튼한 갑옷입니다.");

    public static Item SpartanSpear = new Item("스파르타의 창", 7, 0, "스파르타의 전사들이 사용했다는 전설의 창입니다.");

    public static Item OldSword = new Item("낡은 검", 2, 0, "쉽게 볼 수 있는 낡은 검입니다.");
}
  • 이렇게 만들어진 아이템들을 리스트로 관리하여 isEquipped 불리언 변수를 통해 장착 여부를 판별하겠다.
    Inventory.cs
public static class Inventory
{
    public static List<Item> Items = new List<Item>();
    public static void Show()
    {
.
.
.
            if (Items.Count == 0)
            {
                Console.WriteLine("아이템이 없습니다.\n");
            }
            else
            {
                foreach (var item in Items)
                {
                    string Equip = item.IsEquipped ? "[E]" : "   ";
                    Console.WriteLine($"- {Equip}{item.Name} | + {item.Atk} | + {item.Def} | {item.Description}");
                }
  • foreach 반복문을 이용해 List Items를 Inventory 화면에 띄운다.
  • 만약 isEquippedtrue[E], false면 공백을 표시하는 삼항 연산자 ?를 사용하여 장착 여부를 표현한다.

이후 Inventory.cs 안에 장착 관리를 담당하는EquipManager() 메서드를 작성하였다.

    private static void EquipManager()
    {
        while (true)
        {
            Console.Clear();
            Console.WriteLine("인벤토리 - 장착 관리");
            Console.WriteLine("보유 중인 아이템을 관리할 수 있습니다.\n");
            Console.WriteLine("[아이템 목록]");

            if (Items.Count == 0)
            {
                Console.WriteLine("아이템이 없습니다.\n");
            }
            else
            {
                for (int i = 0; i < Items.Count; i++)
                {
                    var item = Items[i];
                    string Equip = item.IsEquipped ? "[E]" : "   ";
                    Console.WriteLine($"{i + 1}. {Equip}{item.Name} | 공격력 + {item.Atk} | 방어력 + {item.Def} | {item.Description}");
                }
            }
            Console.WriteLine("\n0. 나가기");
            Console.WriteLine("\n장착할 아이템의 번호를 입력해주세요.");
            Console.Write(">> ");
            string input = Console.ReadLine();
            if (input == "0")
            {
                break;
            }
            if (int.TryParse(input, out int selection))
            {
                int index = selection - 1;
 
                if (index >= 0 && index < Items.Count)
                {
                    var item = Items[index];
                    if (item.IsEquipped == true)
                    {
                        item.IsEquipped = false;
                        Console.WriteLine($"{item.Name}의 장착을 해제했습니다.");
                    }
                    else
                    {
                        item.IsEquipped = true;
                        Console.WriteLine($"{item.Name}을 장착했습니다.");
                    }
                }
                else
                {
                    Console.WriteLine("잘못된 입력입니다. 다시 시도해주세요.");
                }
            }
            else
            {
                Console.WriteLine("잘못된 입력입니다. 숫자를 입력해주세요.");
            }
        }
    }
}
  • 코드 설명

  •              if (Items.Count == 0)
               {
                   Console.WriteLine("아이템이 없습니다.\n");
               }
               else
               {
                   for (int i = 0; i < Items.Count; i++)
                   {
                       var item = Items[i];
                       string Equip = item.IsEquipped ? "[E]" : "   ";
                       Console.WriteLine($"{i + 1}. {Equip}{item.Name} | 공격력 + {item.Atk} | 방어력 + {item.Def} | {item.Description}");
                   }
               }
    • 가져온 아이템 리스트의 숫자를 세서 0이면 "아이템이 없습니다" 텍스트를 출력한다.
    • 아이템이 1개 이상이면 아이템 리스트에서 0부터 아이템을 가져와서 아이템의 정보를 출력한다.
  •             if (int.TryParse(input, out int selection))
              {
                  int index = selection - 1;
    
                  if (index >= 0 && index < Items.Count)
                  {
                      var item = Items[index];
                      if (item.IsEquipped == true)
                      {
                          item.IsEquipped = false;
                          Console.WriteLine($"{item.Name}의 장착을 해제했습니다.");
                      }
                      else
                      {
                          item.IsEquipped = true;
                          Console.WriteLine($"{item.Name}을 장착했습니다.");
                      }
                  }
    • 플레이어한테 입력을 받고 input, out int selection int index = selection - 1 를 통해 input을 정수로 변환하고, 그 정수(-1)에 맞는 아이템 목록을 장착하거나 해제한다.

아이템에 따른 능력치 변화

  • 장착 아이템에 따른 능력치 변화를 구현해보겠다.
    • 먼저 플레이어 스크립트로 IsEquippedtrue인 아이템을 가져온다.
    • 만약 아이템의 isEquippedtrue인 상태라면, 플레이어의 기본 공격력에 아이템의 공격력을 더해 totalAtk으로 정의한다.
    • 방어력도 마찬가지로 totalDef로 정의한다.
    • 이를 토대로 코드로 구현해보겠다.
    public static int totalAtk()
    {
        int totalAtk = Atk;
        foreach (var item in Inventory.Items)
        {
            if (item.IsEquipped)
                totalAtk += item.Atk;
        }
        return totalAtk;
    }
    public static int totalDef()
    {
        int totalDef = Def;
        foreach (var item in Inventory.Items)
        {
            if (item.IsEquipped)
                totalDef += item.Def;
        }
        return totalDef;
    }
}
  • 이를 상태창에 표시한다.
            Console.WriteLine($"공격력 : {Player.totalAtk}");
            Console.WriteLine($"방어력 : {Player.totalDef}");

  • 변수를 불러오는 실수를 했다. 메서드를 불러오는 것으로 수정했더니 제대로 표시되었다.


상점 기능 구현

  • 상점 기능을 구현해보겠다.
    • 먼저 Item.cs에서 아이템의 가격 정보를 정의해주겠다.
  public class Item
{
    public string Name;
    public int Atk;
    public int Def;
    public string Description;
    public int Price;
    public bool IsEquipped;

    public Item(string name, int atk, int def, string description, int price, bool isEquipped = false)
    {
        Name = name;
        Atk = atk;
        Def = def;
        Description = description;
        Price = price;
        IsEquipped = isEquipped;
    }
    public static Item TraineeArmor = new Item("수련자 갑옷", 0, 5, "수련에 도움을 주는 갑옷입니다.", 1000);
    public static Item IronArmor = new Item("무쇠갑옷", 0, 9, "무쇠로 만들어져 튼튼한 갑옷입니다.", 2000);
.
.
.
}
  • 다음으로 Shop.cs로 아이템 목록을 가져오겠다.
    static List<Item> StoreItems = new List<Item>()
    {
        Item.TraineeArmor,
        Item.IronArmor,
        Item.SpartanArmor,
        Item.OldSword,
        Item.BronzeAxe,
        Item.SpartanSpear
    };
  • 다음으로 아이템 구매 창, 판매 창으로 이동하기 위한 기반을 만들겠다.
    public static void Show()
    {
        while (true)
        {
            Console.Clear();
            Console.WriteLine("상점");
            Console.WriteLine($"보유 골드: {Player.Gold} G\n");
            Console.WriteLine("1. 아이템 구매");
            Console.WriteLine("2. 아이템 판매");
            Console.WriteLine("0. 나가기");
            Console.WriteLine("\n원하시는 행동을 입력해주세요.");
            Console.Write(">> ");

            string input = Console.ReadLine();
            if (input == "0") break;
            else if (input == "1") Buy();
            else if (input == "2") Sell();
            else
            {
                Console.WriteLine("\n잘못된 입력입니다. 다시 시도해주세요.");
                Console.ReadKey();
            }
            Console.Clear();
        }
    }
  • 다음으로 아이템 구매창과 판매창을 구현했다.
    private static void Buy()
    {
        Console.Clear();
        Console.WriteLine("상점 - 아이템 구매");
        Console.WriteLine("구매할 아이템을 선택해주세요.\n");
        Console.WriteLine("[보유골드]");
        Console.WriteLine(Player.Gold);
        Console.WriteLine("\n[아이템 목록]");
        if (StoreItems.Count == 0)
        {
            Console.WriteLine("아이템이 없습니다.\n");
        }
        else
        {
            foreach (var item in StoreItems)
            {
                Console.WriteLine($"- {item.Name} | + {item.Atk} | + {item.Def} | {item.Description}");
            }
            Console.WriteLine();
        }
    }
    private static void Sell()
    {
        Console.Clear();
        Console.WriteLine("상점 - 아이템 판매");
        Console.WriteLine("판매할 아이템을 선택해주세요.\n");
        Console.WriteLine("[보유골드]");
        Console.WriteLine(Player.Gold);
        Console.WriteLine("\n[아이템 목록]");
        if (Inventory.Items.Count == 0)
        {
            Console.WriteLine("아이템이 없습니다.\n");
        }
        else
        {
            foreach (var item in Inventory.Items)
            {
                Console.WriteLine($"- {item.Name} | + {item.Atk} | + {item.Def} | {item.Description}");
            }
            Console.WriteLine();
        }
    }
}

오늘은 여기까지 구현해보겠다. 내일은 남은 상점 기능들을 구현해보고 추가 기능을 구현해보겠다.


회고 및 느낀점

  • 오늘은 어제에 이어 C# 강의를 수강하며, 객체 지향 프로그래밍의 핵심적인 개념들을 학습했다.
  • TRPG 프로젝트에서는 장착 기능 구현과 능력치 연동, 상점의 아이템 리스트 출력까지 비교적 수월하게 진행되었다.
  • 하지만 아무리 C# 문법을 배워도 그 내용을 실제 프로젝트에 자연스럽게 녹여내는 것은 결코 쉽지 않았다. 강의에서 배운 예제는 구조가 단순한 반면, 직접 만드는 프로젝트는 더 많은 고려사항이 필요한 것 같다.
  • 오늘 구현한 코드에서도 get, setout, ref 등의 키워드를 조금 더 잘 활용할 수 있는 방법이 있을텐데 하는 생각이 들어 아쉬웠다..
  • 내일 필수 구현 기능들을 다 구현해본 후, 오늘 배운 객체지향 문법들에 맞춰서 스크립트를 수정하는 데에 더 집중해보려고 한다.

0개의 댓글