2025년 4월 26일 토요일

6-3강: 러스트 라이브러리(Fs, Path, PathBuf)

1. fs : 파일 시스템 관련 작업을 지원하는 표준 라이브러리

//fs 모듈을 사용하여 파일을 생성하고 읽는 예제
use std::fs::File;
use std::io::{self, Read, Write};

//? 전제 조건
// main() 함수나 해당 위치 함수의 반환 타입이 Result<...> 이어야 한다.
// 그래야 에러를 return할 수 있다.
fn main() -> io::Result<()> {
    //?를 붙이면 에러가 발생하면 즉시 현재 함수를 빠져나가고,
    //에러를 호출자에게 전달
    let mut file = File::create("example.txt")?; // 파일 생성

    // b"Hello"처럼 b를 붙이면 &[u8] 타입, 즉 바이트 슬라이스가 된다.
    // write_all 메서드는 &[u8] 타입의 데이터를 받아들이기 때문
    file.write_all(b"Hello, Rust!")?; // 파일에 내용 추가

    // 파일 읽기
    let mut file = File::open("example.txt")?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    println!("{}", content);

    Ok(())
}

/*실행결과
Hello, Rust!
*/

2. 디렉터리 생성, 읽기, 삭제

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    // 디렉터리 생성
    fs::create_dir("example_directory")?;
    println!("example_directory 생성됨");

    // 현재 실행 디렉터리의 모든 내용 출력
    let entries = fs::read_dir(".")?;
    println!("현재 실행 디렉터리 내용:");
    for entry in entries {
        let entry = entry?;
        println!("{:?}", entry.path());
    }

    // 디렉터리 삭제
    //fs::remove_dir("example_directory")?;
    //println!("example_directory 삭제됨");

    Ok(())
}
/*실행 결과
example_directory 생성됨
현재 실행 디렉터리 내용:
".\\example_directory"
".\\main.exe"
".\\main.pdb"
".\\main.rs"
*/

3. Path, PathBuf

use std::path::{Path, PathBuf};

fn main() {
    // Path : 참조용, 읽기만 가능
    let path = Path::new("/tmp/test.txt");

    // 경로의 파일명 추출
    if let Some(filename) = path.file_name() {
        println!("파일명: {:?}", filename);
    }

    // 경로의 확장자 추출
    if let Some(extension) = path.extension() {
        println!("확장자: {:?}", extension);
    }

    // PathBuf : 소유 및 수정 가능
    let mut path_buf = PathBuf::from("/tmp/foo");
   
    // 경로에 파일명 추가
    path_buf.push("example.txt");
    println!("전체 경로: {:?}", path_buf);  
}

/*실행 결과
파일명: "test.txt"
확장자: "txt"
전체 경로: "/tmp/foo\\example.txt"
*/

6-2강: 러스트 라이브러리(From, Into, AsRef, AsMut)

1. From : 한 자료형을 다른 자료형으로 변환하는 로직을 정의

2. Into : From 의 역방향으로 자료형을 변환

// 러스트는 타입에 관한 엄격한 안정성을 보장한다.
// 그래서 타입을 변환할 때는 명시적으로 변환을 해줘야 한다.
// From 은 한 자료형을 다른 자료형으로 변환하는 로직을 정의할 때 사용
// Into는 From을 통해 변환된 자료형을 사용.
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// Point 구조체를 (i32, i32) 튜플로 변환하는 From trait 구현
impl From<(i32, i32)> for Point {
    fn from(tuple: (i32, i32)) -> Self {
        Point { x: tuple.0, y: tuple.1 }
    }
}

fn main() {
    let tuple = (1, 2);

    // 주어진 tuple을 바탕으로 Point객체를 생성
    let pt: Point = Point::from(tuple);
    println!("Point::from = {:?}", pt);
   
    // tuple을 기반으로 point를 생성합니다. 이때 Point::from이 호출
    let pt: Point = tuple.into();
    println!("tuple.into = {:?}", pt);
}

/*실행결과
Point::from = Point { x: 1, y: 2 }
tuple.into = Point { x: 1, y: 2 }
*/

3. AsRef : 객체를 참조값으로 변환 

// AsRef 트레잇은 어떤 타입을 다른 타입으로 변환할 수 있는 방법
// 이 트레잇을 구현하면, 해당 타입을 다른 타입으로 쉽게 변환
// 예를 들어, String을 &str로 변환하거나, Vec<T>를 &[T]로 변환
// 이 메서드는 AsRef 트레잇을 구현한 타입의 인스턴스에서 호출
struct Person {
    name: String,
    age: u32,
}

impl AsRef<str> for Person {
    // Person의 name을 str형태로 참조할 수 있습니다.
    fn as_ref(&self) -> &str {
        &self.name
    }
}

fn greet_person<P: AsRef<str>>(person: P) {
    println!("안녕! {}!", person.as_ref());
}

fn main() {
    let person = Person { name: String::from("루나"), age: 30 };

    // Person 구조체에 AsRef<str>를 구현했기 때문에,
    // greet_person 함수는 Person을 인자로 받아 사용할 수 있습니다.
    greet_person(person); //안녕! 루나!

    // 물론, String과 &str도 여전히 함수의 인자로 사용할 수 있습니다.
    let name_string = String::from("러스트");
    greet_person(name_string); //안녕! 러스트!
    greet_person("하이!"); //안녕! 하이!!
}

/*실행결과
안녕! 루나!
안녕! 러스트!
안녕! 하이!!
*/

4. AsMut : 객체를 수정 가능한 참조로 변환

// AsMut 는 객체를 수정 가능할 참조로 바꾸는 트레잇이다.
// AsMut 트레잇을 구현한 타입은 &mut T로 변환할 수 있다.
struct Person {
    name: String,
    age: u32,
}

// AsMut 트레잇을 구현하여 name 필드에 대한 가변 참조를 제공한다.
// AsMut 트레잇을 구현하면, &mut T로 변환할 수 있다.
impl AsMut<String> for Person {
    fn as_mut(&mut self) -> &mut String {
        &mut self.name
    }
}

// name_change 함수는 AsMut 트레잇을 구현한 타입에 대해
// name 필드의 값을 변경하는 기능을 제공한다.
// 이 함수는 person 매개변수로 전달된 객체의 name 필드를
// 가변 참조로 가져와서 새로운 이름으로 변경한다.
fn name_change<P: AsMut<String>>(person: &mut P, new_name: &str) {
    let name = person.as_mut();
    name.clear();
    name.push_str(new_name);
}

fn main() {
    let mut person = Person {
        name: String::from("루나"),
        age: 10
    };

    println!("변경 전: {}", person.name); //변경 전: 루나
    name_change(&mut person, "러스트");
    println!("변경 후: {}", person.name); //변경 후: 러스트
}

/*실행 결과
변경 전: 루나
변경 후: 러스트
*/

6-1강: 러스트 라이브러리(Copy, Clone, Drop)

1. Copy 예제

#[derive(Debug, Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn add_points(p1: Point, p2: Point) -> Point {
    Point {
        x: p1.x + p2.x,
        y: p1.y + p2.y,
    }
}

fn main() {
    let a = Point { x: 1, y: 2 };
    let b = Point { x: 3, y: 4 };

    // add_point의 인자로 들어가는 a, b는 copy트레잇에 의해 복제됩니다.
    let result = add_points(a, b);

    println!("{:?}", a); // a에 접근 가능
    println!("{:?}", b); // b에 접근 가능
    println!("{:?}", result);
}
/*실행결과
Point { x: 1, y: 2 }
Point { x: 3, y: 4 }
Point { x: 4, y: 6 }
*/

2. Clone 예제
// Clone trait을 구현하지 않은 타입은 clone() 메서드를 사용할 수 없다.
#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
    cloned: bool,
}

impl Clone for Person {
    fn clone(&self) -> Self {
        Person {
            name: self.name.clone(),
            age: self.age,
            cloned: true
        }
    }
}

fn main() {
    let person1 = Person {
        name: String::from("루나"),
        age: 10,
        cloned: false
    };

    // person1을 복제합니다. 소유권을 잃지 않습니다.
    let person2 = person1.clone();

    println!("{:?}", person1);
    println!("{:?}", person2);
}

/*실행결과
Person { name: "루나", age: 10, cloned: false }
Person { name: "루나", age: 10, cloned: true }
*/

3. Drop 예제
// 객체가 메모리에서 벗어날 때 수행해야 할 작업 지정
struct Book {
    title: String,
}

impl Drop for Book {
    // Drop트레잇을 구현합니다.
    fn drop(&mut self) {
        println!("Book객체 해제: {}", self.title);
    }
}

fn main() {
    {
        let book = Book { title: String::from("러스트") };
    } // book의 Drop트레잇이 자동으로 호출됩니다.
}

/*실행결과
Book객체 해제: 러스트
*/

2025년 4월 21일 월요일

5-5-2강: 네트워킹과 IPC(채팅 클라이언트)

 1. 간단한 채팅 프로그램 클라이언트 부분.

use std::io::{self, Write}; // 표준 입력/출력 사용
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; // 비동기 읽기/쓰기 및 버퍼 사용
use tokio::net::TcpStream; // TCP 스트림 사용

#[tokio::main] // 비동기 런타임의 진입점
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut username = String::new(); // 사용자 이름을 저장할 변수

    let stream = TcpStream::connect("localhost:20000").await?; // 서버와의 연결을 시도
    let (reader, mut writer) = tokio::io::split(stream); // 읽기와 쓰기 스트림으로 분리
    let mut reader = BufReader::new(reader); // 버퍼를 이용해 읽기 작업 최적화

    print!("대화명을 입력하세요: "); // 사용자에게 대화명 입력 요청
    io::stdout().flush()?; // 출력 버퍼를 즉시 플러시
    io::stdin().read_line(&mut username)?; // 사용자 입력 받기
    writer.write_all(username.as_bytes()).await?; // 서버에 대화명 전송

    // 서버로부터 메시지를 수신하는 비동기 작업
    tokio::spawn(async move {
        loop {
            let mut message = String::new();

            match reader.read_line(&mut message).await { // 서버로부터 메시지 읽기
                Ok(_) => {
                    print!("{}", message); // 메시지 출력
                },
                Err(_) => { // 오류 발생 시 루프 종료
                    break;
                }
            };
        }
    });

    // 사용자가 메시지를 입력하는 메인 루프
    loop {
        let mut input = String::new();
        io::stdin().read_line(&mut input)?; // 사용자로부터 메시지 입력 받기
        writer.write_all(input.as_bytes()).await?; // 서버로 메시지 전송

        if input.trim() == "/exit" { // "/exit" 입력 시 종료
            break;
        }
    }

    Ok(()) // 프로그램 종료
}

/*실행결과
대화명을 입력하세요: hi
hi 님이 입장하셨습니다.
안녕~?
hi: 안녕~?
뭐여?
hi: 뭐여?
/exit */

5-5-1강: 네트워킹과 IPC(채팅 서버)

 1. 간단한 채팅 프로그램 서버 부분.

// 채팅 서비스 서버 부분
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::sync::broadcast;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 비동기 TCP 서버를 localhost:20000 포트에서 바인딩합니다.
    let listener = TcpListener::bind("localhost:20000").await?;

    // broadcast 채널 생성 (tx: 송신자, _: 수신자)
    // 크기는 10이며, 연결된 클라이언트들에게 메시지를 브로드캐스트
    let (tx, _) = broadcast::channel(10);

    // 송신자를 Arc로 감싸서 여러 클라이언트 스레드에서 안전하게 공유
    let shared_tx = Arc::new(tx);

    loop {
        // 클라이언트 접속을 기다리며, 접속이 오면 stream을 받아옵니다.
        let (stream, _) = listener.accept().await?;
        let shared_tx = shared_tx.clone(); // Arc는 clone으로 참조를 복사
        let mut rx = shared_tx.subscribe(); // 새 수신자 생성

        // 클라이언트 한 명에 대해 별도 작업 실행
        tokio::spawn(async move {
            // stream을 읽기와 쓰기로 나눕니다.
            let (reader, mut writer) = tokio::io::split(stream);

            // 다른 유저가 보낸 메시지를 수신해서 현재 유저에게 출력
            tokio::spawn(async move {
                loop {
                    // broadcast로부터 메시지 수신
                    let data: String = match rx.recv().await {
                        Ok(data) => data,
                        Err(_) => return, // 실패하면 종료
                    };

                    if data == "/exit" {
                        break; // 종료 명령
                    }

                    print!("{}", data); // 서버 콘솔 출력 (선택사항)
                    match writer.write_all(data.as_bytes()).await {
                        Ok(_) => {},
                        Err(err) => {
                            println!("네트워크 오류: {:?}", err);
                            return;
                        }
                    };
                }
            });

            // 클라이언트에서 보내는 메시지를 받기 위한 BufReader 생성
            let mut buf_reader = BufReader::new(reader);
            let mut username = String::new();

            // 첫 줄에 유저 이름이 들어온다고 가정
            buf_reader.read_line(&mut username).await;
            let username = username.trim();

            // 입장 메시지를 전 클라이언트에게 전송
            match shared_tx.send(format!("{} 님이 입장하셨습니다.\n", username)) {
                Ok(_) => {},
                Err(_) => return,
            }

            loop {
                let mut message = String::new();
                // 클라이언트로부터 메시지 수신
                buf_reader.read_line(&mut message).await;

                let mut message = String::from(message.trim());
                if message != "/exit" {
                    // 사용자 이름을 포함하여 메시지를 포맷팅
                    message = format!("{}: {}\n", username, message);
                }

                // 모든 클라이언트에게 메시지 브로드캐스트
                match shared_tx.send(message) {
                    Ok(_) => {},
                    Err(_) => break,
                };
            }

            // 클라이언트 종료 메시지 브로드캐스트
            match shared_tx.send(format!("{} 님이 채팅방을 나갔습니다.\n", username)) {
                Ok(_) => {},
                Err(_) => return,
            }
        });
    }
}

2025년 4월 20일 일요일

5-4-2강: 네트워킹과 IPC(웹서버)

 1. 간단한 웹 서버 만들기

/ 에 접근하면 Hello World 출력하고, 다른 리소스 접근시 404 not found 출력하는 예제

//간단한 웹서버 만들기 예제
/*의존성 추가
[dependencies]
tokio = { version = "1.25.0", features = ["full"] }
hyper = { version = "0.14", features = ["full"] }
 */
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode};

// 에러 처리를 위한 타입 정의
type GenericError = Box<dyn std::error::Error + Send + Sync>;
type Result<T> = std::result::Result<T, GenericError>;

// 클라이언트의 요청에 따라 적절한 응답을 반환하는 비동기 함수
async fn response_examples(req: Request<Body>) -> Result<Response<Body>> {
    // 기본 페이지 내용
    let index_html = String::from("<h1>Hello World!</h>");
    // 없는 페이지 요청 시 출력할 내용
    let notfound_html = String::from("<h1>404 not found</h>");

    // HTTP 메서드(GET)와 요청 경로(/)에 따라 응답 처리
    match (req.method(), req.uri().path()) {
        // GET / 요청이면 index 페이지 반환
        (&Method::GET, "/") => Ok(Response::new(index_html.into())),
        _ => {
            // 그 외의 요청은 404 Not Found 응답
            Ok(Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(notfound_html.into())
                .unwrap())
        }
    }
}

// 비동기 main 함수로 서버 구동
#[tokio::main]
async fn main() -> Result<()> {
    // 바인딩할 IP 주소와 포트
    let addr = "127.0.0.1:20000".parse().unwrap();

    // 클라이언트 연결이 생길 때마다 서비스 인스턴스를 생성하는 함수
    let new_service = make_service_fn(move |_| {
        async {
            Ok::<_, GenericError>(service_fn(move |req| {
                // 요청이 오면 response_examples 함수로 처리
                response_examples(req)
            }))
        }
    });

    // 서버를 해당 주소에서 실행
    let server = Server::bind(&addr).serve(new_service);
    println!("Listening on http://{}", addr); // 서버 실행 로그 출력
    server.await?; // 서버 실행을 대기
    Ok(())
}

/* 실행결과
127.0.0.1:20000/ 에 접속시  Hello World! 출력
그 외 에는 404 not found 출력
*/

2025년 4월 18일 금요일

5-4-1강: 네트워킹과 IPC(HTTP, REST API)

 1. 간단한 HTTP 클라이언트 만들기

/*의존성 추가
[dependencies]
tokio = { version = "1.25.0", features = ["full"] }
hyper = { version = "0.14", features = ["full"] } */
use hyper::{body::HttpBody as _, Client};
use tokio::io::{stdout, AsyncWriteExt as _};

#[tokio::main]
async fn main() {
    let client = Client::new();
    // 외부 ip 조회하는 사이트
    let uri = "http://httpbin.org/ip".parse().unwrap();

    // http 요청을 보낸다.
    let mut resp = client.get(uri).await.unwrap();
    println!("Response: {}", resp.status());
   
    // 응답 온 body 값을 확인 한다.
    while let Some(chunk) = resp.body_mut().data().await {
        stdout().write_all(&chunk.unwrap()).await.unwrap();
    }
}
/*실행결과
Response: 200 OK
{
  "origin": "154.20.54.2"
}
*/

2. REST API 사용하기

REST API 란? 

HTTP 프로토콜 기반의 API, 웹서비스 <-> 클라이언트 간 데이터 통신하는 방법

GET, POST, PUT, DELETE 등으로 외부 자원에 접근

/*의존성 추가
[dependencies]
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
tokio = { version = "1.25.0", features = ["full"] }
hyper = { version = "0.14", features = ["full"] } */

use hyper::body::Buf;
use hyper::Client;
// JSON 파싱을 위한 Deserialize trait 사용
use serde::Deserialize;

// JSON 데이터를 담을 구조체 정의 (역직렬화 대상)
#[derive(Deserialize, Debug)]
struct User {
    id: i32,         // 사용자 ID
    name: String,    // 사용자 이름
}

// tokio의 비동기 런타임에서 main 함수 정의
#[tokio::main]
async fn main() {
    // API URL 문자열을 URL 객체로 파싱
    let url = "http://jsonplaceholder.typicode.com/users".parse().unwrap();
   
    // HTTP 클라이언트 생성
    let client = Client::new();

    // GET 요청 보내고 응답을 기다림
    let res = client.get(url).await.unwrap();

    // 응답 body 전체를 메모리에 모아서 가져옴
    //(aggregate는 비동기적으로 전체 body 수신)
    let body = hyper::body::aggregate(res).await.unwrap();

    // JSON 데이터를 파싱해서 Vec<User>로 변환
    //(reader는 Buf를 읽을 수 있는 std::io::Read처럼 사용)
    let users: Vec<User> = serde_json::from_reader(body.reader()).unwrap();
   
    // 파싱된 결과를 보기 좋게 출력
    println!("users: {:#?}", users);
}
/*실행결과
users: [
    User {
        id: 1,
        name: "Leanne Graham",
    },
    User {
        id: 2,
        name: "Ervin Howell",
    },
    ... 생략...
] */

5-3-3강: SQLite 사용하기

 SQLite 를 사용하는 간단한 예제

/* SQLite 의존성 추가
[dependencies]
sqlite = "0.30" */
use sqlite;
use sqlite::State;

fn main() {
    // 메모리에 sqlite db 생성
    let connection = sqlite::open(":memory:").unwrap();

    // users 테이블 만들고, 2개 데이터 삽입
    let query = "
        CREATE TABLE users (name TEXT, age INTEGER);
        INSERT INTO users VALUES ('루나', 3);
        INSERT INTO users VALUES ('러스트', 13);
    ";
    // 테이블 생성 쿼리를 실행
    connection.execute(query).unwrap();

    // ?는 나중에 이 자리에 값을 넣을꺼야 표시
    // SQL 인젝션을 막기 위해 사용
    let query = "SELECT * FROM users WHERE age > ?";
   
    // 쿼리를 실행
    let mut statement = connection.prepare(query).unwrap();

    // (1, 5) → 1은 첫 번째 파라미터 위치 (?)를 의미
    // 5는 ?에 들어갈 값
    statement.bind((1, 5)).unwrap(); // age > 5
   
    // 테이블의 데이터를 조회
    while let Ok(State::Row) = statement.next() {
        println!("name = {}", statement.read::<String, _>("name").unwrap());
        println!("age = {}", statement.read::<i64, _>("age").unwrap());
    }
}

/*실행결과
name = 러스트
age = 13 */

5-3-2강: 파일 입출력(데이터 버퍼링, 데이터 직렬화)

 1. 데이터 버퍼링이란?

데이터를 주고 받는 동안 일시적으로 데이터를 버퍼에 저장하는 것

// 데이터 버퍼링 예제
use std::fs::File;
use std::io::{BufRead, BufReader};

/*input.txt 내용
hello world
i am ferris!
haha...
*/

fn main() {
    let file = File::open("input.txt").unwrap();
    // BufReader 생성
    let reader = BufReader::new(file);

    // File 을 읽는다.
    for line in reader.lines() {
        let line = line.unwrap();
        println!("{}", line);
    }
}
/*실행결과
hello world
i am ferris!
haha...  */

2.데이터 직렬화란?

자료의 상태를 저장하기 위해 바이트 스트림으로 변환하는 것

직렬화된 데이터는  파일, 네트워크, 메모리에 저장할 수 있다.

반대로 원래의 형태로 복원하는 것을 역 직렬화 라고 한다.

예제는 Serde 를 사용해서 json 파일로 저장하고 복원한다.

// 데이터 직렬화 예제
/* serde, serde_json 를 사용하려면 의존성 추가 해야 한다.
[dependencies]
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
*/

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let pt = Point { x: 10, y: 20 };
    //pt 를 json 형식으로 변환한다.
    let json = serde_json::to_string(&pt).unwrap();
    println!("json: {}", json);

    // json을 사용해서 Point를 생성한다.
    let pt: Point = serde_json::from_str(&json).unwrap();
    println!("point: [{}, {}]", pt.x, pt.y);
}

/*실행결과
json: {"x":10,"y":20}
point: [10, 20] */

5-3-1강: 파일 입출력(동기식, 비동기식)

 1. 동기식 파일 입출력 이란?

동기식 입출력 방식은 작업이 완료될 때 까지 무한정 기다린다.

입출력이 완료될 때까지 기다리니 CPU 자원을 비효율적으로 사용한다.

// 동기식 입출력 방식의 예제
use std::fs::File;
use std::io::{Read, Write};

fn main() {
    let mut file = File::open("input.txt").unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
    // 파일을 읽을때 까지 대기합니다.

    println!("{}", contents);

    let mut file = File::create("output.txt").unwrap();
    file.write_all(contents.as_bytes()).unwrap();
    // 파일을 쓸때 까지 대기합니다.
}

2. 비동기식 파일 입출력이란?
비동기식 입출력 방식은 메인 스레드를 멈추지 않기에
CPU 자원을 효율적으로 사용한다.

/*cargo.toml에 의존성 추가
[dependencies]
tokio = { version = "1.25.0", features = ["full"] } */
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() {
    // 비동기 방식으로 file 핸들러를 얻는다.
    //input.txt 엔 hello world 가 입력되어 있다.
    let mut file = File::open("input.txt").await.unwrap();
    let mut contents = String::new();

    // 비동기 방식으로 file 읽기
    file.read_to_string(&mut contents).await.unwrap();

    // input.txt의 내용을 출력
    println!("{}", contents);

    //비동기 방식으로 file 생성
    let mut file = File::create("output.txt").await.unwrap();
    //비동기 방식으로 file 저장 (input.txt에 있는 hello world가 output.txt에 저장된다.)
    file.write_all(contents.as_bytes()).await.unwrap();
}

// 비동기 방식으로 구현한 이벤트 루프 예제
/* 의존성 추가
[dependencies]
tokio = { version = "1.25.0", features = ["full"] }
*/
use tokio::io::{stdin, BufReader, AsyncBufReadExt};
use tokio::fs::File;

#[tokio::main]
async fn main() {
    let mut reader = BufReader::new(stdin());
    let mut lines = reader.lines();

    loop { // quit가 입력될때 까지 입력을 받음
        match lines.next_line().await.unwrap() {
            Some(input) => {
                println!("입력: {}", input);
       
                if input == "quit" {
                    break;
                }
            }
            None => {
                break;
            },
        };
    }
}