반응형

간단하게 정리해본 Rust 강좌입니다. 

 

다음 문서를 기반으로 작성했습니다.

Tour of Rust https://tourofrust.com/00_ko.html 

 

2022. 09. 07  최초 작성

2023. 03. 22  최종 작성



Rust 개발 환경 만드는 방법은 아래 포스트를 참고하세요.

 

Windows에 Visual Studio Code + Rust 개발 환경 만들기

https://webnautes.tistory.com/2110



Hello World

문자열 "Hello, 🦀"를 화면에 출력해주는 간단한 코드입니다. 

fn main() {
    println!("Hello, 🦀");
}

 

이름이 main인 함수를 정의하기 위해 fn 키워드 다음에 함수 이름 main을 적습니다. main 함수는 Rust 프로그램이 실행되면 가장 먼저 실행되는 함수입니다.  main 뒤에 오는 괄호 ( ) 안에 아무것도 없는 것은 main 함수에서 전달받는 값이 아무것도 없다는 의미입니다.

fn main() {
    println!("Hello, 🦀");
}

 

중괄호 {}안에 main 함수가 호출되면 실행되는 코드를 추가합니다.  여기에서는 println! 함수를 실행하여 문자열 "Hello, 🦀"를 화면에 출력합니다. 

fn main() {

    println!("Hello, 🦀");

}

 

println! 함수는 큰따옴표(“) 안에 있는 문자열을 화면에 출력해줍니다. 주의할 점은 Rust에서 모든 문의 끝에는 세미콜론(;)이 있어야 합니다.

println!("Hello, 🦀");



코드를 실행해보면 문자열 "Hello, 🦀”가 화면에 출력됩니다. 




println!

문자열을 출력하는 println! 함수 예제를 하나 더 살펴봅니다.

 

// 참고 https://doc.rust-lang.org/rust-by-example/hello/print.html

fn main() {
  

    // 하나의 문자열을 출력할 때에는 {} 하나를 사용합니다. 
    println!("{}", "안녕 세상아");

    // {}를 사용하면 문자열 사이의 원하는 위치에 값을 출력할 수 있습니다.
    // 다음 코드는 문자열 사이에 추가한 3개의 {} 자리에 뒤에 ,로 구분하여 적은 3개의 값이 차례로 출력됩니다.
    println!("{} + {} = {}", 1,2,3);

    // 괄호{}안에 숫자를 적어서 출력할 위치를 지정할 수 있습니다. {0} 자리엔 철수가, {1} 자리엔 영이가 출력됩니다.
    println!("안녕 {0}, 나는 {1}야. 안녕 {1}, 나는 {0}야.", "철수", "영이");


    // 키워드 인수(named argument)를 사용하여 출력 위치를 지정할 수 있습니다.
    println!("{subject} {object} {verb}",
            object="사과를",
            subject="나는",
            verb="좋아한다.");
}



실행결과

 

안녕 세상아

1 + 2 = 3

안녕 철수, 나는 영이야. 안녕 영이, 나는 철수야.

나는 사과를 좋아한다.



변수

변수에 값을 대입하여 값 대신에 사용할 수 있습니다. 변수를 활용할 수 있는 것중 하나를 예로 들면 계산 결과를 저장하는 용도로 사용할 수 있습니다.

 

변수는 let키워드를 사용하여 선언합니다. 다음처럼 let 키워드 다음에 변수 이름 x를 적고 대입할 값을  = 다음에 42를 적어줍니다. Rust에서는 변수에 대입된 값에 대한 데이터 타입을 지정해주지 않아도 Rust가 알아서 데이터 타입을 추측합니다.  정수의 경우 디폴트 데이터 타입은 32비트 부호 있는 정수를 의미하는 i32입니다. 

let x = 42;

 

데이터 타입을 적어주려면 다음처럼 하면 됩니다.  

let x:i32 = 42;

 

변수 선언시 데이터 타입을 지정하기 위해 콜론( : )을 사용합니다. 부호 있는 정수 데이터 타입으로는 i8, i16, i32, i64, i128이 있고, 부호 없는 정수 데이터 타입으로는 u8, u16, u32, u64, u128이 있습니다. 숫자는 저장 가능한 최대 비트수입니다. i는 부호가 있다는 것을 의미하고 u는 부호가 없다는 것을 의미합니다.

 

변수를 먼저 선언하고 나중에 값을 대입할 수 도 있습니다.

let x: i32; // 변수 x의 타입을 부호 있는 32비트 정수(signed 32-bit integer)로 지정합니다.
x = 42;  // 변수 x에 정수 42를 대입합니다.



변수 x에 정수 42를 대입하면 코드에서 정수 42을 적을 위치에 변수 x를 대신 적어서 사용할 수 있습니다.

예를 들어 다음처럼 직접 정수 42를 사용하는 대신에 

fn main() {
    println!("{}", 42 + 1);
}

실행결과

42

 

변수 x에 42를 대입한 후 정수 42가 위치하던 자리에 변수 x를 사용할 수 있습니다.

fn main() {
    let x = 42;
    println!("{}", x + 1);
}

실행결과

42



변수 이름은  snake_case 형태로 짓습니다. 변수 이름을 구성하는 단어를 모두  소문자로 적고 단어가 이어질 때 마다 언더바(_)를 사용하는 방법입니다. 아래 예제 코드에서 단어 apple과 단어 price 사이에 언더바를 추가하여 변수 이름으로 사용하고 있습니다. 

let apple_price = 100;



변수를 선언시 값을 대입하는 2가지 방법이 있습니다. 

 

다음처럼 변수를 선언하면서 변수에 값을 바로 대입하거나

let x = 42; 

 

또는 다음처럼 변수 x를 먼저 선언하고 나서 나중에 변수 x에 값 42를 대입할 수도 있습니다.  

let x; // 변수 x를 선언합니다.
x = 42; // 변수 x에 정수 42를 대입합니다.



변수를 선언 한 후, 나중에 변수에 값을 대입하는 경우 주의할 점이 있습니다.  Rust 컴파일러는 변수에 값이 대입되기 전에는 변수를 사용하지 못하도록 한다는 점입니다. 

 

예를 들어 다음 코드처럼 변수 x를 선언하고나서 변수 x에 값을 대입하기 전에 변수 x의 값을 출력하기 위해 접근하려고 하면 컴파일시  에러가 발생합니다. 

fn main() {
    let x;


    println!("{}", x);
}

 

실행하면 에러가 발생합니다. 하지만 여기에서는 변수의 데이터 타입을 선언 안했다고 에러를 보여주고 있습니다. 변수에 값을 대입하지 않고 사용했다는 에러를 먼저 보여주었으면 좋았을텐데 다른 에러를 먼저 보여줍니다.

  

error[E0282]: type annotations needed




에러를 해결하기 위해 다음처럼 변수 x에 데이터 타입 i32을 지정해주면 예상했던 에러 메시지를 볼 수 있습니다. 

fn main() {
    let x : i32;

    println!("{}", x);
}

 

변수 x를 초기화하지 않고 사용했다는 에러 메시지를 보여줍니다.

 

실행결과

error[E0381]: used binding `x` isn't initialized



이 문제를 해결하려면 다음처럼 변수 x에 값을 대입해줘야 합니다.

fn main() {
    let x : i32;


    x = 42;
    println!("{}", x); // 변수 x에 값이 대입되었기 때문에 사용할 수 있습니다.
}

 

실행결과 42가 출력됩니다.



변수를 선언한 후 코드에서 해당 변수를 사용하지 않으면 컴파일시 해당 변수를 사용하지 않았다는 경고문이 보입니다. 에러는 아니기 때문에 컴파일하여 프로그램을 실행하는 데 문제가 있는건 아닙니다. 

fn main() {
    let x = 32;
}

 

실행결과

warning: unused variable: `x`



변수 이름 앞에 밑줄( _ )를 아래 코드처럼 추가해주면 컴파일시 변수를 사용하지 않았다는 경고문을 안보이게 해줍니다. 

fn main() {
    let _x = 32;
}



동일한 이름의 변수를 여러 번 다시 선언하는 것이 가능합니다.  이때 데이터 타입도 변경이 가능합니다. 

fn main() {

    let x = 10;    // 변수 x를 정수 타입으로 선언합니다.
    let x = x + 3; // 앞에서 선언한 변수 x에 3을 더한 값을 저장하는 새로운 변수 x를 선언합니다.
                  // 이제 앞에서 선언한 변수 x는 지금 선언한 변수 x로 대체됩니다.

    println!("{}", x); // 13이 출력됩니다.


    let x = "hello"; // 정수 타입의 값을 저장했던 변수 x를 문자열 타입으로 변경하여 다시 선언할 수 있습니다.
    println!("{}", x); // 문자열 “hello”가 출력됩니다.
}




한 쌍의 대괄호 {} 는 블록을 선언합니다. 블록 내에서 선언한 변수는 지역변수가 되며 블록을 벗어나는 순간 해당 변수는 스택에서 삭제되기 때문에 더 이상 해당 변수를 사용할 수 없습니다. 

fn main() {

    // 변수 x에 문자열 "out"을 저장합니다.
    let x = "out";


    {// 여기에서 새로운 블록이 시작됩니다. 블록바깥에 있는 변수 x를 여기서 사용가능하지만

     // 블록 내부에서 같은 이름의 변수 x를 선언하면 블록내에서는 x가 가리키는 것이 블록 바깥에 있는 변수 x가 아닌 블록 안에 있는 변수 x를 의미하게 됩니다.

        // 블록 외부에 있는 변수 x를 사용할 수 있습니다.  문자열 “out”이 출력됩니다. 
        println!("{}", x);

        // 변수 x를 선언하면서 문자열 "in"을 저장합니다.
        // 블록 바깥에서 선언한 변수 x와는 다른 변수가 됩니다.
        let x = "in";

        // 변수 x의 값을 출력해보면 "in"이 출력됩니다.
        println!("{}", x);


        // 블록내에서 선언된 변수 x는 블록을 벗어나면 스택에서 삭제됩니다.
    } // 여기에서 블록이 끝납니다.

    // 블록 바깥에 있던 변수 x에 저장된 값인 "out"이 출력됩니다.
    println!("{}", x);
}




변수의 값 변경하기

Rust에는 두 가지 종류의 변수가 있습니다.  

 

1. let 키워드를 사용하여 변수를 선언하면 대입된 값을 변경할 수 없는 (immutable) 변수가 됩니다. 

 

fn main() {

    let x = 42; // let을 사용하여 정수 42를 저장하는 변수 x를 선언합니다.

    x = 100; // 변수 x의 값을 변경하려고 하면 에러가 발생합니다. let 키워드를 사용하여 선언한 변수는 값을 변경할 수 없기 때문입니다.
   
    println!("{}", x);
       
}

 

error[E0384]: cannot assign twice to immutable variable `x`



2. let 키워드와 함께 mut 키워드를 사용하여 변수를 선언하면 변수에 대입된 값을 변경할 수 있는(mutable) 변수가 됩니다.

 

fn main() {

    let mut x = 42; // let mut를 사용하여 정수 42를 저장하는 변수 x를 선언합니다.

    println!("{}", x);

    x = 100; // 이번엔 변수 x의 값을 변경할 수 있습니다.
   
    println!("{}", x);
}



데이터 타입

Rust에서 제공하는 데이터 타입입니다. 

 

bool

부울값 true 또는 false 값을 가집니다.

 

u8 u16 u32 u64 u128

부호가 없는 정수인 양의 정수를 저장할 수 있습니다.

u 뒤에 있는 숫자는 저장가능한 최대 비트수를 의미합니다.

 

 i8 i16 i32 i64 i128 

부호가 있는 정수인 양의 정수 또는 음의 정수를 저장할 수 있습니다. 

i 뒤에 있는 숫자는 저장가능한 최대 비트수를 의미합니다.

 

usize isize

포인터 크기를 의미하는 정수를 저장합니다. 에를 들어 메모리에 저장되어 있는 값들에 대한 인덱스와 크기입니다.

 

f32 f64

실수를 저장할 수 있습니다.

f 뒤에 있는 숫자는 저장가능한 최대 비트수를 의미합니다.

 

튜플(tuple)

여러 종류의 데이터 타입을 같이 저장할 수 있는 컬렉션(collection)입니다.

 

배열(array)

튜플과 달리 같은 종류의 데이터 타입의 값을 저장할 수 있는 컬렉션입니다.

 

char

하나의 문자를 저장하며 문자를 작은 따옴표(')로 감싸야합니다. 

 

str

문자들로 구성되는 문자열을 저장하며 문자열을 큰 따옴표(")로 감싸야 합니다. 



숫자 데이터 타입의 경우  숫자 뒤에 자료형 이름을 붙여 명시적으로 데이터 타입을 지정할 수 있습니다 

fn main() {

    let a = 12; // 데이터 타입을 지정하지 않으면 정수는 i32,부호있는 32비트정수입니다.
    let b = 12u8; // u8을 정수 뒤에 붙이면 부호없는 8비트정수가 됩니다.

    let c = 4.3; // 데이터 타입을 지정하지 않으면 실수는 f64, 64비트 실수입니다.
    let d = 4.3f32; // f32를 실수 뒤에 붙이면 32비트 실수가 됩니다.
}



튜플

튜플은 괄호 () 안에 값을 콤마로 구분하여 추가합니다. 튜플에 저장되는 값이 모두 똑같은 데이터 타입이 아니어도 됩니다. 

fn main() {

    let pair = ('a', 17); //문자와 정수를 하나의 튜플에 같이 저장할 수 있습니다.
    

    //튜플의 값을 다음처럼 나누어 출력할 수 있습니다.
    println!("{}", pair.0);
    println!("{}", pair.1);
}

 

실행결과

a

17



Rust에서 튜플은 내부적으로 구조체라서 for문 같은 반복문을 사용하여 인덱스를 증가시키며  튜플의 원소들을 출력할 수 없다고 합니다.  필요한 경우 튜플 대신에 배열을 사용하라고 하네요.




튜플에 저장되는 값에 대한 데이터 타입을 다음처럼 각각 지정할 수 있습니다. 

fn main() {

    let pair: (char, i32) = ('a', 17);
   
    println!("{}", pair.0);  // 문자 ‘a’가 출력됩니다.
    println!("{}", pair.1);   // 정수 17이 출력됩니다.
}




튜플 형태로 변수를 선언하여 값을 대입할 수 있습니다. 

fn main() {

    let (some_char, some_int) = ('a', 17);
   
    println!("{}", some_char);  // 문자 ‘a’가 출력됩니다.
    println!("{}", some_int);   // 정수 17이 출력됩니다.
}




함수가 튜플을 반환할 수 있습니다. 

fn main() {
    // 인덱스는 0123456789
    let str = "1234567890";
    

    // 다음 코드는 숫자 5가 위치한 인덱스 4를 기준으로 문자열을 둘로 나누어 튜플에 저장합니다. 
    // left에는 인덱스 0 ≦ s < 3 범위의 문자인 1234가 저장되고
    // right에는 인덱스 4 ≦ s 범위의 문자인 567890이 저장됩니다.
    let (left, right) = str.split_at(4);

    println!("{}", left);  // 1234가 출력됩니다.
    println!("{}", right);  // 567890이 출력됩니다.
}




_를 사용하여 튜플의 일부값을 저장하지 않도록 할 수 있습니다. 

fn main() {

    let str = "1234567890";
    

    // 바로 전 예제 코드에서 left에 해당하는 문자인 1234는 저장하지 않습니다.
    let (_, right) = str.split_at(4);

    println!("{}", right);  // 567890이 출력됩니다.
}




데이터 타입  변환

as 키워드를 사용해 데이터 타입을 변환할 수 있습니다. 

 

다음 예제는 u8(부호 없는 8비트 정수)로 선언된 변수 a를 u32(부호없는 32비트 정수)로 선언된 변수 b와 더하기 위해서 변수 a를 as 키워드를 사용하여 u32(부호없는 32비트 정수) 타입으로 변환했습니다.

fn main() {
    let a = 13u8;
    let b = 7u32;

    let c = a as u32 + b;

    println!("{}", c); // 20이 출력됩니다.

}



as 키워드를 제거하면 다음과 같은 에러가 납니다. 

fn main() {
    let a = 13u8;
    let b = 7u32;

    let c = a  + b;

    println!("{}", c);

}



데이터 타입이 다른 정수끼리 연산이 안된다는 에러입니다. 

error[E0308]: mismatched types



상수

여러 번 사용되는 고정된 값을 상수로 지정하여 사용합니다. 

사용될 때 값이 복사되는 변수와 달리, 상수는 컴파일 타임에 상수 적었던 부분을 직접 값으로 대체합니다.

변수와 달리, 상수는 반드시 명시적으로 자료형을 지정해야 합니다.

 

상수의 이름은 전부 대문자로 적고 언더바(_)로 단어 사이를 구분하는 SCREAMING_SNAKE_CASE 형태를 갖습니다.  

const PI: f64 = 3.14159;

fn main() {

    let r : f64 = 10.0;
    let circumference = 2.0 * PI * r;

    println!("{}", circumference); // 62.8318이 출력됩니다.
}



배열

배열(array)은 튜플과 달리 모든 원소의 데이터 타입이 같아야 합니다. 배열의 데이터 타입은  [원소의 데이터 타입;원소의 개수] 형태로 지정합니다.

 

다음은 i32(부호있는 32비트 정수) 데이터 타입으로 3개의 원소를 갖는다는 의미입니다.

[i32; 3]

 

i32 데이터 타입으로 3개의 원소를 갖는 배열을 변수 nums에 대입하는 것을 보여줍니다. 

let nums: [i32; 3] = [1, 2, 3];

 

배열에 포함되어 있는 원소는 인덱스를 사용하여 가져 올 수 있습니다. 인덱스는 0부터 시작합니다. 

fn main() {

    // i32(부호있는 32비트 정수) 데이터 타입의 정수 3개를 원소로 가지는 배열 nums를 선언합니다.
    let nums: [i32; 3] = [1, 2, 3];

    // 배열의 원소를 모두 출력합니다.
    println!("{:?}", nums);

    // 인덱스 1에 해당하는 두번째 원소를 출력합니다. 배열의 인덱스는 0부터 시작하므로 두번째 원소의 인덱스가 1이 됩니다.
    println!("{}", nums[1]);
}

 

실행결과

[1, 2, 3]

2



아직 for문을 다루지 않았지만 for문을 사용하여 간단하게 배열의 원소를 출력해봅니다.

fn main() {

    let nums: [i32; 3] = [1, 2, 3];


    // for문을 사용하여 인덱스를 0 부터 2까지 증가시킵니다.
    // num.len()는 배열의 크기인 3이고, 따라서 0..nums.len()는 0..3으로 바꿀 수 있는데 3은 빼고 0부터 2까지 인덱스를 증가시킨다는 의미입니다.
    for i in 0..nums.len(){
        println!("{}", nums[i]);
    }
       
}



실행결과

1

2

3



함수

fn 키워드를 사용하여 함수를 선언합니다. 함수의 이름은 소문자로 적고 단어 사이를 언더바(_)로 연결하는 snake_case 형태를 사용합니다. 함수 다음에 오는 괄호()에 함수에서 전달받는 아규먼트를 추가합니다.

 

함수는 아규먼트 없이 호출될 수도 있고 한 개 이상의 아규먼트와 함께 호출될 수도 있습니다. 

함수를 호출시 아규먼트를 사용하여 데이터를 호출한 함수로 넘겨줄 수 있습니다.

 

다음 예제 코드는 main 함수에서 greet 함수를 호출합니다.  greet 함수에는 전달받는 아규먼트가 없습니다.

greet 함수에서 main 함수로 값을 전달하는 것도 없습니다.  

fn greet() {
    println!("Hi there!");
}

fn main() {
    greet()
}

 

실행결과

Hi there!



다음 코드는 fair_dice_roll 함수에서 자신을 호출한 main 함수로 32비트 부호 있는 정수(i32)를 전달합니다.  첫줄에 보이는 -> 32의 의미는 함수 fair_dice_roll에서 자신을 호출한 함수로 전달되는 값의 데이터 타입이  i32라는 것입니다.  

 

함수 마지막 줄에 값이나 변수를 적으면 해당 값이 함수를 호출한 곳으로 전달됩니다. 

fn fair_dice_roll() -> i32 {
    let v = 4;


    // 변수 v의 값이 main함수로 전달됩니다.
    v
}

fn main() {


    // fair_dice_roll 함수에서 전달된 값이 변수 ret에 저장됩니다.
    let ret = fair_dice_roll();

    println!("{}", ret); // 4가 출력됩니다.
}



Rust에도 return문이 있습니다. 함수 마지막에 자기를 호출한 곳으로 전달할 변수 또는 값을 적을 때 앞에 return 키워드를 적어주면 코드가 더 직관적으로 보일 수 있습니다. 

fn fair_dice_roll() -> i32 {
    let v = 4;

    return v;
}

fn main() {
    let ret = fair_dice_roll();

    println!("{}", ret);
}



다음 예제에서 add 함수는 i32 데이터 타입의 아규먼트 2개 x, y를 전달받고 한개의 i32 데이터 타입의 값을 자신을 호출한 함수로 전달합니다.

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}

fn main() {
    // add 함수에 정수 1,2를 전달하여 계산결과 3을 다시 전달받습니다. 3이 출력됩니다.

    ret = add(1, 2);
    println!("{}", ret);

    // add 함수에서 전달한 값을 변수에 대입하지 않고 바로 출력해도 됩니다.
    println!("{}", add(1, 2));
}



함수에서 튜플을 자신을 호출한 함수에 전달함으로써 여러 개의 값을 한번에 전달할 수 있습니다.

아래 예제에서 함수에서 전달되는 값의 데이터 타입을 i32 데이터 타입 두 개가 포함된 튜플로 지정하고 있습니다.

fn swap(x: i32, y: i32) -> (i32, i32) {
    return (y, x); // 전달 받은 변수의 순서를 바꾸어서 호출한 곳으로 다시 돌려줍니다.
}

fn main() {

    // 두 개의 정수 123, 321를 함수 swap에 전달하고 변수 result에 튜플을 전달받습니다.
    let result = swap(123, 321);


    // 튜플을 나누어 출력합니다. 순서가 바뀐 321 123이 출력됩니다.
    println!("{} {}", result.0, result.1);



    // 두 개의 정수 123, 321를 함수 swap에 전달하고 다시 전달받은 튜플을 두개의 변수에 나누어 대입합니다.
    let (a, b) = swap(123, 321);


    // 두개의 변수를 출력합니다. 순서가 바뀐 321 123이 출력됩니다.
    println!("{} {}", a, b);
}




함수에서 자신을 호출한 함수로 전달하는 값을 -> 다음에 데이터 타입으로 지정하지 않은 경우 비어있는 튜플을 리턴합니다.  비어 있는 튜플을 ()로 표현합니다. 

// 자신을 호출한 함수로 전달되는 값의 데이터 타입을 비어있는 튜플()로 명시한 경우
fn make_nothing() -> () {

    // return 문에 ()를 자신을 호출한 함수로 전달한다고 명시합니다.
    return ();
}

// 자신을 호출한 함수로 전달되는 값의 데이터 타입을 적지 않으면 -> ()를 생략한게 됩니다.
fn make_nothing2() {

    // return 문도 생략할 수 있습니다.
}

fn main() {

    let a = make_nothing();
    let b = make_nothing2();

    // 전달 받은 값을 출력해봅니다.
    println!("The value of a: {:?}", a);
    println!("The value of b: {:?}", b);
}

 

실행 결과

The value of a: ( )

The value of b: ( )



간단하게 정리해본 Rust 강좌 1 : 변수, 상수, 함수, 튜플, 배열, 데이터타입, println

https://webnautes.tistory.com/2194

 

간단하게 정리해본 Rust 강좌 2 : if-else, loop, while, for, match, struct, method

https://webnautes.tistory.com/2195

 

간단하게 정리해본 Rust 강좌 3 : 열거형, 제네릭 데이터 타입, Option, Result, unwrap, vector

https://webnautes.tistory.com/2196

 

간단하게 정리해본 Rust 강좌 4 : 소유권, 참조, 역참조, 생명주기

https://webnautes.tistory.com/2197

 

간단하게 정리해본 Rust 강좌 5 : 문자열, utf-8

https://webnautes.tistory.com/2198

 

간단하게 정리해본 Rust 강좌 6 : 모듈

https://webnautes.tistory.com/2199

 

간단하게 정리해본 Rust 강좌 7 : &self, &mut self, trait, 동적 디스패치, 정적 디스패치, Generic 메서드, Box

https://webnautes.tistory.com/2200

 

간단하게 정리해본 Rust 강좌 8 : 참조자, 댕글링 참조, 원시 포인터

https://webnautes.tistory.com/2203

 

간단하게 정리해본 Rust 강좌 9 : Box, 재귀적 데이터타입, Deref 트레잇

https://webnautes.tistory.com/2202



반응형

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts