Skip to content

[2주차] 김민준 과제 제출합니다. - #7

Open
mrainbeat wants to merge 13 commits into
CEOS-Developers:masterfrom
mrainbeat:minjun.kim
Open

mrainbeat wants to merge 13 commits into
CEOS-Developers:masterfrom
mrainbeat:minjun.kim

Conversation

@mrainbeat

@mrainbeat mrainbeat commented Sep 15, 2026

Copy link
Copy Markdown

버셀 배포

https://react-memo-24th-omega.vercel.app/

개발 과정

이번 과제에서는 1주차 Vanilla JS 메모 서비스를 React로 옮기면서 컴포넌트를 어떤 기준으로 나눌지를 가장 많이 고민했습니다.

먼저 Figma 화면을 보고 반복되는 UI부터 찾았습니다. 아이콘만 들어간 버튼(닫기·수정·삭제·검색·뒤로가기)과 태그칩은 여러 화면에서 똑같이 쓰여서, IconButton과 TagChip을 공통 컴포넌트로 먼저 만들고 크기 같은 차이만 props로 받게 했습니다.

나머지 화면은 역할 단위로 나눴습니다. 상단바(TopBar), 검색창(SearchInput), 카드 목록(MemoBoard)과 카드 한 장(MemoCard), 상세 모달(Modal, MemoView), 편집 화면(MemoEditor)이 각자 하나의 일만 하도록 했습니다. 모달의 어두운 배경·닫기 동작은 Modal에, 안의 내용은 MemoView에 따로 둬서 역할이 섞이지 않게 했습니다.

state도 필요한 곳에만 두려고 했습니다. 예를 들어 태그선택이 열렸는지나 편집 중인 제목·본문처럼 그 컴포넌트만 알면 되는 값은 분리했습니다. 메모 목록처럼 여러 곳에서 쓰는 데이터는 useMemos 커스텀 훅으로 모아서 추가·수정·삭제·고정 로직과 localStorage 저장을 한 곳에서 관리했습니다.

구현 결과

image image

Review Question

1. Virtual DOM은 무엇이고, 이를 사용함으로써 얻는 이점은 무엇인가요?

Virtual DOM은 실제 화면의 설계도를 JS 객체로 만들어둔 것이다.
상태가 바뀌면 React가 새 설계도를 그린 뒤 이전 설계도와 비교해서, 달라진 부분만 실제 화면에 반영한다.
"어디를 어떻게 고칠지" 신경 쓰지 않고 "이 상태면 화면은 이렇게 생겼다"만 적으면 됨.

지난 과제에서는 innerHTML로 모달을 통째로 다시 그리면 입력하던 제목·본문이 날아가서, 태그를 바꿀 때만 DOM을 직접 골라서 고쳐야 했다.
지금은 setTag만 호출하면 React가 카드 색과 태그칩만 바꾸고 입력창은 그대로 둔다.
즐겨찾기 버튼도 마찬가지로, togglePin으로 pinned만 바꾸면 그 카드의 별 아이콘과 위치만 바뀐다.

2. React에서 컴포넌트를 분리하는 기준은 무엇이며, 컴포넌트 분리를 통해 얻을 수 있는 이점은 무엇인가요?

여러 번 반복되는 모양: 똑같이 생긴 걸 한 번만 만들어 여러 곳에서 쓴다.
한 가지 일만 하는 부분: 화면을 역할 별로 나눈다.
state를 가진 부분: 그 상태를 쓰는 곳 안에만 둔다.

이렇게 나누면 같은 코드를 복붙 할 필요가 없고, 고칠 때 그 파일만 보면 되고, state가 어디 있는지 찾기 쉽다.

이번 과제에서 일관된 기준과 효율성을 우선으로 컴포넌트를 분리해보려 노력했다.

반복되는 모양:
IconButton은 닫기·수정·삭제·검색·메모추가·뒤로가기 버튼이 전부 쓴다.
TagChip은 태그 필터, 상세 모달, 편집 화면에서 같이 쓴다.

한 가지 일:
MemoCard는 카드 한 장, MemoBoard는 카드 목록과 빈 화면
SearchInput은 검색창, Modal은 어두운 배경과 닫기 동작만 맡는다.

자기만의 상태: TagSelect의 드롭다운 열림 여부(open)는 다른 컴포넌트가 알 필요가 없어서 안에 뒀다->나중에 메모 수정화면에서 재사용
MemoEditor의 입력 중인 제목·본문도 저장 버튼을 누르기 전까진 편집 화면만 알면 된다.
반대로 메모 목록은 여러 곳이 같이 써서 useMemos 훅에 모았다.

3. React 컴포넌트의 생명주기에 대해서 설명해주세요.

컴포넌트는 세 단계를 거친다.

마운트: 화면에 처음 나타남
업데이트: 상태나 props가 바뀌어서 다시 그려짐
언마운트: 화면에서 사라짐

함수형 컴포넌트에서는 useEffect로 이 시점에 할 일을 정한다. 뒤에 붙는 배열(의존성 배열)에 따라 실행 시점이 달라진다.

빈 배열 []: 처음 한 번만 실행
값이 들어 있을 때 [memos]: 그 값이 바뀔 때마다 실행
return한 함수(정리 함수): 사라질 때 또는 다음 실행 직전에 실행. 달아둔 이벤트를 떼는 데 쓴다.

마운트: useMemos의 useState(() => loadMemos())가 앱이 처음 뜰 때 localStorage에서 메모를 한 번만 불러온다.

업데이트: useMemos의 useEffect(() => saveMemos(memos), [memos])가 메모를 추가·수정·삭제·고정할 때마다 자동으로 저장한다.

마운트 + 언마운트: Modal은 열리면(마운트) Esc 키 리스너를 달고, 닫히면(언마운트) 정리 함수로 리스너를 뗀다. 안 떼면 모달이 없는데도 Esc 리스너가 계속 남는다.
값에 따라 붙였다 떼기: TagSelect는 [open]을 보고, 드롭다운이 열릴 때만 바깥 클릭·Esc 리스너를 달고 닫히면 정리 함수로 뗀다.

과제 정리

https://www.notion.so/CEOS-2-3dc1cc63126880bf823fd6d4a43f6b92?source=copy_link

@mrainbeat mrainbeat changed the title Minjun.kim 김민준 2주차 과제 제출합니다. Sep 15, 2026
@mrainbeat mrainbeat changed the title 김민준 2주차 과제 제출합니다. [2주차] 김민준 과제 제출합니다. Sep 15, 2026

@sumin0423 sumin0423 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

민준님 2주차 과제 고생하셨어요! 메모 관리 로직을 useMemos로 분리해두셔서 코드 흐름을 이해하기 좋았던 것 같아요. 다음 주차도 화이팅입니당!! 😊

Comment thread src/hooks/useMemos.js
};

return { memos, addMemo, updateMemo, deleteMemo, togglePin };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메모 추가·수정·삭제 로직을 useMemos에 모아두신 점이 좋은 것 같아요! App에서는 함수를 호출하는 부분만 보면 돼서 화면 흐름을 한눈에 보기 편해요 :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동감입니다ㅎㅎ! 메모 CRUD와 영속성을 훅 하나로 캡슐화하신 게 정말 깔끔하네요!
addMemo에서 id/date/createdAt/pinned 같은 파생 필드를 훅 안에서 채워주는 것도 좋구요!

Comment thread src/utils/memo.js
@@ -0,0 +1,34 @@
export function sortByNewest(list) {
return [...list].sort((a, b) => b.createdAt - a.createdAt);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정렬 전에 [...list]로 배열을 복사해주신 부분이 원본 메모 배열은 유지하면서 정렬된 목록을 만드는 방식이라 좋은 것 같아요 !

<article
onClick={() => onOpen(memo.id)}
className={`flex size-[285px] shrink-0 cursor-pointer flex-col gap-4 rounded-[20px] p-6 text-white-00 shadow-[0_4px_6px_rgba(0,0,0,0.05)] ${bg}`}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 article에 클릭 이벤트가 있어서 키보드만으로는 상세 보기를 열기 어려워보여요. 고정 버튼과 별도로 상세 보기용 버튼을 두면 Tab으로 이동하고 Enter로 열 수도 있을 것 같아서 추천드립니다 !

Comment thread src/utils/storage.js
return raw === null ? SAMPLE_MEMOS : JSON.parse(raw);
} catch {
return [];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON 파싱 오류를 처리해주신 점 좋아요! 다만 저장된 값이 "null"이나 "{}"이면 파싱은 성공해서 이후 filter나 find에서 오류가 날 수 있습니다. 파싱 결과가 배열인지도 확인해주면 더욱 좋아질 것 같아요 !

@minnngo minnngo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체적으로 훅 분리, 함수 추출이 잘 돼 있어서 편하게 읽었네요!
이번 과제도 수고많으셨습니다~~

Comment thread src/hooks/useMemos.js
};

return { memos, addMemo, updateMemo, deleteMemo, togglePin };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동감입니다ㅎㅎ! 메모 CRUD와 영속성을 훅 하나로 캡슐화하신 게 정말 깔끔하네요!
addMemo에서 id/date/createdAt/pinned 같은 파생 필드를 훅 안에서 채워주는 것도 좋구요!

Comment thread src/utils/memo.js
Comment on lines +1 to +22
export function sortByNewest(list) {
return [...list].sort((a, b) => b.createdAt - a.createdAt);
}

export function partition(list, predicate) {
const matched = [];
const rest = [];
list.forEach((item) => {
if (predicate(item)) matched.push(item);
else rest.push(item);
});
return [matched, rest];
}

export function formatDate(date) {
const pad = (n) => String(n).padStart(2, "0");
return `${date.getFullYear()}.${pad(date.getMonth() + 1)}.${pad(date.getDate())}`;
}

export function generateId() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정렬, 분할, 날짜 포맷, ID 생성을 전부 함수로 빼두시고, 이름도 명확해서 코드 읽기에 편했습니다~~

import { TAGS, TAG_STYLES } from "../../constants/tag";
import TagChip from "./TagChip";

export default function TagSelect({ value, onChange, includeAll = false }) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includeAll prop 하나로 헤더 드롭다운과에디터의 태그 선택을 같은 컴포넌트로 처리하신 부분 좋습니다!

Comment thread src/utils/storage.js
Comment on lines +14 to +16
export function saveMemos(memos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(memos));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadMemos에는 try/catch가 있는데 saveMemos에는 빼먹으신 것 같아요!
localStorage.setItem은 저장 용량이 꽉 차거나 프라이빗 모드일 때마다 에러를 던지는데 해당 상황에서 앱이 멈추는걸 방지하기 위해 읽기와 동일하게 감싸주면 더 좋겠네요!

Comment on lines +12 to +20
return (
<div
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
className="fixed inset-0 z-50 flex items-center justify-center bg-[rgba(0,27,81,0.5)] p-6"
>
{children}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESC와 바깥 클릭까지 챙기신 점은 좋은데, fixed inset-0 div라서 모달이 떠 있어도 뒷배경이 휠로 스크롤되고 Tab을 누르면 포커스가 모달 밖으로 새나가고 있어요! 네이티브 <dialog> + showModal()로 바꾸면 스크롤 잠금이나 포커스 트랩, ESC 닫기 등이 모두 적용되니 참고해보세요!

https://developer.mozilla.org/ko/docs/Web/HTML/Reference/Elements/dialog
https://blog.logrocket.com/creating-reusable-pop-up-modal-react/

@baek-32 baek-32 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

민준님 2주차 과제도 수고 많으셨습니다. 다음주도 화이팅입니다!

export default function EmptyState() {
return (
<div className="flex min-h-[480px] w-full flex-col items-center justify-center gap-7 rounded-3xl border-2 border-dashed border-blue-02 text-center">
<img src={iconEmptyStateAdd} alt="" className="size-30" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EmptyState의 아이콘을 단순 이미지 대신 클릭 가능한 버튼으로 만들고, 메모 추가 함수를 onClick에 연결해도 좋을 것 같습니다! 그러면 사용자가 아이콘 빈 화면 아이콘으로도 새 메모를 작성할 수 있어 UX가 더 자연스러워질 것 같아요.

aria-haspopup="listbox"
aria-expanded={open}
onClick={() => setOpen((prev) => !prev)}
className="flex cursor-pointer"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

태그를 변경할 때 Work, Daily, Others의 글자 길이 차이 때문에 태그버튼 너비도 같이 달라지는 것 같습니다!
태그버튼에 일정한 width를 지정해두면 선택된 태그가 바뀌어도 크기가 유지돼서 UI가 더 안정적으로 보일 것 같아요.

document.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [open]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

드롭다운 바깥 부분 클릭과 esc로 닫을 수 있도록 처리한 점과 등록한 이벤트도 함께 정리될 수 있게 구현해주신 점이 좋았습니다!

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.

4 participants