Skip to content

Chapter 10. '좋아요' 낙관적 업데이트(Optimistic Update)와 Pinia 플러그인 ​

"9장에서 구축한 usePostStore와 BoardApp.vue에 현대적인 UX를 입혀보자. 500ms의 네트워크 지연을 기다리지 않고 화면을 즉시 바꾸는 낙관적 업데이트와, 탭 닫힘 방어 플러그인의 결합!"


10.1 BoardApp.vue의 [좋아요] 버튼에 숨겨진 답답함 ​

사내 게시판에서 동료의 유용한 기술 글을 보고 [좋아요(❤️)] 버튼을 누를 때, 현재 BoardApp.vue는 전통적인 비관적(Pessimistic) 방식으로 동작합니다:

  1. 사용자가 [좋아요] 클릭
  2. 버튼에 멍하니 딜레이 발생
  3. 400ms 후 Spring Boot 서버에서 200 OK 응답 도착
  4. 그제서야 숫자가 5에서 6으로 바뀌고 하트가 칠해짐

모바일 사내망이나 와이파이가 느린 환경에서는 "버튼이 안 눌렸나?" 하고 서너 번 연타하게 만듭니다.


10.2 [실습 1] usePostStore에 롤백 가능한 낙관적 액션 추가 ​

우리는 9장에서 만든 usePostStore.ts에 낙관적 업데이트(Optimistic Update) 액션을 추가합니다:

  • 클릭한 그 0.001초 즉시 숫자를 5에서 6으로 올림
  • 백그라운드에서 Spring Boot 백엔드로 비동기 통신
  • 만약 실패(500 에러 / 네트워크 단절)하면? 이전 상태로 즉각 롤백!

[코드 10-1] src/stores/usePostStore.ts에 추가된 낙관적 액션 ​

typescript
// usePostStore.ts 내부
async function toggleLikeOptimistic(postId: number) {
  const targetPost = posts.value.find(p => p.id === postId);
  if (!targetPost) return;

  // 1. 실패 시 롤백을 위해 이전 수치 백업
  const previousLikeCount = targetPost.likeCount;

  // 2. [선제적 반영] 서버 응답을 기다리지 않고 화면 숫자를 즉시 증가!
  targetPost.likeCount++;

  try {
    // 3. 백그라운드에서 실제 Spring Boot API 호출
    const res = await axios.post<Post>(`/api/posts/${postId}/like`);
    targetPost.likeCount = res.data.likeCount; // 최종 정합성 보정
  } catch (error) {
    // 4. [롤백] 실패 시 이전 숫자로 원상 복구!
    targetPost.likeCount = previousLikeCount;
    alert('네트워크 장애로 좋아요 처리에 실패하여 원래대로 롤백되었습니다.');
  }
}

이제 BoardApp.vue에서 @click="toggleLikeOptimistic(row.id)"를 호출하는 순간, 네이티브 앱처럼 빛의 속도로 반응하는 극상의 UX가 완성됩니다.


10.3 [실습 2] Pinia 플러그인: 글 작성 중 탭 닫힘 방어 ​

BoardApp.vue에서 열심히 질문 글을 작성하다가 실수로 브라우저 탭을 닫거나 새로고침을 누르면 본문이 날아갑니다.
이를 방지하기 위해, 모든 스토어의 상태 변경을 가로채서 localStorage에 자동 보관하는 Pinia 플러그인을 장착합니다.

[코드 10-2] src/plugins/piniaPersist.ts ​

typescript
import type { PiniaPluginContext } from 'pinia';

export function piniaLocalPersistPlugin({ store, options }: PiniaPluginContext) {
  if ((options as any).persist) {
    const storageKey = `BOARD_APP_${store.$id.toUpperCase()}`;

    // 1. 앱 기동 시 브라우저 로컬 스토리지에서 이전 작성 글 복원
    const fromStorage = localStorage.getItem(storageKey);
    if (fromStorage) {
      try {
        store.$patch(JSON.parse(fromStorage));
      } catch (e) {
        console.error('스토리지 파싱 에러:', e);
      }
    }

    // 2. 사용자가 글을 타이핑할 때마다 자동으로 로컬 스토리지에 자동 저장
    store.$subscribe((_, state) => {
      localStorage.setItem(storageKey, JSON.stringify(state));
    }, { detached: true });
  }
}

10.4 다음 챕터로 이어지는 질문 ​

이제 BoardApp.vue는 극상의 사용자 경험(UX)과 데이터 영속성까지 갖추었습니다.

하지만 보안 관점에서 결정적인 문제가 있습니다:
현재 BoardApp.vue의 [게시글 등록] 폼과 [삭제] 버튼은 로그인하지 않은 외부 방문자에게도 무방비로 노출되어 있습니다.
누군가 브라우저 주소창에 /write나 /admin을 직접 치고 들어오면 그대로 화면이 열립니다.

"비로그인 사용자가 글쓰기 URL로 직접 접근하려 할 때, 이를 가로채서 로그인 페이지로 보내고, 로그인 후에는 원래 쓰려던 화면으로 부드럽게 복귀시킬 수는 없을까?"

이어지는 Chapter 11에서는 프론트엔드의 관문(Gateway)인 **Vue Router 네비게이션 가드(AOP)**를 구축하여 보안 인가 파이프라인을 완성해 보겠습니다.

Moon HomeLab Engineering Knowledge Hub