Skip to content

[Fix] 질문 삭제/수정 기능 리팩토링 및 낙관적 업데이트 강화#48

Merged
E0min merged 2 commits intodevelopfrom
fix-shutdowndelete
Nov 19, 2025
Merged

[Fix] 질문 삭제/수정 기능 리팩토링 및 낙관적 업데이트 강화#48
E0min merged 2 commits intodevelopfrom
fix-shutdowndelete

Conversation

@E0min
Copy link
Contributor

@E0min E0min commented Nov 19, 2025

Ⅰ. 개요

본 PR은 사용자가 질문 노드를 삭제하거나 수정할 때 발생하는 핵심적인 버그들을 해결하고, 전반적인 사용자 경험(UX)을 ChatGPT와 유사한 직관적인 방식으로 대폭 개선하는 것을 목표로 합니다.

주요 변경 사항은 다음과 같습니다.

  1. 버그 수정: 질문 삭제 시 D3 그래프와 카드 뷰(메시지 버블 리스트)가 동기화되지 않던 치명적인 버그를 해결했습니다.
  2. 대규모 리팩토링: 비직관적인 모달(Modal) 기반 수정 방식을 완전 제거하고, 사용자가 텍스트를 즉시 편집할 수 있는 인라인(In-line) 수정 방식으로 전면 개편했습니다.
  3. UX 개선: 사용자 실수를 방지하기 위한 삭제 확인 창을 추가하고, 수정 중에도 컨텍스트(답변)를 계속 확인할 수 있도록 UI를 개선했습니다.

Ⅱ. 핵심 변경 사항 상세 설명

1. 질문 삭제: 상태 불일치 버그 해결 및 낙관적 업데이트 로직 완성

가. 문제점 (The Bug)

  • 현상: 질문 노드를 삭제하면 그래프 뷰에서는 즉시 반영되었지만, 하위 질문 목록을 보여주는 카드 뷰는 새로고침해야만 변경 사항이 반영되었습니다.
  • 근본 원인: React 상태의 불일치(State Inconsistency).
    • handleDeleteQuestion 함수 내에서 전체 트리 데이터인 viewData 상태는 자식 노드를 올바르게 승계하여 즉시 업데이트되었습니다.
    • 하지만, 현재 뷰의 경로를 나타내는 currentPath 상태는 변경 전의 오래된(stale) 경로 정보를 기반으로 업데이트(currentPath.slice(0, -1))되고 있었습니다.
    • 이로 인해 viewData는 최신인데 currentPath는 과거인 상태가 되어, currentPath로부터 파생되는 currentQuestion이 오래된 자식 목록(children)을 계속 참조하게 되었습니다. 카드 뷰는 바로 이 currentQuestion.children을 기반으로 렌더링되므로, 업데이트가 누락된 것입니다.

나. 해결 과정 (The Fix)

  • 핵심 해결책: viewDatacurrentPath 두 상태가 항상 새로운 데이터를 기반으로 함께, 원자적으로(atomically) 업데이트되도록 로직을 전면 수정했습니다.

  • 개선된 로직 (handleDeleteQuestion):

    1. 안전한 데이터 복사: JSON.parse(JSON.stringify(viewData))를 통해 현재 트리의 완전한 복사본(newViewData)을 생성하여, 원본 상태를 건드리지 않고 안전하게 조작하도록 보장했습니다.
    2. 정확한 부모 경로 탐색: 삭제할 노드의 부모 경로(parentPath)를 언제나 새로운 트리 데이터(newViewData)에서 findPathToNode 유틸 함수를 통해 찾도록 하여, 항상 최신 경로를 참조하게 했습니다.
    3. 자식 승계 및 삭제: 찾은 부모 노드의 children 배열에서 삭제할 노드를 제거하고, 삭제된 노드의 자식들을 그 자리에 추가합니다. (Array.prototype.splice 활용)
    4. 일관된 경로 업데이트:
      • 만약 현재 보고 있던 질문을 삭제했다면, newCurrentPath를 이전처럼 slice하는 대신, 새로운 트리에서 찾은 parentPath로 설정하여 데이터 정합성을 맞췄습니다.
      • 만약 현재 질문의 하위 노드를 삭제했다면, newCurrentPath새로운 트리(newViewData)에서 findPathToNode를 다시 호출하여 경로 정보를 최신으로 동기화했습니다.
    5. 원자적 상태 업데이트: setViewData(newViewData)setCurrentPath(newCurrentPath)를 연속으로 호출하여 두 상태의 불일치 가능성을 원천적으로 차단했습니다.
    6. API 통신 및 롤백: 백엔드 API 통신 실패 시, 미리 저장해 둔 원본 상태(originalViewData, originalCurrentPath)로 완벽하게 되돌리는 롤백 로직을 유지했습니다.

2. 질문 수정: 직관적인 인라인(In-line) 방식으로 전면 개편

가. 기존 방식의 문제점 (As-Is)

  • UX 저해: 간단한 텍스트 수정을 위해 별도의 모달 창이 뜨는 것은 사용자 흐름을 끊고, 불필요한 인터페이스로 인식되었습니다.
  • 데이터 동기화 버그: 브레드크럼으로 다른 질문으로 이동한 뒤 '수정' 버튼을 누르면, 모달에 현재 질문이 아닌 이전에 봤던 질문의 내용이 그대로 남아있는 버그가 있었습니다.

나. 개선된 방식 (To-Be: ChatGPT-Style Editing)

  • 핵심 개선: 사용자가 '수정' 버튼을 누르면, 보고 있던 질문 텍스트가 그 자리에서 바로 입력창(Textarea)으로 전환되는, ChatGPT나 Gemini와 같은 매우 직관적인 방식으로 UX를 개편했습니다.

  • 구현 상세 (message-bubble.tsx):

    1. 대규모 리팩토링: 불필요해진 EditQuestionDialog.tsx 파일을 완전히 삭제하고, use-question-tree.tsindex.tsx에서 모달 관련 상태와 함수를 모두 제거하여 코드를 단순화하고 유지보수성을 높였습니다.
    2. 조건부 렌더링: isEditing이라는 내부 상태를 사용하여, true일 때는 <Textarea> 컴포넌트를, false일 때는 <div> 태그를 렌더링하도록 삼항 연산자로 구현했습니다.
    3. 데이터 바인딩 및 동기화:
      • TextareavalueeditText 상태와 바인딩했습니다.
      • editTextquestionText prop으로 초기화되며, useEffect 훅을 통해 questionText가 변경될 때마다 editText도 함께 업데이트됩니다. 이것이 브레드크럼 이동 시에도 항상 최신 질문 내용이 Textarea에 채워지도록 보장하는 핵심 로직입니다.
    4. 답변 영역 유지: 수정 중에도 컨텍스트 파악이 용이하도록, 답변(OptimisticAnswer) 영역은 isEditing 상태와 관계없이 항상 표시되도록 JSX 구조를 수정했습니다.
    5. 컨트롤 버튼: isEditing 상태일 때만 Textarea 하단에 '수정 완료'와 '취소' 버튼이 나타나도록 구현했습니다.

Ⅲ. 기타 개선 사항

  • 사용자 실수 방지: sonner 라이브러리를 활용하여, 사용자가 '삭제' 버튼 클릭 시 "정말 삭제하시겠습니까?" 라는 확인 토스트(Toast)를 띄워 의도치 않은 데이터 삭제를 방지하는 안전장치를 마련했습니다.
  • 코드 가독성: 이번 리팩토링 과정에서 변경된 모든 주요 로직에 상세한 한글 주석을 추가하여 코드의 가독성과 유지보수성을 크게 향상시켰습니다.

이러한 변경들을 통해 애플리케이션의 핵심 기능인 질문 관리의 안정성과 사용성을 크게 향상시켰습니다.

@E0min E0min merged commit de9675b into develop Nov 19, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants