[velog 클론 코딩 개발기 -3 ] 트렌딩 포스트 페이지 개발

Yeojin Choi·2022년 1월 11일
0

velog 클론코딩

목록 보기
3/5

HomeTab Component 구현

구조

HomeTabLeftAreaBox

HomeTabLink 2개, TimeFrameDropdownButton 1개로 구성

설치
npm i -D react-icon 를 통해 react-icon package 설치
Import

import { MdTrendingUp, MdAccessTime, MdArrowDropDown, MdMoreVert } from 'react-icons/md';

아이콘 찾기
https://react-icons.github.io/react-icons/icons?name=md 에서 아이콘 이름 검색 가능

TimeFrameDropdownButton

  • useLocacion Hook 을 통해 location 객체를 참조하여 현재 pathName 이 recent (RecentPostPage) 인지 판단

  • 최신 글 페이지가 아닐 때만 해당 버튼을 보여준다.
  • TimeFrameDropdownButton Component 의 children 으로 Dropdown 으로 나타날 Menu Component 를 전달해준다.

  • 메뉴 아이템을 클릭했을 때 상태를 업데이트한다.
  • 메뉴 on/off state 는 해당 컴포넌트 내에서 관리하고, 메뉴 바깥이나 메뉴 아이템을 클릭했을 때 메뉴 open state 를 false로 변경

Redus Store 에 homeState, HomeReducer 추가

  • 포스트 데이터를 Server 에 요청할 때 timeFrameMap 의 key를 parameter 로 사용하기 위해 선택된 TimeFrame 상태를 Redux Store 에서 관리한다.

  • TimeFrameDropdownButton, MoreButton 에서 Dropdown Menu 를 구성하기 위해 공통으로 사용
  • string 배열을 받아 li 요소로 보여주고 click event 발생 시 해당 string 을 인자로 onSelect callback function 을 실행
  • window 객체에 click eventlistener 를 등록하고, 이벤트 발생 객체가 Menu DOM 을 포함하지 않을 경우 (메뉴 바깥 영역을 클릭할 경우) onOutSideClick callback function 을 실행

TrendingPostPage 구현

useLoadPost custom hook 구현

  • 기존에 RecentPostPage 에서 사용하던 로직(fetch, 무한 스크롤 관련)을 재사용 하기 위해 custom hook 으로 분리
  • 이때 트렌딩인지 / 최신 글인지에 따라 서버에 요청하는 URL 이 다르기 때문에 인자로 url 을 전달받을 수 있도록 함

RecentPostsPage

TrendingPostsPage

  • store 에 저장되어있던 timeFrame state 을 가져와 url 과 함께 전송

Server

  • timeFrame parameter 받아 SQL 쿼리문 작성 시 활용할 계획
  • velog-server 코드 분석하여 어떤 기준으로 trending 인지 판단하는 기준 필요
  • post_score 라는 table 이 따로 있는 것으로 보임
  • 왜 post table 에 score 필드로 하지 않을까 .. ?
trendingPosts: async (parent: any, { offset = 0, limit = 20, timeframe = 'month' }) => {
      const timeframes: [string, number][] = [
        ['day', 1],
        ['week', 7],
        ['month', 30],
        ['year', 365],
      ];
      const selectedTimeframe = timeframes.find(([text]) => text === timeframe);
      if (!selectedTimeframe) {
        throw new ApolloError('Invalid timeframe', 'BAD_REQUEST');
      }
      if (limit > 100) {
        throw new ApolloError('Limit is too high', 'BAD_REQUEST');
      }

      let ids: string[] = [];
      const cacheKey = `trending-${selectedTimeframe[0]}-${offset}-${limit}`;

      const cachedIds = lruCache.get(cacheKey);
      if (cachedIds) {
        ids = cachedIds;
      } else {
        const rows = (await getManager().query(
          `
          select posts.id, posts.title, SUM(score) as score  from post_scores
          inner join posts on post_scores.fk_post_id = posts.id
          where post_scores.created_at > now() - interval '${selectedTimeframe[1]} days'
          and posts.released_at > now() - interval '${selectedTimeframe[1] * 1.5} days'
          group by posts.id
          order by score desc, posts.id desc
          offset $1
          limit $2
        `,
          [offset, limit]
        )) as { id: string; score: number }[];

        ids = rows.map(row => row.id);
        lruCache.set(cacheKey, ids);
      }
profile
프론트가 좋아요

0개의 댓글