21. RESTful 서비스를 위한 ASP.NET 4.8 Web API

  • 50 minutes to read
NOTE

ASP.NET 4.8 Web API를 사용하면 클라이언트측 자바스크립트 라이브러리와 연동되는 코드를 쉽게 제작할 수 있습니다. Web API를 사용하여 JSON 데이터를 만들고 이를 사용하여 제이쿼리와 앵귤러 라이브러리를 사용한 단일 페이지 응용 프로그램을 제작하는 방법을 살펴보겠습니다.

21.1 ASP.NET Web API 소개
Web API는 브라우저 및 모바일 장치를 비롯한 다양한 클라이언트에 연결할 수 있는 RESTful HTTP 서비스를 말합니다. ASP.NET Web API는 여러 장치(모바일, 태블릿, PC 등)에서 사용할 HTTP 서비스를 쉽게 구축할 수 있도록 도와주는 프레임워크다. REST(REpresentational State Transfer)는 클라이언트와 서버 간의 데이터를 주고 받는 일반적인 스타일을 의미하며 상태가 없고(Stateless), 리소스 기반(URL)으로 HTTP 메서드(Get, Post, Put, Delete)를 구현하는 방법을 말합니다. 쉽게 말해서 브라우저와 다른 클라이언트가 서버의 자료를 쉽게 읽어갈 수 있도록 허용하는 서비스다. 이러한 RESTful 서비스를 구축하는 방법 중 최선의 선택이 될 수 있는 기술이 바로 ASP.NET Web API입니다. 그림 21 1 ASP.NET Web API과 ASP.NET의 다른 서비스들

ASP.NET Web API는 직접 UI를 출력하는 대신에 JSON과 XML이 데이터를 주고받는 형태로 많이 사용됩니다. 이러한 Web API는 jQuery, Angular 등을 사용한 SPA(Single Page Application)를 구현하는데 필수 항목 중 하나로 사용됩니다. ASP.NET Web API는 MVC 프레임워크의 기능 대부분을 제공합니다. 단, HTML을 출력하는 부분은 MVC와 다른 JavaScript 라이브러리가 담당합니다. ASP.NET Core로 넘어오면서 MVC에 Web API가 포함된 형태로 구현할 수 있게 되었습니다. 그림 21 2 ASP.NET Web Pages, MVC, Web API

21.1.1 ASP.NET Web API 주요 특징:

  • HTTP 기반 서비스 생성
  • RESTful API
  • 데이터를 다양한 클라이언트에게 전송
  • GET, PUT, POST, DELETE HTTP 메서드(Verbs) 제공
  • 사용자 정의 라우트 제공
  • 클라이언트에게 전송되는 데이터 커스터마이징 제공

21.2 SPA(Single Page Application): 단일 페이지 응용 프로그램 단일 페이지 응용 프로그램이라 불리는 SPA는 말 그대로 하나의 페이지에서 데이터 입력, 출력, 수정, 삭제 기능을 모두 구현하는 웹 페이지 제작 방식입니다. 이를 구현하기 위해서는 JavaScript 라이브러리와 Web API가 필요합니다. ASP.NET Web API로 서버 측에 CRUD(Create, Read, Update, Delete) 코드를 구현해 놓는다. 클라이언트 측에서는 jQuery, Angular 등의 라이브러리를 사용하거나 순수 JavaScript 코드를 사용하여 서버에 접근해서 데이터를 주고 받습니다. 이런 방식으로 단일 페이지 또는 순수 HTML 페이지에서 서버의 자원을 사용하는 응용 프로그램을 제작할 수 있습니다. SPA는 페이지를 한 번 로드한 후에는 JSON, XML과 같이 순수 데이터만을 주고 받기 때문에 네트워크 트래픽을 줄여 성능을 향상시킬 수 있습니다.

21.3 Web API를 사용한 CRUD 서비스 HTTP Verb 또는 HTTP 메서드는 입력, 출력, 수정, 삭제, 네 가지 동작을 구현합니다. 이러한 동작은 POST, GET, PUT, DELETE 단어로 표현됩니다.

21.4 Web API 구현 절차 ASP.NET Web API를 사용하여 간단한 Web API를 구현하는 절차는 이어질 실습 내용을 참고하면 좀 더 쉽게 이해할 수 있을 것입니다. 간단히 미리 정리해보면 다음과 같은 순서로 진행할 수 있습니다. 첫 번째, ApiController 클래스를 상속하는 클래스를 만듭니다. 두 번째, 각각의 액션 메서드를 구현합니다. 액션 메서드는 HTTP 메서드와 매핑되고, 액션 메서드의 접두사 이름은 HTTP 메서드와 매핑됩니다. 예를 들어 GetComment 메서드는 HTTP GET 메서드와 매핑되고, PostComment 메서드는 HTTP POST 메서드와 매핑됩니다. 물론 액션 메서드를 기존 이름으로 사용하되 HTTP 메서드로 노출하려면 [HttpGet/HttpPost/HttpPut/HttpDelete] 등의 특성을 사용합니다.

21.5 [실습] ASP.NET Web API 처음으로 만들어보기 21.5.1 소개 RESTful 서비스 구축을 위한 ASP.NET Web API를 한 번 만들어보겠습니다. 텍스트, JSON, XML 등으로 값을 반환하고자 할 때 사용되는 Web API의 기본 내용을 배운다. 이를 통해 서버 측의 정보를 가져와 클라이언트 측의 JavaScript 등에서 사용할 수 있습니다.

<참고 > 이번 실습과 관련해서 "ASP.NET 4.6 Web API 처음으로 만들어보기"라는 제목의 동영상 강좌로도 마련하였으니 참고하기 바랍니다. https://youtu.be/fczA7NDY3pM </참고>

21.5.2 따라하기 (1) <Visual Studio 2019 > 파일 > 새로 만들기 > 프로젝트>를 선택합니다. 그림 21 3 새 프로젝트 생성

(2) 다음 그림과 같이 <Visual C# > 웹 > ASP.NET 웹 응용 프로그램>을 선택 후 프로젝트 이름으로 DevWebAPI를 입력 후 <확인> 버튼을 클릭하여 프로젝트를 생성합니다. 그림 21 4 프로젝트 이름 및 위치 지정

(3) 템플릿 선택 화면에서 비어 있음(Empty) 선택 후 Web API에 대한 핵심 참조 추가 체크박스를 선택한 후 <확인> 버튼을 클릭합니다. 그림 21 5 프로젝트 템플릿 선택

(4) 최소 옵션으로 ASP.NET Web API 프로젝트가 생성되었습니다. 그림 21 6 Web API 프로젝트 생성

(5) DevWebAPI 프로젝트의 App_Start 폴더의 WebApiConfig.cs 파일을 열어보면 다음과 같이 Web API에 대한 기본 설정이 저장되어 있습니다. 그림 21 7 WebApiConfig.cs 파일 보기

WebApiConfig.cs
using System.Web.Http;

namespace DevWebAPI
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 구성 및 서비스

            // Web API 경로
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

Config.Route.MapHttpRoute() 메서드에 의해서 기본적인 Web API의 호출 위치가 결정됩니다. 기본값으로는 /api/WebAPI컨트롤러이름/id와같은매개변수이름 식으로 호출됩니다.

(6) 프로젝트 루트에 있는 Global.asax 파일을 열어보면 Web API에 대한 등록 코드가 있습니다. 만약 Web API 옵션을 선택하지 않고 프로젝트를 만들었다면 아래와 같은 코드와 클래스 파일은 생성되지 않습니다.

Global.asax
using System.Web.Http;

namespace DevWebAPI
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }
    }
}

(7) 솔루션 탐색기에서 참조 영역을 확장해보면 ASP.NET Web API 관련 어셈블리가 추가되어 있습니다. 그림 21 8 솔루션 탐색기의 참조 영역 확인

(8) Web API를 만들려면 Controller 폴더에서 마우스 오른쪽 버튼을 클릭한 후 <추가 > 컨트롤러>를 선택합니다. 그림 21 9 Web API 컨트롤러 추가

(9) <스캐폴드 추가> 창에서 <Web API 2 컨트롤러 - 비어 있음>을 선택 후 추가 학습: 버튼을 클릭합니다. 그림 21 10 Web API 컨트롤러 – 비어 있음 옵션 선택

(10) <컨트롤러 추가> 창에서 <컨트롤러 이름>을 HelloWorldController로 지정한 후 추가 학습: 버튼을 클릭합니다. 그림 21 11 HelloWorldController 컨트롤러 추가

(11) 생성된 HelloWorldController.cs 파일에 다음과 같이 코드를 추가합니다.

/Controllers/HelloWorldController.cs
using System.Collections.Generic;
using System.Web.Http;
 
namespace DevWebAPI.Controllers
{
    public class HelloWorldController : ApiController
    {
        public IEnumerable<string> Get()
        {
            return new string[] { "안녕하세요.", "반갑습니다." };
        }
    }
} 

Get() 메서드에서는 단순히 IEnumerable<T> 형태로 문자열 배열을 전송하는 형태입니다. 이곳의 반환값을 문자열 배열이 아닌 List<T> 형태의 컬렉션 형태로 반환하면 클라이언트 사이드에서는 JSON 또는 XML로 받을 수 있습니다.

(12) Visual Studio에서 [Ctrl]+[F5]를 입력하여 프로젝트를 실행합니다. 실행 시 UI 페이지가 따로 없으므로 주소 창에 다음과 같이 /api/HelloWorld 경로를 직접 요청해서 실행합니다. 참고로 엣지 브라우저에서는 웹 브라우저 화면에 바로 텍스트로 표현해줍니다. 정확히 클라이언트 측으로 전달된 값을 확인하려면 브라우저에서 [F12]을 눌러 개발자 도구를 실행합니다. 결과가 나타나지 않을 경우에는 웹 브라우저의 새로 고침 버튼을 다시 눌러서 응답 결과를 재전송 받습니다. 그림 21 12 Web API를 브라우저로 실행 결과

같은 경로를 크롬 웹 브라우저로 실행하면 다음과 같이 출력됩니다. 그림 21 13 크롬 웹 브라우저로 실행

<참고> Web API 테스트 도구 ASP.NET Web API를 테스트하기 위한 무료 도구로는 크롬 웹 브라우저의 확장 도구인 Postman 이 있는데 아래 경로에서 다운로드 받아 사용할 수 있습니다.  http://www.getpostman.com/ </참고>

추가 학습: 웹 디버깅 도구로는 Fiddler 또는 크롬 웹 브라우저의 확장 도구인 Postman이 많이 사용됩니다. 이 책에서는 Postman이 사용됩니다.

(13) 개발자 도구의 네트워크 탭을 선택 후 페이지를 새로 고침합니다. 네트워크 탭의 HelloWorld Web API를 선택하고 오른쪽 영역에 있는 본문을 선택하면 다음 그림과 같이 서버 측으로부터 전송된 문자열 두 개가 JSON 형태로 반환됨을 알 수 있습니다.

그림 21 14 F12 개발자 도구의 네트워크 탭의 본문 영역 확인

참고로 구글 크롬 웹 브라우저로 같은 경로를 요청하면 JSON 대신에 XML로 반환됩니다. 그림 21 15 크롬 웹 브라우저로 Web API 호출

(14) 자, 이번에는 한 단계 더 진행해보겠습니다. 쿼리스트링으로 매개변수를 전달해서 매개변수에 따라서 서로 다른 값을 반환시켜주는 방식입니다. <솔루션 탐색기 > Controller > 추가 > 새 항목을 선택합니다.
그림 21 16 새로운 컨트롤러 추가

(15) 새 항목 추가 화면에서 <웹 > Web API 컨트롤러 클래스(V2.1)>를 선택하고, DefaultController.cs라는 이름을 입력한 후 추가 학습: 버튼을 클릭하여 Web API 컨트롤러를 프로젝트에 추가합니다. 그림 21 17 DefaultController 컨트롤러 추가

(16) 생성된 Web API 컨트롤러를 보겠습니다. 기본적인 코드가 제공되는 Web API가 만들어졌다. 그림 21 18 자동으로 만들어지는 Web API 코드

(17) value 부분 텍스트만 다음 코드와 같이 간단히 변경합니다.

/Controllers/DefaultController.cs 
using System.Collections.Generic;
using System.Web.Http;
 
namespace DevWebAPI.Controllers
{
    public class DefaultController : ApiController
    {
        // GET api/<controller>
        public IEnumerable<string> Get()
        {
            return new string[] { "안녕하세요.", "반갑습니다." };
        }
 
        // GET api/<controller>/5
        public string Get(int id)
        {
            return "입력한 값: " + id.ToString();
        }
 
        // POST api/<controller>
        public void Post([FromBody]string value)
        {
        }
 
        // PUT api/<controller>/5
        public void Put(int id, [FromBody]string value)
        {
        }
 
        // DELETE api/<controller>/5
        public void Delete(int id)
        {
        }
    }
}

코드에서처럼 일반적인 Get() 메서드는 전체 데이터를 출력하는 리스트 형태를 출력하고, 매개변수가 있는 Get() 메서드는 Web API 요청 쿼리스트링에 전송된 매개변수에 해당하는 값을 따로 출력합니다. 그 외 Post() 메서드는 데이터를 입력할 때, Put() 메서드는 데이터를 수정할 때, Delete() 메서드는 데이터를 삭제할 때 사용합니다. Get 메서드와 달리 Post, Put, Delete 메서드에 대한 테스트는 Web API 테스트 도구인 텔레릭의 피들러나 구글 크롬의 확장 기능인 PostMan 등을 통해서 테스트해 볼 수 있습니다. 물론 직접 jQuery와 Angular 코드를 구현해서 구현할 수도 있습니다.

(18) [Ctrl]+[F5]를 눌러 프로젝트를 웹 브라우저로 실행합니다. /api/Default 경로를 요청하면 다음과 같이 문자열 두 개가 반환됩니다. /api/Default/1234 또는 /api/Default?id=1234 식으로 경로를 요청하면 넘겨준 id 값에 해당하는 값을 출력해주는 Web API 액션 메서드가 실행됩니다.

그림 21 19 매개변수를 요청하여 Web API 실행

21.5.3 마무리 ASP.NET Web API는 서버 측에서 생성된 데이터를 텍스트나 JSON, XML 등으로 변환하여 반환시켜주어 클라이언트 측에서 사용할 수 있게 해줍니다. 이를 사용하면 JavaScript, jQuery, Angular와 같은 JavaScript 기반 기술을 사용하여 Web API에서 생성된 값을 가져다가 소비할 수 있는 웹 응용 프로그램을 쉽게 만들어 낼 수 있습니다.

21.5.4 매개변수를 갖는 Web API를 jQuery Ajax로 호출하기 데모 21.5.4.1 Web API 만들기

WeatherByProvincesController.cs
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace MemoEngine.Controllers
{
    public class WeatherByProvincesController : ApiController
    {
        [HttpGet]
        public IEnumerable<Weather> Get(string locationName)
        {
            List<Weather> weathers = new List<Weather>();

            weathers.Add(new Weather { Num = 1, Location = "서울", Cloud = 1234 });
            weathers.Add(new Weather { Num = 2, Location = "서울", Cloud = 2345 });
            weathers.Add(new Weather { Num = 3, Location = "서울", Cloud = 3456 });
            weathers.Add(new Weather { Num = 1, Location = "부산", Cloud = 3456 });

            return weathers.Where(w => w.Location == locationName).ToList(); 
        }
    }

    public class Weather
    {
        public int Num { get; set; }
        public string Location { get; set; }
        public float Cloud { get; set; }
    }
}

21.5.4.2 Web API 실행: Postman 사용 <실행> /api/WeatherByProvinces?locationName=서울 [ { "Num": 1, "Location": "서울", "Cloud": 1234 }, { "Num": 2, "Location": "서울", "Cloud": 2345 }, { "Num": 3, "Location": "서울", "Cloud": 3456 } ] </실행> <실행> /api/WeatherByProvinces?locationName=부산 [ { "Num": 1, "Location": "부산", "Cloud": 3456 } ] </실행>

21.5.4.3 Web API 테스트용 MVC 컨트롤러 생성

WeatherByProvinceController.cs
using System.Web.Mvc;

namespace MemoEngine.Controllers
{
    public class WeatherByProvinceController : Controller
    {
        // GET: WeatherByProvince
        public ActionResult Index()
        {
            return View();
        }
    }
}

21.5.4.4 jQuery를 사용하여 Web API 호출 및 HTML 동적으로 생성

/Views/WeatherByProvince/Index.cshtml
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title></title>
    <link href="~/Content/bootstrap.css" rel="stylesheet" />
    <script src="~/Scripts/jquery-3.3.1.js"></script>
</head>
<body>
    <div>
        <input type="button" name="btnSeoul" id="btnSeoul" value="서울 데이터" />
        <input type="button" name="btnBusan" id="btnBusan" value="부산 데이터" />
    </div>
    <div style="height: 185px; overflow: hidden; overflow-y: auto;">
        <table id="tblWeathers" class="table">
            <colgroup>
                <col style="width: 10%;" />
                <col style="width: 45%;" />
                <col style="width: 45%;" />
            </colgroup>
            <thead style="color:#00b9b7; background-color:#e1faf7;">
                <tr>
                    <th>번호</th>
                    <th>지역명</th>
                    <th>Cloud</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td colspan="3" class="text-center" style="height: 138px;">
                        <div>
                            날씨 정보가 없습니다. 
                        </div>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>

    <script>
        /*
         * 전역 변수
         */
        var weathers = null; 

        $(function () {
            $("#btnSeoul").click(function () {
                getWeatherByProvinces("서울");
            });
            $("#btnBusan").click(function () {
                getWeatherByProvinces("부산");
            });
        });

        /**
         * @@description 지역별 날씨 리스트를 가져옵니다.  
         * @@param locationName 지역 이름 
         */
        function getWeatherByProvinces(locationName) {
            $.get("/api/WeatherByProvinces?locationName=" + locationName)
                .done(function (data, textStatus, jqXHR) {
                    weathers = data; 

                    var tbody = $("#tblWeathers > tbody");
                    if (weathers.length > 0) {
                        tbody.html(""); 
                    }

                    for (var i = 0; i < weathers.length; i++) {
                        var html = "";
                        var item = weathers[i];

                        html += "<td>" + item.Num + "</td>";
                        html += "<td>" + item.Location + "</td>";
                        html += "<td>" + item.Cloud + "</td>";

                        tbody.append("<tr>" + html + "</tr>");
                    }       
                })
                .fail(function (jqXHR, textStatus, errorThrown) {

                });
        }
    </script>
</body>
</html>

```csharp 


21.5.4.5	웹 브라우저로 Web API와 jQuery Ajax 테스트
<실행> /WeatherByProvince/Index
 
</실행>
 

21.5.5	ASP.NET Web API에서 jQuery Ajax 그리고 Chart.js 까지 한번에 살펴보기
특정 프로젝트별 연간 난방 비용에 대해서 Chart로 관리자하고합니다. 
테스트 경로는 다음 URL에서 볼 수 있습니다.
http://kepop.azurewebsites.net/AnnualHeatingCostsPage.aspx

21.5.5.1	모델 클래스 만들기

```csharp 
/Controllers/HeatingCost.cs => /Controllers/AnnualHeatingCosts/HeatingCost.cs 위치 변경 
namespace MemoEngine.Controllers
{
    public class HeatingCost
    {
        public int Month { get; set; }
        public double Cost { get; set; }
    }
}

21.5.5.2 리포지토리 인터페이스 만들기

/Controllers/IAnnualHeatingCostsRepository.cs
using System.Collections.Generic;

namespace MemoEngine.Controllers
{
    public interface IAnnualHeatingCostsRepository
    {
        List<HeatingCost> GetHeatingCosts();
    }
}

21.5.5.3 리포지토리 클래스 만들기: 가짜 데이터를 갖는 가짜 데이터가 아닌 진짜 데이터를 갖는 AnnualHeatingCostsRepository.cs에서는 추후 실제 데이터를 출력할 예정입니다.

/Controllers/AnnualHeatingCostsRepositoryInMemory.cs
using System.Collections.Generic;

namespace MemoEngine.Controllers
{
    public class AnnualHeatingCostsRepositoryInMemory : IAnnualHeatingCostsRepository
    {
        public List<HeatingCost> GetHeatingCosts()
        {
            List<HeatingCost> costs = new List<HeatingCost>();

            // 1월부터 12월까지의 난방 비용 리스트 채우기: DB, 인-메모리(테스트)
            costs.Add(new HeatingCost { Month = 1, Cost = 10000 });
            costs.Add(new HeatingCost { Month = 2, Cost = 9000 });
            costs.Add(new HeatingCost { Month = 3, Cost = 8000 });
            costs.Add(new HeatingCost { Month = 4, Cost = 7000 });
            costs.Add(new HeatingCost { Month = 5, Cost = 5000 });
            costs.Add(new HeatingCost { Month = 6, Cost = 3000 });
            costs.Add(new HeatingCost { Month = 7, Cost = 1000 });
            costs.Add(new HeatingCost { Month = 8, Cost = 0 });
            costs.Add(new HeatingCost { Month = 9, Cost = 2000 });
            costs.Add(new HeatingCost { Month = 10, Cost = 5000 });
            costs.Add(new HeatingCost { Month = 11, Cost = 9000 });
            costs.Add(new HeatingCost { Month = 12, Cost = 12000 });

            return costs;
        }
    }
}

<실행>

21.5.5.4 ASP.NET Web API 컨트롤러 클래스 만들기

/Controllers/AnnualHeatingCostsController.cs
using System.Collections.Generic;
using System.Web.Http;

namespace MemoEngine.Controllers
{
    public class AnnualHeatingCostsController : ApiController
    {
        private readonly IAnnualHeatingCostsRepository _repository;

        public AnnualHeatingCostsController()
        {
            _repository = new AnnualHeatingCostsRepositoryInMemory();
        }

        // Route 특성을 사용하여 API 라우팅 재 정의: 특성 라우팅 
        [Route("api/Charts/AnnualHeatingCosts")]
        [HttpGet]
        public IEnumerable<HeatingCost> Get(string chartName = "heatingCosts")
        {
            return _repository.GetHeatingCosts(); 
        }
    }
}

21.5.5.5 Postman에서 Web API 테스트하기 <실행> /api/Charts/AnnualHeatingCosts [ { "Month": 1, "Cost": 10000 }, { "Month": 2, "Cost": 9000 }, … { "Month": 11, "Cost": 9000 }, { "Month": 12, "Cost": 12000 } ] </실행>

/Charts/AnnualHeatingCosts.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>연간 난방 비용</title>
    <link href="../Content/bootstrap.css" rel="stylesheet" />
</head>
<body>
    <div style="height: 185px; overflow: hidden; overflow-y: auto;">
        <table id="tblHeatingCosts" class="table table-bordered">
            <colgroup>
                <col style="width: 20%;" />
                <col style="width: 80%;" />
            </colgroup>
            <thead>
                <tr>
                    <th>월</th>
                    <th>난방비용</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td colspan="2" class="text-center" style="height: 138px;">
                        난방 정보가 없습니다. 
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
    <div style="width: 400px; height: 400px;">
        <canvas id="chartHeatingCosts" width="100" height="100"></canvas>
    </div>
    
    <script src="../Scripts/jquery-3.3.1.js"></script>
    <script src="../Scripts/bootstrap.js"></script>
    <script src="../Scripts/chartjs/Chart.js"></script>
    <script>
        /*
         *  전역 변수 
         */
        var heatingCosts = null; 

        $(function () {
            getAnnualHeatingCosts("아무거나");
        });

        /**
         * @description 연간 난방 비용 차트 정보를 가져옵니다. 
         * @param chartName 차트 이름: 연간 난방 비용, ... 
         */
        function getAnnualHeatingCosts(chartName) {
            var apiUrl = "/api/Charts/AnnualHeatingCosts?chartName=" + chartName;

            $.get(apiUrl)
                .done(function (data, textStatus, jqXHR) {
                    heatingCosts = data;

                    // 테이블 그리기
                    drawingTable();

                    // 차트 그리기
                    drwaingChart(); 
                })
                .fail(function (jqXHR, textStatus, errorThrown) {

                });
        }

        /**
         * @description 레코드셋을 가지고 테이블을 그립니다. 
         * */
        function drawingTable() {
            var tbody = $("#tblHeatingCosts > tbody");
            if (heatingCosts.length > 0) {
                tbody.html(""); 
            }

            // 테이블 그리기
            for (var i = 0; i < heatingCosts.length; i++) {
                var item = heatingCosts[i]; 
                var html = "";
                html += "<td>" + item.Month + "</td>";
                html += "<td>" + item.Cost + "</td>";

                tbody.append("<tr>" + html + "</tr>");
            }   
        }

        /**
         * @description Chart.js 라이브러리를 사용하여 bar 차트를 그립니다.
         * */
        function drwaingChart() {
            // 연간 난방 비용 차트 그리기
            var ctxHeatingCosts = document.getElementById("chartHeatingCosts");

            var arrMonth = [];
            var arrCost = [];

            for (var i = 0; i < heatingCosts.length; i++) {
                arrMonth.push(heatingCosts[i].Month);
                arrCost.push(heatingCosts[i].Cost);
            }   

            var chart = new Chart(ctxHeatingCosts, {
                type: "bar",
                data: {
                    labels: arrMonth,
                    datasets: [{
                        data: arrCost,
                        radius: 0,
                        fill: true,
                        lineTension: 0,
                        borderColor: "rgba(54, 162, 235, 0.75)",
                        backgroundColor: "rgba(54, 162, 235, 0.2)",
                        borderWidth: 0.7
                    }]
                },
                options: {
                    legend: {
                        display: false
                    },
                    scales: {
                        xAxes: [{
                            display: true,
                            scaleLabel: {
                                display: true,
                                labelString: '월'
                            }
                        }],
                        yAxes: [{
                            display: true,
                            scaleLabel: {
                                display: true,
                                labelString: '난방비용'
                            },
                            ticks: {
                                beginAtZero: true
                            }
                        }]
                    }
                }
            })
        }
    </script>
</body>
</html>

21.5.5.6 웹 페이지에서 최종 코드 실행하기

21.6 [실습] jQuery와 Angular 등을 사용한 SPA 응용 프로그램 구현 21.6.1 소개 Web API를 사용하여 간단한 CRUD 작업을 진행하는 JavaScript 웹 사이트를 만들어보겠습니다. jQuery와 Angular도 이용합니다. 다만, Web API 이외에 jQuery, Angular 등 JavaScript 라이브러리의 사용은 이 책의 내용을 벗어나므로 필수 학습용이 아닌 참고용으로 살펴보기 바랍니다.

21.6.2 따라하기 1: 데이터베이스에 테이블 구성 (1) 웹 응용 프로그램으로 명언을 입력하고 조회하는 서비스를 구축해보겠습니다. SQL Server DB에 다음과 같이 Maxims란 이름으로 테이블을 구성합니다. 만약 SQL Server 데이터베이스 프로젝트에 Maxims.sql 파일로 테이블을 만들고 관리하면 좀 더 체계적으로 테이블을 관리할 수 있을 것입니다. DotNetNote.Database 프로젝트를 열고 Tables 폴더에 Maxims 테이블을 다음과 같이 작성 후 DotNetNote 데이터베이스로 게시합니다.

/dbo/Tables/Maxims.sql
-- 명언(Maxim) 서비스
CREATE TABLE [dbo].[Maxims]
(
    [Id]            Int Primary Key Not Null Identity(1, 1),
    [Name]          NVarChar(25)    Not Null,                   -- 작성자
    [Content]       NVarChar(255)   Null,                       -- 명언 내용
    [CreationDate]  DateTime Default(GetDate())
)
Go

21.6.3 따라하기 2: 모델 클래스와 리포지토리 클래스 생성 (1) 새로운 프로젝트를 만들어서 진행해도 되지만, 앞서 ASP.NET 4.6 게시판 프로젝트를 진행한 경험이 있기에 이때 사용한 MemoEngine 프로젝트를 그대로 사용하겠습니다. C:\ASP.NET\ 폴더에 만들어 놓은 MemoEngine 프로젝트를 실행합니다.

(2) MemoEngine 프로젝트의 루트에 있는 Models 폴더에 Maxim.cs라는 이름으로 클래스를 생성합니다. SQL Server에 테이블이 생성되면 이 테이블에 대해서 데이터를 주고 받습니다. 사용할 모델 클래스인 Maxim 클래스를 다음과 같이 작성합니다.

/Models/Maxim.cs
using System;
 
namespace MemoEngine.Models
{
    /// <summary>
    /// Maxim 모델 클래스 : Maxims 테이블과 일대일
    /// </summary>
    public class Maxim
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Content { get; set; }
        public DateTime CreationDate { get; set; }
    }
}

(3) MemoEngine 프로젝트의 Models 폴더에 MaximServiceRepository.cs 이름으로 클래스를 생성합니다. 테이블과 모델 클래스를 바탕으로 데이터를 주고 받는 기능을 한 곳에 모아 보겠습니다. 리포지토리 패턴에 의해서 MaximServiceRepository 클래스를 다음과 같이 구현합니다. 데이터베이스 처리 학습 장에서 사용했던 Micro ORM인 Dapper를 사용합니다. 간단히 인라인 SQL 구문으로 데이터 입력, 출력, 수정, 삭제 구문을 만들었습니다.

/Models/MaximServiceRepository.cs
using Dapper;
using System.Collections.Generic;
using System.Configuration;
using System.Data.SqlClient;
using System.Linq;
 
namespace MemoEngine.Models
{
    /// <summary>
    /// 명언(Maxim) 서비스에 대한 DB 연동 코드 부분
    /// </summary>
    public class MaximServiceRepository
    {
        // Database 개체 생성
        private SqlConnection db;
 
        public MaximServiceRepository()
        {
            db = new SqlConnection(ConfigurationManager.ConnectionStrings[
                "ConnectionString"].ConnectionString);
        }
 
        // 입력
        public Maxim AddMaxim(Maxim model)
        {
            string sql = @"
                Insert Into Maxims (Name, Content) Values (@Name, @Content);
                Select Cast(SCOPE_IDENTITY() As Int);
            ";
            var id = this.db.Query<int>(sql, model).Single();
            model.Id = id;
            return model;
        }
 
        // 출력
        public List<Maxim> GetMaxims()
        {
            string sql = @"Select Id, Name, Content, CreationDate 
                From Maxims Order By Id Asc";
            return this.db.Query<Maxim>(sql).ToList();
        }
 
        // 상세
        public Maxim GetMaximById(int id)
        {
            string sql = @"Select Id, name, Content, CreationDate 
                From Maxims Where Id = @Id";
            return this.db.Query<Maxim>(sql, new { Id = id }).SingleOrDefault();
        }
 
        // 수정
        public Maxim UpdateMaxim(Maxim model)
        {
            string sql = @"Update Maxims 
                Set Name = @Name, Content = @Content Where Id = @Id";
            this.db.Execute(sql, model);
            return model;
        }
 
        // 삭제
        public void RemoveMaxim(int id)
        {
            string sql = "Delete Maxims Where Id = @Id";
            this.db.Execute(sql, new { id });
        }
    }
}

21.6.4 따라하기 3: Web API 코드 구현 (1) MemoEngine 프로젝트의 Controllers 폴더에 마우스 오른쪽 버튼을 클릭 후 <추가 > 컨트롤러>를 선택합니다. <스캐폴드 추가> 창에서는 <Web API 2 컨트롤러 – 비어 있음>을 선택하고, MaximServiceController.cs 이름으로 컨트롤러를 추가합니다. 그림 21 20 Web API 2 컨트롤러 추가

(2) MaximServiceController.cs 파일을 열고 다음과 같이 작성합니다. ASP.NET의 웹 폼 페이지 또는 MVC 뷰 페이지를 사용하면 직접 앞에서 작성한 리포지토리 클래스만을 사용하여 데이터를 주고 받는 페이지들을 구현할 수 있습니다. 하지만 클라이언트 측에서 가져다 사용할 Web API를 만들어야 하므로 ApiController 클래스를 상속 받아서 각각의 CRUD를 구현해야 합니다. 다음 코드는 CRUD를 서비스하고 이에 대한 예외 처리(HTTP 에러 메시지 출력)를 함께 하는 전형적인 스타일의 ASP.NET Web API 코드입니다. 물론 구현하는 상태에 따라서 다른 모습으로 보일 수 있지만, 교과서다운 코드로 손색이 없기에 기본 Web API에 사용하는 코드 스타일로 학습해 두자.

/Controllers/MaximServiceController.cs 
using MemoEngine.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
 
namespace MemoEngine.Controllers
{
    /// <summary>
    /// 명언(Maxim) 제공 서비스: /api/maximservice/
    /// 기본 뼈대 만드는 것은 Web API 스캐폴딩으로 구현 후 각각의 코드를 구현
    /// </summary>
    public class MaximServiceController : ApiController
    {
        MaximServiceRepository repo = new MaximServiceRepository();
 
        // GET: api/MaximService
        public IEnumerable<Maxim> Get()
        {
            return repo.GetMaxims().AsEnumerable();
        }
 
        // GET: api/MaximService/5
        public Maxim Get(int id)
        {
            // 데이터 조회
            Maxim maxim = repo.GetMaximById(id);
            if (maxim == null)
            {
                throw new HttpResponseException(
                    Request.CreateResponse(HttpStatusCode.NotFound));
            }
            return maxim;
        }
 
        // POST: api/MaximService
        public HttpResponseMessage Post([FromBody]Maxim maxim)
        {
            if (ModelState.IsValid)
            {
                // 데이터 입력
                repo.AddMaxim(maxim);
 
                HttpResponseMessage response =
                    Request.CreateResponse(HttpStatusCode.Created, maxim);
                response.Headers.Location =
                    new Uri(Url.Link("DefaultApi", new { id = maxim.Id }));
                return response;
            }
            else
            {
                return Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, ModelState);
            }
        }
 
        // PUT: api/MaximService/5
        public HttpResponseMessage Put(int id, [FromBody]Maxim maxim)
        {
            if (!ModelState.IsValid)
            {
                return Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, ModelState);
            }
            if (id != maxim.Id)
            {
                return Request.CreateResponse(HttpStatusCode.BadRequest);
            }
 
            // 데이터 수정
            repo.UpdateMaxim(maxim);
 
            return Request.CreateResponse(HttpStatusCode.OK);
        }
 
        // DELETE: api/MaximService/5
        public HttpResponseMessage Delete(int id)
        {
            Maxim maxim = repo.GetMaximById(id);
            if (maxim == null)
            {
                return Request.CreateResponse(HttpStatusCode.NotFound);
            }
 
            // 데이터 삭제
            repo.RemoveMaxim(id);
 
            return Request.CreateResponse(HttpStatusCode.OK, maxim);
        }
    }
}

위 코드에서 HttpStatusCode 열거형은 OK, BadRequest, NotFound와 같은 공통 상태 코드에 대한 값을 int 형으로 제공합니다. 추가 학습: 현재 예제에서는 repo 개체를 만들 때 필드에서 직접 생성했지만, 권장 사항은 생성자에서 repo 개체를 생성한 후 이를 Get, Post 등의 액션 메서드에서 사용하면 됩니다.

(3) Web API에서는 C#의 Name, Content 등의 속성 값이 대문자로 클라이언트에 전달됩니다. 하지만 JavaScript 프로그래밍 기본 규칙에서의 변수명은 소문자로 시작하는 것을 권장합니다. 이러한 규칙을 맞추기 위해서 Web API의 결괏값을 소문자로 시작할 수 있도록 추가 설정이 필요합니다. MemoEngine 프로젝트의 App_Start 폴더의 WebApiConfig.cs 파일을 열고 다음 코드를 추가합니다. 주석 부분은 입력할 필요는 없습니다.

/App_Start/WebApiConfig.cs 
using System.Web.Http;
 
namespace MemoEngine
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 구성 및 서비스
 
            // [!] Web API에서 JSON 값을 반환시 
            // 파스칼 케이스가 아닌 카멜 케이스로 표현하기 위한 방법
            // [a] 표현 방법 1
            // var formatter = 
            //    GlobalConfiguration.Configuration.Formatters.JsonFormatter;
            // formatter.SerializerSettings.ContractResolver = new Newtonsoft
            //    .Json.Serialization.CamelCasePropertyNamesContractResolver();
            // [b] 표현 방법 2 
            config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
                new Newtonsoft.Json.Serialization.
                    CamelCasePropertyNamesContractResolver();
 
            // Web API 경로
            config.MapHttpAttributeRoutes();
 
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

21.6.5 따라하기 4: 프로젝트에 jQuery, Bootstrap, Angular 추가 (1) 현재 사용하고 있는 프로젝트에 NuGet, Bower 등의 패키지 관리 기술 등을 사용해서 jQuery, Bootstrap, Angular를 추가합니다. 이 책(강의)은 ASP.NET에 대한 내용이 위주이므로 jQuery, Bootstrap, 앵귤러 등에 대한 자세한 내용은 다루지 않습니다. ASP.NET Web API를 구현 후 jQuery, Bootstrap, AngularJS 를 사용해서 어떻게 보여지는지에 대한 내용을 데모 형식으로 보여주는 용도로만 사용합니다. 해당 기술에 대해서 좀 더 자세한 내용을 알고자 한다면 관련 전문 서적을 참고하기 바랍니다. ASP.NET 프로젝트에는 기본적으로 jQuery와 Bootstrap이 기본으로 장착되어 있습니다. Visual Studio에서 <도구 > NuGet 패키지 관리자 > 패키지 관리자 콘솔>에서 다음 명령어를 입력하여 AngularJS.Core를 추가합니다.  PM> Install-Package AngularJS.Core

그림 21 21 AngularJS 추가

(2) Scripts 폴더를 열어 angular.js, bootstrap.js, jquery-XXX.js 파일이 있는지 확인합니다. 참고로 jQuery의 버전은 책의 버전과 다를 수 있습니다. 그림 21 22 Scripts 폴더

21.6.6 따라하기 5: ASP.NET Web API와 jQuery를 사용한 SPA 구현

(1) MemoEngine 프로젝트에 Maxim 폴더를 생성합니다. Maxim 폴더에 Default라는 이름으로 HTML 문서를 생성하고 다음과 같이 작성합니다.

/Maxim/Default.html
<!DOCTYPE html>
<html>
<head>
    <title>명언 서비스</title>
</head>
<body>
    <h2>명언 서비스</h2>
    <ul>
        <li>
            <a href="MaximCrudWithAngular.html">
                Angular + Web API를 사용한 CRUD 작업
            </a>
        </li>
        <li>
            <a href="MaximCrudWithJavaScriptQuery.html">
                jQuery와 ASP.NET Web API를 사용한 CRUD 구현하기
            </a>
        </li>
    </ul>
</body>
</html>

(2) Maxim 폴더에 MaximCrudWithJavaScriptQuery.html이란 이름으로 HTML 문서를 생성하고 앞서 구현한 Web API를 jQuery를 사용해서 SPA로 표현해보겠습니다. 이에 대한 교과서다운 코드를 다음과 같이 작성합니다.

/Maxim/MaximCrudWithJavaScriptQuery.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>jQuery와 ASP.NET Web API를 사용한 CRUD 구현하기</title>
    <script src="/Scripts/jquery-1.10.2.min.js"></script>
    <script type="text/javascript">
        // ASP.NET Web API와 jQuery Ajax를 사용한 CRUD 기능 구현
 
        // Web API 주소
        var API_URL = "/api/maximservice/";
 
        // 상태 클리어
        function clearStatus() {
            $("#lblError").html('');
        }
        function clearField() {
            $("#name").val('');
            $("#content").val('');
        }
 
        // [!] 데이터 조회 전용 함수
        function displayData() {
            $('#lstMaxims').html('');
            // cRud : Read
            // 전체 명언 리스트를 읽어서 출력
            $.getJSON(API_URL, function (data) {
                // console.log(data.length); // 데이터의 개수
                $.each(data, function (key, val) {
                    var str = val.id + ", " + val.name + ", " + val.content;
                    $("<li />", { html: str }).appendTo("#lstMaxims");
                });
            });
        }
 
        // Page_Load
        $(function () {
 
            // 전체 레코드 출력
            displayData();
 
            // Crud : Create
            $("#btnAdd").click(function () {
                clearStatus();
                // [a] 데이터 받기
                var name = $('#name').val();
                var content = $('#content').val();
                // [b] JSON 개체로 묶기 : " + 변수 + "
                var json =
                    "{name:\"" + name + "\", content:\"" + content + "\"}";
                // [c] 전송
                $.ajax({
                    url: API_URL,
                    cache: false,
                    type: 'POST',
                    contentType: 'application/json; charset=utf-8',
                    data: json,
                    statusCode: {
                        201: function (data) {
                            var str = data.id + ", " + data.name
                                + ", " + data.content;
                            $("<li />", { html: str }).appendTo("#lstMaxims");
                        }
                    }
                });
                clearField();
            });
 
            // cRud : Retrieve(조회)
            $("#btnFind").click(function () {
                clearStatus();
                clearField();
                // id값 가져오기
                var id = $("#id").val();
                // 특정 번호에 해당하는 명언 정보를 읽어서 출력
                $.getJSON(API_URL + id, function (data) {
                    $("#name").val(data.name);
                    $("#content").val(data.content);
                }).fail(function (xhr, sts, err) {
                    $("#lblError").html("에러 : " + err);
                });
            });
 
            // crUd : PUT
            $("#btnUpdate").click(function () {
                clearStatus();
                // [!] ID 값 받기
                var id = $("#id").val();
                // [a] 데이터 받기
                var name = $('#name').val();
                var content = $('#content').val();
                // [b] JSON 개체로 묶기 : " + 변수 + "
                var json =
                    "{id:\"" + id + "\", name:\"" + name + "\", content:\""
                    + content + "\"}";
                // [c] 전송
                $.ajax({
                    url: API_URL + id, // /api/maximservice/3
                    cache: false,
                    type: 'PUT',
                    contentType: 'application/json; charset=utf-8',
                    data: json,
                    success: function () {
                        displayData(); // 데이터가 수정되었으면 다시 출력
                    }
                }).fail(function (xhr, textStatus, err) {
                    $('#lblError').html("에러 : " + err);
                });
                clearField();
            });
 
            // cruD : DELETE
            $("#btnDelete").click(function () {
                clearStatus();
                // [!] ID 값 받기
                var id = $("#id").val();
                // [c] 전송
                $.ajax({
                    url: API_URL + id, // /api/maximservice/3
                    cache: false,
                    type: 'DELETE',
                    contentType: 'application/json; charset=utf-8',
                    data: {},
                    success: function () {
                        displayData();
                    }
                }).fail(function (xhr, textStatus, err) {
                    $("#lblError").html("에러 : " + err);
                });
                clearField();
            });
        }); // end of Page_Load
    </script>
</head>
<body>
    <h2>명언 리스트</h2>
    <div>
        <h3>명언 리스트</h3>
        <ul id="lstMaxims"></ul>
    </div>
    <div>
        <h3>명언 상세</h3>
        <div>
            <label for="id">번호: </label>
            <input type="text" name="id" id="id" value="" />
        </div>
        <div>
            <label for="name">이름: </label>
            <input type="text" name="name" id="name" value="" />
        </div>
        <div>
            <label for="content">내용: </label>
            <input type="text" name="content" id="content" value="" />
        </div>
        <div>
            <input type="button" name="btnAdd" id="btnAdd" value="추가" />
            <input type="button" name="btnFind" id="btnFind"
                   value="찾기(번호검색)" />
            <input type="button" name="btnUpdate" id="btnUpdate" value="수정" />
            <input type="button" name="btnDelete" id="btnDelete" value="삭제" />
        </div>
    </div>
    <div>
        <p id="lblError" style="color:red;"></p>
    </div>
</body>
</html>

추가 학습: Content Type JSON : application/json XML : text/xml JSONP : application/javascript

<참고> 참고: JSON 개체로 묶기 JSON 개체를 묶는 방법은 위 코드처럼 수작업으로 하는 방법도 있고, jQuery의 플러그인 중에서 serializeObject를 사용하는 방법도 있습니다. 이렇게 하면 아래 형태로 사용할 수 있습니다. //[b] JSON 개체로 묶기 : " + 변수 + " //var json = "{name:"" + name + "", title:"" + title + ""}";

//[b] jQuery 플러그인 사용: serializeObject() var json = JSON.stringify($('#frmDotNetNoteCreate').serializeObject()); serializeObject 코드는 다음과 같습니다. (function ($) { $.fn.serializeObject = function () { "use strict";

    var result = {};
    var extend = function (i, element) {
        var node = result[element.name];

        if ('undefined' !== typeof node && node !== null) {
            if ($.isArray(node)) {
                node.push(element.value);
            } else {
                result[element.name] = [node, element.value];
            }
        } else {
            result[element.name] = element.value;
        }
    };

    $.each(this.serializeArray(), extend);
    return result;
};

})(jQuery); </참고>

(3) MaximCrudWithJavaScriptQuery.html 페이지를 웹 브라우저 보기를 사용해서 실행하면 다음과 같이 출력됩니다. 그림 21 23 제이쿼리 SPA 페이지 실행

(4) 웹 브라우저에 이름과 내용 데이터를 입력한 후 추가 학습: 버튼을 클릭합니다. 입력 및 출력하는 동작을 하나의 페이지(심지어는 순수 HTML 페이지)에서 진행되는 것을 확인할 수 있습니다. 그림 21 24 제이쿼리를 사용한 SPA 구현

21.6.7 따라하기 6: ASP.NET Web API와 Angular를 사용한 SPA 구현 (1) 앞의 jQuery 예제와 같은 내용을 이번에는 AngularJS를 사용해서 구현해보겠습니다. Maxim 폴더에 MaximCrudWithAngular라는 이름으로 HTML 문서를 생성하고 다음과 같이 코드를 작성합니다.

/Maxim/MaximCrudWithAngular.html
<!DOCTYPE html>
<html>
<head>
    <title>Angular + Web API를 사용한 CRUD 작업</title>
    <link href="/Content/bootstrap.min.css" rel="stylesheet" />
    <style>
        #divFullScreen {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 1000;
            background-color: grey;
            opacity: .7;
        }
 
        .ajaxIndicator {
            position: absolute;
            left: 50%;
            top: 50%;
            margin-left: -32px;
            margin-top: -32px;
            display: block;
        }
    </style>
</head>
<body>
    <div ng-app="maximApp">
        <div data-ng-controller="maximController" class="container">
            <h2>{{ title }}</h2>
            <div class="row">
                <div class="col-md-12">
                    <strong class="error">{{ error }}</strong>
                    <p data-ng-hide="isAddMode">
                        <a data-ng-click="changeAddMode()"
                           class="btn btn-primary">내용 추가하기</a>
                    </p>
                    <form name="frmAddMaxim" id="frmAddMaxim"
                          data-ng-show="isAddMode"
                          style="width:600px;margin:0 auto;">
                        <div class="form-group">
                            <label for="cname"
                                   class="col-sm-2 control-label">이름:</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control"
                                       id="name" placeholder="이름"
                                       required
                                       ng-minlength="1"
                                       ng-maxlength="25"
                                       data-ng-model="maximInput.name" />
                            </div>
                        </div>
                        <div class="form-group">
                            <label for="content"
                                   class="col-sm-2 control-label">내용:</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control"
                                       id="content" placeholder="명언"
                                       required
                                       ng-minlength="3"
                                       ng-maxlength="255"
                                       data-ng-model="maximInput.content" />
                            </div>
                        </div>
                        <br />
                        <div class="form-group">
                            <div class="col-sm-offset-2 col-sm-10">
                                <input type="submit" value="입력"
                                       data-ng-click="add()"
                                       data-ng-disabled="!frmAddMaxim.$valid"
                                       class="btn btn-primary" />
                                <input type="button" value="취소"
                                       data-ng-click="changeAddMode()"
                                       class="btn btn-primary" />
                            </div>
                        </div>
                        <br />
                    </form>
                </div>
            </div>
            <div class="row">
                <div class="col-md-12">
                    <div class="table-responsive">
                        <table class="table table-bordered table-hover"
                               style="width:100%;">
                            <tr>
                                <th>#</th>
                                <td>이름</td>
                                <th>명언</th>
                                <th></th>
                            </tr>
                            <tr data-ng-repeat="maxim in maxims">
                                <td>
                                    <strong data-ng-hide="maxim.editStatus">
                                        {{ maxim.id }}
                                    </strong>
                                </td>
                                <td>
                                    <p data-ng-hide="maxim.editStatus">
                                        {{ maxim.name }}
                                    </p>
                                    <input type="text"
                                           data-ng-show="maxim.editStatus"
                                           data-ng-model="maxim.name" />
                                </td>
                                <td>
                                    <p data-ng-hide="maxim.editStatus">
                                        {{ maxim.content }}
                                    </p>
                                    <input type="text"
                                           data-ng-show="maxim.editStatus"
                                           data-ng-model="maxim.content" />
                                </td>
                                <td>
                                    <p data-ng-hide="maxim.editStatus">
                                        <a ng-click="changeEditStatus(maxim)"
                                           href="javascript:;">수정</a> |
                                        <a data-ng-click="remove(maxim)"
                                           href="javascript:;">삭제</a>
                                    </p>
                                    <p ng-show="maxim.editStatus">
                                        <a ng-click="save(maxim)"
                                           href="javascript:;">저장</a> |
                                        <a ng-click="changeEditStatus(maxim)"
                                           href="javascript:;">취소</a>
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>
            </div>
            <div id="divFullScreen" data-ng-show="loading">
                <img src="/images/ajaxIndicator.gif" class="ajaxIndicator" />
            </div>
        </div>
    </div>
 
 
 
    <script src="/Scripts/jquery-2.1.3.min.js"></script>
    <script src="/Scripts/bootstrap.min.js"></script>
    <script src="/Scripts/angular.min.js"></script>
    <script>
        (function () {
            'use strict';
 
            // [1] Angualr 모듈 선언
            var app = angular.module('maximApp', []);
 
            // [2][1] Angualr 컨트롤러 선언
            app.controller('maximController'
                , ['$scope', '$http', maximController]);
 
            // [2][2] 컨트롤러 구현부
            function maximController($scope, $http) {
 
                // Web API 주소: ASP.NET Web API로 구현
                // const API_URL = "/api/maximservice/";
                var API_URL = "/api/maximservice/";
 
                // 속성(Property)
                $scope.title = "명언 관리";
                $scope.loading = true; // 로딩 창 띄우기
                $scope.isAddMode = false; // 입력 모드(true: 입력 폼 출력)
 
                // 입력 값 임시 저장용 개체
                $scope.maximInput = {
                    id: 0,
                    name: '',
                    content: '',
                };
 
                // 메서드(함수, Function)
                $scope.changeEditStatus = function () {
                    this.maxim.editStatus = !this.maxim.editStatus;
                };
 
                $scope.changeAddMode = function () {
                    $scope.isAddMode = !$scope.isAddMode;
                };
 
 
                // 출력: GET
                $http.get(API_URL).success(function (data) {
                    $scope.maxims = data;
                    $scope.loading = false;
                })
                .error(function () {
                    $scope.error = "데이터를 가져오는 동안 에러가 발생했습니다. ";
                    $scope.loading = false;
                });
 
 
                // 입력: POST
                $scope.add = function () {
                    $scope.loading = true;
 
                    // POST: get()의 success().error() 대신 then(f,f) 사용 가능
                    $http.post(API_URL, $scope.maximInput)
                        .then(function (result) {
                            alert("데이터가 입력되었습니다.");
                            $scope.isAddMode = false;
                            $scope.maxims.push(result.data); // resut.data 주의
                            $scope.loading = false;
 
                            $scope.maximInput = {}; // 입력 폼 클리어
                        }, function (error) {
                            $scope.error =
                                "데이터를 입력하는 동안 에러가 발생했습니다. "
                                + error.data.ExceptionMessage; // 또 다른 방법
                            $scope.loading = false;
                        });
                };
 
 
                // 수정: PUT
                $scope.save = function () {
                    $scope.loading = true;
                    var maxim = this.maxim;
 
                    // PUT
                    $http.put(API_URL + maxim.id, maxim).success(
                        function (data) {
                            alert("데이터가 수정되었습니다.");
                            maxim.editStatus = false;
                            $scope.loading = false;
                        })
                    .error(function (data) {
                        $scope.error =
                            "데이터를 수정하는 동안 에러가 발생했습니다. " + data;
                        $scope.loading = false;
                    });
                };
 
 
                // 삭제: DELETE
                $scope.remove = function () {
                    $scope.loading = true;
                    var id = this.maxim.id;
 
                    // DELETE
                    $http.delete(API_URL + id).success(function (data) {
                        alert("데이터가 삭제되었습니다.");
                        $.each($scope.maxims, function (i) {
                            if ($scope.maxims[i].id === id) {
                                $scope.maxims.splice(i, 1);
                                return false;
                            }
                        });
                        $scope.loading = false;
                    })
                    .error(function (data) {
                        $scope.error =
                            "데이터를 삭제하는 동안 에러가 발생했습니다. " + data;
                        $scope.loading = false;
                    });
                };
            } // maximController
        })();
    </script>
</body>
</html>

(2) MaximCrudWithAngular.html 페이지를 웹 브라우저 보기를 사용해서 실행하면 다음과 같이 출력됩니다. jQuery 예제와 마찬가지로 Angular를 사용해서도 SPA를 구현할 수 있다는 것을 확인합니다. 이처럼 JavaScript 라이브러리로 어떤 것을 사용하느냐일 뿐이지 서버 측에 구현한 ASP.NET Web API는 변하지 않는다는 것에 주목합니다. 그림 21 25 앵귤러를 사용한 SPA 구현

(3) <내용 추가하기> 버튼을 클릭하면 입력 폼이 출력됩니다. 데이터를 넣고 <입력> 버튼을 클릭합니다. 그림 21 26 입력 폼 출력

(4) 내용 입력 후 데이터가 출력된 모습입니다. 이 모든 작업이 페이지 깜박거림 없이 순수 HTML 기반에서 이루어진다. 그림 21 27 앵귤러를 사용한 단인 페이지 응용 프로그램

(5) MemoEngine 프로젝트 루트에 있는 Site.Master 파일을 열고 메뉴 링크를 하나 더 추가합니다.

Site.Master 메뉴 부분
<ul class="nav navbar-nav">
    <li><a runat="server" href="~/">홈</a></li>
    <li><a runat="server" 
        href="~/DotNetNote/BoardList.aspx">게시판</a></li>
    <li><a runat="server" 
        href="~/Maxim/Default.html">명언</a></li>
    <li><a runat="server" href="~/About">정보</a></li>
    <li><a runat="server" href="~/Contact">연락처</a></li>
</ul>

(6) MemoEngine 프로젝트의 루트에 있는 Default.aspx 페이지를 실행하면 언제든 게시판과, 명언 링크를 통해서 각각의 기능을 실행하는 페이지로 이동할 수 있습니다. 그림 21 28 명언 서비스 링크 생성

21.6.8 마무리 이 강의에서는 ASP.NET Web API를 사용하여 RESTful 서비스를 구현하는 방법을 다루었습니다. 서버 측에 구현된 CRUD 기능은 JSON 또는 XML 방식으로 클라이언트 측에 전송됩니다. 서버와 클라이언트 간의 데이터 송수신은 이 Web API를 통해 외부 JavaScript 라이브러리의 도움을 받아서 손쉽게 구현할 수 있습니다. ASP.NET은 서버 측과 클라이언트 측 모두 구현할 수 있지만, 최근 웹 개발 추세를 따라 ASP.NET Web API의 사용이 빈번이 일어나고 있습니다. 클라이언트는 JavaScript 라이브러리로 대부분 구현하기 때문입니다. 어쨌든 ASP.NET Web API는 손쉽게 RESTful 서비스를 구현할 수 있는 최적의 기술이라 생각하고, 이를 적극적으로 활용하기 바랍니다.

21.7 jQuery Ajax를 사용하여 서버 쪽에 텍스트파일 생성하기

MemoEngine/Controllers/MakeLocationFileServicesController.cs
using System;
using System.IO;
using System.Web;
using System.Web.Http;

namespace MemoEngine.Controllers
{
    /// <summary>
    /// jQuery Ajax를 사용하여 서버 쪽에 텍스트파일 생성하기
    /// </summary>
    public class MakeLocationFileServicesController : ApiController
    {
        [HttpPost]
        [Route("api/MakeLocationFileServices/PostMakeFile")]
        public void PostMakeFile(int id)
        {
            var folder = HttpContext.Current.Server.MapPath("/BoardFiles/Projects");
            var file = Path.Combine(folder, Guid.NewGuid().ToString() + ".dat");

            using (StreamWriter writer = new StreamWriter(file))
            {
                writer.WriteLine(id.ToString());
            }
        }
    }
}
MemoEngine/Demos/jQuery/Ajax/MakeLocationFile.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>jQuery Ajax를 사용하여 서버 쪽에 텍스트파일 생성하기</title>
    <script src="lib/jquery/jquery.js"></script>
</head>
<body>
    <script>
        $(function () {
            id = 2345; 
            makeLocationFile(id);
        });

        function makeLocationFile(id) {
            $.ajax({
                type: 'POST',
                url: '/api/MakeLocationFileServices/PostMakeFile?Id=' + id,
                dataType: 'JSON',
                success: function (data) {
                    alert('파일이 생성되었습니다.');
                }
            });
        }
    </script>
</body>
</html>

HTTP 내용 협상

ASP.NET Web API의 HTTP 내용 협상

소개 ASP.NET Web API는 클라이언트의 선호에 따라 응답 형식을 조정하는 HTTP 내용 협상을 지원합니다. 이 기능은 다양한 웹 브라우저에서 널리 사용되며, 이를 '내용 협상'이라고 합니다. 예를 들어, 인터넷 익스플로러(IE)를 통해 ASP.NET Web API에 접근하면 일반적으로 JSON 형식으로 데이터를 반환합니다. 반면, 크롬을 사용할 경우에는 XML 형식으로 응답을 받게 됩니다.

이러한 차이는 클라이언트가 HTTP 요청 헤더에 특정 형식을 지정함으로써 명시적으로 제어할 수 있습니다. 예를 들어, Accept: application/xml 헤더를 보내면 응답은 XML 형식으로, Accept: application/json 헤더를 보내면 JSON 형식으로 반환됩니다. 따라서 클라이언트 측 코드(예: 자바스크립트)에서 필요에 따라 JSON과 XML을 명확히 구분할 수 있어야 합니다.

간단하고 용량이 작은 데이터 처리에는 XML보다 JSON 형식이 더 적합할 수 있습니다.

추가 정보 HTTP 내용 협상에 대한 자세한 정보는 다음 링크에서 확인하실 수 있습니다: HTTP 내용 협상 위키

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com