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

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 use(임포트)와 struct(구조체) 부분의 코드에 대해 알아보겠습니다.

1. use

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

가. use reqwest;

HTTP 클라이언트 라이브러리를 불러오는(import) 기능으로, Yahoo Finance API에서 주식 데이터를 가져오는 HTTP를 요청하기 위해 사용합니다.
예시: reqwest::get(“https://api.example.com”)

Cargo.toml에서는 dependency를 선언한 것이고, 사용하려면 다시 use를 해야 합니다.

나. use serde::{Deserialize, Serialize};

직렬화/역직렬화 라이브러리인 serde를 import 하는 구문으로, ::{Deserialize, Serialize}는 라이브러리의 전체를 가져오는 것이 아니라 Deserialize와 Serialize 트레이트(trait)만 가져오는 것입니다.

JSON 데이터를 Rust 구조체로 변환하거나, 그 반대 역확을 하며, 본 코드에서는 Yahoo API의 JSON 응답을 StockData 구조체로 변환하는데 사용합니다.

dependencies의 serde와 main.rs의 serde의 차이점을 알아보면 아래와 같습니다.

[Cargo.toml의 dependencies]
serde = { version = "1.0", features = ["derive"] }는 serde 크레이트의 추가 기능인 derive를 활성화하는 것으로 #[derive(Serialize, Deserialize)] 매크로를 사용 가능하게 합니다.

[main.rs의 use]
use serde::{Deserialize, Serialize};는 실제로 구조체에 적용할 트레이트인 Deserialize, Serialize 트레이트를 사용하기 위한 것입니다.

[main.rs의 struct 구문]
// derive 기능이 활성화되어서 이 매크로 사용 가능
#[derive(Debug, Deserialize, Serialize)]
struct StockData {
symbol: String,
price: f64,
}

[3단계 구조]
features = ["derive"]로 derive 기능을 활성화하고,
use serde::{...}로 Deserialize, Serialize 트레이트를 가져오고,
#[derive(...)]로 구조체에 실제 매크로를 적용하는 3단계 구조입니다.

다. use std::collections::HashMap;

키-값 쌍을 저장하는 해시맵을 import 하는 것인데, 현재 코드에서는 사용하지 않습니다. 지우고 실행해보니 문제없습니다.

claude.ai가 필요없으니 삭제를 해야 하는데, 삭제를 하지 않았네요.

cargo run을 하니 unused가 HashMap뿐만 아니라 DateTime, e도 있습니다.

use std::collections::HashMap;은 한 줄을 지우고,

use chrono::{DateTime, Utc, TimeZone};에서는 DateTime만 지우고,

error를 의미하는 e는 위 화면의 제안에 따라 _e로 바꾸고 실행하니 문제없이 깔끔하게 실행됩니다.

라. use tokio;

tokio라는 비동기 런타임 라이브러리를 임포트하는 구문으로 async/await를 이용해 여러 주식의 정보를 동시에 병렬로 가져오기가 가능해집니다. 따라서, 10개 주식을 순차적으로 가져오면 10초가 걸리는데, 병렬로 가져오면 1초뿐이 안걸립니다.

마. use chrono::{Utc, TimeZone};

chrono는 날짜/시간 처리 라이브러리로서 Unix 타임스탬프를 사람이 읽기 쉬운 날짜로 변환해줍니다. 예를 들어 1692345600를 “2023-08-18 12:00:00″로 바꿔줍니다.

Unix 타임 스탬프는 UTC 기준으로 1970.1.1부터의 누적된 초이며, UTC는 협정 세계시(Coordinated Universal Time의 약어)로서 그리니치 표준시 (GMT)의 후속 표준이라고 합니다.

2. struct

이 코드는 Yahoo Finance API에서 주식 정보를 받아오기 위한 **데이터 구조(Struct)**를 정의한 것입니다.

serde 라이브러리를 이용해 JSON 데이터 → Rust 구조체 변환(Deserialize)과 반대로 변환(Serialize)을 하기 위해 설계되어 있습니다.

postman 사이트에서 https://query1.finance.yahoo.com/v8/finance/chart/AAPL를 열어보면 아래와 같이 깔끔한 JSON 포맷의 데이터를 보여줍니다.

중괄호 안에 chart(key)가 있고, 그 안에 result와 대괄호(배열)가 있고, 다시 중괄호 다음에 meta가 있으며, 그 안에 우리가 얻고자 하는 currency, symbol, regularMarketPrice 등이 key: Value 쌍으로 담겨져 있습니다. 이에 따라 단계별로 struct를 만듭니다.

가. YahooResponse

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
chart: Chart,
}
  • API 응답 전체를 감싸는 최상위 구조체로서,
  • Yahoo API가 반환하는JSON 최상단에 있는 “chart” 필드를 받기 위해 사용하는데, 데이터형식은 아래 ‘나. Chart’입니다.

나. Chart

struct Chart {
result: Vec<ChartResult>,
error: Option<serde_json::Value>,
}
  • result: 실제 주식 데이터가 담긴 배열이므로 Vec 타입이고, ChartResult 형식의 데이터를 담습니다.
  • error: 에러가 있을 경우 그 내용을 담는 필드로서, 값이 없을 수도 있으니 Option 열거형이며, T값은 serde_json::Value입니다.

다. ChartResult

struct ChartResult {
meta: Meta,
}
  • meta에는 해당 종목의 기본 정보(symbol, regularMartketPrice, currency 등)가 들어 있으며, 데이터 타입은 ‘라. Meta’입니다.

라. Meta

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>,
}
  • 개별 종목의 메타데이터입니다.
  • #[serde(rename = “longName”)]  속성(Attribute)은 JSON의 필드 이름인 long_name을 Rust 필드 이름인 longName으로 바꾸는 역할을 하며,
  • Option을 쓰는 이유는 해당 값이 API 응답에서 없을 수 있기 때문입니다.

마. StockData

#[derive(Debug, Clone)]
struct StockData {
symbol: String,
long_name: String,
regular_market_price: f64,
currency: String,
regular_market_time: i64,
}
  • 실제 사용할 가공된 데이터 구조체로서, Debug와 Clone trait을 자동 구현하며,
  • 위의 Meta에서 필요한 값만 골라와서, 모두 Option 없이 바로 사용 가능한 형태로 변환한 것이며,
  • 프로그램 내부 로직(예: UI 표시, 계산)에서 바로 쓰기 편하도록 만든 것입니다.

Rust의 attribute 종류 및 의미

Rust에서의 attribute(속성)는 컴파일러에게 특정 코드에 대한 추가적인 정보를 제공하여, 코드의 컴파일 방식 또는 동작 방식을 제어하거나, 경고/에러 메시지를 제어하는 데 사용됩니다. derive, allow, warn, cfg, test 등 다양한 종류가 있습니다.

1. attribute의 적용 범위별 종류

Rust의 attribute는 크게 다음과 같은 종류로 나눌 수 있습니다:

종류형태 예시설명
Item attribute#[derive(Debug)]함수, 구조체 등 개별 항목에 적용
Crate attribute#![allow(dead_code)]크레이트 전체에 적용, 보통 파일 상단에 위치
Inner attribute#![cfg(test)]모듈 또는 크레이트 내부에 선언, 내부 항목에 영향을 줌
Outer attribute#[test]특정 항목(함수 등)에만 적용

2. 주요 attribute

가. 컴파일러 관련 attribute

Attribute설명예시
#[allow(…)]특정 경고를 무시#[allow(dead_code)]
#[warn(…)]경고를 표시 (기본값)#[warn(unused_variables)]
#[deny(…)]해당 사항이 있으면 컴파일 에러#[deny(missing_docs)]
#[forbid(…)]deny보다 강하게 재정의 불가#[forbid(unsafe_code)]

나. 파생(derive) 관련 attribute

Rust의 많은 기능은 trait을 자동으로 구현해주는 #[derive(…)]를 통해 사용합니다.

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

Debug, Clone, PartialEq 트레이트를 자동 구현합니다.

다. 조건부 컴파일 관련 attribute

Attribute설명예시
#[cfg(…)]조건에 따라 컴파일 여부 결정#[cfg(target_os = “windows”)]
#[cfg_attr(…)]조건이 참일 때에만 다른 attribute 적용#[cfg_attr(test, derive(Debug))]

[cfg_attr(test, derive(Debug))]는 테스트를 실행할 때만 Debug 트레이트를 자동 구현합니다.

라. 테스트/벤치마크 관련 attribute

Attribute설명예시
#[test]단위 테스트 함수임을 표시#[test]
fn test_add() { … }
#[bench]벤치마크 함수 표시 (불안정)#[bench]
fn bench_sort(…)
#[ignore]테스트 실행 시 무시됨#[test] #[ignore]
fn test_heavy() {}

마. 매크로 관련 attribute

Attribute설명예시
#[macro_use]외부 크레이트의 매크로를 현재 스코프로 불러옴 (Rust 2015 방식)#[macro_use]
extern crate log;
#[proc_macro]프로시저 매크로 정의프로시저 매크로 crate에서 사용됨

바. 기타 유용한 attribute

Attribute설명예시
#[inline], #[inline(always)]함수 인라인화 힌트#[inline(always)]
fn fast_fn() {}
#[repr(C)]구조체 메모리 레이아웃 C와 호환#[repr(C)]
struct MyStruct { … }
#[non_exhaustive]미래 확장을 위해 열거형 등의 exhaustive 검사 방지#[non_exhaustive]
enum MyEnum { … }
#[must_use]반환값을 사용하지 않으면 경고#[must_use]
fn compute() -> i32 { … }

※ 함수를 호출할 때 일반적으로는 함수 본문은 어딘가에 있고 호출하면 그 함수로 점프(jump)해서 실행한 뒤 다시 돌아옵니다. 하지만 인라인화는 이런 점프 없이 함수의 코드를 호출한 곳에 ‘복붙’하듯 직접 삽입하는 방식입니다.

사. #![…]과 #[…] 차이

구분형태설명
Inner attribute#![…]모듈, 크레이트 전체에 적용 (파일 상단에 위치)
Outer attribute#[…]특정 항목(함수, struct 등)에 적용

3. attribute 예제

가. #[derive(…)] — 트레이트 자동 구현

(1) 설명: 구조체(struct)나 열거형(enum)에 대해, 특정 트레이트의 기본 구현을 자동 생성합니다.

(2) 자주 사용되는 트레이트:

  • Debug : {:?} 포맷으로 출력 가능
  • Clone, Copy : 값 복사 가능
  • PartialEq, Eq : 값 비교 가능
  • PartialOrd, Ord : 정렬 가능
  • Hash : 해시 사용 가능

(3) 예제:

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

fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone 사용
println!("{:?}", p2); // Debug 사용
println!("p1 == p2? {}", p1 == p2); // PartialEq 사용
}

나. #[allow(…)], #[warn(…)], #[deny(…)], #[forbid(…)]

(1) 설명: 컴파일 경고/에러를 제어합니다. => 위 2.의 가. 컴파일러 관련 attribute 설명 참고

(2) 주요 옵션:

  • dead_code : 사용되지 않는 코드
  • unused_variables : 사용되지 않는 변수
  • non_snake_case : snake_case 규칙 위반

(3) 예제:

#[allow(dead_code)]
fn unused() {
println!("사용되지 않지만 경고 없음");
}

#[deny(unused_variables)]
fn foo() {
let x = 5; // 컴파일 에러 발생: 변수 사용 안 함
}

다. #[test], #[ignore] — 테스트 함수 지정

(1) 설명: 테스트 프레임워크를 위한 attribute입니다.

(2) 예제:

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn heavy_test() {
// 실행하려면 `cargo test -- --ignored`
}
}

라. #[cfg(…)] — 조건부 컴파일

(1) 설명: 플랫폼, 기능 등에 따라 코드 포함 여부 결정

(2) 예제:

#[cfg(target_os = "windows")]
fn run_on_windows() {
println!("Windows 전용 코드");
}

#[cfg(not(target_os = "windows"))]
fn run_on_unix() {
println!("Unix 계열 전용 코드");
}

(3) Cargo.toml에서 feature와 연계:

[features]
my_feature = []
#[cfg(feature = "my_feature")]
fn my_func() {
println!("my_feature가 활성화됨");
}

“my_feature”라는 기능(feature)이 활성화됐을 때만 my_func() 함수의 정의 자체를 컴파일에 포함시키고, 아니면 아예 없는 코드처럼 무시해버립니다.

마. #[inline] — 인라인 최적화 힌트

(1) 설명: 컴파일러에게 해당 함수를 인라인하도록 유도

(2) 예제:

#[inline(always)]
fn fast_add(a: i32, b: i32) -> i32 {
a + b
}

fn main() {
let x = add(3, 4);
}
  • 일반 호출: main에서 add 함수로 점프하고, 결과를 받아와서 x에 저장
  • 인라인: 컴파일할 때 add(3, 4)를 그냥 3 + 4로 바꿔서 main 함수 안에 넣어버림

바. #[repr(…)] — 메모리 레이아웃 제어

(1) 설명: 구조체/열거형의 메모리 정렬 방법을 지정(repr은 representation(표현)의 약어)

(2) 종류:

  • C: C 언어와 동일한 레이아웃
  • packed: 패딩 없이 압축
  • transparent: 단일 필드 감싸기

(3) 예제:

#[repr(C)]
struct MyStruct {
a: u8,
b: u32,
}

사. #[non_exhaustive] — 미래 확장을 위한 열거형

(1) 설명: 나중에 항목이 더 생길 수 있으니, “지금 있는 것만 가지고 match를 완벽하게 쓰지 마세요“라는 의미입니다.

(2) 예제:

#[non_exhaustive]
pub enum Error {
Io,
Parse,
}

이 코드는 다음과 같은 의미를 가집니다.

Error enum은 지금은 Io와 Parse 두 가지 variant만 있지만, 앞으로 새로운 variant가 추가될 수 있으므로 다른 크레이트에서는 이 enum을 match할 때 지금 있는 것만으로 열거하지 못하게 제한하는 것입니다.

그래서 #[non_exhaustive]를 붙이면, 사용자는 반드시 _를 이용한 default arm을 추가해야만 컴파일이 가능하며, 이것은 다른 크레이트가 match를 지금 있는 것만으로 exhaustively(완전하게) 작성하면, 나중에 enum에 새로운 variant를 추가했을 때 컴파일이 깨질 수 있기 때문입니다.

아. #[must_use] — 반환값 사용하지 않으면 경고

(1) 설명: 실수로 무시하면 안 되는 반환값을 경고로 알려줌

(2) 예제:

#[must_use]
fn important_result() -> Result<(), String> {
Err("Error".into())
}

fn main() {
important_result(); // 경고 발생!
}

위 코드는 important_result 함수의 반환 값을 사용하지 않아 warning(경고)이 발생하므로 아래와 같은 식으로 수정해야 합니다.

fn main() {
    let result = important_result(); // OK
    if let Err(e) = result {
        println!("에러 발생: {}", e);
    }
}

자. #[macro_use], #[macro_export] — 매크로 관련

(1) 설명: 외부 crate의 매크로를 가져오거나 내보낼 때 사용

(2) 예제:

// 외부 매크로 사용
#[macro_use]
extern crate log;

// 매크로 정의 및 export
#[macro_export]
macro_rules! hello {
() => {
println!("Hello, macro!");
};
}
  • #[macro_use] : 외부 크레이트 log에서 정의된 매크로(log!, info!, warn! 등)들을 이 파일 안에서 직접 사용할 수 있도록 가져오라는 의미로서, 예전 Rust 스타일 (Rust 2018 이전)의 방식이며, 최신 Rust (2018 edition 이후)에서는 use log::info; 식으로 가져오는 게 일반적입니다.
  • #[macro_export] : 이 매크로를 다른 모듈/크레이트에서 사용할 수 있게 공개(export) 하겠다는 의미입니다. 따라서, 이 매크로를 crate_name::hello!() 식으로 다른 crate에서도 쓸 수 있습니다.

매크로(Macro)와 메타 프로그래밍

Rust는 정적 타입 언어이면서도 매우 강력한 매크로 시스템을 제공합니다. 매크로는 코드를 작성하는 코드를 작성할 수 있게 해 주며, 반복되는 코드의 중복을 줄이고, 컴파일 타임에 코드를 생성하여 성능 저하 없이 유연성을 확보할 수 있습니다.

Rust의 매크로는 크게 두 가지로 나뉩니다:

  • 매크로 by 예시 (macro_rules!)
  • 절차적 매크로 (Procedural Macros)

이번 시간에는 두 매크로의 차이점과 사용법, 그리고 메타 프로그래밍의 개념에 대해 알아보겠습니다.


1. macro_rules! 기본 매크로

Rust에서 가장 널리 사용되는 매크로는 macro_rules!로 작성하는 선언형 매크로(declarative macro)입니다.

가. 예시

macro_rules! say_hello {
() => {
println!("Hello, macro!");
};
}

fn main() {
say_hello!(); // Hello, macro!
}

이 매크로는 함수처럼 보이지만, 실제로는 코드를 치환하는 역할을 합니다. 괄호 안에 아무 인자도 없을때, println! 코드를 삽입하는 구조입니다.

  • macro_rules!는 Rust에서 매크로를 정의하는 키워드입니다. 함수와 비슷해 보이지만, 컴파일 타임에 코드 조각을 만들어주는 메타 프로그래밍 도구입니다.
  • say_hello라는 이름의 매크로를 정의합니다.
    ()는 이 매크로가 인자를 받지 않음을 의미합니다.
    => { … }는 매크로가 어떤 코드로 확장될지를 정의합니다.
    여기서는 println!(“Hello, macro!”);라는 코드를 삽입합니다.
    즉, 이 매크로는 호출되면 println!을 실행하는 코드로 치환됩니다.
  • say_hello!();는 매크로를 호출하는 문법입니다.
    함수 호출과 다르게 ! 기호가 사용됩니다.
  • 결과적으로 Hello, macro!가 출력됩니다.

나. 매크로는 함수보다 더 유연하다.

  • 함수는 특정 타입에 대해 정의되어야 하지만,
  • 매크로는 타입과 관계없이 패턴으로 처리할 수 있습니다.
macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("함수 {} 호출됨", stringify!($name));
}
};
}

create_function!(foo);
create_function!(bar);

fn main() {
foo(); // 함수 foo 호출됨
bar(); // 함수 bar 호출됨
}

위의 매크로는 여러 개의 함수를 자동으로 생성하는 예시입니다.

(1) 매크로 정의

macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("함수 {} 호출됨", stringify!($name));
}
};
}
  • macro_rules! create_function: create_function이라는 이름의 매크로를 정의합니다.
  • ($name:ident) => { … }: 이 매크로는 식별자 하나를 입력 인자로 받아 내부에서 함수 하나를 생성합니다.
    ident: 식별자(identifier) 타입을 의미합니다. 변수명, 함수명 같은 이름을 받을 때 사용합니다.니다.
    • ident: 식별자(identifier) 타입을 의미합니다. 변수명, 함수명 같은 이름을 받을 때 사용합니다.
  • fn $name() { … }: 입력받은식별자 $name을 함수 이름으로 사용하여 새 함수를 만듭니다.
  • stringify!($name): 식별자 $name을 문자열로바꿔줍니다.
    • 예: stringify!(foo) → “foo”

(2) 매크로 호출

create_function!(foo);
create_function!(bar);
  • 이 부분은 위에서 정의한 매크로를 호출하는 부분입니다.
  • create_function!(foo)는 foo를 인수로 받아 아래와 같은 함수를 만들어냅니다:
fn foo() {
println!("함수 foo 호출됨");
}
  • create_function!(bar)도 마찬가지로 bar를 인수로 받아 아래와 같은 함수를 만들어냅니다:
fn bar() {
println!("함수 bar 호출됨");
}

즉, 이 두줄 덕분에 foo와 bar라는 이름의 함수가 자동으로 생성됩니다.


(3) main 함수 – 함수 호출

fn main() {
foo(); // 함수 foo 호출됨
bar(); // 함수 bar 호출됨
}
  • 앞서 매크로를통해 생성된 foo()와 bar() 함수를 호출합니다.
  • 각각의 함수는 다음과 같은 출력을 합니다:
함수 foo 호출됨
함수 bar 호출됨

2. 메타 프로그래밍(Metaprogramming)이란?

Rust에서 매크로를 사용하는 이유는 곧 메타 프로그래밍을 위한 것입니다. 메타 프로그래밍이란 프로그램이 프로그램 코드를 다루거나 생성하는 것을 말합니다.

Rust에서는 컴파일 타임에 코드를 생성하여

  • 코드 반복 제거
  • 에러 감소
  • 성능 저하 없이 추상화 제공

이라는 장점을 누릴 수 있습니다.


3. 절차적 매크로(Procedural Macro)

macro_rules!는 구조가 제한적이므로 복잡한 로직을 처리하기엔 어렵습니다. 이를 해결하기 위해 Rust는 절차적 매크로를 제공합니다. 절차적 매크로는 함수처럼 동작하며, 다음과 같이 세 가지 유형이 있습니다:

  1. Derive 매크로 (#[derive])
  2. Attribute 매크로 (#[route], #[test] 등)
  3. Function-like 매크로 (my_macro!(…) 형태)

가. Derive 매크로

Rust는 많은 표준 트레잇을 #[derive(…)]를 통해 자동으로 구현할 수 있습니다.

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

#[DERIVE(…)]

이 부분은 파생 구현(derive macro)라고 부릅니다. Rust에서는 구조체에 대해 자주 사용하는 trait들을 자동으로 구현할 수 있도록 해줍니다.

* Debug
  • 구조체를 {:?} 형식으로 출력할 수 있게 해줍니다.
  • 예: println!(“{:?}”, p); // 출력: Point { x: 3, y: 5 }
*Clone
  • 구조체를 복사(clone) 할 수 있게 해줍니다.
  • let p2 = p.clone(); 같이 사용 가능하며, 깊은 복사가 이루어집니다.
  • let p2 = p; 이라고 하면 Copy가 아니기 때문에 p는 이동(move)되고, 사용 불가가 되므로 clone을 사용하는 것입니다.
* PartialEq
  • 두 구조체가 같은지 비교할 수 있게 해줍니다 (==, != 사용 가능).
  • 예:
    let a = Point { x: 1, y: 2 };
    let b = Point { x: 1, y: 2 };
    assert_eq!(a, b); // true

나. Attribute 매크로

Attribute 매크로는 함수, 구조체 등 앞에 붙여 동작을 변경합니다.

#[route(GET, "/")]
fn index() {
// 라우트 처리
}
🔸 #[route(GET, “/”)]

Rocket 프레임워크에서 제공하며, 해당 함수가 HTTP 요청을 어떻게 처리할지를 지정합니다.

  • GET → HTTP 메서드 GET, POST, PUT, DELETE 등 중 하나입니다.
  • “/” → 경로. 이 경우 루트 경로(예: http://localhost:8000/)입니다.

🔸 fn index()

이것은 실제 요청을 처리할 함수입니다.
보통 이 함수는 -> &’static str이나 -> Html<String> 같은 반환값을 갖습니다.
index는 함수 이름으로, 자유롭게 바꿀 수 있습니다 (home, root 등).
이 함수는 라우팅된 요청이 들어왔을 때 호출됩니다.

위 코드는 다음 의미를 갖습니다:

  • 클라이언트가 GET / 요청을 보냈을 때,
  • index() 함수를 실행해서 그 요청을 처리한다.

다. Function-like 매크로

마치 함수처럼 사용하는 매크로입니다.

my_macro!(input);

일반적으로 다음처럼 정의합니다.

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
// input을 파싱하고 새로운 코드 생성
}
  • pub fn my_macro(input: TokenStream) -> TokenStream
    my_macro는 매크로 이름입니다. 실제 사용 시에는 my_macro!(…) 형태로 사용됩니다.
    입력: input: TokenStream
    출력: TokenStream
  • 즉, 입력으로 받은 코드를 읽고, 새로운 코드로 변환하는 함수입니다.

4. 매크로의 장단점

가. 장점

  • 코드 중복 제거
  • 성능 저하 없이 추상화 가능
  • 정적 분석 기반으로 안전성 확보

나. 단점

  • 디버깅이 어려움
  • 복잡한 로직일수록 가독성 저하
  • IDE의 지원이 제한적일 수 있음

5. 매크로 관련 도구

  • syn: Rust 코드를 파싱하는 라이브러리
  • quote: 코드 생성을 위한 DSL(Domain Specific Languages)
  • proc-macro2: proc_macro를 확장한 안정적인 인터페이스
[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"

6. 마무리

Rust의 매크로 시스템은 다른 언어의 템플릿이나 메타 프로그래밍 기능보다 훨씬 강력하고 안전하게 설계되어 있습니다. macro_rules!로 간단한 코드 생성을 처리하고, 절차적 매크로로 복잡한 로직도 커버할 수 있습니다.

메타 프로그래밍은 러스트의 안전성과 추상화를 동시에 만족시키는 중요한 도구이므로, 초반에는 어렵더라도 반드시 익혀야 할 개념입니다.