Mocco Messenger로 앱 안에서 문의받기
Messenger를 쓰면 앱에 로그인한 사용자가 앱을 나가지 않고 팀에 글을 보낼 수 있고, 팀은 Mocco의 한 인박스에서 답변할 수 있습니다. 문의 하나가 대화 하나입니다. 사용자는 카테고리를 고르고 글을 쓰며, 같은 스레드에서 계속 답장할 수 있습니다. 앱 버전, 빌드, 플랫폼이 함께 전달되므로 사용자가 무엇을 실행 중이었는지 알 수 있습니다.
1. Messenger 켜기
워크스페이스 멤버가 워크스페이스의 Products 페이지에서 Messenger를 켭니다. 그러면 각 프로젝트에 Inbox 페이지가 생깁니다.
2. Messenger 설정하기
프로젝트의 Inbox 페이지에서 Set up messenger를 선택합니다. Mocco가 프로젝트의 identity secret을 만들어 한 번만 보여 줍니다. 복사해서 서버의 시크릿으로 저장하세요(예: MOCCO_MESSENGER_SECRET).

이 시크릿으로 Mocco는 요청이 실제로 내 사용자에게서 왔는지 확인합니다. 서버가 로그인한 사용자의 ID를 시크릿으로 서명하고, 앱은 ID와 함께 그 서명을 보냅니다. 앱만 가진 사람은 서명을 만들 수 없으므로 다른 사용자인 척 글을 쓸 수 없습니다.
시크릿이 유출되면 인박스 설정에서 Rotate secret을 선택하세요. 이전 시크릿으로 만든 서명은 즉시 동작하지 않으므로, 새 시크릿을 서버에 바로 적용해야 합니다.
3. 앱용 키 만들기
API keys 페이지에서 messenger:chat 스코프의 Publishable 키를 만듭니다. 이 키는 앱에 넣어 배포해도 안전합니다. 서버의 서명이 없으면 키만으로는 아무것도 열 수 없습니다.
4. 서버에서 사용자 ID 서명하기
앱에 로그인한 사용자의 서명을 앱에 전달하세요. 예를 들어 앱이 로그인 후 이미 호출하는 엔드포인트에서 전달할 수 있습니다.
import { signIdentity } from '@mocco/node';
// For the signed-in user only.
const userHash = signIdentity(process.env.MOCCO_MESSENGER_SECRET!, user.id);signIdentity는 사용자 ID의 HMAC-SHA256을 16진수 문자열로 만든 값입니다. 어떤 언어로도 같은 값을 계산할 수 있습니다.
5. 앱에서 팀에 문의하기
React Native
SDK를 설치합니다. 순수 JavaScript이므로 Expo Go에서도 실행됩니다.
npm install @mocco/react-native @react-native-async-storage/async-storage앱에서 Messenger 클라이언트를 하나 만들고 앱을 프로바이더로 감쌉니다. identity는 서버에 로그인한 사용자의 서명을 요청하고, 로그인한 사람이 없으면 null을 반환합니다.
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createMessenger, MessengerProvider } from '@mocco/react-native/messenger';
import * as Application from 'expo-application';
import { Platform } from 'react-native';
const messenger = createMessenger({
publishableKey: 'mk_pub_…',
identity: async () => {
const user = await getSignedInUser();
if (user === null) return null;
const { userHash } = await api.messengerIdentity(); // your endpoint, signs user.id
return { userId: user.id, userHash, name: user.name, email: user.email };
},
context: () => ({
appVersion: Application.nativeApplicationVersion ?? undefined,
build: Application.nativeBuildVersion ?? undefined,
platform: Platform.OS === 'ios' ? 'ios' : 'android',
}),
storage: AsyncStorage,
});
export default function App() {
return (
<MessengerProvider client={messenger}>
<Navigation />
</MessengerProvider>
);
}그다음 훅으로 화면을 원하는 디자인대로 그립니다.
import { useConversation, useConversations, useMessengerCategories, useUnreadCount } from '@mocco/react-native/messenger';
function SettingsRow() {
const unread = useUnreadCount(); // a badge on "Contact us"
// …
}
function ContactList() {
const { conversations, start } = useConversations(); // refreshed while the screen is open
const categories = useMessengerCategories();
// start({ category: 'bug', body }) to open a new one
}
function Thread({ id }: { id: string }) {
const { messages, send, isSending } = useConversation(id); // new messages are marked read
// messages: { seq, author: 'contact' | 'operator', authorName, body, createdAt }
}사용자가 앱에서 계정을 삭제하면 useMessenger().deleteMyData()를 호출하세요. Mocco에 있는 대화, 메시지, 스크린샷을 지우고 기기에서도 사용자 정보를 잊습니다. 누군가 앱에 로그인하면 useMessenger().reidentify()를 호출해 Messenger가 그 사람의 세션을 열게 하세요. 사용자가 앱에서 로그아웃하면 useMessenger().signOut()을 호출해 다음 사용자가 새로 시작하게 하세요.
스크린샷(PNG, JPEG, WebP, GIF)이나 PDF를 첨부하려면 먼저 업로드하고 그 ID를 보냅니다. 파일은 각각 10 MB 이하, 메시지당 최대 3개입니다.
const messenger = useMessenger();
const picked = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'] });
const asset = picked.assets?.[0];
if (asset !== undefined) {
// Send bytes: with a Blob, React Native replaces the upload's Content-Type and it is refused.
const body = new Uint8Array(await (await fetch(asset.uri)).arrayBuffer());
const attachmentId = await messenger.attach({
body,
contentType: 'image/jpeg', // the picked image's real type
sizeBytes: body.byteLength,
});
await send('Here is what I see', [attachmentId]); // from useConversation(id)
}PDF도 같은 방식이며 contentType: 'application/pdf'를 사용합니다(예: 문서 선택기에서 고른 파일). 파일의 실제 타입을 선언하세요. Mocco는 파일의 앞부분을 읽으며, 파일이 선언한 타입과 다르면 메시지를 거부하고 파일을 삭제합니다.
메시지는 attachments와 함께 돌아오며, 각 첨부에는 filename, sizeBytes, 몇 분 동안만 유효한 url이 있습니다. 이미지는 <Image source={{ uri: attachment.url }} />로 표시하세요. PDF의 url은 다운로드이므로, 화면에 바로 띄우지 말고 파일(이름과 크기)로 보여 주고 Linking.openURL(attachment.url)로 열거나 저장하게 하세요.
앱이 닫혀 있을 때 팀의 답변을 알림으로 받으려면 로그인 후 기기의 Expo 푸시 토큰을 등록하고, 사용자가 알림을 누르면 해당 대화를 여세요.
import * as Notifications from 'expo-notifications';
import { messengerConversationIdOf, useMessenger } from '@mocco/react-native/messenger';
const messenger = useMessenger();
const { data: token } = await Notifications.getExpoPushTokenAsync();
await messenger.registerPushToken({ provider: 'expo', token, platform: Platform.OS === 'ios' ? 'ios' : 'android' });
Notifications.addNotificationResponseReceivedListener(response => {
const conversationId = messengerConversationIdOf(response.notification.request.content.data);
if (conversationId !== undefined) navigation.navigate('ContactThread', { id: conversationId });
});알림의 제목은 프로젝트 이름이고 본문은 답변의 첫 부분입니다. 사용자가 앱에서 이미 읽은 답변은 푸시하지 않습니다. signOut()은 기기를 제거하므로, 그 기기에 다음으로 로그인한 사람이 이전 사용자의 답변을 받지 않습니다.
다른 클라이언트
SDK는 어디서든 쓸 수 있는 작은 HTTP API를 호출합니다. 기본 URL은 https://api.mocco.club/v1/messenger입니다.
POST /sessions
Authorization: Bearer mk_pub_…
Content-Type: application/json
{
"userId": "user-42",
"userHash": "4b8d8248…",
"name": "Minji",
"email": "minji@example.com",
"traits": { "plan": "premium" },
"context": { "appVersion": "3.8.0", "build": "412", "platform": "ios", "locale": "ko-KR" }
}응답에는 sessionToken(mms_…, 30일간 유효)과 프로젝트의 categories가 있습니다. 다른 호출에는 토큰을 Authorization: Bearer mms_…로 보냅니다.
| 호출 | 동작 |
|---|---|
GET /conversations | 사용자의 대화를 최신순으로 반환합니다. 마지막으로 읽은 뒤 팀이 답변했으면 hasUnread가 true입니다 |
POST /conversations | 대화를 시작합니다: { "category": "bug", "body": "…", "clientMessageId": "<uuid>", "context": {…} } |
GET /conversations/{id}/messages?afterSeq=0 | 메시지를 오래된 순으로 반환합니다. 각 메시지에 seq가 있으며, 가지고 있는 마지막 값을 afterSeq로 보내면 새 메시지만 받습니다 |
POST /conversations/{id}/messages | 답장합니다: { "body": "…", "clientMessageId": "<uuid>" } |
POST /conversations/{id}/read | 사용자가 해당 메시지까지 확인하면 { "seq": 5 }를 보냅니다 |
POST /push-tokens | 답변을 기기로 푸시하도록 { "provider": "expo", "token": "ExponentPushToken[…]", "platform": "ios" }를 보냅니다. 로그아웃할 때는 DELETE /push-tokens { "token": … }를 보냅니다 |
DELETE /me | 사용자와 사용자가 쓴 모든 것을 삭제합니다. 앱의 "계정 삭제"에 사용합니다 |
POST /attachments | { "contentType": "image/png", "sizeBytes": 48213 }(또는 application/pdf)를 보내면 attachmentId와 바이트를 PUT할 upload URL을 반환합니다. 이후 대화를 시작하거나 답장할 때 그 ID를 attachmentIds에 넣습니다 |
메시지마다 새 clientMessageId를 만들고, 재시도할 때는 같은 값을 다시 사용하세요. 요청이 몇 번 도착해도 Mocco는 메시지를 한 번만 저장합니다. 사용자는 분당 메시지 20개를 보내고 시간당 대화 5개를 시작할 수 있습니다.
로그인하지 않은 사람
로그인하지 않은 사람을 포함해 누구나 문의할 수 있게 하려면, 인박스 설정에서 Let people who aren't signed in write를 켭니다. 이 사람들은 연락받을 이메일을 남깁니다. 앱에서 Messenger 상태가 signed_out이면 이메일을 입력받아 continueAsGuest를 호출하세요.
const { status } = useMessengerState();
const messenger = useMessenger();
if (status === 'signed_out') {
// show an email field, then:
await messenger.continueAsGuest({ email, name });
}기기가 게스트를 기억하므로, 다음에 앱을 열 때 답변을 볼 수 있습니다. 같은 기기에서 나중에 로그인하고 앱이 reidentify()를 호출하면, 게스트일 때 쓴 내용이 그 계정으로 옮겨집니다. 인박스에서 게스트의 대화에는 Guest 표시가 붙고, 그 옆에 Not signed in과 이메일이 표시됩니다. Mocco는 아직 대신 이메일을 보내지 않습니다. 앱에서 답장하거나, 남긴 이메일로 직접 연락하세요.

6. 인박스에서 답변하기
Inbox 페이지에는 열린 대화가 최근 활동순으로 나열되며, 읽지 않은 대화에는 표시가 있습니다. 나머지는 Closed에 있습니다. 인박스를 열어 두면 몇 초마다 새 메시지를 확인합니다.

대화를 열면 스레드를 읽고 답변할 수 있습니다.
Reply는 사용자가 앱에서 보는 메시지를 보냅니다.
Internal note는 팀 전용입니다. 사용자에게는 보이지 않습니다.
Attach는 답장이나 메모에 이미지(PNG, JPEG, WebP, GIF) 또는 PDF를 최대 3개, 각 10 MB까지 추가합니다.
해결되면 Close를 선택합니다. 사용자가 다시 글을 쓰면 저절로 다시 열립니다.
사이드 패널에는 사용자가 누구인지(앱이 설명한 대로), 지금 사용 중인 앱 버전과 플랫폼, 대화를 시작할 때 실행 중이던 환경이 표시됩니다.
Block user는 해당 사용자가 글을 쓰지 못하게 합니다. 이미 가진 내용은 계속 읽을 수 있습니다.

사용자가 첨부한 스크린샷은 스레드에 표시되며, 선택하면 원본 크기로 열립니다.

PDF는 이름과 크기가 있는 파일로 표시됩니다. 선택하면 내려받습니다. PDF에는 스크립트가 들어 있을 수 있으므로 Mocco는 사용자의 PDF를 브라우저에서 열지 않습니다.

사용자에게 파일을 보내려면 답장 입력란 아래의 Attach를 선택하고 이미지나 PDF를 고른 뒤 Send reply를 선택하세요. 각 파일이 텍스트 아래에 크기와 함께 표시됩니다. 보내기 전에 빼려면 **×**를 선택합니다.

보낸 답장은 스레드에 파일과 함께 표시되고, 사용자의 앱은 사용자가 보낸 파일을 받는 것과 같은 방식으로 메시지와 함께 파일을 받습니다. 이미지는 표시하고 PDF는 내려받게 합니다. 내부 메모에 첨부한 파일은 팀에만 보입니다.

Mocco는 사용자의 파일을 검사하는 것과 같은 방식으로 내 파일도 검사합니다. 실제로는 선언한 이미지나 PDF가 아닌 파일은 거부되고 아무것도 전송되지 않습니다.
누군가 데이터 삭제를 요청하면 사이드 패널에서 Erase user's data를 선택하고 확인하세요. Mocco가 사용자와 모든 대화, 메시지, 첨부를 삭제합니다. 되돌릴 수 없으며, 감사 로그에는 삭제가 일어났다는 사실만 기록됩니다.

사용자가 고르는 카테고리는 인박스 설정에 있습니다. 앱은 세션마다 카테고리를 받으며, 레이블을 그대로 보여 주거나 키로 번역해서 보여 줄 수 있습니다.
라운드 로빈으로 업무 나누기
Mocco는 새 대화를 팀원 한 명에게 차례로 배정해, 일이 고르게 나뉘고 각자 맡은 대화를 알 수 있게 합니다. 인박스의 각 대화에는 담당자 또는 Unassigned가 표시됩니다.
인박스 상단의 Join the rotation을 선택하면 새 대화를 받기 시작합니다. 다른 사람을 추가하려면 인박스 설정의 Round robin에서 그 사람을 고르세요. 워크스페이스의 누구든 추가할 수 있습니다.
인박스 상단의 스위치는 내 근무 가능 여부입니다. 자리를 비운 동안에는 새 대화가 나를 건너뛰고 다음으로 가능한 사람에게 갑니다. 돌아오면 다음 대화가 내 것이 됩니다.
가능한 사람이 없으면 누군가 맡을 때까지 새 대화는 미배정 상태로 남습니다.
대화를 다른 사람에게 넘기거나 미배정 대화를 가져오려면 대화 상단의 Assigned to에서 사람을 고르세요. 로테이션에 있든 없든 워크스페이스의 누구나 맡을 수 있습니다. 다시 돌려놓으려면 No one을 선택합니다. 모든 변경은 감사 로그에 남습니다.
로테이션에서 누군가를 빼도 그 사람이 이미 가진 대화는 그대로입니다.


7. 알림 받기
새 대화(messenger.conversation.created)와 사용자의 추가 메시지(messenger.message.received)는 Mocco 이벤트입니다. Discord 채널의 규칙에 Mocco 프리셋을 적용하거나 이 두 이벤트에 대한 규칙을 추가하면, 팀이 이미 쓰는 곳에서 알림을 받을 수 있습니다(Mocco 이벤트 참고). 새 대화 알림에는 누구에게 배정되었는지가 표시됩니다.
아무도 맡지 않은 대화만 알림받고 싶다면 대신 messenger.conversation.unassigned 규칙을 추가하세요. 로테이션에 가능한 사람이 없을 때 대화가 시작되면 Mocco가 이 이벤트를 보냅니다. 대화 이벤트는 앱이 재시도해도 채널에 한 번만 전달됩니다.
인박스를 브라우저 탭에 열어 두면 Mocco가 새 대화마다 데스크톱 알림을 보여 줄 수도 있습니다. 인박스 상단의 Turn on desktop notifications를 선택하고 브라우저에서 허용하세요. 알림을 누르면 해당 대화가 열립니다. 탭을 닫으면 알림도 멈춥니다.