์ปจํ
์คํธ ๋ธ๋ก
| Key |
Value |
| Category |
frontend |
| Checklist |
ISS-UI-R6 โ React keys using array index for dynamic lists |
| Priority |
P2 ๐ก |
| Scan Date |
2026-04-16 |
| Flagged By |
@code-review |
์์ฝ
- WHAT: 4๊ฐ ์ปดํฌ๋ํธ๊ฐ ์ถ๊ฐ/์ญ์ ๊ฐ๋ฅํ ๋ฆฌ์คํธ์
key={index} ์ฌ์ฉ โ ์ค๊ฐ ํญ๋ชฉ ์ญ์ ์ ๋ชจ๋ ํ์ ํญ๋ชฉ์ key๊ฐ ๋ฐ๋์ด React ์ฌ์กฐ์ ์คํจ
- WHY: ์
๋ ฅ ํฌ์ปค์ค ์์ค, ์ปจํธ๋กค๋ ์ธํ์ state corruption, ์คํธ๋ฆฌ๋ฐ ํ๊ฒฝ์์ ๋ถํ์ํ re-mount โ ์ฑ๋ฅ ์ ํ + UX ๋ฒ๊ทธ
- WHERE: 4๊ฐ ํ์ผ โ
ToolCalls.tsx, ArrayField.tsx, FileArrayField.tsx, ContentBlocksPreview.tsx
- SEVERITY: MEDIUM โ ๊ฐ์์ UX ๋ฒ๊ทธ + ์ ์ฌ์ ๋ฐ์ดํฐ ์์ค
Evidence
| # |
File |
Line |
Finding |
Flagged By |
Confidence |
| 1 |
frontend/src/features/chat/components/messages/ToolCalls.tsx |
23-26 |
toolCalls.map((tc, idx) => <ToolCallItem key={idx} ... />) โ tc.id๊ฐ lin 91์์ toolCall.id.slice(0, 6)๋ก ์ฌ์ฉ๋จ, ์ฆ stable ID ์กด์ฌ |
@code-review |
High |
| 2 |
frontend/src/features/chat/components/schema-ui/fields/ArrayField.tsx |
50-52 |
ํธ์ง ๊ฐ๋ฅํ array์ key={index}, ํญ๋ชฉ ์ ๊ฑฐ ๊ฐ๋ฅ (handleRemove line 31-37) |
@code-review |
High |
| 3 |
frontend/src/features/chat/components/schema-ui/fields/FileArrayField.tsx |
115-117 |
ํ์ผ ๋ฆฌ์คํธ์ key={index}, ํ์ผ ์ ๊ฑฐ ๊ฐ๋ฅ (handleRemove line 133) |
@code-review |
High |
| 4 |
frontend/src/features/chat/components/content/ContentBlocksPreview.tsx |
45-47 |
blocks.map((block, idx) => <MultimodalPreview key={idx} ...>) โ onRemove ํตํด ์ ๊ฑฐ ๊ฐ๋ฅ |
@code-review |
High |
| 5 |
(PASS ์ฐธ๊ณ ) ThreadSkeleton.tsx, HistorySkeleton.tsx, admin/loading.tsx |
โ |
์ ์ placeholder ๋ฐฐ์ด โ index key ํ์ฉ |
@code-review |
High |
์ํฅ ๋ถ์
์ํฅ ๋ฒ์
- ์ฑํ
์ค tool call ์คํธ๋ฆฌ๋ฐ (ToolCalls)
- Schema UI form ์
๋ ฅ (ArrayField, FileArrayField)
- ๋ฉํฐ๋ชจ๋ฌ ์ปจํ
์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (ContentBlocksPreview)
์ฅ์ ์๋๋ฆฌ์ค
- ์ฌ์ฉ์๊ฐ ArrayField์ 5๊ฐ ํญ๋ชฉ ์
๋ ฅ, ๋ ๋ฒ์งธ ์
๋ ฅ ์ค
- ์ฒซ ๋ฒ์งธ ํญ๋ชฉ ์ญ์
- React ์ฌ์กฐ์ : index 0
4 โ 03, ๋ชจ๋ input์ key 1์ฉ ๊ฐ์
- ์
๋ ฅ ์ค์ด๋ ๋ ๋ฒ์งธ ํญ๋ชฉ(์ด์ ์ฒซ ๋ฒ์งธ)์ input์ด unmount/re-mount โ ํฌ์ปค์ค ์์ค, ์
๋ ฅ ์ํ ๊นจ์ง
- ๋๋ ToolCalls ์คํธ๋ฆฌ๋ฐ ์ค ์ tool call ์ถ๊ฐ ์ ๊ธฐ์กด carded๋ค์ด ๋ชจ๋ re-mount โ ๊น๋นก์ + ํผ์น ์ํ ๋ฆฌ์
๊ธด๊ธ๋
- ์ฌ์ฉ์๊ฐ ์ง์ ๋ง์ฃผ์น๋ UX ๋ฒ๊ทธ
- React 19 strict mode์์ ๋ ๋ช
ํํ ๋
ธ์ถ (double rendering)
์ ์ ํด๊ฒฐ ๋ฐฉ์
์ ๊ทผ ๋ฐฉ๋ฒ
๊ฐ ํ์ผ์์ key={idx}๋ฅผ stable ID๋ก ๊ต์ฒด:
ToolCalls.tsx:26:
// ๋ณ๊ฒฝ ์
{toolCalls.map((tc, idx) => <ToolCallItem key={idx} ... />)}
// ๋ณ๊ฒฝ ํ
{toolCalls.map((tc) => <ToolCallItem key={tc.id} ... />)}
ArrayField.tsx:50-52 โ items์ stable ID๊ฐ ์๋ค๋ฉด ์ถ๊ฐ ์ generate:
// items: Array<{ id: string; value: T }> ํํ๋ก ๋ณ๊ฒฝ
const handleAdd = () => setItems([...items, { id: crypto.randomUUID(), value: defaultValue }]);
// render
{items.map((item) => <div key={item.id}>...</div>)}
FileArrayField.tsx, ContentBlocksPreview.tsx: ๋์ผ ํจํด ์ ์ฉ. File ๊ฐ์ฒด์๋ name + lastModified ์กฐํฉ๋ ์ฌ์ฉ ๊ฐ๋ฅ (์ถฉ๋ ๊ฐ๋ฅ์ฑ ๋ฎ์).
ESLint ๊ท์น ์ถ๊ฐ:
// frontend/eslint.config.js
\"react/no-array-index-key\": \"warn\",
๋์
- ๋ฌด์: ๋จ๊ธฐ์ ์ผ๋ก ๋์ํ์ง๋ง React 19 concurrent rendering์์ ๋ ์์ฃผ ๋
ธ์ถ
- Map state: items๋ฅผ Map<string, T>๋ก ๊ด๋ฆฌ โ ํฐ ๋ฆฌํฉํฐ, ์ค์ฝํ ๊ณผ๋
์์ฉ ๊ธฐ์ค
์ฐธ์กฐ
์ฌํ ๋ฐฉ๋ฒ
์ฌ์ ์กฐ๊ฑด
- dev ์๋ฒ, schema-ui ArrayField๊ฐ ์ฌ์ฉ๋๋ ํผ ์ ๊ทผ ๊ฐ๋ฅ
๋จ๊ณ
- ArrayField์ ํญ๋ชฉ 3๊ฐ ์
๋ ฅ (๊ฐ๊ฐ "a", "b", "c")
- ๋ ๋ฒ์งธ ์
๋ ฅ("b") ํด๋ฆญ ํ ํ
์คํธ ์ถ๊ฐ ์
๋ ฅ ์ค
- ์ฒซ ๋ฒ์งธ ํญ๋ชฉ("a") ์ญ์
- ํฌ์ปค์ค ์์น ๋ฐ ์
๋ ฅ ์ํ ๊ด์ฐฐ
๊ธฐ๋ ๊ฒฐ๊ณผ
"b" ์
๋ ฅ ํฌ์ปค์ค ์ ์ง, ์
๋ ฅ ๋ฐ์ดํฐ ๋ณด์กด
์ค์ ๊ฒฐ๊ณผ
ํฌ์ปค์ค ์์ค, "b"๊ฐ ์ฌ๋ผ์ง๊ฑฐ๋ ๋ค๋ฅธ ํญ๋ชฉ์ผ๋ก ์ด๋
๊ด๋ จ ์ฝ๋ ์ปจํ
์คํธ
| File |
Role |
Relevance |
frontend/src/features/chat/components/messages/ToolCalls.tsx |
Tool call ์คํธ๋ฆฌ๋ฐ UI |
์์ ๋์ |
frontend/src/features/chat/components/schema-ui/fields/ArrayField.tsx |
๋์ array ์
๋ ฅ |
์์ ๋์ |
frontend/src/features/chat/components/schema-ui/fields/FileArrayField.tsx |
ํ์ผ ๋ฆฌ์คํธ ์
๋ ฅ |
์์ ๋์ |
frontend/src/features/chat/components/content/ContentBlocksPreview.tsx |
๋ฉํฐ๋ชจ๋ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ |
์์ ๋์ |
frontend/eslint.config.js |
ESLint ์ค์ |
๊ท์น ์ถ๊ฐ |
Detected by oh-my-braincrew `omb:issue` scan
Category: frontend | Scan date: 2026-04-16
`omb-issue-scan category=frontend checklist=ISS-UI-R6`
์ปจํ ์คํธ ๋ธ๋ก
ISS-UI-R6โ React keys using array index for dynamic lists์์ฝ
key={index}์ฌ์ฉ โ ์ค๊ฐ ํญ๋ชฉ ์ญ์ ์ ๋ชจ๋ ํ์ ํญ๋ชฉ์ key๊ฐ ๋ฐ๋์ด React ์ฌ์กฐ์ ์คํจToolCalls.tsx,ArrayField.tsx,FileArrayField.tsx,ContentBlocksPreview.tsxEvidence
frontend/src/features/chat/components/messages/ToolCalls.tsxtoolCalls.map((tc, idx) => <ToolCallItem key={idx} ... />)โtc.id๊ฐ lin 91์์toolCall.id.slice(0, 6)๋ก ์ฌ์ฉ๋จ, ์ฆ stable ID ์กด์ฌfrontend/src/features/chat/components/schema-ui/fields/ArrayField.tsxkey={index}, ํญ๋ชฉ ์ ๊ฑฐ ๊ฐ๋ฅ (handleRemoveline 31-37)frontend/src/features/chat/components/schema-ui/fields/FileArrayField.tsxkey={index}, ํ์ผ ์ ๊ฑฐ ๊ฐ๋ฅ (handleRemoveline 133)frontend/src/features/chat/components/content/ContentBlocksPreview.tsxblocks.map((block, idx) => <MultimodalPreview key={idx} ...>)โonRemoveํตํด ์ ๊ฑฐ ๊ฐ๋ฅThreadSkeleton.tsx,HistorySkeleton.tsx,admin/loading.tsx์ํฅ ๋ถ์
์ํฅ ๋ฒ์
์ฅ์ ์๋๋ฆฌ์ค
4 โ 03, ๋ชจ๋ input์ key 1์ฉ ๊ฐ์๊ธด๊ธ๋
์ ์ ํด๊ฒฐ ๋ฐฉ์
์ ๊ทผ ๋ฐฉ๋ฒ
๊ฐ ํ์ผ์์
key={idx}๋ฅผ stable ID๋ก ๊ต์ฒด:ToolCalls.tsx:26:
ArrayField.tsx:50-52 โ items์ stable ID๊ฐ ์๋ค๋ฉด ์ถ๊ฐ ์ generate:
FileArrayField.tsx, ContentBlocksPreview.tsx: ๋์ผ ํจํด ์ ์ฉ. File ๊ฐ์ฒด์๋
name + lastModified์กฐํฉ๋ ์ฌ์ฉ ๊ฐ๋ฅ (์ถฉ๋ ๊ฐ๋ฅ์ฑ ๋ฎ์).ESLint ๊ท์น ์ถ๊ฐ:
// frontend/eslint.config.js \"react/no-array-index-key\": \"warn\",๋์
์์ฉ ๊ธฐ์ค
key={idx}โ stable ID๋ก ๊ต์ฒดcd frontend && pnpm lint && pnpm test์ฐธ์กฐ
์ฌํ ๋ฐฉ๋ฒ
์ฌ์ ์กฐ๊ฑด
๋จ๊ณ
๊ธฐ๋ ๊ฒฐ๊ณผ
"b" ์ ๋ ฅ ํฌ์ปค์ค ์ ์ง, ์ ๋ ฅ ๋ฐ์ดํฐ ๋ณด์กด
์ค์ ๊ฒฐ๊ณผ
ํฌ์ปค์ค ์์ค, "b"๊ฐ ์ฌ๋ผ์ง๊ฑฐ๋ ๋ค๋ฅธ ํญ๋ชฉ์ผ๋ก ์ด๋
๊ด๋ จ ์ฝ๋ ์ปจํ ์คํธ
frontend/src/features/chat/components/messages/ToolCalls.tsxfrontend/src/features/chat/components/schema-ui/fields/ArrayField.tsxfrontend/src/features/chat/components/schema-ui/fields/FileArrayField.tsxfrontend/src/features/chat/components/content/ContentBlocksPreview.tsxfrontend/eslint.config.js