현대적인 네트워크 통신 방법 : fetch API

    반응형

    fetch API

     

    과거의 웹페이지는 html 파일 전체를 서버로부터 전송받아 화면을 그리는 방식으로 구동되었다. 그래서 아주 작은 변화에서도 전체 html 파일을 다시 받아와서 그려야 했고, 데이터가 변할 때마다 html 파일 전체를 새로 받아와 그리기 때문에 화면 전환 시 화면 전체가 보이지 않는 깜빡이는 현상이 발생하기도 했으며, 클라이언트와 서버가 동기적으로 통신했기 때문에 서버에 응답이 없다면 블로킹(다른 작업이 진행되는 동안 자신의 작업을 처리하지 않는) 현상 즉 서버에서 응답이 올 때까지 기다려야 하는 문제들이 발생했다.

     

    이러한 문제들은 Ajax라는 개념이 등장하면서 확연히 차이가 나게 바뀌게 되었다. 기존의 문제들을 해결했을 뿐만 아니라 부드러운 화면전환이 가능해졌다. 기존에는 자바스크립트를 사용해서 네트워크 통신을 하려면 Web API인 XMLHttpRequest 객체를 사용해서 데이터를 받아 사용했다. 하지만 현대적인 방법으로 fetch API, axios가 등장했고 더 사용하기 쉬워지면서 현대에는 fetch API, axios를 더 많이 사용한다. 오늘은 그중에서 fetch API에 대해 공부하고 알아보려고 한다.

     

    fetch API


    fetch API는 Web API의 하나로 브라우저에서 동작한다. XMLHttpRequest 사용하여 네트워크 통신을 하던 방식보다 현대적이며 강력하고 유연한 대체제이다. 기존의 XMLHttpRequest 이벤트 핸들러와 콜백 함수를 이용해 비동기 작업을 하기 때문에 복잡한 비동기 작업에서는 콜백 지옥에 빠질 수 있는 가능성이 높았지만 fetch API는 promise를 반환하기 때문에 . then( ) .catch( ) , async/await 문법을 사용해 읽기 쉽고 유지 보수가 용이한 코드를 작성할 수 있도록 한다.

    function getUserInfo(userId) {
        return fetch(`/api/users/${userId}`).then(res => res.json());
    }
    
    function getUserPosts(userId) {
        return fetch(`/api/users/${userId}/posts`).then(res => res.json());
    }
    
    function getPostComments(postId) {
        return fetch(`/api/posts/${postId}/comments`).then(res => res.json());
    }
    
    function getCommentAuthor(authorId) {
        return fetch(`/api/users/${authorId}`).then(res => res.json());
    }
    
    
    getUserInfo('user123')
        .then(user => getUserPosts(user.id))
        .then(posts => {
            if (posts.length === 0) throw new Error('게시글이 없습니다.');
            return getPostComments(posts[0].id);
        })
        .then(comments => {
            if (comments.length === 0) throw new Error('댓글이 없습니다.');
            return getCommentAuthor(comments[0].authorId);
        })
        .then(author => {
            console.log('Comment author:', author);
        })
        .catch(err => {
            console.error('Error:', err);
        });

     

    위 코드는 fetch API를 사용해서 동작한다. 사용자의 정보를 가져온 후 사용자의 게시물 목록을 가져온다. 그 이후 그 게시글의 첫 번째 댓글을 가져오고 그 댓글을 작성한 사용자의 프로필을 가져온다. .then( ), .catch( )를 통해 읽기 쉽고 수정과 유지 보수가 용이하다는 걸 한눈에 확인할 수 있다.

     

    하지만 같은 기능을 가진 코드를 XMLHttpRequest 객체를 사용하여 작성하면 아래와 같이 콜백 함수가 늘어나고 기능이나 코드가 늘어나거나 길어질수록 콜백 지옥에 빠지고 가독성과 유지 보수가 힘들어진다.

    function getUserInfo(userId, callback) {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', `/api/users/${userId}`);
        xhr.send();
        xhr.onload = function() {
            if (xhr.status === 200) {
                callback(null, JSON.parse(xhr.responseText));
            } else {
                callback(new Error('User info request failed'));
            }
        };
    }
    
    function getUserPosts(userId, callback) {
        // 유사한 XMLHttpRequest 코드...
    }
    
    function getPostComments(postId, callback) {
        // 유사한 XMLHttpRequest 코드...
    }
    
    function getCommentAuthor(authorId, callback) {
        // 유사한 XMLHttpRequest 코드...
    }
    
    // 사용 예
    getUserInfo('user123', function(err, user) {
        if (err) {
            console.error(err);
            return;
        }
        getUserPosts(user.id, function(err, posts) {
            if (err) {
                console.error(err);
                return;
            }
            if (posts.length === 0) {
                console.log('게시글이 없습니다.');
                return;
            }
            getPostComments(posts[0].id, function(err, comments) {
                if (err) {
                    console.error(err);
                    return;
                }
                if (comments.length === 0) {
                    console.log('댓글이 없습니다.');
                    return;
                }
                getCommentAuthor(comments[0].authorId, function(err, author) {
                    if (err) {
                        console.error(err);
                        return;
                    }
                    console.log('Comment author:', author);
                });
            });
        });
    });

     

     

     

    fetch 사용법


    fetch(url, options)
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));

     

    fetch( )는 2개의 매개변수를 받을 수 있다. 첫 번째 매개변수는 필수로 입력받아야 하는 리소스 경로이다. 쉽게 말하면 API url 경로를 입력받는데 options의 값으로 아무것도 입력되지 않는다면 기본 요청인 'GET'이 요청된다.

     

    async function fetchData() {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('통신 오류');
        }
        const data = await response.json();
        console.log(data);
      } catch (error) {
        console.error('Error:', error);
      }
    }

     

    promise 체인이 아닌 async/await를 사용한 fetch( ) 사용 예시 async/await를 사용하면 비동기적으로 동작하는 fetch( )를 동기적으로 동작하는 것처럼 보일 수 있다

     

    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((response) => console.log(response))
      .catch((error) => console.log(error));

     

    위 코드를 동작해 실행해서 console 창을 확인하면 Promise 객체의 상태와. then( )이 실행된 후 Response 객체를 확인할 수 있다. Promise 객체는 나중에 알아보는 것으로 하고 Response 객체만 살펴보려고 한다. 성공적으로 통신을 완료했기에 Response 객체가 생성되었다고 생각할 수 있지만 fetch( )는 서버에서 오류로 응답을 보내도 Response 객체를 반환한다.

     

    fetch( )의 response.ok 상태 차이

     

    두 번째 매개변수로는 options 값을 받는다. options는 객체 형태로 다양한 속성값들을 가지고 있지만 이 글에서는 자주 사용되는 몇 가지들만 정리하고 알아보려고 한다.

     

    1. method

    HTTP 요청의 메서드를 지정하는 속성으로 기본값은 'GET' 이다. 외에도 서버에 데이터를 전송하는 'POST', 특정 리소스를 업데이트할 때 사용하는 'PUT', 특정 리소스를 삭제하는 'DELETE' 가 있다. 특정 리소스를 삭제하는 'DELETE' 같은 경우에는 다른 속성들은 입력하지 않는 경우가 많다.

    fetch(url, {
      method: "POST",
    });
    
    fetch(url, {
      method: "POST",
    });
    
    fetch(url, {
      method: "PUT",
    });
    
    fetch(url, {
      method: "DELETE",
    });

     

    2.headers

    요청에 포함되는 헤더를 설정하는 속성으로 객체의 형태이다. key-value 형태로 작성해야 한다. "Content-Type" : "application/json"은 body 속성 즉 본문을 통해 전달되는 데이터가 JSON 형식이라는 것을 서버에 알리는 역할이다.

    fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
    });
    
    fetch(url, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
    });

     

    3.body

    요청의 본문 즉 콘텐츠로 POST, PUT 요청 시에 주로 사용된다. 문자열의 형태로 전달해야 한다. 혹은 JSON 데이터를 보내는 경우에는 JSON.stringigy( )를 사용해 JSON 형식으로 변환해 주어야 한다.

    fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: "hoo",
        body: "min",
        userId: 1,
      }),
    });
    
    fetch(url, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: "hoo",
        body: "min",
        userId: 1,
      }),
    });

     

    4.mode

    요청의 CORS 정책을 설정한다. mode의 기본값은 cors이다. CORS는 다시 정리할 예정이지만 쉽게 말해 보안정책으로 다른 출처의 리소스에 대한 요청을 제한한다. 여기서 말하는 다른 출처란 쉽게 말해 URL이 다른 것이라고 보면 이해하기 편하다. 프로토콜, 도메인, 포트 번호 중 하나라도 다르면 다른 출처라고 간주하는데 이러한 출처들이 서로 다르다면 접근을 제한하는 것이다. 하지만 서버에서 일부 접근을 허용할 수 있는데 이러한 접근을 할 때 어떤 접근인지 확인하는 속성이라고 볼 수 있다.

     

    fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      mode: "cors",
      body: JSON.stringify({
        title: "hoo",
        body: "min",
        userId: 1,
      }),
    });
    
    fetch(url, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      mode: "no-cors",
      body: JSON.stringify({
        title: "hoo",
        body: "min",
        userId: 1,
      }),
    });
    
    fetch(url, {
      method: "DELETE",
      mode: 'same-origin'
    });

     

    cors는 다른 출처에서 리소스를 요청할 때 사용한다. no-cors는 CORS 정책을 무시하고 요청한다. same-origin은 동일한 출처에서만 요청을 허용하는 것으로 보안을 강화할 때 주로 사용할 수 있다. 

     

    이 외에도 다양한 options 값들이 있다. 추가로 확인 가능하니 꼭 한 번씩 읽어보자.

    반응형

    댓글