Skip to content

Commit c9866b1

Browse files
committed
feat. 컴포넌트 및 상태 시스템 구현
- component 함수를 통해 컴포넌트 정의 - tagged template 방식으로 마크업 작성 - useState 및 useEffect를 사용한 기초적인 상태 관리 및 리렌더링 구현 - on 함수를 사용한 이벤트 핸들러 등록 기능 구현
1 parent 501c7e1 commit c9866b1

21 files changed

Lines changed: 1632 additions & 22 deletions

File tree

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,11 @@
4949
"workerDirectory": [
5050
"public"
5151
]
52+
},
53+
"dependencies": {
54+
"es-toolkit": "^1.41.0",
55+
"morphdom": "^2.7.7",
56+
"nanoid": "^5.1.6",
57+
"xml-js": "^1.6.11"
5258
}
5359
}

pnpm-lock.yaml

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/component/index.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { on } from "../on";
2+
import { useEffect } from "../state/useEffect";
3+
import { useState } from "../state/useState";
4+
5+
type ComponentConstructor<
6+
Props extends Record<string, unknown>,
7+
Children extends string | undefined = undefined,
8+
> = Children extends string
9+
? (props: Props) => {
10+
html: (children: TemplateStringsArray) => HtmlTemplateNode;
11+
}
12+
: (props: Props) => HtmlTemplateNode;
13+
14+
/**
15+
* @description HTML 템플릿 노드를 나타내는 클래스
16+
* Template literal의 strings와 expressions를 캡슐화
17+
*/
18+
export class HtmlTemplateNode {
19+
constructor(
20+
private readonly html: {
21+
strings: TemplateStringsArray;
22+
expressions: unknown[];
23+
},
24+
) {}
25+
26+
/**
27+
* @description 저장된 HTML 템플릿 데이터 반환
28+
* @returns {Object} 템플릿 데이터 객체
29+
* @returns {TemplateStringsArray} returns.strings - Template literal의 문자열 배열
30+
* @returns {unknown[]} returns.expressions - Template literal의 표현식 배열
31+
* @example
32+
* const node = html`<div>${'Hello'}</div>`;
33+
* const { strings, expressions } = node.getHTML();
34+
* console.log(strings);
35+
* // 출력: ['<div>', '</div>']
36+
* console.log(expressions);
37+
* // 출력: ['Hello']
38+
*/
39+
getHTML(): { strings: TemplateStringsArray; expressions: unknown[] } {
40+
return this.html;
41+
}
42+
}
43+
44+
/**
45+
* @description 재사용 가능한 컴포넌트를 생성하는 함수
46+
* Props와 Children을 받아 HtmlTemplateNode를 반환하는 컴포넌트 생성
47+
* @param {Function} renderer - Props를 받아 HtmlTemplateNode를 반환하는 렌더 함수
48+
* @param {Props} renderer.props - 컴포넌트에 전달될 속성 객체
49+
* @param {Children} [renderer.children] - 자식 템플릿 (children을 받는 컴포넌트인 경우)
50+
* @returns {Function} 컴포넌트 생성자 함수
51+
* @example
52+
* // Props만 받는 기본 컴포넌트
53+
* const Button = component((props: { text: string; onClick: () => void }) => {
54+
* return html`
55+
* <button ${on('click', props.onClick)}>
56+
* ${props.text}
57+
* </button>
58+
* `;
59+
* });
60+
*
61+
* render`
62+
* <div>
63+
* ${Button({ text: 'Click me', onClick: () => console.log('Clicked!') })}
64+
* </div>
65+
* `;
66+
*
67+
* @example
68+
* // State를 사용하는 컴포넌트
69+
* const Counter = component((props: { initial: number }) => {
70+
* const $count = useState(props.initial);
71+
*
72+
* return html`
73+
* <div>
74+
* <p>Count: ${$count}</p>
75+
* <button ${on('click', () => $count.set(prev => prev + 1))}>
76+
* Increment
77+
* </button>
78+
* </div>
79+
* `;
80+
* });
81+
*
82+
* render`${Counter({ initial: 0 })}`;
83+
*
84+
* @example
85+
* // Children을 받는 컴포넌트
86+
* const Card = component((props: { title: string }, children: string) => {
87+
* return html`
88+
* <div class="card">
89+
* <h2>${props.title}</h2>
90+
* <div class="card-body">
91+
* ${children}
92+
* </div>
93+
* </div>
94+
* `;
95+
* });
96+
*
97+
* render`
98+
* ${Card({ title: 'My Card' }).html`
99+
* <p>Card content here</p>
100+
* `}
101+
* `;
102+
*
103+
* @example
104+
* // useEffect와 함께 사용
105+
* const Timer = component((props: { interval: number }) => {
106+
* const $seconds = useState(0);
107+
*
108+
* useEffect((deps) => {
109+
* const [interval] = deps;
110+
* const timerId = setInterval(() => {
111+
* $seconds.set(prev => prev + 1);
112+
* }, interval);
113+
*
114+
* return () => clearInterval(timerId);
115+
* }, [props.interval]);
116+
*
117+
* return html`<div>Seconds: ${$seconds}</div>`;
118+
* });
119+
*
120+
* render`${Timer({ interval: 1000 })}`;
121+
* // 1초마다 seconds가 증가하며 자동으로 화면 업데이트
122+
*/
123+
export function component<const Props extends Record<string, unknown>>(
124+
renderer: (props: Props) => HtmlTemplateNode,
125+
): (props: Props) => HtmlTemplateNode;
126+
export function component<
127+
const Props extends Record<string, unknown>,
128+
const Children extends string,
129+
>(
130+
renderer: (props: Props, children: Children) => HtmlTemplateNode,
131+
): (props: Props) => {
132+
html: (children: TemplateStringsArray) => HtmlTemplateNode;
133+
};
134+
export function component<
135+
const Props extends Record<string, unknown>,
136+
const Children extends string | undefined = undefined,
137+
>(renderer: (props: Props, children?: Children) => HtmlTemplateNode) {
138+
return ((props: Props) => {
139+
if (renderer.length === 1) {
140+
return renderer(props);
141+
}
142+
143+
return {
144+
html: (children: Children) => renderer(props, children),
145+
};
146+
}) as ComponentConstructor<Props, Children>;
147+
}
148+
149+
/**
150+
* @description Template literal을 HtmlTemplateNode로 변환하는 태그 함수
151+
* render 함수나 component 내부에서 HTML 템플릿을 작성할 때 사용
152+
* @param {TemplateStringsArray} strings - Template literal의 문자열 배열
153+
* @param {...unknown[]} expressions - Template literal의 표현식 배열
154+
* @returns {HtmlTemplateNode} HTML 템플릿 노드
155+
* @example
156+
* // 기본 사용
157+
* const node = html`<div>Hello World</div>`;
158+
*
159+
* @example
160+
* // 동적 값 포함
161+
* const name = 'John';
162+
* const node = html`<div>Hello, ${name}!</div>`;
163+
*
164+
* @example
165+
* // State 포함
166+
* const $count = useState(0);
167+
* const node = html`<div>Count: ${$count}</div>`;
168+
*
169+
* @example
170+
* // 컴포넌트 내부에서 사용
171+
* const MyComponent = component((props: { title: string }) => {
172+
* return html`
173+
* <div>
174+
* <h1>${props.title}</h1>
175+
* <p>Content</p>
176+
* </div>
177+
* `;
178+
* });
179+
*
180+
* @example
181+
* // 배열 렌더링
182+
* const items = ['Apple', 'Banana', 'Orange'];
183+
* const node = html`
184+
* <ul>
185+
* ${items.map(item => html`<li>${item}</li>`)}
186+
* </ul>
187+
* `;
188+
* // <ul><li>Apple</li><li>Banana</li><li>Orange</li></ul>
189+
*/
190+
export function html(
191+
strings: TemplateStringsArray,
192+
...expressions: unknown[]
193+
): HtmlTemplateNode {
194+
return new HtmlTemplateNode({ strings, expressions });
195+
}

0 commit comments

Comments
 (0)