diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index c5a43bb3..678e7580 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -2,65 +2,59 @@
### 기본과제
-#### 1) 라우팅 구현:
-- [ ] History API를 사용하여 SPA 라우터 구현
- - [ ] '/' (홈 페이지)
- - [ ] '/profile' (프로필 페이지)
- - [ ] '/404' (Not Found 페이지)
-- [ ] 각 라우트에 해당하는 컴포넌트 렌더링 함수 작성
-- [ ] 네비게이션 이벤트 처리 (링크 클릭 시 페이지 전환)
-
-#### 2) 사용자 관리 기능:
-- [ ] LocalStorage를 사용한 간단한 사용자 데이터 관리
- - [ ] 사용자 정보 저장 (이름, 간단한 소개)
- - [ ] 로그인 상태 관리 (로그인/로그아웃 토글)
-- [ ] 로그인 폼 구현
- - [ ] 사용자 이름 입력 및 검증
- - [ ] 로그인 버튼 클릭 시 LocalStorage에 사용자 정보 저장
-- [ ] 로그아웃 기능 구현
- - [ ] 로그아웃 버튼 클릭 시 LocalStorage에서 사용자 정보 제거
-
-#### 3) 프로필 페이지 구현:
-- [ ] 현재 로그인한 사용자의 정보 표시
- - [ ] 사용자 이름
- - [ ] 간단한 소개
-- [ ] 프로필 수정 기능
- - [ ] 사용자 소개 텍스트 수정 가능
- - [ ] 수정된 정보 LocalStorage에 저장
-
-#### 4) 컴포넌트 기반 구조 설계:
-- [ ] 재사용 가능한 컴포넌트 작성
- - [ ] Header 컴포넌트
- - [ ] Footer 컴포넌트
-- [ ] 페이지별 컴포넌트 작성
- - [ ] HomePage 컴포넌트
- - [ ] ProfilePage 컴포넌트
- - [ ] NotFoundPage 컴포넌트
-
-#### 5) 상태 관리 초기 구현:
-- [ ] 간단한 상태 관리 시스템 설계
- - [ ] 전역 상태 객체 생성 (예: 현재 로그인한 사용자 정보)
-- [ ] 상태 변경 함수 구현
- - [ ] 상태 업데이트 시 관련 컴포넌트 리렌더링
-
-#### 6) 이벤트 처리 및 DOM 조작:
-- [ ] 사용자 입력 처리 (로그인 폼, 프로필 수정 등)
-- [ ] 동적 컨텐츠 렌더링 (사용자 정보 표시, 페이지 전환 등)
-
-#### 7) 기본적인 에러 처리:
-- [ ] 잘못된 라우트 접근 시 404 페이지로 리다이렉션
-- [ ] 로그인 실패 시 에러 메시지 표시
-
-### 심화과제
-
-#### 1) 고급 라우팅
-- [ ] 라우트 가드 구현
- - [ ] 로그인 상태에 따른 접근 제어
- - [ ] 비로그인 사용자의 특정 페이지 접근 시 로그인 페이지로 리다이렉션
-
-#### 2) 고급 이벤트 처리
-
-- [ ] 이벤트 위임 활용
+#### 1) createVNode
+
+- [ ] 올바른 구조의 vNode를 생성해야 한다
+- [ ] 여러 자식을 처리해야 한다
+- [ ] 자식 배열을 평탄화해야 한다
+- [ ] 중첩 구조를 올바르게 표현해야 한다
+- [ ] JSX로 표현한 결과가 createVNode 함수 호출과 동일해야 한다
+
+#### 2) createElement
+
+- [ ] 문자열 입력에 대해 텍스트 노드를 생성해야 한다
+- [ ] 숫자 입력에 대해 텍스트 노드를 생성해야 한다
+- [ ] null 입력에 대해 빈 텍스트 노드를 생성해야 한다
+- [ ] false 입력에 대해 빈 텍스트 노드를 생성해야 한다
+- [ ] 배열 입력에 대해 DocumentFragment를 생성해야 한다
+- [ ] 함수 컴포넌트를 처리해야 한다
+- [ ] 올바른 속성으로 요소를 생성해야 한다
+- [ ] 이벤트 리스너를 연결해야 한다
+- [ ] 중첩된 자식 요소를 올바르게 처리해야 한다
+- [ ] 깊게 중첩된 구조를 처리해야 한다
+- [ ] 혼합 콘텐츠(텍스트와 요소)를 처리해야 한다
+- [ ] 빈 자식 배열을 처리해야 한다
+- [ ] undefined 자식을 무시해야 한다
+- [ ] 불리언 속성을 처리해야 한다
+- [ ] 데이터 속성을 처리해야 한다
+
+#### 3) 컴포넌트를 jsx로 정의하여 사용했는지 확인하기
+- [ ] Post
+- [ ] PostForm
+- [ ] Header
+- [ ] Navigation
+- [ ] Footer
+- [ ] HomePage
+- [ ] LoginPage
+- [ ] NotFoundPage
+- [ ] ProfilePage
+- [ ] HomePage를 렌더링 했을 때 html 문자열로 잘 변환되는지 확인
+
+### 심화 과제
+
+#### 1) Diff 알고리즘 구현
+
+- [ ] 초기 렌더링이 올바르게 수행되어야 한다
+- [ ] diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
+- [ ] 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
+- [ ] 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
+- [ ] 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다
+
+#### 2) 이벤트 위임 구현
+
+- [ ] 이벤트가 위임 방식으로 등록되어야 한다
+- [ ] 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
+- [ ] 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다
## 리뷰 받고 싶은 내용
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..f840004f
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,8 @@
+__tests__
+*.md
+.babelrc
+vite.config.js
+
+# Ignore GitHub workflow files
+.github/workflows/**/*.yml
+.github/workflows/**/*.yaml
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..d16bc4c2
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 120,
+ "tabWidth": 2,
+ "semi": true
+}
diff --git a/package-lock.json b/package-lock.json
index b1e3134e..cad6c045 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@vitejs/plugin-react": "^4.3.1",
"@vitest/ui": "^2.1.1",
"jsdom": "^25.0.0",
+ "prettier": "3.3.3",
"vite": "^5.1.0",
"vitest": "^2.1.1"
}
@@ -864,9 +865,9 @@
"dev": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.2.tgz",
- "integrity": "sha512-8Ao+EDmTPjZ1ZBABc1ohN7Ylx7UIYcjReZinigedTOnGFhIctyGPxY2II+hJ6gD2/vkDKZTyQ0e7++kwv6wDrw==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz",
+ "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==",
"cpu": [
"arm"
],
@@ -877,9 +878,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.2.tgz",
- "integrity": "sha512-I+B1v0a4iqdS9DvYt1RJZ3W+Oh9EVWjbY6gp79aAYipIbxSLEoQtFQlZEnUuwhDXCqMxJ3hluxKAdPD+GiluFQ==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz",
+ "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==",
"cpu": [
"arm64"
],
@@ -890,9 +891,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.2.tgz",
- "integrity": "sha512-BTHO7rR+LC67OP7I8N8GvdvnQqzFujJYWo7qCQ8fGdQcb8Gn6EQY+K1P+daQLnDCuWKbZ+gHAQZuKiQkXkqIYg==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz",
+ "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==",
"cpu": [
"arm64"
],
@@ -903,9 +904,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.2.tgz",
- "integrity": "sha512-1esGwDNFe2lov4I6GsEeYaAMHwkqk0IbuGH7gXGdBmd/EP9QddJJvTtTF/jv+7R8ZTYPqwcdLpMTxK8ytP6k6Q==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz",
+ "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==",
"cpu": [
"x64"
],
@@ -916,9 +917,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.2.tgz",
- "integrity": "sha512-GBHuY07x96OTEM3OQLNaUSUwrOhdMea/LDmlFHi/HMonrgF6jcFrrFFwJhhe84XtA1oK/Qh4yFS+VMREf6dobg==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz",
+ "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==",
"cpu": [
"arm"
],
@@ -929,9 +930,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.2.tgz",
- "integrity": "sha512-Dbfa9Sc1G1lWxop0gNguXOfGhaXQWAGhZUcqA0Vs6CnJq8JW/YOw/KvyGtQFmz4yDr0H4v9X248SM7bizYj4yQ==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz",
+ "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==",
"cpu": [
"arm"
],
@@ -942,9 +943,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.2.tgz",
- "integrity": "sha512-Z1YpgBvFYhZIyBW5BoopwSg+t7yqEhs5HCei4JbsaXnhz/eZehT18DaXl957aaE9QK7TRGFryCAtStZywcQe1A==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz",
+ "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==",
"cpu": [
"arm64"
],
@@ -955,9 +956,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.2.tgz",
- "integrity": "sha512-66Zszr7i/JaQ0u/lefcfaAw16wh3oT72vSqubIMQqWzOg85bGCPhoeykG/cC5uvMzH80DQa2L539IqKht6twVA==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz",
+ "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==",
"cpu": [
"arm64"
],
@@ -968,9 +969,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.2.tgz",
- "integrity": "sha512-HpJCMnlMTfEhwo19bajvdraQMcAq3FX08QDx3OfQgb+414xZhKNf3jNvLFYKbbDSGBBrQh5yNwWZrdK0g0pokg==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz",
+ "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==",
"cpu": [
"ppc64"
],
@@ -981,9 +982,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.2.tgz",
- "integrity": "sha512-/egzQzbOSRef2vYCINKITGrlwkzP7uXRnL+xU2j75kDVp3iPdcF0TIlfwTRF8woBZllhk3QaxNOEj2Ogh3t9hg==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz",
+ "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==",
"cpu": [
"riscv64"
],
@@ -994,9 +995,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.2.tgz",
- "integrity": "sha512-qgYbOEbrPfEkH/OnUJd1/q4s89FvNJQIUldx8X2F/UM5sEbtkqZpf2s0yly2jSCKr1zUUOY1hnTP2J1WOzMAdA==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz",
+ "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==",
"cpu": [
"s390x"
],
@@ -1007,9 +1008,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.2.tgz",
- "integrity": "sha512-a0lkvNhFLhf+w7A95XeBqGQaG0KfS3hPFJnz1uraSdUe/XImkp/Psq0Ca0/UdD5IEAGoENVmnYrzSC9Y2a2uKQ==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz",
+ "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==",
"cpu": [
"x64"
],
@@ -1020,9 +1021,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.2.tgz",
- "integrity": "sha512-sSWBVZgzwtsuG9Dxi9kjYOUu/wKW+jrbzj4Cclabqnfkot8Z3VEHcIgyenA3lLn/Fu11uDviWjhctulkhEO60g==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz",
+ "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==",
"cpu": [
"x64"
],
@@ -1033,9 +1034,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.2.tgz",
- "integrity": "sha512-t/YgCbZ638R/r7IKb9yCM6nAek1RUvyNdfU0SHMDLOf6GFe/VG1wdiUAsxTWHKqjyzkRGg897ZfCpdo1bsCSsA==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz",
+ "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==",
"cpu": [
"arm64"
],
@@ -1046,9 +1047,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.2.tgz",
- "integrity": "sha512-kTmX5uGs3WYOA+gYDgI6ITkZng9SP71FEMoHNkn+cnmb9Zuyyay8pf0oO5twtTwSjNGy1jlaWooTIr+Dw4tIbw==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz",
+ "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==",
"cpu": [
"ia32"
],
@@ -1059,9 +1060,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.2.tgz",
- "integrity": "sha512-Yy8So+SoRz8I3NS4Bjh91BICPOSVgdompTIPYTByUqU66AXSIOgmW3Lv1ke3NORPqxdF+RdrZET+8vYai6f4aA==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz",
+ "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==",
"cpu": [
"x64"
],
@@ -1207,9 +1208,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"node_modules/@vitejs/plugin-react": {
@@ -2142,6 +2143,21 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prettier": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
+ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -2234,12 +2250,12 @@
"dev": true
},
"node_modules/rollup": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.2.tgz",
- "integrity": "sha512-JWWpTrZmqQGQWt16xvNn6KVIUz16VtZwl984TKw0dfqqRpFwtLJYYk1/4BTgplndMQKWUk/yB4uOShYmMzA2Vg==",
+ "version": "4.22.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz",
+ "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==",
"dev": true,
"dependencies": {
- "@types/estree": "1.0.5"
+ "@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -2249,22 +2265,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.22.2",
- "@rollup/rollup-android-arm64": "4.22.2",
- "@rollup/rollup-darwin-arm64": "4.22.2",
- "@rollup/rollup-darwin-x64": "4.22.2",
- "@rollup/rollup-linux-arm-gnueabihf": "4.22.2",
- "@rollup/rollup-linux-arm-musleabihf": "4.22.2",
- "@rollup/rollup-linux-arm64-gnu": "4.22.2",
- "@rollup/rollup-linux-arm64-musl": "4.22.2",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.22.2",
- "@rollup/rollup-linux-riscv64-gnu": "4.22.2",
- "@rollup/rollup-linux-s390x-gnu": "4.22.2",
- "@rollup/rollup-linux-x64-gnu": "4.22.2",
- "@rollup/rollup-linux-x64-musl": "4.22.2",
- "@rollup/rollup-win32-arm64-msvc": "4.22.2",
- "@rollup/rollup-win32-ia32-msvc": "4.22.2",
- "@rollup/rollup-win32-x64-msvc": "4.22.2",
+ "@rollup/rollup-android-arm-eabi": "4.22.5",
+ "@rollup/rollup-android-arm64": "4.22.5",
+ "@rollup/rollup-darwin-arm64": "4.22.5",
+ "@rollup/rollup-darwin-x64": "4.22.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.22.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.22.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.22.5",
+ "@rollup/rollup-linux-arm64-musl": "4.22.5",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.22.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.22.5",
+ "@rollup/rollup-linux-x64-gnu": "4.22.5",
+ "@rollup/rollup-linux-x64-musl": "4.22.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.22.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.22.5",
+ "@rollup/rollup-win32-x64-msvc": "4.22.5",
"fsevents": "~2.3.2"
}
},
diff --git a/package.json b/package.json
index 59b8baca..0f45a6da 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@vitejs/plugin-react": "^4.3.1",
"@vitest/ui": "^2.1.1",
"jsdom": "^25.0.0",
+ "prettier": "3.3.3",
"vite": "^5.1.0",
"vitest": "^2.1.1"
}
diff --git a/src/App.jsx b/src/App.jsx
index 3764f6dc..7dfd85de 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,4 +1,38 @@
/** @jsx createVNode */
-import{ createVNode } from "./lib";
+import { createVNode } from './lib';
+import { globalStore } from './stores/index.js';
+import { NotFoundPage } from './pages/index.js';
-export const App = () => ({});
+export const App = ({ targetPage }) => {
+ const PageComponent = targetPage ?? NotFoundPage;
+ const error = globalStore.getState().error;
+
+ const closeError = (e) => {
+ e.preventDefault();
+ globalStore.setState({ error: null });
+ };
+
+ return (
+
+
+ {error ? (
+
+
+
+ 오류 발생!
+ {error.message || '알 수 없는 오류가 발생했습니다.'}
+
+
+
+
+ ) : (
+ ''
+ )}
+
+ );
+};
diff --git a/src/__tests__/chapter1-2/advanced.test.jsx b/src/__tests__/chapter1-2/advanced.test.jsx
index 01384637..94611aed 100644
--- a/src/__tests__/chapter1-2/advanced.test.jsx
+++ b/src/__tests__/chapter1-2/advanced.test.jsx
@@ -99,6 +99,53 @@ describe('Chapter1-2 > 심화과제 > Virtual DOM과 이벤트 관리', () => {
expect(container.innerHTML).toBe('Hello');
expect(container.firstChild).not.toBe(originalElement); // 다른 요소 참조 확인
});
+
+ it('함수형 컴포넌트가 업데이트될 때 필요한 부분만 렌더링해야 한다', () => {
+ const FuncComponent = ({ title, content }) => (
+
+ );
+
+ const initialVNode = ;
+ renderElement(initialVNode, container);
+
+ const originalH1 = container.querySelector('h1');
+ const originalP = container.querySelector('p');
+
+ const updatedVNode = ;
+ renderElement(updatedVNode, container);
+
+ expect(container.querySelector('h1')).toBe(originalH1);
+ expect(container.querySelector('p')).toBe(originalP);
+ expect(container.querySelector('h1').textContent).toBe('Updated Title');
+ expect(container.querySelector('p').textContent).toBe('Initial Content');
+ });
+
+ it('중첩된 함수형 컴포넌트에서 깊은 레벨의 변경사항만 업데이트해야 한다', () => {
+ const ChildComponent = ({ text }) => {text}
;
+ const ParentComponent = ({ title, childText }) => (
+
+
{title}
+
+
+ );
+
+ const initialVNode = ;
+ renderElement(initialVNode, container);
+
+ const originalH1 = container.querySelector('h1');
+ const originalP = container.querySelector('p');
+
+ const updatedVNode = ;
+ renderElement(updatedVNode, container);
+
+ expect(container.querySelector('h1')).toBe(originalH1);
+ expect(container.querySelector('p')).toBe(originalP);
+ expect(container.querySelector('h1').textContent).toBe('Parent Title');
+ expect(container.querySelector('p').textContent).toBe('Updated Child Text');
+ });
});
describe('이벤트 관리 > ', () => {
diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx
index 2fc9f16c..ce9bb6b5 100644
--- a/src/components/posts/Post.jsx
+++ b/src/components/posts/Post.jsx
@@ -1,4 +1,24 @@
/** @jsx createVNode */
-import{ createVNode } from "../../lib";
+import { createVNode } from '../../lib';
-export const Post = () => ({});
+export const Post = ({ id, author, time, content }) => {
+ return (
+
+
+
+
+
+
{content}
+
+
+ 좋아요
+
+ 댓글
+ 공유
+
+
+ );
+};
diff --git a/src/components/posts/PostForm.jsx b/src/components/posts/PostForm.jsx
index 7788cd67..e901b26e 100644
--- a/src/components/posts/PostForm.jsx
+++ b/src/components/posts/PostForm.jsx
@@ -1,4 +1,17 @@
/** @jsx createVNode */
-import{ createVNode } from "../../lib";
+import { createVNode } from '../../lib';
-export const PostForm = () => ({});
+export const PostForm = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/src/components/templates/Footer.jsx b/src/components/templates/Footer.jsx
index c2d3a109..acee97fb 100644
--- a/src/components/templates/Footer.jsx
+++ b/src/components/templates/Footer.jsx
@@ -1,4 +1,10 @@
/** @jsx createVNode */
-import{ createVNode } from "../../lib";
+import { createVNode } from '../../lib';
-export const Footer = () => ({});
+export const Footer = () => {
+ return (
+
+ );
+};
diff --git a/src/components/templates/Header.jsx b/src/components/templates/Header.jsx
index 2d799a6a..f53e8b31 100644
--- a/src/components/templates/Header.jsx
+++ b/src/components/templates/Header.jsx
@@ -1,4 +1,10 @@
/** @jsx createVNode */
-import{ createVNode } from "../../lib";
+import { createVNode } from '../../lib';
-export const Header = () => ({});
+export const Header = () => {
+ return (
+
+ );
+};
diff --git a/src/components/templates/Navigation.jsx b/src/components/templates/Navigation.jsx
index 63a020ea..c911852b 100644
--- a/src/components/templates/Navigation.jsx
+++ b/src/components/templates/Navigation.jsx
@@ -1,4 +1,48 @@
/** @jsx createVNode */
-import{ createVNode } from "../../lib";
+import { createVNode } from '../../lib';
-export const Navigation = () => ({});
+const getNavItemClass = (path) => {
+ const currentPath = window.location.pathname;
+ return currentPath === path ? 'text-blue-600 font-bold' : 'text-gray-600';
+};
+
+export const Navigation = ({ loggedIn }) => {
+ return (
+
+ );
+};
diff --git a/src/lib/createElement.js b/src/lib/createElement.js
index e6092bf3..45a6ea5d 100644
--- a/src/lib/createElement.js
+++ b/src/lib/createElement.js
@@ -1,14 +1,48 @@
-// TODO: createElement 함수 구현
-// 1. vNode가 falsy면 빈 텍스트 노드를 반환합니다.
-// 2. vNode가 문자열이나 숫자면 텍스트 노드를 생성하여 반환합니다.
-// 3. vNode가 배열이면 DocumentFragment를 생성하고 각 자식에 대해 createElement를 재귀 호출하여 추가합니다.
-// 4. vNode.type이 함수면 해당 함수를 호출하고 그 결과로 createElement를 재귀 호출합니다.
-// 5. 위 경우가 아니면 실제 DOM 요소를 생성합니다:
-// - vNode.type에 해당하는 요소를 생성
-// - vNode.props의 속성들을 적용 (이벤트 리스너, className, 일반 속성 등 처리)
-// - vNode.children의 각 자식에 대해 createElement를 재귀 호출하여 추가
-
-export function createElement(vNode) {
- // 여기에 구현하세요
- return {}
-}
+export const createElement = (vNode) => {
+ if (vNode === null || vNode === undefined || typeof vNode === 'boolean') {
+ return document.createTextNode('');
+ }
+
+ if (typeof vNode === 'string' || typeof vNode === 'number') {
+ return document.createTextNode(vNode);
+ }
+
+ if (Array.isArray(vNode)) {
+ const fragment = document.createDocumentFragment();
+ vNode.forEach((child) => fragment.appendChild(createElement(child)));
+ return fragment;
+ }
+
+ if (typeof vNode.type === 'function') {
+ return createElement(vNode.type(vNode.props));
+ }
+
+ const element = document.createElement(vNode.type);
+
+ if (vNode.props) {
+ Object.keys(vNode.props).forEach((key) => {
+ const value = vNode.props[key];
+
+ if (key.startsWith('on') && typeof value === 'function') {
+ const eventType = key.slice(2).toLowerCase();
+ element.addEventListener(eventType, value);
+ } else if (key === 'className') {
+ element.className = value;
+ } else if (key === 'style' && typeof value === 'object') {
+ Object.keys(value).forEach((styleName) => {
+ element.style[styleName] = value[styleName];
+ });
+ } else {
+ element.setAttribute(key, value);
+ }
+ });
+ }
+
+ if (vNode.children) {
+ vNode.children.forEach((child) => {
+ element.appendChild(createElement(child));
+ });
+ }
+
+ return element;
+};
diff --git a/src/lib/createElement__v2.js b/src/lib/createElement__v2.js
index fea8dd12..54fcf19d 100644
--- a/src/lib/createElement__v2.js
+++ b/src/lib/createElement__v2.js
@@ -1,11 +1,50 @@
-export function createElement__v2(vNode) {
- // 이 함수는 createElement의 개선된 버전입니다.
-// 1. falsy vNode 처리
-// 2. 문자열 또는 숫자 vNode 처리
-// 3. 배열 vNode 처리 (DocumentFragment 사용)
-// 4. 일반 요소 vNode 처리:
-// - 요소 생성
-// - 속성 설정 (이벤트 함수를 이벤트 위임 방식으로 등록할 수 있도록 개선)
-// - 자식 요소 추가
- return {}
-}
+import { addEvent } from './eventManager.js';
+
+export const createElement__v2 = (vNode) => {
+ if (vNode === null || vNode === undefined || typeof vNode === 'boolean') {
+ return document.createTextNode('');
+ }
+
+ if (typeof vNode === 'string' || typeof vNode === 'number') {
+ return document.createTextNode(vNode);
+ }
+
+ if (Array.isArray(vNode)) {
+ const fragment = document.createDocumentFragment();
+ vNode.forEach((child) => fragment.appendChild(createElement__v2(child)));
+ return fragment;
+ }
+
+ if (typeof vNode.type === 'function') {
+ return createElement__v2(vNode.type(vNode.props));
+ }
+
+ const element = document.createElement(vNode.type);
+
+ if (vNode.props) {
+ Object.keys(vNode.props).forEach((key) => {
+ const value = vNode.props[key];
+
+ if (key.startsWith('on') && typeof value === 'function') {
+ const eventType = key.slice(2).toLowerCase();
+ addEvent(element, eventType, value);
+ } else if (key === 'className') {
+ element.className = value;
+ } else if (key === 'style' && typeof value === 'object') {
+ Object.keys(value).forEach((styleName) => {
+ element.style[styleName] = value[styleName];
+ });
+ } else {
+ element.setAttribute(key, value);
+ }
+ });
+ }
+
+ if (vNode.children) {
+ vNode.children.forEach((child) => {
+ element.appendChild(createElement__v2(child));
+ });
+ }
+
+ return element;
+};
diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js
index bd0ee273..d8bf9c84 100644
--- a/src/lib/createVNode.js
+++ b/src/lib/createVNode.js
@@ -1,10 +1,7 @@
-// TODO: createVNode 함수 구현
-// 1. type, props, ...children을 매개변수로 받는 함수를 작성하세요.
-// 2. 반환값은 { type, props, children } 형태의 객체여야 합니다.
-// 3. children은 평탄화(flat)되어야 하며, falsy 값은 필터링되어야 합니다.
-// 4. Infinity를 사용하여 모든 깊이의 배열을 평탄화하세요.
-
-export function createVNode(type, props, ...children) {
- // 여기에 구현하세요
- return {}
-}
+export const createVNode = (type, props, ...children) => {
+ return {
+ type,
+ props,
+ children: children.flat(Infinity).filter((child) => !!child),
+ };
+};
diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js
index 3bf1eb31..54592524 100644
--- a/src/lib/eventManager.js
+++ b/src/lib/eventManager.js
@@ -1,40 +1,58 @@
-// eventManager.js
-
-// 이벤트 위임을 위한 전역 이벤트 맵
-// 이 맵은 이벤트 타입별로 요소와 해당 요소의 이벤트 핸들러를 저장합니다.
const eventMap = new Map();
-
-// 이벤트 위임이 설정될 루트 요소
let rootElement = null;
-// TODO: setupEventListeners 함수 구현
-// 이 함수는 루트 요소에 이벤트 위임을 설정합니다.
-export function setupEventListeners(root) {
- // 1. rootElement 설정
- // 2. 기존에 설정된 이벤트 리스너 제거 (있다면)
- // 3. eventMap에 등록된 모든 이벤트 타입에 대해 루트 요소에 이벤트 리스너 추가
- // 주의: 이벤트 캡처링을 사용하여 이벤트를 상위에서 하위로 전파
-}
-
-// TODO: handleEvent 함수 구현
-// 이 함수는 실제 이벤트가 발생했을 때 호출되는 핸들러입니다.
-function handleEvent(event) {
- // 1. 이벤트 타겟에서 시작하여 루트 요소까지 버블링
- // 2. 각 요소에 대해 해당 이벤트 타입의 핸들러가 있는지 확인
- // 3. 핸들러가 있다면 실행
- // 이를 통해 하위 요소에서 발생한 이벤트를 상위에서 효율적으로 처리할 수 있습니다.
-}
-
-// TODO: addEvent 함수 구현
-export function addEvent(element, eventType, handler) {
- // 1. eventMap에 이벤트 타입과 요소, 핸들러 정보 저장
- // 2. 필요한 경우 루트 요소에 새 이벤트 리스너 추가
- // 이 함수를 통해 개별 요소에 직접 이벤트를 붙이지 않고도 이벤트 처리 가능
-}
-
-// TODO: removeEvent 함수 구현
-export function removeEvent(element, eventType, handler) {
- // 1. eventMap에서 해당 요소와 이벤트 타입에 대한 핸들러 제거
- // 2. 해당 이벤트 타입의 모든 핸들러가 제거되면 루트 요소의 리스너도 제거
- // 이를 통해 더 이상 필요 없는 이벤트 핸들러를 정리하고 메모리 누수 방지
-}
+export const setupEventListeners = (root) => {
+ rootElement = root;
+
+ if (rootElement) {
+ eventMap.forEach((_, eventType) => {
+ rootElement.removeEventListener(eventType, handleEvent, true);
+ rootElement.addEventListener(eventType, handleEvent, true);
+ });
+ }
+};
+
+const handleEvent = (event) => {
+ let target = event.target;
+
+ while (target && target !== rootElement) {
+ const handlers = eventMap.get(event.type);
+ if (handlers) {
+ const handler = handlers.get(target);
+ if (handler) {
+ handler(event);
+ if (event.cancelBubble) {
+ break;
+ }
+ }
+ }
+ target = target.parentNode;
+ }
+};
+
+export const addEvent = (element, eventType, handler) => {
+ if (!eventMap.has(eventType)) {
+ eventMap.set(eventType, new Map());
+ }
+
+ const handlers = eventMap.get(eventType);
+ handlers.set(element, handler);
+
+ if (rootElement && !eventMap.get(eventType).size) {
+ rootElement.addEventListener(eventType, handleEvent, true);
+ }
+};
+
+export const removeEvent = (element, eventType, handler) => {
+ const handlers = eventMap.get(eventType);
+ if (handlers && handlers.get(element) === handler) {
+ handlers.delete(element);
+
+ if (handlers.size === 0) {
+ eventMap.delete(eventType);
+ if (rootElement) {
+ rootElement.removeEventListener(eventType, handleEvent, true);
+ }
+ }
+ }
+};
diff --git a/src/lib/index.js b/src/lib/index.js
index 8bdf3883..20705105 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -1,6 +1,9 @@
-export * from "./createStore";
-export * from "./createStorage";
-export * from "./createRouter";
-export * from "./createVNode";
-export * from "./createElement";
-export * from "./renderElement";
+export * from './createStore';
+export * from './createStorage';
+export * from './createRouter';
+export * from './createVNode';
+export * from './createElement';
+export * from './renderElement';
+export * from './eventManager';
+export * from './createElement__v2.js';
+export * from './createObserver.js';
diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js
index 9b96f62c..fafdd2ae 100644
--- a/src/lib/renderElement.js
+++ b/src/lib/renderElement.js
@@ -1,62 +1,126 @@
-// renderElement.js
import { addEvent, removeEvent, setupEventListeners } from './eventManager';
-import { createElement__v2 } from "./createElement__v2.js";
-
-// TODO: processVNode 함수 구현
-function processVNode() {
- // vNode를 처리하여 렌더링 가능한 형태로 변환합니다.
- // - null, undefined, boolean 값 처리
- // - 문자열과 숫자를 문자열로 변환
- // - 함수형 컴포넌트 처리 <---- 이게 제일 중요합니다.
- // - 자식 요소들에 대해 재귀적으로 processVNode 호출
-}
-
-// TODO: updateAttributes 함수 구현
-function updateAttributes() {
- // DOM 요소의 속성을 업데이트합니다.
- // - 이전 props에서 제거된 속성 처리
- // - 새로운 props의 속성 추가 또는 업데이트
- // - 이벤트 리스너, className, style 등 특별한 경우 처리
- // <이벤트 리스너 처리>
- // - TODO: 'on'으로 시작하는 속성을 이벤트 리스너로 처리
- // - 주의: 직접 addEventListener를 사용하지 않고, eventManager의 addEvent와 removeEvent 함수를 사용하세요.
- // - 이는 이벤트 위임을 통해 효율적으로 이벤트를 관리하기 위함입니다.
-}
-
-// TODO: updateElement 함수 구현
-function updateElement() {
- // 1. 노드 제거 (newNode가 없고 oldNode가 있는 경우)
- // TODO: oldNode만 존재하는 경우, 해당 노드를 DOM에서 제거
-
- // 2. 새 노드 추가 (newNode가 있고 oldNode가 없는 경우)
- // TODO: newNode만 존재하는 경우, 새 노드를 생성하여 DOM에 추가
-
- // 3. 텍스트 노드 업데이트
- // TODO: newNode와 oldNode가 둘 다 문자열 또는 숫자인 경우
- // TODO: 내용이 다르면 텍스트 노드 업데이트
-
- // 4. 노드 교체 (newNode와 oldNode의 타입이 다른 경우)
- // TODO: 타입이 다른 경우, 이전 노드를 제거하고 새 노드로 교체
-
- // 5. 같은 타입의 노드 업데이트
- // 5-1. 속성 업데이트
- // TODO: updateAttributes 함수를 호출하여 속성 업데이트
-
- // 5-2. 자식 노드 재귀적 업데이트
- // TODO: newNode와 oldNode의 자식 노드들을 비교하며 재귀적으로 updateElement 호출
- // HINT: 최대 자식 수를 기준으로 루프를 돌며 업데이트
-
- // 5-3. 불필요한 자식 노드 제거
- // TODO: oldNode의 자식 수가 더 많은 경우, 남은 자식 노드들을 제거
-}
-
-// TODO: renderElement 함수 구현
-export function renderElement(vNode, container) {
- // 최상위 수준의 렌더링 함수입니다.
- // - 이전 vNode와 새로운 vNode를 비교하여 업데이트
- // - 최초 렌더링과 업데이트 렌더링 처리
-
- // 이벤트 위임 설정
- // TODO: 렌더링이 완료된 후 setupEventListeners 함수를 호출하세요.
- // 이는 루트 컨테이너에 이벤트 위임을 설정하여 모든 하위 요소의 이벤트를 효율적으로 관리합니다.
-}
+import { createElement__v2 } from './createElement__v2.js';
+
+const processVNode = (vNode) => {
+ if (vNode === null || vNode === undefined || typeof vNode === 'boolean') {
+ return '';
+ }
+
+ if (typeof vNode === 'string' || typeof vNode === 'number') {
+ return String(vNode);
+ }
+
+ if (typeof vNode.type === 'function') {
+ return processVNode(vNode.type(vNode.props));
+ }
+
+ if (typeof vNode === 'object') {
+ const children = vNode.children.map(processVNode);
+ return { ...vNode, children };
+ }
+
+ return vNode;
+};
+
+const updateAttributes = (element, oldProps, newProps) => {
+ Object.keys(oldProps).forEach((key) => {
+ if (!(key in newProps)) {
+ if (key.startsWith('on') && typeof oldProps[key] === 'function') {
+ const eventType = key.slice(2).toLowerCase();
+ removeEvent(element, eventType, oldProps[key]);
+ } else {
+ element.removeAttribute(key);
+ }
+ }
+ });
+
+ Object.keys(newProps).forEach((key) => {
+ const newValue = newProps[key];
+ const oldValue = oldProps[key];
+
+ if (key.startsWith('on') && typeof newValue === 'function') {
+ const eventType = key.slice(2).toLowerCase();
+ if (oldValue !== newValue) {
+ if (oldValue) {
+ removeEvent(element, eventType, oldValue);
+ }
+ addEvent(element, eventType, newValue);
+ }
+ } else if (key === 'className') {
+ if (newValue !== oldValue) {
+ element.className = newValue;
+ }
+ } else if (key === 'style' && typeof newValue === 'object') {
+ Object.keys(newValue).forEach((styleName) => {
+ if (newValue[styleName] !== oldValue?.[styleName]) {
+ element.style[styleName] = newValue[styleName];
+ }
+ });
+
+ Object.keys(oldValue || {}).forEach((styleName) => {
+ if (!(styleName in newValue)) {
+ element.style[styleName] = '';
+ }
+ });
+ } else if (newValue !== oldValue) {
+ element.setAttribute(key, newValue);
+ }
+ });
+};
+
+const updateElement = (container, oldVNode, newVNode, index = 0) => {
+ const oldElement = container.childNodes[index];
+ const newElement = createElement__v2(newVNode);
+
+ if (!newVNode && oldVNode) {
+ container.removeChild(oldElement);
+ return;
+ }
+
+ if (newVNode && !oldVNode) {
+ container.appendChild(newElement);
+ return;
+ }
+
+ if (typeof newVNode === 'string' || typeof newVNode === 'number') {
+ if (newVNode !== oldVNode) {
+ container.replaceChild(newElement, oldElement);
+ }
+ return;
+ }
+
+ if (newVNode.type !== oldVNode.type) {
+ container.replaceChild(newElement, oldElement);
+ return;
+ }
+
+ updateAttributes(oldElement, oldVNode.props || {}, newVNode.props || {});
+
+ const oldChildren = oldVNode.children;
+ const newChildren = newVNode.children;
+ const maxLen = Math.max(oldChildren.length, newChildren.length);
+
+ for (let i = 0; i < maxLen; i++) {
+ if (i < newChildren.length) {
+ updateElement(oldElement, oldChildren[i], newChildren[i], i);
+ } else if (i < oldChildren.length) {
+ oldElement.removeChild(oldElement.childNodes[i]);
+ }
+ }
+};
+
+export const renderElement = (vNode, container) => {
+ const oldVNode = container._vNode;
+ const newVNode = processVNode(vNode);
+
+ if (!oldVNode) {
+ const newElement = createElement__v2(newVNode);
+ container.appendChild(newElement);
+ } else {
+ updateElement(container, oldVNode, newVNode);
+ }
+
+ container._vNode = newVNode;
+
+ setupEventListeners(container);
+};
diff --git a/src/main.jsx b/src/main.jsx
index 826615fa..7fe1971e 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,58 +1,53 @@
/** @jsx createVNode */
-import { createElement, createRouter, createVNode, renderElement } from "./lib";
-import { HomePage, LoginPage, ProfilePage } from "./pages";
-import { globalStore } from "./stores";
-import { ForbiddenError, UnauthorizedError } from "./errors";
-import { userStorage } from "./storages";
-import { addEvent, registerGlobalEvents } from "./utils";
-import { App } from "./App";
+import { createRouter, createVNode, renderElement } from './lib';
+import { HomePage, LoginPage, ProfilePage } from './pages';
+import { globalStore } from './stores';
+import { ForbiddenError, UnauthorizedError } from './errors';
+import { userStorage } from './storages';
+import { addEvent, registerGlobalEvents } from './utils';
+import { App } from './App';
-const router = createRouter({
- "/": HomePage,
- "/login": () => {
+export const router = createRouter({
+ '/': HomePage,
+ '/login': () => {
const { loggedIn } = globalStore.getState();
if (loggedIn) {
throw new ForbiddenError();
}
- return ;
+ return ;
},
- "/profile": () => {
+ '/profile': () => {
const { loggedIn } = globalStore.getState();
if (!loggedIn) {
throw new UnauthorizedError();
}
- return ;
+ return ;
},
});
-function logout() {
+const logout = () => {
globalStore.setState({ currentUser: null, loggedIn: false });
router.push('/login');
userStorage.reset();
-}
+};
function handleError(error) {
globalStore.setState({ error });
}
// 초기화 함수
-function render() {
+const render = () => {
const $root = document.querySelector('#root');
try {
- const $app = createElement();
- if ($root.hasChildNodes()) {
- $root.firstChild.replaceWith($app)
- } else{
- $root.appendChild($app);
- }
+ renderElement(, $root);
} catch (error) {
if (error instanceof ForbiddenError) {
- router.push("/");
+ router.push('/');
return;
}
if (error instanceof UnauthorizedError) {
- router.push("/login");
+ router.push('/login');
return;
}
@@ -61,9 +56,9 @@ function render() {
// globalStore.setState({ error });
}
registerGlobalEvents();
-}
+};
-function main() {
+const main = () => {
router.subscribe(render);
globalStore.subscribe(render);
window.addEventListener('error', handleError);
@@ -85,6 +80,6 @@ function main() {
});
render();
-}
+};
main();
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index 6e61db75..83d2852d 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -1,4 +1,29 @@
/** @jsx createVNode */
-import{ createVNode } from "../lib";
+import { createVNode } from '../lib';
+import { globalStore } from '../stores/index.js';
+import { Footer, Header, Navigation, Post, PostForm } from '../components/index.js';
-export const HomePage = () => ({});
+export const HomePage = () => {
+ const { loggedIn, posts } = globalStore.getState();
+
+ return (
+
+
+
+
+
+
+
+ {loggedIn && }
+
+ {posts.map(({ id, author, time, content }) => {
+ return
;
+ })}
+
+
+
+
+
+
+ );
+};
diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx
index 36ab4ccb..426720e4 100644
--- a/src/pages/LoginPage.jsx
+++ b/src/pages/LoginPage.jsx
@@ -1,4 +1,50 @@
/** @jsx createVNode */
-import{ createVNode } from "../lib";
+import { createVNode } from '../lib';
+import { globalStore } from '../stores/index.js';
+import { router } from '../main.jsx';
+import { userStorage } from '../storages/index.js';
-export const LoginPage = () => ({});
+export const LoginPage = () => {
+ const login = (e) => {
+ e.preventDefault();
+
+ const username = document.getElementById('username').value;
+ const currentUser = { username, email: '', bio: '' };
+ globalStore.setState({
+ currentUser,
+ loggedIn: true,
+ });
+ router.push('/');
+ userStorage.set(currentUser);
+ };
+
+ return (
+
+
+
항해플러스
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx
index 09f48ef8..6160239b 100644
--- a/src/pages/NotFoundPage.jsx
+++ b/src/pages/NotFoundPage.jsx
@@ -1,4 +1,18 @@
/** @jsx createVNode */
-import{ createVNode } from "../lib";
+import { createVNode } from '../lib';
-export const NotFoundPage = () => ({});
+export const NotFoundPage = () => {
+ return (
+
+
+
항해플러스
+
404
+
페이지를 찾을 수 없습니다
+
요청하신 페이지가 존재하지 않거나 이동되었을 수 있습니다.
+
+ 홈으로 돌아가기
+
+
+
+ );
+};
diff --git a/src/pages/ProfilePage.jsx b/src/pages/ProfilePage.jsx
index 70fb9a9e..a36d873a 100644
--- a/src/pages/ProfilePage.jsx
+++ b/src/pages/ProfilePage.jsx
@@ -1,4 +1,77 @@
/** @jsx createVNode */
-import{ createVNode } from "../lib";
+import { createVNode } from '../lib';
+import { Footer, Header, Navigation } from '../components/index.js';
+import { globalStore } from '../stores/index.js';
+import { userStorage } from '../storages/index.js';
-export const ProfilePage = () => ({});
+const updateProfile = (e) => {
+ e.preventDefault();
+ const formData = new FormData(e.target);
+ const updatedProfile = Object.fromEntries(formData);
+ const currentUser = { ...globalStore.getState().currentUser, ...updatedProfile };
+ globalStore.setState({ currentUser });
+ userStorage.set(currentUser);
+ alert('프로필이 업데이트되었습니다.');
+};
+
+export const ProfilePage = () => {
+ const { loggedIn, currentUser } = globalStore.getState();
+ const { username = '', email = '', bio = '' } = currentUser ?? {};
+
+ return (
+
+ );
+};
diff --git a/src/template.md b/src/template.md
index 99cab607..65beb921 100644
--- a/src/template.md
+++ b/src/template.md
@@ -74,9 +74,9 @@ const HomePage = () => {
${Navigation({ loggedIn })}
- ${loggedIn ? NotFoundPage() : ''}
+ ${loggedIn ? PostForm() : ''}
- ${posts.map(ProfilePage).join('')}
+ ${posts.map(Post).join('')}