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

[10팀 김서준][Chapter1-2] 프레임워크 없이 SPA 만들기 - Basic #117

Closed
wants to merge 11 commits into from
Closed
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
15 changes: 13 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
/** @jsx createVNode */
import{ createVNode } from "./lib";
import { createVNode } from './lib';
import { NotFoundPage } from "./pages";
import { globalStore } from './stores';

export const App = () => ({});
export const App = ({ targetPage }) => {
const PageComponent = targetPage ?? NotFoundPage;
const error = globalStore.getState().error;

return (
<div>
{!error ? PageComponent() : `<h2>오류 발생!</h2><p>의도적인 오류입니다.</p>`}
</div>
);
};
20 changes: 19 additions & 1 deletion src/components/posts/Post.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
/** @jsx createVNode */
import{ createVNode } from "../../lib";

export const Post = () => ({});
export const Post = ({ author, time, content, id }) => {
return (
<div className="bg-white rounded-lg shadow p-4 mb-4">
<div className="flex items-center mb-2">
<img src="https://via.placeholder.com/40" alt="프로필" className="rounded-full mr-2" />
<div>
<div className="font-bold">{author}</div>
<div className="text-gray-500 text-sm">{time}</div>
</div>
</div>
<p>{content}</p>
<div className="mt-2 flex justify-between text-gray-500">
<span className="like-button" data-post-id={id}>좋아요</span>
<span>댓글</span>
<span>공유</span>
</div>
</div>
);
};
9 changes: 8 additions & 1 deletion src/components/posts/PostForm.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
/** @jsx createVNode */
import{ createVNode } from "../../lib";

export const PostForm = () => ({});
export const PostForm = () => {
return (
<div className="mb-4 bg-white rounded-lg shadow p-4">
<textarea id="post-content" placeholder="무슨 생각을 하고 계신가요?" className="w-full p-2 border rounded"></textarea>
<button id="post-submit" className="mt-2 bg-blue-600 text-white px-4 py-2 rounded">게시</button>
</div>
);
};
8 changes: 7 additions & 1 deletion src/components/templates/Footer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
/** @jsx createVNode */
import{ createVNode } from "../../lib";

export const Footer = () => ({});
export const Footer = () => {
return (
<footer className="bg-gray-200 p-4 text-center">
<p>&copy; 2024 항해플러스. All rights reserved.</p>
</footer>
);
};
8 changes: 7 additions & 1 deletion src/components/templates/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
/** @jsx createVNode */
import{ createVNode } from "../../lib";

export const Header = () => ({});
export const Header = () => {
return (
<header className="bg-blue-600 text-white p-4 sticky top-0">
<h1 className="text-2xl font-bold">항해플러스</h1>
</header>
);
};
22 changes: 21 additions & 1 deletion src/components/templates/Navigation.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
/** @jsx createVNode */
import{ createVNode } from "../../lib";
import { userStorage } from "../../storages";
import { globalStore } from "../../stores";

export const Navigation = () => ({});
export const Navigation = () => {
const getNavItemClass = (path) => {
const currentPath = window.location.pathname;
return currentPath === path ? 'text-blue-600 font-bold' : 'text-gray-600';
}

const isLoggedIn = globalStore.getState().loggedIn;

return (
<nav className="bg-white shadow-md p-2 sticky top-14">
<ul className="flex justify-around">
<li><a href="/" className={getNavItemClass('/')} data-link>홈</a></li>
{!isLoggedIn ? <li><a href="/login" className={getNavItemClass('/login')} data-link>로그인</a></li> : ''}
{isLoggedIn ? <li><a href="/profile" className={getNavItemClass('/profile')} data-link>프로필</a></li> : ''}
{isLoggedIn ? <li><a href="#" id="logout" className="text-gray-600">로그아웃</a></li> : ''}
</ul>
</nav>
);
};
55 changes: 44 additions & 11 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
// 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 {}
if (!vNode) {
return document.createTextNode("");
}

if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(vNode);
}

if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();

vNode.forEach((node) => {
fragment.appendChild(createElement(node));
});

return fragment;
}

if (typeof vNode.type === 'function') {
const componentVNode = vNode.type(vNode.props || {});
return createElement(componentVNode);
}

const domElement = document.createElement(vNode.type);

if (vNode.props) {
Object.keys(vNode.props).forEach((key) => {
if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase(); // on뒤에 나오는 string
domElement.addEventListener(eventName, vNode.props[key]);
} else if (key === 'className') {
domElement.setAttribute('class', vNode.props[key]);
} else {
domElement.setAttribute(key, vNode.props[key]);
}
});
}

vNode.children.forEach((child) => {
domElement.appendChild(createElement(child));
});

return domElement;
}

56 changes: 47 additions & 9 deletions src/lib/createElement__v2.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
import { addEvent } from "./eventManager";

// TODO: createElement__v2 함수 구현
export function createElement__v2(vNode) {
// 이 함수는 createElement의 개선된 버전입니다.
// 1. falsy vNode 처리
// 2. 문자열 또는 숫자 vNode 처리
// 3. 배열 vNode 처리 (DocumentFragment 사용)
// 4. 일반 요소 vNode 처리:
// - 요소 생성
// - 속성 설정 (이벤트 함수를 이벤트 위임 방식으로 등록할 수 있도록 개선)
// - 자식 요소 추가
return {}
if (!vNode) {
return document.createTextNode("");
}

if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(vNode);
}

if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();

vNode.forEach((node) => {
fragment.appendChild(createElement__v2(node));
});

return fragment;
}

if (typeof vNode.type === 'function') {
const componentVNode = vNode.type(vNode.props || {});
return createElement__v2(componentVNode);
}

const domElement = document.createElement(vNode.type);

if (vNode.props) {
Object.keys(vNode.props).forEach((key) => {
if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase(); // on뒤에 나오는 string
addEvent(domElement, eventName, vNode.props[key]);
} else if (key === 'className') {
domElement.setAttribute('class', vNode.props[key]);
} else {
domElement.setAttribute(key, vNode.props[key]);
}
});
}

vNode.children.forEach((child) => {
domElement.appendChild(createElement__v2(child));
});

return domElement;
}

7 changes: 5 additions & 2 deletions src/lib/createVNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// 4. Infinity를 사용하여 모든 깊이의 배열을 평탄화하세요.

export function createVNode(type, props, ...children) {
// 여기에 구현하세요
return {}
return {
type,
props,
children: children.flat(Infinity).filter(Boolean)
}
}
76 changes: 51 additions & 25 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,66 @@
// eventManager.js

// 이벤트 위임을 위한 전역 이벤트 맵
// 이 맵은 이벤트 타입별로 요소와 해당 요소의 이벤트 핸들러를 저장합니다.
const eventMap = new Map();

// 이벤트 위임이 설정될 루트 요소
let rootElement = null;

// TODO: setupEventListeners 함수 구현
// 이 함수는 루트 요소에 이벤트 위임을 설정합니다.
export function setupEventListeners(root) {
// 1. rootElement 설정
// 2. 기존에 설정된 이벤트 리스너 제거 (있다면)
// 3. eventMap에 등록된 모든 이벤트 타입에 대해 루트 요소에 이벤트 리스너 추가
// 주의: 이벤트 캡처링을 사용하여 이벤트를 상위에서 하위로 전파
if (rootElement !== root) {
if (rootElement) {
eventMap.forEach((handlers, eventType) => {
rootElement.removeEventListener(eventType, handleEvent, true);
});
}

rootElement = root;

eventMap.forEach((handlers, eventType) => {
rootElement.addEventListener(eventType, handleEvent, true);
});
}
}

// TODO: handleEvent 함수 구현
// 이 함수는 실제 이벤트가 발생했을 때 호출되는 핸들러입니다.
function handleEvent(event) {
// 1. 이벤트 타겟에서 시작하여 루트 요소까지 버블링
// 2. 각 요소에 대해 해당 이벤트 타입의 핸들러가 있는지 확인
// 3. 핸들러가 있다면 실행
// 이를 통해 하위 요소에서 발생한 이벤트를 상위에서 효율적으로 처리할 수 있습니다.
let target = event.target;

while (target && target !== rootElement) {

const handlers = eventMap.get(event.type);
if (handlers && handlers.has(target)) {
const handler = handlers.get(target);
handler(event);
}
target = target.parentNode;
}
}

// TODO: addEvent 함수 구현

export function addEvent(element, eventType, handler) {
// 1. eventMap에 이벤트 타입과 요소, 핸들러 정보 저장
// 2. 필요한 경우 루트 요소에 새 이벤트 리스너 추가
// 이 함수를 통해 개별 요소에 직접 이벤트를 붙이지 않고도 이벤트 처리 가능
if (!eventMap.has(eventType)) {
eventMap.set(eventType, new Map());

if (rootElement) {
rootElement.addEventListener(eventType, handleEvent, true);
}
}

const handlers = eventMap.get(eventType);
handlers.set(element, handler);
}

// TODO: removeEvent 함수 구현

export function removeEvent(element, eventType, handler) {
// 1. eventMap에서 해당 요소와 이벤트 타입에 대한 핸들러 제거
// 2. 해당 이벤트 타입의 모든 핸들러가 제거되면 루트 요소의 리스너도 제거
// 이를 통해 더 이상 필요 없는 이벤트 핸들러를 정리하고 메모리 누수 방지
}
if (!eventMap.has(eventType)) return;

const handlers = eventMap.get(eventType);
if (handlers.has(element)) {
handlers.delete(element);


if (handlers.size === 0) {
eventMap.delete(eventType);
if (rootElement) {
rootElement.removeEventListener(eventType, handleEvent, true);
}
}
}
}
Loading
Loading