앱에 새 소식 보여 주기

팀이 피드백 보드에 변경 로그 항목을 게시하면, 사용자가 이미 머무는 곳인 앱에서 그 소식을 접해야 합니다. "What's new" 배지는 사용자가 아직 보지 않은 항목 수를 세고, 배지를 열면 최신 항목이 나오면서 숫자가 0으로 돌아갑니다.

읽지 않은 항목 3개를 세는 배지가 달린 "What's new" 버튼

1. 시작하기 전에

  • 변경 로그 항목이 게시된 피드백 보드가 필요합니다(공개 피드백 보드 열기 참고). 보드는 공개여야 하며, 앱은 ideas 같은 슬러그로 보드를 읽습니다.

  • 프로젝트의 publishable 키(Settings → API keys)가 필요하고, feedback:read 권한이 있어야 합니다. 사용자가 로그인하는 앱이라면 feedback:write도 필요합니다. 로그인한 사용자의 항목을 읽음으로 표시하는 것은 쓰기 작업이기 때문입니다. publishable 키는 앱에 넣어 배포해도 안전합니다.

  • 웹 앱이라면 그 주소를 프로젝트의 웹 앱 web origin(Settings → Apps)으로 등록해야 합니다. 예: https://app.acme.com. 브라우저는 다른 페이지에 대한 API 응답을 거부합니다.

2. 로그인한 사용자와 방문자

배지는 모든 사용자에게 동작합니다. "읽음"의 의미는 앱이 Mocco에 사용자가 누구인지 알려 주는지에 따라 달라집니다.

  • 로그인한 사용자. 서버가 로그인한 사용자를 위해 수명이 짧은 토큰에 서명합니다. 공개 보드에서 쓰는 것과 같은 토큰으로, 프로젝트의 identity secret으로 서명한 HS256 JWT입니다. 사용자 ID를 sub에 넣고, 만료는 최대 한 시간 뒤로 합니다. 토큰을 가져오는 함수를 SDK에 넘겨 주세요. 그러면 Mocco가 사용자의 "마지막으로 본 시점"을 직접 보관하므로, 휴대폰에서 변경 로그를 열면 웹의 배지도 사라집니다.

  • 방문자. 토큰이 없으면 기기가 자신이 보여 준 가장 최신 항목의 날짜를 보관합니다. 웹에서는 localStorage에, React Native에서는 직접 넘긴 스토리지에 저장합니다. 새 기기를 쓰거나 브라우저 데이터를 지우면 모든 항목이 다시 새 항목으로 계산됩니다.

SDK는 토큰이 처음 필요할 때 함수를 호출하고, Mocco가 만료를 이유로 토큰을 거부할 때만 다시 호출합니다. 아무도 로그인하지 않은 동안에는 null을 반환하세요. 사용자가 로그아웃하거나 다른 사용자가 로그인하면 whatsNew.feedback.resetEndUser()를 호출합니다.

3. 웹에서

브라우저 SDK를 설치합니다.

npm install @mocco/js

페이지의 버튼에 위젯을 마운트합니다. 버튼에는 읽지 않은 수가 배지로 붙고, 버튼을 누르면 작은 팝오버에 최신 항목이 열립니다. 열면 항목이 읽음으로 표시됩니다.

import { createWhatsNew } from '@mocco/js/whats-new';

const whatsNew = createWhatsNew({
  publishableKey: 'mk_pub_…',
  board: 'ideas',
  // Leave out for visitors only. Return null while nobody is signed in.
  endUserToken: async () => {
    const response = await fetch('/api/mocco-token');
    return response.ok ? ((await response.json()) as { token: string }).token : null;
  },
});

whatsNew.mount('#whats-new', {
  // Optional: a link under the list, such as the board's public changelog.
  moreUrl: 'https://acme.help.mocco.club/feedback/ideas/changelog',
});

최신 항목 3개와 날짜, 태그, 짧은 발췌문이 보이는 팝오버

팝오버는 각 항목의 제목, 날짜, 태그, 본문 첫 몇 줄을 일반 텍스트로 보여 줍니다. 서식, 링크, 이미지는 빠지므로 항목 안의 내용이 페이지에서 코드로 실행되는 일은 없습니다. Escape를 누르거나 팝오버 바깥을 클릭하면 닫힙니다.

mount의 옵션은 다음과 같습니다.

옵션동작
labels위젯에 쓰는 문구: title, loading, empty, error, more, unread(숫자 뒤에 스크린 리더가 읽음)
moreUrl목록 아래에 표시하는 링크
markSeenOnOpenfalse로 두면 whatsNew.markSeen()으로 직접 읽음 처리
formatDate항목 날짜를 표시하는 방식(기본값은 브라우저 언어이며, 예를 들어 "Oct 17, 2026")
onOpenChange팝오버가 열리거나 닫힐 때 true 또는 false와 함께 호출됨. 분석 용도 등에 사용

mount는 배지와 팝오버를 다시 제거하는 함수를 반환합니다. 색을 바꾸려면 페이지에 --mocco-wn-accent(배지), --mocco-wn-bg, --mocco-wn-fg, --mocco-wn-border(팝오버)를 설정합니다. 팝오버는 기본적으로 시스템의 다크 모드를 따릅니다.

위젯 없이 직접 그리려면 await whatsNew.unreadCount()가 개수를 반환하고, whatsNew.markSeen()이 개수를 지우며, whatsNew.client에 항목이 있습니다(loadEntries(), getState(), subscribe(listener)).

위젯과 필요한 클라이언트는 번들된 앱에서 gzip 기준 약 5 KB이고, Mocco의 검사는 25 KB 미만을 유지합니다.

4. React Native에서

React Native SDK를 설치합니다(Expo Go에서 실행됩니다).

npx expo install @mocco/react-native @react-native-async-storage/async-storage

앱에서 WhatsNew를 컴포넌트 바깥에 하나만 만듭니다. AsyncStorage를 함께 쓰면 방문자의 "마지막으로 본 시점"이 앱을 다시 시작해도 유지됩니다.

import AsyncStorage from '@react-native-async-storage/async-storage';
import { createWhatsNew } from '@mocco/react-native/whats-new';

export const whatsNew = createWhatsNew({
  publishableKey: 'mk_pub_…',
  board: 'ideas',
  storage: AsyncStorage,
  endUserToken: async () => await fetchMoccoTokenFromYourServer(),
});

배지는 메뉴 항목이나 탭 옆에 둡니다. 새 소식이 없으면 아무것도 그리지 않고, 앱이 포그라운드로 돌아오면 새로고침됩니다.

import { WhatsNewBadge } from '@mocco/react-native/whats-new';

<WhatsNewBadge whatsNew={whatsNew} style={{ backgroundColor: '#4f46e5' }} />;

항목을 나열하는 화면에서는 useWhatsNew가 데이터를 주고, 목록은 앱의 디자인과 언어로 직접 그립니다.

import { useEffect } from 'react';
import { FlatList, Text } from 'react-native';
import { useWhatsNew } from '@mocco/react-native/whats-new';

export function WhatsNewScreen() {
  const { entries, markSeen } = useWhatsNew(whatsNew, { loadEntries: true });
  useEffect(() => {
    void markSeen().catch(() => undefined);
  }, []);
  return <FlatList data={entries} keyExtractor={entry => entry.id} renderItem={({ item }) => <Text>{item.title}</Text>} />;
}

항목의 body는 Markdown입니다. HTML을 실행하지 않는 Markdown 컴포넌트로 렌더링하거나, 제목과 날짜만 보여 주세요.

5. 게시물 읽기와 투표

같은 키로 보드의 게시물을 읽을 수 있고, 로그인한 사용자는 투표할 수 있습니다. 웹에서는 @mocco/js/feedback을, React Native에서는 @mocco/react-native/whats-new의 FeedbackClient를 사용합니다.

import { createFeedback } from '@mocco/js/feedback';

const feedback = createFeedback({ publishableKey: 'mk_pub_…', endUserToken: fetchMoccoToken });
const board = feedback.board('ideas');

const { posts } = await board.posts({ sort: 'top', limit: 20 });
await feedback.vote(posts[0].id); // voting again changes nothing; feedback.unvote(id) takes it back
const { viewer } = await feedback.post(posts[0].id); // viewer.vote: 'counted' once they voted

로그인한 사용자 없이 투표하면 아무것도 전송되기 전에 MoccoError로 거부되며, 이 오류의 code는 missing_end_user_token입니다. 사용자가 앱에서 아이디어를 올리고, 댓글을 달고, 팔로우하게 하거나 아이디어 시트를 바로 열려면 앱에서 아이디어 모으기를 참고하세요.

6. 한도

로그인한 사용자는 한 시간에 120번까지 변경 로그를 읽음으로 표시할 수 있습니다. 자주 여는 위젯도 이 한도에 한참 못 미칩니다. 읽지 않은 수는 100에서 멈춥니다.