[토이프로젝트] 랜덤 아티클 추천 서비스 05 아티클 추천 페이지 기능 구현

대만이·2023년 5월 30일
0
post-thumbnail

🏋️ Todo

아티클 추천(메인) 페이지에서 구현할 기능은 다음과 같다.

  • 아티클 랜덤 추천 기능
    - 추천받기 버튼 클릭시 랜덤으로 하나의 아티클 추천
  • 추천 아티클 카드 UI
    - 아티클 카드에는 이미지, 글제목, 내용미리보기가 들어감
    - 아티클 카드 클릭시 해당 페이지로 이동
  • 읽음 데이터로 플래그 변경
    - 사용자가Done에 체크할 경우 해당 아티클은 다시 추천되지 않음
  • 우측 상단 리스트 아이콘을 클릭시 데이터 페이지로 이동



01 아티클 랜덤 추천 기능

📄 container.tsx

export default function MainPage() {
  const [listsData, setListsData] = useState<any[]>([]);
  const [selectedArticle, setSelectedArticle] = useState({
    id: '',
    isRead: false,
    title: '',
    link: '',
  });

  useEffect(() => {
    const fetchLists = async () => {
      const queryLists = query(
        collection(db, 'list'),
        where('isRead', '==', false) // isRead가 false인 doc만 불러오기
      );
      const result = await getDocs(queryLists);
      const datas = result.docs.map((el) => el.data());
      setListsData(datas);
    };
    fetchLists();
  }, []); // 처음 렌더링 되었을 때 실행

  const clickCreateRandomArticle = () => {
    const dataNum = listsData.length;
    const randomNum = Math.floor(Math.random() * dataNum); // 데이터의 수가 최대값일 때 그 범위 내에서 랜덤숫자 고르기
    setSelectedArticle(listsData[randomNum]); // 해당 번호의 데이터 객체를 selectedArticle에 넣고 이 변수를 활용한다.
  };

데이터 불러오기

  • useEffect를 사용하여 페이지가 처음 렌더링될 때 한번 데이터를 불러온다.
  • 이 때 firebase의 query()메소드를 사용하여 isRead필드값이 false인 데이터만 불러오도록 했다. 따라서 사용자가 읽음 처리한 아티클은 추천되지 않는다.

랜덤으로 하나의 데이터 뽑기

  • clickCreateRandomArticle함수를 통해 랜덤으로 하나의 아티클 데이터의 정보를 뽑는다.
  • 데이터의 수가 최대값일 때 그 범위 내에서 랜덤숫자 고른다.
  • 해당 번호의 데이터 객체를 selectedArticle에 넣고 이 변수를 활용한다.

💬 기사 생성을 여러번 클릭 했을 때 같은 아티클이 연속으로 두번 이상 추천되지 않도록 보완할 것


02 추천 아티클 카드 UI

📄 unit.ts

export const getRandomImage = () => {
  const seed = Math.floor(Math.random() * 1000);
  const image = `https://picsum.photos/200/300?seed=${seed}`;
  return image;
};
📄 container.tsx
...
  const clickCreateRandomArticle = () => {
    const dataNum = listsData.length;
    const randomNum = Math.floor(Math.random() * dataNum);
    setSelectedArticle(listsData[randomNum]);

    const image = getRandomImage();
    setRandomImage(image);
  };
...
return (
    <MainPageUI
      clickCreateRandomArticle={clickCreateRandomArticle}
      selectedArticle={selectedArticle}
      randomImage={randomImage}
    />
  );
📄 presenter.tsx

export default function MainPageUI(props: IPropsMainPageUI) {
  return (
		...
          <a href={props.selectedArticle.link} target='_blank'>
            <s.Article
              hoverable
              style={{ width: 240 }}
              cover={
                <img
                  alt='example'
                  src={props.randomImage}
                  height={150}
                  style={{ objectFit: 'cover' }}
                />
              }
            >
              <Meta
                title={props.selectedArticle.title}
                description={props.selectedArticle.link}
                style={{ textAlign: 'left' }}
              />
            </s.Article>
          </a>
		...

selectedArticle state

  • 01에서 설명한 아티클 랜덤 추천기능을 통해 가져온 selectedArticle데이터를 props로 불러와서 링크와 타이틀 등에 사용했다.
    💬 추후 해당 링크의 메타데이터를 활용하여 썸네일, 제목, 소개 등을 자동으로 불러오게 할 것

randomImage state

  • 메타데이터로 썸네일을 받아오기 전에 openAPI도 사용해볼 겸 Lorem Picsum을 활용하여 썸네일 이미지를 랜덤으로 생성하기로 했다.
  • 랜덤으로 이미지를 생성하는 getRandomImage함수를 생성하고, 추후 다른 부분에서도 활용도가 높을 것 같아 unit 디렉토리에 따로 빼두었다.
  • 클릭할 때마다 새로운 이미지가 생성되어야 하므로 clickCreateRandomArticle함수 안에서 getRandomImage함수를 호출했다.

🤬 에러일지 : 웹브라우저 이미지 캐싱
처음에는 getRandomImage함수를 다음과 같이 정의했다.

<export const getRandomImage = () => {
  const seed = Math.floor(Math.random() * 1000);
  const image = `https://picsum.photos/200`; 👈👈
  return image;
};

그런데 함수를 처음 호출할 때는 이미지가 제대로 생성되고, 이후 호출부터는 첫 이미지가 계속 유지되는 문제가 있었다. 원인은 웹브라우저의 이미지 캐시 문제였다.

  • 웹브라우저가 초기에 'https://picsum.photos/200' 라는 주소로 서버로부터 이미지를 받아온다.
  • 해당 이미지를 브라우저 캐시에 저장해놓는다.
  • 다시 함수를 호출해 'https://picsum.photos/200' 로 이미지를 받아오고 API 서버는 이번에 다른 이미지를 보내준다.
  • 하지만 웹브라우저는 같은 주소이므로 기존에 캐시에 저장해놓은 이미지를 띄운다.


    😇 해결방법
const image = `https://picsum.photos/200/300?seed=${seed}`;

위와 같이 주소 뒤에 ?특정값을 추가하여 웹브라우저에 매번 새로운 주소를 보내게 하여 문제를 해결했다.


참고


03 읽음 데이터로 플래그 변경

📄 container.tsx
...
  const onChangeIsRead = async () => {
    const changedIsRead = !selectedArticle.isRead;
    setSelectedArticle({ ...selectedArticle, isRead: changedIsRead }); // ui에 변화 반영
    await updateDoc(doc(db, 'list', selectedArticle.id), { // db에 변화 반영
      isRead: changedIsRead,
    });
  };
📄 presenter.tsx
...
<s.IsRead
  checked={props.selectedArticle.isRead}
  onChange={props.onChangeIsRead}
  >
    Done
</s.IsRead>
  • 체크박스(IsRead컴포넌트)에 체크하면 onChange함수 실행
  • 체크박스 UI에 변경사항 반영하기 위해 selectArticle에 변경사항 업데이트
  • updateDoc메소드 활용하여 db에 변경사항 업데이트

04 리스트 아이콘 클릭시 페이지 이동

const onClickMoveToData = () => {
    router.push('/list');
  };

useRouter()push()메소드를 사용해 아이콘 클릭시 list 페이지로 이동하도록 구현




드디어 기초공사가 끝났다!
어떻든 일단은 하나의 서비스를 완성했다.
이제 버그와 개선사항을 쫌쫌따리 해결해나가보자.

간단한 서비스 하나에도 이렇게 많은 기능이 필요하고, 신경쓸 점이 많다.
예상치 못한 곳에서 오류가 나 새로운 배움도 많이 얻었고,
역시 직접 만들어보는 것이 최고의 복습이고 공부인가보다🥹

profile
느려도 오래 걷습니다👟

0개의 댓글