Web 접속 방법 Rust, Python 비교

러스트의 경우 처음에 명령 프롬프트 창에서 포트를 열어놓고 Cargo run을 하라고 하여 Python은 포트를 열 필요없이 잘 접속되는데 해서 비교하게 되었습니다. 그리고, 보면 파이썬은 편리한 상태를 만들어 놓았는데, Rust는 처음부터 내가 만들어가야 하는 상황입니다.

1. Rust를 이용한 웹 접속

가. 명령 프롬프트 방식

(1) 명령 프롬프트에서 프트 열기

chromedriver –port=9515를 실행해서 Chromedriver를 실행시킵니다.

명령 프롬프트에서 9515 포트 열기

Chromedriver가 Path에 있다면 그냥 실행하면 되는데, path가 설정되어 있지 않아, chromedriver.exe가 있는 폴더로 이동해서 실행했습니다.

(2) 브라우저 열기 Rust 코드

(가) Cargo.toml
[dependencies]
thirtyfour = "0.36.1"
tokio = { version = "1", features = ["full"] }

thirtyfour와 tokio 라이브러리를 가져와야 합니다.

(나) main.rs
use thirtyfour::prelude::*;
use tokio;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    // 이미 chromedriver가 9515 포트에서 실행 중이라고 가정
    let driver = WebDriver::new("http://localhost:9515", DesiredCapabilities::chrome()).await?;

    // 페이지 접속
    driver.get("https://www.rust-lang.org").await?;

    // 타이틀 가져오기
    let title = driver.title().await?;
    println!("Page title: {}", title);

    // 종료
    driver.quit().await?;
    Ok(())
}

코드이 내용은 port가 열려 있기 때문에 다시 열 필요는 없고, 9515 포트로 driver를 설정한 다음, rust-lang.org에 접속한 후 title을 가져와서 화면에 출력하는 것입니다.

문제 없이 코드가 실행되고, title이 표시됩니다.

cargo run을 이용해 rust-lang-org에 접속한 후 타이틀을 가져와 화면에 출력

나. 코드로 포트 열기 방식

명령 프롬프트에서 포트를 연 다음 Cargo run을 한다는 것이 이상하므로 Rust에서 포트를 열고, 실행하려면 아래와 같이 코드를 작성하면 됩니다.

Cargo.toml은 동일하고,

main.rs만 아래와 같이 수정하면 됩니다.

use std::process::Command;
use thirtyfour::prelude::*;
use tokio;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    // 실행파일이 있는 디렉토리 경로
    let exe_dir = std::env::current_exe()
        .unwrap()
        .parent()
        .unwrap()
        .to_path_buf();
    let chromedriver_path = exe_dir.join("chromedriver.exe");
    println!("chromedriver 경로: {}", chromedriver_path.display());

    // chromedriver 실행
    let mut child = Command::new(&chromedriver_path)
        .arg("--port=9515")
        .spawn()
        .expect("chromedriver 실행 실패");

    // WebDriver 클라이언트 연결
    let caps = DesiredCapabilities::chrome();
    let driver = WebDriver::new("http://localhost:9515", caps).await?;

    driver.get("https://www.rust-lang.org").await?;
    println!("현재 페이지 타이틀: {}", driver.title().await?);

    // 브라우저 닫기
    driver.quit().await?;
    // chromedriver 프로세스 종료
    child.kill().ok();

    Ok(())
}

use std::process::Command;가 추가되었습니다.

main에서 먼저 chromedriver_path를 설정하고,

Command::new를 이용해 포트를 연 다음 child에 저장하고,

caps는 python Selenium에서 사용하는 ChromeOptions 역할입니다. 기본적인 옵션만 설정하는 것입니다. caps.add_arg(“–headless”)?; 등을 추가해서 옵션을 추가할 수 있습니다.

그리고, WebDriver::new로 http://localhost:9515라고 9515포트를 이용해 localhost를 연 다음

driver.get로 https://www.rust-lang.org를 연 다음

driver.title().await?로 title을 가져와서 화면에 출력합니다.

아래는 Visual Studio Code에서 Run한 장면입니다.

Compile과 실행 잘 되고, ChromeDriver was started successfully on port 9515.와

현재 페이지 타이틀 : Rust Programming Language라고 잘 나옵니다.

9515 포트 열기와 rust-lang.org에 접속해서 title 가져오기를 통합한 실행 화면

2. Python을 이용한 웹 접속

가. chrome_connect.py

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
import os

def main():
    # 실행 파일이 있는 디렉토리 기준으로 chromedriver.exe 찾기
    exe_dir = os.path.dirname(os.path.abspath(__file__))
    chromedriver_path = os.path.join(exe_dir, "chromedriver.exe")

    # chromedriver.exe 고정 경로
    # chromedriver_path = r"C:\android\chromedriver.exe"
    # print(f"chromedriver 경로: {chromedriver_path}")

    # chromedriver 실행 (포트 지정 없이 내부적으로 관리)
    service = Service(chromedriver_path)
    options = webdriver.ChromeOptions()

    driver = webdriver.Chrome(service=service, options=options)

    try:
        driver.get("https://www.rust-lang.org")
        print(f"현재 페이지 타이틀: {driver.title}")
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

Python은 Cargo.toml과 같은 설정이 필수가 아니고,

바로 python code를 위와 같이 작성하면 됩니다.

실행 결과는 아래와 같습니다.

파이썬으로 chromedriver를 실행한 후 rust-lang.org의 타이틀을 화면에 출력

나. 코드 내용

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
import os

필요한 selenium과 os 라이브러리를 불러옵니다.

    # 실행 파일이 있는 디렉토리 기준으로 chromedriver.exe 찾기
    exe_dir = os.path.dirname(os.path.abspath(__file__))
    chromedriver_path = os.path.join(exe_dir, "chromedriver.exe")

    # chromedriver.exe 고정 경로
    # chromedriver_path = r"C:\android\chromedriver.exe"


실행 파일이 있는 폴더의 chromedriver.exe를 chromedriver_path로 설정합니다.

주석 처리한 것처럼 고정 경로로 지정할 수도 있습니다. 여러 프로그램에서 공통적으로 사용할 수 있으므로 PC에서만 작업한다면 이것이 편할 수 있습니다.

print(f"chromedriver 경로: {chromedriver_path}")

chromedriver_path를 화면에 출력합니다.

    # chromedriver 실행 (포트 지정 없이 내부적으로 관리)
service = Service(chromedriver_path)
options = webdriver.ChromeOptions()

Service 메소드를 이용해 service를 생성하고, webdriver.ChromeOptions()로 크롬 설정을 기본으로 합니다.

driver = webdriver.Chrome(service=service, options=options)

service와 options 설정으로 크롬을 열고, driver 객체에 담습니다.

    try:
driver.get("https://www.rust-lang.org")
print(f"현재 페이지 타이틀: {driver.title}")
finally:
driver.quit()

www.rust-lang.org 열기를 시도해서 성공하면 페이지의 타이틀을 driver.title로 가져와서 화면에 표시합니다.

그리고, 크롬을 종료합니다.

3. 러스트와 파이썬 비교

Rust는 Port를 반드시 지정해야 하는데, Python은 selenium을 이용하기 때문에 포트를 지정할 필요가 없는 차이점이 있습니다.

자세히 말하면 Rust의 thirtyfour는 W3C WebDriver 프로토콜을 따르는 라이브러리라서, chromedriver.exe를 서버처럼 띄우고 http://localhost:9515 같은 포트를 통한 HTTP 요청으로 제어하는데 비해

Python의 selenium은 내부적으로 chromedriver.exe를 subprocess로 실행하고, 외부에서 포트 번호를 직접 지정할 필요 없이 Selenium이 알아서 관리합니다.

4. 실행 파일 사이즈

pyinstaller -F -w chrome_connect.py로 실행한 후 파일 사이즈를 보면

18메가 정도되는데,

cargo run으로 생성한 실행 파일 크기는 8메가 정도되는데,

cargo build –release해서 만든 Rust 실행 파일을 보면 4메가 정도로 파이썬 18메가의 22%뿐이 안됩니다.

속도도 미세하지만 Rust가 빠른 듯 합니다.

Rust 까다롭고, 생소한 측면이 너무 많지만 좋기때문에 자꾸 익히고 적응해나가야 하겠습니다.

Yahoo Finance에서 주식 정보 가져오기 (1)

모방은 창조의 어머니인가요? claude.ai의 도움을 받아 만든 “Yahoo Finace API를 이용해서 주식 데이터를 가져오는 프로그램”을 살펴보겠습니다. Rust는 먼저 Cargo.toml 파일에서 가져올 크레이트(라이브러리)를 정의하고, main.rs에서 실행 코드를 구현합니다.

1. Cargo.toml

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }

dependencies 섹션에 reqwest, serde, serde_json, tokio 크레이트(crate, library)를 버전과 features를 이용해 지정합니다.

가. reqwest

request가 맞는 단어인데, reqwest로 약간 다른 점 주의해야 합니다.
reqwest 크레이트는 편리하고 높은 수준의 HTTP 클라이언트를 제공합니다.

version은 크레이트의 버전을 지정하는 것은 알겠는데, features는 크레이트의 특정 기능(선택적 기능)을 활성화하거나 비활성화할 때 사용되며, 조건부 컴파일을 가능하게 하여, 필요한 기능만 컴파일하도록 하는 것입니다.

features = [“json”]은 “json” 기능을 켜서 JSON 직렬화/역직렬화 기능을 사용할 수 있게 하는 것입니다.

아래와 같이 버전 목록이 표시되는데, 맨 위에 0.12.23이 있으므로 클릭합니다.

그러면 버전이 자동으로 변경되고, X표시가 없어지고, 녹색 체크 표시로 바뀝니다.

나. serde

데이터 직렬화(Serialize) / 역직렬화(Deserialize) 라이브러리입니다.
JSON, TOML, YAML 등 다양한 포맷과 Rust 구조체를 변환할 때 사용하며,

features = [“derive”]는

[derive(Serialize, Deserialize)] 어트리뷰트를 쓸 수 있게 해주는 것입니다.

다. serde_json

serde의 JSON 전용 확장판으로서, Rust 데이터와 JSON 문자열간에 변환을 가능하게 해줍니다.

let user = User { name: "Kim".into(), age: 30 };
let json_str = serde_json::to_string(&user)?; // 구조체 → JSON 문자열
let parsed: User = serde_json::from_str(&json_str)?; // JSON → 구조체

라. tokio

Rust의 비동기 런타임 (async/await 동작을 실제로 수행하는 엔진)으로서,
features = [“full”]은 모든 기능(네트워킹, 파일 I/O, 타이머 등)을 한 번에 활성화하는 것이며,
#[tokio::main] 매크로로 main 함수를 비동기로 만들 수 있습니다.

2. main.rs

가. 코드 1

아래 코드를 src 폴더의 main.rs를 연 후 Ctrl + A해서 전체를 선택한 후 Ctrl + V를 하면 기존 내용에 덮어씌워집니다.

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
    chart: Chart,
}

#[derive(Debug, Deserialize, Serialize)]
struct Chart {
    result: Vec<ChartResult>,
    error: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize, Serialize)]
struct ChartResult {
    meta: Meta,
}

#[derive(Debug, Deserialize, Serialize)]
struct Meta {
    currency: String,
    symbol: String,
    #[serde(rename = "longName")]
    long_name: Option<String>,
    #[serde(rename = "regularMarketPrice")]
    regular_market_price: Option<f64>,
    #[serde(rename = "regularMarketTime")]
    regular_market_time: Option<i64>,
}

#[derive(Debug, Clone)]
struct StockData {
    symbol: String,
    long_name: String,
    regular_market_price: f64,
    currency: String,
    regular_market_time: i64,
}

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
    let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);
    
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .send()
        .await?;

    let text = response.text().await?;
    let yahoo_response: YahooResponse = serde_json::from_str(&text)?;

    if let Some(result) = yahoo_response.chart.result.first() {
        let meta = &result.meta;
        
        Ok(StockData {
            symbol: symbol.to_string(),
            long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
            regular_market_price: meta.regular_market_price.unwrap_or(0.0),
            currency: meta.currency.clone(),
            regular_market_time: meta.regular_market_time.unwrap_or(0),
        })
    } else {
        Err(format!("No data found for symbol: {}", symbol).into())
    }
}

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
    let mut handles = Vec::new();
    
    for &symbol in symbols {
        let symbol_owned = symbol.to_string();
        let handle = tokio::spawn(async move {
            fetch_stock_data(&symbol_owned).await
        });
        handles.push(handle);
    }
    
    let mut results = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(result) => results.push(result),
            Err(e) => results.push(Err(e.into())),
        }
    }
    
    results
}

fn format_timestamp(timestamp: i64) -> String {
    use std::time::{UNIX_EPOCH, Duration};
    
    let datetime = UNIX_EPOCH + Duration::from_secs(timestamp as u64);
    
    // 간단한 포맷팅 (실제로는 chrono 크레이트 사용 권장)
    format!("Unix timestamp: {}", timestamp)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let symbols = [
        "005930.KS", // 삼성전자
        "AAPL",      // Apple
        "TSLA",      // Tesla
        "LIT",       // Global X Lithium & Battery Tech ETF
        "BRK-B",     // Berkshire Hathaway Class B
        "AMZN",      // Amazon
        "O",         // Realty Income Corporation
        "TQQQ",      // ProShares UltraPro QQQ
        "XOM",       // Exxon Mobil
        "WMT",       // Walmart
    ];

    println!("Fetching stock data...\n");
    
    let results = fetch_multiple_stocks(&symbols).await;
    
    for (i, result) in results.iter().enumerate() {
        match result {
            Ok(stock) => {
                println!("Symbol: {}", stock.symbol);
                println!("Long Name: {}", stock.long_name);
                println!("Regular Market Price: {:.2} {}", stock.regular_market_price, stock.currency);
                println!("Currency: {}", stock.currency);
                println!("Regular Market Time: {}", format_timestamp(stock.regular_market_time));
                println!("---");
            }
            Err(e) => {
                println!("Error fetching data for {}: {}", symbols[i], e);
                println!("---");
            }
        }
    }

    Ok(())
}

cargo run을 하면 compile과 build를 한 후

Run을 하는데, 주식 정보 조회 결과를 주식별로 하나씩 보여주고, 시간이 Unix timestampt로 보여줘서 날짜와 시간을 알 수 없습니다.

나 코드 2

그래서 엑셀 처럼 표 형태로 보여주고, Unix time을 년월일시로 바꿔달라고 했더니 아래 코드가 되는데,

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;
use chrono::{DateTime, Utc, TimeZone};

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
    chart: Chart,
}

#[derive(Debug, Deserialize, Serialize)]
struct Chart {
    result: Vec<ChartResult>,
    error: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize, Serialize)]
struct ChartResult {
    meta: Meta,
}

#[derive(Debug, Deserialize, Serialize)]
struct Meta {
    currency: String,
    symbol: String,
    #[serde(rename = "longName")]
    long_name: Option<String>,
    #[serde(rename = "regularMarketPrice")]
    regular_market_price: Option<f64>,
    #[serde(rename = "regularMarketTime")]
    regular_market_time: Option<i64>,
}

#[derive(Debug, Clone)]
struct StockData {
    symbol: String,
    long_name: String,
    regular_market_price: f64,
    currency: String,
    regular_market_time: i64,
}

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
    let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);
    
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .send()
        .await?;

    let text = response.text().await?;
    let yahoo_response: YahooResponse = serde_json::from_str(&text)?;

    if let Some(result) = yahoo_response.chart.result.first() {
        let meta = &result.meta;
        
        Ok(StockData {
            symbol: symbol.to_string(),
            long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
            regular_market_price: meta.regular_market_price.unwrap_or(0.0),
            currency: meta.currency.clone(),
            regular_market_time: meta.regular_market_time.unwrap_or(0),
        })
    } else {
        Err(format!("No data found for symbol: {}", symbol).into())
    }
}

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
    let mut handles = Vec::new();
    
    for &symbol in symbols {
        let symbol_owned = symbol.to_string();
        let handle = tokio::spawn(async move {
            fetch_stock_data(&symbol_owned).await
        });
        handles.push(handle);
    }
    
    let mut results = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(result) => results.push(result),
            Err(e) => results.push(Err(e.into())),
        }
    }
    
    results
}

fn format_timestamp(timestamp: i64) -> String {
    match Utc.timestamp_opt(timestamp, 0) {
        chrono::LocalResult::Single(datetime) => datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
        _ => "Invalid timestamp".to_string(),
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let symbols = [
        "005930.KS", // 삼성전자
        "AAPL",      // Apple
        "TSLA",      // Tesla
        "LIT",       // Global X Lithium & Battery Tech ETF
        "BRK-B",     // Berkshire Hathaway Class B
        "AMZN",      // Amazon
        "O",         // Realty Income Corporation
        "TQQQ",      // ProShares UltraPro QQQ
        "XOM",       // Exxon Mobil
        "WMT",       // Walmart
    ];

    println!("Fetching stock data...\n");
    
    let results = fetch_multiple_stocks(&symbols).await;
    
    // 테이블 헤더 출력
    println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}", 
             "No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
    println!("{}", "-".repeat(95));
    
    for (i, result) in results.iter().enumerate() {
        let row_number = i + 1;
        match result {
            Ok(stock) => {
                println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}", 
                         row_number,
                         stock.symbol,
                         if stock.long_name.len() > 35 {
                             format!("{}...", &stock.long_name[..32])
                         } else {
                             stock.long_name.clone()
                         },
                         stock.regular_market_price,
                         stock.currency,
                         format_timestamp(stock.regular_market_time)
                );
            }
            Err(e) => {
                println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}", 
                         row_number,
                         symbols[i],
                         "Error fetching data",
                         "N/A",
                         "N/A",
                         "N/A"
                );
            }
        }
    }

    Ok(())
}

앞 부분에 use chrono::{DateTime, Utc, TimeZone};이 있어서

먼저 Cargo.toml에 chrono 크레이트를 추가해야 합니다.

chrono 버전에 커서를 갖다대보니 버전이 0.4.41로 표시되는데, 0.4 버전대이므로 그대로 둬도 문제 없습니다.

Cargo.toml과 main.rs를 저장하고, 실행하면

표 형태로 잘 표시되고, 날짜도 연월일 시분초로 잘 표시됩니다.

다음 편에서는 main.rs 코드를 하나씩 살펴보겠습니다.

Rust 프로젝트 구조 및 배포

Rust 프로젝트가 커짐에 따라 모듈화와 구조화, 그리고 최종적으로 바이너리 또는 라이브러리로 배포하는 과정이 중요해집니다. 이번에는 Rust 프로젝트를 어떻게 구성하고, 빌드하며, 배포하는지에 대해 알아보겠습니다.


1. Rust 프로젝트 구조

Rust는 기본적으로 cargo를 사용하여 프로젝트를 관리합니다. 일반적인 프로젝트 구조는 다음과 같습니다:

my_project/
├── Cargo.toml
├── Cargo.lock
├── src/
│ ├── main.rs
│ └── lib.rs
├── tests/
│ └── integration_test.rs
├── benches/
│ └── benchmark.rs
└── examples/
└── hello.rs

✅ 주요 구성 요소 설명

  • Cargo.toml: 프로젝트의 메타데이터, 의존성 등을 관리하는 설정 파일입니다.
  • src/main.rs: 실행 가능한 바이너리 프로그램의 진입점입니다.
  • src/lib.rs: 라이브러리 crate의 코드가 들어가는 곳입니다. 모듈화된 코드 작성 시 여기에 구현합니다.
  • tests/: 통합 테스트를 위한 디렉터리입니다.
  • benches/: 성능 벤치마크용 테스트가 위치합니다.
  • examples/: 사용 예제를 담는 폴더로, cargo run –example 예제이름으로 실행할 수 있습니다.

2. 모듈과 라이브러리 구조

기본적인 모듈 구성

my_project/
├── Cargo.toml
└── src/
├── main.rs // 실행 가능한 바이너리 프로그램
├── lib.rs // 라이브러리로 정의된 코드
└── utils.rs // lib.rs에서 공개하는 하위 모듈

프로젝트명은 my_project이고, 그 아래 Cargo.toml과 src 폴더가 있고,
src 폴더에 main.rs, lib.rs, utils.rs가 있습니다.

  • main.rs는 fn main()을 통해 프로그램을 실행하는 진입점(entry point)
  • lib.rs는 모듈과 함수들을 정의하고 외부에 노출하는 라이브러리
  • utils.rs는 lib의 서브 모듈로 기능 분리용

가. utils.rs

pub fn add(a: i32, b: i32) -> i32 {
a + b
}
  • add 함수는 두 정수를 더해서 반환하는 간단한 유틸 함수입니다.
  • pub으로 선언했기 때문에 이 함수는 외부 모듈 (lib.rs나 main.rs)에서도 사용할 수 있습니다.

나. lib.rs

pub mod utils;

pub fn greet(name: &str) {
println!("Hello, {}!", name);
}
  • pub mod utils;
    → src/utils.rs 파일을 모듈로 포함함.
    utils 모듈이 공개(pub)되었기 때문에 외부에서 사용 가능.
  • pub fn greet(name: &str)
    → greet 함수도 공개되어 있어서 외부 crate나 main.rs에서 사용할 수 있음.

다. main.rs

use my_project::greet;
use my_project::utils::add;

fn main() {
greet("Rust");
println!("2 + 3 = {}", add(2, 3));
}
  • use my_project::greet;
    → lib.rs에서 정의된 greet 함수를 가져와 사용함.
  • use my_project::utils::add;
    → lib.rs가 공개한 utils 모듈을 통해 add 함수를 가져옴.
  • main() 함수에서는
    greet(“Rust”)를 호출해서 Hello, Rust!를 출력하고,
    add(2, 3)의 결과 5를 출력합니다.

라. Cargo 관점에서

Cargo.toml에는 특별한 설정 없이도 다음이 자동 처리됨:

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
  • lib.rs는 my_project라는 이름의 라이브러리 크레이트로 인식됨
  • main.rs는 실행 파일용 바이너리 크레이트로 인식됨

따라서 main.rs에서 my_project::를 통해 lib.rs의 함수와 모듈을 가져올 수 있는 것임.


마. 빌드 및 실행

cargo run

실행하면 아래와 같이 출력됩니다.

Hello, Rust!
2 + 3 = 5

사. 요약

파일역할주요 내용
lib.rs라이브러리 크레이트greet 함수 정의, utils 모듈 공개
utils.rs하위 모듈add 함수 정의
main.rs실행 크레이트greet와 add 호출하여 메시지 출력

이 구조는 라이브러리를 재사용하거나 다른 프로젝트에 공유하고 싶을 때 매우 적합하며, 기능 분리모듈화에도 강력한 패턴입니다.


3. 패키지와 워크스페이스

여러 crate를 하나의 프로젝트로 묶고싶다면 workspace 기능을 사용할 수 있습니다.

[workspace]
members = [
"core_lib",
"cli_tool",
]

이 구조에서는 core_lib는 공통 로직을 담고, cli_tool은 실행 바이너리를 담당할 수 있습니다. 모노레포(Monorepo) 방식으로 관리하기에 유리합니다.

Cargo.toml 파일에 [workspace] 섹션을 추가하여 workspace를 정의하는데, members 필드에 각 하위 프로젝트의 경로를 지정합니다.

모노레포는 멀티레포와 대비되는 개념으로, 여러 하위 프로젝트들을 하나의 저장소에서 관리하는 것을 말하며, 이렇게 하면 코드 공유, 재사용, 일관된 빌드 및 테스트 설정 등을 효율적으로 관리할 수 있습니다. 일반적으로 Cargo를 사용하여 모노레포를 구성합니다. 
* 레포는 Repository의 약어

4. 빌드 및 최적화

기본 빌드

cargo build
  • 기본 빌드 모드에서는 target/debug/에 실행 파일이 생성됩니다.

릴리즈 빌드 (최적화 포함)

cargo build --release
  • 릴리즈 모드에서는 최적화가 적용되며, target/release/에 실행 파일이 생성됩니다.
  • 실제 배포 시에는 릴리즈 모드를 사용하는 것이 일반적입니다.

5. 바이너리 배포

Rust는 정적 바이너리 생성을 기본으로 하므로, 종속성 없는 실행 파일을 만들 수 있습니다.

가. 윈도우/리눅스/맥용 빌드

# 리눅스용으로 크로스 컴파일
cargo build --release --target x86_64-unknown-linux-gnu

# 윈도우용
cargo build --release --target x86_64-pc-windows-msvc

cargo build –release –target x86_64-unknown-linux-gnu실행 시
note: the x86_64-unknown-linux-gnu target may not be installed
라고 표시되면
rustup target add x86_64-unknown-linux-gnu를 통해 target을 먼저 설치해야 합니다.

나. 크로스 컴파일

  • cross 크레이트를 사용하면 플랫폼별 크로스 컴파일을 간편하게 할 수 있습니다.
    cargo install cross
    cross build –target x86_64-unknown-linux-gnu –release
  • 위 명령어를 실행하면 docker 또는 podman이 설치되어 있는지 묻습니다. 따라서, docker 등을 설치하고 그 안에서 위 명령을 실행해야 합니다.

다. 실행 파일 위치

  • 기본 빌드: target/debug/프로젝트명
  • 릴리즈 빌드: target/release/프로젝트명

이 파일을 원하는 디렉토리나 서버로 옮기면 됩니다.


6. Crates.io에 라이브러리 배포

만약 자신이 만든 라이브러리를 다른 사람들과 공유하고 싶다면, crates.io에 등록할 수 있습니다.

가. 준비 단계

  • https://crates.io에 계정을 만들고 API Token을 생성합니다.
  • ~/.cargo/credentials에 토큰을 저장합니다.
  • Cargo.toml에 필수 정보를 입력합니다:
[package]
name = "my_crate"
version = "0.1.0"
authors = ["홍길동 <email@example.com>"]
edition = "2021"
license = "MIT OR Apache-2.0"
description = "간단한 수학 라이브러리"
repository = "https://github.com/username/my_crate"

나. 배포 명령

cargo publish
  • 첫 배포 전에는 cargo package로 정상 패키징이 되는지 테스트해보는 것이 좋습니다.

7. CI/CD 통합

CI/CD는 지속적인 통합(Continuous Integration)과 지속적인 제공/배포(Continuous Delivery/Deployment)를 의미하며, 소프트웨어 개발 및 배포 과정을 자동화하여 효율성과 속도를 높이는 방법론입니다. CI/CD는 개발자가 코드 변경 사항을 주기적으로 메인 저장소에 통합하고, 테스트를 거쳐 자동으로 배포될 수 있도록 합니다. 

프로젝트가 커지면 GitHub Actions나 GitLab CI와 같은 CI 도구를 통해 자동으로 테스트와 빌드, 배포까지 설정할 수 있습니다.

예: .github/workflows/rust.yml

name: Rust

on: [push]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- run: cargo build --release
- run: cargo test

8. 마무리

Rust 프로젝트는 명확한 구조와 cargo의 강력한 관리 도구 덕분에 규모가 커져도 효율적으로 관리할 수 있습니다. 프로젝트 구조를 잘 나누고, 최적화된 빌드를 통해 빠르고 안정적인 배포 환경을 만드는 것이 중요합니다.