백엔드 개발자 블로그

C# 기초 문법 본문

ETC

C# 기초 문법

backend-dev 2023. 8. 23. 20:19

C# 프로그래밍 언어

C# 이란?

오픈 소스

크로스 플랫폼

객체 지향 프로그래밍 언어

막대한 커뮤니티

모든 플랫폼(Desktop, Web, Cloud, Mobile, Games, IoT, AI) 개발 가능

빠른 응답 시간, 에너지 효율적

 

C# 소스코드

확장자 .cs, 별도의 헤더 파일 없음

 

IDE

Visual Studio 사용

www.visualstudio.com 설치

 

Download Visual Studio Tools - Install Free for Windows, Mac, Linux

Download Visual Studio IDE or VS Code for free. Try out Visual Studio Professional or Enterprise editions on Windows, Mac.

visualstudio.microsoft.com

 

C# 버전

https://www.csharpstudy.com/CSharp/CSharp-version.aspx

 

C# 버전 - C# 프로그래밍 배우기 (Learn C# Programming)

C# 버전 C#은 .NET Framework 버전 및 Visual Studio 버전과 밀접한 관련이 있으며, 다음 도표에서 각 버젼별 연관성을 살펴 볼 수 있다. .NET 버전 C# 버전 Visual Studio .NET 1.0 C# 1.0 Visual Studio .NET .NET 1.1 C# 1.1 Vi

www.csharpstudy.com


데이터 타입

C# 데이타 타입 .NET 데이타 타입 설명
bool System.Boolean True or False
byte System.Byte 8비트 unsigned integer
sbyte System.SByte 8비트 signed integer
short System.Int16 16비트 signed integer
int System.Int32 32비트 signed integer
long System.Int64 64비트 signed integer
ushort System.UInt16 16비트 unsigned integer
uint System.UInt32 32비트 unsigned integer
ulong System.UInt64 64비트 unsigned integer
float System.Single 32비트 single precision 부동소수점 숫자
double System.Double 64비트 double precision 부동소수점 숫자
decimal System.Decimal 128비트 Decimal
char System.Char 16비트 유니코드 문자
string System.String 유니코드 문자열
  System.DateTime 날짜와 시간, 별도의 C# 키워드가 없음
object System.Object 모든 타입의 기본 클래스로 모든 유형을 포함할 수 있음

 

타입 지정 접미어(Suffix)

C# 리터럴 데이타 타입 Suffix (대소문자 모두 가능) 예제
long L 1024L
uint U 1024U
ulong UL 1024UL
float F 10.24F
double D 10.24D 또는 10.24
decimal M 10.24M

예제

// Bool
bool b = true;

// Numeric
short sh = -32768;   
int i = 2147483647;  
long l = 1234L;      // L suffix
float f = 123.45F;   // F suffix
double d1 = 123.45; 
double d2 = 123.45D; // D suffix
decimal d = 123.45M; // M suffix

// Char/String
char c = 'A';
string s = "Hello";

// DateTime  2011-10-30 12:35
DateTime dt = new DateTime(2011, 10, 30, 12, 35, 0);

 

최댓값, 최솟값

int i = int.MaxValue;
float f = float.MinValue;

 

NULL

어떤 변수가 메모리 상에 어떤 데이타도 가지고 있지 않다는 의미

Reference 타입만 가능

 

Nullable Type

C# 2.0에서부터 Value 타입들에 NULL을 가질 수 있게 함

예제

// Nullable 타입
int? i = null;
i = 101;
            
bool? b = null;

//int? 를 int로 할당
Nullable<int> j = null;
j = 10;
int k = j.Value;

변수와 상수

변수

필드 변수(전역 변수) : 클래스의 객체가 살아있는 한 계속 존속하며 또한 다른 메서드들에서 필드를 참조할 수 있다

로컬 변수 : 해당 메서드내에서만 사용되며, 메서드 호출이 끝나면 소멸된다.

예제

using System;

namespace ConsoleApplication1
{
    class CSVar
    {
        //필드 (클래스 내에서 공통적으로 사용되는 전역 변수)
        int globalVar;
        const int MAX = 1024;

        public void Method1()
        {
            // 로컬변수
            int localVar;

            // 아래 할당이 없으면 에러 발생
            localVar = 100;
        }
    }
}

 

상수

초기에 정한 값을 중간에 변경할 수 없다.

const를 사용하여 정의

const int MAX_VALUE = 1024;

배열

일련의 동일한 데이타 타입 요소들로 구성된 데이타 집합

첫번째 요소가 인덱스 0

[,] 와 같이 괄호안에 콤마로 분리하여 다차원을 표현

예제

// 1차 배열
string[] players = new string[10];
string[] Regions = { "서울", "경기", "부산" };

// 2차 배열 선언 및 초기화
string[,] Depts = {{"김과장", "경리부"},{"이과장", "총무부"}};

// 3차 배열 선언
string[,,] Cubes;

 

가변 배열

 [][] 와 같이 각 차원마다 괄호를 별도로 사용

고정된 크기를 사용하면 메모리의 낭비가 심한 경우에 사용

예제

//Jagged Array (가변 배열)
//1차 배열 크기(3)는 명시해야
int[][] A = new int[3][];

//각 1차 배열 요소당 서로 다른 크기의 배열 할당 가능
A[0] = new int[2];
A[1] = new int[3] { 1, 2, 3 };
A[2] = new int[4] { 1, 2, 3, 4 };

A[0][0] = 1;
A[0][1] = 2;

 

사용 

배열명[idx]

 

전달 

보내는 쪽에서는 배열명을 사용하고, 받는 쪽에서 동일한 배열타입의 배열을 받아들이면 된다.


문자열

Immutable 즉 한번 문자열이 설정되면, 다시 변경할 수 없다.

새로운 string 객체를 생성하여 데이타로 초기화 한 후 이를 변수에 할당

// 문자열(string) 변수
string s1 = "C#";

// 문자(char) 변수 
char c1 = 'A';

// str -> char array
str.ToCharArray();

// 문자 연산
char c1 = 'A';
char c2 = (char)(c1 + 3);	// D

 

StringBuilder 클래스

메모리를 생성,소멸하지 않고 일정한 버퍼를 갖고 문자열 갱신을 효율적으로 처리


Enum(열거형)

상수 숫자들을 보다 의미있는 단어들로 표현할 수 있어서 프로그램을 읽기 쉽게 해준다.

enum의 각 요소는 별도의 지정없이는 첫번째 요소가 0, 두번째 요소가 1, 세번째 요소가 2 등과 같이 1씩 증가된 값들을 할당받는다. 물론, 개발자가 임의로 의미있는 번호를 지정해 줄 수도 있다.

enum City
{
    Seoul,   // 0
    Daejun,  // 1
    Busan = 5,  // 5
    Jeju = 10   // 10
 }

 

flag enum

 enum 타입이 비트 필드를 갖는다는 것을 표시하기 위해 enum 선언문 바로 위에 [Flags] 라는 Attribute (주: Type 혹은 그 멤버를 선언할 때 그 위에 붙이는 특별한 특성값으로 해당 타입 혹은 멤버가 어떤 특성을 갖고 있는지 나타내게 된다)를 지정할 수 있다.

예제

[Flags]
enum Border
{
    None = 0,
    Top = 1,
    Right = 2,
    Bottom = 4,
    Left = 8
}

static void Main(string[] args)
{
    // OR 연산자로 다중 플래그 할당
    Border b = Border.Top | Border.Bottom;

    // & 연산자로 플래그 체크
    if ((b & Border.Top) != 0)
    {
        //HasFlag()이용 플래그 체크
        if (b.HasFlag(Border.Bottom))
        {
            // "Top, Bottom" 출력
            Console.WriteLine(b.ToString());
        }
    }
}


연산자

연산자 타입연산자예제

연산자 타입  연산자 예제
산술 연산자 +, -, *, /, % int a = (x + y - z) * (b / c) % d;
할당 연산자 =, +=, -=, *=, /=, %= int a = 100;
sum += a;
[설명] sum += a 는 sum = sum + a 를 축약한 표현이다.
증감 연산자 ++, -- int i = 1;
i++;
[설명] i++ 는 i = i + 1 를 축약한 표현이다.
논리 연산자 && (And), || (Or), ! (Not) if ((a > 1 && b < 0) || c == 1 || !d)
관계/비교 연산자 <, >, ==, !=, >=, <= if (a <= b)
비트 연산자 & (AND), | (OR), ^ (XOR) byte a=7;
byte b=(a & 3) | 4;
[설명] 비트 연산에서 & 는 둘이 1인 경우만 1이 되고 (예: 1 & 1 = 1), | 는 둘 중에 하나라도 1인 경우 1이 되며, ^ 는 둘 중에 하나만 1 인 경우 1이 된다.
Shift 연산자 >>, << int i=2;
i = i << 5;
[설명] i의 값을 왼쪽으로 5 비트 이동한다. 결과값은 2의 6승 즉 64가 된다.
조건 연산자 ?
?? (C# 3.0 이상만 지원)
int val = (a > b) ? a : b;
[설명] a가 b보다 크면 val에 a 값을 대입하고, 같거나 작으면 b 값을 대입한다
string s = str ?? "(널)";
[설명] 변수 str가 null 이면 "(널)" 이라는 문자열을 s 에 대입한다. null 이 아니면 str의 값을 s 에 대입.

 

?? 연산자

C# 3.0 이상에서 지원하는 연산자이다.

왼쪽 피연산자의 값이 NULL인 경우 ?? 뒤의 피연산자 값을 리턴하고, 아니면 그냥 ?? 앞의 피연산자 값을 리턴한다.

int? i = null;
i = i ?? 0;

string s = null;
s = s ?? string.Empty;

조건문

if

예제

int a = -11;

if (a>=0)
{
    val = a;
}
else
{
    val = -a;
}

// 출력값 : 11
Console.Write(val);

swtich

예제

switch (category)
{
   case "사과":
      price = 1000;
      break;
   case "딸기":
      price = 1100;
      break;
   case "포도":
      price = 900;
      break;
   default:
      price = 0;
      break;
}

반복문

for

for (int i = 0; i < 10; i++)
{
   Console.WriteLine("Loop {0}", i);
}

foreach

// foreach 루프
foreach (string s in array)
{
   Console.WriteLine(s);
}

while

while (i <= 10)
{
   Console.WriteLine(i);
   i++;
}

do while

do
{
   Console.WriteLine(i);
   i++;
} while (i < 10);

yield

yield 키워드는 호출자(Caller)에게 컬렉션 데이타를 하나씩 리턴할 때 사용한다.

(1) yield return은 컬렉션 데이타를 하나씩 리턴하는데 사용

(2) yield break는 리턴을 중지하고 Iteration 루프를 빠져 나올 때 사용

예제

using System;
using System.Collections.Generic;

class Program
{
    static IEnumerable<int> GetNumber()	// 한번 호출시마다 다음 yield return 문의 값을 리턴
    {
        yield return 10;  // 첫번째 루프에서 리턴되는 값
        yield return 20;  // 두번째 루프에서 리턴되는 값
        yield return 30;  // 세번째 루프에서 리턴되는 값
    }

    static void Main(string[] args)
    {
        foreach (int num in GetNumber())
        {
            Console.WriteLine(num);
        }             
    }
}

예제

using System;
using System.Collections;

public class MyList
{
    private int[] data = { 1, 2, 3, 4, 5 };
    
    public IEnumerator GetEnumerator()
    {
        int i = 0;
        while (i < data.Length)
        {
            yield return data[i];
            i++;                
        }
    }

    //...
}

class Program
{
    static void Main(string[] args)
    {
        // (1) foreach 사용하여 Iteration
        var list = new MyList();

        foreach (var item in list)  
        {
            Console.WriteLine(item);
        }

        // (2) 수동 Iteration
        IEnumerator it = list.GetEnumerator();
        it.MoveNext();
        Console.WriteLine(it.Current);  // 1
        it.MoveNext();
        Console.WriteLine(it.Current);  // 2
    }
}

 

실행 순서

IEnumerable을 리턴하는 메서드(B)를 호출하면, yield return문에서 하나의 값을 리턴하고, 해당 메서드(B)의 위치를 기억해 둔다. 호출자(A)가 다시 루프를 돌아 다음 값을 메서드(B)에 요청하면, 메서드의 기억된 위치 다음 문장부터 실행하여 다음 yield 문을 만나 값을 리턴한다.


예외 처리

try-catch-finally

예제

try
{
   //실행 문장들
}
catch (ArgumentException ex)
{
   // Argument 예외처리
}
catch (AccessViolationException ex)
{
   // AccessViolation 예외처리
}
finally
{
   // 마지막으로 실행하는 문장들
}

 

throw

(1) throw 문 다음에 catch에서 전달받은 Exception 객체를 쓰는 경우

throw문 이전의 콜스택(Call Stack) 정보를 유실 됨

 

(2) throw 문 다음에 새 Exception 객체를 생성해서 전달하는 경우

 동일 메서드에서 발생했다 하더라도 정확히 어떤 라인에서 에러가 발생했는지를 알게 해 준다.

 

(3) throw 문 다음에 아무것도 없는 경우

동일 메서드의 어느 라인에서 에러가 발생했는지는 포함하지 않는다.

예제

try
{
    // 실행 문장들
    Step1();
    Step2();
    Step3();
}
catch(IndexOutOfRangeException ex)
{
    // 새로운 Exception 생성하여 throw
    throw new MyException("Invalid index", ex);
}
catch(FileNotFoundException ex)
{    
    bool success = Log(ex);
    if (!success)
    {
        // 기존 Exception을 throw
        throw ex;
    }
}
catch(Exception ex)
{    
    Log(ex);
    // 발생된 Exception을 그대로 호출자에 전달
    throw;
}

구조체

구조체를 생성하고 Value Type을 정의하기 위해 사용된다.

클래스보다 상대적으로 가벼운 오버헤드를 지닌 구조체가 필요할 수 있다.

클래스와 같이 메서드, 프로퍼티 등 거의 비슷한 구조를 가지고 있지만, 상속은 할 수 없다.

예제

using System;

namespace MySystem
{
   class Program
   {
      // 구조체 정의
      struct MyPoint
      {
         public int X;
         public int Y;

         public MyPoint(int x, int y)
         {
            this.X = x;
            this.Y = y;
         }

         public override string ToString()
         {
            return string.Format("({0}, {1})", X, Y);
         }
      }

      static void Main(string[] args)
      {
         // 구조체 사용
         MyPoint pt = new MyPoint(10, 12);
         Console.WriteLine(pt.ToString());
      }
   }
}

클래스

class 키워드는 Reference Type을 정의하는데 사용된다.


클래스 멤버종류 설명
메서드 (Method) 클래스에서 실제 행동을 일으키는 코드 블럭. 대개 동사 혹은 동사+명사 식으로 메서드명을 정함. 예) Calculate(), DeleteData()
속성 (Property) 클래스의 내부 데이타를 외부에서 사용할 수 있게 하거나, 외부에서 클래스 내부의 데이타를 간단하게 설정할 때 사용한다.
필드 (Field) 클래스의 내부 데이타는 필드에 저장하게 되며, 필드들은 클래스 객체의 상태를 유지하는데 이용된다. 클래스는 동일하더라도 클래스로부터 생성된 여러 객체들은 다른 필드값을 가짐에 따라 서로 다른 객체 상태를 갖게 된다. 필드는 접근제한자(Access Modifier)에 따라 외부 객체 혹은 상속 객체에서 보여질 수 있다. (public 필드를 만들어 문법적으로 필드를 외부에 노출할 수는 있지만, 이는 객체 지향 프로그래밍 방식에 어긋난다. 이 경우 주로 private 필드를 만들고 public 프로퍼티를 이용해 필드값을 외부에 전달하는 방식을 사용한다)
이벤트 (Event) 이벤트는 객체 내부의 특정 상태를, 혹은 어떤 일이 있어났다는 이벤트를 외부로 전달하는데 이용된다. 예를 들어 Button 클래스의 경우 버튼이 클릭되면, 버튼클릭 이벤트에 가입한 모든 외부 객체들에게 그 사실(이벤트)을 통보하게 된다.

 

접근제어자

public : 모두 접근 가능

protected : 같은 클래스나 상속 받은 클래스만 접근가능

private : 클래스 내에서만 접근 가능

 

Partial 클래스

C# 2.0에서부터 Partial 클래스라는 개념이 도입되었다. 이는 하나의 클래스를 2개 이상의 파일에 나누어 정의할 수 있는 기능이다

public partial class Form1 { ... }

Nullable 타입

Value Type에도 null을 할당할 수 있는 Nullable 타입을 지원

int? i = null;
bool? b = null;
int?[] a = new int?[100];

 

Nullable<T> 타입

테이블의 NULL 속성을 표현하게 된다.

예제

double _Sum = 0;
DateTime _Time;
bool? _Selected;

public void CheckInput(int? i, double? d, DateTime? time, bool? selected)
{
    if (i.HasValue && d.HasValue)
        this._Sum = (double)i.Value + (double)d.Value;

    // time값이 있는 체크.
    if (!time.HasValue)
        throw new ArgumentException();
    else
        this._Time = time.Value;

    // 만약 selected가 NULL이면 false를 할당
    this._Selected = selected ?? false;
}

 

Nullable 정적 클래스

Nullable<T> 타입을 위한 몇 가지 편리한 정적 메서드들을 제공

예제

void NullableTest()
{
    int? a = null;
    int? b = 0;            
    int result = Nullable.Compare<int>(a, b);
    Console.WriteLine(result); //결과 -1

    double? c = 0.01;
    double? d = 0.0100;
    bool result2 = Nullable.Equals<double>(c, d);
    Console.WriteLine(result2); //결과 true
}

메서드 파라미터

Pass by Value

인수의 값은 호출자에서 원래 값 그대로 유지된다.

예제

class Program
{
    private void Calculate(int a)
    {
        a *= 2;
    }

    static void Main(string[] args)
    {
        Program p = new Program();

        int val = 100;
        p.Calculate(val);  
        // val는 그대로 100        
    }
}

 

Pass by Reference

메서드 내에서 변경된 값은 리턴 후에도 유효하다.

ref는 해당 변수가 사전에 초기화되어야 하지만, C# out은 사전에 변수를 초기화할 필요는 없다.

예제

// ref 정의
static double GetData(ref int a, ref double b)
{ return ++a * ++b; }

// out 정의
static bool GetData(int a, int b, out int c, out int d)
{
    c = a + b;
    d = a - b;
    return true;
}

static void Main(string[] args)
{
    // ref 사용. 초기화 필요.
    int x = 1;
    double y = 1.0;
    double ret = GetData(ref x, ref y);

    // out 사용. 초기화 불필요.
    int c, d;
    bool bret = GetData(10, 20, out c, out d);
}

 

Named 파라미터

C# 4.0부터는 위치와 상관없이 파라미터명을 지정하여 파라미터를 전달할 수 있게 하였다. 이러한 파라미터를 Named Parameter라 부른다.

Method1(name: "John", age: 10, score: 90);

 

Optional 파라미터

C# 4.0에서부터 어떤 메서드의 파라미터가 디폴트 가질 수 있게 함

예제

class Program
{
    // Optional 파라미터: calcType
    int Calc(int a, int b, string calcType = "+")
    {
        switch (calcType)
        {
            case "+":
                return a + b;
            case "-":
                return a - b;
            case "*":
                return a * b;
            case "/":
                return a / b;
            default:
                throw new ArithmeticException();
        }
    }

    static void Main(string[] args)
    {
        Program p = new Program();
        int ret = p.Calc(1, 2);
        ret = p.Calc(1, 2, "*");
    }
}

 

params

파라미터 갯수를 미리 알 수 없는 경우 사용

//메서드
int Calc(params int[] values)

//사용
int s = Calc(1,2,3,4);
s = Calc(6,7,8,9,10,11);

이벤트

클래스내에 특정한 일(event)이 있어났음을 외부의 이벤트 가입자(subscriber)들에게 알려주는 기능을 한다.

예제

// 클래스 내의 이벤트 정의
class MyButton
{
   public string Text;
   // 이벤트 정의
   public event EventHandler Click;

   public void MouseButtonDown()
   {
      if (this.Click != null)
      {
         // 이벤트핸들러들을 호출
         Click(this, EventArgs.Empty);
      }
   }
}

// 이벤트 사용
public void Run()
{
   MyButton btn = new MyButton();
   // Click 이벤트에 대한 이벤트핸들러로
   // btn_Click 이라는 메서드를 지정함
   btn.Click += new EventHandler(btn_Click);
   btn.Text = "Run";
   //....
}

void btn_Click(object sender, EventArgs e)
{
   MessageBox.Show("Button 클릭");
}

 

add와 remove

get, set 을 사용하듯이 event 에서는 add, remove 를 사용할 수 있다. 

value 델리게이트를 추가하거나 삭제하는 기능

+= 이나  -= 을 사용

예제

class MyButton
{
    // 이벤트 정의하는 다른 방법
    private EventHandler _click;
    public event EventHandler Click
    {
        add
        {
            _click += value;
            // _click = value;   // 싱글캐스트
        }
        remove
        {
            _click -= value;
        }
    }

    public void MouseButtonDown()
    {
        if (this._click != null)
        {
            // 이벤트핸들러들을 호출
            _click(this, EventArgs.Empty);                                
        }
    }

    /* 속성 정의
    private string _name;
    public string Name 
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
        }
    }
    */
}

 

지시어

특별한 명령을 미리 처리하도록 지시하는 것

 

조건별 컴파일

#define은 심벌을 정의할 때 사용

#if를 결합하여 조건별로 서로 다른 코드 블럭을 컴파일하도록 할 수 있다.

예제

#define TEST_ENV
//#define PROD_ENV

using System;

namespace App1
{
    class Program
    {
        static void Main(string[] args)
        {
            bool verbose = false;
            // ...

#if (TEST_ENV)	// TEST_ENV 심벌 정의 된 경우
            Console.WriteLine("Test Environment: Verbose option is set.");
            verbose = true;
#else			// 정의 안된 경우
            Console.WriteLine("Production");
#endif

            if (verbose)
            {
                //....
            }
        }
    }
}

 

region 전처리기 지시어

코드 블럭을 논리적으로 묶을 때 유용하다. 예를 들어, Public 메서드들만 묶어 [Public Methods]라고 명명할 수 있고, Private 메소드들을 묶어 [Privates] 라고 명명할 수 있다.

예제

class ClassA
{
    #region Public Methods        
    public void Run() { }
    public void Create() { }        
    #endregion

    #region Public Properties
    public int Id { get; set; }
    #endregion

    #region Privates
    private void Execute() { }
    #endregion
}

 

기타 전처리기 지시어

#undef 는 지정된 심벌을 해제 할 때 사용

#line 라인번호를 임의로 변경하거나 파일명을 임의로 다르게 설정할 수 있게 해준다.

#error 는 전처리시 Preprocessing을 중단하고 에러 메시지를 출력하게 한다.

#warning 은 경고 메서지를 출력하지만 Preprocessing은 계속 진행한다.

예제

// #warning 예제 -----------------------------------
#if (!ENTERPRISE_EDITION)
#warning This class should be used in Enterprise Edition
#endif

namespace App1 {
    class EnterpriseUtility {
    }
}

// #error 예제 --------------------------------------
#define STANDARD_EDITION
#define ENTERPRISE_EDITION

#if (STANDARD_EDITION && ENTERPRISE_EDITION)
#error Use either STANDARD or ENTERPRISE edition. 
#endif

namespace App1 {
    class Class1 {
    }
}

 

pragma 전처리기 지시어

컴파일러 제작업체가 고유하게 자신들의 것을 만들어 사용할 수 있는 지시어이다. 즉, 어떤 컴파일러를 쓰느냐에 따라 지원되는 #pragma가 서로 다르며, 개발자가 임의로 지정하여 사용할 수 없다.

MS의 C# 컴파일러는 현재 #prama warning과 #pragma checksum 2개를 지원하고 있다.
#prama warning는 경고메서지를 Disable/enable 하게 할 수 있으며, #pragma checksum는 주로 ASP.NET 페이지 디버깅을 위해 만들어진 것으로 ASPX 페이지의 파일 체크섬을 생성할 때 사용된다.

아래 예제는 (1) #pragma warning을 사용하여 컴파일시 CS3021 경고를 Disable하는 예와 (2) if (false) 블럭에 대한 경고를 전체 disable 했다가 다시 enable (restore)하는 예를 든 것이다.

예제

// CS3021 Warning을 Disable
#pragma warning disable 3021

namespace App1
{
    [System.CLSCompliant(false)] 
    class Program
    {
        static void Main(string[] args)
        {
            //...
        
#pragma warning disable
        if (false)
        {
            Console.WriteLine("TBD");
        }
#pragma warning restore
        
            //...
        }
    }
}

https://www.csharpstudy.com/CSharp/CSharp-preprocessor.aspx 참고

'ETC' 카테고리의 다른 글

ASP.NET Core 웹 개발  (0) 2023.08.24
MS-SQL  (0) 2023.08.24
MS-SQL  (0) 2023.08.23
ASP.NET Core  (0) 2023.08.23