Ajax란?
Ajax(Asynchronous JavaScript and XML)는 자바스크립트를 사용하여 브라우저가 서버에게 비동기 방식으로 데이터를 요청하고, 서버가 응답한 데이터를 수신하여 웹페이지를 동적으로 갱신하는 프로그래밍 방식이다. 이는 브라우저가 제공하는 Web API인 XMLHttpRequest 객체를 기반으로 동작하는데, 이 객체는 HTTP 비동기 통신을 위한 다양한 메서드와 프로퍼티들을 제공한다. XMLHttpRequest를 통해 개발자들은 서버와의 비동기 통신을 쉽게 구현할 수 있게 되었다.
Ajax의 역사는 1999년으로 거슬러 올라간다. 마이크로소프트가 XMLHttpRequest를 처음 개발했을 당시에는 이 기술이 큰 주목을 받지 못했다. 하지만 2005년, 구글이 발표한 '구글 맵스'가 이 기술의 진가를 세상에 알리는 전환점이 되었다. 구글 맵스는 웹 브라우저에서 자바스크립트와 Ajax를 기반으로 동작하면서도 데스크톱 애플리케이션과 비교해도 전혀 손색없는 퍼포먼스와 부드러운 화면 전환 효과를 보여주었다. 이는 웹 애플리케이션 개발 프로그래밍 언어로서 자바스크립트의 가능성을 전 세계에 입증하는 계기가 되었다.
Ajax 등장 이전의 웹페이지는 매우 단순한 방식으로 동작했다. 모든 웹페이지는 HTML 태그로 시작해서 HTML 태그로 끝나는 완전한 HTML 문서 형태로 서버로부터 전송받았고, 페이지 전환이 필요할 때마다 웹페이지 전체를 처음부터 다시 렌더링해야만 했다. 이러한 방식은 웹의 초기 단계에서는 충분했을지 모르나, 웹 애플리케이션이 복잡해지고 사용자 경험이 중요해지면서 여러 가지 심각한 문제점들이 드러나기 시작했다.
전통적인 웹 방식의 가장 큰 문제점들을 자세히 살펴보면 다음과 같다.
- 불필요한 데이터 통신의 과다 발생: 페이지의 일부분만 변경이 필요한 상황에서도 전체 HTML을 다시 받아와야 했다. 이는 이전 웹페이지와 새로운 웹페이지 사이에 차이가 거의 없는 경우에도 마찬가지였다. 예를 들어, 네비게이션 바와 푸터가 동일한 페이지에서 컨텐츠 영역만 변경하고 싶은 경우에도 변경이 불필요한 네비게이션 바와 푸터까지 포함된 전체 HTML을 다시 전송받아야 했다. 이는 불필요한 네트워크 트래픽을 발생시키고, 서버에도 추가적인 부하를 주었다.
- 비효율적인 렌더링 프로세스: 변경이 필요없는 부분까지 모두 다시 렌더링하는 방식은 브라우저에 불필요한 작업을 강요했다. 이로 인해 화면 전환이 일어날 때마다 사용자는 화면이 순간적으로 깜박이는 현상을 경험해야 했다. 이러한 깜박임 현상은 사용자 경험을 크게 저해하는 요소였으며, 웹 애플리케이션이 데스크톱 애플리케이션에 비해 열등하다고 여겨지는 주요 원인 중 하나였다.
- 동기 방식 통신의 한계: 클라이언트와 서버 간의 통신이 동기 방식으로 이루어졌기 때문에, 서버로부터 응답이 올 때까지 다음 작업은 모두 블로킹되었다. 이는 사용자가 웹페이지와 상호작용하는 동안 서버 응답을 기다려야 하는 상황을 만들었고, 결과적으로 웹 애플리케이션의 반응성을 크게 떨어뜨렸다. 특히 네트워크 상태가 좋지 않은 환경에서는 이러한 문제가 더욱 심각하게 나타났다.
Ajax의 등장은 이러한 전통적인 웹 개발 패러다임을 완전히 뒤바꾸어 놓았다. Ajax를 통해 개발자들은 서버로부터 웹페이지의 변경에 필요한 데이터만을 비동기 방식으로 전송받을 수 있게 되었다. 이는 웹페이지에서 변경이 필요한 부분만을 선택적으로 업데이트할 수 있게 만들었고, 변경이 불필요한 부분은 그대로 유지할 수 있게 했다. 이러한 혁신적인 접근 방식은 웹 애플리케이션의 성능과 사용자 경험을 획기적으로 개선했다.
Ajax가 가져온 혁신적인 장점들을 자세히 살펴보면 다음과 같다.
- 효율적인 데이터 통신의 구현: Ajax는 필요한 데이터만을 주고받을 수 있게 만들었다. 예를 들어, 소셜 미디어 피드를 업데이트할 때 전체 페이지를 다시 로드하는 대신 새로운 게시물 데이터만을 받아와서 기존 페이지에 추가할 수 있게 되었다. 이는 네트워크 트래픽을 크게 줄이고, 서버의 부하도 감소시켰다. 특히 모바일 환경에서 이러한 최적화는 데이터 사용량 절감과 배터리 수명 연장으로 이어졌다.
- 향상된 사용자 경험과 부분 렌더링: 변경이 필요한 부분만을 업데이트함으로써 화면 깜박임 현상이 사라졌다. 이는 단순히 시각적인 개선을 넘어서 웹 애플리케이션의 사용성을 데스크톱 애플리케이션 수준으로 끌어올리는 데 크게 기여했다. 사용자들은 페이지 전환 시 발생하는 불필요한 로딩 시간과 시각적 방해 없이 더욱 매끄러운 경험을 할 수 있게 되었다.
- 비동기 통신을 통한 반응성 향상: 서버와의 통신이 비동기적으로 이루어지면서 사용자는 데이터를 요청한 후에도 다른 작업을 계속할 수 있게 되었다. 예를 들어, 긴 목록을 필터링하는 동안에도 사용자는 다른 UI 요소와 상호작용할 수 있다. 이러한 비동기 처리는 웹 애플리케이션의 반응성과 동시성을 크게 향상시켰다.
이러한 Ajax의 특징들은 현대 웹 개발에 지대한 영향을 미쳤다. 구글 맵스를 시작으로 많은 웹 애플리케이션들이 Ajax를 적극적으로 도입했고, 이는 웹이 단순한 문서 표시 플랫폼에서 벗어나 풍부한 상호작용이 가능한 애플리케이션 플랫폼으로 발전하는 데 핵심적인 역할을 했다. 현재는 대부분의 현대적인 웹 프레임워크와 라이브러리들이 Ajax를 기본적으로 지원하고 있으며, 이를 더욱 쉽게 사용할 수 있도록 다양한 추상화 계층을 제공하고 있다.
이처럼 Ajax는 웹 개발의 패러다임을 변화시키고, 웹 애플리케이션의 가능성을 크게 확장시킨 혁신적인 기술이다. 현대 웹 개발에서 Ajax는 필수적인 기술로 자리 잡았으며, 이를 통해 개발자들은 더 나은 사용자 경험을 제공하는 웹 애플리케이션을 만들 수 있게 되었다.
JSON
JSON(JavaScript Object Notation)은 클라이언트와 서버 간의 HTTP 통신을 위한 텍스트 데이터 포맷이다. 웹 애플리케이션에서 데이터를 교환할 때 사용하는 가장 일반적인 데이터 포맷으로, 특히 REST API를 통한 통신에서 널리 사용된다.
JSON의 가장 큰 특징은 자바스크립트에 종속되지 않는 언어 독립형 데이터 포맷이라는 점이다. 이는 JSON이 단순한 텍스트 형식을 취하고 있어 자바, 파이썬, C++등 대부분의 프로그래밍 언어에서 쉽게 파싱하고 생성할 수 있기 때문이다. 심지어 SQL 데이터베이스에서도 JSON 타입을 지원할 만큼 범용성이 높은 데이터 포맷이다.
JSON 표기 방식
JSON의 표기 방식은 자바스크립트의 객체 리터럴과 매우 유사한 형태를 가지고 있다. 기본적으로 키와 값으로 구성된 순수한 텍스트 형식을 취하며, 중첩된 객체나 배열 구조도 표현할 수 있다. 하지만 JSON은 데이터 교환을 위한 순수한 데이터 포맷이므로, 자바스크립트의 객체 리터럴과는 다른 몇 가지 엄격한 규칙이 존재한다.
JSON에서 가장 중요한 표기 규칙은 모든 키가 반드시 큰따옴표로 묶여야 한다는 것이다. 자바스크립트의 객체 리터럴에서는 키를 따옴표로 묶지 않아도 되지만, JSON에서는 이를 필수로 요구한다. 또한 문자열 값 역시 반드시 큰따옴표로 묶어야 하며, 작은따옴표는 허용되지 않는다. 이는 JSON 파서의 구현을 단순화하고 파싱 오류를 줄이기 위한 설계 결정이다. 다음은 올바른 JSON 표기의 예시이다.
{
"name": "Lee",
"age": 20,
"alive": true,
"hobby": ["traveling", "tennis"],
"family": {
"father": "John",
"mother": "Jane"
}
}
값으로는 문자열, 숫자, 불리언, null, 배열, 객체를 사용할 수 있다. 단, undefined, 함수, Date 객체 등은 JSON으로 표현할 수 없다. 이러한 제약은 JSON이 다양한 프로그래밍 언어에서 호환성을 유지하기 위한 것이다.
JSON.stringfy
JSON.stringify 메서드는 자바스크립트의 객체나 배열을 JSON 포맷의 문자열로 변환하는 직렬화 기능을 수행한다. 이는 클라이언트가 서버로 데이터를 전송할 때 필수적인 과정이다. HTTP 통신은 기본적으로 텍스트 기반으로 이루어지기 때문에, 자바스크립트 객체를 그대로 전송할 수 없다. 따라서 객체를 문자열로 변환하는 과정이 필요한데, 이를 '직렬화(serializing)'라고 부른다.
JSON.stringify 메서드는 세 가지 매개변수를 받을 수 있다. 첫 번째는 변환할 대상 객체이고, 두 번째는 replacer 함수 또는 배열, 세 번째는 들여쓰기 옵션이다. 기본적인 사용 방법은 다음과 같다.
const obj = {
name: "Lee",
age: 20,
alive: true,
hobby: ["traveling", "tennis"],
spouse: undefined,
sayHi: function() { console.log('Hi!'); }
};
// 기본 변환
const json = JSON.stringify(obj);
console.log(typeof json, json);
// string {"name":"Lee","age":20,"alive":true,"hobby":["traveling","tennis"]}
위 예제에서 주목할 점은 undefined와 함수가 직렬화 과정에서 제외되었다는 것이다.
JSON.stringify는 직렬화 과정에서 다음과 같은 특별한 동작을 수행한다.
- undefined, 함수, Symbol 값은 변환 과정에서 누락된다.
- NaN, Infinity, -Infinity는 null로 변환된다.
- Date 객체는 ISO 형식의 문자열로 변환된다.
- 순환 참조가 있는 객체는 오류를 발생시킨다.
JSON.stringify의 두 번째 매개변수인 replacer를 활용하면 변환 과정을 더 세밀하게 제어할 수 있다. replacer 함수는 키와 값을 매개변수로 받아서 변환 방식을 직접 정의할 수 있게 해준다.
// 숫자 타입의 값을 제외하는 replacer 함수
function filter(key, value) {
// undefined를 반환하면 해당 속성은 JSON 문자열에 포함되지 않는다
return typeof value === 'number' ? undefined : value;
}
const strFiltered = JSON.stringify(obj, filter, 2);
console.log(strFiltered);
/*
{
"name": "Lee",
"alive": true,
"hobby": [
"traveling",
"tennis"
]
}
*/
세 번째 매개변수는 들여쓰기를 제어한다. 숫자를 전달하면 해당 숫자만큼의 공백으로 들여쓰기를 수행하며, 문자열을 전달하면 그 문자열로 들여쓰기를 수행한다. 이는 디버깅이나 로깅 목적으로 JSON 문자열을 보기 좋게 포맷팅할 때 유용하다.
const todos = [
{ id: 1, content: 'HTML', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 3, content: 'JavaScript', completed: false }
];
const json = JSON.stringify(todos, null, 2);
console.log(typeof json, json);
/*
string [
{
"id": 1,
"content": "HTML",
"completed": false
},
{
"id": 2,
"content": "CSS",
"completed": true
},
{
"id": 3,
"content": "JavaScript",
"completed": false
}
]
*/
JSON.stringify는 객체뿐만 아니라 배열도 JSON 포맷의 문자열로 변환할 수 있다. 위의 예제처럼 배열 내부에 객체가 중첩되어 있는 경우에도 모든 요소를 적절히 직렬화한다. 이는 복잡한 데이터 구조를 서버로 전송할 때 특히 유용하다.
JSON.parse
JSON.parse 메서드는 JSON.stringify와 반대되는 역할을 수행한다. JSON 포맷의 문자열을 자바스크립트의 객체나 배열로 변환하는 역직렬화 기능을 제공한다. 서버로부터 클라이언트가 전송받은 JSON 데이터는 항상 문자열 형태이므로, 이를 자바스크립트에서 실제 객체나 배열로 사용하기 위해서는 반드시 역직렬화 과정이 필요하다.
JSON.parse 메서드의 기본적인 사용법은 다음과 같다.
const obj = {
name: "Lee",
age: 20,
alive: true,
hobby: ["traveling", "tennis"]
};
// 객체를 JSON 문자열로 변환
const json = JSON.stringify(obj);
// JSON 문자열을 다시 객체로 변환
const parsed = JSON.parse(json);
console.log(typeof parsed, parsed);
// object { name: "Lee", age: 20, alive: true, hobby: ["traveling", "tennis"] }
JSON.parse는 JSON 문자열을 파싱할 때 문자열이 유효한 JSON 포맷이 아니면 신택스 에러를 발생시킨다. 이는 데이터의 무결성을 보장하는 중요한 안전장치이다. 예를 들어, 작은따옴표를 사용하거나, 키를 따옴표로 감싸지 않은 경우 에러가 발생한다.
// 유효하지 않은 JSON 문자열 예시들
const invalidJson1 = "{ name: 'Lee' }"; // 키가 따옴표로 감싸져 있지 않음
const invalidJson2 = "{ 'name': 'Lee' }"; // 작은따옴표 사용
// 다음과 같이 호출하면 신택스 에러가 발생한다
// JSON.parse(invalidJson1); // SyntaxError
// JSON.parse(invalidJson2); // SyntaxError
JSON.parse는 배열이 JSON 포맷의 문자열로 변환되어 있는 경우, 이를 다시 배열 객체로 변환한다. 이때 배열의 요소가 객체인 경우에도 모든 요소를 올바르게 객체로 변환한다. 이러한 특성은 서버로부터 받은 복잡한 데이터 구조를 그대로 복원할 수 있게 해준다.
const todos = [
{ id: 1, content: 'HTML', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 3, content: 'JavaScript', completed: false }
];
// 배열을 JSON 문자열로 변환
const json = JSON.stringify(todos);
// JSON 문자열을 다시 배열로 변환
const parsed = JSON.parse(json);
console.log(Array.isArray(parsed), parsed);
/*
true [
{ id: 1, content: 'HTML', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 3, content: 'JavaScript', completed: false }
]
*/
이처럼 JSON.parse와 JSON.stringify는 웹 애플리케이션에서 데이터를 주고받을 때 필수적으로 사용되는 메서드이다. 이 두 메서드를 통해 복잡한 자바스크립트 데이터 구조를 문자열로 변환하고, 다시 원래의 데이터 구조로 복원할 수 있다. 이는 웹 애플리케이션의 클라이언트-서버 통신에서 핵심적인 역할을 수행한다.
XMLHttpRequest
브라우저는 기본적으로 웹 페이지를 요청하고 받아오는 여러 가지 방법을 제공한다. 사용자가 주소창에 URL을 입력하거나, HTML의 form 태그를 통해 데이터를 제출하거나, a 태그를 클릭하는 등의 방법으로 HTTP 요청을 보낼 수 있다. 하지만 이러한 기본적인 방법들은 사용자의 직접적인 행동을 필요로 하며, 프로그래밍적으로 제어하기 어렵다.
XMLHttpRequest는 이러한 한계를 극복하고 자바스크립트를 사용하여 HTTP 통신을 프로그래밍적으로 제어할 수 있게 해주는 핵심적인 객체이다. 즉, 브라우저에서 제공하는 Web API로, HTTP 요청 전송과 HTTP 응답 수신을 위한 다양한 메서드와 프로퍼티들을 포함하고 있다.
XMLHttpRequest 객체 생성
XMLHttpRequest 객체는 브라우저에서 제공하는 Web API 중 하나로, XMLHttpRequest 생성자 함수를 통해 생성할 수 있다. 이 객체는 브라우저의 Web API이기 때문에 Node.js와 같은 서버 사이드 환경에서는 사용할 수 없다는 특징이 있다. 오직 브라우저 환경에서만 정상적으로 동작하며, 이는 이 객체가 클라이언트 사이드 웹 개발을 위해 특별히 설계되었기 때문이다.
const xhr = new XMLHttpRequest();
이렇게 생성된 XMLHttpRequest 객체는 HTTP 요청을 생성하고 전송하는데 필요한 모든 기능을 포함하고 있다. 객체 생성은 간단하지만, 이 객체가 제공하는 다양한 프로퍼티와 메서드를 이해하고 적절히 활용하는 것이 중요하다.
XMLHttpRequest 객체의 프로퍼티와 메서드
XMLHttpRequest 객체는 HTTP 통신의 모든 단계를 세밀하게 제어할 수 있도록 풍부한 프로퍼티와 메서드를 제공한다.
이러한 프로퍼티와 메서드들은 크게 네 가지 카테고리로 나눌 수 있다. 프로토타입 프로퍼티, 이벤트 핸들러 프로퍼티, 메서드, 그리고 정적 프로퍼티이다.
프로토타입 프로퍼티는 HTTP 통신의 현재 상태와 결과를 표현하는 다양한 값들을 포함한다.
각 프로토타입 프로퍼티의 특징은 다음과 같다.
- readyState: HTTP 요청의 진행 상태를 숫자로 표현하는 정수값이다. 이 프로퍼티는 통신이 진행됨에 따라 다음과 같은 값들로 변화한다.
- UNSENT (0): XMLHttpRequest 객체가 생성되었지만, open() 메서드가 아직 호출되지 않은 초기 상태이다.
- OPENED (1): open() 메서드가 호출되어 연결이 설정된 상태이다. 이 상태에서는 요청 헤더를 설정할 수 있다.
- HEADERS_RECEIVED (2): send() 메서드가 호출되어 서버가 응답 헤더를 전송한 상태이다.
- LOADING (3): 서버가 응답 본문을 전송하고 있는 상태이다. 이 때 일부 데이터는 이미 수신되어 있을 수 있다.
- DONE (4): 서버로부터의 응답이 완전히 완료된 상태이다.
- status: HTTP 응답 상태를 나타내는 숫자 코드이다. 가장 흔한 상태 코드로는 200(성공), 404(찾을 수 없음), 500(서버 내부 오류) 등이 있다. 이 코드를 통해 요청이 성공적으로 처리되었는지 아니면 어떤 문제가 발생했는지를 판단할 수 있다.
- statusText: HTTP 응답 상태를 설명하는 문자열이다. 예를 들어 status가 200일 때는 "OK", 404일 때는 "Not Found"와 같은 값을 가진다. 이는 주로 디버깅 목적으로 사용된다.
- responseType: 서버로부터 받을 데이터의 형식을 지정하는 프로퍼티이다. 설정 가능한 값으로는 다음과 같은 것들이 있다.
- "text": 일반 텍스트 형식
- "json": JSON 형식
- "document": XML 또는 HTML 문서
- "blob": 바이너리 데이터
- "arraybuffer": 바이너리 데이터의 저수준 표현
- response: 서버가 보낸 응답 데이터를 담고 있는 프로퍼티이다. responseType 설정에 따라 이 프로퍼티의 타입이 달라진다. 예를 들어, responseType이 "json"이면 자동으로 파싱된 JavaScript 객체가 되고, "text"이면 문자열이 된다.
이벤트 핸들러 프로퍼티들은 HTTP 통신의 각 단계에서 발생하는 이벤트를 처리하는 함수를 할당받는다.
각 이벤트 핸들러의 특징은 다음과 같다.
- onreadystatechange: readyState 프로퍼티의 값이 변경될 때마다 호출되는 이벤트 핸들러이다. HTTP 통신의 전체 생명주기를 추적하고 싶을 때 사용한다. 다음과 같은 상황에서 발생한다.
- 요청이 시작될 때
- 헤더가 수신될 때
- 응답 데이터가 수신되는 동안
- 응답이 완료될 때
- onloadstart: HTTP 요청이 시작되어 서버로부터 첫 번째 응답을 받기 시작할 때 호출된다. 이 시점에서 로딩 인디케이터를 표시하는 등의 작업을 수행할 수 있다.
- onprogress: 데이터를 수신하는 동안 주기적으로 호출되는 이벤트 핸들러이다. 이벤트 객체를 통해 전체 데이터 크기 대비 현재까지 받은 데이터의 크기를 알 수 있어, 진행률 표시 등에 활용할 수 있다.
- onabort: 진행 중인 HTTP 요청이 abort() 메서드 호출에 의해 중단되었을 때 호출된다. 사용자가 페이지를 벗어나거나, 새로운 요청을 시작하기 위해 이전 요청을 취소하는 경우 등에 발생한다.
- onerror: HTTP 요청 중 네트워크 오류나 기타 오류가 발생했을 때 호출된다. 서버가 응답을 보내지 못하거나, CORS 정책 위반이 발생하는 등의 상황에서 이 이벤트가 발생한다.
- onload: HTTP 요청이 성공적으로 완료되었을 때 호출된다. 이는 전체 응답을 받았다는 것을 의미하며, 이 시점에서 response 프로퍼티를 통해 서버의 응답 데이터에 안전하게 접근할 수 있다.
- ontimeout: 설정된 시간 내에 서버로부터 응답을 받지 못했을 때 호출된다. 네트워크 상태가 좋지 않거나 서버의 응답이 늦어지는 경우에 발생한다.
- onloadend: HTTP 요청이 어떤 형태로든 완료되었을 때 호출된다. 성공적인 완료(onload)든 오류 발생(onerror)이든, 요청 중단(onabort)이든 상관없이 요청이 종료되면 이 이벤트가 발생한다.
XMLHttpRequest 객체는 또한 HTTP 통신을 제어하기 위한 다양한 메서드를 제공한다.
각 XMLHttpRequest 객체 메서드의 특징은 다음과 같다.
- open(): HTTP 요청을 초기화하는 메서드이다. 요청 메서드(GET, POST 등), URL, 비동기 여부 등을 설정한다. 이 메서드는 실제로 요청을 보내지는 않으며, 단지 요청을 준비하는 단계이다.
- send(): 준비된 HTTP 요청을 실제로 서버로 전송한다. POST 요청의 경우 이 메서드를 통해 요청 본문(payload)을 전송할 수 있다.
- abort(): 진행 중인 HTTP 요청을 중단한다. 긴 시간이 걸리는 요청을 취소하거나, 더 이상 필요하지 않은 요청을 중단할 때 사용한다.
- setRequestHeader(): HTTP 요청 헤더를 설정한다. 예를 들어, Content-Type 헤더를 설정하여 서버에게 전송하는 데이터의 형식을 알려줄 수 있다.
HTTP 요청 전송
HTTP 요청을 전송하는 과정은 세 단계로 이루어진다. 각 단계는 순차적으로 실행되어야 하며, 특히 두 번째 단계는 선택적이다. 이러한 단계적 접근은 요청을 세밀하게 제어할 수 있게 해준다.
1. 초기화 단계: XMLHttpRequest.prototype.open() 메서드를 사용하여 요청을 초기화한다. 이 메서드는 요청의 기본 설정을 정의하는 중요한 단계이다.
여기서 각 매개변수는 다음과 같은 의미를 가진다.
xhr.open(method, url[, async])
- method: HTTP 요청 메서드를 지정한다. 주로 사용되는 메서드들은 다음과 같다.
- GET: 데이터를 조회할 때 사용한다. URL의 쿼리 문자열로 데이터를 전달한다.
- POST: 새로운 데이터를 생성할 때 사용한다. 요청 본문에 데이터를 포함한다.
- PUT: 기존 데이터를 완전히 교체할 때 사용한다.
- PATCH: 기존 데이터를 부분적으로 수정할 때 사용한다.
- DELETE: 데이터를 삭제할 때 사용한다.
2. 헤더 설정 단계: XMLHttpRequest.prototype.setRequestHeader() 메서드를 사용하여 요청 헤더를 설정한다. 이 단계에서는 특히 Content-Type과 Accept 헤더가 중요하다.
// 전송할 데이터의 형식 지정
xhr.setRequestHeader('Content-Type', 'application/json');
// 받고자 하는 데이터의 형식 지정
xhr.setRequestHeader('Accept', 'application/json');
- Content-Type 헤더에 지정할 수 있는 주요 MIME 타입들은 다음과 같다.
- text/plain: 일반 텍스트
- text/html: HTML 문서
- application/json: JSON 데이터
- application/x-www-form-urlencoded: 폼 데이터
- multipart/form-data: 파일 업로드를 포함한 폼 데이터
3. 요청 전송 단계: XMLHttpRequest.prototype.send() 메서드를 사용하여 실제로 요청을 전송한다. POST나 PUT 요청의 경우 send() 메서드의 인자로 전송할 데이터를 넣을 수 있다.
// GET 요청의 경우
xhr.send();
// POST 요청의 경우
const data = { name: 'John', age: 30 };
xhr.send(JSON.stringify(data));
- GET 요청 메서드: 데이터를 URL의 일부분인 쿼리 문자열로 서버에 전송한다.
- POST 요청 메서드: 데이터(페이로드)를 요청 몸체(request body)에 담아 전송한다.
- 여기서 주의할 점은 객체를 전송할 때는 반드시 JSON.stringify()를 사용하여 문자열로 변환해야 한다는 것이다. 또한, GET 요청의 경우 send() 메서드에 전달된 데이터는 무시되고 본문이 null로 설정된다.
페이로드
페이로드(영어: payload)는 전송되는 '순수한 데이터'를 뜻한다. 페이로드는 전송의 근본적인 목적이 되는 데이터의 일부분으로 그 데이터와 함께 전송되는 헤더, 메타데이터와 같은 부분은 제외한다.
[중략...]
페이로드라는 용어는 큰 데이터 덩어리 중에 '흥미 있는' 데이터를 구별하는 데 사용된다. 이 용어는 운송업에서 비롯하였는데, 지급(pay)해야 하는 적화물(load)을 의미한다. 예를 들어, 여객기의 경우에는 승객과 화물(passengers & cargo)이 페이로드가 된다.
유조차가 20톤의 기름을 운반한다면 트럭의 총 무게는 차체, 운전자 등의 무게 때문에 그것보다 더 될 것이다. 이 모든 무게를 운송하는데 비용이 들지만, 고객은 오직 기름 20톤 하중의 운송비만 지급(pay)하게 된다. 즉, 하중(적하)에 대한 보수란 의미로 'payload'란 말이 나온 것이다.
<한국어판 위키피디아, 페이로드(컴퓨팅) 문서 中>
페이로드(payload)는 전송되는 데이터에서 실제로 전달하고자 하는 데이터 자체를 의미한다. 프로토콜 오버헤드나 메타데이터를 제외한 순수 데이터 부분을 가리키는데, 이는 택배 시스템에 비유하면 이해하기 쉽다. 택배 상자에는 실제 보내고자 하는 물건 외에도 송장, 포장재, 완충재 등 부가적인 요소들이 포함되어 있다. 이때 실제 물건이 페이로드에 해당하고, 나머지 부가적인 요소들은 데이터 전송을 위한 프로토콜 정보에 해당한다.
HTTP 통신에서 페이로드는 HTTP 요청이나 응답에서 헤더와 메타데이터를 제외한 실제 데이터 부분을 의미한다.
그리고 HTTP 요청에서 페이로드는 메서드 타입에 따라 처리 방식이 다르다.
1. GET 요청: 일반적으로 페이로드를 포함하지 않는다. 대신 데이터를 URL의 쿼리 문자열로 전달한다
// GET 요청의 예시
xhr.open('GET', 'https://api.example.com/data?name=John&age=30');
xhr.send(); // 페이로드 없음
2. POST, PUT, PATCH 요청: 페이로드를 요청 본문에 포함하여 전송한다.
// POST 요청의 예시
const payload = { name: 'John', age: 30 };
xhr.open('POST', 'https://api.example.com/data');
xhr.send(JSON.stringify(payload)); // 페이로드를 포함
종합하여 JSON 형식의 통신을 예로 들면, 다음과 같은 HTTP 요청에서...
{
"status": "200",
"from": "client",
"to": "https://api.example.com/users",
"method": "POST",
"data": {
"username": "john_doe",
"email": "john@example.com"
}
}
"data" 부분이 실제 페이로드이며, 나머지는 통신을 위한 부가 정보들이다.
아까 말했던 것처럼, HTTP 요청 메서드에 따라 페이로드의 처리 방식도 달라지는데, GET 요청의 경우 데이터를 URL의 일부분인 쿼리 문자열로 서버에 전송하고, POST나 PUT 같은 요청의 경우 데이터를 요청 몸체(request body)에 담아 전송한다. 이는 각 요청 메서드의 설계 목적과 용도에 따른 것이다.
HTTP 응답 처리
서버로부터의 응답을 처리하는 방법은 크게 두 가지가 있다. readystatechange 이벤트를 사용하는 전통적인 방식과 load 이벤트를 사용하는 현대적인 방식이다.
1. readystatechange 이벤트를 사용한 응답 처리
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.onreadystatechange = function() {
// 통신이 완료되지 않은 경우 무시
if (xhr.readyState !== XMLHttpRequest.DONE) return;
// 응답 상태 코드에 따른 처리
if (xhr.status === 200) {
const response = JSON.parse(xhr.response);
console.log('데이터 수신 성공:', response);
} else {
console.error('오류 발생:', xhr.status, xhr.statusText);
}
};
xhr.send();
이 방식에서는 readyState의 변화를 감지하여 응답을 처리한다. readyState가 DONE(4)이 되었을 때 실제 응답 처리를 시작하며, status 값을 확인하여 요청의 성공 여부를 판단한다. 이 방식의 특징은 통신의 모든 상태 변화를 추적할 수 있다는 것이다.
2. load 이벤트를 사용한 응답 처리
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.onload = function() {
if (xhr.status === 200) {
const response = JSON.parse(xhr.response);
console.log('데이터 수신 성공:', response);
} else {
console.error('오류 발생:', xhr.status, xhr.statusText);
}
};
xhr.send();
load 이벤트는 통신이 완료되었을 때만 발생하므로, readyState 검사가 필요 없다는 장점이 있다. 이 방식이 더 간단하고 직관적이지만, 통신 과정의 중간 상태를 확인할 수 없다는 특징이 있다.
실제 애플리케이션에서는 다음과 같은 추가적인 에러 처리와 타임아웃 설정도 고려해야 한다.
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
// 타임아웃 설정 (5초)
xhr.timeout = 5000;
// 성공적인 응답 처리
xhr.onload = function() {
if (xhr.status === 200) {
const response = JSON.parse(xhr.response);
console.log('데이터 수신 성공:', response);
} else {
console.error('서버 오류:', xhr.status, xhr.statusText);
}
};
// 네트워크 에러 처리
xhr.onerror = function() {
console.error('네트워크 오류 발생');
};
// 타임아웃 처리
xhr.ontimeout = function() {
console.error('요청 시간 초과');
};
// 진행 상황 모니터링
xhr.onprogress = function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`진행률: ${percentComplete}%`);
}
};
xhr.send();
이러한 XMLHttpRequest의 복잡성과 콜백 기반의 비동기 처리 방식 때문에, 최근에는 fetch API나 axios와 같은 더 현대적인 HTTP 클라이언트를 사용하는 것이 일반적이다. 이러한 도구들은 프로미스 기반으로 작동하여 더 깔끔한 코드 작성이 가능하고, 에러 처리도 더 쉽게 할 수 있다.
하지만 XMLHttpRequest는 여전히 웹 플랫폼의 기본적인 부분이며, 이러한 현대적인 도구들의 내부 구현 기반이 되고 있다.
요약
Ajax의 기본 개념과 역사
- Ajax(Asynchronous JavaScript and XML)는 브라우저와 서버 간의 비동기 통신을 가능하게 하는 프로그래밍 방식이다.
- 1999년 마이크로소프트가 XMLHttpRequest를 개발했으며, 2005년 구글 맵스를 통해 그 가치가 입증되었다.
- Ajax는 전통적인 웹 페이지의 한계를 극복하고, 데스크톱 애플리케이션과 유사한 사용자 경험을 제공하게 되었다.
Ajax의 특징과 장점
- 비동기 통신
- 서버와의 통신이 백그라운드에서 이루어짐
- 페이지 전체를 다시 로드하지 않고도 데이터 갱신 가능
- 사용자 경험 향상과 서버 부하 감소
- 부분적 페이지 갱신
- 필요한 부분만 업데이트 가능
- 화면 깜박임 현상 제거
- 더 나은 사용자 경험 제공
JSON의 이해와 활용
- JSON의 기본 개념
- 클라이언트와 서버 간의 HTTP 통신을 위한 텍스트 데이터 포맷
- 자바스크립트 객체 리터럴과 유사한 형식을 가짐
- 언어 독립적인 데이터 포맷으로, 대부분의 프로그래밍 언어에서 지원
- JSON 표기 방식의 특징
- 모든 키는 반드시 큰따옴표로 묶어야 함
- 값으로는 문자열, 숫자, 불리언, null, 배열, 객체만 사용 가능
- undefined, 함수, Date 객체 등은 사용 불가
- 작은따옴표 사용이 허용되지 않음
- JSON 메서드 (아래 코드 참고)
// JSON.stringify
const obj = { name: "Lee", age: 20 };
const json = JSON.stringify(obj); // 직렬화
// JSON.parse
const parsed = JSON.parse(json); // 역직렬화
XMLHttpRequest 객체의 이해
- 기본 구성
- 브라우저에서 제공하는 Web API
- HTTP 요청 전송과 응답 수신을 위한 다양한 메서드와 프로퍼티 제공
- 비동기 통신의 기본이 되는 객체
- 주요 프로퍼티
- readyState: HTTP 통신의 현재 상태를 나타내는 정수
- status: HTTP 응답 상태 코드
- responseType: 응답 데이터의 타입
- response: 응답 데이터
- 이벤트 핸들러 (아래 코드 참고)
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
// 성공적인 응답 처리
} else {
// 에러 처리
}
}
};
HTTP 요청과 응답 처리
// 1. XMLHttpRequest 객체 생성
const xhr = new XMLHttpRequest();
// 2. 요청 초기화
xhr.open('GET', '/api/users');
// 3. 헤더 설정 (필요한 경우)
xhr.setRequestHeader('Content-Type', 'application/json');
// 4. 요청 전송
xhr.send();
- HTTP 요청 전송 단계 (위 코드 참고)
- HTTP 메서드의 특징
- GET: 데이터 조회, URL로 데이터 전달
- POST: 데이터 생성, 요청 몸체에 데이터 포함
- PUT: 데이터 전체 교체
- PATCH: 데이터 일부 수정
- DELETE: 데이터 삭제
- 응답 처리 방식 (아래 코드 참고)
// readystatechange 이벤트 사용
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status === 200) {
console.log(JSON.parse(xhr.response));
} else {
console.error('Error:', xhr.status);
}
};
// load 이벤트 사용 (더 간단한 방식)
xhr.onload = () => {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.response));
} else {
console.error('Error:', xhr.status);
}
};
에러 처리와 타임아웃
const xhr = new XMLHttpRequest();
// 네트워크 에러
xhr.onerror = () => {
console.error('Network error occurred');
};
// 타임아웃
xhr.timeout = 5000; // 5초
xhr.ontimeout = () => {
console.error('Request timed out');
};
// 진행 상황 모니터링
xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Progress: ${percentComplete}%`);
}
};
- 에러 상황별 처리 (위 코드 참고)
- HTTP 상태 코드별 처리
- 2XX: 성공 (200 OK, 201 Created 등)
- 3XX: 리다이렉션
- 4XX: 클라이언트 에러 (404 Not Found 등)
- 5XX: 서버 에러
모던 웹에서의 Ajax 활용
- 실제 응용 사례
- 실시간 검색어 추천
- 무한 스크롤
- 실시간 폼 유효성 검사
- 동적 콘텐츠 로딩
- 최신 대안 기술
- Fetch API: 프로미스 기반의 더 현대적인 API
- Axios: XMLHttpRequest를 래핑한 라이브러리
- GraphQL: 더 유연한 데이터 요청 가능
- Ajax 사용 시 주의사항
- CORS(Cross-Origin Resource Sharing) 정책 고려
- 적절한 에러 처리
- 사용자 피드백 제공
- 성능과 네트워크 부하 고려
예상문제 [🔥]
https://github.com/junh0328/prepare_frontend_interview?tab=readme-ov-file
Ajax가 뭔가요 어떤 것을 담당하고 있죠?
Ajax는 Asynchronous JavaScript and XML의 약자로, 자바스크립트를 사용하여 브라우저가 서버에 비동기적으로 데이터를 요청하고 응답 받아 동적으로 웹 페이지를 갱신하는 프로그래밍 방식입니다.
Ajax는 1999년 마이크로소프트가 개발한 XMLHttpRequest를 기반으로 동작하며, 2005년 구글 맵스를 통해 그 가치가 입증되었습니다. 특히 구글 맵스는 웹 브라우저에서도 데스크톱 애플리케이션과 같은 퍼포먼스와 부드러운 화면 전환이 가능하다는 것을 보여주었죠.
Ajax의 가장 핵심적인 역할은 페이지 전체를 새로고침하지 않고도 서버로부터 데이터를 비동기적으로 받아와 웹 페이지의 일부분만을 동적으로 갱신할 수 있게 해준다는 것입니다. 이를 통해 더 나은 사용자 경험과 서버 자원의 효율적인 사용이 가능해졌습니다.
Ajax를 사용하면 기존 방식과 어떤 차이가 있을까요?
Ajax를 사용하기 전의 전통적인 웹 방식에는 세 가지 주요한 한계가 있었습니다.
첫째, 불필요한 데이터 통신이 많았습니다. 페이지의 일부분만 변경하고 싶어도 전체 HTML을 다시 받아와야 했죠. 예를 들어 네비게이션 바와 푸터가 동일한 페이지에서 컨텐츠 영역만 변경하고 싶은 경우에도, 변경이 불필요한 부분까지 포함된 전체 HTML을 다시 전송받아야 했습니다.
둘째, 화면 깜빡임 문제가 있었습니다. 변경이 필요 없는 부분까지 모두 다시 렌더링하다 보니, 화면 전환 시 사용자는 항상 깜빡임을 경험해야 했습니다. 이는 웹 애플리케이션의 사용성을 크게 저해하는 요소였죠.
셋째, 동기 방식으로 인한 블로킹 현상이 있었습니다. 서버로부터 응답이 올 때까지 다음 작업이 모두 중단되었기 때문에, 사용자는 요청이 완료될 때까지 아무것도 할 수 없었습니다.
반면 Ajax를 사용하면 이러한 문제들이 해결됩니다.
- 필요한 데이터만 서버에서 받아올 수 있어 불필요한 통신이 줄어들고,
- 페이지의 일부분만 갱신할 수 있어 화면 깜박임이 없어지며,
- 비동기 방식으로 동작하기 때문에 서버 응답을 기다리는 동안에도 다른 작업이 가능합니다.
이러한 차이점들로 인해 Ajax는 더 나은 사용자 경험과 성능을 제공할 수 있게 되었습니다.
JSON 이 뭔가요?
JSON(JavaScript Object Notation)은 클라이언트와 서버 간의 HTTP 통신을 위한 텍스트 데이터 포맷입니다. 자바스크립트의 객체 리터럴과 매우 유사한 형식을 가지고 있지만, 더 엄격한 규칙이 적용됩니다.
JSON의 가장 큰 특징은 언어 독립적이라는 점입니다. 비록 자바스크립트에서 유래했지만, 대부분의 프로그래밍 언어에서 파싱하고 생성할 수 있는 데이터 포맷이죠. 심지어 SQL 데이터베이스에서도 JSON 타입을 지원할 만큼 범용성이 높습니다.
JSON은 몇 가지 중요한 규칙이 있습니다.
- 모든 키는 반드시 큰따옴표로 묶어야 합니다.
- 값으로는 문자열, 숫자, 불리언, null, 배열, 객체만 사용할 수 있습니다.
- undefined, 함수, Date 객체 등은 JSON으로 표현할 수 없습니다.
- 작은따옴표는 사용할 수 없고, 반드시 큰따옴표를 사용해야 합니다.
JSON이 제공하는 정적 프로토타입 메서드에 대해 몇가지 말해볼 수 있나요?
JSON에서 가장 중요한 두 가지 정적 메서드는 JSON.stringify()와 JSON.parse()입니다.
첫째, JSON.stringify()는 자바스크립트 객체를 JSON 포맷의 문자열로 변환하는 직렬화를 수행합니다. 이는 클라이언트가 서버로 데이터를 전송할 때 필수적입니다. HTTP 통신은 기본적으로 텍스트 기반이기 때문이죠. 이 메서드는 세 가지 매개변수를 받을 수 있습니다.
// 기본 사용
JSON.stringify({ name: 'Lee', age: 20 });
// replacer 함수를 사용한 변환 제어
JSON.stringify(obj, (key, value) =>
typeof value === 'number' ? undefined : value);
// 들여쓰기 옵션 추가
JSON.stringify(obj, null, 2);
둘째, JSON.parse()는 JSON 포맷의 문자열을 자바스크립트 객체로 변환하는 역직렬화를 수행합니다. 서버로부터 받은 JSON 데이터를 자바스크립트에서 사용할 수 있게 해주죠.
const obj = JSON.parse('{"name":"Lee","age":20}');
이때 주의할 점은, JSON.parse()는 잘못된 형식의 JSON 문자열을 만나면 신택스 에러를 발생시킨다는 것입니다. 또한 Date 객체의 경우 JSON.stringify() 시에는 ISO 형식의 문자열로 변환되지만, JSON.parse() 시에는 다시 Date 객체로 자동 변환되지 않는다는 특징이 있습니다.
Ajax로 HTTP 요청을 보내기 위해서는 어떤 방법을 사용할 수 있나요?
Ajax로 HTTP 요청을 보내는 방법은 크게 세 가지가 있습니다.
첫째, XMLHttpRequest 객체를 사용하는 방법입니다. 가장 오래된 전통적인 방법이죠.
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.onload = () => {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.response));
}
};
xhr.send();
둘째, fetch API를 사용하는 방법입니다. 이는 프로미스 기반의 더 현대적인 API입니다.
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
셋째, axios와 같은 HTTP 클라이언트 라이브러리를 사용하는 방법입니다. 이는 XMLHttpRequest를 래핑한 라이브러리로, 더 편리한 API를 제공합니다.
axios.get('/api/users')
.then(response => console.log(response.data))
.catch(error => console.error(error));
각각의 방법은 장단점이 있지만, 최근에는 fetch나 axios를 주로 사용하는 추세입니다.
XMLHttpRequest와 fetch 메서드의 차이는 무엇이라고 생각하시나요? 🔥
XMLHttpRequest와 fetch는 몇 가지 중요한 차이점이 있습니다.
첫째, 가장 큰 차이점은 프로미스 지원 여부입니다. XMLHttpRequest는 콜백 기반으로 동작하여 여러 요청을 연달아 처리할 때 콜백 지옥이 발생할 수 있습니다. 반면 fetch는 프로미스를 반환하므로 then과 catch를 사용해 더 깔끔한 코드를 작성할 수 있고, async/await와도 함께 사용할 수 있습니다.
// XMLHttpRequest 방식
const xhr = new XMLHttpRequest();
xhr.open('GET', 'url1');
xhr.onload = () => {
const data1 = JSON.parse(xhr.response);
const xhr2 = new XMLHttpRequest();
xhr2.open('GET', 'url2');
xhr2.onload = () => {
const data2 = JSON.parse(xhr2.response);
// 콜백 지옥 발생 가능
};
xhr2.send();
};
xhr.send();
// fetch 방식
fetch('url1')
.then(response => response.json())
.then(data1 => fetch('url2'))
.then(response => response.json())
.then(data2 => console.log(data2));
// 또는 async/await 사용
async function getData() {
const data1 = await fetch('url1').then(res => res.json());
const data2 = await fetch('url2').then(res => res.json());
}
둘째, 에러 처리 방식이 다릅니다. XMLHttpRequest는 네트워크 에러를 onerror 이벤트로 처리하고, HTTP 에러는 status 코드를 확인해야 합니다. 반면 fetch는 네트워크 에러만 reject하고, HTTP 에러는 response의 ok 속성을 확인해야 합니다.
셋째, 기능의 범위가 다릅니다. XMLHttpRequest는 요청의 진행 상태 모니터링이나 업로드 진행률 확인 등 더 많은 기능을 제공합니다. 반면 fetch는 더 단순하고 현대적인 API를 제공하지만, 일부 고급 기능은 별도로 구현해야 합니다.
// XMLHttpRequest의 진행 상태 모니터링
const xhr = new XMLHttpRequest();
xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Progress: ${percentComplete}%`);
}
};
// fetch는 이런 기능이 기본적으로 제공되지 않음
이러한 차이점들 때문에, 오늘날의 웹 개발에서는 주로 fetch나 axios같은 라이브러리를 사용하는 것이 선호됩니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 45장 - Promise (0) | 2024.11.26 |
---|---|
[모던 자바스크립트 Deep Dive] 44장 - REST API (2) | 2024.11.24 |
[모던 자바스크립트 Deep Dive] 42장 - 비동기 프로그래밍 (0) | 2024.11.12 |
[모던 자바스크립트 Deep Dive] 41장 - 타이머 (0) | 2024.11.12 |
[모던 자바스크립트 Deep Dive] 40장 - 이벤트 (0) | 2024.11.11 |