[글마디 프로젝트] 카테고리, 검색 기능 구현 회고

Minsu Han·2023년 5월 1일
0

Side Projects

목록 보기
4/10
post-thumbnail

시작하며

글마디 프로젝트에 아래와 같은 카테고리별로 글마디를 렌더링하는 기능과, 특정 분류기준과 키워드로 글마디를 검색하는 기능을 구현한 회고입니다.

  • 최근 : 가장 최근에 등록된 글마디 순으로 렌더링
  • 인기 : 좋아요 수가 많은 글마디 순으로 렌더링
  • 내 글마디 : 현재 유저가 등록한 글마디를 렌더링
  • 좋아요 : 현재 유저가 좋아요 표시한 글마디를 렌더링

카테고리 기능 구현하기

카테고리는 아래와 같이 글마디가 렌더링되는 위치 바로 위에 버튼 형태로 유저에게 표시했습니다. 유저가 임의의 카테고리를 클릭하면 url의 hash(#) 정보가 해당 카테고리 id로 설정되도록 href 값을 각각 지정했습니다.

<!-- 글마디 리스트 컨테이너 -->
<div class="collection__container">
  <div class="filters__container">
    <div class="btn"><a href="#recent">최신</a></div>
    <div class="btn"><a href="#trending">인기</a></div>
    <div class="btn"><a href="#my">내 글마디</a></div>
    <div class="btn"><a href="#likes">좋아요</a></div>
  </div>
  <div class="posts__container">
    <div class="spinner__container hidden"></div>
  </div>
</div>

현재 작업중인 주소는 http://localhost:1234/ 인데 유저가 인기 탭을 클릭한다면 주소는 http://localhost:1234/#trending 으로 바뀌게 됩니다.

전체 페이지를 reload 하지 않고 글마디 리스트가 표시될 컨테이너만 부분적으로 reload하기 위해, 브라우저가 주소창의 hash(#)값 변경을 감지했을 때 발생하는 hashchange 이벤트를 활용했습니다. 바뀐 hash값에 따라 해당하는 카테고리의 글마디를 로드합니다.

hashchange 이벤트는 postListView.js에서 감지한 다음, 바뀐 hash 값을 postController.js의 핸들러에 전달합니다. postController는 hash값에 따라 model.js에게 해당 카테고리에 맞게 글마디들을 가공해서 달라고 요청한 다음, 결과를 받아 postListView.js에 렌더링을 하게 합니다.

/* postListView.js */

  /**
   * @description 필터가 선택되거나 페이지가 새로고침될 때 controller가 등록한 handler 실행
   * @param { Function } handler : 주어진 필터에 해당하는 글마디 리스트 로드하는 핸들러
   */
  addHandlerFilter(handler) {
    ['hashchange'].forEach(ev =>
      window.addEventListener(ev, () => {
        handler(location.hash);
      })
    );
  }
/* postController.js */

/**
 * @description 글마디 데이터를 불러와서 렌더링
 * @param { string } hash 주소창 hash값
 */
export const controlLoadPosts = async function (hash) {
  try {
    // ...

    let data;
    if ((hash === "#recent") | !hash) {
      data = await model.loadPost("recent");
    }
    if (hash === "#trending") {
      data = await model.loadPost("trending");
    }
    if (hash === "#my") {
      if (!uid) {
        postListView.renderInfoMessage("로그인이 필요합니다");
      } else {
        data = await model.loadPost("my", uid);
      }
    }
    if (hash === "#likes") {
      if (!uid) {
        postListView.renderInfoMessage("로그인이 필요합니다");
      } else {
        data = await model.loadPost("likes", uid, userFavorites);
      }
    }

    // 글마디 렌더링 (VIEW)
    if (data.length > 0) {
      // ...
    } else {
      // ...
    }
  } catch (err) {
    // ...
  }
};

/**
 * @description 필터 버튼 클릭시 주소창의 hash값을 글마디 로드 메서드에 전달
 * @param { string } hash 주소창 hash값
 */
export const controlFilter = async function (hash) {
  await controlLoadPosts(hash);
};

postListView.addHandlerFilter(controlFilter);

model.js의 loadPost 메서드에서 hash 값에 따라 글마디를 가공하여 제공합니다.

Realtime Database의 경우 REST API 요청으로는 사용 가능한 쿼리 매개변수가 한정적이어서 유저가 업로드한 글마디 정도를 제외하면 직접 클라이언트에서 자바스크립트로 가공해야 했습니다.

sort 메서드에 comparator 함수를 전달하여 커스텀 기준으로 정렬하거나, filter 메서드로 조건에 맞는 글마디만 필터링하였습니다.

또한, GET 요청으로 반환받은 글마디 데이터는 글마디 id를 key로, 글마디 객체를 value로 한 형태의 json 객체이기 때문에, Object.entries() 메서드를 사용하면 key, value를 하나의 element로 한 배열로 변환하여 배열 메서드들을 사용할 수 있었습니다.

데이터 검색 - URI 매개변수 추가
https://firebase.google.com/docs/database/rest/retrieve-data?hl=ko#section-rest-uri-params

/* model.js */

/**
 * @description 글마디 리스트 불러오기
 * @param { string } type recent | trending | my | likes
 * @param { string } uid 유저 id (default null)
 * @param { string[] } userFavorites 유저가 좋아요한 글마디 id 리스트 (default null)
 * @returns 글마디 리스트
 */
export const loadPost = async function (
  type = "recent",
  uid = null,
  userFavorites = null
) {
  try {
    let ret;
    if (type === "recent") {
      ret = await AJAX(`${API_URL_POSTS}.json`, "GET");
      // 최신순 정렬
      ret = Object.entries(ret).reverse();
    }
    if (type === "trending") {
      ret = await AJAX(`${API_URL_POSTS}.json`, "GET");
      // 좋아요순 정렬
      ret = Object.entries(ret).sort(function (a, b) {
        // entry[0] = 글마디id
      	// entry[1] = 글마디 객체
        return b[1].likesNum - a[1].likesNum;
      });
    }
    if (type === "my") {
      // 업로드한 유저 id가 uid인 글마디들을 요청
      ret = await AJAX(
        `${API_URL_POSTS}.json?orderBy="uid"&equalTo="${uid}"`,
        "GET"
      );
      // 최신순 정렬
      ret = Object.entries(ret).sort(function (a, b) {
        return b[1].timestamp - a[1].timestamp;
      });
    }
    if (type === "likes") {
      ret = await AJAX(`${API_URL_POSTS}.json`, "GET");
      // 유저가 좋아요한 글만 필터링
      ret = Object.entries(ret).filter((entry) =>
        userFavorites.includes(entry[0])
      );
      // 최신글순 정렬
      ret.reverse();
    }
    return ret;
  } catch (err) {
    throw err;
  }
};


검색 기능 구현하기

검색 기능 역시 hashchange를 활용하여 비슷한 방식으로 구현했습니다.

<div class="search__container">
  <input placeholder="search"></input>
    <select name="types" id="type-select">
      <option value="body">본문</option>
      <option value="reference">제목</option>
      <option value="author">작가/가수</option>
      <option value="tags">태그</option>
    </select>
  <button>
    검색
  </button>
</div>

유저는 글마디 본문, 제목, 작가, 태그 중 검색 기준을 선택할 수 있고, 키워드를 입력하여 검색 버튼을 누르면 선택한 기준에서 해당 키워드를 포함하는 글마디를 찾을 수 있습니다.

유저가 검색 버튼을 누르면 hash값은 아래와 같이 바뀌게 했습니다.

#search?type=(선택한 기준 value)&key=(입력한 키워드)

그러면 유저가 카테고리를 선택함으로써 발생한 hashchange 이벤트를 감지했던 것처럼, 유저가 검색 버튼을 클릭하면 동일한 이벤트를 감지하여 postController.js에 변경된 hash값을 전달합니다.

postController.js의 controlLoadPosts 메서드에서 hash값에 따른 분기문에 아래 코드를 추가하여, hash값에서 검색기준과 키워드를 추출하여 model.js에게 조건에 맞는 글마디를 검색해달라고 요청합니다.

/* postController.js */

/**
 * @description 글마디 데이터를 불러와서 렌더링
 * @param { string } hash 주소창 hash값
 */
export const controlLoadPosts = async function (hash) {
  try {
    // ...

    let data;
    // 다른 분기문들 ...
    // ...
	if (hash.startsWith("#search")) {
      const query = decodeURI(hash); // 주소 한글 디코딩
      
      // #search?type=(기준)&key=(키워드)
      const type = query.slice(13, query.indexOf("&")); // 검색 기준
      const keyword = query.slice(query.lastIndexOf("=") + 1); // 키워드

      data = await model.loadSearchResults(type, keyword);
    }
    
    // 글마디 렌더링 (VIEW)
    if (data.length > 0) {
      // ...
    } else {
      // ...
    }
  } catch (err) {
    // ...
  }
};
/* model.js */

/**
 * @description 주어진 분류 기준과 검색 키워드를 가지고 필터링 수행 및 결과 리턴
 * @param {*} type 분류 기준
 * @param {*} keyword 검색 키워드
 * @returns 검색 결과
 */
export const loadSearchResults = async function (type, keyword) {
  try {
    let ret = await AJAX(`${API_URL_POSTS}.json`, "GET");
	
    if (type !== "tags") {
      // entry[0] = 글마디id
      // entry[1] = 글마디 객체
      // entry[1][`${type}`] = 글마디 객체의 `${type}` 속성 값
      ret = Object.entries(ret).filter((entry) =>
        entry[1][`${type}`].toLowerCase().includes(keyword.toLowerCase())
      );
    } else {
      // 태그는 여러 개가 포함된 배열임
      ret = Object.entries(ret).filter((entry) =>
        entry[1][`${type}`].map((t) => t.toLowerCase()).includes(keyword.toLowerCase())
      );
    }

    return ret;
  } catch (err) {
    throw err;
  }
};

추후 태그를 클릭했을 때 해당 태그를 포함하는 글마디가 검색되도록 할 때에도 같은 메서드를 사용할 계획입니다.

profile
기록하기

0개의 댓글