자습서: C#을 사용하여 .NET 콘솔 앱에서 HTTP 요청 만들기

이 자습서에서는 GitHub에서 REST 서비스에 대해 HTTP 요청을 발행하는 앱을 빌드합니다. 앱은 정보를 JSON 형식으로 읽고 JSON을 C# 개체로 변환합니다. JSON에서 C# 개체로 변환하는 것을 deserialization이라고 합니다.

자습서에서는 다음을 수행하는 방법을 보여 줍니다.

  • HTTP 요청 보내기
  • JSON 응답 역직렬화하기
  • 특성을 사용하여 deserialization 구성하기

이 자습서의 최종 샘플을 따르려는 경우 해당 샘플을 다운로드할 수 있습니다. 다운로드 지침은 샘플 및 자습서를 참조하세요.

사전 요구 사항

  • .NET SDK 6.0 이상
  • [Visual Studio Code (오픈 소스 플랫폼 간 편집기)와 같은 코드 편집기입니다. 샘플 앱은 Windows, Linux, macOS 또는 Docker 컨테이너에서 실행할 수 있습니다.

클라이언트 앱 만들기

  1. 명령 프롬프트를 열고 앱을 저장할 새 디렉터리를 만듭니다. 해당 디렉터리를 현재 디렉터리로 지정합니다.

  2. 콘솔 창에 다음 명령을 입력합니다.

    dotnet new console --name WebAPIClient
    

    이 명령은 기본 “Hello World” 앱을 위한 시작 파일을 만듭니다. 프로젝트 이름은 "WebAPIClient"입니다.

  3. “WebAPIClient” 디렉터리로 이동하여 앱을 실행합니다.

    cd WebAPIClient
    
    dotnet run
    

    dotnet run은 자동으로 dotnet restore를 실행하여 앱에 필요한 모든 종속성을 복원합니다. 필요한 경우 dotnet build도 실행합니다. 앱 출력 "Hello, World!"이 표시됩니다. 터미널에서 Ctrl+C 를 눌러 앱을 중지합니다.

HTTP 요청 만들기

이 앱은 GitHub API를 호출하여 .NET Foundation의 프로젝트에 관한 정보를 가져옵니다. 엔드포인트가 https://api.github.com/orgs/dotnet/repos인 경우 정보를 가져오기 위해 HTTP GET 요청을 수행합니다. 브라우저도 HTTP GET 요청을 수행하므로 해당 URL을 브라우저 주소 표시줄에 붙여넣어 수신되고 처리될 정보를 볼 수 있습니다.

HttpClient 클래스를 사용하여 HTTP 요청을 수행합니다. HttpClient는 오래 실행되는 API에 대해 비동기 메서드만 지원합니다. 따라서 다음 단계는 비동기 메서드를 만들고 Main 메서드에서 해당 메서드를 호출합니다.

  1. Program.cs 프로젝트 디렉터리에서 파일을 열고 해당 내용을 다음으로 바꿉니다.

    await ProcessRepositoriesAsync();
    
    static async Task ProcessRepositoriesAsync(HttpClient client)
    {
    }
    

    이 코드에서는 다음을 수행합니다.

    • Console.WriteLine 문을 await 키워드를 사용하는 ProcessRepositoriesAsync에 대한 호출로 바꿉니다.
    • ProcessRepositoriesAsync 메서드를 정의합니다.
  2. 클래스에서 Program 를 사용하여 HttpClient 콘텐츠를 다음 C#으로 바꿔 요청 및 응답을 처리합니다.

    using System.Net.Http.Headers;
    
    using HttpClient client = new();
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
    client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
    
    await ProcessRepositoriesAsync(client);
    
    static async Task ProcessRepositoriesAsync(HttpClient client)
    {
    }
    

    이 코드에서는 다음을 수행합니다.

    • 모든 요청에 대해 다음과 같은 HTTP 헤더를 설정합니다.
      • JSON 응답을 받는 Accept 헤더
      • User-Agent 헤더입니다. 이러한 헤더는 GitHub 서버 코드에서 확인되며 GitHub에서 정보를 가져오는 데 필요합니다.
  3. ProcessRepositoriesAsync 메서드에서 .NET Foundation 조직에 있는 모든 리포지토리의 목록을 반환하는 GitHub 엔드포인트를 호출합니다.

     static async Task ProcessRepositoriesAsync(HttpClient client)
     {
         var json = await client.GetStringAsync(
             "https://api.github.com/orgs/dotnet/repos");
    
         Console.Write(json);
     }
    

    이 코드에서는 다음을 수행합니다.

    • 호출 HttpClient.GetStringAsync(String) 메서드에서 반환된 작업을 기다립니다. 이 메서드는 지정된 URI에 HTTP GET 요청을 보냅니다. 응답의 본문은 String으로 반환되는데, 이것은 작업이 완료되면 사용할 수 있습니다.
    • 응답 문자열 json 이 콘솔에 인쇄됩니다.
  4. 앱을 빌드하고 실행합니다.

    dotnet run
    

    이제 ProcessRepositoriesAsyncawait 연산자가 있으므로 빌드 경고가 발생하지 않습니다. 출력으로 JSON 텍스트가 길게 표시됩니다.

JSON 결과 역직렬화

다음 단계는 JSON 응답을 C# 개체로 변환합니다. JSON을 개체로 역직렬화하려면 System.Text.Json.JsonSerializer 클래스를 사용합니다.

  1. Repository.cs라는 파일을 만들고 다음 코드를 추가합니다.

    public record class Repository(string name);
    

    위 코드는 GitHub API에서 반환된 JSON 개체를 나타내도록 클래스를 정의합니다. 이 클래스는 리포지토리 이름 목록을 표시하는 데 사용합니다.

    리포지토리 개체의 JSON은 수십 개의 속성을 포함하지만, 이 중에서 name 속성만 역직렬화됩니다. 직렬 변환기는 대상 클래스에 일치하는 항목이 없는 JSON 속성을 자동으로 무시합니다. 따라서 대규모 JSON 패킷의 일부 필드에만 작용하는 형식을 쉽게 만들 수 있습니다.

    C# 변환은 속성 이름의 첫 문자를 대문자로 변환하지만, 여기서 name 속성은 JSON에 있는 항목과 정확하게 일치하도록 소문자로 시작합니다. 뒤에서 JSON 속성 이름과 일치하지 않는 C# 속성 이름을 사용하는 방법을 알아봅니다.

  2. 직렬 변환기를 사용하여 JSON을 C# 개체로 변환합니다. ProcessRepositoriesAsync 메서드의 GetStringAsync(String) 호출을 다음 줄로 바꿉니다.

    await using Stream stream =
        await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
    var repositories =
        await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
    

    업데이트된 코드는 GetStringAsync(String)GetStreamAsync(String)으로 바꿉니다. 이 직렬 변환기 메서드는 문자열이 아닌 스트림을 소스로 사용합니다.

    JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions, CancellationToken)에 대한 첫 번째 인수는 await 식입니다. await 식은 지금까지는 대입문의 일부로만 볼 수 있었지만 코드의 거의 모든 위치에 나올 수 있습니다. 다른 두 매개 변수 JsonSerializerOptionsCancellationToken은 선택 사항이며 코드 조각에서 생략됩니다.

    DeserializeAsync 메서드는 ‘제네릭’입니다. 즉, JSON 텍스트에서 만들어야 하는 개체 종류에 대한 형식 인수를 제공해야 합니다. 이 예제에서는 다른 제네릭 개체 System.Collections.Generic.List<T>List<Repository>로 역직렬화합니다. List<T> 클래스는 개체의 컬렉션을 저장합니다. 형식 인수는 List<T>에 저장된 개체의 형식을 선언합니다. JSON 텍스트는 Repository 리포지토리 개체의 컬렉션을 나타내므로 type 인수는 레코드입니다.

  3. 각 리포지토리의 이름을 표시하도록 코드를 추가합니다. 다음 줄을

    Console.Write(json);
    

    다음 코드와 바꿉니다.

    foreach (var repo in repositories ?? Enumerable.Empty<Repository>())
        Console.Write(repo.name);
    
  4. 다음 using 지시문은 파일의 맨 위에 있어야 합니다.

    using System.Net.Http.Headers;
    using System.Text.Json;
    
  5. 앱을 실행합니다.

    dotnet run
    

    .NET Foundation에 포함된 리포지토리의 이름 목록이 출력됩니다.

deserialization 구성

  1. Repository.cs에서 파일 내용을 다음 C#으로 바꿉니다.

    using System.Text.Json.Serialization;
    
    public record class Repository(
        [property: JsonPropertyName("name")] string Name);
    

    이 코드에서는 다음을 수행합니다.

    • name 속성의 이름을 Name로 변경합니다.
    • JsonPropertyNameAttribute 추가하여 이 속성이 JSON에 표시되는 방식을 지정합니다.
  2. Program.cs에서 Name 속성의 새로운 대문자 표시를 사용하도록 코드를 업데이트합니다.

    foreach (var repo in repositories)
       Console.Write(repo.Name);
    
  3. 앱을 실행합니다.

    출력은 동일합니다.

코드 리팩터링

ProcessRepositoriesAsync 메서드는 비동기 작업을 수행하고 리포지토리 컬렉션을 반환할 수 있습니다. 를 반환 Task<List<Repository>>하도록 해당 메서드를 변경하고 호출자 근처에 있는 콘솔에 쓰는 코드를 이동합니다.

  1. ProcessRepositoriesAsync의 시그니처를 변경하여 Repository 개체의 목록을 해당 결과로 표시하는 작업을 반환합니다.

    static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
    
  2. JSON 응답을 처리한 후 리포지토리를 반환합니다.

    await using Stream stream =
        await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
    var repositories =
        await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
    return repositories ?? new();
    

    이 개체를 async로 표시했으므로 컴파일러는 반환 값에 대해 Task<T> 개체를 생성합니다.

  3. Program.cs 파일을 수정하고 에 대한 호출을 ProcessRepositoriesAsync 다음으로 바꿔 결과를 캡처하고 각 리포지토리 이름을 콘솔에 씁니다.

    var repositories = await ProcessRepositoriesAsync(client);
    
    foreach (var repo in repositories)
        Console.Write(repo.Name);
    
  4. 앱을 실행합니다.

    출력은 동일합니다.

역직렬화 추가 속성

다음 단계에서는 받은 JSON 패킷에서 더 많은 속성을 처리하는 코드를 추가합니다. 모든 속성을 추가할 필요는 없겠지만, 몇 개를 추가해 봄으로써 C#의 다른 기능에 대해 알아볼 수 있습니다.

  1. 클래스의 Repository 내용을 다음 record 정의로 바꿉니다.

    using System.Text.Json.Serialization;
    
    public record class Repository(
        [property: JsonPropertyName("name")] string Name,
        [property: JsonPropertyName("description")] string Description,
        [property: JsonPropertyName("html_url")] Uri GitHubHomeUrl,
        [property: JsonPropertyName("homepage")] Uri Homepage,
        [property: JsonPropertyName("watchers")] int Watchers);
    

    Uriint 형식은 문자열 표현 간에 변환하는 기능을 기본적으로 제공합니다. JSON 문자열에서 이러한 대상 유형으로 역직렬화할 때 추가 코드를 사용할 필요가 없습니다. JSON 패킷에 대상 형식으로 변환되지 않는 데이터가 있는 경우 serialization 작업이 예외를 throw합니다.

  2. foreachProgram.cs 파일의 루프를 업데이트하여 속성 값을 표시합니다.

    foreach (var repo in repositories)
    {
        Console.WriteLine($"Name: {repo.Name}");
        Console.WriteLine($"Homepage: {repo.Homepage}");
        Console.WriteLine($"GitHub: {repo.GitHubHomeUrl}");
        Console.WriteLine($"Description: {repo.Description}");
        Console.WriteLine($"Watchers: {repo.Watchers:#,0}");
        Console.WriteLine();
    }
    
  3. 앱을 실행합니다.

    이제 목록에 추가 속성이 포함됩니다.

데이터 속성 추가

JSON 응답에서 마지막 푸시 작업의 날짜는 다음과 같은 형식을 갖습니다.

2016-02-08T21:27:00Z

이 형식은 UTC(협정 세계시) 형식이므로 deserialization의 결과는 Kind 속성이 UtcDateTime 값입니다.

사용자의 표준 시간대로 표현된 날짜와 시간을 가져오려면 사용자 지정 변환 메서드를 작성해야 합니다.

  1. Repository.cs에서 날짜 및 시간의 UTC 표현에 대한 속성과 로컬 시간으로 변환된 날짜를 반환하는 readonly LastPush 속성을 추가합니다. 파일은 다음과 같습니다.

    using System.Text.Json.Serialization;
    
    public record class Repository(
        [property: JsonPropertyName("name")] string Name,
        [property: JsonPropertyName("description")] string Description,
        [property: JsonPropertyName("html_url")] Uri GitHubHomeUrl,
        [property: JsonPropertyName("homepage")] Uri Homepage,
        [property: JsonPropertyName("watchers")] int Watchers,
        [property: JsonPropertyName("pushed_at")] DateTime LastPushUtc)
    {
        public DateTime LastPush => LastPushUtc.ToLocalTime();
    }
    

    LastPush 속성은 get 접근자에 대한 식 본문 멤버를 사용하여 정의됩니다. set 접근자는 없습니다. 접근자를 set 생략하는 것은 C#에서 읽기 전용 속성을 정의하는 한 가지 방법입니다. (C#에서 쓰기 전용 속성을 만들 수 있지만 해당 값은 제한됩니다.)

  2. Program.cs에서 또 다른 출력 문을 추가합니다.

    Console.WriteLine($"Last push: {repo.LastPush}");
    
  3. 전체 앱은 다음 Program.cs 파일과 유사해야 합니다.

    using System.Net.Http.Headers;
    using System.Text.Json;
    
    using HttpClient client = new();
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
    client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
    
    var repositories = await ProcessRepositoriesAsync(client);
    
    foreach (var repo in repositories)
    {
        Console.WriteLine($"Name: {repo.Name}");
        Console.WriteLine($"Homepage: {repo.Homepage}");
        Console.WriteLine($"GitHub: {repo.GitHubHomeUrl}");
        Console.WriteLine($"Description: {repo.Description}");
        Console.WriteLine($"Watchers: {repo.Watchers:#,0}");
        Console.WriteLine($"{repo.LastPush}");
        Console.WriteLine();
    }
    
    static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
    {
        await using Stream stream =
            await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
        var repositories =
            await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
        return repositories ?? new();
    }
    
  4. 앱을 실행합니다.

    출력에 각 리포지토리에 대한 마지막 푸시의 날짜 및 시간이 포함됩니다.

다음 단계

이 자습서에서는 웹 요청을 수행하고 결과를 구문 분석하는 앱을 만들었습니다. 여러분이 만든 앱의 버전은 이제 완성된 샘플과 일치할 것입니다.

.NET에서 JSON을 직렬화 및 역직렬화(마샬링 및 역 마샬링)하는 방법에서 JSON serialization을 구성하는 방법을 알아보세요.