본문 바로가기

React/Rest API

REST API #3 - Fetch

구글은 정말 대단합니다

Fetch API?

예전의 방식인 server layer를 통해서 API call을 하는 것보다는 client layer에서 직접 API를 호출하는 것이 웹 개발의 새로운 트렌드로 도입되었습니다. 네트워크 요청은 대부분 요청 전송 -> 사용자 정보와 요청에 대한 정보를 통해서 타당성 확인 -> 서버에서 해당 요청에 따른 최신 변경분을 반환 과 같은 방식으로 수행되었습니다. 또한 전자의 경우는 반환을 할 때 페이지 새로고침이 필요했습니다. 후자의 방식은 이러한 모든 것을 페이지 새로 고침 없이 가능하게 했습니다.

 

google.com을 통해서 처음 보이는 페이지에서 검색어를 입력하면 해당 부분에 대한 자동완성이 되는 것을 '새롭다'라고 생각하지 않고 현재는 사용하지만, 맨 처음에 해당 기술을 도입한 시기에는 모든 사람이 놀라움에 빠졌습니다. 해당 기술은 AJAX( Asynchronous Javascript And XML)이라는 용어로 브라우저에서 제공하는 Web API인 XMLHttpRequest 객체를 기반으로 동작합니다. XMLHttpRequest는 HTTP 비동기 통신을 위한 메서드와 프로퍼티를 제공합니다. 해당 기술에 대해서도 차후 다뤄보도록 하겠습니다.

 

AJAX 이외에도 서버에 네트워크 요청을 보내고 요청에 따른 필요한 정보를 받아오는 방법은 request, axios, jQuery... 등 많이 존재합니다. 브라우저에서 fetch() 함수를 지원하지 않았을 때에는 client side에서 HTTP request-response format를 작성하는 것이 어려웠으나, 현재는 말씀드린 서드 파티 라이브러리가 아닌 자바스크립트에서 제공하는 Fetch API를 사용하는 것으로 대체를 하게 된다면 webpack bundle파일 크기를 줄이는 것을 통해서 더욱 빠르게 사용자에게 페이지를 제공할 수 있다는 장점이 있습니다.

fetch() function

fetch() function은 polyfill을 통해서 bundler option을 주면 대부분의 브라우저에서 지원이 가능합니다.

Promise객체를 반환하는 함수는 다음과 같습니다.

 

const promise = fetch(url[, options ]);

 

- url : 접근하고자 하는 url이며 backend에서 구성해 둔 API contract에 해당합니다.

- options : 객체로 들어가는 선택적 매개변수이며, method, header적용이 가능합니다. no option의 경우 HTTP method GET으로 호출하게 됩니다.

 

Promise객체의 반환 이후 response를 재처리할 수 있습니다. 

 

fetch(url, options)
	.then(response => console.log(response))
	.catch(error => console.log(error));

 

상단의 코드 방식을 try - catch와 함께 await를 사용하는 것으로 에러 처리를 효과적으로 할 수 있습니다.

 

const MY_OPTIONS = [];
const MY_URL = 'ansrlm.tistory.com';

const MyService = {
	getBlogPostList: () => {
		const url = MY_URL;
		return baseFetchService(url, 'getBlogPostList error', 'array');
	},
    
	getBlogPost: (number) => {
		const url = MY_URL + '/' + number;
		return baseFetchService(url, 'getBlogPost ' + number + 'th error', 'object');
	},
};

const baseFetchService: (url, errorLog, responseType) => {
	try {
		const response = await fetch(url);
		if(response.ok) {
			const responseJSON = await response.json();
			return responseJSON;
		}
	} catch(e) {
		console.log(errorLog, e);
		if(responseType === 'array') {
			return [];
		} else if(responseType === 'object') {
			return {};
		}
    }
};

 

response.ok와 response.status를 통해서 HTTP상태를 확인할 수 있습니다. (status는 ansrlm.tistory.com/6의 끝에서 소개드린 상태 코드에 해당합니다. ok의 경우 boolean type으로 HTTP status code가 200 ~ 299 사이, 즉 성공이라고 response가 올 때 true를 반환합니다)

response를 받은 것을 responseJSON과 같이 변환하는 것을 확인하실 수 있는데요, response에는 Promise기반의 다양한 메서드가 존재해서 원하는 형태로 변환하거나 처리할 수 있습니다.

- response.text() - string타입으로 반환합니다.

- response.json() - JSON타입으로 반환합니다.

- response.formData() - FormData타입으로 반환합니다. 반환을 통해서 form에 다시 입력값을 전달하는 경우가 있으나 FormData타입은 request body parameter로 변환되는 경우가 많습니다. 하단 소스처럼 form tag에 해당하는 values를 body parameter에 보낼 수 있습니다.

 

<form id="myForm">
	<input type="id" name="id" />
	<input type="password" name="password" />
	<input type="submit" />
</form>

<script>
	const myForm.onSubmit = async (e) => {
	
		fetch('/ansrlm.tistory.com', { method: 'POST', body: new FormData(myForm) })
			.then(response => response.json())
			.then(jsonObj => console.log(jsonObj));
	};
</script>

 

- response.blob() - Blob타입으로 반환합니다. Blob의 경우 이미지와 같은 정보를 전달하기 위해서 사용하는 경우가 있습니다.
{type?: string, blobparts: Blob []}의 형태를 가집니다. 만약 HTML 파일에 대한 정보를 받는다면 하단과 같은 형식으로 표현이 가능합니다.

 

const htmlBlob = new Blob(["<span>this is for blob</span>"], {type: 'text/html'});

 

blob타입은 첫 번째 인자로 ArrayBuffer로 들어오기 때문에 해당 부분에 여러 개의 blob를 slice / splice를 통해서 필요한 부분만을 가져올 수 있습니다.

 

htmlBlob.slice([byteStart], [byteEnd], [contentType]);

Parallel fetch requests

병렬적 fetch call은 Promise.all() method를 통해서 구현할 수 있습니다. 여러 개의 정보가 순차적으로 진행될 필요가 없을 때 해당 방식을 통해서 구현을 하게 되면 성능을 향상할 수 있습니다. 만약 병렬적 fetch의 결과들을 합쳐서 하나의 결과를 만들어야 할 경우, Promise.allSettled()를 통해서 사용하는 것으로 쉽게 구현할 수 있습니다.

 

const fetchMyBlogPage: async () => {
	const [headerList, categoryList, postList] = await Promise.all([
		fetch('/ansrlm.tistory.com/headers'),
		fetch('/ansrlm.tistory.com/categories'),
		fetch('/ansrlm.tistory.com/posts'),
	]);
    
    const headerListJSON = await headerList.json();
	const categoryListJSON = await categoryList.json();
	const postListJSON = await postList.json();
    
    return { headerList: headerListJSON,
		categoryList: categoryListJSON,
		postList: postListJSON, };
}

fetchMyBlogPage().then({ headerList, categoryList, postList }) => {
	dispatch({ type: BLOG_PAGE_INFORMATION, 
    		  pageInformation: { headerList: headerList,
				categoryList: categoryList,
				postList: postList, } });
}

 

해당 방식을 통해서 (저가 작성한 것은 사실 json()하는 부분이 await로 걸려 있어서 병렬적 처리가 효과적이지는 않습니다.) context API를 사용하고 있다면, 해당 부분에 값을 넣거나, 화면에서 쓰고 싶은 위치에서 사용할 수 있습니다. race()등 다른 것들에 응용해서 적용이 가능하며 비즈니스 로직에 따른 유연한 적용이 가능합니다.

Credentials

인증서(자격 증명을 포함한)를 보내기 위해서 fetch() method에 credentials options을 추가할 수 있습니다.

 

const fetchWithCredentials = () => {
	fetch('ansrlm.tistory.com', {
		credentials: 'include',
	}
}

const fetchWithCredentialsWhenSameOrigin = () => {
	fetch('ansrlm.tistory.com', {
		credentials: 'same-origin',
	}
}

const fetchWithoutCredentials = () => {
	fetch('ansrlm.tistory.com', {
		credentials: 'omit',
	}
}

 

-  include : cross-origin request에 동일하게 적용 가능하며, 자격 증명이 포함된 인증서를 보낼 수 있습니다.

- same-origin : URL(ansrlm.tistory.com)이 만약 fetch 하고 있는 request URL과 동일하다면 그 경우 자격 증명이 포함된 인증서를 보내도록 처리할 수 있습니다.

- omit : 자격 증명을 포함하지 않는 기능을 적용할 수 있습니다.

File upload

앞서 말씀드렸던, FormData()타입에 해당하는 정보나, <input type="file" multiple? />에 해당하는 input element를 통해서 업로드를 할 수 있습니다.

 

// Form inside of render()
<Form onSubmit={fileUpload}>
	<FormGroup>
		<FormControl
			id = "fileInput"
			type = "file"
			multiple
			accept = ".pdf, .txt, application/gzip, .gz"
			{/* accept type can be added */} 
			{/* onChange = {canBeUsedForTrackingOrSomethingElse} */}
		/>
	</FormGroup>
    
	<Button type = "submit">
		첨부 파일을 포함해서 댓글 작성하기
	</Button>
</Form>

const fileUpload = (e) => {
	e.preventDefault;
	const myFormData = new FormData();
	e.target.files && e.target.files.forEach( (value) => myFormData.append('files', value); );

	fetch('ansrlm.tistory.com/comments', {
		method: 'POST',    
		body: myFormData,
	})
	.then(response => response.json())
	.then(response => console.log(JSON.stringify(response)))
	.catch(error => console.log(error));
}

 

해당 방식처럼 bootstrap, rsuite의 Form / FormGroup를 통해서 파일 업로드를 할 수 있습니다. 다른 방식으로는, createRef를 사용하거나, component안에서 ref를 통해서 fileUpload를 수행할 수 있습니다.

Headers

Headers()를 통해서 FormData()를 선언하는 방식처럼 선언해서 값을 options에 넣어주거나, headers: {}를 통해서 값을 직접적으로 담는 방식이 존재합니다. contents type의 validation과, ServiceWorkers에 도움이 되도록 헤더를 조작할 수 있습니다.

 

fetchA('ansrlm.tistory.com/posts', {
	method: 'POST',
	body: JSON.stringfy(requestBody),
	headers: {
		'Content-Type': 'application/json',
		'X-Api-Key': myAwsApiKey,
		...
	}
});

// those are the same !

const myHeaders = new Headers({
	'Content-Type': 'text/html',
});

myHeaders.set('Content-Type', 'application/json');
myHeaders.append('X-Api-Key', myAwsApiKey);
...

fetchB('ansrlm.tistory.com/post', {
	method: 'POST',
	body: JSON.stringfy(requestBody),
	headers: myHeaders
});

 

x-api-key를 통해서 AWS API Gateway는 이 키를 읽고 사용량 계획에 있는 키를 비교합니다. 일치하는 항목이 있으면 요청을 조절해서 반환하고, 만약에 없다면 403 Forbidden응답을 수신받게 됩니다.

docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/api-gateway-api-key-source.html 에 존재합니다. 

또한 request / response는 앞서 말씀드린 service worker를 통해서 PWA (Progressive Web Application)에 사용될 수 있습니다.

 

※ 잘못된 부분이나 궁금한 점 댓글로 작성해 주시면 최대한 답변하겠습니다. 읽어주셔서 감사합니다.

※ REST API #4 - Axios에서 axios API에 대해서 소개할 예정입니다.

※ service worker와 PWA는 정확한 정보 전달을 위해서(ㅜㅜ) 공부 이후 글을 써보도록 하겠습니다.

'React > Rest API' 카테고리의 다른 글

REST API #5 - Axios vs Fetch ?  (0) 2021.02.27
REST API #4 - Axios  (2) 2021.02.08
REST API #2 - 개념 정리 (2)  (0) 2021.01.12
REST API #1 - 개념 정리 (1)  (0) 2021.01.10