Skip to content

๐ŸŸก[P2] fix(frontend): React key={index} on mutable lists causes focus loss and re-mount churnย #82

Description

@teddylee777

์ปจํ…์ŠคํŠธ ๋ธ”๋ก

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)

์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค

  1. ์‚ฌ์šฉ์ž๊ฐ€ ArrayField์— 5๊ฐœ ํ•ญ๋ชฉ ์ž…๋ ฅ, ๋‘ ๋ฒˆ์งธ ์ž…๋ ฅ ์ค‘
  2. ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ ์‚ญ์ œ
  3. React ์žฌ์กฐ์ •: index 04 โ†’ 03, ๋ชจ๋“  input์˜ key 1์”ฉ ๊ฐ์†Œ
  4. ์ž…๋ ฅ ์ค‘์ด๋˜ ๋‘ ๋ฒˆ์งธ ํ•ญ๋ชฉ(์ด์ œ ์ฒซ ๋ฒˆ์งธ)์˜ input์ด unmount/re-mount โ†’ ํฌ์ปค์Šค ์†์‹ค, ์ž…๋ ฅ ์ƒํƒœ ๊นจ์ง
  5. ๋˜๋Š” 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>๋กœ ๊ด€๋ฆฌ โ†’ ํฐ ๋ฆฌํŒฉํ„ฐ, ์Šค์ฝ”ํ”„ ๊ณผ๋Œ€

์ˆ˜์šฉ ๊ธฐ์ค€

  • 4๊ฐœ ํŒŒ์ผ์—์„œ key={idx} โ†’ stable ID๋กœ ๊ต์ฒด
  • ArrayField/FileArrayField์— ID ์ƒ์„ฑ ๋กœ์ง ์ถ”๊ฐ€
  • e2e: array field ์ค‘๊ฐ„ ํ•ญ๋ชฉ ์‚ญ์ œ โ†’ ์ž…๋ ฅ ํฌ์ปค์Šค ์œ ์ง€
  • ESLint ๊ทœ์น™ ํ™œ์„ฑํ™” (warn ๋ ˆ๋ฒจ)
  • ํ…Œ์ŠคํŠธ ์ปค๋งจ๋“œ: cd frontend && pnpm lint && pnpm test

์ฐธ์กฐ

  • ๊ด€๋ จ ํŒŒ์ผ: ์œ„ 4๊ฐœ ํŒŒ์ผ
  • Checklist ํ•ญ๋ชฉ: ISS-UI-R6
  • React Docs: Lists and Keys
  • ๊ด€๋ จ ์ด์Šˆ: ์—†์Œ

์žฌํ˜„ ๋ฐฉ๋ฒ•

์‚ฌ์ „ ์กฐ๊ฑด

  • dev ์„œ๋ฒ„, schema-ui ArrayField๊ฐ€ ์‚ฌ์šฉ๋˜๋Š” ํผ ์ ‘๊ทผ ๊ฐ€๋Šฅ

๋‹จ๊ณ„

  1. ArrayField์— ํ•ญ๋ชฉ 3๊ฐœ ์ž…๋ ฅ (๊ฐ๊ฐ "a", "b", "c")
  2. ๋‘ ๋ฒˆ์งธ ์ž…๋ ฅ("b") ํด๋ฆญ ํ›„ ํ…์ŠคํŠธ ์ถ”๊ฐ€ ์ž…๋ ฅ ์ค‘
  3. ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ("a") ์‚ญ์ œ
  4. ํฌ์ปค์Šค ์œ„์น˜ ๋ฐ ์ž…๋ ฅ ์ƒํƒœ ๊ด€์ฐฐ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ

"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`

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:frontendfrontend domain scanomb-issue-scanDetected by omb:issue scanpriority:p2Medium โ€” fix if time permits

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions