HTTP 클라이언트
HttpClient를 주입받아 외부 REST API를 호출하고 JSON 데이터를 처리합니다.
📌 학습 목표
- Blazor 프로젝트에서 HttpClient를 의존성 주입(의존성 주입(DI))으로 등록하는 방법을 이해합니다.
- 컴포넌트 내에서 HttpClient를 주입받아 외부 REST API에 GET·POST 요청을 비동기로 수행합니다.
- API 응답 JSON을 C# 모델 클래스로 역직렬화하여 화면에 데이터를 바인딩합니다.
- HttpResponseMessage를 직접 다루어 상태 코드 확인과 오류를 처리합니다.
- Blazor WebAssembly와 Blazor Server에서 HttpClient 등록 방식이 다른 이유를 파악합니다.
📝 개념 설명
1. HttpClient란?
HttpClient는 .NET에서 HTTP 요청을 보내고 응답을 받는 클래스입니다. Blazor 앱에서는 이 클래스를 사용해 백엔드 API나 공개 REST API에 데이터를 요청하고, JSON 형태로 받아 C# 객체로 변환합니다.
Blazor에서 HttpClient는 싱글턴 또는 스코프드 서비스로 의존성 주입(DI) 컨테이너에 등록한 뒤, 컴포넌트에서 @inject로 주입받아 사용합니다. 직접 new HttpClient()를 생성하는 방식은 소켓 고갈(Socket Exhaustion) 문제가 발생하므로 절대 권장하지 않습니다.
2. Blazor WebAssembly vs Blazor Server — 등록 방식 차이
Program.cs에서 BaseAddress를 지정해야 합니다.Blazor Server는 서버 측에서 실행되므로 일반적인 ASP.NET Core의
IHttpClientFactory를 사용하거나 AddHttpClient()로 등록합니다.
// Program.cs (Blazor WebAssembly)
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
// Program.cs (Blazor Server)
builder.Services.AddHttpClient();
Named Client로 등록하면 BaseAddress를 포함해 관리하기 편리합니다:
builder.Services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
3. 컴포넌트에서 HttpClient 주입받기
Razor 컴포넌트에서는 @inject 지시어로 주입받습니다.
@inject HttpClient Http
code-behind 파일(.cs)에서는 [Inject] 속성을 사용합니다:
[Inject]
private HttpClient Http { get; set; } = default!;
4. JSON 역직렬화 — System.Text.Json
Blazor는 기본적으로 System.Text.Json을 사용합니다. GetFromJsonAsync<T> 메서드는 HTTP GET 요청과 JSON 역직렬화를 한 번에 처리하는 편의 메서드입니다. 이 메서드를 사용하려면 System.Net.Http.Json 네임스페이스가 필요합니다(Blazor 기본 포함).
// 모델 클래스 정의
public class Todo
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public bool Completed { get; set; }
}
// GET + 역직렬화 한 번에 처리
var todo = await Http.GetFromJsonAsync<Todo>("https://jsonplaceholder.typicode.com/todos/1");
@inject HttpClient
메서드 호출
JSON 응답 수신
C# 모델 변환
StateHasChanged
| 메서드 | 용도 | 반환 타입 |
|---|---|---|
| GetFromJsonAsync<T> | GET 요청 + JSON 역직렬화 | T? |
| PostAsJsonAsync | POST 요청 + JSON 직렬화 | HttpResponseMessage |
| PutAsJsonAsync | PUT 요청 + JSON 직렬화 | HttpResponseMessage |
| DeleteAsync | DELETE 요청 | HttpResponseMessage |
| GetAsync | GET 요청 (수동 처리) | HttpResponseMessage |
| SendAsync | 커스텀 HttpRequestMessage 전송 | HttpResponseMessage |
💡 예제 & 실습
실습 1 — 공개 API에서 Todo 목록 가져오기 (GET)
JSONPlaceholder(jsonplaceholder.typicode.com)는 테스트용 무료 REST API입니다. 이 API에서 Todo 목록을 가져와 화면에 표시하는 컴포넌트를 작성합니다.
// Models/Todo.cs
public class Todo
{
public int Id { get; set; }
public int UserId { get; set; }
public string Title { get; set; } = string.Empty;
public bool Completed { get; set; }
}
@page "/todos"
@inject HttpClient Http
<h3>Todo 목록</h3>
@if (isLoading)
{
<p>⏳ 데이터 로딩 중...</p>
}
else if (todos is null || todos.Count == 0)
{
<p>데이터가 없습니다.</p>
}
else
{
<ul>
@foreach (var todo in todos)
{
<li>
@todo.Id. @todo.Title
<span>@(todo.Completed ? "✅" : "⬜")</span>
</li>
}
</ul>
}
@code {
private List<Todo>? todos;
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
todos = await Http.GetFromJsonAsync<List<Todo>>(
"https://jsonplaceholder.typicode.com/todos?_limit=10"
);
isLoading = false;
}
}
@inject HttpClient Http: 의존성 주입(DI) 컨테이너에 등록된 HttpClient를Http라는 이름으로 주입받습니다.OnInitializedAsync: 컴포넌트가 초기화될 때 자동으로 호출되는 생명주기 메서드입니다. 비동기 데이터 로드에 적합합니다.GetFromJsonAsync<List<Todo>>: GET 요청을 보내고 응답 JSON을List<Todo>로 역직렬화합니다.isLoading플래그: 데이터가 로드되기 전까지 로딩 메시지를 표시해 사용자 경험을 향상시킵니다.
실습 2 — POST 요청으로 새 데이터 전송
PostAsJsonAsync를 사용해 C# 객체를 JSON으로 직렬화하여 서버에 전송하고, 응답 상태 코드를 확인합니다.
@page "/create-todo"
@inject HttpClient Http
<h3>새 Todo 추가</h3>
<div>
<input @bind="newTitle" placeholder="할 일을 입력하세요" />
<button @onclick="CreateTodo">추가</button>
</div>
@if (!string.IsNullOrEmpty(resultMessage))
{
<p>@resultMessage</p>
}
@code {
private string newTitle = string.Empty;
private string resultMessage = string.Empty;
private async Task CreateTodo()
{
if (string.IsNullOrWhiteSpace(newTitle)) return;
var newTodo = new Todo
{
Title = newTitle,
UserId = 1,
Completed = false
};
var response = await Http.PostAsJsonAsync(
"https://jsonplaceholder.typicode.com/todos",
newTodo
);
if (response.IsSuccessStatusCode)
{
var created = await response.Content
.ReadFromJsonAsync<Todo>();
resultMessage = $"생성 완료! 새 Todo ID: {created?.Id}";
newTitle = string.Empty;
}
else
{
resultMessage = $"오류 발생: {(int)response.StatusCode} {response.StatusCode}";
}
}
}
PostAsJsonAsync: C# 객체를 자동으로 JSON으로 직렬화하여 POST 요청을 보냅니다. Content-Type 헤더도 자동으로application/json으로 설정됩니다.response.IsSuccessStatusCode: HTTP 200~299 상태 코드이면true를 반환합니다.ReadFromJsonAsync<T>: 응답 본문 JSON을 C# 객체로 역직렬화합니다.- 문자열 보간(
$): 응답 상태 코드를 숫자와 이름 모두 표시합니다.
실습 3 — BaseAddress 활용 (상대 경로 사용)
Program.cs에서 BaseAddress를 설정했다면, 이후 모든 요청에서 상대 경로만 사용할 수 있어 코드가 간결해집니다.
// Program.cs — BaseAddress 설정
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
});
// 컴포넌트에서 상대 경로만 사용
var todos = await Http.GetFromJsonAsync<List<Todo>>("todos");
var todo = await Http.GetFromJsonAsync<Todo>("todos/1");
var posts = await Http.GetFromJsonAsync<List<Post>>("posts?userId=1");
실습 4 — JSON 프로퍼티 이름 매핑 ([JsonPropertyName])
API가 반환하는 JSON 키와 C# 프로퍼티 이름이 다를 경우 [JsonPropertyName] 속성으로 명시적으로 매핑합니다.
using System.Text.Json.Serialization;
// API 응답 JSON:
// { "user_id": 1, "product_name": "노트북", "unit_price": 1200000 }
public class Product
{
[JsonPropertyName("user_id")]
public int UserId { get; set; }
[JsonPropertyName("product_name")]
public string ProductName { get; set; } = string.Empty;
[JsonPropertyName("unit_price")]
public decimal UnitPrice { get; set; }
}
// 사용 예시
var product = await Http.GetFromJsonAsync<Product>("api/products/1");
⚠️ 자주 틀리는 것 / 주의사항
⚠️ 1. Blazor WebAssembly에서 CORS 오류
Blazor WebAssembly는 브라우저에서 실행되므로, 요청하는 외부 API 서버가 CORS를 허용해야 합니다. 자체 ASP.NET Core 백엔드라면 AddCors()로 허용 도메인을 설정해야 합니다. 서드파티 공개 API는 대부분 CORS를 허용하지만, 그렇지 않은 경우 백엔드 프록시를 통해 우회해야 합니다.
⚠️ 2. new HttpClient() 직접 생성 금지
매 요청마다 new HttpClient()를 생성하면 TCP 소켓이 TIME_WAIT 상태로 누적되어 소켓 고갈 문제가 발생합니다. 반드시 의존성 주입(DI)로 주입받아 사용하세요.
// ❌ 잘못된 방법 — 소켓 고갈 위험
var http = new HttpClient();
var data = await http.GetFromJsonAsync<Todo>("todos/1");
// ✅ 올바른 방법 — @inject로 주입받은 인스턴스 재사용
var data = await Http.GetFromJsonAsync<Todo>("todos/1");
⚠️ 3. null 처리 누락
GetFromJsonAsync<T>는 응답이 204(No Content)이거나 역직렬화에 실패할 경우 null을 반환합니다. null 체크를 생략하면 런타임에 NullReferenceException이 발생합니다.
var todo = await Http.GetFromJsonAsync<Todo>("todos/1");
if (todo is null)
{
Console.WriteLine("데이터를 불러오지 못했습니다.");
return;
}
Console.WriteLine(todo.Title); // 안전하게 접근 가능
⚠️ 4. Blazor Server에서 .Result 또는 .Wait() 사용 금지
Blazor Server는 SynchronizationContext가 있어 .Result나 .Wait()를 사용하면 교착 상태(Deadlock)가 발생합니다. 반드시 await를 사용하세요.
// ❌ 교착 위험 (Blazor Server)
todos = Http.GetFromJsonAsync<List<Todo>>("todos").Result;
// ✅ 올바른 비동기 사용
todos = await Http.GetFromJsonAsync<List<Todo>>("todos");
⚠️ 5. BaseAddress 끝에 슬래시(/) 누락
BaseAddress를 설정할 때 끝에 /가 없으면 상대 경로 결합이 예상과 다르게 동작합니다. 반드시 /로 끝내야 합니다.
// ❌ 슬래시 누락 — "https://api.example.com" + "todos" = "https://api.example.com/todos" (우연히 동작)
// 하지만 "https://api.example.com/v1" + "todos" = "https://api.example.com/todos" (v1 경로 손실!)
new Uri("https://api.example.com/v1") // 잘못된 예
// ✅ 슬래시 포함
new Uri("https://api.example.com/v1/") // 올바른 예
🎯 마무리
이 글에서는 Blazor에서 HttpClient를 사용해 외부 REST API와 통신하는 전 과정을 살펴보았습니다. 의존성 주입으로 HttpClient를 등록하고, GetFromJsonAsync와 PostAsJsonAsync 같은 편의 메서드를 활용하면 API 호출과 JSON 처리를 매우 간결하게 구현할 수 있습니다.
- 반복되는 API 호출 로직은 서비스 클래스로 분리하여 컴포넌트를 깔끔하게 유지하세요. 예를 들어
TodoService를 만들어 HttpClient 로직을 캡슐화할 수 있습니다. - Blazor Server에서는 IHttpClientFactory와 Named Client를 조합하면 여러 API 엔드포인트를 독립적으로 관리할 수 있습니다.
- API 통신 중
isLoading플래그를 별도로 관리하면 사용자에게 진행 상태를 명확히 알릴 수 있습니다. - 프로덕션 환경에서는 반드시 try-catch로
HttpRequestException과JsonException을 처리하여 네트워크 오류와 파싱 오류에 대비하세요.
- HttpClient는 의존성 주입(DI)로 등록하고
@inject로 주입받아 사용한다. 직접new HttpClient()생성은 소켓 고갈 위험이 있어 금지. - Blazor WebAssembly는
BaseAddress를 지정한 스코프드(Scoped) HttpClient를 등록한다. GetFromJsonAsync<T>는 GET 요청과 JSON 역직렬화를 한 번에 처리하는 편의 메서드다.PostAsJsonAsync는 C# 객체를 JSON으로 직렬화하여 POST 요청을 보낸다.HttpResponseMessage.IsSuccessStatusCode로 HTTP 200~299 성공 여부를 확인한다.- JSON 키와 C# 프로퍼티 이름이 다를 때는
[JsonPropertyName]속성으로 매핑한다. - Blazor Server에서
.Result·.Wait()사용은 교착 상태를 유발하므로 반드시 await를 사용한다. - BaseAddress 끝에는 반드시
/를 붙여야 상대 경로 결합이 올바르게 동작한다.