이번 포스트에서는 린캔버스 애플리케이션의 사용자 경험을 더욱 향상시키기 위해 노트 추가 및 삭제 기능을 구현하고, 한글 입력 이슈를 해결하는 과정을 다룹니다. 각 파일별로 코드 변경 사항을 상세히 설명하며, 이러한 변경이 애플리케이션에 미치는 영향을 분석하겠습니다.
src/components/Note.jsx
수정// ... 생략 ...
<button
aria-label="Close Note"
className="text-gray-700"
onClick={e => {
e.stopPropagation();
onRemoveNote(id);
}}
>
<AiOutlineClose size={20} />
</button>
// ... 생략 ...
이벤트 전파 방지 (e.stopPropagation()
):
onClick={() => onRemoveNote(id)}
onClick={e => { e.stopPropagation(); onRemoveNote(id); }}
노트 삭제 버튼을 클릭할 때, 버튼의 클릭 이벤트가 부모 요소로 전파되는 것을 방지하기 위해 e.stopPropagation()
을 추가했습니다. 이는 사용자가 노트를 삭제할 때 의도치 않게 노트 전체가 편집 모드로 전환되는 현상을 방지합니다. 예를 들어, 노트가 클릭되면 편집 모드로 전환되는데, 삭제 버튼을 클릭할 때 이 이벤트가 부모로 전파되어 노트가 동시에 편집 모드로 전환되는 것을 막습니다.
노트 삭제 로직 (onRemoveNote(id)
):
id
를 인자로 onRemoveNote
함수를 호출하여 노트를 삭제합니다. 이 함수는 상위 컴포넌트에서 관리되며, 삭제된 노트는 UI에서 즉시 제거됩니다.이 변경을 통해 사용자가 노트를 삭제할 때 보다 직관적이고 오류 없는 경험을 제공할 수 있게 되었습니다. 이벤트 전파를 중지함으로써 노트 삭제 시 불필요한 편집 모드 전환을 방지하고, 사용자 의도에 맞는 동작만을 수행하도록 했습니다.
src/components/LeanCanvas.jsx
수정// ... 생략 ...
<CanvasCard
title={'4. 해결안'}
notes={canvas.solution.notes}
onNotesChange={notes => handleNotesChange('solution', notes)}
/>
<CanvasCard
title={'3. 가치제안'}
notes={canvas.valueProposition.notes}
onNotesChange={notes => handleNotesChange('valueProposition', notes)}
/>
<CanvasCard
title={'5. 경쟁우위'}
notes={canvas.unfairAdvantage.notes}
onNotesChange={notes => handleNotesChange('unfairAdvantage', notes)}
/>
<CanvasCard
title={'2. 목표 고객'}
notes={canvas.customerSegments.notes}
onNotesChange={notes => handleNotesChange('customerSegments', notes)}
/>
<CanvasCard
title={'기존 대안'}
isSubtitle
notes={canvas.existingAlternatives.notes}
onNotesChange={notes =>
handleNotesChange('existingAlternatives', notes)
}
/>
<CanvasCard
title={'8. 핵심지표'}
notes={canvas.keyMetrics.notes}
onNotesChange={notes => handleNotesChange('keyMetrics', notes)}
/>
<CanvasCard
title={'상위개념'}
isSubtitle
notes={canvas.highLevelConcept.notes}
onNotesChange={notes => handleNotesChange('highLevelConcept', notes)}
/>
<CanvasCard
title={'9. 고객 경로'}
notes={canvas.channels.notes}
onNotesChange={notes => handleNotesChange('channels', notes)}
/>
<CanvasCard
title={'얼리 어답터'}
isSubtitle
notes={canvas.earlyAdopters.notes}
onNotesChange={notes => handleNotesChange('earlyAdopters', notes)}
/>
<CanvasCard
title={'7. 비용 구조'}
notes={canvas.costStructure.notes}
onNotesChange={notes => handleNotesChange('costStructure', notes)}
/>
<CanvasCard
title={'6. 수익 흐름'}
notes={canvas.revenueStreams.notes}
onNotesChange={notes => handleNotesChange('revenueStreams', notes)}
/>
// ... 생략 ...
다양한 CanvasCard
컴포넌트 추가:
CanvasCard
컴포넌트를 추가하여 린캔버스의 모든 섹션을 포괄적으로 관리할 수 있게 했습니다.<CanvasCard
title={'4. 해결안'}
notes={canvas.solution.notes}
onNotesChange={notes => handleNotesChange('solution', notes)}
/>
title
: 섹션의 제목을 설정합니다.notes
: 해당 섹션의 노트 데이터를 전달합니다.onNotesChange
: 노트가 변경될 때 호출되는 함수로, 상위 컴포넌트(LeanCanvas
)의 handleNotesChange
함수를 호출하여 섹션별로 노트 변경 사항을 처리합니다.섹션별 노트 변경 처리:
handleNotesChange
함수는 특정 섹션의 노트가 변경될 때 호출되어 전체 캔버스 데이터를 업데이트합니다.LeanCanvas
컴포넌트는 린캔버스의 각 섹션을 CanvasCard
컴포넌트로 분리하여 관리함으로써, 코드의 재사용성과 유지보수성을 크게 향상시켰습니다. 각 섹션별로 노트를 추가, 수정, 삭제할 수 있는 기능을 제공하여 사용자가 린캔버스를 보다 체계적으로 관리할 수 있게 되었습니다.
src/components/CanvasCard.jsx
수정import { v4 as uuidv4 } from 'uuid';
import { FaPlus } from 'react-icons/fa';
import Note from './Note';
function CanvasCard({ title, isSubtitle = false, notes = [], onNotesChange }) {
const handleAddNote = () => {
const newNote = {
id: uuidv4(),
content: '',
color: '',
};
onNotesChange([...notes, newNote]);
};
const handleRemoveNote = id => {
onNotesChange(notes.filter(note => note.id !== id));
};
const handleUpdateNote = (id, content, color) => {
onNotesChange(
notes.map(note => (note.id === id ? { ...note, content, color } : note)),
);
};
return (
<div className="row-span-1 bg-white min-h-48 border border-gray-300 p-4 rounded shadow">
<h2 className={`text-xl font-semibold mb-4 ${isSubtitle ? 'text-gray-700' : 'text-black'}`}>
{title}
</h2>
<div className="space-y-2">
{notes.map(note => (
<Note
key={note.id}
id={note.id}
content={note.content}
color={note.color}
onRemoveNote={handleRemoveNote}
onUpdateNote={handleUpdateNote}
/>
))}
<button
className="flex items-center justify-center w-full p-2 bg-green-500 text-white rounded hover:bg-green-600 transition"
onClick={handleAddNote}
>
<FaPlus /> 추가
</button>
</div>
</div>
);
}
export default CanvasCard;
UUID를 사용한 노트 ID 생성 (uuidv4()
):
import { v4 as uuidv4 } from 'uuid';
를 추가하여 각 노트에 고유한 id
를 부여합니다.handleAddNote
함수는 uuidv4()
를 사용하여 새로운 노트의 id
를 생성하고, 기본 content
와 color
를 설정합니다.노트 추가 (handleAddNote
):
handleAddNote
함수가 호출됩니다.onNotesChange
함수를 통해 상위 컴포넌트에 전달합니다.노트 삭제 (handleRemoveNote
):
handleRemoveNote
함수가 호출됩니다.id
를 가진 노트를 필터링하여 제거한 후, 업데이트된 노트 배열을 onNotesChange
함수를 통해 상위 컴포넌트에 전달합니다.노트 업데이트 (handleUpdateNote
):
handleUpdateNote
함수가 호출됩니다.id
를 찾아 새로운 content
와 color
로 업데이트한 후, 전체 노트 배열을 onNotesChange
함수를 통해 상위 컴포넌트에 전달합니다.스타일링 및 레이아웃:
row-span-1
, bg-white
, min-h-48
, border
, border-gray-300
, p-4
, rounded
, shadow
등의 Tailwind CSS 클래스를 사용하여 카드의 시각적 스타일을 지정하고, 사용자에게 깔끔하고 직관적인 UI를 제공합니다.노트 렌더링 및 추가 버튼:
notes.map
을 사용하여 각 노트를 Note
컴포넌트로 렌더링합니다.CanvasCard
컴포넌트는 린캔버스의 각 섹션을 관리하는 핵심 요소로, 사용자가 노트를 자유롭게 추가, 수정, 삭제할 수 있도록 설계되었습니다. UUID를 사용하여 각 노트에 고유한 id
를 부여함으로써, 노트 관리의 정확성과 신뢰성을 높였습니다. 또한, Tailwind CSS를 활용한 스타일링은 컴포넌트의 일관된 디자인을 유지하면서도 사용자에게 친숙한 인터페이스를 제공합니다.
src/components/Note.jsx
수정import { useEffect, useRef, useState } from 'react';
import { AiOutlineClose, AiOutlineCheck } from 'react-icons/ai';
const Note = ({
id,
content,
color: initialColor,
onUpdateNote,
onRemoveNote,
}) => {
const [localContent, setLocalContent] = useState(content);
const colorOptions = [
'bg-yellow-300',
'bg-pink-300',
'bg-blue-300',
'bg-green-300',
];
const [color, setColor] = useState(() => {
if (initialColor) return initialColor;
const randomIndex = Math.floor(Math.random() * colorOptions.length);
return colorOptions[randomIndex];
});
const [isEditing, setIsEditing] = useState(false);
const textareaRef = useRef(null);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height =
textareaRef.current.scrollHeight + 'px';
}
}, [content]);
const handleContentChange = () => {
onUpdateNote(id, localContent, color);
};
const handleColorChange = newColor => {
setColor(newColor);
onUpdateNote(id, content, newColor);
};
return (
<div
className={`p-4 ${color} relative max-h-[32rem] overflow-hidden`}
onClick={() => setIsEditing(true)}
>
<div className="absolute top-2 right-2">
{isEditing ? (
<button
aria-label="Check Note"
className="text-gray-700"
onClick={e => {
e.stopPropagation();
setIsEditing(false);
}}
>
<AiOutlineCheck size={20} />
</button>
) : (
<button
aria-label="Close Note"
className="text-gray-700"
onClick={e => {
e.stopPropagation();
onRemoveNote(id);
}}
>
<AiOutlineClose size={20} />
</button>
)}
</div>
<textarea
ref={textareaRef}
value={localContent}
onChange={e => setLocalContent(e.target.value)}
onBlur={handleContentChange}
className={`w-full h-full bg-transparent resize-none border-none focus:outline-none text-gray-900 overflow-hidden`}
aria-label="Edit Note"
placeholder="메모를 작성하세요."
style={{ height: 'auto', minHeight: '8rem' }}
readOnly={!isEditing}
/>
{isEditing && (
<div className="flex space-x-2">
{colorOptions.map((option, index) => (
<button
key={index}
className={`w-6 h-6 rounded-full cursor-pointer outline outline-gray-50 ${option}`}
onClick={() => handleColorChange(option)}
aria-label={`Change color to ${option}`}
/>
))}
</div>
)}
</div>
);
};
export default Note;
로컬 상태 관리 (useState
):
localContent
: 노트의 현재 내용을 로컬 상태로 관리하여 사용자가 입력하는 동안 실시간으로 업데이트됩니다.color
: 노트의 배경색을 관리합니다. 초기 값은 initialColor
로 설정되며, 설정되지 않은 경우 랜덤하게 선택됩니다.isEditing
: 노트가 현재 편집 모드인지 여부를 관리합니다.참조 관리 (useRef
):
textareaRef
: 텍스트 영역의 DOM 요소에 접근하여 자동 높이 조절 기능을 구현합니다.자동 높이 조절 (useEffect
):
useEffect
훅을 사용하여 content
가 변경될 때마다 텍스트 영역의 높이를 자동으로 조절합니다. 이는 사용자가 노트의 내용을 입력할 때 텍스트 영역이 자동으로 확장되어 스크롤 없이 모든 내용을 볼 수 있게 합니다.내용 변경 처리 (handleContentChange
):
onBlur
이벤트) handleContentChange
함수가 호출되어 상위 컴포넌트에 변경된 내용을 전달합니다.색상 변경 처리 (handleColorChange
):
handleColorChange
함수가 호출되어 노트의 배경색을 변경합니다. 변경된 색상은 즉시 상위 컴포넌트에 전달되어 노트의 시각적 구분을 용이하게 합니다.편집 모드 전환:
isEditing
상태가 true
로 변경되어 텍스트 영역이 편집 가능 상태로 전환됩니다.AiOutlineCheck
)을 클릭하면 isEditing
상태가 false
로 변경되어 편집 모드가 종료됩니다.AiOutlineClose
)을 클릭하면 e.stopPropagation()
을 호출하여 이벤트 전파를 중지시키고, 노트를 삭제합니다.노트 삭제 버튼 (AiOutlineClose
):
onRemoveNote
함수가 호출되어 해당 노트가 삭제됩니다. 이때 e.stopPropagation()
을 사용하여 노트 클릭 이벤트가 상위 요소로 전파되지 않도록 합니다.노트 내용 표시 및 편집:
Note
컴포넌트는 사용자가 노트를 직관적으로 수정하고 관리할 수 있도록 다양한 기능을 제공합니다. 노트의 색상 변경 기능을 통해 시각적으로 구분할 수 있으며, 텍스트 영역의 자동 높이 조절 기능은 사용자 경험을 크게 향상시킵니다. 또한, 편집 모드 전환과 이벤트 전파 방지를 통해 사용자가 의도한 동작만을 수행하도록 하여 보다 안정적이고 직관적인 인터페이스를 제공합니다.
src/components/Note.jsx
한글 입력 문제 해결import { useEffect, useRef, useState } from 'react';
import { AiOutlineClose, AiOutlineCheck } from 'react-icons/ai';
const Note = ({
id,
content,
color: initialColor,
onUpdateNote,
onRemoveNote,
}) => {
const [localContent, setLocalContent] = useState(content);
const colorOptions = [
'bg-yellow-300',
'bg-pink-300',
'bg-blue-300',
'bg-green-300',
];
const [color, setColor] = useState(() => {
if (initialColor) return initialColor;
const randomIndex = Math.floor(Math.random() * colorOptions.length);
return colorOptions[randomIndex];
});
const [isEditing, setIsEditing] = useState(false);
const textareaRef = useRef(null);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height =
textareaRef.current.scrollHeight + 'px';
}
}, [content]);
const handleContentChange = () => {
onUpdateNote(id, localContent, color);
};
const handleColorChange = newColor => {
setColor(newColor);
onUpdateNote(id, content, newColor);
};
return (
<div
className={`p-4 ${color} relative max-h-[32rem] overflow-hidden`}
onClick={() => setIsEditing(true)}
>
<div className="absolute top-2 right-2">
{isEditing ? (
<button
aria-label="Check Note"
className="text-gray-700"
onClick={e => {
e.stopPropagation();
setIsEditing(false);
}}
>
<AiOutlineCheck size={20} />
</button>
) : (
<button
aria-label="Close Note"
className="text-gray-700"
onClick={e => {
e.stopPropagation();
onRemoveNote(id);
}}
>
<AiOutlineClose size={20} />
</button>
)}
</div>
<textarea
ref={textareaRef}
value={localContent}
onChange={e => setLocalContent(e.target.value)}
onBlur={handleContentChange}
className={`w-full h-full bg-transparent resize-none border-none focus:outline-none text-gray-900 overflow-hidden`}
aria-label="Edit Note"
placeholder="메모를 작성하세요."
style={{ height: 'auto', minHeight: '8rem' }}
readOnly={!isEditing}
/>
{isEditing && (
<div className="flex space-x-2">
{colorOptions.map((option, index) => (
<button
key={index}
className={`w-6 h-6 rounded-full cursor-pointer outline outline-gray-50 ${option}`}
onClick={() => handleColorChange(option)}
aria-label={`Change color to ${option}`}
/>
))}
</div>
)}
</div>
);
};
export default Note;
localContent
) 추가:localContent
라는 로컬 상태를 도입하였습니다. 이는 노트의 내용을 임시로 저장하여 입력 도중 발생할 수 있는 비동기 문제를 방지합니다.readOnly
속성:readOnly={!isEditing}
속성을 추가하여 노트가 편집 모드가 아닐 때는 텍스트 영역이 읽기 전용으로 설정됩니다. 이는 사용자가 실수로 내용을 변경하지 않도록 보호합니다.style={{ height: 'auto', minHeight: '8rem' }}
를 추가하여 텍스트 영역의 최소 높이를 설정하고, 내용에 따라 자동으로 높이가 조절되도록 했습니다. 이는 한글 입력 시 텍스트 영역의 크기가 적절하게 유지되도록 도와줍니다.onBlur={handleContentChange}
: 텍스트 영역에서 포커스를 잃을 때(onBlur
이벤트) handleContentChange
함수를 호출하여 변경된 내용을 상위 컴포넌트에 전달합니다. 이는 입력이 완료된 후에만 변경 사항이 반영되도록 합니다.e.stopPropagation()
을 호출하여 부모 요소로의 이벤트 전파를 방지하였습니다. 이는 노트 내 버튼 클릭 시 노트 전체가 편집 모드로 전환되는 것을 막습니다.한글 입력 시 발생할 수 있는 비동기 문제를 해결하기 위해 로컬 상태 localContent
를 도입하였습니다. 이는 사용자가 텍스트를 입력하는 동안 실시간으로 노트의 내용을 업데이트하여 입력 도중 발생할 수 있는 데이터 손실을 방지합니다. 또한, readOnly
속성을 통해 노트가 편집 모드가 아닐 때는 내용을 수정할 수 없도록 하여 데이터의 안정성을 확보했습니다.
텍스트 영역의 스타일링을 통해 한글 입력 시 텍스트 영역의 크기가 적절하게 유지되도록 하였으며, onBlur
이벤트를 활용하여 입력이 완료된 후에만 변경 사항을 상위 컴포넌트에 반영하도록 설계하였습니다. 이는 사용자의 의도치 않은 데이터 변경을 방지하고, 입력의 정확성을 높이는 데 기여합니다.
src/components/Note.jsx
수정:
localContent
추가로 한글 입력 이슈 해결.readOnly
속성 도입.src/components/LeanCanvas.jsx
수정:
CanvasCard
컴포넌트 추가:CanvasCard
추가.handleNotesChange
함수 호출로 데이터 일관성 유지.src/components/CanvasCard.jsx
수정:
handleAddNote
):id
를 가진 새로운 노트 추가.handleRemoveNote
):id
를 가진 노트를 필터링하여 삭제.handleUpdateNote
):content
와 color
를 업데이트하여 전체 노트 배열을 상위 컴포넌트에 전달.한글 입력 이슈 처리:
src/components/Note.jsx
수정:localContent
추가로 한글 입력 시 발생할 수 있는 데이터 손실 방지.readOnly
속성 도입으로 사용자 경험 향상.이번 린캔버스 수정 작업을 통해 다음과 같은 주요 내용을 학습하고 적용할 수 있었습니다:
이벤트 전파 관리:
e.stopPropagation()
을 활용하여 특정 이벤트가 부모 요소로 전파되지 않도록 제어하는 방법을 배웠습니다. 이는 사용자 인터페이스의 예기치 않은 동작을 방지하는 데 유용합니다.로컬 상태 관리:
localContent
)를 도입하여 입력 중인 내용을 안정적으로 관리하는 방법을 익혔습니다.자동 높이 조절:
UUID 활용:
uuidv4()
를 사용하여 고유한 식별자를 생성함으로써, 동적으로 생성되는 노트의 id
를 관리하는 방법을 익혔습니다. 이는 데이터의 신뢰성과 관리의 용이성을 높입니다.컴포넌트 분리 및 재사용성:
CanvasCard
와 Note
컴포넌트를 통해 린캔버스의 각 섹션과 노트를 독립적으로 관리하고, 재사용 가능한 구조로 설계하는 방법을 배웠습니다. 이는 코드의 유지보수성과 확장성을 크게 향상시킵니다.사용자 경험(UX) 향상:
한글 입력 이슈 해결:
코드 최적화 및 유지보수성 향상:
이번 작업을 통해 린캔버스 애플리케이션의 기능을 확장하고, 사용자 경험을 크게 향상시킬 수 있었습니다.