MainPage.vue

<div class="article-container">
            <TheArticle v-for="(article, index) in articles" :article="article" :key="article.id"
              @open-article-modal="openModal" />
          </div>

TheArticle.vue

<script setup>
import CommentListItem from "./CommentListItem.vue"
import axios from "axios";
import { onMounted } from "vue";
import { ref, watch } from "vue";

import { useAuthStore } from "@/store/auth";
const store = useAuthStore();
const article = ref({});
const comments = ref({});
const isLiked = ref(false);
const isBookmarked = ref(false);
const noImage = ref(true);

const props = defineProps({
  article: Object,
});
const emit = defineEmits([
  "openArticleModal",
])

onMounted(async () => {
  if (sessionStorage.getItem('token') && props.article) { //이미 로그인 되어 있으면 토큰 갱신
    // await store.getToken();

    //로그인 된 상태 -> 모든 정보 API
    //게시물 정보 받아오기
    await axios.get(`http://localhost:8080/api/articles/${props.article.id}`, {
      headers: {
        Authorization: `Bearer ${sessionStorage.getItem('token')}`,
      },
    })
      .then(({ data }) => {
        article.value = data.response.articleInfo;
        isLiked.value = data.response.articleInfo.isLiked;
        isBookmarked.value = data.response.articleInfo.isBookmarked;
      })
      .catch()

    //댓글 정보 받아오기
    await axios.get(`http://localhost:8080/api/comments?articleId=${props.article.id}`,
      {
        headers: {
          Authorization: `Bearer ${sessionStorage.getItem('token')}`,
        },
      }
    )
      .then(({ data }) => {
        comments.value = data.response.comments
      })
      .catch
    return;
  }

  else { // 로그인 안된 상태 -> 제한된 정보 API
    //게시물 정보 받아오기
    article.value = props.article;
    isLiked.value = false;
    isBookmarked.value = false;
  }
})

watch(article, async () => {
  if (store.isAuthenticated && props.article) { //이미 로그인 되어 있으면 토큰 갱신
    // await store.getToken();

    //로그인 된 상태 -> 모든 정보 API
    //게시물 정보 받아오기
    axios.get(`http://localhost:8080/api/articles/${props.article.id}`, {
      headers: {
        Authorization: `Bearer ${sessionStorage.getItem('token')}`,
      },
    })
      .then(({ data }) => {
        article.value = data.response.articleInfo;
        isLiked.value = data.response.articleInfo.isLiked;
        isBookmarked.value = data.response.articleInfo.isBookmarked;
      })
      .catch()

  }

  else { // 로그인 안된 상태 -> 제한된 정보 API
    //게시물 정보 받아오기
    article.value = props.article;
    isLiked.value = false;
    isBookmarked.value = false;
  }
})

const openArticleModal = () => {
  if (!store.isAuthenticated) {
    alert("게시물 상세보기는 로그인 후 이용하실 수 있습니다!");
    return;
  }
  emit('openArticleModal', article.value.id);
}
</script>

Untitled

이러한 형태의 메인 페이지가 있고, 게시글 목록을 API로 받아와 하나의 게시물을 컴포넌트로 렌더링한다. 이 때, 게시글 컴포넌트가 무한하게 로딩되는 현상이 발생했다.

Untitled

문제의 원인은 watch로 게시글 목록 객체의 변경을 감지해 작업을 실행하는 부분인데, 이 부분에서 게시글 목록의 게시글 하나가 변경(게시글 로딩, 좋아요, 북마크 등) 될 때 마다, watch가 그 변경을 감지해 실행된다. 전체 게시물을 다 담고 있으므로, 로딩 또한 하나의 변경으로 감지되어 결과적으로 해당 watch 구문은 무한 루프에 빠지게 된다.

Untitled

코드를 위와 같이 작성했던 이유는 ArticleDetailModal.vue 페이지(게시글 상세보기 모달)에서 변경되는 좋아요, 북마크, 댓글 등의 내역을 메인 페이지에서도 실시간으로 반영하기 위함이었다.

다시 말해서 모달에서 State 변경이 일어날 경우 watch로 이를 감시해 메인 페이지의 게시물 또한 그 State 변경을 반영하기 위함이었다.

그러나, 무한 루프 문제로 인해 이를 해결해야 하는 상황이 되었고, 2가지 방법에 대해서 생각해봤다.

  1. 실시간으로 State 변경을 반영하지 않고, 필요시 새로고침을 하거나 게시글 목록을 새로 로드하는 버튼(트리거)를 만들기
  2. 무한 스크롤을 할 때 게시글 목록 배열에 호출된 게시물을 push 하는 것이 아니라, 다음 page까지의 게시물을 새로 받아오기