# 셀 수 있고, 순위를 매기고, 답할 수 있는 기능 요청 모으기

> 해결책 대신 문제를 묻고, 요청을 테마별로 저장하고, 행이 아닌 사람 수를 세고, 출시되면 이메일을 보내는 기능 요청 폼입니다.

2026-10-25 게시 · https://ikiform.com/ko/guides/feedback-form/feature-request

원하는 기능이 아니라 무엇을 하려는지, 지금은 어떻게 하는지를 물으세요. 출시되면 알릴 수 있도록 이메일은 필수로 받으세요. 각 요청을 웹훅으로 테이블에 저장하고, 요청에 테마를 붙이고, 막혀 있는 서로 다른 사람의 수로 테마의 순위를 매기세요. 테마가 출시되면 요청한 모두에게 이메일을 보내세요.

## 기능 요청이 다른 점

버그 리포트는 고치면 끝납니다. 기능 요청은 데이터 포인트입니다. 같은 빈틈을 저마다 다른 말로 설명하는 여러 사람 중 한 명입니다. 여기서 세 가지가 따라 나옵니다.

- **사람들은 해결책을 말합니다.** "CSV 버튼을 추가해 주세요"는 "회계사가 지난달 인보이스를 필요로 해요"라는 뜻일 수 있습니다. 하려는 일을 물으면 요청받은 것보다 나은 해결책을 고를 수 있습니다.
- **같은 요구가 여러 번 들어옵니다.** Ikiform은 모든 POST를 저장하고 반복을 합치지 않으며, 두 사람이 같은 말을 쓰는 경우도 드뭅니다. 요청은 직접 테마별로 묶고 서로 다른 사람 수를 세세요.
- **가치는 후속 연락에 있습니다.** "요청하신 기능을 만들었습니다"라고 알려 주는 것이 애초에 물어보는 가장 좋은 이유입니다. 그러려면 모든 요청에 이메일이 있어야 하므로 이 폼은 이메일을 필수로 받습니다.

다이어그램: 기능 요청이 집계된 테마와 출시 이메일이 되는 과정

1. 누군가 하려는 일을 설명하고, 영역과 영향도를 고르고, 이메일을 남깁니다.
2. 웹훅 릴레이가 요청을 데이터베이스에 제출 ID당 한 번 삽입합니다.
3. 매주 새 요청에 직접, 또는 패턴으로 테마를 붙입니다.
4. 기존 테마에 맞으면 그 테마를 원하는 사람이 한 명 늘어납니다.
5. 맞지 않으면 새 테마가 시작됩니다.
6. 테마가 출시되면 그 테마의 모든 이메일을 골라 메시지 하나를 보냅니다.

순위는 원시 요청 수가 아니라 테마별 사람 수입니다. 한 고객이 다섯 번 요청해도 한 명입니다.

## 문제를 묻기

폼: Feature request

- **무엇을 하려고 하나요?** (`problem`, textarea, 필수)
- **지금은 어떻게 하고 있나요?** (`workaround`, textarea)
- **제품의 어느 부분인가요?** (`area`, select, 필수)
- **얼마나 중요한가요?** (`impact`, radio, 필수)
- **이메일** (`email`, email, 필수)

`problem`은 최소 30자라서 "다크 모드 좀요"가 이유가 담긴 문장으로 바뀝니다.

| 필드 | 필요한 이유 |
| --- | --- |
| `problem` | 본인의 말로 쓴 하려는 일. 단연 가장 유용한 필드 |
| `workaround` | 얼마나 불편한지 알려 줍니다. "스프레드시트에 손으로 복사해요"는 강한 신호입니다 |
| `area` | 대략적인 1차 분류. 전체가 아니라 결제 안에서 태그를 붙일 수 있습니다 |
| `impact` | 막힌 고객과 궁금한 고객을 구분합니다. 사람들이 판단할 수 있는 결과로 표현했습니다 |
| `email` | 필수. 없으면 결과를 알리거나 질문할 수 없습니다 |

우선순위나 투표 필드는 빼세요. 누구나 자기 요청을 높은 우선순위로 표시하며, 테마별 사람 수가 어떤 버튼보다 나은 투표입니다.

## 폼

이 폼은 스크립트가 필요 없습니다. POST하고 리디렉션됩니다. **Redirect after submit**을 다음에 무슨 일이 일어나는지 알려 주는 페이지로 설정하세요. 프레임워크에서 페이지에 머무는 버전이 필요하면 필러의 [폼 코드](https://ikiform.com/ko/guides/feedback-form.md#form)를 가져와 이 필드들로 바꾸세요.

`feature-request.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="problem">What are you trying to do?</label>
  <textarea id="problem" name="problem" rows="5" required minlength="30"
    placeholder="Send last month's invoices to our accountant in one go"></textarea>
  <label for="workaround">How do you do it today?</label>
  <textarea id="workaround" name="workaround" rows="3"></textarea>
  <label for="area">Which part of the product?</label>
  <select id="area" name="area" required>
    <option value="billing">Billing and invoices</option>
    <option value="reports">Reports</option>
    <option value="team">Team and permissions</option>
    <option value="integrations">Integrations and API</option>
    <option value="other">Something else</option>
  </select>
  <fieldset>
    <legend>How much does it matter?</legend>
    <label><input type="radio" name="impact" value="blocking" required /> It stops us using the product for something</label>
    <label><input type="radio" name="impact" value="slow" /> It costs us time every week</label>
    <label><input type="radio" name="impact" value="nice" /> It would be nice</label>
  </fieldset>
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send request</button>
</form>
```

## 웹훅으로 요청 저장하기

처음 수십 건까지는 대시보드와 CSV 내보내기로 충분합니다. 매주 테마를 붙이고 사람 수를 세기 시작하면 테이블이 더 편합니다. Postgres에 테이블을 만드세요.

`schema.sql`:

```text
create table feature_requests (
  submission_id text primary key,
  area text not null,
  impact text not null,
  problem text not null,
  workaround text,
  email text not null,
  theme text,
  created_at timestamptz not null
);
```

그런 다음 Ikiform에서 이 라우트를 가리키는 [웹훅](https://ikiform.com/ko/docs/webhooks.md) 엔드포인트를 추가하세요. `pg` 패키지만 있으면 어느 Node 호스트에서나 동작하며, [자체 서버](https://ikiform.com/blog/form-submissions-to-your-own-server.md)에서 같은 패턴을 더 자세히 다룹니다.

`app/api/feature-requests/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const INSERT = `insert into feature_requests (submission_id, area, impact, problem, workaround, email, created_at)
values ($1, $2, $3, $4, $5, lower($6), $7)
on conflict (submission_id) do nothing`;

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const { problem, workaround, area, impact, email } = event.data.fields;
  await pool.query(INSERT, [
    event.data.submission_id,
    area,
    impact,
    problem,
    workaround ?? null,
    email,
    event.data.created_at,
  ]);
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook`은 엔드포인트의 `whsec_` 시크릿으로 서명되지 않은 요청을 모두 거부합니다.
- `on conflict (submission_id) do nothing` 덕분에 재시도해도 안전합니다. Ikiform은 실패한 전달을 같은 제출로 다시 보내고, 두 번째 insert는 아무 일도 하지 않습니다.
- 데이터베이스 오류가 나면 예외가 발생하고 라우트가 `500`으로 응답하며, Ikiform이 나중에 재시도합니다. 데이터베이스가 재시작되는 동안에도 요청을 잃지 않습니다.
- 이메일은 들어올 때 소문자로 바꾸므로 서로 다른 사람 수를 셀 수 있습니다.

> Ikiform은 같은 이메일의 반복 제출을 합치거나 거부하지 않습니다. 24시간 안의 동일한 반복은 스팸 점수만 올립니다. 중복 제거는 목록이 있는 곳이나 내보낼 때 하세요.

## 태그와 순위

### 새 요청에 태그 붙이기

일주일에 한 번 태그 없는 행을 읽고 각각에 테마를 붙이세요. 뻔한 것은 패턴으로 처리하고 나머지는 직접 읽으세요. 흥미로운 요청은 패턴에 맞지 않습니다.

`tag.sql`:

```text
update feature_requests
set theme = 'invoice-export'
where theme is null
  and area = 'billing'
  and problem ilike any (array['%export%', '%accountant%', '%download%']);
```

### 테마 순위 매기기

아래 쿼리는 막혔다고 답한 요청 수, 그다음 요청한 서로 다른 사람 수로 테마의 순위를 매깁니다.

`ranking.sql`:

```text
select coalesce(theme, 'untagged') as theme,
       count(*) as requests,
       count(distinct email) as people,
       count(*) filter (where impact = 'blocking') as blocking
from feature_requests
group by 1
order by blocking desc, people desc;
```

1. 무엇이든 설계하기 전에 상위 세 테마의 `problem` 텍스트를 전부 읽으세요.
2. 우회 방법을 보세요. 작은 변경 하나로 가장 큰 불편이 사라지는 경우가 많습니다.
3. 순위는 계획과 견주어 판단하세요. 순위는 수요를 보여 줄 뿐, 무엇을 만드는 게 맞는지 알려 주지 않습니다.

## 결과 알리기

테마가 출시되면 그것을 요청한 모든 사람을 가져오세요.

`notify.sql`:

```text
select distinct email
from feature_requests
where theme = 'invoice-export';
```

이메일 도구나 자신의 받은편지함에서 무엇이 출시되었는지 말하고 링크를 담은 짧은 이메일 한 통을 보내세요. Ikiform은 폼을 작성한 사람에게 이메일을 보내지 않습니다. 알림 이메일은 나와 팀에게만 가므로 자동 응답, 확인 이메일, 더블 옵트인이 없습니다. 그런 이메일은 [웹훅](https://ikiform.com/ko/docs/webhooks.md)으로 트리거해 이메일 도구에서 보내세요. 여기서도 마찬가지로 출시 이메일은 Ikiform이 아니라 내가 보냅니다.

## 작동 확인하기

1. 10자짜리 문제로 요청을 제출하세요. Ikiform이 `problem`을 지목하는 `422`로 응답합니다.
2. 실제 요청을 제출하세요. **Submissions**와 `feature_requests`의 행으로 나타납니다.
3. **Webhooks**에서 전달을 다시 보내세요. 테이블에는 그 제출의 행이 여전히 하나뿐입니다.
4. 같은 이메일을 대소문자만 바꿔 두 번 제출하세요. 순위 쿼리는 한 명으로 셉니다.

## 주의할 점

- Canny 같은 공개 피드백 보드는 투표와 공개 상태 페이지를 더합니다. 고객끼리 서로의 요청을 보길 원한다면 그쪽이 더 나은 도구이고, 이 구성은 요청을 비공개로 유지합니다.
- 링크가 여러 개 담긴 요청은 스팸으로 점수가 매겨질 수 있습니다. 양이 적은 동안에는 매주 **Spam** 필터를 확인하세요.
- 나중에 `area` 옵션을 추가하면 먼저 정의에 추가하세요. 목록에 없는 값은 거부됩니다.
- 필요한 것만 저장하세요. 이메일이 담긴 요청 테이블은 개인정보입니다. 개인정보 처리방침에 밝히고 더 이상 필요 없는 행은 삭제하세요.

## 문제 해결

| 문제 | 해결 방법 |
| --- | --- |
| 테이블에 행이 없음 | **Webhooks**에서 엔드포인트를 여세요. `400`은 시크릿이 틀렸다는 뜻이고, 데이터베이스 오류와 함께 `500`이면 insert가 실패해 Ikiform이 재시도 중이라는 뜻입니다. |
| `422 validation_failed` | POST가 폼의 필드 정의와 맞지 않습니다. `error.fields`에 각 필드와 이유가 나옵니다. 입력의 `name`을 필드 ID와 맞추세요. |
| 제출은 대시보드에 있는데 이메일이나 웹훅이 오지 않았습니다 | 스팸으로 저장되었습니다. 스팸은 아무것도 보내지 않습니다. **Spam** 필터에서 열어 이유를 확인하고, 스팸이 아님으로 표시하면 이메일과 웹훅이 발송됩니다. |

## 질문

### 기능 요청 폼에서는 무엇을 물어야 하나요?

무엇을 하려는지, 지금은 어떻게 하는지, 제품의 어느 부분과 관련되는지, 얼마나 중요한지, 그리고 이메일입니다. 기능이 아니라 문제를 물으면 더 나은 해결책을 찾을 여지가 생깁니다.

### 중복된 기능 요청은 어떻게 처리하나요?

중복은 당연히 생긴다고 보고 행이 아니라 사람을 세세요. 각 요청에 테마를 붙이고 서로 다른 이메일 수로 테마의 순위를 매기세요. Ikiform은 모든 제출을 저장하고 반복을 합치지 않습니다.

### 사용자가 기능 요청에 투표할 수 있나요?

폼만으로는 안 됩니다. 폼은 요청을 비공개로 유지하고, Canny 같은 공개 보드가 투표를 더합니다. 테마별로 서로 다른 사람 수를 세면 비공개로 같은 효과를 얻습니다.

### 요청한 기능이 출시되면 어떻게 알리나요?

각 요청과 함께 이메일을 보관하고, 출시된 테마의 모든 이메일을 골라 이메일 도구에서 메시지 하나를 보내세요. Ikiform이 제출자에게 직접 이메일을 보내지는 않습니다.

## 가이드 더 보기

- [웹사이트나 앱용 피드백 폼](https://ikiform.com/ko/guides/feedback-form.md): 어떤 페이지나 앱 안에든 피드백 폼을 둡니다. 평점, 코멘트, 보낸 페이지, 스팸 필터링, 낮은 점수 알림까지 다룹니다.
- [버그 리포트 폼](https://ikiform.com/ko/guides/feedback-form/bug-report.md): 재현 단계와 심각도는 사람에게 묻고, URL, 브라우저, 뷰포트, 최근 오류는 페이지가 알아서 추가하는 버그 리포트 폼입니다.
- [베타 테스터 피드백](https://ikiform.com/ko/guides/feedback-form/beta-tester-feedback.md): 테스터와 빌드별 개인 링크, 두 가지 평점 척도, 빌드마다 나아졌는지 보여 주는 주간 요약을 갖춘 베타 피드백 폼입니다.
- [NPS 설문](https://ikiform.com/ko/guides/feedback-form/nps-survey.md): 이메일에 0~10 점수를 넣고, 점수에 따라 바뀌는 후속 질문을 내 페이지에서 묻고, 스크립트로 NPS를 계산하는 NPS 설문을 운영합니다.
- [React 피드백 위젯](https://ikiform.com/ko/guides/feedback-form/react-feedback-widget.md): 작은 패널을 열고, 답변마다 화면, 앱 버전, 계정을 함께 보내고, 작성 중인 내용을 잃지 않는 React 앱용 플로팅 피드백 버튼입니다.
- [해지 설문](https://ikiform.com/ko/guides/feedback-form/cancellation-survey.md): SaaS 해지 흐름 안에서 고객이 해지하는 이유를 묻고, 건너뛰기는 클릭 한 번으로 두고, 이유를 Stripe와 Ikiform에 저장하며, 해지를 절대 지연시키지 않습니다.
