Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[15팀 김재환] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #24

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 35 additions & 17 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,86 @@

#### 가상돔을 기반으로 렌더링하기

- [ ] createVNode 함수를 이용하여 vNode를 만든다.
- [ ] normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- [ ] createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- [ ] 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.
- [x] createVNode 함수를 이용하여 vNode를 만든다.
- [x] normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- [x] createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- [x] 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.

#### 이벤트 위임

- [ ] 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- [ ] 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- [ ] 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다
- [x] 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- [x] 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- [x] 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다

### 심화 과제

#### 1) Diff 알고리즘 구현

- [ ] 초기 렌더링이 올바르게 수행되어야 한다
- [ ] diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- [ ] 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- [ ] 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- [ ] 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다
- [x] 초기 렌더링이 올바르게 수행되어야 한다
- [x] diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- [x] 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- [x] 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- [x] 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다

#### 2) 포스트 추가/좋아요 기능 구현

- [ ] 비사용자는 포스트 작성 폼이 보이지 않는다
- [ ] 비사용자는 포스트에 좋아요를 클릭할 경우, 경고 메세지가 발생한다.
- [ ] 사용자는 포스트 작성 폼이 보인다.
- [ ] 사용자는 포스트를 추가할 수 있다.
- [ ] 사용자는 포스트에 좋아요를 클릭할 경우, 좋아요가 토글된다.
- [x] 비사용자는 포스트 작성 폼이 보이지 않는다
- [X] 비사용자는 포스트에 좋아요를 클릭할 경우, 경고 메세지가 발생한다.
- [x] 사용자는 포스트 작성 폼이 보인다.
- [x] 사용자는 포스트를 추가할 수 있다.
- [x] 사용자는 포스트에 좋아요를 클릭할 경우, 좋아요가 토글된다.

## 과제 셀프회고
이번 과제를 통해서 가상돔에 대한 개념을 배울 수 있었고, 이벤트 위임과 Diff 알고리즘에 대해서 학습할 수 있게 되었습니다.
아직 개념에 대해 완전히 이해할 수 있는 상태는 아니여서 복습과 심화학습이 필요할 것 같습니다.
이번 과제를 처음부터 복기하면서 다시 확인이 필요할 것 같습니다.

<!-- 과제에 대한 회고를 작성해주세요 -->

### 기술적 성장

- 새로 학습한 개념
1. 재귀함수가 무엇인지, 어떻게 동작하는지 알 수 있었습니다.

<!-- 예시
- 새로 학습한 개념
- 기존 지식의 재발견/심화
- 구현 과정에서의 기술적 도전과 해결
-->

### 코드 품질

<!-- 예시
- 특히 만족스러운 구현
- 리팩토링이 필요한 부분
- 코드 설계 관련 고민과 결정
-->

### 학습 효과 분석
- 학습을 진행하면서 알고리즘 해결 문제와 JavaScript에 대한 이해도가 많이 취약하다는 것을 알게되었습니다.
- 알고리즘 구현에 있어 필요한 메소드가 무엇이 있는지에 대한 학습이 필요하다는 것을 알게되었습니다.
<!-- 예시
- 가장 큰 배움이 있었던 부분
- 추가 학습이 필요한 영역
- 실무 적용 가능성
-->

### 과제 피드백
- 재귀 함수나 알고리즘 사고가 필요한 부분에서 많은 어려움을 겪었습니다.
- 가상돔과 Diff 알고리즘 부분은 코드는 작성했지만, 개념적인 이해가 부족한 상태입니다.
<!-- 예시
- 과제에서 모호하거나 애매했던 부분
- 과제에서 좋았던 부분
-->

## 리뷰 받고 싶은 내용
1. JavaScript의 기본기를 강화하고 싶은데 어떻게 하면 좋을까요?
- 과제를 진행하면서 JavaScript의 기본 메소드나 문법에 대한 이해가 부족하다고 느꼈습니다.
- JavaScript의 어떤 영역을 중점적으로 공부해야 이번 과제를 수월하게 진행하는데에 도움이 되는지 궁금합니다.

2. 알고리즘 사고에 대한 학습방법이 궁금합니다.
- 재귀 함수가 무엇인지 이번 과제에서 어떻게 동작했는지에 대해서는 간신히 이해는 하였지만 다음에도 사용할 수 있을것 같지는 않습니다..
- 이런 알고리즘 사고를 향상시킬 수 있는 추천하는 방법이 있을까요?
<!--
피드백 받고 싶은 내용을 구체적으로 남겨주세요
모호한 요청은 피드백을 남기기 어렵습니다.
Expand Down
7 changes: 6 additions & 1 deletion src/components/posts/Post.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export const Post = ({
content,
likeUsers,
activationLike = false,
onClick,
}) => {
const testFunc = () => {
console.log(author);
};
return (
<div className="bg-white rounded-lg shadow p-4 mb-4">
<div className="flex items-center mb-2">
Expand All @@ -21,8 +25,9 @@ export const Post = ({
<div className="mt-2 flex justify-between text-gray-500">
<span
className={`like-button cursor-pointer${activationLike ? " text-blue-500" : ""}`}
onClick={(e) => onClick(e)} // 이벤트 객체 전달
>
좋아요 {likeUsers.length}
좋아요 {`${likeUsers.length}`}
</span>
<span>댓글</span>
<span>공유</span>
Expand Down
3 changes: 2 additions & 1 deletion src/components/posts/PostForm.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsx createVNode */
import { createVNode } from "../../lib";

export const PostForm = () => {
export const PostForm = ({ handlePostSubmit }) => {
return (
<div className="mb-4 bg-white rounded-lg shadow p-4">
<textarea
Expand All @@ -12,6 +12,7 @@ export const PostForm = () => {
<button
id="post-submit"
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded"
onClick={handlePostSubmit}
>
게시
</button>
Expand Down
46 changes: 44 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
import { addEvent } from "./eventManager";
import { normalizeVNode } from "./normalizeVNode";

export function createElement(vNode) {}
export function createElement(vNode) {
const normalVNode = normalizeVNode(vNode);

function updateAttributes($el, props) {}
if (typeof normalVNode == "object") {
if (typeof vNode.type == "function") {
console.log("에러?");
throw new Error("컴포넌트 예외");
}

const fragment = document.createDocumentFragment();
if (Array.isArray(normalVNode)) {
normalVNode.forEach((data) => {
fragment.appendChild(createElement(data));
});
return fragment;
}

// 객체(단일 Virtual DOM 노드) 처리
const $el = document.createElement(normalVNode.type);
if (normalVNode.props) {
updateAttributes($el, normalVNode.props);
}
normalVNode.children.forEach((data) => {
$el.appendChild(createElement(data));
});
return $el;
}

const result = document.createTextNode(normalVNode);
return result;
}

function updateAttributes($el, props) {
Object.keys(props).forEach((key) => {
if (key.startsWith("on") && typeof props[key] == "function") {
const eventType = key.toLowerCase().substring(2);
addEvent($el, eventType, props[key]);
} else if (key == "className") {
$el.setAttribute("class", props["className"]);
} else {
$el.setAttribute(key, props[key]);
}
});
}
13 changes: 12 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
export function createVNode(type, props, ...children) {
return {};
return {
type,
props,
children: children.flat(Infinity).filter((value) => {
if (value && typeof value != "boolean") {
return true;
} else if (typeof value == "number" || typeof value == "string") {
return true;
}
return false;
}),
};
}
44 changes: 41 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
export function setupEventListeners(root) {}
const eventMap = new Map();
let rootElement = null;

export function addEvent(element, eventType, handler) {}
export function setupEventListeners(root) {
rootElement = root;
eventMap.forEach((handlers, eventType) => {
rootElement.addEventListener(eventType, handleEvent);
});
}

export function removeEvent(element, eventType, handler) {}
function handleEvent(event) {
let target = event.target;
while (target && target !== rootElement) {
const elementHandlers = eventMap.get(event.type).get(target);
if (elementHandlers) {
elementHandlers.forEach((handler) => handler(event));
break;
}
target = target.parentNode;
}
}

export function addEvent(element, eventType, handler) {
if (!eventMap.has(eventType)) {
eventMap.set(eventType, new WeakMap());
}
const elementMap = eventMap.get(eventType);
if (!elementMap.has(element)) {
elementMap.set(element, new Set());
}
elementMap.get(element).add(handler);
}

export function removeEvent(element, eventType, handler) {
const elementMap = eventMap.get(eventType);
const elementHandlers = elementMap.get(element);
if (elementHandlers) {
elementHandlers.delete(handler);
if (elementHandlers.size === 0) {
elementMap.delete(element);
}
}
}
36 changes: 36 additions & 0 deletions src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
import { createVNode } from "./createVNode";

export function normalizeVNode(vNode) {
// undefined / null / boolean인 경우, 빈 문자열 출력
if (vNode == undefined || vNode == null || typeof vNode === "boolean") {
return "";
}

// 문자열 또는 숫자일 경우, 문자열 출력
if (typeof vNode == "string" || typeof vNode == "number") {
return vNode.toString();
}

// 함수인 경우,
if (typeof vNode == "object") {
// console.log("1. vNode는", vNode);
if (typeof vNode.type == "function") {
const res = vNode.type({
...vNode.props,
children: vNode.children,
});
// console.log("2. res는", res);
return normalizeVNode(res);
}
if (!Array.isArray(vNode)) {
return createVNode(
vNode.type,
vNode.props,
...vNode.children.map((child) => {
// console.log("child는?", child);
if (child != "") {
return normalizeVNode(child);
}
}),
);
}
}
return vNode;
}
16 changes: 15 additions & 1 deletion src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,22 @@ import { createElement } from "./createElement";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";

const oldNodeMap = new WeakMap();

export function renderElement(vNode, container) {
// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
const oldNode = oldNodeMap.get(container);
const newNode = normalizeVNode(vNode);

if (!oldNode) {
container.appendChild(createElement(newNode));
} else {
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
updateElement(container, newNode, oldNode);
}

oldNodeMap.set(container, newNode);

// 렌더링이 완료되면 container에 이벤트를 등록한다.
setupEventListeners(container);
}
Loading
Loading