웹사이트나 앱용 피드백 폼4분 분량
빌드마다 베타 테스터 피드백 모으기
테스터와 빌드별 개인 링크, 두 가지 평점 척도, 빌드마다 나아졌는지 보여 주는 주간 요약을 갖춘 베타 피드백 폼입니다.
각 테스터에게 불투명한 테스터 코드와 빌드 번호가 담긴 개인 링크를 주면, 폼이 묻지 않고도 누가 어느 빌드에 대해 답하는지 압니다. 빌드를 무엇에 썼는지, 안정성과 유용성을 각각 1~5점으로, 그리고 무엇이 막았는지를 물으세요. 그런 다음 개별 답변이 아니라 빌드끼리 비교해 릴리스마다 수치가 움직였는지 보세요.
베타 피드백이 다른 점
공개 피드백은 모르는 사람이 한 번 보냅니다. 베타 피드백은 같은 소수의 사람이 매주 바뀌는 소프트웨어에 대해 반복해서 보냅니다. 답변 하나보다 추세가 중요합니다. 같은 사람들에게 빌드 1.4.0이 1.3.0보다 안정적으로 느껴졌을까요? 그러려면 일반 피드백 폼에 없는 두 가지가 필요합니다. 누가 답했는지, 그리고 어느 빌드를 쓰고 있었는지입니다.
테스터에게 이메일과 버전 번호를 입력하라고 하면 실제로는 실패합니다. 버전을 잊거나, 잘못 입력하거나, "최신"이라고 씁니다. 그래서 둘 다 보내는 링크에 넣고 페이지가 채우게 합니다.
- 새 빌드가 베타 그룹에 배포됩니다.
- 스크립트가 테스터마다 코드와 빌드 번호가 담긴 링크를 하나씩 만들고, 이를 이메일로 보냅니다.
- 테스터가 링크를 엽니다. 페이지가 hidden 테스터와 빌드 필드를 채우고 어느 빌드에 대해 묻는지 보여 줍니다.
- 요약 스크립트가 API로 모든 답변을 읽고 빌드별로 점수의 평균을 냅니다.
- 가장 낮은 점수의 차단 요인이 다음 빌드의 수정 목록이 됩니다.
필드
미리보기
Ikiform의 검증기를 브라우저에서 실행합니다. 아무것도 전송되지 않습니다.
tester와 build는 링크에서 채워지는 hidden 입력입니다. 패턴이 실제 초대에서 오지 않은 값은 모두 거부합니다.| 필드 | 이유 |
|---|---|
tester | 무작위 10자 코드. URL에 이메일을 넣지 않고도 빌드를 넘나들며 한 사람을 추적할 수 있습니다 |
build | 모든 점수를 릴리스와 연결합니다. 패턴은 1.4.0 같은 버전만 받습니다 |
tried | 실제로 무엇을 했는지. 이게 없으면 점수를 해석하기 어렵습니다 |
stability, usefulness | 1~5점 rating 필드 두 개. 잘못된 것을 안정적으로 만든 빌드도 여전히 잘못된 것이므로 따로 둡니다 |
would_miss | 없으면 얼마나 아쉬울지. "매우"의 비율이 오르는 것이 베타가 잘되고 있다는 가장 강한 신호입니다 |
blockers | 선택 입력이며, 수정 목록이 여기서 나옵니다 |
테스터마다 링크 만들기
testers.txt에 테스터 이메일 목록을 일반 텍스트로 두세요. 이 스크립트는 처음 보는 테스터에게 고정 코드를 부여하고, 매핑을 testers.json에 저장하고, 지정한 빌드에 대한 초대 링크를 테스터마다 하나씩 씁니다. testers.json은 {}로 시작하세요.
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을 감사 페이지로 설정하세요. 스크립트를 허용하는 정적 호스트나 빌더라면 어디서든 동작합니다.
<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에 시크릿 키를 넣고 실행하세요.
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)}%`,
})),
);평점은 숫자로 도착하므로 평균을 낼 때 파싱이 필요 없습니다. 각 평균 옆에 테스터 수를 세어 두세요. 답변이 세 개인 빌드는 서른 개인 빌드와 공정하게 비교할 수 없습니다.
작동 확인하기
node links.mjs 1.4.0을 실행하고 링크 하나를 여세요. 폼에 "Feedback for build 1.4.0"이 표시됩니다.- 쿼리 문자열 없이 페이지를 여세요. 초대 메시지만 표시됩니다.
- 답변을 제출한 뒤 브라우저 개발자 도구에서
build를latest로 바꾸고 다시 제출하세요. Ikiform이 "Use a version like 1.4.0"과 함께422로 응답합니다. summary.mjs를 실행하세요. 표에 테스터 한 명과 두 점수가 있는 빌드 1.4.0이 표시됩니다.
주의할 점
- 베타 그룹은 같은 IP를 쓰는 한 사무실에서 테스트하는 경우가 많습니다. Standard 리밋은 이들을 함께 세므로, 이 폼에는 방문자당 분당 20회인 Relaxed를 고르세요.
- TestFlight의 모바일 베타는 이미 테스터가 App Store Connect로 스크린샷 피드백을 보낼 수 있게 해 줍니다. 화면 단위 버그에는 그것을, 주간 질문에는 이 폼을 쓰세요.
- 매주 같은 질문을 하세요. 문구를 바꾸면 빌드 간 비교가 깨집니다.
- 베타가 진행될수록 테스터의 응답이 줄어듭니다. 폼은 2~3분 안에 끝나게 하고, 릴리스 노트에서 이름을 불러 감사를 전하세요.
- Ikiform은 반복 답변을 합치지 않습니다. 테스터가 한 빌드에 두 번 답하면 요약에서는 나중 것을 남기세요.
문제 해결
| 문제 | 해결 방법 |
|---|---|
"Open the link from your invite email"과 함께 422 | tester 값이 패턴에 맞지 않습니다. 대개 이메일 클라이언트에서 링크가 잘렸기 때문입니다. 링크를 다시 보내세요. |
429 rate_limited | 한 방문자가 폼의 레이트 리밋보다 많이 보냈습니다. Standard는 방문자당 분당 5회를 허용합니다. Retry-After만큼 기다리거나, 공유 IP 하나에서 POST하는 폼이라면 Relaxed나 Custom을 고르세요. |
403 origin_not_allowed, 또는 모든 POST가 스팸으로 분류됨 | 페이지의 도메인이 폼의 Allowed domains에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 localhost도 추가하세요. 등록된 도메인의 하위 도메인은 허용됩니다. |
질문
각 테스터에게 무작위 코드를 주고 개인 링크에 넣으세요. 페이지가 이를 hidden 필드에 복사하고, 코드를 이메일과 연결하는 것은 비공개 매핑 파일뿐입니다.
빌드 번호도 링크에 넣고, 패턴으로 검증하고, 답변을 빌드별로 묶으세요. SDK의 submissions.iterate로 요약에 필요한 모든 답변을 읽을 수 있습니다.
빌드를 무엇에 썼는지, 안정성과 유용성을 1~5점으로 어떻게 느꼈는지, 없으면 얼마나 아쉬울지, 그리고 무엇이 막았는지입니다. 매주 같은 질문을 유지하세요.
재현이 필요한 것은 별도의 버그 리포트 폼으로 받으세요. 주간 폼은 빌드의 전반적인 느낌을 위한 것이고, 둘을 섞으면 둘 다 읽기 어려워집니다.
막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.