[블로그만들기] 최근 읽은 포스트 기능 (typescript)

✨ 최근 읽은 포스트 목록 구현

기능 구현

  • 유저가 최근에 읽은 포스트 목록을 제공한다.
  • strapi를 활용해 최근 읽은 포스트 db 모델을 설계한다.
  • intersection observer 를 활용해, 유저가 포스트를 끝까지 읽었는 지 확인할 것이다.
  • 최신 순서대로 정보를 제공한다. (예를 들어 유저가 a,b,c 순으로 포스트를 읽었으면 c-b-a 순으로 보여주고, 유저가 a포스트를 다시 읽으면 a-c-b 순으로 보여준다.
  • postman을 활용해 api 테스트를 진행할 것이다.

1. 최근 읽은 포스트 DB 모델

🏷 relation

최근 읽은 포스트에서는 2가지 정보가 필요하다. 어떤 유저어떤 포스트를 읽었는 가. 구상한 데이터 모델은 다음과 같다.

[
  {
   user: 유저 정보,
   post: 포스트 정보,
   updatedAt: 읽은 시각,
  },
  {
   user: 유저 정보,
   post: 포스트 정보,
   updatedAt: 읽은 시각,
  },
  ....
]

그래서 최근 읽은 포스트 데이터에 유저 데이터와 포스트 데이터를 relation 연결할 것이다.
릴레이션(relation)에는 6가지 유형이 있다.

  • one way(단방향): 엔티티 A에 하나의 엔티티 B가 있다.
  • one to one(일대일): 엔티티 A에 하나의 엔티티 B가 속한다.
  • one to many(일대다): 엔티티 A에 많은 엔티티가 B가 속한다.
  • many to one(다대일): 엔티티 B에 많은 엔티티가 A가 속한다.
  • many to many(다대다): 엔티티 A에 많은 많은 엔티티 B가 있고 속한다.
  • many way(다방면): 엔티티 A에 많은 많은 엔티티 B가 있다.

이 데이터 간의 relation 연관 관계를 설정할 때 고려해야할 것은 3가지다.
1. 방향 2. 관계의 주체 3. 다중성 이 3가지 기준을 통해 최근 읽은 포스트, 포스트,

최근 읽은 포스트, 포스트, 유저 간의 relation을 설정해보자.

🏷 방향

단방향과 양방향이 있다. 예를 들어 참조용 필드가 있는 객체만 다른 객체를 참조하고 있으면 단방향, 두 객체가 각각 참조형 필드를 가지고 서로 참조하면 양방향 관계다.

  • 최근 읽은 포스트 -> 포스트
    최근 읽은 포스트 데이터는 이 포스트가 어떤 포스트인지 데이터를 참조할 필요가 있다.
    포스트 데이터는 최근 읽은 포스트 데이터를 참조할 필요가 없다.
    때문에 이들은 단방향 관계가 적합하다.

  • 최근 읽은 포스트 <-> 유저
    최근 읽은 포스트 데이터는 이 포스트를 읽은 유저가 누구인 지 참조할 필요가 있다.
    유저는 자신이 최근에 어떤 포스트를 읽었는 지 참조할 필요가 있다.
    때문에 이들은 양방향 관계가 적합하다.

🏷 다중성

  • 최근 읽은 포스트 1 : 포스트 1
    최근 읽은 포스트 데이터는 한 가지 포스트 데이터만 담고 있으며 된다. 때문에 일대일 관계가 적합하다.

  • 최근 읽은 포스트 1 : 유저 N
    한 유저(1)는 최근 읽은 포스트(N)가 여러 개 일 것이다.
    때문에 다대일 관계가 적합하다.

🏷 relation 최종 세팅

최근 읽은 포스트 1 -> 포스트 1

최근 읽은 포스트 1 <-> 유저 N

🤔 최근 읽은 포스트 컬렉션을 별도로 생성한 이유

처음에는 유저 컬렉션, 포스트 컬렉션을 다대다 연결하여 기능을 구현하려고 했다.

유저(1)는 여러 개 포스트(N)를 작성할 수 있다.
유저(1)는 여러 개 포스트(N)를 읽을 수 있다.

유저컬렉션과 포스트컬렉션을 연결하는 데, 이름만 다르게 하는 것이다.
하나는
마치 유저 데이터에 유저가 작성한 포스트들이 배열로 저장되는 것처럼, 최근 읽은 포스트도 배열로 저장하면 되지 않을까 생각했다. 문제는 업데이트 시각이었다.

예를 들어 유저가 자신이 작성한 포스트들을 최신 순으로 불러오는 것은 쉽다. 포스트 데이터에 저장되어 있는 발행 시각 순(publishedAt, createdAt) 순으로 가져오면 된다.
그런데 최근 읽은 포스트는 다르다. 유저가 포스트를 '언제 읽었는 지', 데이터가 들어가있어야 한다. 따라서 유저-포스트를 다대다로 연결하는 것이 아니라 '최근 읽은 포스트'컬렉션을 중심으로 유저-'최근 읽은 포스트'-포스트로 연결해주었다.

2. 최근 읽은 포스트 기능 구현

✨ IntersectionObserver API를 활용해 유저 동작 감지

이전에 목업 데이터로 무한스크롤(링크)을 구현할 때 사용했던 IntersectionObserver API다. 요약하면 타켓 요소가 기기의 뷰포트와 교차할 때 비동기로 이벤트를 발생시키는 Web API다. (IntersectionObserver API에 대한 자세한 내용은 이전 무한스크롤 포스트에서 살펴볼 수 있다.)

이 API를 활용해서 단순히 유저가 포스트 링크에 접속했을 때가 아닌, 유저가 포스트를 끝까지 읽었을 때 이벤트를 발생시켜 '최근 읽은 포스트' 데이터를 추가해줄 것이다. 미리 IntersectionObserver API를 활용한 useIO 훅을 만들어놓고, useIO를 통해 생성한 타겟 요소를 포스트 컨텐츠 바로 밑에 달아주었다. 앞으로 모든 포스트의 하단엔 IntersectionObserver API의 타켓이 붙어있게 된다.

const { setTarget } = useIO({
    root: null,
    rootMargin: '0px',
    threshold: 1,
    onIntersect,
  });

<MDviewer title="" contents={postObj.contents} />
<div ref={setTarget}></div>

✨ 유저가 이미 읽었던 포스트인가 : querystring을 활용한 필터링

예를 들어 어떤 유저가 a,b,c라는 포스트를 읽었다고 가정해보자. 이 때 유저가 c포스트를 다시 읽으면, a,b,c,c가 제공되는 것이 아니라 순서만 바뀌어서 c,a,b가 제공되어야한다. 그러니까 데이터를 post해서 새로 만들지, 아니면 데이터를 put해서 수정할 지 결정하려면 이전에 유저가 포스트를 읽었는지 체크해야한다. qs모듈을 이용하여 '최근 읽은 포스트' 데이터 > 로그인한 유저 아이디와 현재 포스트 아이디를 필터링하여 데이터를 가져온다.

    const query = qs.stringify({
    filters: {
      user: {
        userid: userName,
      },
      post: {
        id: postId,
      },
    },
  });

  const response = await axios({
    method: 'get',
    url: `${API_ENDPOINT}/readingposts?${query}`,
  });

console에 찍어보면 다음과 같이 response가 들어온다. 이 때 data가 null이면 처음 읽는 포스트고, data가 들어있으면 유저가 이미 읽었던 포스트이다. 따라서 data.length를 기준으로 falsepost 를 해주고, true면 data의 id 값과 함께 put을 해주면된다.

🐛 axios method를 setState로 관리하지말자

const [method, setMethod] = useState('post');

axios method가 post, put 등 동적으로 변화하는 값이라고 생각해서, 이를 useState로 만드는 오류를 범했다. "1. data.length 를 체크하고 2.setState로 상태값 업데이트하고 3. api 요청을 보내자!" 는 식으로 코드를 짰다. 그러나, setStateaxios은 비동기로 처리되며 axios post 요청이 우선순위가 더 높아서 업데이트 값이 반영되지도 않는다. 그래서 다음과 같이, 삼항연산자를 통해 함수의 인자를 다르게 전달하는 코드로 바꿨다.

  data.length
    ? postReadingData('put', `${data[0].id}`, userId, postId)
    : postReadingData('post', '', userId, postId);
};

const postReadingData = async (
  method: string,
  putid: string = '',
  userId: number | undefined,
  postId: number
) => {
  const res = await axios({
    method: method as Method,
    url: `${API_ENDPOINT}/readingposts/${putid}`,
    data: {
      data: {
        userid: userId,
        postid: postId,
      },
    },
  });
};

setState를 꼭 써야 하는 상황인지 한 번 더 고민해보자.

📸 기능 구현 화면

이미 읽었던 포스트를 다시 읽었을 경우 최신 순으로 잘 업데이트되는 것을 확인할 수 있다.

좋은 웹페이지 즐겨찾기