66일차 (2) - JavaScript (AJAX - XMLHttpRequest, 콜백지옥, promise, fetchAPI GET요청)

Yohan·2024년 5월 27일
0

코딩기록

목록 보기
100/156
post-custom-banner

AJAX

비동기 함수를 지원하는 라이브러리

  • 자바스크립트는 싱글 스레드(single-threaded) 언어
    -> 한 번에 하나의 작업만 처리(동기 처리)할 수 있음을 의미, 그러나 이벤트 루프와 콜백 함수를 사용하여 비동기 처리 가능

1. 동기와 비동기

동기코드 (Synchronous Code)

  • 작업이 순서대로 실행

비동기코드 (Asynchronous Code)

  • 특정 작업이 완료되기를 기다리지 않고 다음 작업으로 넘어갈 수 있음
  • callback, promise, async/await 등의 패턴을 사용하여 비동기 코드를 관리

2. XMLHttpRequest로 네트워크 통신하기

XMLHttpRequest 객체

XMLHttpRequest (XHR) 객체는 웹 브라우저와 서버 간에 데이터를 비동기적으로 교환하기 위해 사용

  • XMLHttpRequest는 XML 뿐만 아니라 JSON, HTML, 일반 텍스트 등 다양한 데이터 형식의 데이터를 처리가능

주의사항

XMLHttpRequest는 오래된 API이기 때문에, 대부분의 현대 웹 애플리케이션은 fetch API나 다른 라이브러리(예: Axios)를 사용하여 HTTP 요청 수행

사용법

1. XMLHttpRequest 객체 생성

const xhr = new XMLHttpRequest();

2. 요청 초기화

xhr.open(method, url, async);

3. 응답 처리

xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 400) {
        console.log(xhr.responseText);
    } else {
        console.error('서버에서 에러 응답:', xhr.statusText);
    }
};

xhr.onerror = function() {
    console.error('요청 중 에러 발생');
};

4. 요청 보내기

xhr.send();

GET 요청

  • 서버의 데이터는 JSON, 화면에 보여주려면 JS로 변경해야함
    • JSON.parse() : JSON -> JS
  • JSON의 타입은 문자열 포맷 데이터이기 때문에 JS의 배열처럼 쓸려면 파싱 필수
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div class="video">
    <iframe width="640" height="360" src="https://www.youtube.com/embed/phuiiNCxRMg" title="aespa 에스파 &#39;Supernova&#39; MV" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  </div>
  
    <button id="load">서버에서 정보 불러오기</button>

    <ul id="content"></ul>

    <script>
      // 버튼 클릭 이벤트
      document.getElementById('load').addEventListener('click', e => {
        // 서버에 비동기 통신 요청을 보내야 함.
        // 게시물 정보를 가져오기
        console.log('click!!');

        const xhr = new XMLHttpRequest();
        // 요청 방식과 요청 URL을 적음
        const url = 'https://jsonplaceholder.typicode.com';
        xhr.open('GET', `${url}/users/1/posts`);
      
        // 요청 전송
        xhr.send();

        // 응답 정보 확인
        xhr.onload = e => {
          // 서버가 응답한 데이터는 JSON이라는 문자열 포맷의 데이터다.
          // JSON은 자바스크립트가 아니다.
          // JSON -> JS로 변환해야 함.

          // JSON.parse() : JSON -> JS
          // JSON.stringify() : JS -> JSON
          const response = xhr.response;
          const boardList = JSON.parse(response);
          console.log(boardList[0]);

          // 화면에 렌더링
          const $ul = document.getElementById('content');
          let liTag = '';

          boardList.forEach(b => {
            liTag += `
              <li>
                #글번호: ${b.id}, 제목: ${b.title}, 작성자id: ${b.userId}
              </li>
            `;
          });

          $ul.innerHTML += liTag;

        };
      });
    </script>

</body>
</html>

POST 요청

  • post요청은 서버에 보낼 데이터(payload)를 추가
  • 요청 헤더에 payload의 mime type을 명시
  • 읽은데이터는 JS, 서버에 보낼 때는 JSON으로 변환
    • JSON.stringify() : JS -> JSON
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .video {
      width: 300px;
    }
    .video iframe {
      width: 100%;
    }
  </style>
</head>
<body>

  <div class="video">
    <iframe src="https://www.youtube.com/embed/phuiiNCxRMg" title="aespa 에스파 &#39;Supernova&#39; MV" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  </div>
  
  <div>
    글제목: <input type="text" id="title">
  </div>
  <div>
    글내용: <textarea id="content"></textarea>
  </div>
  <div>
    <button id="add">등록</button>
  </div>

  <script>
    document.getElementById('add').addEventListener('click', e => {
      // 서버에 post 요청 보내는 방법
      const xhr = new XMLHttpRequest();
      const url = 'https://jsonplaceholder.typicode.com';
      
      // 요청정보 설정
      xhr.open('POST', `${url}/posts`);

      // 요청 보내기 : post요청은 서버에 보낼 데이터(payload)를 추가해야 함
      const payload = {
        title: document.getElementById('title').value,
        body: document.getElementById('content').value,
        userId: 1
      };

      // 요청 헤더에 payload의 mime type을 명시해야 함.
      xhr.setRequestHeader('content-type', 'application/json');

      // 읽은데이터는 순수자바스크립트이므로 서버에 보낼 때는
      // JSON으로 변환하여 보내야 한다.
      xhr.send(JSON.stringify(payload));

      // 응답 확인
      xhr.onload = e => {
        console.log(xhr);
      };
    });
  </script>

</body>
</html>

3. 콜백지옥

여러 비동기 연산을 순서대로 처리해야 할 때 발생하는 현상

  • 비동기에서 순서를 정하기 위해선 콜백지옥을 작성할 수 밖에 없는 한계
    • 아래와 같은 > 형태를 띄게됨
get(`${url}/users`, (response) => {
       const userId = response[1].id;
            get(`${url}/users/${userId}/posts`, (response) => {
                const postId = response[2].id;
                get(`${url}/posts/${postId}/comments`, (response) => {
                    console.log(response)
                }); // 해당 글의 댓글 목록 요청
            }); // 해당 회원이 쓴 글 목록 요청
        }); // 회원 정보 조회

Promise

  • 비동기 호출의 순서를 보장하기 위해 callback대신에 promise사용
  • promise는 js의 라이브러리
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
  <button id="show">Promise요청!</button>

  <script>

    // HTTP 비동기 요청을 위한 함수
    // 비동기 호출의 순서를 보장하기 위해 callback대신 promise사용
    function get(url) {

      // promise는 자바스크립트 비동기 통신(ajax)의 순서를 보장하고
      // 데이터 처리를 용이하게 하기 위한 api입니다.

      // resolve: 요청에 성공했을 때 실행할 함수
      // reject: 요청에 실패했을 때 실행할 함수
      const promise = new Promise((resolve, reject) => {

        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.send();

        xhr.onload = e => {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.response);
            resolve(response);
          } else {
            reject(`error!!`);
          }
        };
      });
      return promise;
      
    }

    document.getElementById('show').addEventListener('click', e => {
      // 버튼을 클릭하면 1. 두번째 회원을 조회하여
      // 해당 회원이 쓴 2. 글의 목록을 조회한 후
      // 해당 글의 3. 댓글들을 조회한다.
      const url = 'https://jsonplaceholder.typicode.com';

      // promiseState
      // 1. pending: 요청대기중
      // 2. fulfilled: 요청 성공  ->  resolve() 함수 실행
      // 3. rejected: 요청 실패   ->  reject() 함수 실행

      get(`${url}/users`)
        .then(res => get(`${url}/users/${res[1].id}/posts`))
        .then(res => get(`${url}/posts/${res[2].id}/comments`))
        .then(res => console.log(res))
    });
  </script>


</body>
</html>

4. fetch API (중요)

네트워크 요청을 수행하기 위한 인터페이스를 제공
XMLHttpRequest에 비해 더 유연하고 강력하며, Promise 기반의 구조로 설계

사용 예시

javascript

  • fetch(URL)을 통해 GET요청
  • ul 태그에 동적으로 렌더링될 태그를 템플릿화한 것을 집어 넣도록함
    • forEach문을 통해 템플릿 안의 h2, p의 textContent를 채움
	
// 서버 URL
const URL = 'https://jsonplaceholder.typicode.com/posts';
const URL2 = 'http://localhost:8383/api/v1/replies/30';

// ul 태그 가져오기
const $postUl = document.querySelector('.posts');

// 화면에 게시물을 렌더링하는 함수
const renderPosts = postList => {
  
  postList.forEach(post => {
    // 게시물 하나를 li태그로 만들어서 ul에 집어넣기
    // 템플릿 태그 가져오기
    const $template = document.getElementById('single-post');
    // 템플릿 태그에서 li태그 추출
    // true : 아래있는 것 모두 가져옴, false : li태그만 가져옴
    const $li = document.importNode($template.content, true);
    // console.log($li);
    $li.querySelector(('h2')).textContent = post.title;
    $li.querySelector(('p')).textContent = post.body;
    
    $postUl.appendChild($li);
  })
}

// 서버에서 게시물 정보 받아오기
fetch(URL)
  .then(res => res.json())
  .then(json => {
    console.log(json);
    // 게시물 정보 화면에 그리기
    renderPosts(json);

  });

html

fetch API 실습

javascript

  • fetch의 renderMovied 함수에서 파라미터를 json.data.movies 인 것에 주의
// 서버 URL
const URL = "https://yts.mx/api/v2/list_movies.json";

// 정렬 URL
const sortYear =
  "https://yts.mx/api/v2/list_movies.json?sort_by=year&order_by=desc";
const sortLike =
  "https://yts.mx/api/v2/list_movies.json?sort_by=like_count&order_by=desc";
const sortDownload =
  "https://yts.mx/api/v2/list_movies.json?sort_by=download_count&order_by=desc";

// 태그 가져오기
const $movieList = document.querySelector(".movie-list");

// 초기 영화정보 로딩하는 함수
const renderMovies = (movieList) => {
  $movieList.innerHTML = "";
  movieList.forEach((movie) => {
    const $template = document.getElementById("movie-post");
    // 템플릿 태그에서 movie 클래스 태그 추출
    const $movie = document.importNode($template.content, true);

    $movie.querySelector(".img-box > img").src = movie.large_cover_image;
    $movie.querySelector(".inner .title").textContent = movie.title;
    $movie.querySelector(".inner .year").textContent = movie.year;
    $movie.querySelector(".inner .rating").textContent = movie.rating;
    $movieList.appendChild($movie);
  });
};

// 서버에서 게시물 정보 받아오기
fetch(URL)
  .then((res) => res.json())
  .then((json) => {
    console.log(json);
    // 게시물 정보 화면에 그리기
    renderMovies(json.data.movies);
  });

document.getElementById("s_rate").addEventListener("click", (e) => {
  fetch(sortDownload)
    .then((res) => res.json())
    .then((json) => {
      console.log(json.data.movies);
      renderMovies(json.data.movies);
    });
});

document.getElementById("s_year").addEventListener("click", (e) => {
  fetch(sortYear)
    .then((res) => res.json())
    .then((json) => {
      console.log(json.data.movies);
      renderMovies(json.data.movies);
    });
});

document.getElementById("s_like").addEventListener("click", (e) => {
  fetch(sortLike)
    .then((res) => res.json())
    .then((json) => {
      console.log(json.data.movies);
      renderMovies(json.data.movies);
    });
});

html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <script src="practice.js" defer></script>

    <style>
        * {
            margin: 0;
            padding: 0;
        }
        ul {
            list-style: none;
        }
        a {
            color: inherit;
            text-decoration: none;
        }
        .movie-list {
            width: 80%;
            margin: 0 auto;
        }
        .movie-list .movie {
            float: left;
            width: 23%;
            height: 500px;
            margin-right: 2%;
            margin-bottom: 10px;
            border: 1px solid gray;
            box-sizing: border-box;
            padding: 10px;
        }
        .movie-list .movie .img-box {
            width: 100%;
            height: 70%;
            overflow: hidden;
        }
        .movie-list .movie .img-box img {
            width: 100%;
        }
        .movie-list .movie .inner {
            padding: 25px 15px;
        }
        .movie-list .movie .inner * {
            font-size: 1.3em;
            font-weight: 700;
            margin-bottom: 5px;
        }

        .clearfix::after {
            content: '';
            display: block;
            clear: both;
        }

        header {
            width: 100%;
            padding: 10px 120px;
            box-sizing: border-box;
            border-bottom: 2px solid gray;
            margin-bottom: 20px;
            display: flex;
            justify-content: space-between;
        }
        header h1 {
            flex: 1;
        }
        header .gnb {
            flex: 1;
            margin-top: 10px;
        }
        header .gnb ul {
            display: flex;
            justify-content: space-evenly;
            align-items: center;
        }

    </style>

</head>
<body>

    <header>
        <h1>WhatSsa!!</h1>
        <nav class="gnb">
            <ul>
                <li><a id="s_rate" href="#">다운로드순</a></li>
                <li><a id="s_year" href="#">발매연도순</a></li>
                <li><a id="s_like" href="#">좋아요순</a></li>
            </ul>
        </nav>
    </header>

    <!-- 동적으로 렌더링될 태그를 템플릿화 -->
    <template id="movie-post">
        <div class="movie">
            <div class="img-box">
                <img src="" alt="표지사진">
            </div>
            <div class="inner">
                <div class="title"></div>
                <div class="year"></div>
                <div class="rating"></div>
            </div>
        </div>
    </template>

    <div class="movie-list clearfix">
        <!-- <div class="movie">
            <div class="img-box">
                <img src="https://yts.mx/assets/images/movies/doctor_who_the_day_of_the_doctor_2013/large-cover.jpg" alt="표지사진">
            </div>
            <div class="inner">
                <div class="title">제목</div>
                <div class="year">개봉년도</div>
                <div class="rating">평점</div>
            </div>
        </div> -->
    </div>

    
    <!-- Modal -->
    <div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
        <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLabel">아바타</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                
                <img class="desc-image" src="" alt="" style="width:200px;float: left; margin-right: 20px;">
                <p class="movie-description"></p>

            </div>
            <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
            </div>
        </div>
        </div>
    </div>
    

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>

    </body>
    </html>
  • 기본 화면
  • 다운로드순
  • 연도순
  • 좋아요순
profile
백엔드 개발자
post-custom-banner

0개의 댓글