Rust 웹 서버 만들기 2편. Rust에서 Reqwest를 이용해서 HTTP 요청(Request)하기
안녕하세요?
지난 시간에 이어 Rust로 웹 서버 만들기 강좌를 계속 이어 나가겠습니다.
Rust 웹 서버 만들기 1편. Actix Web 그리고 Fly.io에 배포하기
우리가 NodeJS로 웹 애플리케이션을 만들 때 가장 많이 이용하는 게 바로 fetch 함수인데요.
axios, request 등 HTTP 요청을 쉽게 해주는 여러 가지 패키지가 존재합니다.
Rust에서는 Reqwest가 아주 유명한데요.
이거 하나만 익혀도 되니 다른 건 안 찾아보셔도 됩니다.
테스트를 위해서 빈 폴더에 다음과 같이 앱을 하나 만듭니다.
cargo new reqwest-test
cd reqwest-test
cargo add reqwest --features json
cargo add tokio --features full
비동기식에 있어 tokio는 필수 패키지죠.
이제, 본격적인 reqwest(리퀘스트)를 해볼까요?
GET 요청
#[tokio::main]
async fn main() {
let response = reqwest
::get("https://jsonplaceholder.typicode.com/users").await;
println!("{:?}", response);
}
우리가 사용할 테스트 서버는 jsonplaceholder 입니다.
HTTP GET 요청은 reqwest::get(url) 방식으로 하고,
await를 이용해서 비동기식으로 작동시키면 됩니다.
➜ reqwest-test git:(master) ✗ cargo run
Compiling reqwest-test v0.1.0 (/Users/cpro95/Codings/Rust/reqwest-test)
Finished dev [unoptimized + debuginfo] target(s) in 2.01s
Running `target/debug/reqwest-test`
Ok(Response { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("jsonplaceholder.typicode.com")), port: None, path: "/users", query: None, fragment: None }, status: 200, headers: {"date": "Mon, 04 Sep 2023 13:16:15 GMT", "content-type": "application/json; charset=utf-8", "transfer-encoding": "chunked", "connection": "keep-alive", "x-powered-by": "Express",cldPwVWkvxTa03X3zwofv66zbL88VMiAMa0%2F7%2BUU6msXke5ntw\"}],\"group\":\"cf-nel\",\"max_age\":604800}", "nel": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", "server": "cloudflare", "cf-ray": "801681834df019c2-KIX", "alt-svc": "h3=\":443\"; ma=86400"} })
➜ reqwest-test git:(master) ✗
실행결과는 조금 이상한데요.
다른 언어에서 Rust로 넘어오셔서 Result를 처음 접하시면 헷갈립니다.
저도 지금 매우 헷갈리는데요.
일단 Visual Studio Code에서 reqwest::get 위치에 마우스를 갖다 대면 아래와 같이 나옵니다.
reqwest
pub async fn get<T>(url: T) -> crate::Result<Response>
where
T: IntoUrl,
즉, 리턴 값이 Result에 Response가 있다는 뜻입니다.
즉, 위 코드는 Result의 Response를 보여주게 됩니다.
그럼, 우리가 원하는 데이터는 어떻게 보죠.
바로 Result를 match 시켜서 분해하는 방법이 있는데요.
더 쉽게 unwrap() 메서드를 쓰면 됩니다.
코드를 다시 바꿔 볼까요?
#[tokio::main]
async fn main() {
let response = reqwest::get("https://jsonplaceholder.typicode.com/users")
.await
.unwrap()
.text()
.await;
println!("{:?}", response);
}
reqwest::get() 다음에 await 하고 나서 unwrap() 했습니다.
unwrap()은 Result를 그냥 에러 상관없이 풀어버리라고 하는 명령어입니다.
그리고 우리가 보기 쉽게 text()로 바꾸는 명령어를 주었습니다.
text() 명령어도 비동기식이라 최종적으로 await를 한번 더 줬습니다.
이제 결과를 볼까요?
➜ reqwest-test git:(master) ✗ cargo run
Compiling reqwest-test v0.1.0 (/Users/cpro95/Codings/Rust/reqwest-test)
Finished dev [unoptimized + debuginfo] target(s) in 1.91s
Running `target/debug/reqwest-test`
Ok("[\n {\n \"id\": 1,\n \"name\": \"Leanne Graham\",\n \"bs\": \"target end-to-end models\"\n }\n }\n]")
➜ reqwest-test git:(master) ✗
최종적으로 OK() 안에 우리가 원하는 데이터가 있네요.
reqwest::get의 statusCode에 따른 match 구문 작성하기
이제, 코드를 바꿔서 match 구문을 활용해 볼까요?
#[tokio::main]
async fn main() {
let response = reqwest::get("https://jsonplaceholder.typicode.com/users").await;
match response {
Ok(body) => {
let data = body.text().await;
println!("{:?}", data);
}
Err(err) => {
eprintln!("{:?}", err);
}
}
}
이 방식으로도 써도 되고요.
아래와 같은 방식으로도 써도 됩니다.
#[tokio::main]
async fn main() {
let response = reqwest::get("https://jsonplaceholder.typicode.com/users")
.await
.unwrap();
match response.status() {
reqwest::StatusCode::OK => {
// parse our response
match response.text().await {
Ok(parsed) => println!("parsed {:?}", parsed),
Err(_) => println!("error"),
};
}
reqwest::StatusCode::UNAUTHORIZED => {
println!("unauthorized");
}
other => {
panic!("panic!!, {:?}", other);
}
}
}
response를 딱 unwrap()까지만 했습니다.
그리고 response의 status() 메서드를 이용했고요.
실행 결과는 똑같을 겁니다.
TMDB_API_KEY 키를 활용한 Reqwest GET 요청하기
제가 예전에 만든 myMovies 앱이 있는데요.
TMDB에서 popular movies를 불러와서 브라우저에 뿌려주는 앱을 만들었습니다.
오늘은 여기서 사용한 TMDB_API_KEY를 활용해서 Reqwest GET 요청을 해볼까 합니다.
NodeJS에서 .env 파일을 쓰는데요.
Rust에서도 .env 파일을 쓸 수 있습니다.
일단 다음과 같이 dotenv 패키지를 설치합시다.
cargo add dotenv
dotenv 패키지는 아래와 같이 사용하시면 됩니다.
use dotenv::dotenv;
fn main() {
dotenv().ok();
for (key, value) in std::env::vars() {
println!("{}: {}", key, value);
}
println!("{}", std::env::var("TMDB_API_KEY").unwrap());
}
실행 결과는 현재 터미널의 환경변수를 전부 보여주고요.
그다음에 .env에 있는 TMDB_API_KEY를 출력해 줍니다.
Reqwest를 이용해서 TMDB 영화 불러오기
이제 우리가 위에서 배운 Get 메서드를 좀 더 확장해 볼까요?
use dotenv::dotenv;
#[tokio::main]
async fn main() {
dotenv().ok();
let tmdb_url = format!(
"https://api.themoviedb.org/3/movie/popular?api_key={}&page=1",
std::env::var("TMDB_API_KEY").unwrap()
);
let client = reqwest::Client::new();
let response = client
.get(tmdb_url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await
.unwrap();
match response.status() {
reqwest::StatusCode::OK => {
// parse our response
match response.text().await {
Ok(parsed) => println!("parsed {:?}", parsed),
Err(_) => println!("error"),
};
}
reqwest::StatusCode::UNAUTHORIZED => {
println!("unauthorized");
}
other => {
panic!("panic!!, {:?}", other);
}
}
}
위 코드에서 어려운 거는 없는데요.
일단 reqwest::get으로 GET 요청을 한 게 아니라 reqwest Client 객체를 하나 만들고 그 객체에다가 여러 가지 header를 추가하고 요청을 수행했습니다.
header를 추가한 걸로 봐서 POST 요청할 때 token을 bearer 방식으로 넣는 것도 가능하겠네요.
let client = reqwest::Client::new();
let response = client
.get(url)
.header(AUTHORIZATION, "Bearer [AUTH_TOKEN]")
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.send()
.await
.unwrap();
println!("Success! {:?}", response)
token을 넣는 방식은 위와 같이 header를 하나 추가하면 됩니다.
이제, 실행 결과를 볼까요?
➜ reqwest-test git:(master) ✗ cargo run
Compiling reqwest-test v0.1.0 (/Users/cpro95/Codings/Rust/reqwest-test)
Finished dev [unoptimized + debuginfo] target(s) in 2.00s
Running `target/debug/reqwest-test`
parsed "{\"page\":1,\"results\":[{\"adult\":false,\"backdrop_path\":\"/8pjWz2lt29KyVGoq1mXYu6Br7dE.jpg\",\"genre_ids\":[28,878,27],the Galaxy Vol. 3\",\"video\":false,\"vote_average\":8,\"vote_count\":4638}],\"total_pages\":39861,\"total_results\":797216}"
➜ reqwest-test git:(master) ✗
parsed 값이 너무 많이 제가 조금 줄여서 보여드린 겁니다.
Reqwest Client를 이용해서 타임아웃이 있는 요청을 지속적으로 해보기
우리가 요청을 할 때 한번 할 수도 있는데요.
보통 될 때까지 연결하라고 명령을 줄 필요가 있습니다.
이 부분을 작성해 볼까 하는데요.
use dotenv::dotenv;
async fn send_request_with_retry(url: &str) -> Result<String, reqwest::Error> {
let mut retry_count = 0;
let max_retries = 5;
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
loop {
let response = client
.get(url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await?;
if response.status().is_success() {
return Ok(response.text().await?);
} else if retry_count < max_retries {
retry_count += 1;
eprintln!("Request failed, attempt {} of {}", retry_count, max_retries);
} else {
break Ok("timeout".to_string());
}
}
}
#[tokio::main]
async fn main() {
dotenv().ok();
let tmdb_url = format!(
"https://api.themoviedb.org/3/movie/popular?api_key={}&page=1",
std::env::var("TMDB_API_KEY").unwrap()
);
match send_request_with_retry(&tmdb_url).await {
Ok(body) => {
println!("{}", body);
}
Err(err) => {
eprintln!("Failed to fetch data: {}", err);
}
}
}
여기서는 send_request_with_retry 함수를 잘 봐야 하는데요.
이 함수에서는 Reqwest::Client의 Builder를 이용해서 timeout을 줬습니다.
timeout 10초를 줬는데요.
만약 10초 후에는 어떻게 될까요?
더 이상 연결 안 하겠죠?
그래서 저는 max_retries와 retry_count 변수를 활용해서 loop를 돌렸습니다.
5번 시도했는데 결과가 없다면 "timeout" 문자열을 리턴하게끔 만들었습니다.
이걸 테스트하기 위해 tmdb 서버의 주소를 약간 바꿔서 일부러 에러가 나게끔 해보겠습니다.
https://api.themoviedb.org/0/movie/
위에서처럼 3이란 숫자를 0으로 바꿨는데요.
이거는 TMDB API 서버의 버전입니다.
현재는 버전 3이란 뜻이죠.
버전 0으로 바꾸고 테스트해 볼까요?
➜ reqwest-test git:(master) ✗ cargo run
Compiling reqwest-test v0.1.0 (/Users/cpro95/Codings/Rust/reqwest-test)
Finished dev [unoptimized + debuginfo] target(s) in 1.83s
Running `target/debug/reqwest-test`
Request failed, attempt 1 of 5
Request failed, attempt 2 of 5
Request failed, attempt 3 of 5
Request failed, attempt 4 of 5
Request failed, attempt 5 of 5
timeout
➜ reqwest-test git:(master) ✗
강제로 에러가 나오게끔 했더니 위와 같이 5번 시도했고, 최종적으로 timeout 문구를 리턴 했습니다.
앞으로 제가 만들 웹 서버에서는 이 함수를 사용할 예정입니다.
왜냐하면 기존 사이트를 미러링하는 서버이기 때문에 될 때까지 시도해야 하거든요.
Reqwest POST 요청하기
이제 POST 요청으로 넘어가 보겠습니다.
#[tokio::main]
async fn main() {
let url = "https://rust-web-app-tutorial.fly.dev/echo";
let json_data = r#"
{"id":"1", "name":"brian"}
"#;
let client = reqwest::Client::new();
let response = client
.post(url)
.header("Content-Type", "application/json")
.body(json_data.to_owned())
.send()
.await;
let response_body = response.unwrap().text().await;
println!("{:?}", response_body);
}
POST 요청을 위해 지난 시간에 만든 Rust 웹 서버의 echo route를 이용했습니다.
실행 결과를 볼까요?
➜ reqwest-test git:(master) ✗ cargo run
Compiling reqwest-test v0.1.0 (/Users/cpro95/Codings/Rust/reqwest-test)
Finished dev [unoptimized + debuginfo] target(s) in 1.59s
Running `target/debug/reqwest-test`
Ok("\n {\"id\":\"1\", \"name\":\"brian\"}\n ")
➜ reqwest-test git:(master) ✗
아주 잘 작동하고 있네요.
이제 POST까지 배웠으니까, PUT, DELETE도 직접 Doc.rs 사이트에서 찾아보고 도전해 보시기 바랍니다.
그럼.