Tour API를 연동하여 추천 코스 필터/검색하기

Dave·2023년 8월 20일
0
post-thumbnail

해당 기능이 있는 페이지 : 추천 코스

1. Tour API란?

한국관광공사에서 정부에서 수집한 여행 관련 정보를 제공하는 API이다. 관광지, 문화시설, 축제공연행사, 여행 코스 등 다양한 정보를 제공한다.

1-1. Tour API 세팅하기

공공 데이터 신청 절차

해당 웹 사이트에 들어간 후 로그인을 진행한다. 기존 가입한 네이버, 카카오톡, 구글 계정과 연동 가능하다.

이후 웹 사이트에서 [ 관광데이터 Hub ] - [ OpenAPI 서비스 목록 / OpenAPI 가이드 ] 를 들어가준다.

갑자기 많은 선택지들이 나와 당황스럽겠지만 국문관광정보 서비스를 클릭해준다. 앵간한 데이터는 전부 [ 국문 관광정보 서비스 ]에 들어있다.

그다음 해당 서비스에 있는 [ 공공데이터 포털활용신척 ] 버튼을 눌러준다.

신청이 완료되면 아래와 같이 승인 창구가 뜬다.

이후 [승인] 한국관광공사_ 국문 관광정보 서비스_GW를 클릭하면 아래와 같이 내가 신청한 내용이 승인됐는지와 API 요청을 위한 엔드 포인트, 그리고 API key에 대한 데이터를 얻을 수 있다.

인증키는 Encoding 방식을 사용했다.

이후 다시 메인페이지로 들어와 OpenAPI 활용방법으로 들어가면 어떤 URL로 데이터를 요청할지 정보를 얻을 수 있다.

1-2. 데이터 요청 URL

기본적으로 데이터 요청 URL은 아래 루트 URL로 시작된다. 이후 KorService1 다음에 슬래시 / 를 넣고 어떤 옵션을 넣냐에 따라서 지역 정보, 행사 정보 키워드 검색 등등 다양한 데이터에 대한 요청을 할 수 있게된다.

KorServie1/ 뒤에 기입할 옵션들은 아래와 같다.

예를 들어 내가 키워드 검색 기능을 사용하고 싶으면 아래와 같이 입력해주면 된다.

http://apis.data.go.kr/B551011/KorService1/searchKeyword1

그 다음 키워드 검색 기준으로 세부적인 쿼리 파라미터를 전달해줘야 내가 원하는 값을 얻을 수 있다.

어떤 파라미터를 전달해야 하는 지는 아래 요청 메시지 섹션에서 확인할 수 있다.

예를 들어 다음과 같은 요청을 가진 URL을 작성하면 다음과 같다.

  • serviceKey

내가 발급받은 키 값

  • numOfrows

한 페이지 당 결과 수

  • pageNo

현재 열람할 페이지

  • MobileApp = AppTest

(무슨 의미인지는 모르겠으나 넣어줘야 데이터를 불러올 수 있다.)

  • arrange

정렬 방법

  • contentTypeId

불러올 데이터의 타입
ex) 관광지, 숙박 등

  • _type

어떤 타입 (XML or json)으로 데이터를 받아올 지, 자바스크립트를 이용하여 json을 객체 형태로 변환할 수 있기 때문에 json 으로 받아준다.

이에 따라 데이터 요청을 하는 URL은 다음과 같다.

http://apis.data.go.kr/B551011/KorService1/detailInfo1?serviceKey=(내가 발급받은 키)&pageNo=1&numOfRows=10&MobileApp=AppTest&MobileOS=ETC&contentId=2987549&contentTypeId=25&_type=json

데이터 종류에 따라 아래와 같은 파라미터를 통해 상세한 검색을 진행할 수 있다.

  • areaCode

지역 코드 (서울, 경기 등)

  • sigunguCode

지역 코드 내부 상세 지역 코드 (서울시 관악구, 강남구 등)

  • cat1, cat2, cat3

대분류, 중분류, 소분류 코드

  • keyword

검색한 키워드

또한 단순한 get 요청이니까 브라우저 URL 주소 창에 해당 URL들을 테스트 해 나가며 데이터가 제대로 들어오는 지 확인할 수 있다.

2. 필터 기능 구현하기

일단 필터 기능을 구현하기 위해서는 해당 지역들의 areaCode를 알고 있어야 한다. 그래야 그 값을 URL 파라미터에 넣어 검색이 가능하다.

아니 근데 원래 이 정도는 Tour API에서 제공을 해줘야 하는데 문서가 없어서 일일이 다 찾아야 했다.. 아래 HTML 코드를 보고 본인이 원하는 areaCode를 찾아가길 바란다.

2-1. HTML 코드

지역에 대해 컨텐츠를 필터링하기 위한 HTML 태그는 아래와 같다.

<select id="regionFilter" class="course_regionFilterSelect">
    <option value="">전체</option>
    <option value="1">서울</option>
    <option value="6">부산</option>
    <option value="4">대구</option>
    <option value="2">인천</option>
    <option value="5">광주</option>
    <option value="3">대전</option>
    <option value="7">울산</option>
    <option value="8">세종</option>
    <option value="31">경기</option>
    <option value="32">강원</option>
    <option value="33">충북</option>
    <option value="34">충남</option>
    <option value="35">경북</option>
    <option value="36">경남</option>
    <option value="37">전북</option>
    <option value="38">전남</option>
    <option value="39">제주</option>
</select>

코스 종류를 필터링하기 위한 코드는 아래와 같다.

<select id="hashtagFilter" class="course_hashtagFilterSelect">
    <option value="">전체</option>
    <option value="C0112">#가족코스</option>
    <option value="C0113">#나홀로코스</option>
    <option value="C0114">#힐링코스</option>
    <option value="C0115">#도보코스</option>
    <option value="C0116">#캠핑코스</option>
    <option value="C0117">#맛코스</option>
</select>

2-2. 자바스크립트 코드

각 option 태그 내에 있는 value 값을 이벤트가 발생함에 따라 미리 선언한 변수에 할당하고 검색 이벤트가 발생했을 때 해당 변수를 URL 파라미터 값에 할당하여 요청한다.

1) 변수 선언

먼저 현재 유저가 선택한 지역 필터 값을 담을 currentRegion 변수와, 카테고리 필터 값을 담을 currentHashtag 변수를 선언한다.

// 지역 선택값을 저장하는 변수
let currentRegion='';
// 해시태그 선택값을 저장하는 변수
let currentHashtag='';
// 컨텐츠가 로드되는 영역
const courseContent = document.querySelector('.course_content_wrap');

그 다음 유저가 선택한 값을 받아오기 위해 참조할 select 태그들을 getElementById를 통해 가져온다.

// 폼 태그를 지정
const filterForm = document.getElementById('filterForm');
// 폼 태그 안 지역 필터를 지정
const regionFilter = document.getElementById('regionFilter');
// 폼 태그 안 해시태그 필터를 지정
const hashtagFilter = document.getElementById('hashtagFilter');
// 검색 버튼
const filterButton = document.querySelector('.course_formButton');

2) 제출 이벤트 구현

기존에는 필터링 하는 영역과 키워드를 검색하는 영역을 분리해서 요소를 구성했어야 했는데 그 생각을 하지 않아 필터 기능과 검색기능을 다같이 묶어서 만들었다.

filterButton.addEventListener('click', ()=>{
    currentPage=1;
    currentRegion=regionFilter.value;
    currentHashtag=hashtagFilter.value;
    encodeUTF8();
    courseRenderHTML();
})

언급하지는 않았지만 페이지 네이션도 같이 구현했기 때문에 검색 이벤트가 발생하는 코드에는 다른 코드들이 추가로 들어가있다. 언급하지 않은 코드들은 다음 게시물에 페이지네이션 기능을 언급할 때 재차 언급한다.

일단 기존에 선언한 currentRegion 변수 값에 지역 코드 값을 갖고 있는 select 태그에서 선택된 값을 할당하고

마찬가지로 currentHashtag 변수 값에 카테고리 코드 값을 갖고 있는 select 태그에서 선택된 값을 할당한다.

currentRegion=regionFilter.value;
currentHashtag=hashtagFilter.value;

이후 courseRenderHTML 함수를 실행하여 이 함수 내부에 있는 데이터를 불러오는 메서드를 통해 데이터를 가져온다.

3) 지역 코드, 키워드 코드를 통해 데이터를 불러오는 함수

async function fetchDataByCategory(){
    const response = await fetch(`https://apis.data.go.kr/B551011/KorService1/areaBasedList1?serviceKey=(내가 제공받은 키)&pageNo=${currentPage}&numOfRows=10&MobileApp=AppTest&MobileOS=ETC&arrange=Q&areaCode=${currentRegion}&contentTypeId=25&cat1=C01&cat2=${currentHashtag}&_type=json`);
    const jsonData = await response.json();
    return jsonData;
}

비동기 식으로 데이터를 불러오려면 내가 아는 선에서는 XMLHttpRequest 방식, fetch 방식이 있는데 fetch 방식을 사용하면 코드가 간결해지므로 채택했다.

또한 반환하는 프로미스를 처리하기 위해 then을 사용하기 보다는, 동기식으로 데이터 처리를 하기 위해 async await 키워드를 사용했다. 내가 아는 async await 키워드를 사용한 비동기 데이터 처리는 try, catch문을 통해 에러 핸들링을 할 수 있다는 건데, 이 부분에서는 지식이 없어 미흡했다.

결론은 동기식으로 데이터를 받아오기 위해 사용했다.

URL 내부를 보면 areaCode 파라미터, 중분류(cat2) 파라미터에 각각 currentRegion, currentHashtag가 각각 할당된 것을 확인할 수 있다.

4) 받아온 코스 데이터들을 HTML 태그로 바꿔 입력해주는 함수

async function insertHTML(jsonDataArray){
    const newUl = document.createElement('ul');
    newUl.className='course_contentListChange_box'
    for (const data of jsonDataArray){
        const courseRenderImageUrl = data.firstimage ? data.firstimage : 'https://play-lh.googleusercontent.com/6lRtpyZB4xCFnj1WUUoZC12I_88BoTn48LIjUe0FvHdP4zD0oYc5JpAKMNB5-jTAxKY=w600-h300-pc0xffffff-pd';
        const courseId = data.contentid;
        let courseRenderAddr;
        let courseRenderHashtag;
        let courseRenderOverview;

        if(data.addr1){
            courseRenderAddr=data.addr1;
        } else if(data.addr2){
            courseRenderAddr=data.addr2;
        } else{
            courseRenderAddr='전국';
        }

        if(data.cat2){
            switch (data.cat2){
                case 'C0112':
                    courseRenderHashtag='#가족코스';
                    break;
                
                case 'C0113':
                    courseRenderHashtag='#나홀로코스';
                    break;
                
                case 'C0114':
                    courseRenderHashtag='#힐링코스';
                    break;

                case 'C0115':
                    courseRenderHashtag='#도보코스';
                    break;

                case 'C0116':
                    courseRenderHashtag='#캠핑코스';
                    break;

                case 'C0117':
                    courseRenderHashtag='#맛코스';
                    break;
            }
        } else{
            courseRenderHashtag='#추천코스'
        }

        const detailedData = await fetchDataMoreInfo(courseId);
        courseRenderOverview=detailedData.response.body.items.item[0].overview;

        newUl.insertAdjacentHTML('beforeend', `<li class="course_contentListItem">
            <div class="course_contentListItemImg_box">
                <a class="course_contentListItemLink" href="../course/courseContent.html">
                    <img class="course_contentListItemLinkImg" src=${courseRenderImageUrl} alt="">
                </a>
            </div>
            <div class="course_contentListItemText_box">
                <a class="course_contentListItemTextLink" href="../course/courseContent.html">
                    <h4 class="course_contentListItemTextLinkTitle">${data.title}</h4>
                    <p class="course_contentListItemTextLinkText">${courseRenderOverview}</p>
                </a>
                <ul class="course_contentListItemTextList">
                    <li class="course_contentListItemTextListRegion">${courseRenderAddr}</li>
                    <li class="course_contentListItemTextListHashtag">${courseRenderHashtag}</li>
                </ul>
            </div>
        </li>`)
    }
    courseContent.insertAdjacentElement('afterbegin', newUl);
}

우선, 객체 데이터를 담은 배열 형태로 반환되는 데이터를 담기 위한 ul 태그를 생성하고 ui 태그의 레이아웃을 담당하는 CSS를 클래스 선택자를 통하여 할당해준다.

const newUl = document.createElement('ul');
newUl.className='course_contentListChange_box'

이후 for ... of 순회문을 사용해서 json 형식으로 받아온 여행 코스 데이터 jsonDataArray(매개변수 명)를 각각 하나의 여행 코스 컨텐츠 하나로 만들어준다.

for (const data of jsonDataArray){
    const courseRenderImageUrl = data.firstimage ? data.firstimage : 'https://play-lh.googleusercontent.com/6lRtpyZB4xCFnj1WUUoZC12I_88BoTn48LIjUe0FvHdP4zD0oYc5JpAKMNB5-jTAxKY=w600-h300-pc0xffffff-pd';
    const courseId = data.contentid;
    let courseRenderAddr;
    let courseRenderHashtag;
    let courseRenderOverview;

courseRenderImageUrl 변수의 경우 여행 코스 컨텐츠에 들어갈 섬네일 사진 URL이다. 이 값이 데이터에 따라 존재하거나 하지 않기 때문에 삼항 연산자를 통해 존재하지 않을 경우 대체 이미지를 출력하도록 설정한다.

그 외에 courseRenderAddr (코스에 대한 주소 정보), courseRenderHashtag (코스에 대한 해시태그 정보), courseRenderOverview (코스의 대략적인 개요에 대한 정보)에 대한 변수를 선언한다.

또한 상세 페이지에서 정보를 가져와야 하므로 courseId 변수에서 받아온 데이터의 contentid 값을 받아와 추후에 상세 정보를 요청하는 함수에 인수로 넣어준다.

if(data.addr1){
    courseRenderAddr=data.addr1;
} else if(data.addr2){
    courseRenderAddr=data.addr2;
} else{
    courseRenderAddr='전국';
}

만약 받아온 데이터에서 addr1, addr2 같이 주소 정보 데이터가 없으면 전국으로 표시해준다. 내가 임의로 정한 게 아니라 전국 단위로 돌아가는 코스이면 addr1, addr2에 주소 데이터가 없다.

if(data.cat2){
    switch (data.cat2){
        case 'C0112':
            courseRenderHashtag='#가족코스';
            break;
        case 'C0113':
            courseRenderHashtag='#나홀로코스';
            break;
        case 'C0114':
            courseRenderHashtag='#힐링코스';
            break;
        case 'C0115':
            courseRenderHashtag='#도보코스';
            break;
        case 'C0116':
            courseRenderHashtag='#캠핑코스';
            break;
        case 'C0117':
            courseRenderHashtag='#맛코스';
            break;
    }
} else{
    courseRenderHashtag='#추천코스'
}

이후 중분류 코드인 cat2 프로퍼티에 값에 따라 여행 코스 컨텐츠에 넣을 텍스트를 switch/case 문을 통해 결정한다.

기존 if문 보다 가독성이 좋아서 선택했다.

const detailedData = await fetchDataMoreInfo(courseId);
courseRenderOverview= detailedData.response.body.items.item[0].overview;

기존에 받아온 courseId 값을 통해 상세 정보를 fetchDataMoreInfo 함수를 통하여 받아온다. 기존 소개한 fetchDataByCategory와 형식이 유사하다.

async function fetchDataMoreInfo(courseId){
    const response = await fetch(`https://apis.data.go.kr/B551011/KorService1/detailCommon1?serviceKey=(내가 발급받은 키)&pageNo=1&numOfRows=10&MobileApp=AppTest&MobileOS=ETC&contentId=${courseId}&contentTypeId=25&overviewYN=Y&_type=json`);
    const jsonData = await response.json();
    return jsonData;
}

기존에 생성한 ul 태그인 newUl에 insertAdjacentHTML 메서드를 활용하여 내부 컨텐츠들을 채워준다.

newUl.insertAdjacentHTML('beforeend', `<li class="course_contentListItem">
    <div class="course_contentListItemImg_box">
        <a class="course_contentListItemLink" href="../course/courseContent.html">
            <img class="course_contentListItemLinkImg" src=${courseRenderImageUrl} alt="">
        </a>
    </div>
    <div class="course_contentListItemText_box">
        <a class="course_contentListItemTextLink" href="../course/courseContent.html">
            <h4 class="course_contentListItemTextLinkTitle">${data.title}</h4>
            <p class="course_contentListItemTextLinkText">${courseRenderOverview}</p>
        </a>
        <ul class="course_contentListItemTextList">
            <li class="course_contentListItemTextListRegion">${courseRenderAddr}</li>
            <li class="course_contentListItemTextListHashtag">${courseRenderHashtag}</li>
        </ul>
    </div>
</li>`)

이후 컨텐츠 내용이 들어간 newUl 태그를 insertAdjacentElement를 다시 활용하여 끝부분에 붙여준다.

courseContent.insertAdjacentElement('afterbegin', newUl);

5) 코스 데이터를 렌더링 할 때 로딩 화면 제어, 데이터 요청, 페이지네이션을 함께 수행하는 함수

async function courseRenderHTML(){
    loadingPage.classList.remove('hidden');
    if(currentEncodedData){
        try{
            courseContent.innerHTML='';
            const jsonData = await fetchDataByInput();
            const jsonDataArray = jsonData.response.body.items.item;
            const totalPages = Math.ceil(jsonData.response.body.totalCount / itemsPerPage);
            await insertHTML(jsonDataArray);
            createPageButtons(totalPages);
            loadingPage.classList.add('hidden');
        } catch(error){
            buttonBox.innerHTML='';
            loadingPage.classList.add('hidden');
            window.alert('검색된 데이터가 없습니다.');
        }
    } else{
        try{
            courseContent.innerHTML='';
            const jsonData = await fetchDataByCategory();
            const jsonDataArray = jsonData.response.body.items.item;
            const totalPages = Math.ceil(jsonData.response.body.totalCount / itemsPerPage);
            await insertHTML(jsonDataArray);
            createPageButtons(totalPages);
            loadingPage.classList.add('hidden');
        } catch(error){
            buttonBox.innerHTML='';
            loadingPage.classList.add('hidden');
            window.alert('검색된 데이터가 없습니다.');
        }
    }
}

우선 해당 메서드가 실행되면 로딩 페이지의 visibility: hidden을 주는 CSS를 없애 로딩 화면이 실행되도록 만든다.

loadingPage.classList.remove('hidden');

만약에 사용자가 검색창에 아무거나 입력을 했다면 currentEncodedData 값이 존재하며 이를 통해 사용자가 키워드 검색을 통해 값을 검색하려고 하는지, 카테고리 필터를 통해 데이터를 검색하려고 하는지 판단한다.

if(currentEncodedData){
    }
} else{
}

여기서는 필터 기능을 언급하므로 else 블록 코드로 넘어간다.

else{
    try{
        courseContent.innerHTML='';
        const jsonData = await fetchDataByCategory();
        const jsonDataArray = jsonData.response.body.items.item;
        const totalPages = Math.ceil(jsonData.response.body.totalCount / itemsPerPage);
        await insertHTML(jsonDataArray);
        createPageButtons(totalPages);
        loadingPage.classList.add('hidden');
    } catch(error){
        buttonBox.innerHTML='';
        loadingPage.classList.add('hidden');
        window.alert('검색된 데이터가 없습니다.');
    }
}

일단 코스 영역에 해당하는 courseContent의 innerHTML을 빈 문자열로 하여 내부를 빈 태그로 만들어준다.

courseContent.innerHTML='';

그 다음 데이터를 카테고리에 따라 검색하여 await 키워드를 통해 데이터를 받아올 때까지 기다린다.

const jsonData = await fetchDataByCategory();
const jsonDataArray = jsonData.response.body.items.item;

그리고 받아온 jsonDataArray를 insertHTML 메서드에 인수로 넣어줘서 태그를 생성하고, 컨텐츠 필터가 들어가는 레이아웃 태그 내부에 넣어준다.

await insertHTML(jsonDataArray);

여기까지 성공적으로 일이 진행되면 로딩 화면에 다시 visibility: hidden을 주는 클래스를 할당하여 없앤다.

loadingPage.classList.add('hidden');

3. 키워드 검색 기능 구현하기

필터 기능과 동작은 유사하다.

3-1. HTML 코드

<div class="course_searchFilter_box">
    <label class="course_searchFilterLabel">키워드</label>
    <input id="searchFilter" class="course_searchFilterInput" type="text" placeholder="검색어를 입력하세요">
</div>

3-2. 자바스크립트 코드

input 태그 내에 있는 value 값을 이벤트가 발생함에 따라 미리 선언한 변수에 할당하고, 검색 이벤트가 발생했을 때 해당 변수를 URL 파라미터 안에 할당하여 요청한다.

1) 변수 선언

// 사용자가 검색한 값을 저장하는 변수
let currentEncodedData;
// 폼 태그 안 검색어 필터를 지정
const searchFilter = document.getElementById('searchFilter');
// 컨텐츠가 로드되는 영역
const courseContent = document.querySelector('.course_content_wrap');

2) 제출 이벤트

filterButton.addEventListener('click', ()=>{
    currentPage=1;
    currentRegion=regionFilter.value;
    currentHashtag=hashtagFilter.value;
    encodeUTF8();
    courseRenderHTML();
})

3) 유저가 입력한 문자열 키워드를 UTF-8 문자열로 변환하기

function encodeUTF8(){
    currentEncodedData=encodeURIComponent(searchFilter.value);
}

받은 키워드를 그대로 입력하면 검색이 안 된다. 이 문자열을 UTF-8로 변환하여 URL에 넣어줘야 하는데, 자바스크립트의 encodeURIComponent 메서드를 활용하여 변환해주고, 이 값을 기존에 선언한 currentEncodedData에 할당해준다.

4) UTF-8로 변환한 키워드를 통해 데이터 요청하기

async function fetchDataByInput(){
    const response = await fetch(`https://apis.data.go.kr/B551011/KorService1/searchKeyword1?serviceKey=(내가 받은 키 값)&pageNo=${currentPage}&numOfRows=10&MobileApp=AppTest&MobileOS=ETC&arrange=Q&contentTypeId=25&keyword=${currentEncodedData}&_type=json`);
    const jsonData = await response.json();
    return jsonData;
}

5) 받은 배열 데이터를 렌더링하기

async function courseRenderHTML(){
loadingPage.classList.remove('hidden');
if(currentEncodedData){
    try{
        courseContent.innerHTML='';
        const jsonData = await fetchDataByInput();
        const jsonDataArray = jsonData.response.body.items.item;
        const totalPages = Math.ceil(jsonData.response.body.totalCount / itemsPerPage);
        await insertHTML(jsonDataArray);
        createPageButtons(totalPages);
loadingPage.classList.add('hidden');
    } catch(error){
        buttonBox.innerHTML='';
        loadingPage.classList.add('hidden');
        window.alert('검색된 데이터가 없습니다.');
    }
} else{
    try{
courseContent.innerHTML='';
const jsonData = await fetchDataByCategory();
const jsonDataArray = jsonData.response.body.items.item;
        const totalPages = Math.ceil(jsonData.response.body.totalCount / itemsPerPage);
await insertHTML(jsonDataArray);
        createPageButtons(totalPages);
        loadingPage.classList.add('hidden');
    } catch(error){
        buttonBox.innerHTML='';
        loadingPage.classList.add('hidden');
        window.alert('검색된 데이터가 없습니다.');
    }
}
}

기존에 살펴봤던 courseRenderHTML에서 이제는 currentEncodedData가 존재하는 상황이다.

if(currentEncodedData){
    try{
        courseContent.innerHTML='';
        const jsonData = await fetchDataByInput();
        const jsonDataArray = jsonData.response.body.items.item;
        const totalPages = Math.ceil(jsonData.response.body.totalCount / itemsPerPage);
        await insertHTML(jsonDataArray);
        createPageButtons(totalPages);
loadingPage.classList.add('hidden');
    } catch(error){
        buttonBox.innerHTML='';
        loadingPage.classList.add('hidden');
        window.alert('검색된 데이터가 없습니다.');
    }
}

이런 식으로 데이터를 필터링 할 수 있다.

4. 배운 내용

4-1. 폼 태그에 있는 데이터를 불러오는 방법

select, input과 같은 폼 태그들은 value 프로퍼티를 통해 현재 값을 참조할 수 있다. 페이지 내에서는 폼 요소는 독립적이므로 id 선택자를 통해 참조할 수 있었다.

4-2. querySelector를 통해 클래스 선택자를 받은 요소 참조하기

출처 : 자바스크립트 딥 다이브

class 선택자를 사용한 HTML 요소를 참조하기 위해서는 Element.prototype.getElementsByClassName 메서드를 사용해 검색하는 방법, querySelector를 통해 클래스 선택자 문법으로 검색하는 방법이 존재한다.

querySelector, querySelectorAll 메서드는 getElement~ 메서드보다 다소 느린 것으로 알려져 있다. 하지만 CSS 선택자 문법을 적용하여 일관된 방식으로 요소 노드를 취득할 수 있는 querySelector 방법이 코드의 통일성을 유지함에 있어선 좋다고 느꼈다.

앞으로 바닐라 자바스크립트로 요소 노드를 참조할 경우에는 책의 권장사항 대로 id 선택자의 경우 getElementById 메서드, 그 외의 선택자의 경우 querySelector 종류를 사용하여 참조할 것이다.

4-3. fetch API

출처 : 자바스크립트 딥 다이브

fetch 함수는 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API이다. 기존 XMLHttpRequest 객체보다 사용법이 간단하고 프로미스를 지원하기 때문에 비동기 처리를 위한 콜백 패턴의 단점에서 자유롭다.

fetch 함수는 HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환한다.

Response 객체는 HTTP 응답 몸체(받아온 정보들)를 위한 다양한 메서드를 제공한다. MIME 타입이 application/json인 HTTP 응답 몸체를 취득하려면 Response.prototype.json 메서드를 사용한다. 이 메서드는 Response 객체에서 HTTP 응답 몸체를 취득하여 역직렬화한다.

4-4. for ... of ... 순회문

for ... of 문은 순회하면서 이터러블의 요소를 변수에 할당할 수 있게 해준다.

문법은 다음과 같다.

for (변수 선언문 of 이터러블 ) { ... }

for (const data of jsonDataArray){ ... }

기존에 사용했던 코드를 기준으로 말하면, jsonDataArray에 담겨있는 각 객체 데이터들을 data라는 변수로 선언하여 처리한다.

4-5. switch/case 문

일단 if ... else 문의 경우 주어진 조건식의 평가 결과 논리적 참 또는 거짓에 따라 실행할 코드 블록을 결정한다. 평가 결과가 true일 경우 if 문의 코드 블록이 실행되고, false일 경우 else 문의 코드 블록이 실행된다.

if (조건식) {
	// 조건식이 참이면 이 코드 블록이 실행
} else {
	// 조건식이 거짓이면 이 코드 블록이 실행
}

if 문의 조건식은 불리언으로 평가되어야 하며 만약 불리언 값이 아니면 자바스크립트 엔진에 의해 암묵적으로 불리언 값으로 강제 변환되어 실행할 코드 블록을 결정한다.

그다음 switch/case 문의 경우 표현식을 평가하여 그 값과 일치하는 표현식을 갖는 case 문으로 실행 흐름을 옮긴다. case 문은 상황을 의미하는 표현식을 지정하고 콜론으로 마친다.

아까 사용했던 코드에서 for...of 문으로 순회하면서 선언한 data의 cat2 값의 문자열 값이 'C0112' ~ 'C0117'임에 따라 courseRenderHashtag 값을 할당한다.

if(data.cat2){
    switch (data.cat2){
        case 'C0112':
            courseRenderHashtag='#가족코스';
            break;
        case 'C0113':
            courseRenderHashtag='#나홀로코스';
            break;
        case 'C0114':
            courseRenderHashtag='#힐링코스';
            break;
        case 'C0115':
            courseRenderHashtag='#도보코스';
            break;
        case 'C0116':
            courseRenderHashtag='#캠핑코스';
            break;
        case 'C0117':
            courseRenderHashtag='#맛코스';
            break;
    }
} else{
    courseRenderHashtag='#추천코스'
}

4-6. HTML 태그 삽입하기

  • Element.prototype.innerHTML

요소 노드의 마크업을 취득하거나 변경할 수 있다. 요소 노드의 innerHTML 프로퍼티를 참조하면 요소 노드의 콘텐츠 영역(시작 태그와 종료 태그 사이) 내에 포함된 모든 HTML 마크업을 문자열로 반환한다.

textContent 프로퍼티의 경우 마크업은 무시하고 텍스트만 반환하지만 innerHTML은 HTML 마크업이 포함된 문자열을 그대로 반환한다.

요소 노드의 innerHTML 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열에 포함되어 있는 HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.

다만 innerHTML의 경우 문자열을 할당하면 유지되어도 좋은 기존의 자식 노드까지 모두 제거하고 다시 처음부터 새롭게 자식 노드를 생성하여 DOM에 반영하므로 비효율적이다.

또한 새로운 요소를 삽입할 때 삽입될 위치를 지정할 수 없다는 단점도 존재한다.

  • Element.prototype.insertAdjacentHTML(position, DOMString)

기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.

두 번째 인수로 전달한 HTML 마크업 문자열을 파싱하고 그 결과로 생성된 노드를 첫 번째 인수로 전달한 위치에 삽입하여 DOM에 반영한다.

첫 번째 인수는 아래 사진과 같이 위치를 지정할 수 있다.

4-7. 노드 생성/추가하기

  • Document.prototype.createElement(tagName)

노드를 생성하는데 사용되는 메서드이다.

전달하는 인수로는 태그 이름을 나타내는 문자열을 넣어준다.

이렇게 생성된 노드는 기존 DOM에 추가되지 않고 홀로 존재하는 상태이다. 따라서 DOM에 추가하는 별도의 처리가 필요하다.

  • Node.prototype.appendChild

요소 노드를 요소 노드의 마지막 자식 요소로 추가한다.

이 과정에서 새롭게 생성한 요소 노드가 DOM에 추가된다. 기존의 DOM에 요소 노드를 추가하는 처리는 이 과정 뿐이다. 이 때 리플로우와 리페인트가 실행된다.

하지만 각 요소가 추가될 때마다 리플로우와 리페인트가 발생되어 높은 비용이 들게된다. 따라서 가급적 횟수를 줄이는 편이 성능에 유리하다.

이를 줄이는 방법으로는 추가되는 요소 노드를 담을 Container 노드를 생성하여 Container에 추가한 후 Container만 DOM에 올리는 방법과 DocumentFragment를 사용하는 방법이 있다.

나는 여기서 Container를 추가하는 방법을 선택했다.

일단 내가 추가해야 하는 구조가 ul 태그 하위에 배열의 개수만큼 li 태그를 채워야 하는 구조이므로 컨테이너 역할을 하는 ul 태그가 불필요한 태그는 아니었고, 추가할 때 insertAdjacentHTML을 통해 문자열 노드를 추가했으므로 appendChild로는 요소 노드의 추가가 불가능했다. appendChild는 노드 객체를 인수로 받기 때문이다.

newUl.insertAdjacentHTML('beforeend', `<li class="course_contentListItem">
    <div class="course_contentListItemImg_box">
        <a class="course_contentListItemLink" href="../course/courseContent.html">
            <img class="course_contentListItemLinkImg" src=${courseRenderImageUrl} alt="">
        </a>
    </div>
    <div class="course_contentListItemText_box">
        <a class="course_contentListItemTextLink" href="../course/courseContent.html">
            <h4 class="course_contentListItemTextLinkTitle">${data.title}</h4>
            <p class="course_contentListItemTextLinkText">${courseRenderOverview}</p>
        </a>
        <ul class="course_contentListItemTextList">
            <li class="course_contentListItemTextListRegion">${courseRenderAddr}</li>
            <li class="course_contentListItemTextListHashtag">${courseRenderHashtag}</li>
        </ul>
    </div>
</li>`)

4-8. 문자열을 UTF-8 형태로 변환하기

출처 : MDN

  • encodeURIComponent

특정 문자를 UTF-8로 인코딩할 때 사용된다.

profile
프론트엔드를 희망했었던 화학공학과 취준생

0개의 댓글