Conversation
sumin0423
left a comment
There was a problem hiding this comment.
규민님 2주차 과제 고생하셨습니다! 작성·수정 화면을 재사용하신 부분이 좋았어요. 다음 주차도 화이팅입니다!!! 😊
| const [category, setCategory] = useState(initialMemo?.category ?? ""); | ||
| const [exitModalType, setExitModalType] = useState(null); | ||
|
|
||
| const isEditing = initialMemo !== null; |
There was a problem hiding this comment.
initialMemo 유무로 작성과 수정 화면을 같이 사용하는 부분 좋은 것 같습니다 ! 입력 UI를 따로 두지 않아서 나중에 수정할 때도 한 곳만 확인하면 될 것 같아요~
| document.removeEventListener("keydown", handleEscapeKey); | ||
| }; | ||
| }, [isOpen]); | ||
|
|
There was a problem hiding this comment.
드롭다운이 열렸을 때만 이벤트를 등록하고 닫히면 정리해주신 것과 바깥 클릭이랑 Esc로 닫는 동작까지 아주 좋은 것 같습니다 !
|
|
||
| <p className="text-body-large font-medium text-white-00"> | ||
| {memo.content} | ||
| </p> |
There was a problem hiding this comment.
본문에 여러 줄을 입력해도 상세 화면에서는 줄바꿈이 합쳐져 보일 수도 있어요. 여기에 whitespace-pre-wrap을 추가하면 작성할 때 넣은 줄바꿈을 그대로 보여줄 수 있어서 이부분 추천드립니다 !
| ); | ||
|
|
||
| setSelectedMemo(null); | ||
| }; |
There was a problem hiding this comment.
MemoDetail.jsx 에서의 삭제 안내에는 휴지통에서 확인할 수 있다고 되어 있는데, 현재 App.jsx 코드로는 목록에서 바로 제거되는 방식으로 보입니다. 아직 휴지통 기능이 없다면 “삭제하면 복구할 수 없습니다”처럼 실제 동작에 맞춰 안내해주면 좋을 것 같습니다!
| --text-action-medium--line-height: 28px; | ||
|
|
||
| --text-action-small: 16px; | ||
| --text-action-small--line-height: 24px; |
There was a problem hiding this comment.
색상, 폰트 weight, 타이포그래피 스케일을 전부 @theme 토큰으로 정리하신 게 좋았습니다!
덕분에 컴포넌트 쪽 클래스가 의미 단위로 읽혀서 디자인 시스템이 눈에 잘 들어왔네요!
| {onCancel && ( | ||
| <button | ||
| type="button" | ||
| className="h-14 flex-1 rounded-xl bg-gray-01 px-6 text-action-medium font-extrabold text-gray-03" | ||
| onClick={onCancel} | ||
| > | ||
| {cancelText} | ||
| </button> | ||
| )} |
There was a problem hiding this comment.
onCancel로 1버튼/2버튼 모달을 자동 전환하는 구조가 좋네요!
삭제 확인, 작성 취소, 작성 완료까지 3가지 상태를 컴포넌트 하나로 다 커버하고 있어서 효율적인 것 같아요 :)
| const handleCardKeyDown = (event) => { | ||
| if (event.target !== event.currentTarget) { | ||
| return; | ||
| } | ||
|
|
||
| if (event.key === "Enter" || event.key === " ") { | ||
| event.preventDefault(); | ||
| onSelectMemo(memo); | ||
| } |
There was a problem hiding this comment.
안쪽 핀 버튼에 포커스를 두고 Enter를 눌렀을 때 카드 상세까지 같이 열리는 걸
event.target !== event.currentTarget으로 막으신 점 좋습니다~~!
| setMemos((currentMemos) => [ | ||
| { ...memoDraft, id: nextId, isPinned: false }, | ||
| ...currentMemos, | ||
| ]); | ||
|
|
||
| setIsAddCompleteModalOpen(true); | ||
| }; |
There was a problem hiding this comment.
isAddCompleteModalOpen만 켜고 isAddingMemo는 끄지 않아서, 완료 모달 뒤에 MemoEditor가 그대로 살아있어요! 시각적으로는 모달에 가려지지만 setIsAddingMemo(false)도 같이 호출해주면 완성도가 더 높아질 것 같습니당!
| <article | ||
| className={`flex h-71.25 w-71.25 cursor-pointer flex-col gap-2.5 rounded-[20px] p-5 py-8 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-05 ${categoryStyle.card}`} | ||
| role="button" | ||
| tabIndex={0} | ||
| aria-label={`${title} 상세 보기`} | ||
| onClick={handleCardClick} | ||
| onKeyDown={handleCardKeyDown} | ||
| > | ||
| <div className="flex items-center justify-between gap-3"> | ||
| <h2 className="min-w-0 flex-1 truncate text-heading-small font-bold text-white-00"> | ||
| {title} | ||
| </h2> | ||
|
|
||
| <IconButton | ||
| icon={isPinned ? pinActiveIcon : pinIcon} | ||
| label={isPinned ? "메모 고정 해제" : "메모 고정"} | ||
| className="h-7 w-7 shrink-0 focus-visible:outline-white-00 [&_img]:h-5.5 [&_img]:w-5.75" | ||
| onClick={handlePinClick} | ||
| /> |
There was a problem hiding this comment.
카드 전체를 role="button" tabIndex={0}으로 만들었는데, 그 안에 진짜 <button>이 또 들어있어요! 사소한 부분이기는 하지만 버튼 안의 버튼 구조라 카드는 <article> 시맨틱 그대로 두고 제목만 버튼으로 만들거나, 카드 안에 투명한 "상세 보기" 버튼을 깔아주는 방식이 더 좋을 것 같아요!
| import NoResultsState from "./components/NoResultsState.jsx"; | ||
|
|
||
| function App() { | ||
| const [memos, setMemos] = useState([]); |
There was a problem hiding this comment.
useState([])라서 새로고침하면 작성한 메모가 전부 사라져요!
lazy initializer로 localStorage를 읽고, useEffect로 memos 변경 시 저장하는 흐름을 붙이면 더 좋을 것 같습니당~~ 아랴 링크 내용 확인해보세요!
https://ko.react.dev/reference/react/useState#avoiding-recreating-the-initial-state
| <div className="fixed inset-0 z-60 flex items-center justify-center bg-blue-07/50 px-6"> | ||
| <section | ||
| className="flex h-60 w-full max-w-120 flex-col items-center justify-between rounded-3xl bg-white-00 px-8 pt-12 pb-6 shadow-md" | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-labelledby="common-modal-title" | ||
| aria-describedby="common-modal-message" | ||
| > |
There was a problem hiding this comment.
aria-modal, aria-labelledby, aria-describedby까지 꼼꼼히 챙겨주셨네요!👍
다만 지금 fixed inset-0 div라서 모달이 떠 있어도 뒷배경이 휠로 스크롤되고, Tab을 누르면 포커스가 모달 밖으로 새나가요. 네이티브 <dialog> + showModal()로 바꾸면 스크롤 잠금 · 포커스 트랩 · ESC 닫기 · backdrop이 전부 공짜로 따라오니 참고해보세용!
https://developer.mozilla.org/ko/docs/Web/HTML/Reference/Elements/dialog
https://blog.logrocket.com/creating-reusable-pop-up-modal-react/
🔗 배포링크
https://react-memo-24th-three.vercel.app
💭 느낀 점 및 배운 점
1주차에는 HTML, CSS, JavaScript를 사용하면서
querySelector로 요소를 선택하고 DOM을 직접 변경했습니다. 이번 과제에서는 같은 메모 서비스를 React로 다시 구현하면서, State가 변경되면 화면도 함께 변경되는 React의 동작 방식을 배울 수 있었습니다. 처음에는 Props와 State의 차이와 컴포넌트를 어느 정도까지 분리해야 하는지가 가장 헷갈렸습니다. 구현을 진행하면서 공통으로 사용하는 상태는App에서 관리하고, 자식 컴포넌트에는 필요한 값과 함수를 Props로 전달하는 구조를 이해하게 되었습니다. 또한 검색 결과나 고정 메모 목록처럼 기존 State로 계산할 수 있는 값은 별도의 State로 만들지 않고filter()를 이용해 계산했습니다. 컴포넌트를 역할별로 분리하면서 파일의 개수는 많아졌지만, 각 파일이 담당하는 역할이 명확해져 코드를 찾고 수정하기 쉬워졌습니다.💬 Review Questions
1. Virtual DOM은 무엇이고, 이를 사용함으로써 얻는 이점은 무엇인가요?
Virtual DOM은 React가 현재 화면의 모습을 메모리 안에서 표현하기 위해 사용하는 구조입니다. State나 Props가 바뀌면 React는 새로운 화면의 모습을 계산하고, 이전 결과와 비교하여 실제 DOM에 필요한 변경을 반영합니다.
1주차에는 메모를 검색하거나 고정할 때 DOM 요소를 직접 찾아 내용을 바꾸거나 클래스를 추가해야 했습니다. 이번에는 메모 데이터와 검색어를 State로 관리하고, State를 변경하면 그 결과에 맞게 화면이 렌더링되도록 구현했습니다.
이러한 방식은 DOM을 변경하는 순서를 하나씩 작성하지 않아도 되고, 데이터와 화면의 관계를 파악하기 쉽다는 장점이 있습니다. Virtual DOM이 항상 가장 빠른 방법이라는 의미보다는, 복잡한 UI의 변경 과정을 React가 일관된 방식으로 관리해준다는 점이 중요하다고 생각합니다.
2. React에서 컴포넌트를 분리하는 기준은 무엇이며, 컴포넌트 분리를 통해 얻을 수 있는 이점은 무엇인가요?
이번 과제에서는 화면에서 담당하는 역할이 구분되거나, 반복해서 사용하거나, 자체적인 동작을 가진 부분을 컴포넌트로 분리했습니다. 메모 한 개는
MemoItem, 메모 목록은MemoList, 카테고리 선택은TagFilter, 상세 화면은MemoDetail이 담당하도록 구성했습니다. 여러 곳에서 사용하는 아이콘 버튼은IconButton으로 만들어 재사용했습니다.이렇게 분리하면 하나의 파일에 모든 코드가 모이는 것을 방지하고, 기능을 수정할 때 관련된 코드를 쉽게 찾을 수 있습니다. 같은 컴포넌트에 서로 다른 Props를 전달하여 재사용할 수 있다는 점도 장점이었습니다.
다만 UI 요소를 무조건 별도 컴포넌트로 나누면 파일을 오가며 코드를 확인해야 해서 오히려 흐름을 이해하기 어려울 수 있습니다. 따라서 단순히 코드의 길이보다는 역할과 재사용 가능성을 기준으로 분리하는 것이 적절하다고 생각합니다.
3. React 컴포넌트의 생명주기에 대해서 설명해주세요.
React 컴포넌트는 화면에 처음 나타나는 마운트, State나 Props의 변화로 다시 렌더링되는 업데이트, 화면에서 사라지는 언마운트 과정을 거칩니다.
함수 컴포넌트에서는
useEffect를 이용하여 렌더링 이후 필요한 작업을 수행할 수 있습니다. Effect가 의존하는 값이 변경되면 기존 작업을 정리한 후 다시 실행하며, 컴포넌트가 화면에서 제거될 때도 정리 함수가 실행됩니다.이번 과제에서는 카테고리 드롭다운이 열렸을 때 바깥 클릭과 ESC 키를 감지하는 이벤트를 등록했습니다. 드롭다운이 닫힐 때는 해당 이벤트를 제거했습니다. 메모 상세 모달에서도 ESC 키 이벤트를 등록하고, 모달이 사라질 때 이벤트 리스너를 정리하도록 구현했습니다. 이를 통해 컴포넌트가 사라진 뒤에도 이벤트가 계속 남아 있는 문제를 방지할 수 있었습니다.