# product-copy — 제품 UI 카피 4단계에서 버튼·폼 라벨·상태 메시지·오류·빈 상태·토글처럼 제품이 스스로 말하는 문구를 쓸 때 읽는다. 5단계 프리플라이트의 카피 검토에서 다시 연다. 브랜드 차별성을 주장하는 마케팅 헤드라인·브랜드 약속의 슬롭 지문(`so you can…` 테스트, 구문·어휘 지문)은 이 문서가 아니라 [antipatterns.md](antipatterns.md) 6절이 정본이다. 두 문서는 같은 대상을 다른 각도에서 본다 — 헤드라인이 버튼·토글 같은 제품 UI 라벨을 겸하면 두 문서를 모두 확인한다. 라벨은 셋 중 하나다. **하드 게이트**는 위반하면 통과시키지 않는다(WCAG 등 규범·기능 요건). **프로젝트 계약**은 이 프로젝트가 그 조건을 선택했을 때 지키는 약속이다(용어 체계, 어투 통일 같은 자체 결정). **관찰 후보**는 시작값이고 실제 렌더·독자 반응에서 확인해 조정한다. 절 제목에 주된 위계를 붙이고, 절 안에서 다른 위계가 섞이면 문장 앞에 굵게 라벨을 따로 단다. ## 이 문서를 읽는 법 | 상황 | 읽을 곳 | |---|---| | 새 카피를 쓰기 전에 뭘 먼저 봐야 하나 | §1 | | 같은 개념을 화면마다 다르게 부르고 있는 것 같다 | §2 | | 오류·삭제 확인·결제 화면의 톤이 다 같아 보인다 | §3 | | 한 화면에 해요체와 합니다체가 섞인다 | §4 | | 버튼 라벨을 어떻게 짓나 | §5 | | 오류 메시지를 쓴다 | §6 | | 빈 상태·빈 목록 문구를 쓴다 | §7 | | 내부 개발 용어가 라벨에 새어 나온 것 같다 | §8 | | `{name}을(를) 삭제했습니다` 같은 변수 삽입 문장 | §9 | | 토글·링크 텍스트·placeholder | §10 | | 물음표·느낌표·줄임표를 언제 쓰나 | §11 | | 영문 라벨의 대소문자 규칙 | §12 | | 실제 서비스 사례가 궁금하다 | §13 | | 카피 findings를 어떻게 검증·보고하나 | §14 | | 감사 직전 | §15 | --- ## 0. 언제·범위 이 문서는 4단계에서 폼·상태·오류·빈 상태·토글처럼 **제품이 스스로 말하는 문구**를 쓸 때 연다. 5단계 프리플라이트에서 카피만 따로 다시 훑을 때도 연다. 링크 텍스트·placeholder의 접근성 배선(스크린리더 순서, `aria-describedby`, 포커스 이동)은 이 문서가 아니라 [accessibility.md](accessibility.md)가 정본이며, 이 문서는 그 위에 얹히는 **문구 내용**만 다룬다. --- ## 1. 기존 보이스·용어 확인 — 프로젝트 계약 새 카피를 쓰기 전에 프로젝트에 이미 있는 카피를 읽는다. `design.md`, 기존 화면의 라벨·토스트·오류 문구, 저장소에 남은 스타일 가이드를 먼저 훑어 제품이 이미 쓰고 있는 용어·어투·로케일 관행을 파악한다[SKILL-BETTER-WRITING]. SKILL.md의 우선순위 원칙(사용자 지시 → design.md → 기존 토큰·코드 → designpaca 기본값)이 토큰·코드에 대해 요구하는 것과 같은 태도를 카피에도 적용한다 — 새로 지어내기 전에 이미 있는 답을 먼저 찾는다. --- ## 2. 용어 일관성 — 프로젝트 계약 한 개념에는 한 용어만 쓴다. 메뉴에서 `보관함`이라고 부른 것을 토스트에서 `저장소로 이동`이라고 부르면, 사용자는 같은 일을 두 번 배워야 한다[SKILL-BETTER-WRITING]. **grep 방법** 1. 화면에 쓰인 명사·동사를 후보로 뽑는다(예: `삭제`/`제거`/`지우기`, `보관`/`저장소`/`아카이브`). 2. 저장소 전체에서 각 후보를 `grep`해 실제로 몇 군데서 어떤 말로 쓰이는지 센다. 3. 가장 많이 쓰인 쪽을 기준으로 정하거나, 사용자에게 더 명확한 쪽을 고른다. 어느 쪽이든 design.md에 기록한다. 4. 소수파 표현을 전부 grep으로 찾아 바꾼다. 코드 식별자(`archiveItem` 같은 함수명)까지 바꾸라는 뜻은 아니다 — 사용자가 보는 문자열만 대상이다. --- ## 3. 톤과 위험 매트릭스 — 관찰 후보 모든 문구를 같은 톤으로 쓰면, 정말 조심해야 하는 순간(삭제·결제)도 일상 안내와 구분되지 않는다. 상황의 위험 수위에 따라 톤 강도를 다르게 가져간다[SKILL-BETTER-WRITING]. | 위험 수위 | 상황 예 | 톤 | 이유 | |---|---|---|---| | 일상 | 목록 화면, 설정, 일반 안내 문구 | 중립·간결 | 매번 감정을 실으면 정말 중요한 순간의 톤이 묻힌다 | | 주의 | 로그아웃, 구독 변경, 여러 항목 일괄 처리 | 신중·결과를 분명히 말함 | 다음에 무슨 일이 벌어지는지 오해하면 안 된다 | | 파괴 | 삭제 확인, 탈퇴, 결제 취소 | 차분·군더더기 없음(장난기·이모지 배제) | 가벼운 톤은 행동의 무게와 어긋난다 | | 오류 | 폼 검증 실패, 저장 실패, 네트워크 오류 | 담백·비난하지 않음 | §6 참고 — 과한 사과·느낌표는 문제를 가리지 않는다 | | 금전·보안 | 결제, 개인정보 변경, 본인 인증 | 진지·조건을 흐리지 않고 명시 | 오해가 실제 손실로 이어질 수 있다 | 이 위험 수위는 0단계 인터뷰에서 정한 브리프의 톤 프리셋([brief-interview.md](brief-interview.md))과 다른 층이다 — 브리프의 톤이 전체 브랜드 인상을 정하고, 이 표는 그 톤 안에서 상황별 강도를 조절한다. 장난스러운 브랜드 톤이라도 결제·보안 화면까지 그 장난기를 그대로 옮기지 않는다. --- ## 4. 어투 일관성 — 프로젝트 계약 한 제품 안에서 해요체(`~해요`, `~돼요`)와 합니다체(`~합니다`, `~됩니다`)를 섞으면 어투가 흔들려 보인다. 어느 쪽을 쓸지는 브랜드 톤에 따라 프로젝트가 정하고, 정했으면 화면 전체에서 지킨다. 이 규칙의 근거는 1차 출처로 확인되지 않았다 — 실무에서 두 어체를 상황별로 섞어 쓰는 사례가 보고되긴 하나, 그 출처가 1차 공식 자료가 아니라서 이 문서는 규범이 아니라 **프로젝트 계약**으로만 둔다. 브리프나 기존 화면이 이미 특정 어체를 쓰고 있으면 그것을 따르고(§1), 새로 정해야 하면 0단계 인터뷰의 톤 질문과 함께 확정한다. 확정한 뒤에는 §2와 같은 방법으로 grep해 혼용된 곳을 찾는다. --- ## 5. 버튼 레이블 — 프로젝트 계약 행정안전부 KRDS(공공 디자인 시스템) 지침은 버튼 텍스트를 원칙적으로 동사형으로 쓰도록 규정한다. `완료`·`닫기`·`취소`·`추가`·`삭제`처럼 이미 관용적으로 쓰이는 명사형은 예외다[KO-KRDS]. 명사·형용사 라벨(`저장 버튼입니다` 같은 서술이 아니라 그냥 `저장 완료` 같은 애매한 상태명)은 버튼을 눌렀을 때 무슨 일이 일어나는지 예측하기 어렵게 만든다. - **결과를 말한다.** 버튼을 누르면 정확히 무슨 일이 일어나는지 동사구로 쓴다 — `제출` 대신 `변경사항 저장`, `확인` 대신 `계정 삭제`[SKILL-FRONTEND-DESIGN]. - **확인 대화상자 버튼은 결과를 반복한다.** `이 프로젝트를 삭제할까요?`라는 대화상자의 버튼은 `확인`/`취소`가 아니라 `삭제`/`취소`로 쓴다. `예`/`아니오`로 두면 사용자가 방금 읽은 질문을 다시 떠올려야 동의 대상을 알 수 있다[SKILL-BETTER-WRITING]. - **같은 행동은 버튼 → 확인 → 토스트까지 같은 이름을 쓴다.** `게시` 버튼을 누르면 `게시됨` 토스트를 띄운다. 버튼은 `게시`인데 토스트가 `발행을 완료했습니다`로 바뀌면 같은 행동인지 다시 확인해야 한다[SKILL-FRONTEND-DESIGN]. - **다단계 플로우의 어휘를 한 체계로 고정한다.** 진입은 `시작하기`, 진행은 `계속` 또는 `다음` 중 하나만(둘을 같은 플로우에서 섞지 않는다), 종료는 `완료`처럼 각 단계 역할에 한 단어만 배정하고 프로젝트 전체에서 유지한다[SKILL-BETTER-WRITING]. --- ## 6. 오류 문구 — 프로젝트 계약(내용) / 관찰 후보(어휘) 오류 상태가 **존재**해야 한다는 요구는 [preflight.md](preflight.md) §4-1의 상태 완결성 검사가 이미 다룬다. 여기서는 그 오류 문구가 실제로 무엇을 말해야 하는지를 다룬다. 화면에서 문구가 나타나는 위치·타이밍(실시간 검증인지 제출 시인지)·`aria-live`·포커스 이동 같은 배선은 [accessibility.md](accessibility.md) §6(타이밍·포커스 이동)·§7(상태 알림)이 정본이다. **프로젝트 계약 — 오류 문구는 항상 두 가지를 함께 말한다.** 무엇이 잘못됐는지, 그리고 어떻게 고치는지다. 하나만 있으면 사용자는 다음 행동을 스스로 추측해야 한다[SKILL-BETTER-WRITING]. **하드 게이트 — 입력 오류가 감지되고 고칠 방법을 알고 있으면 그 방법을 제시해야 한다**(WCAG 3.3.3 Error Suggestion, AA)[WCAG-22]. 그 밖의 오류 문구 기준(어휘·톤·비난 주어 회피 등)은 기존대로 프로젝트 계약이다. **관찰 후보 — 어휘 선택.** 비난하는 주어를 쓰지 않는다(`당신이 잘못 입력했습니다` 대신 `형식이 올바르지 않습니다`처럼 상황을 주어로 둔다). 과한 사과와 느낌표를 쓰지 않는다 — §3의 `오류` 톤과 같은 이유다. | 상황 | 나쁜 예 | 좋은 예 | 왜 | |---|---|---|---| | 필수 입력 누락 | 오류가 발생했습니다 | 이메일을 입력해 주세요 | 무엇이 비었는지 말하지 않음 → 말함 | | 형식 오류 | 잘못된 값입니다 | 전화번호는 숫자만 10~11자리로 입력해 주세요 | 고치는 법이 없음 → 있음 | | 저장 실패 | 죄송합니다ㅠㅠ 문제가 생겼어요! | 저장하지 못했습니다. 다시 시도해 주세요 | 과한 사과·느낌표 → 담백하게, 다음 행동 제시 | 같은 오류가 반복해서 발생한다면 문구를 더 다듬기보다 왜 반복되는지(입력 제약, 폼 흐름)를 4단계 인터랙션 설계에서 다시 본다. 이 문서는 문구 내용만 다루며, 인터랙션 재설계는 이 문서의 범위 밖이다. --- ## 7. 빈 상태 — 프로젝트 계약(내용) / 관찰 후보(어휘) **프로젝트 계약 — 빈 상태는 세 가지를 한 번에 말한다.** 무엇이 비었는지(정체성), 왜 비었는지, 다음에 무엇을 할 수 있는지다. `항목이 없습니다` 한 줄만 있으면 사용자는 이 화면이 고장인지 정상인지부터 판단해야 한다[SKILL-BETTER-WRITING]. **관찰 후보 — 검색·필터 빈 상태는 사용자가 입력한 조건을 그대로 보여준다.** `검색 결과가 없습니다`보다 `'디자인'에 대한 검색 결과가 없습니다`가 사용자의 입력을 그대로 확인해 주고, 이어서 탈출구(필터 초기화, 다른 검색어 제안)를 함께 둔다. | 상황 | 나쁜 예 | 좋은 예 | |---|---|---| | 첫 사용, 데이터 없음 | 항목이 없습니다 | 아직 등록한 프로젝트가 없습니다. 새 프로젝트를 만들어 보세요 | | 검색 결과 없음 | 검색 결과가 없습니다 | `'디자인'`에 대한 검색 결과가 없습니다. 다른 검색어를 시도하거나 필터를 초기화해 보세요 | | 필터로 전부 걸러짐 | 결과 없음 | 적용한 필터와 일치하는 항목이 없습니다. 필터 초기화 | 도움말 링크 같은 지속적인 정보를 빈 상태에만 있게 두지 않는다 — 데이터가 채워진 뒤에도 같은 도움말을 찾을 수 있어야 한다. --- ## 8. 평이한 말·기기 동사·독자 지칭·내부 용어 — 관찰 후보 - **최종 사용자 어휘로 쓴다.** 시스템 구현 방식이 아니라 사용자가 실제로 하는 일로 이름 붙인다 — `webhook`이 아니라 `알림`, `payload`가 아니라 `보낸 내용`. 이 검사는 [antipatterns.md](antipatterns.md) §6(진단)·§9(grep)의 내부 구현 용어 grep 후보(`webhook`·`config`·`payload`·`schema`)와 같은 검사이며, 여기서는 화면에 노출되는 **모든** 라벨·설정 이름에 같은 기준을 적용한다[SKILL-FRONTEND-DESIGN]. - **입력 방식에 맞는 동사를 쓴다.** 터치 인터페이스 안내에 `클릭`을, 마우스 중심 안내에 `탭`을 쓰지 않는다. 입력 방식을 특정할 수 없는 라벨(아이콘 버튼 등)은 동사 없는 명사형도 대안이다[SKILL-BETTER-WRITING]. - **독자 지칭과 소유격을 절제한다.** 안내문은 한국어의 주어 생략을 살려 `-하세요`체로 직접 쓰는 편이 자연스럽다. 오류 문구에서 `저희가`·`우리가` 같은 서비스 쪽 주어를 남용하지 않는다(§6의 `불러오지 못했습니다`처럼 상황 주어가 더 담백하다). 소유격도 필요할 때만 쓴다 — `내 즐겨찾기`가 다른 사용자의 즐겨찾기와 헷갈릴 우려가 없으면 `즐겨찾기`로 충분하다[SKILL-BETTER-WRITING]. - **특정 집단을 전제하는 표현을 쓰지 않는다.** 성별·연령 등을 전제한 예시 문구나 관용구는 다른 독자를 배제할 수 있다. --- ## 9. 문장 조각 조립 금지와 한국어 조사 처리 — 관찰 후보 **관찰 후보(엔지니어링 관행).** 변수 주변에 고정 문구를 이어 붙여 문장을 조립하지 않는다. `${count} 개의 항목이 삭제되었습니다`처럼 숫자·이름 앞뒤에 고정된 조각을 붙이는 방식은 그 변수가 조사·복수형·문장 구조에 영향을 주는 언어에서 깨지기 쉽다[SKILL-BETTER-WRITING]. 완성된 문장을 통째로 템플릿화하거나, 조사가 필요 없는 문장 구조로 바꾼다. 한국어에서는 조사(을/를, 이/가, 은/는, 으로/로)가 변수의 마지막 글자 받침 유무에 따라 갈린다는 문제가 하나 더 있다. 완성형 한글 음절(유니코드 `U+AC00`~`U+D7A3`, 11,172자)은 마지막 글자의 코드값에서 `0xAC00`을 뺀 값을 28로 나눈 나머지가 0이면 받침이 없다. ```js // 받침 유무로 조사 짝을 고른다 function hasBatchim(syllable) { const code = syllable.charCodeAt(syllable.length - 1) - 0xAC00; if (code < 0 || code > 11171) return null; // 완성형 한글 음절이 아님(로마자·숫자 등) return code % 28 !== 0; } function pickJosa(word, withBatchim, withoutBatchim) { const has = hasBatchim(word); if (has === null) return null; // 판별 불가 — 아래 재구성 대안을 쓴다 return has ? withBatchim : withoutBatchim; } // "${name}을(를) 삭제했습니다" 대신 const josa = pickJosa(name, '을', '를'); `${name}${josa} 삭제했습니다` ``` **로마자·숫자로 끝나는 변수는 코드로 판별할 수 없다.** `iPhone`, `3번` 처럼 완성형 한글 음절이 아닌 문자로 끝나면 받침 여부가 발음마다 갈려 규칙 하나로 못 맞힌다. 이런 값이 들어갈 자리는 조사가 아예 필요 없도록 문장을 다시 짠다 — 예: `${name}${josa} 삭제했습니다` 대신 `${name} 항목을 삭제했습니다`처럼 조사 앞에 고정 명사(`항목`)를 끼워 받침 문제를 피한다. --- ## 10. 토글 라벨·링크 텍스트·placeholder ### 10-1. 토글 라벨 — 관찰 후보 토글은 **켜졌을 때 일어나는 일**을 기준으로 라벨링한다. `알림 끄기`라는 라벨의 토글을 껐을 때 알림을 받는 상태가 되면(이중 부정), 사용자는 지금 상태가 무엇인지 헷갈린다. `알림 받기`로 쓰고 켜진 상태 = 알림을 받는 상태로 맞춘다[SKILL-BETTER-WRITING]. ### 10-2. 링크 텍스트 **하드 게이트** — 링크의 목적은 링크 텍스트만으로, 또는 텍스트와 프로그램적으로 결정 가능한 맥락을 합쳐서 알 수 있어야 한다[WCAG-LINK]. **관찰 후보** — 스크린리더의 링크 목록 탐색 모드는 맥락 없이 링크 텍스트만 순서대로 읽으므로, 그런 상황에서도 통하도록 목적어를 넣는다 — `더보기` 단독 대신 `가격표 보기`[SKILL-BETTER-WRITING]. 한 화면에 `더보기` 링크가 여러 개면 각각 접미사로 구분한다(`사례 더보기`, `요금제 더보기`). 링크 존재 여부 자체는 이미 [preflight.md](preflight.md) §4-1이 확인한다 — 이 절은 그 링크의 문구 작성법만 더한다. ### 10-3. placeholder는 라벨이 아니다 — 하드 게이트 모든 입력에는 보이는 `