앱에서 아이디어 모으기

사용자는 웹사이트를 둘러볼 때가 아니라 앱을 쓰는 동안 아이디어가 떠오릅니다. 피드백 시트는 보드를 앱 안으로 가져옵니다. 득표 수가 많은 아이디어를 나열하고, 로그인한 사용자가 투표하게 하고, 새 아이디어를 받습니다. 제목을 입력하는 동안 비슷한 아이디어가 먼저 나타나므로, 자기 아이디어를 찾은 사용자는 새로 올리는 대신 투표합니다.

제목을 입력하는 시트: 버튼 위에 비슷한 아이디어가 제안되고, 아래에 상위 아이디어가 득표 수와 함께 나열된 모습

1. 시작하기 전에

  • 공개 피드백 보드가 필요합니다(공개 피드백 보드 열기 참고). 앱은 ideas 같은 슬러그로 보드를 읽습니다.

  • 프로젝트의 publishable 키(Settings → API keys)가 필요하고, feedback:read와 feedback:write 권한이 있어야 합니다. publishable 키는 앱에 넣어 배포해도 안전합니다.

  • 서버가 서명하는 토큰으로 구분되는 로그인 사용자가 필요합니다. 프로젝트의 identity secret으로 서명한 HS256 JWT이며, 사용자 ID를 sub에 넣고 한 시간 안에 만료됩니다. 공개 보드와 "What's new" 배지에서 쓰는 것과 같은 토큰입니다. 토큰이 없으면 시트는 아이디어 목록만 보여 주고, 투표하거나 글을 올리려면 로그인해야 한다고 안내합니다.

  • 웹 앱이라면 그 주소를 프로젝트의 웹 앱 web origin(Settings → Apps)으로 등록해야 합니다.

2. 웹에서

브라우저 SDK를 설치하고 페이지의 버튼에 시트를 마운트합니다. 버튼을 누르면 페이지 옆에 시트가 열리고, Escape 키나 닫기 버튼으로 닫습니다.

npm install @mocco/js
import { createFeedbackSheet } from '@mocco/js/feedback-sheet';

const ideas = createFeedbackSheet({
  publishableKey: 'mk_pub_…',
  board: 'ideas',
  // Return null while nobody is signed in: the sheet is then read-only.
  endUserToken: async () => {
    const response = await fetch('/api/mocco-token');
    return response.ok ? ((await response.json()) as { token: string }).token : null;
  },
});

ideas.mount('#ideas-button');

로그인한 사용자는 각 아이디어 옆의 버튼으로 투표하고(다시 누르면 투표가 취소됩니다), 맨 위의 입력란으로 아이디어를 올립니다. 새 아이디어는 작성자의 표와 함께 under review 상태로 시작하고 목록 맨 위에 올라갑니다.

글을 올린 뒤: 버튼 아래의 감사 메시지와, 작성자의 표가 달려 목록 맨 위에 있는 새 아이디어

토큰이 없는 방문자에게도 같은 목록이 읽기 전용으로 보입니다.

방문자에게 보이는 시트: 목록 위에 "Sign in to vote and post ideas."가 표시되고 투표 버튼이 비활성화된 모습

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

옵션동작
labels시트에 쓰는 문구: title(기본값은 보드 이름), submit, similar, signIn, statuses(게시물 상태별 하나씩), votes(스크린 리더용으로 개수를 말해 주는 함수) 등
categoryId새 아이디어를 보드의 카테고리 중 하나에 올림
onOpenChange시트가 열리거나 닫힐 때 true 또는 false와 함께 호출됨. 분석 용도 등에 사용

mount는 시트를 다시 제거하는 함수를 반환합니다. 색을 바꾸려면 페이지에 --mocco-fs-accent(투표와 게시 버튼), --mocco-fs-bg, --mocco-fs-fg, --mocco-fs-border를 설정합니다. 시트는 기본적으로 시스템의 다크 모드를 따릅니다. 사용자가 쓴 글을 포함해 모든 텍스트는 텍스트로만 표시되므로, 게시물 안의 내용이 페이지에서 코드로 실행되는 일은 없습니다.

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

3. React Native에서

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

npx expo install @mocco/react-native

앱에서 시트를 컴포넌트 바깥에 하나만 만들고, 아무 버튼에서나 <FeedbackSheet />를 엽니다. 모달 페이지 시트이며, 열 때마다 보드를 불러옵니다.

import { useState } from 'react';
import { Button } from 'react-native';
import { createFeedbackSheet, FeedbackSheet } from '@mocco/react-native/feedback';

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

export function IdeasButton() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <Button title="Ideas" onPress={() => setIsOpen(true)} />
      <FeedbackSheet sheet={ideas} visible={isOpen} onClose={() => setIsOpen(false)} accentColor="#4f46e5" />
    </>
  );
}

FeedbackSheet은 labels(웹과 같은 문구), accentColor, categoryId를 받습니다. 화면을 직접 디자인해서 그리려면 useFeedbackSheet(ideas)가 같은 상태와 동작을 제공합니다. posts, votes, isSignedIn, draft(similar 포함), toggleVote(postId), setDraft({ title, body }), submit()입니다. "Help & feedback" 화면에서 메신저와 나란히 두기 좋습니다. 둘 다 같은 로그인 사용자를 쓰기 때문입니다.

4. 직접 그리거나 더 나아가기

시트는 FeedbackClient 위에 만들어져 있고, 이 클라이언트를 직접 사용할 수 있습니다(웹에서는 @mocco/js/feedback, React Native에서는 @mocco/react-native/feedback의 FeedbackClient).

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

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

const similar = await board.similar('Dark theme'); // up to 5 posts like the title
const post = await board.createPost({ title: 'Dark theme', body: 'For late nights.' });
await feedback.comment(post.id, 'Also for the mobile app, please.');
const { comments } = await feedback.comments(post.id); // isMine marks the user's own
await feedback.unfollow(post.id); // voting or posting follows a post; this opts out
const roadmap = await board.roadmap(); // { planned, in_progress, shipped }

모든 쓰기 작업에는 로그인한 사용자가 필요합니다. 토큰이 없으면 아무것도 전송되기 전에 MoccoError로 거부되며, 이 오류의 code는 missing_end_user_token입니다.

5. 예제 앱

  • 웹. Mocco 저장소의 examples/web-feedback은 "What's new" 배지와 "Ideas" 시트가 있는 정적 페이지이고, 의존성이 없는 작은 Node 서버가 서버처럼 사용자 토큰에 서명합니다. 내 프로젝트에 대고 실행하는 방법은 README에 있습니다.

  • Expo. 3절의 코드가 통합의 전부입니다. Expo 앱의 아무 화면에나 IdeasButton을 두고(npx create-expo-app을 실행한 뒤 npx expo install @mocco/react-native), fetchMoccoTokenFromYourServer가 서버의 토큰 엔드포인트를 호출하게 하세요.

6. 한도

로그인한 사용자는 한 시간에 아이디어를 5개 올리고, 투표를 30번 하고, 댓글을 20개 달 수 있습니다. 비슷한 아이디어 검색은 한 주소에서 분당 최대 60번 실행됩니다. 시트는 입력이 멈출 때까지 기다리므로 시트에서 입력하는 정도로는 이 한도에 닿지 않습니다. 목록에는 득표 수가 많은 아이디어 20개가 나옵니다.

로그인한 사용자에게는 시트가 열리는 즉시, 이 시트에서든 앱의 다른 곳에서든 이전에 투표한 모든 아이디어에 Voted가 표시됩니다. 누르면 투표가 취소됩니다. 로그인하지 않은 방문자에게는 자기 표로 표시되는 아이디어가 없습니다.