본문 바로가기
JavaScript

Ajax 사용법 (XMLHttpRequest) + cross domain 문제

by enai 2019. 8. 3.

Ajax는 웹에서 데이터를 갱신할 때 브라우저 새로고침 없이도 서버로부터 데이터를 받아올 수 있으면 좋겠다는 생각에서 출발한 기술이다.

 

예를 들어, 상단 메뉴의 탭을 누를 때마다 콘텐츠가 달라지는 사이트가 있을 때 누르지도 않은 탭의 콘텐츠까지 초기 로딩 시점에 모두 불러온다면 초기 로딩 속도에 영향을 줬을 것이다. 따라서 동적으로 필요한 시점에 콘텐츠를 받아와서 표현하면 더 좋을 것이다.

Ajax로 전체 새로고침 없이 일부분만 받아온 데이터로 갱신하여 이를 해결할 수 있다.

 

 

1. Ajax 실행 코드

 

XMLHttpRequest 객체를 사용하는 표준 방법

 

function ajax(data) {
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("load", function() {
		console.log(this.responseText);
	});
	xhr.open("GET", "http://enai.tistory.com/getdata?data="+data);
	xhr.send();
}

 

XMLHttpRequest 객체를 생성하여, open 메서드로 요청을 준비하고, send 메서드로 데이터를 서버로 전송한다.

요청 처리가 완료되어 서버에서 응답이 오면 load 이벤트가 발생하고, 이때 call stack에 실행되고 있는 함수가 없어 비어있다면 콜백 함수가 stack에 올라가 실행된다. (콜백 함수가 실행될 때는 이미 ajax 함수는 반환하고 call stack에서 사라진 상태이다.)

이는 setTimeout 함수의 콜백 함수의 실행과 유사하게 동작하는 비동기 로직이다.

 

 

위 코드의 open 메서드를 보면 GET으로 요청을 하고 있다.

GET 뿐만 아니라, POST, PUT, DELETE 등도 가능하다.

 

 

POST처럼 데이터를 요청 바디로 보낼 때는 다음과 같이 send 메서드로 보내면 된다.

 

function ajax(data) {
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("load", function() {
		console.log(this.responseText);
	});
	xhr.open("POST", "http://enai.tistory.com/setdata");
	xhr.send(data);
}

 

클라이언트에서 서버로 데이터를 보낼 땐 문자열 데이터로 보낸다.

send 메서드로 보낼 때 나중에도 언급되는 JSON 객체의 stringify() 메서드로 JSON 데이터를 문자열로 바꿔서 보낼 수 있다.

 

function ajax(data) {
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("load", function() {
		console.log(this.responseText);
	});
	xhr.open("POST", "http://enai.tistory.com/setdata");
	xhr.send(JSON.stringify(data));
}

 

 

 

컨트롤러(controller)에서 데이터를 받을 때는 다음과 같다.

 

GET 요청 메서드일 때

@GetMapping(path="/getdata")
public Map<String, Object> getData(@RequestParam(name="data")int id) {
  ...
}

 

POST 요청 메서드일 때

@PostMapping(path="/setdata")
public Data setData(@RequestBody Data data) {
  ...
}

 

 

+) 컨트롤러에서 반환하는 데이터를 Ajax로 보내려면 컨트롤러 메서드 위에 @ResponseBody가 필요하다.

하지만 컨트롤러 위에 @RestController가 있으면 메서드 위에 일일이 @ResponseBody를 붙일 필요 없다.

@RequestParam과 @RequestBody는 Ajax가 보낸 데이터를 받기 위한 어노테이션이다.

@RequestParam은 "id=1234&name='name'"같은 데이터를 id, name 각각으로 하나씩 받아올 때 사용하고, @RequestBody는 HTTP의 요청 바디 부분의 데이터를 받아올 때 사용한다.

 

 

 

 

 

2. Ajax 응답 처리

 

Ajax를 통해 서버로부터 받아온 데이터 포맷으로 JSON 데이터 포맷을 많이 사용한다.

JSON은 아래 예시와 같은 { 키 : 값 } 구조의 데이터다.

 

var json = {
	"key":"data",
	"key2":"data2"
	}

 

이런 서버로부터 받아온 JSON 데이터는 문자열 형태이므로 브라우저에서 바로 실행할 수가 없다. (형태는 JSON 데이터 포맷에 맞춰주지만, 브라우저로 전송할 땐 문자열로 보내줘야 하기 때문이다.)

따라서 문자열을 자바스크립트 객체로 변환해야 데이터에 접근할 수 있어, 문자열 파싱을 일일이 해야 하는 불편함이 있다. 다행히도 브라우저에서는 JSON 객체를 제공해주어, 이를 활용해서 자바스크립트 객체로 변환할 수가 있다.

 

var jsonData = JSON.parse("서버에서 받은 JSON 문자열");

 

결국, 다음과 같이 사용할 수 있다.

function ajax(data) {
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("load", function(event) {
		var jsonData = JSON.parse(event.target.response);
	});
	xhr.open("GET", "http://enai.tistory.com/getData?data="+data);
	xhr.send();
}

 

위 코드에서는 XMLHttpRequest로 서버와 통신하여 받아온 결과는 event라는 이름으로 받아온다.

event를 콘솔에 출력해보면 알 수 있겠지만, 서버에서 주는 데이터는 target의 response 안에 있다.

그래서 JSON.parse(event.target.response)로 해당 데이터를 JSON 객체로 변환하여 사용할 수 있다.

 

 

 

 

 

3. cross domain 문제

 

XHR 통신은 보안을 이유로 다른 도메인 간에는 요청이 안 된다.

즉, A 도메인에서 B 도메인으로 XHR 통신인 Ajax 통신을 할 수 없다는 말이다.

 

이 문제를 해결하기 위해 JSONP라는 방식이 널리 사용되고 있다.

최근에는 CORS라는 표준적인 방법이 제공되고 있어 이를 활용하는 경우도 등장했다.

CORS를 사용하기 위해서 프로그램 코드에서 별도로 해야 할 것은 없고, 백엔드 코드에서 헤더 설정을 해야 하는 번거로움은 있다.

 

JSONP는 비표준이지만 아직 많은 곳에서 사용하여 사실상 표준으로 사용되고 있다. CORS로 가기 전에 많은 곳에서 사용 중이다.

 

 

 

1) JSONP (JSON with Padding)

 

JSONP는 XMLHttpRequest 객체를 사용하는 것이 아닌, <script> 태그를 사용한다.

 

다른 도메인에게 외부 파일을 요청하면 보안 상의 이유로 통신을 할 수 없지만, 외부 스크립트는 이런 문제가 없다. JSONP는 이 점을 이용하여 <script> 태그를 사용하여 외부 파일을 요청한다.

 

<script src="데이터 받아 올 url">

 

예시)

외부 파일: demo_json.php

 

<?php
$myJSON = '{ "name":"John", "age":30, "city":"New York" }';

echo "myFunc(".$myJSON.");";
?>

 

▶result : 

 

myFunc({"name":"John", "age":30, "city":"New York"});

 

 

외부 JSON 데이터를 호출할 JavaScript 함수

 

<script>
function myFunc(obj) {
	console.log(obj.name);  //결과값: John
}
</script>

<script src = "demo_jsonp.php"><script>

 

demo_jsonp.php가 실행되면서 myFunc()myFunc({"name":"John", "age":30, "city":"New York"});가 실행되고,

script 태그 안의 myFunc 함수가 호출되며, 이 함수의 인자 값 obj엔 {"name":"John", "age":30, "city":"New York"}가 들어간다.

이런 방식으로 다른 도메인의 JSON 데이터를 사용할 수 있게 된다.

 

 

 

2) CORS (Cross-Origin Resource Sharing)

 

웹 브라우저가 사용하려는 정보를 읽을 수 있도록 허가된 도메인 집합을 서버에게 알려주는 HTTP 헤더를 추가하는 방식으로 Cross Domain 문제를 해결한다.

 

 

CORS가 동작하는 3가지 방식

 

- Simple requests

 

다음 조건들을 모두 충족해야 한다.

 

  • 허용되는 메서드
    • GET
    • HEAD
    • POST
  • 사용자 에이전트에서 자동으로 설정되는 헤더들과 Fetch 스펙에서 CORS-safelisted request-header로 정의된 헤더들만 허용
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • 허용되는 Content-Type 헤더 값
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • XMLHttpRequestUpload 객체에 이벤트 리스너가 등록되지 않아야 한다. (XMLHttpRequestUpload 객체는 XMLHttpRequest.upload 속성으로 얻는 객체이며, 업로드 진행 상황을 모니터링할 수 있다.)
  • 요청에 ReadableStream 객체가 사용되지 않는다.

 

예시)

http://foo.example이 다른 도메인인 http://bar.other의 웹 콘텐츠를 호출하기 위한 자바스크립트 코드이다.

 

var xhr = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
   
function callOtherDomain() {
  if(xhr) {    
    xhr.open('GET', url, true);
    xhr.onreadystatechange = handler;
    xhr.send(); 
  }
}

 

위 코드로 브라우저가 서버에 전송한 내용과 서버가 응답한 내용

 

1 GET /resources/public-data/ HTTP/1.1
2 Host: bar.other
3 User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
4 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
5 Accept-Language: en-us,en;q=0.5
6 Accept-Encoding: gzip,deflate
7 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
8 Connection: keep-alive
9 Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
10 Origin: http://foo.example


13 HTTP/1.1 200 OK
14 Date: Mon, 01 Dec 2008 00:23:53 GMT
15 Server: Apache/2.0.61 
16 Access-Control-Allow-Origin: *
17 Keep-Alive: timeout=2, max=100
18 Connection: Keep-Alive
19 Transfer-Encoding: chunked
20 Content-Type: application/xml

 

1-10번 라인까지가 전송된 헤더이며, 10번 라인의 Origin 헤더로 http://foo.example에서 호출된 것임을 알 수 있다.

 

13-22번 라인은 http://bar.other에 있는 서버의 HTTP 응답을 보여준다. 

16번 라인의 Access-Control-Allow-Origin 헤더의 값에 전송 헤더의 Origin 헤더의 값이 포함되어 있어야 원하는 응답을 받을 수 있다.

서버는 Access-Control-Allow-Origin : *으로 응답하였다. 즉, http://bar.other의 리소스는 모든 도메인에서 액세스 할 수 있다는 의미이다. 특정 도메인으로부터의 액세스만 허용하고 싶다면 Access-Control-Allow-Origin 헤더 값을 그 특정 도메인 값으로 보내면 된다.

 

 

- Preflighted requests

 

OPTION 메서드로 HTTP 요청을 다른 도메인의 리소스로 보내는 것이 안전한지 먼저 확인한 후, 실제 요청 여부를 결정하는 방법이다.

 

다음 조건 중 하나라도 해당이 되면 요청이 미리 조사된다.

 

  • 메서드
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 사용자 에이전트에서 자동으로 설정되는 헤더들과 Fetch 스펙에서 CORS-safelisted request-header로 정의된 헤더들(Simple requests 쪽에 적혀있음)을 제외한 헤더들 (커스텀 헤더 사용)
  • Content-Type 값이 다음 값과 다르다. (다음 값들은 사전 전달 없이 바로 요청 전송할 수 있게 되어 있다.)
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 요청에 사용된 XMLHttpRequestUpload 객체에 하나 이상의 이벤트 리스너가 사용된다.
  • 요청에 ReadableStream 객체가 사용된다.

 

예시)

const xhr = new XMLHttpRequest();
const url = 'http://bar.other/resources/post-here/';
const body = '<?xml version="1.0"?><person><name>Arun</name></person>';
    
function callOtherDomain() {
  if (xhr) {
    xhr.open('POST', url, true);
    xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
    xhr.setRequestHeader('Content-Type', 'application/xml');
    xhr.onreadystatechange = handler;
    xhr.send(body); 
  }
}

 

위 코드는 POST 요청이지만, Content-Type이 application/xml이고 X-PINGOTHER이라는 커스텀 헤더를 사용하였으므로, 미리 요청(Preflighted requests)이 됩니다.

 

 

 

- Requests with credentials

 

XMLHttpRequeest 또는 CORS에는 HTTP 쿠키 및 HTTP 인증 정보를 확인하여 '자격 증명' 요청을 수행하는 기능이 있다.

 

예제)

var xhr = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
    
function callOtherDomain(){
  if(xhr) {
    xhr.open('GET', url, true);
    xhr.withCredentials = true;
    xhr.onreadystatechange = handler;
    xhr.send(); 
  }
}

 

xhr.withCredentials은 쿠키를 사용하여 xhr을 실행하기 위해 true로 설정한다. (쿠키를 사용하지 않는 것이 default값이다)

간단한 GET 요청이므로 미리 검사되지 않겠지만, 브라우저에서 Access-Control-Allow-Credentials : true 헤더가 없는 응답을 거부할 것이다. 서버 측에서 Access-Control-Allow-Credentials가 true로 응답되어야 한다.

 

 

 

 

 

 

 

출처)

edwith 부스트코스 웹 프로그래밍

- Ajax 통신의 이해

- Ajax 응답 처리와 비동기

w3school.com JSONP

MDN CORS

 

 

 

 

P.S. CORS는 아직 이해가 조금 부족하다. HTTP 헤더 다루는 법을 아직 잘 몰라서 그런 것 같다. 크로스 도메인 문제는 실습한 적이 없어서 더 그렇다는 생각이 든다. 공부하던 것 마무리 지으면 다시 공부하고 실습까지 해봐야겠다.

 

 

'JavaScript' 카테고리의 다른 글

Event delegation (이벤트 위임)  (0) 2019.08.03
DOMContentLoaded 이벤트  (0) 2019.08.03
자바스크립트로 Web Animation 구현하기  (0) 2019.08.03
DOM APIs 활용하기  (0) 2019.08.02
자바스크립트 배열과 객체  (0) 2019.08.02

댓글