본문 바로가기

Frontend/JavaScript

[바닐라 자바스크립트로 SPA 만들기] 2. 비동기 처리

반응형

지난 글에서는 카운터 컴포넌트를 통해 기본적인 SPA의 구조를 잡았다. 이번에는 조금 더 나아가서, API 통신을 위한 비동기 처리 컴포넌트를 만들어 보자.

 

비동기 컴포넌트 만들기

이 부분의 구조는 "프레임워크 없는 프론트엔드 개발" 도서의 HTTP요청 부분을 참고했으며, 더미 API를 통해 패치 컴포넌트를 만들어 볼 것이다.

 

폴더 구조

 

새롭게 추가된 부분은 다음과 같다.

 

- api/http.js: HTTP 통신 메서드 모음

- components/List.js: API 결과를 출력할 리스트 컴포넌트

- pages/FetchPage.js: API 패치를 위한 페이지 컴포넌트

 

 

 

 

 

 

 

 

 

src/api/http.js

  • Fetch API를 기반으로 HTTP 통신 메서드를 추상화한 파일이다. (참고 링크)
  • 최대한 외부 라이브러리를 활용하지 않는 버전으로 만들기 위해 Fetch를 사용했지만, axios로 구현할 수도 있다.
const parseResponse = async (response) => {
  const { status } = response;
  let data;
  if (status !== 204) {
    data = await response.json();
  }

  return {
    status,
    data,
  };
};

const request = async (params) => {
  const { method = 'GET', url, headers = {}, body } = params;

  const config = {
    method,
    headers: new window.Headers(headers),
  };

  if (body) {
    config.body = JSON.stringify(body);
  }

  const response = await window.fetch(url, config);

  return parseResponse(response);
};

const get = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'GET',
  });

  return response.data;
};

const post = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'POST',
    body,
  });
  return response.data;
};

const put = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PUT',
    body,
  });
  return response.data;
};

const patch = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PATCH',
    body,
  });
  return response.data;
};

const deleteRequest = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'DELETE',
  });
  return response.data;
};

export default {
  get,
  post,
  put,
  patch,
  delete: deleteRequest,
};

 

src/components/List.js

  • API 응답으로 받은 결과값을 리스트로 출력하기 위해 생성한 컴포넌트이다.
  • List 컴포넌트를 호출할 때 결과를 넘기면, 이 컴포넌트에서 props로 받아 활용할 수 있다.
  • 참고로 map을 통해 리스트를 출력할 때, join('')으로 묶어주어야 배열의 쉼표를 없애고 출력할 수 있다.
import Component from '../core/Component.js';

export default class List extends Component {
  template() {
    const { dummyList } = this.$props;
    return `
      <ul>
        ${dummyList
          .map(({ id, title }) => `<li key=${id}>${title}</li>`)
          .join('')}
      </ul>
    `;
  }
}

 

src/pages/FetchPage.js

jsonplaceholder포스트 API를 활용했다.

 

 

1. 상태 및 템플릿 설정

  • 응답 결과를 저장할 dummyList를 생성한다.
  • List 컴포넌트를 호출할 태그를 생성한다.
  setup() {
    this.$state = {
      dummyList: [],
    };
  }

  template() {
    return `
      <h1>Fetch Page</h1>
      <div data-component="fetch-api"></div>
      `;
  }

 

2. API 호출

  • 컴포넌트가 마운트 되었을 때 API를 호출해야 한다.
  • mounted 메서드에서 API호출 함수를 생성하고, dummyList에 값을 넣는다.
  mounted() {
    const fetchDummy = async () => {
      const dummyPosts = await http.get(
        `https://jsonplaceholder.typicode.com/posts`
      );
      this.setState({ dummyList: [...dummyPosts] });
    };

    fetchDummy();
  }

 

3. 리스트 출력

  • 리스트 컴포넌트에 dummyList를 넘겨야 한다.
  • fetchDummy 호출 -> 리스트 컴포넌트 호출 순으로 동작해야 한다는 것이다.
  • 그리고 dummyList에 값이 없을 때만(초기에만) fetchDummy를 호출해야 한다.

 

근데 만약 이렇게 작성할 경우 fetchDummy가 계속 호출되기 때문에 무한 루프에 빠지게 된다. 

  mounted() {
    const fetchDummy = async () => {
      const dummyPosts = await http.get(
        `https://jsonplaceholder.typicode.com/posts`
      );
      this.setState({ dummyList: [...dummyPosts] });
    };

    fetchDummy();

    const $fetchApi = this.$target.querySelector(
      '[data-component="fetch-api"]'
    );
    new List($fetchApi, this.$state);
  }
console 무한 출력

 

따라서 dummyList의 값을 확인하는 분기를 통해 API 요청과 리스트 컴포넌트 호출 부분을 나누어야 한다.

  mounted() {
    const fetchDummy = async () => {
      const dummyPosts = await http.get(
        `https://jsonplaceholder.typicode.com/posts`
      );
      this.setState({ dummyList: [...dummyPosts] });
    };

    if (this.$state.dummyList.length === 0) {
      fetchDummy();
    } else {
      const $fetchApi = this.$target.querySelector(
        '[data-component="fetch-api"]'
      );
      new List($fetchApi, this.$state);
    }
  }

 

최종 코드

import Component from '../core/Component.js';
import List from '../components/List.js';
import http from '../api/http.js';

export default class FetchPage extends Component {
  setup() {
    this.$state = {
      dummyList: [],
    };
  }

  template() {
    return `
      <h1>Fetch Page</h1>
      <div data-component="fetch-api"></div>
      `;
  }

  mounted() {
    const fetchDummy = async () => {
      const dummyPosts = await http.get(
        `https://jsonplaceholder.typicode.com/posts`
      );
      this.setState({ dummyList: [...dummyPosts] });
    };

    if (this.$state.dummyList.length === 0) {
      fetchDummy();
    } else {
      const $fetchApi = this.$target.querySelector(
        '[data-component="fetch-api"]'
      );
      new List($fetchApi, this.$state);
    }
  }
}

 

렌더링 결과

 

 

Reference

 

 

반응형