ASP.NET Core를 사용하여 네이티브 모바일 앱용 백 엔드 서비스 만들기

James Montemagno 작성

모바일 앱은 ASP.NET Core 백 엔드 서비스와 통신할 수 있습니다. iOS 시뮬레이터 및 Android 에뮬레이터에서 로컬 웹 서비스를 연결하는 방법에 대한 지침은 iOS 시뮬레이터 및 Android 에뮬레이터에서 로컬 웹 서비스에 연결을 참조하세요.

샘플 백 엔드 서비스 코드 보기 및 다운로드

샘플 네이티브 모바일 앱

이 자습서에서는 네이티브 모바일 앱을 지원하기 위해 ASP.NET Core를 사용하여 백 엔드 서비스를 만드는 방법을 보여 줍니다. Android, iOS 및 Windows에 대한 별도 네이티브 클라이언트를 포함하는 네이티브 클라이언트로 Xamarin.Forms TodoRest 앱을 사용합니다. 연결된 자습서에 따라 네이티브 앱을 만들고 필요한 무료 Xamarin 도구를 설치하고 Xamarin 샘플 솔루션을 다운로드할 수 있습니다. Xamarin 샘플에는 이 문서의 ASP.NET Core 앱이 바꾸는 ASP.NET Core Web API 서비스 프로젝트가 포함되어 있습니다(클라이언트에서 필요한 변경 내용 없이).

Android 스마트폰에서 실행되는 To Do Rest 애플리케이션

기능

ToDoREST 앱은 할 일 항목 나열, 추가, 삭제 및 업데이트를 지원합니다. 각 항목에는 ID, 이름, 메모 및 완료되었는지 여부를 나타내는 속성이 있습니다.

이전 예제에서 항목의 기본 보기는 각 항목의 이름을 나열하고 검사mark로 완료되었는지를 나타냅니다.

+ 아이콘을 누르면 항목 추가 대화 상자를 엽니다.

항목 추가 대화 상자

기본 목록 화면에서 항목을 누르면 항목의 이름, 메모 및 완료 설정을 수정할 수 있거나 항목을 삭제할 수 있는 편집 대화 상자를 엽니다.

항목 편집 대화 상자

컴퓨터에서 실행되는 다음 섹션에서 만든 ASP.NET Core 앱을 직접 테스트하려면 앱의 RestUrl 상수를 업데이트합니다.

Android 에뮬레이터는 로컬 컴퓨터에서 실행되지 않으며 루프백 IP(10.0.2.2)를 사용하여 로컬 컴퓨터와 통신합니다. Xamarin.Essentials DeviceInfo를 사용하여 올바른 URL을 사용하기 위해 시스템이 실행 중인 운영 체제를 검색합니다.

TodoREST 폴더로 이동하여 Constants.cs 파일을 엽니다. 파일에는 Constants.cs 다음 구성이 포함됩니다.

using Xamarin.Essentials;
using Xamarin.Forms;

namespace TodoREST
{
    public static class Constants
    {
        // URL of REST service
        //public static string RestUrl = "https://YOURPROJECT.azurewebsites.net:8081/api/todoitems/{0}";

        // URL of REST service (Android does not use localhost)
        // Use http cleartext for local deployment. Change to https for production
        public static string RestUrl = DeviceInfo.Platform == DevicePlatform.Android ? "http://10.0.2.2:5000/api/todoitems/{0}" : "http://localhost:5000/api/todoitems/{0}";
    }
}

필요에 따라 Azure와 같은 클라우드 서비스에 웹 서비스를 배포하고 RestUrl을 업데이트할 수 있습니다.

ASP.NET Core 프로젝트 만들기

Visual Studio에서 새 ASP.NET Core 웹 애플리케이션을 만듭니다. Web API 템플릿을 선택합니다. 프로젝트 이름을 TodoAPI로 지정합니다.

Web API 프로젝트 템플릿이 선택된 새 ASP.NET 웹 애플리케이션 대화 상자

앱은 모바일 클라이언트에 대한 텍스트 지우기 HTTP 트래픽을 포함하여 포트 5000에 대한 모든 요청에 응답해야 합니다. 업데이트 Startup.csUseHttpsRedirection 개발에서 실행되지 않습니다.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // For mobile apps, allow http traffic.
        app.UseHttpsRedirection();
    }

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

참고 항목

IIS Express가 아닌 앱을 직접 실행합니다. IIS Express는 기본적으로 로컬이 아닌 요청을 무시합니다. 명령 프롬프트에서 dotnet run을 실행하거나 Visual Studio 도구 모음의 디버그 대상 드롭다운에서 앱 이름 프로필을 선택합니다.

할 일 항목을 나타내도록 모델 클래스를 추가합니다. [Required] 특성으로 필수 필드를 표시합니다.

using System.ComponentModel.DataAnnotations;

namespace TodoAPI.Models
{
    public class TodoItem
    {
        [Required]
        public string ID { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Notes { get; set; }

        public bool Done { get; set; }
    }
}

API 메서드에는 데이터를 사용하는 방법이 필요합니다. 원래 Xamarin 샘플에서 사용하는 동일한 ITodoRepository 인터페이스를 사용합니다.

using System.Collections.Generic;
using TodoAPI.Models;

namespace TodoAPI.Interfaces
{
    public interface ITodoRepository
    {
        bool DoesItemExist(string id);
        IEnumerable<TodoItem> All { get; }
        TodoItem Find(string id);
        void Insert(TodoItem item);
        void Update(TodoItem item);
        void Delete(string id);
    }
}

이 샘플의 경우 구현은 항목의 개별 컬렉션을 사용합니다.

using System.Collections.Generic;
using System.Linq;
using TodoAPI.Interfaces;
using TodoAPI.Models;

namespace TodoAPI.Services
{
    public class TodoRepository : ITodoRepository
    {
        private List<TodoItem> _todoList;

        public TodoRepository()
        {
            InitializeData();
        }

        public IEnumerable<TodoItem> All
        {
            get { return _todoList; }
        }

        public bool DoesItemExist(string id)
        {
            return _todoList.Any(item => item.ID == id);
        }

        public TodoItem Find(string id)
        {
            return _todoList.FirstOrDefault(item => item.ID == id);
        }

        public void Insert(TodoItem item)
        {
            _todoList.Add(item);
        }

        public void Update(TodoItem item)
        {
            var todoItem = this.Find(item.ID);
            var index = _todoList.IndexOf(todoItem);
            _todoList.RemoveAt(index);
            _todoList.Insert(index, item);
        }

        public void Delete(string id)
        {
            _todoList.Remove(this.Find(id));
        }

        private void InitializeData()
        {
            _todoList = new List<TodoItem>();

            var todoItem1 = new TodoItem
            {
                ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
                Name = "Learn app development",
                Notes = "Take Microsoft Learn Courses",
                Done = true
            };

            var todoItem2 = new TodoItem
            {
                ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
                Name = "Develop apps",
                Notes = "Use Visual Studio and Visual Studio for Mac",
                Done = false
            };

            var todoItem3 = new TodoItem
            {
                ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
                Name = "Publish apps",
                Notes = "All app stores",
                Done = false,
            };

            _todoList.Add(todoItem1);
            _todoList.Add(todoItem2);
            _todoList.Add(todoItem3);
        }
    }
}

다음에서 구현을 구성합니다.Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ITodoRepository, TodoRepository>();
    services.AddControllers();
}

컨트롤러 만들기

프로젝트에 새 컨트롤러, TodoItemsController를 추가합니다. ControllerBase에서 상속되어야 합니다. 컨트롤러가 Route 경로에 대한 요청을 처리함을 나타내는 특성을 추가합니다 api/todoitems. 경로의 [controller] 토큰은 컨트롤러의 이름으로 대체되며(Controller 접미사 생략) 글로벌 경로에 대해 특히 유용합니다. 라우팅에 대해 자세히 알아봅니다.

컨트롤러는 작동하기 위해 ITodoRepository가 필요합니다. 컨트롤러의 생성자를 통해 이 유형의 인스턴스를 요청합니다. 런타임 시 이 인스턴스는 종속성 주입에 대한 프레임워크의 지원을 사용하여 제공됩니다.

[ApiController]
[Route("api/[controller]")]
public class TodoItemsController : ControllerBase
{
    private readonly ITodoRepository _todoRepository;

    public TodoItemsController(ITodoRepository todoRepository)
    {
        _todoRepository = todoRepository;
    }

이 API는 데이터 원본에서 CRUD(만들기, 읽기, 업데이트, 삭제) 작업을 수행하도록 4개의 다른 HTTP 동사를 지원합니다. 이 중 가장 간단한 것은 HTTP GET 요청에 해당하는 읽기 작업입니다.

curl을 사용하여 API 테스트

다양한 도구를 사용하여 API 메서드를 테스트할 수 있습니다. 이 자습서에서는 다음 오픈 소스 명령줄 도구가 사용됩니다.

  • curl: HTTP 및 HTTPS를 비롯한 다양한 프로토콜을 사용하여 데이터를 전송합니다. curl은 HTTP 메서드GET, POSTPUTDELETE.를 사용하여 API를 호출하는 데 이 자습서에서 사용됩니다.
  • jq: JS이 자습서에서 API 응답에서 쉽게 읽을 수 있도록 ON 데이터의 형식 JS을 지정하는 데 사용되는 ON 프로세서입니다.

curl 및 jq 설치

curl은 macOS에 사전 설치되며 macOS 터미널 애플리케이션 내에서 직접 사용됩니다. curl 설치에 대한 자세한 내용은 공식 curl 웹 사이트를 참조하세요.

jq는 터미널에서 brew에서 Home설치할 수 있습니다.

다음 명령을 사용하여 아직 설치되지 않은 경우 brew를 설치 Home합니다.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

설치 관리자가 제공하는 지침을 따릅니다.

다음 명령을 사용하여 brew를 사용하여 Homejq를 설치합니다.

brew install jq

brew 및 jq 설치에 대한 Home자세한 내용은 brewjq를 참조하세요Home.

항목 읽기

항목의 목록 요청은 List 메서드에 대한 GET 요청으로 수행됩니다. List 메서드의 [HttpGet] 특성은 이 작업이 GET 요청만을 처리해야 함을 나타냅니다. 이 작업에 대한 경로는 컨트롤러에 지정된 경로입니다. 경로의 일부로 작업 이름을 사용할 필요가 없습니다. 각 작업에 고유하고 명확한 경로가 있도록 하기만 하면 됩니다. 라우팅 특성은 특정 경로를 작성하도록 컨트롤러와 메서드 수준 모두에 적용될 수 있습니다.

[HttpGet]
public IActionResult List()
{
    return Ok(_todoRepository.All);
}

터미널에서 다음 curl 명령을 호출합니다.

curl -v -X GET 'http://localhost:5000/api/todoitems/' | jq

이전 curl 명령에는 다음 구성 요소가 포함됩니다.

  • -v: 자세한 정보 표시 모드를 활성화하여 HTTP 응답에 대한 자세한 정보를 제공하며 API 테스트 및 문제 해결에 유용합니다.
  • -X GET: 요청에 대한 HTTP GET 메서드의 사용을 지정합니다. curl은 의도한 HTTP 메서드를 유추하는 경우가 많지만 이 옵션을 사용하면 명시적으로 지정할 수 있습니다.
  • 'http://localhost:5000/api/todoitems/': 요청의 대상 URL입니다. 이 경우 API 엔드포인트입니다 REST .
  • | jq: 이 세그먼트는 curl과 직접 관련이 없습니다. 파이프 | 는 왼쪽의 명령에서 출력을 가져와서 오른쪽의 명령으로 "파이프"하는 셸 연산자입니다. jq 는 명령줄 JSON 프로세서입니다. 필수 jq 는 아니지만 반환 JS된 ON 데이터를 더 쉽게 읽을 수 있습니다.

이 메서드는 List ON으로 JS직렬화된 200 OK 응답 코드와 모든 Todo 항목을 반환합니다.

[
  {
    "id": "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
    "name": "Learn app development",
    "notes": "Take Microsoft Learn Courses",
    "done": true
  },
  {
    "id": "b94afb54-a1cb-4313-8af3-b7511551b33b",
    "name": "Develop apps",
    "notes": "Use Visual Studio and Visual Studio for Mac",
    "done": false
  },
  {
    "id": "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
    "name": "Publish apps",
    "notes": "All app stores",
    "done": false
  }
]

항목 만들기

규칙에 따라 새 데이터 항목 만들기는 HTTP POST 동사에 매핑됩니다. Create 메서드는 [HttpPost] 특성이 적용되어 있으며 TodoItem 인스턴스를 허용합니다. item 인수는 POST 본문에 전달되기 때문에 이 매개 변수는 [FromBody] 특성을 지정합니다.

메서드 내부에서 유효성 및 데이터 저장소의 이전 존재에 대해 항목이 선택되며 문제가 발생하지 않는 경우 리포지토리를 사용하여 추가됩니다. ModelState.IsValid를 확인하면 모델 유효성 검사를 수행하고, 사용자 입력을 허용하는 모든 API 메서드에서 수행되어야 합니다.

[HttpPost]
public IActionResult Create([FromBody]TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        bool itemExists = _todoRepository.DoesItemExist(item.ID);
        if (itemExists)
        {
            return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
        }
        _todoRepository.Insert(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
    }
    return Ok(item);
}

샘플은 모바일 클라이언트에 전달되는 오류 코드를 포함하는 enum을 사용합니다.

public enum ErrorCode
{
    TodoItemNameAndNotesRequired,
    TodoItemIDInUse,
    RecordNotFound,
    CouldNotCreateItem,
    CouldNotUpdateItem,
    CouldNotDeleteItem
}

터미널에서 동사를 사용하여 POST 다음 curl 명령을 호출하고 요청 본문에 ON 형식으로 새 개체 JS를 제공하여 새 항목 추가를 테스트합니다.

curl -v -X POST 'http://localhost:5000/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "6bb8b868-dba1-4f1a-93b7-24ebce87e243",
  "name": "A Test Item",
  "notes": "asdf",
  "done": false
}' | jq

이전 curl 명령에는 다음 옵션이 포함되어 있습니다.

  • --header 'Content-Type: application/json': 요청 본문에 Content-Type ON 데이터가 포함되어 있음을 나타내는 헤더 application/json를 JS설정합니다.
  • --data '{...}': 요청 본문에 지정된 데이터를 보냅니다.

메서드는 응답에서 새로 만든 항목을 반환합니다.

항목 업데이트

레코드 수정은 HTTP PUT 요청을 사용하여 수행됩니다. 이 변경 외에 Edit 메서드는 Create와 거의 동일합니다. 레코드를 찾을 Edit 수 없으면 작업은 (404) 응답을 반환합니다 NotFound .

[HttpPut]
public IActionResult Edit([FromBody] TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        var existingItem = _todoRepository.Find(item.ID);
        if (existingItem == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Update(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
    }
    return NoContent();
}

curl로 테스트하려면 동사를 .로 변경합니다 PUT. 요청의 본문에서 업데이트된 개체 데이터를 지정합니다.

curl -v -X PUT 'http://localhost:5000/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "6bb8b868-dba1-4f1a-93b7-24ebce87e243",
  "name": "A Test Item",
  "notes": "asdf",
  "done": true
}' | jq

이 메서드는 성공한 경우 기존 API와의 일관성을 위해 NoContent(204) 응답을 반환합니다.

항목 삭제

레코드 삭제는 서비스에 대한 요청을 수행하고 DELETE 삭제할 항목의 ID를 전달하여 수행됩니다. 업데이트와 마찬가지로 존재하지 않는 항목에 대한 요청은 응답을 받습니다 NotFound . 그렇지 않으면 성공적인 요청은 NoContent (204) 응답을 반환합니다.

[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
    try
    {
        var item = _todoRepository.Find(id);
        if (item == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Delete(id);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
    }
    return NoContent();
}

HTTP 동사를 변경하고 URL 끝에 삭제할 DELETE 데이터 개체의 ID를 추가하여 curl로 테스트합니다. 요청 본문에는 아무 것도 필요하지 않습니다.

curl -v -X DELETE 'http://localhost:5000/api/todoitems/6bb8b868-dba1-4f1a-93b7-24ebce87e243'

과도한 게시 방지

현재 샘플 앱은 전체 TodoItem 개체를 공개합니다. 일반적으로 프로덕션 앱은 모델의 하위 집합을 사용하여 입력 및 반환되는 데이터를 제한합니다. 이 동작에는 여러 가지 이유가 있으며, 보안이 주요 이유 중 하나입니다. 일반적으로 모델의 하위 집합을 DTO(데이터 전송 개체), 입력 모델 또는 뷰 모델이라고 합니다. 이 문서에서는 DTO를 사용합니다.

DTO는 다음과 같은 용도로 사용할 수 있습니다.

  • 과도한 게시를 방지합니다.
  • 클라이언트에서 볼 수 없는 속성을 숨깁니다.
  • 페이로드 크기를 줄이기 위해 일부 속성을 생략합니다.
  • 중첩된 개체를 포함하는 개체 그래프를 평면화합니다. 클라이언트에는 평면화된 개체 그래프가 더 편리할 수 있습니다.

DTO 방법을 보여 주려면 초과 게시 방지를 참조하세요.

공통 Web API 규칙

앱에 대한 백 엔드 서비스를 개발할 때 교차 절삭 문제를 처리하기 위한 일관된 규칙 또는 정책 집합을 마련하려고 합니다. 예를 들어 이전에 표시된 서비스에서 찾을 수 없는 특정 레코드에 대한 요청은 응답이 아닌 BadRequest 응답을 받았습니다NotFound. 마찬가지로, 모델 바인딩 형식을 전달한 이 서비스에 대해 만든 명령은 항상 ModelState.IsValid를 확인했고 잘못된 모델 유형에 대해 BadRequest를 반환했습니다.

API에 대한 일반적인 정책을 식별했으면 필터에서 캡슐화할 수 있습니다. ASP.NET Core MVC 애플리케이션에서 일반적인 API 정책을 캡슐화하는 방법에 대해 자세히 알아봅니다.

추가 리소스