목차
2025.07.01 - [부트캠프/개인학습] - C# 프로그래밍 기초
C# 프로그래밍 기초
팀원들 기다리거나, 애매하게 남은 시간을 활용하기 위해 예전에 구매해놓고 완강하지 못한 강의를 동시에 진행하려고 한다.https://www.udemy.com/course/the-complete-c-sharp-developer-course/?couponCode=24T4MT30062
j000.tistory.com
이어서 객체 지향 프로그래밍과 관련된 항목들 정리.
OOP (Object-Oriented Programming)
객체 지향 프로그래밍(OOP)는 프로그램을 객체 중심으로 구성하는 개발 패러다임이다.
데이터와 그 데이터를 처리하는 함수를 하나의 단위(객체)로 묶고, 객체들 간의 상호작용을 통해 프로그램이 동작한다.
| 특징 | 설명 |
| 캡슐화 (Encapsulation) | 데이터와 기능을 하나의 단위로 묶고 외부 접근을 제한하여 안정성을 높임 |
| 상속 (Inheritance) | 기존 클래스의 기능을 재사용하고 확장 가능하게 함 |
| 다형성 (Polymorphism) | 동일한 인터페이스가 상황에 따라 다른 동작을 하도록 함 (오버로딩, 오버라이딩 포함) |
| 추상화 (Abstraction) | 핵심 개념만 노출하여 복잡성을 줄임 |
| 객체 (Object) | 클래스에서 생성된 실체로, 상태와 행동을 가지며 상호작용함 |
메서드 (Method)
일련의 코드 블록으로 특정 작업을 수행하는 독립 기능 단위이다.
//메서드 선언 구조
[접근제한자] [리턴타입] [메서드이름]([매개변수])
{
// 실행 코드
}
//사용 예제
public void SayHello()
{
Console.WriteLine("안녕하세요!");
}
public int AddNumbers(int a, int b)
{
return a + b;
}
//호출 예제
SayHello();
int sum = AddNumbers(10, 20);
매개변수와 반환 값
매개변수: 메서드에 입력되는 값 (0개 이상 가능)
반환 값: 메서드가 수행 후 돌려주는 결과 (없으면 void)
void PrintFullName(string first, string last)
{
Console.WriteLine(first + " " + last);
}
int Add(int a, int b)
{
return a + b;
}
PrintFullName("John", "Doe");
int result = Add(3, 5);
메서드 오버로딩 (Method Overloading)
같은 이름으로 매개변수 타입이나 개수가 다른 여러 메서드를 정의할 수 있다.
호출 시 전달 인자에 맞는 메서드를 자동으로 선택한다.
void PrintMessage(string msg)
{
Console.WriteLine("Message: " + msg);
}
void PrintMessage(int num)
{
Console.WriteLine("Number: " + num);
}
재귀 호출 (Recursive Call)
메서드가 자기 자신을 호출하는 것을 말한다.
무한 호출을 방지하기 위한 종료 조건이 반드시 필요하다.
호출 스택에 쌓였다가 종료 시 하나 씩 제거된다.
public static int CalculateFactorial(int n)
{
// 기저 조건: n이 0일 경우 1을 반환
if (n == 0)
{
return 1;
}
// 재귀 호출: n * (n-1)!
else
{
return n * CalculateFactorial(n - 1);
}
}
사용 시 장점
- 중복 코드 제거 및 재 사용성 증가.
- 코드 가독성, 유지 보수성 향상
- 기능 별 코드 분리로 명확한 역할 부여
구조체 (struct)
여러 데이터를 묶어 하나의 사용자 정의 값 형식 타입 생성
멤버로 변수와 메서드를 가질 수 있다.
struct Person
{
public string Name;
public int Age;
public void PrintInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
//사용
Person p = new Person();
p.Name = "John";
p.Age = 25;
p.PrintInfo();
구조체 사용처
구조체는 작고 가벼운 데이터를 표현할 때 사용하며, 값 형식이라 복사 시 독립적인 복제가 이루어진다.
스택에 저장되어 생성과 소멸이 빠르고, 불변 객체로 사용하기에 적합하다.
(클래스의 멤버이거나 배열의 요소일 경우 힙 (Heap)에 저장될 수도 있다.)
단순한 데이터를 다룰 때는 구조체가 클래스보다 성능 상 유리할 수 있다.
- 위치, 좌표, 크기 등 단순한 값 묶음: 메모리 오버헤드 없이 빠르게 처리 가능
- 복사 시 원본 보호가 필요한 경우: 값 복사이므로 원본 데이터에 영향 없음
- 불변 객체처럼 사용할 때: 상태를 변경하지 않고 전달만 하는 경우 유리
- 짧은 생명주기를 가진 객체: 스택에 저장될 경우 GC(Garbage Collector) 부담이 없음.
클래스와 객체
클래스
객체를 생성하기 위한 설계도로, 필드와 메서드를 포함한다.
객체는 클래스를 통해 생성되며, 클래스 없이 객체를 만들 수 없다.
구조체와 달리 클래스는 참조형이며, 상속과 소멸자를 지원한다.
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
구조체 vs 클래스
클래스는 상속과 다양한 기능 구현이 가능하여, 복잡한 객체 모델링에 유리하다.
| 구분 | 클래스 (Class) | 구조체 (struct) |
| 형식 | 참조 형식 | 값 형식 |
| 메모리 위치 | 힙 (Heap) | 스택 (Stack) |
| 상속 | 가능 | 불가능 |
| 사용 목적 | 복잡한 객체 표현 | 단순한 데이터 표현 |
접근 제한자 (Access Modifiers)
접근 제한자는 클래스, 필드, 메서드 등의 접근 범위를 지정하는 키워드로, 객체 지향의 캡슐화를 구현하는 핵심 수단이다.
데이터를 보호하고, 외부에서 불필요하게 접근하지 못하도록 제어할 수 있다.
| public | 모든 클래스에서 접근 가능 |
| private | 같은 클래스 내부에서만 접근 가능 |
| protected | 같은 클래스 및 이를 상속 받은 클래스에서 접근 가능 |
class Person
{
public string Name; // 외부에서 자유롭게 접근 가능
private int Age; // 클래스 내부에서만 접근 가능
protected string Address; // 클래스 내부 + 상속받은 클래스에서 접근 가능
public void SetAge(int value)
{
Age = value; // private 필드에 접근하는 공개 메서드
}
}
필드와 메서드 (Field and Method)
- 필드: 객체의 상태를 저장하는 변수
- 메서드: 객체의 동작을 정의하는 함수
보통 필드는 private, 메서드는 public으로 선언해 정보 은닉과 인터페이스를 분리한다.
class Player
{
private string name;
private int level;
public void Attack()
{
Console.WriteLine("Attack!");
}
}
Player player = new Player();
player.Attack();
생성자와 소멸자 (Constructor and Destructor)
- 생성자: 객체를 초기화할 때 호출됨
- 소멸자: 객체가 메모리에서 해체될 때 GC에 의해 호출됨.
생성자는 클래스 이름과 같으며 반환형이 없고, 소멸자는 ~클래스이름 형태로 작성한다.
class Person
{
private string name;
public Person(string name)
{
this.name = name;
Console.WriteLine("생성자 호출");
}
~Person()
{
Console.WriteLine("소멸자 호출");
}
}
C#에서는 GC에 의해 관리되는 메모리 해체를 담당하므로, 명시적으로 소멸자를 호출하는 것은 일반적으로 권장되지 않는다.
프로퍼티 (Property)
필드의 값을 간접적으로 설정하거나 가져오기 위한 접근자 (Accessor) 메서드의 조합이다.
get과 set 접근자를 통해 필드 접근을 제어하며, 자동 프로퍼티로 간단하게 구현할 수도 있다.
class Person
{
public string Name { get; set; }
public int Age
{
get { return age; }
set
{
if (value >= 0)
age = value;
}
}
private int age;
}
Person person = new Person();
person.Name = "John";
person.Age = 25;
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
//자동 프로퍼티
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
상속과 다형성
상속 (Inheritance)
상속은 기존 클래스 (부모 클래스)를 확장하거나 재사용해 새로운 클래스 (자식 클래스)를 만드는 개념이다.
자식 클래스는 부모 클래스의 필드, 메서드, 프로퍼티 등을 물려 받아 사용할 수 있다.
코드 재사용성 향상, 계층 구조 표현, 유지보수 용이성 등의 장점이 있다.
C#은 단일 상속만 지원하지만, 여러 인터페이스 상속은 가능하다.
상속 시 부모 클래스 멤버의 접근 제한자에 따라 자식 클래스 접근 가능 여부가 결정된다.
// 부모 클래스
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void Eat()
{
Console.WriteLine("Animal is eating.");
}
public void Sleep()
{
Console.WriteLine("Animal is sleeping.");
}
}
// 자식 클래스 (상속받음)
public class Dog : Animal
{
}
public class Cat : Animal
{
}
다중 상속을 사용하지 않는 이유
- Diamond Problem: 부모 클래스에서 동일한 멤버를 상속 받을 때 충돌 발생
- 복잡한 구조: 상속 계층이 복잡해져 유지보수 어려움
- 이름 충돌: 동일한 이름의 멤버 충돌 가능성
- 단일 상속 권장: C#은 단일 상속을 통해 코드 명확성과 일관성 유지
다형성 (Polymorphism)
같은 타입이지만 다양한 방식으로 동작할 수 있는 능력.
가상 메서드(virtual)를 부모 클래스에 선언하고, 자식 클래스에서 재정의(override)할 수 있다.
public class Unit
{
public virtual void Move()
{
Console.WriteLine("두발로 걷기");
}
public void Attack()
{
Console.WriteLine("Unit 공격");
}
}
public class Marine : Unit
{
// 기본 동작 그대로 사용
}
public class Zergling : Unit
{
public override void Move()
{
Console.WriteLine("네발로 걷기");
}
}
//
Marine marine = new Marine();
marine.Move(); // 두발로 걷기
marine.Attack();
Zergling zergling = new Zergling();
zergling.Move(); // 네발로 걷기
zergling.Attack();
List<Unit> units = new List<Unit> { marine, zergling };
foreach (Unit unit in units)
{
unit.Move();
}
추상(Abstract) 클래스와 메서드
추상 클래스는 직접 인스턴스 생성 불가, 상속용으로 만들어진다.
설계도 같은 존재로 추상 메서드를 포함해 일반 메서드나 필드 등 구현된 멤버를 가질 수도 있다.
추상 메소드는 반드시 추상 클래스 안에 존재해야한다.
구현부가 없고, 자식 클래스에서 반드시 구현해야 한다.
abstract class Shape
{
public abstract void Draw();
}
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle");
}
}
class Square : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a square");
}
}
class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a triangle");
}
}
List<Shape> shapes = new List<Shape> { new Circle(), new Square(), new Triangle() };
foreach (Shape shape in shapes)
{
shape.Draw();
}
오버라이딩과 오버로딩
오버라이딩 (Overriding)
부모 클래스의 메서드를 자식 클래스에서 재정의하는 것.
이름, 매개변수, 반환 타입 모두 동일해야 한다.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing a shape.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a rectangle.");
}
}
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
shape1.Draw(); // Drawing a circle.
shape2.Draw(); // Drawing a rectangle.
!! 오버라이드 될 메서드는 virtual 키워드를 사용하여 가상 메서드로 선언되어야 한다.
오버로딩(Overloading)
같은 이름의 메서드를 매개변수 개수/타입을 다르게 여러 개 정의하는 것.
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Add(int a, int b, int c)
{
return a + b + c;
}
}
Calculator calc = new Calculator();
int result1 = calc.Add(2, 3); // 5
int result2 = calc.Add(2, 3, 4); // 9
고급 문법 및 기능
제너릭 (Generic)
제너릭은 클래스나 메서드를 다양한 자료형에 대해 일반화하여 재사용할 수 있게 하는 기능이다.
<T> 형태로 제너릭 타입 매개변수를 선언하며, 실제 자료형은 사용 시점에 지정한다.
제너릭을 사용하면 코드 중복을 줄이고, 타입 안전성을 확보할 수 있다.
//제너릭 클래스 기본 구조 예시
class Stack<T>
{
private T[] elements;
private int top;
public Stack()
{
elements = new T[100];
top = 0;
}
public void Push(T item)
{
elements[top++] = item;
}
public T Pop()
{
return elements[--top];
}
}
//사용 예시
Stack<int> intStack = new Stack<int>();
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
Console.WriteLine(intStack.Pop()); // 출력: 3
//매개변수 두 개 이상 사용
class Pair<T1, T2>
{
public T1 First { get; set; }
public T2 Second { get; set; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
public void Display()
{
Console.WriteLine($"First: {First}, Second: {Second}");
}
}
Pair<int, string> pair1 = new Pair<int, string>(1, "One");
pair1.Display(); // 출력: First: 1, Second: One
Pair<double, bool> pair2 = new Pair<double, bool>(3.14, true);
pair2.Display(); // 출력: First: 3.14, Second: True
out, ref 키워드
out과 ref는 메서드가 여러 값을 반환하거나 호출한 변수 자체를 수정할 수 있도록 도와주는 키워드이다.
둘 다 인자를 참조 형태로 전달하지만, 사용 목적과 동작 방식에 차이가 있다.
값 복사 없이 참조로 전달되므로 성능에 이점이 있지만, 가독성과 유지보수성 측면에서 남용은 피하는 것이 좋다.
out 키워드
메서드에서 결과 값을 외부로 전달할 때 사용한다.
호출 전 초기화할 필요 없다.
메서드 내에서 반드시 값을 할당해야 한다.
void Divide(int a, int b, out int quotient, out int remainder)
{
quotient = a / b;
remainder = a % b;
}
int q, r;
Divide(7, 3, out q, out r);
Console.WriteLine($"{q}, {r}"); // 출력: 2, 1
ref 키워드
호출된 메서드에서 기존 변수의 값을 변경하고자 할 때 사용한다.
호출 전에 반드시 초기화되어 있어야 한다.
메서드 안에서 원본 값을 바꾸므로 원치 않는 변경이 일어날 수 있으니 주의해야 한다.
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 출력: 2, 1
인터페이스 (Interface)
인터페이스를 사용하는 이유
- 재사용성: 여러 클래스가 같은 인터페이스를 구현해 기능 공유
- 다중 상속 가능: 클래스는 여러 인터페이스를 구현할 수 있다.
- 유연한 설계: 느슨한 결합으로 확장성 증가
interface 키워드로 선언하며 클래스가 구현해야 할 멤버(메서드, 프로퍼티 등)만을 정의하고 자체 구현은 하지 않는다.
interface IExample
{
void DoSomething();
}
class MyClass : IExample
{
public void DoSomething()
{
// 구현
}
}
다중 구현도 가능하다.
interface IItemPickable { void PickUp(); }
interface IDroppable { void Drop(); }
class Item : IItemPickable, IDroppable
{
public void PickUp() { }
public void Drop() { }
}
인터페이스 vs 추상 클래스 (Interface vs Abstract class)
| 항목 | 인터페이스 | 추상 클래스 |
| 상속 | 다중 상속 가능 | 단일 상속만 가능 |
| 구현 유무 | 없음 | 일부 구현 가능 |
| 목적 | 행위(기능) 설계 | 공통 동작 + 기능 제공 |
| 사용성 | 모든 멤버를 구현해야 함 | 필요한 부분만 오버라이드 가능 |
열거형 (Enum)
의미 있는 상수 집합을 정의해 코드 가독성을 높일 수 있다.
switch문과 함께 유용하게 사용된다.
enum 키워드로 선언하며 내부 값은 기본적으로 int이고 명시적으로 숫자 지정도 가능하다.
enum Direction
{
Up,
Down,
Left,
Right,
}
Direction d = Direction.Left;
int value = (int)Direction.Up; // 0
//형변환
int intValue = (int)MyEnum.Value1; // 열거형 값을 정수로 변환
MyEnum enumValue = (MyEnum)intValue; // 정수를 열거형으로 변환
// 월 열거형
public enum Month
{
January = 1,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December
}
// 처리하는 함수
static void ProcessMonth(int month)
{
if (month >= (int)Month.January && month <= (int)Month.December)
{
Month selectedMonth = (Month)month;
Console.WriteLine("선택한 월은 {0}입니다.", selectedMonth);
// 월에 따른 처리 로직 추가
}
else
{
Console.WriteLine("올바른 월을 입력해주세요.");
}
}
예외 처리
예외는 프로그램 실행 중 발생하는 예기치 못한 상황으로, 정상 흐름을 방해한다.
try-catch 블록을 사용하여 예외를 잡고 처리하며, finally 블록은 예외 발생 여부와 관계 없이 실행된다.
사용자 정의 예외 클래스를 만들어 특정 상황에 맞는 예외 처리를 구현할 수도 있다.
try
{
int result = 10 / 0; // DivideByZeroException 발생
Console.WriteLine("결과: " + result);
}
catch (DivideByZeroException ex)
{
Console.WriteLine("0으로 나눌 수 없습니다.");
}
catch (Exception ex)
{
Console.WriteLine("예외가 발생했습니다: " + ex.Message);
}
finally
{
Console.WriteLine("finally 블록 실행");
}
// 사용자 정의 예외
public class NegativeNumberException : Exception
{
public NegativeNumberException(string message) : base(message) { }
}
try
{
int number = -10;
if (number < 0)
throw new NegativeNumberException("음수는 처리할 수 없습니다.");
}
catch (NegativeNumberException ex)
{
Console.WriteLine(ex.Message);
}
값형 (Value Type)과 참조형 (Reference Type)
값형은 변수에 데이터를 직접 저장하며, 복사 시 값이 복제된다.
참조형은 변수에 데이터의 주소를 저장하고, 복사 시 참조가 복제되어 같은 데이터를 가리킨다.
값형은 기본 데이터 타입이며, 참조형은 클래스, 배열 등이 해당한다.
// 값형 예제
struct MyStruct
{
public int Value;
}
MyStruct struct1 = new MyStruct();
struct1.Value = 10;
MyStruct struct2 = struct1; // 값 복사
struct2.Value = 20;
Console.WriteLine(struct1.Value); // 10
// 참조형 예제
class MyClass
{
public int Value;
}
MyClass obj1 = new MyClass();
obj1.Value = 10;
MyClass obj2 = obj1; // 참조 복사
obj2.Value = 20;
Console.WriteLine(obj1.Value); // 20
박싱(Boxing)과 언박싱(Unboxing)
박싱은 값형을 참조형(obejct)으로 변환하는 과정이다. (혹은 interface)
언박싱은 박싱된 개체를 다시 값형으로 변환하는 과정이며, 명시적 캐스팅이 필요하다.
빈번한 박싱과 언박싱은 성능 저하를 초래할 수 있어 주의해야 한다.
int num = 10;
object boxed = num; // 박싱
int unboxed = (int)boxed; // 언박싱
Console.WriteLine(num); // 10
Console.WriteLine(unboxed); // 10
델리게이트 (Delegate)
델리게이트는 메서드를 참조하는 타입으로, 메서드를 변수에 할당하거나 매개변수로 전달할 수 있다.
여러 메서드를 등록해 순차적으로 호출할 수도 있다.
delegate int Calculate(int x, int y);
static int Add(int x, int y) => x + y;
Calculate calc = Add;
int result = calc(3, 5);
Console.WriteLine(result); // 8
delegate void MyDelegate(string message);
static void Method1(string msg) => Console.WriteLine("Method1: " + msg);
static void Method2(string msg) => Console.WriteLine("Method2: " + msg);
MyDelegate d = Method1;
d += Method2;
d("Hello"); //Method1, Method2 둘다 호출됨
델리게이트는 이벤트 처리와 콜백 구현에 많이 쓰인다.
다른 프로그래밍 언어에서는 함수 포인터라는 용어를 사용하기도 함.
예를 들어, 적의 공격 이벤트에 플레이어 데미지 처리 메서드를 연결해, 공격 시 자동으로 데미지를 처리하도록 만들 수도 있다.
람다 (Lambda)
람다는 이름 없는 메서드를 만들 수 있는 표현식으로, 델리게이트에 해당하거나 매개변수로 전달 가능하다.
Calculate calc = (x, y) => x + y;
Console.WriteLine(calc(4, 6)); // 10
MyDelegate d = message => Console.WriteLine("람다 메시지: " + message);
d("Hi");
//여러줄의 코드가 필요할때
Calculate calc2 = (x, y) =>
{
return x + y;
};
람다는 코드 간결성을 높여주며 익명 메서드로서 유용하다.
Func와 Action
델리게이트를 대체하는 미리 정의된 제너릭 형식들이다.
Func은 반환 값이 있는 메서드를, Action은 반환 값이 없는 메서드를 표현한다.
Func는 마지막 타입 매개변수가 반환 타입이며, Action은 반환 타입이 없다.
Func<int, int, int> addFunc = (x, y) => x + y;
Console.WriteLine(addFunc(3, 7)); // 10
Action<string> print = msg => Console.WriteLine(msg);
print("Hello Func and Action!");
Func와 Action으로 델리게이트 선언을 줄이고 가독성을 향상 시킬 수 있다.
LINQ
LINQ는 통합 쿼리 언어로, 컬렉션 등 데이터 소스를 대상으로 선언적 쿼리를 지원하며, 필터링, 정렬, 그룹화 등을 쉽게 수행할 수 있다.
객체, 데이터베이스, XML 문서 등 다양한 데이터 소스를 지원한다.
List<int> numbers = new() {1, 2, 3, 4, 5};
var evens = from num in numbers
where num % 2 == 0
select num;
foreach(var n in evens)
Console.WriteLine(n); // 2 4
데이터 처리에 매우 강력하며 코드 양도 줄여준다.
Nullable 형식
Nullable은 값형 변수에 null을 할당할 수 있게 해주는 구조이다.
int? doube? 처럼 ?를 붙여서 사용한다.
null 여부는 .HasValue로 검사하고, 값 접근은 .Value로 한다.
int? score = null;
if (score.HasValue)
Console.WriteLine(score.Value);
else
Console.WriteLine("값이 없습니다.");
int result = score ?? 0; // null이면 기본값 0 사용 null이 아니면 score 사용
기본적으로 int, bool, double 같은 값형은 null을 가질 수 없지만, Nullable<T> 혹은 T?로 선언하면 가능하다.
StringBuilder
StringBuilder는 문자열을 자주 수정할 때 효율적인 클래스.
문자열 추가, 삽입, 삭제 등이 빠르게 동작하며 최종 결과는 .ToString()으로 출력한다.
내부 버퍼를 사용해 새로운 문자열을 계속 생성하지 않기 때문에, 성능적으로 일반 문자열보다 유리하다.
StringBuilder sb = new StringBuilder();
sb.Append("Hello").Append(" World");
sb.Insert(5, ",");
sb.Replace("World", "C#");
sb.Remove(5, 1);
Console.WriteLine(sb.ToString()); // Hello C#'개인 프로젝트 > 개인학습' 카테고리의 다른 글
| 유니티 입문 (2D) - TopDown (0) | 2025.07.23 |
|---|---|
| 유니티 입문 (3D) - The Stack (0) | 2025.07.22 |
| 유니티 입문 (2D) - Flappy Plane (0) | 2025.07.21 |
| 알고리즘 (0) | 2025.07.08 |
| C# 프로그래밍 기초 (0) | 2025.07.01 |