토이프로젝트: 공감 기능 최종본

coolchaem·2022년 4월 25일
1

toyproject

목록 보기
15/21

공감 기능과 서버의 interaction을 집중적으로 수정하였다.
문제는 대략 세가지였다.

  • post get API 요청과 like get API 요청을 클라이언트에서 하는 것보다 서버에서 하나의 값으로 반환하게 API 수정하는 것!
  • 클라이언트에서 like 버튼 누를 때 서버와 interaction 처리하고 업데이트 하는 것!
  • 이전 개발에서 사용된 동일한 포스트의 캐시 문제

post get API 수정

client에서 API는 단일 type으로 요청받도록 수정했다.

before

  useEffect(() => {
    axios({
      baseURL: API_HOST,
      url: `/@${userId}/${urlSlug}`,
    })
      .then(response => {
        const _post = response.data;
        setPost(_post);

        if (user.username !== '') {
          // 서버 api 수정까지 연달아 요청(post.id 의존성)
          // API 수정 예정: post + liked join (= SinglePost type)
          axios({
            baseURL: API_HOST,
            url: `/${user.username}/liked/${_post.id}`,
          })
            .then(reponse => {
              setLiked(reponse.data.length !== 0);
            })
            .catch(error => {
              console.error(error);
            });
        }
      })
      .catch(error => {
        console.error(error);
        if (error.response.status === 404) {
          navigate('/NotFound', { replace: true });
        }
      });
  }, [navigate, urlSlug, user.username, userId]);

after

  • 큰 차이점은 axios를 한 번만 요청하고 post에 liked를 저장할 프로퍼티가 존재한다.
    • 그리고 liked 쿼리에 필요한 로그인한 사용자 정보를 query로 넘겨준다.
      • ex. /@userName/PostTitle/?loginUserName=로그인한사용자
    • 대신 서버에서 db 쿼리를 2번 사용한다.
  useEffect(() => {
    axios
      .get(`${API_HOST}/@${userId}/${urlSlug}/`, {
        params: {
          loginUserName: user.username,
        },
      })
      .then(response => {
        const _post = response.data;
        setPost(_post);
        setLiked(_post.liked);
      })
      .catch(error => {
        console.error(error);
        if (error.response.status === 404) {
          navigate('/NotFound', { replace: true });
        }
      });
  }, [navigate, urlSlug, user.username, userId]);

서버
서버의 /@:username/:url_slug/ api에서 기존 post query는 같고 아래 내용이 추가되었다. join을 세번하기엔 login user가 없을 수도 있어서 따로 조건을 체크하고 query를 한 번 더 작성하였다.

//postApi.ts
    const loginUserName = req.query.loginUserName;
    if (loginUserName?.length && typeof loginUserName === 'string') {
      try {
        post.liked = await isLikedPost(loginUserName, post.id);
      } catch (err) {
        post.liked = false;
      }
    }

// postLikeApi.ts
export async function isLikedPost(loginUserName: string, postId: string) {
  try {
    const likedResult = await dbConn.query(
      `SELECT id FROM public."POST_LIKES" WHERE (fk_user_id='${loginUserName}' AND fk_post_id='${postId}');`
    );
    return likedResult.rows.length !== 0;
  } catch (err) {
    console.error(err);
    throw err;
  }
}    

like 버튼 interaction

like 버튼을 누를 때 서버에 업데이트하는 작업을 로그인 기능 완료 후 하려했으나 태스크를 한 번에 완료하고 싶어 로그인 유저 값을 const로 변경해서 진행하였다.

before
like 버튼을 누를 때 단순히 local state를 변경하였다.

  const handleLikeToggle = () => {
    setLiked(!liked);
  };

after

  • like/unlike api를 현재 local state를 보고 정하여 호출하도록 하였다.
    • 단, 로그인하지 않으면 추후 로그인하라고 페이지 전환할 것이다.
  • 큰 변화는 setLiked 위치인데 서버와 데이터 동기의 확실성을 보장하기 위해 post 작업이 완료된 후 local state를 변경하였다.
    • 또한 서버에서 기존 API 는 완료되었다는 단순한 string 메시지를 전달하였으나 liked, likes와 같이 업데이트되는 정보를 반환하니 활용이 좋아 변경하였다.
  const handleLikeToggle = () => {
    try {
      const userName = user.username;
      if (!userName) {
        console.log('login 기능 보수 중^^, 필요하다면 const 값을 주세여');
        return;
      }

      axios
        .post(`${API_HOST}/${userName}/${liked ? 'unlike' : 'like'}/${post.id}/`)
        .then(response => {
          post.likes = response.data.likes;
          setLiked(response.data.liked);
        })
        .catch(error => {
          console.error(error);
        });
    } catch (error) {
      console.error(error);
    }
  };

etag 사용과 캐시 문제

etag를 사용하는 로직을 추가하였었다.
etag(post.body)처럼 post 내용으로 etag hash를 서버에서 구하여 주었는데 서버 리셋 전의 캐시 데이터가 남아있어 cors 에러를 보았다.
그래서 다른 식별자가 필요해보여서 post.id랑 함께 etag hash 구할 때 주게 변경하였다.

// 사용자의 특정 포스팅 GET (by slug)
router.get('/@:username/:url_slug/', async (req, response) => {
  const userName = req.params.username;
  const slug = req.params.url_slug;

  // 사용자 이름이 넘어오지 않은 경우
  if (userName === undefined || userName === '') {
    return response.status(201).json(`Unkown user ${userName}`);
  }

  try {
    // 사용자 이름과 일치하는 포스팅 데이터 (post <-left join- user)
    const result = await dbConn.query(
      `SELECT p.id, p.title, p.released_at, p.body, p.short_description, 
      p.is_markdown, p.is_private, p.thumbnail, p.url_slug, 
      json_build_object(
        'username', u.user_name, 
        'email', u.email_addr, 
        'is_certified', u.is_certified) as user,
      p.likes, false as liked
        FROM public."BLOG_POSTS" as p
        LEFT JOIN public."BLOG_USERS" as u
        ON p.fk_user_name=u.user_name
          WHERE (p.fk_user_name='${userName}' AND p.url_slug='${slug}')`
    );
    if (result.rows.length === 0) {
      return response.status(404).json({ err: 'Post Not Found' });
    }
    const post = result.rows[0];
    let etagHashKey = '' + post.id + post.body + post.likes;
    const loginUserName = req.query.loginUserName;
    if (loginUserName?.length && typeof loginUserName === 'string') {
      try {
        post.liked = await isLikedPost(loginUserName, post.id);
      } catch (err) {
        post.liked = false;
      }
      etagHashKey += post.liked;
    }

    response.setHeader('Cache-Control', 'private, no-cache');
    if (
      req.headers['if-none-match'] === etag(etagHashKey) &&
      req.headers['if-modified-since'] === new Date(post.released_at).toUTCString()
    ) {
      return response.status(304).send();
    }
    response.setHeader('ETag', etag(etagHashKey));
    response.setHeader('Last-Modified', new Date(post.released_at).toUTCString());

    return response.status(200).json(post);
  } catch (err) {
    return response.status(404).json({ err: 'Post Not Found' });
  }
});

최종적으로 released date, post id, post date, liked, likes로 cache를 판단하였다.
그러나 썸네일만 변경되는 케이스 등을 위해 json stringify와 이미지 id값을 추가하여야 할 것으로 보여, 이미지 사용 가능한 환경이 되면 캐쉬관련해서 추가 스터디를 해야할 것으로 보인다.

todo

  • 로그인 + share
  • 포스트 이미지 + 캐쉬 보완
profile
Front-end developer

0개의 댓글