웹사이트나 앱용 피드백 폼

웹사이트나 앱용 피드백 폼4분 분량

빌드마다 베타 테스터 피드백 모으기

테스터와 빌드별 개인 링크, 두 가지 평점 척도, 빌드마다 나아졌는지 보여 주는 주간 요약을 갖춘 베타 피드백 폼입니다.

Preet SutharIkiform 창업자

각 테스터에게 불투명한 테스터 코드와 빌드 번호가 담긴 개인 링크를 주면, 폼이 묻지 않고도 누가 어느 빌드에 대해 답하는지 압니다. 빌드를 무엇에 썼는지, 안정성과 유용성을 각각 1~5점으로, 그리고 무엇이 막았는지를 물으세요. 그런 다음 개별 답변이 아니라 빌드끼리 비교해 릴리스마다 수치가 움직였는지 보세요.

베타 피드백이 다른 점

공개 피드백은 모르는 사람이 한 번 보냅니다. 베타 피드백은 같은 소수의 사람이 매주 바뀌는 소프트웨어에 대해 반복해서 보냅니다. 답변 하나보다 추세가 중요합니다. 같은 사람들에게 빌드 1.4.0이 1.3.0보다 안정적으로 느껴졌을까요? 그러려면 일반 피드백 폼에 없는 두 가지가 필요합니다. 누가 답했는지, 그리고 어느 빌드를 쓰고 있었는지입니다.

테스터에게 이메일과 버전 번호를 입력하라고 하면 실제로는 실패합니다. 버전을 잊거나, 잘못 입력하거나, "최신"이라고 씁니다. 그래서 둘 다 보내는 링크에 넣고 페이지가 채우게 합니다.

새 빌드에서 다음 빌드까지 이어지는 주간 베타 피드백 루프빌드1.4.0배포베타그룹에게초대이메일테스터마다링크 하나테스터응답코드와빌드 미리채움빌드별요약평균,테스터,아쉬움여부빌드1.5.0주요 차단요인 수정?t=…&build=API새 빌드에서 다음 빌드까지 이어지는 주간 베타 피드백 루프빌드 1.4.0 배포베타 그룹에게초대 이메일테스터마다 링크 하나테스터 응답코드와 빌드 미리 채움빌드별 요약평균, 테스터, 아쉬움 여부빌드 1.5.0주요 차단 요인 수정?t=…&build=API
  1. 새 빌드가 베타 그룹에 배포됩니다.
  2. 스크립트가 테스터마다 코드와 빌드 번호가 담긴 링크를 하나씩 만들고, 이를 이메일로 보냅니다.
  3. 테스터가 링크를 엽니다. 페이지가 hidden 테스터와 빌드 필드를 채우고 어느 빌드에 대해 묻는지 보여 줍니다.
  4. 요약 스크립트가 API로 모든 답변을 읽고 빌드별로 점수의 평균을 냅니다.
  5. 가장 낮은 점수의 차단 요인이 다음 빌드의 수정 목록이 됩니다.
행이 아니라 열을 비교하세요. 테스터 한 명의 나쁜 한 주는 노이즈이고, 그룹 전체의 하락은 회귀입니다.

필드

미리보기

Ikiform의 검증기를 브라우저에서 실행합니다. 아무것도 전송되지 않습니다.

얼마나 안정적이었나요?
얼마나 유용했나요?
더 이상 쓸 수 없게 된다면 어떤 기분일까요?
tester와 build는 링크에서 채워지는 hidden 입력입니다. 패턴이 실제 초대에서 오지 않은 값은 모두 거부합니다.
필드이유
tester무작위 10자 코드. URL에 이메일을 넣지 않고도 빌드를 넘나들며 한 사람을 추적할 수 있습니다
build모든 점수를 릴리스와 연결합니다. 패턴은 1.4.0 같은 버전만 받습니다
tried실제로 무엇을 했는지. 이게 없으면 점수를 해석하기 어렵습니다
stability, usefulness1~5점 rating 필드 두 개. 잘못된 것을 안정적으로 만든 빌드도 여전히 잘못된 것이므로 따로 둡니다
would_miss없으면 얼마나 아쉬울지. "매우"의 비율이 오르는 것이 베타가 잘되고 있다는 가장 강한 신호입니다
blockers선택 입력이며, 수정 목록이 여기서 나옵니다

testers.txt에 테스터 이메일 목록을 일반 텍스트로 두세요. 이 스크립트는 처음 보는 테스터에게 고정 코드를 부여하고, 매핑을 testers.json에 저장하고, 지정한 빌드에 대한 초대 링크를 테스터마다 하나씩 씁니다. testers.json은 {}로 시작하세요.

links.mjs
import { randomBytes } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";

const BUILD = process.argv[2];
if (!/^\d+\.\d+\.\d+$/.test(BUILD ?? "")) throw new Error("Usage: node links.mjs 1.4.0");

const known = JSON.parse(readFileSync("testers.json", "utf8"));
const emails = readFileSync("testers.txt", "utf8").split(/\r?\n/).map((line) => line.trim().toLowerCase()).filter(Boolean);

for (const email of emails) {
  known[email] ??= randomBytes(5).toString("hex");
}
writeFileSync("testers.json", JSON.stringify(known, null, 2));

const rows = emails.map((email) => {
  const link = new URL("https://example.com/beta/feedback");
  link.searchParams.set("t", known[email]);
  link.searchParams.set("build", BUILD);
  return `${email},${link}`;
});
writeFileSync(`invites-${BUILD}.csv`, ["email,link", ...rows].join("\n"));
console.log(`${rows.length} links for build ${BUILD}`);

CSV를 이메일 도구의 메일 머지로 보내거나, 소규모 그룹이라면 링크를 직접 붙여 넣으세요. testers.json은 비공개로 두세요. 코드를 사람과 연결하는 유일한 곳입니다.

피드백 페이지

사이트의 /beta/feedback 페이지에 넣으세요. 링크에서 코드와 빌드를 읽고, 둘이 없으면 폼을 보여 주지 않으며, 일반적인 방식으로 POST합니다. Redirect after submit을 감사 페이지로 설정하세요. 스크립트를 허용하는 정적 호스트나 빌더라면 어디서든 동작합니다.

beta/feedback.html
<p id="beta-missing" hidden>This page needs the link from your invite email.</p>

<form id="beta" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" hidden>
  <h2>Feedback for build <span id="beta-build"></span></h2>
  <input type="hidden" name="tester" />
  <input type="hidden" name="build" />
  <label for="tried">What did you use this build for?</label>
  <textarea id="tried" name="tried" rows="4" required minlength="15"></textarea>
  <fieldset>
    <legend>How stable did it feel?</legend>
    <label><input type="radio" name="stability" value="1" required /> 1, it crashed a lot</label>
    <label><input type="radio" name="stability" value="2" /> 2</label>
    <label><input type="radio" name="stability" value="3" /> 3</label>
    <label><input type="radio" name="stability" value="4" /> 4</label>
    <label><input type="radio" name="stability" value="5" /> 5, no problems</label>
  </fieldset>
  <fieldset>
    <legend>How useful was it?</legend>
    <label><input type="radio" name="usefulness" value="1" required /> 1, not at all</label>
    <label><input type="radio" name="usefulness" value="2" /> 2</label>
    <label><input type="radio" name="usefulness" value="3" /> 3</label>
    <label><input type="radio" name="usefulness" value="4" /> 4</label>
    <label><input type="radio" name="usefulness" value="5" /> 5, I rely on it</label>
  </fieldset>
  <fieldset>
    <legend>If you could no longer use it, how would you feel?</legend>
    <label><input type="radio" name="would_miss" value="very" required /> Very disappointed</label>
    <label><input type="radio" name="would_miss" value="somewhat" /> Somewhat disappointed</label>
    <label><input type="radio" name="would_miss" value="not" /> Not disappointed</label>
  </fieldset>
  <label for="blockers">Anything that stopped you?</label>
  <textarea id="blockers" name="blockers" rows="3"></textarea>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send this week's feedback</button>
</form>

<script>
  const params = new URLSearchParams(location.search);
  const tester = params.get("t") ?? "";
  const build = params.get("build") ?? "";
  const form = document.getElementById("beta");
  if (/^[a-z0-9]{10}$/.test(tester) && /^\d+\.\d+\.\d+$/.test(build)) {
    form.elements.tester.value = tester;
    form.elements.build.value = build;
    document.getElementById("beta-build").textContent = build;
    form.hidden = false;
  } else {
    document.getElementById("beta-missing").hidden = false;
  }
</script>
테스터의 링크를 가진 사람은 누구나 그 테스터로 답할 수 있습니다. 베타에서는 받아들일 만합니다. 코드는 무작위이고, 그룹은 작고, 이상한 답변은 눈에 띕니다. 코드를 무언가에 대한 접근 권한 부여에 쓰지 마세요.

빌드 비교하기

대시보드는 개별 답변을 보여 줍니다. 빌드를 비교하려면 SDK로 전부 읽고 build로 묶으세요. IKIFORM_SECRET_KEY에 시크릿 키를 넣고 실행하세요.

summary.mjs
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY });
const builds = new Map();

for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx", status: "received" })) {
  const { build, tester, stability, usefulness, would_miss } = submission.fields;
  const row = builds.get(build) ?? { testers: new Set(), stability: 0, usefulness: 0, very: 0, answers: 0 };
  row.testers.add(tester);
  row.stability += stability;
  row.usefulness += usefulness;
  row.very += would_miss === "very" ? 1 : 0;
  row.answers += 1;
  builds.set(build, row);
}

console.table(
  [...builds].map(([build, row]) => ({
    build,
    testers: row.testers.size,
    stability: (row.stability / row.answers).toFixed(1),
    usefulness: (row.usefulness / row.answers).toFixed(1),
    "very disappointed": `${Math.round((row.very / row.answers) * 100)}%`,
  })),
);

평점은 숫자로 도착하므로 평균을 낼 때 파싱이 필요 없습니다. 각 평균 옆에 테스터 수를 세어 두세요. 답변이 세 개인 빌드는 서른 개인 빌드와 공정하게 비교할 수 없습니다.

작동 확인하기

  1. node links.mjs 1.4.0을 실행하고 링크 하나를 여세요. 폼에 "Feedback for build 1.4.0"이 표시됩니다.
  2. 쿼리 문자열 없이 페이지를 여세요. 초대 메시지만 표시됩니다.
  3. 답변을 제출한 뒤 브라우저 개발자 도구에서 build를 latest로 바꾸고 다시 제출하세요. Ikiform이 "Use a version like 1.4.0"과 함께 422로 응답합니다.
  4. summary.mjs를 실행하세요. 표에 테스터 한 명과 두 점수가 있는 빌드 1.4.0이 표시됩니다.

주의할 점

  • 베타 그룹은 같은 IP를 쓰는 한 사무실에서 테스트하는 경우가 많습니다. Standard 리밋은 이들을 함께 세므로, 이 폼에는 방문자당 분당 20회인 Relaxed를 고르세요.
  • TestFlight의 모바일 베타는 이미 테스터가 App Store Connect로 스크린샷 피드백을 보낼 수 있게 해 줍니다. 화면 단위 버그에는 그것을, 주간 질문에는 이 폼을 쓰세요.
  • 매주 같은 질문을 하세요. 문구를 바꾸면 빌드 간 비교가 깨집니다.
  • 베타가 진행될수록 테스터의 응답이 줄어듭니다. 폼은 2~3분 안에 끝나게 하고, 릴리스 노트에서 이름을 불러 감사를 전하세요.
  • Ikiform은 반복 답변을 합치지 않습니다. 테스터가 한 빌드에 두 번 답하면 요약에서는 나중 것을 남기세요.

문제 해결

문제해결 방법
"Open the link from your invite email"과 함께 422tester 값이 패턴에 맞지 않습니다. 대개 이메일 클라이언트에서 링크가 잘렸기 때문입니다. 링크를 다시 보내세요.
429 rate_limited한 방문자가 폼의 레이트 리밋보다 많이 보냈습니다. Standard는 방문자당 분당 5회를 허용합니다. Retry-After만큼 기다리거나, 공유 IP 하나에서 POST하는 폼이라면 Relaxed나 Custom을 고르세요.
403 origin_not_allowed, 또는 모든 POST가 스팸으로 분류됨페이지의 도메인이 폼의 Allowed domains에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 localhost도 추가하세요. 등록된 도메인의 하위 도메인은 허용됩니다.

질문

막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.