์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽจ PHP์™€ Web Components๋กœ ๋งŒ๋“œ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐ŸŽจ PHP์™€ Web Components๋กœ ๋งŒ๋“œ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

ํ˜„๋Œ€์ ์ธ ์›น ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ, ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ PHP๋กœ ๊ตฌํ˜„ํ•˜๋Š” ์‹ค์ „ ๋…ธํ•˜์šฐ

์•ˆ๋…•! ์˜ค๋Š˜์€ ์ •๋ง ์žฌ๋ฏธ์žˆ๋Š” ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด. ๐Ÿ˜Š ๋ฐ”๋กœ PHP์™€ Web Components๋ฅผ ํ™œ์šฉํ•ด์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. "์–ด? PHP๋กœ๋„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด?"๋ผ๊ณ  ์ƒ๊ฐํ•  ์ˆ˜๋„ ์žˆ๋Š”๋ฐ, ๋ฌผ๋ก ์ด์ง€! ์˜คํžˆ๋ ค PHP์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง๊ณผ Web Components์˜ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์บก์Аํ™”๋ฅผ ๊ฒฐํ•ฉํ•˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ์‹œ๋„ˆ์ง€๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์–ด.

์š”์ฆ˜ ์›น ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ๋ฅผ ๋ณด๋ฉด React, Vue, Angular ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์ด ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ์„ ์ฃผ๋„ํ•˜๊ณ  ์žˆ์ž–์•„? ๊ทธ๋Ÿฐ๋ฐ PHP ๊ฐœ๋ฐœ์ž๋“ค๋„ ์ด๋Ÿฐ ํ˜„๋Œ€์ ์ธ ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ์ถฉ๋ถ„ํžˆ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋‹ค์–‘ํ•œ UI ์š”์†Œ๋“ค์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ์ด ํ•„์ˆ˜์ ์ด๊ฑฐ๋“ . ๐Ÿš€

PHP Server Side Component Template Engine Data Processing Routing Web Components Client Side Custom Element Shadow DOM Encapsulation Reusability Hybrid Architecture Best of Both Worlds Reusable Maintainable Scalable Testable Flexible

๐ŸŽฏ ์™œ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ์ด ์ค‘์š”ํ• ๊นŒ?

๋ณธ๊ฒฉ์ ์œผ๋กœ ๋“ค์–ด๊ฐ€๊ธฐ ์ „์—, ์™œ ์šฐ๋ฆฌ๊ฐ€ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐœ๋ฐœํ•ด์•ผ ํ•˜๋Š”์ง€๋ถ€ํ„ฐ ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ์ „ํ†ต์ ์ธ PHP ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ์ƒ๊ฐํ•ด๋ณด๋ฉด, ํŽ˜์ด์ง€๋งˆ๋‹ค HTML, CSS, JavaScript๋ฅผ ์„ž์–ด์„œ ์ž‘์„ฑํ•˜๊ณ , ๋น„์Šทํ•œ UI ์š”์†Œ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ๋ฅผ ํ•˜๊ณค ํ–ˆ์ง€? ๐Ÿ˜…

๐Ÿ”ฅ ์ „ํ†ต์ ์ธ ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ ๋“ค:


1. ์ฝ”๋“œ ์ค‘๋ณต - ๊ฐ™์€ ๋ฒ„ํŠผ ์Šคํƒ€์ผ์„ 10๊ฐœ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉํ•˜๋ฉด 10๋ฒˆ ์ž‘์„ฑํ•ด์•ผ ํ•ด
2. ์œ ์ง€๋ณด์ˆ˜ ์ง€์˜ฅ - ๋””์ž์ธ ๋ณ€๊ฒฝ ์‹œ ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ์ฐพ์•„๋‹ค๋‹ˆ๋ฉฐ ์ˆ˜์ •
3. ์ผ๊ด€์„ฑ ๋ถ€์กฑ - ๊ฐœ๋ฐœ์ž๋งˆ๋‹ค ๋‹ค๋ฅธ ์Šคํƒ€์ผ๋กœ ๊ตฌํ˜„
4. ํ…Œ์ŠคํŠธ ์–ด๋ ค์›€ - ๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค ๊ฐœ๋ณ„์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ํ•„์š”
5. ํ˜‘์—…์˜ ์–ด๋ ค์›€ - ๋ˆ„๊ฐ€ ์–ด๋–ค ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋Š”์ง€ ์ถ”์  ๊ณค๋ž€

์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ์€ ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ํ•œ ๋ฐฉ์— ํ•ด๊ฒฐํ•ด์ค˜. ๋งˆ์น˜ ๋ ˆ๊ณ  ๋ธ”๋ก์ฒ˜๋Ÿผ ์ž‘์€ ์กฐ๊ฐ๋“ค์„ ๋งŒ๋“ค์–ด๋‘๊ณ , ํ•„์š”ํ•  ๋•Œ๋งˆ๋‹ค ์กฐ๋ฆฝํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์ง€. ๐Ÿงฑ

๐Ÿ’ก ์‹ค์ „ ํŒ: ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ค๊ณ„ํ•  ๋•Œ๋Š” "์ด ์š”์†Œ๊ฐ€ 3๋ฒˆ ์ด์ƒ ์‚ฌ์šฉ๋ ๊นŒ?"๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ƒ๊ฐํ•ด๋ด. 3๋ฒˆ ์ด์ƒ ์‚ฌ์šฉ๋œ๋‹ค๋ฉด ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค ๊ฐ€์น˜๊ฐ€ ์ถฉ๋ถ„ํ•ด!

๐Ÿ—๏ธ PHP ์ปดํฌ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ํ•˜๊ธฐ

์ž, ์ด์ œ ์‹ค์ œ๋กœ PHP๋กœ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์„ ์–ด๋–ป๊ฒŒ ๊ตฌ์ถ•ํ•˜๋Š”์ง€ ์•Œ์•„๋ณผ๊ฒŒ. ๋จผ์ € ๊ธฐ๋ณธ์ ์ธ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋ถ€ํ„ฐ ์žก์•„๋ณด์ž.

project/
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ Button/
โ”‚   โ”‚   โ”œโ”€โ”€ Button.php
โ”‚   โ”‚   โ”œโ”€โ”€ button.css
โ”‚   โ”‚   โ””โ”€โ”€ button.js
โ”‚   โ”œโ”€โ”€ Card/
โ”‚   โ”‚   โ”œโ”€โ”€ Card.php
โ”‚   โ”‚   โ”œโ”€โ”€ card.css
โ”‚   โ”‚   โ””โ”€โ”€ card.js
โ”‚   โ””โ”€โ”€ Modal/
โ”‚       โ”œโ”€โ”€ Modal.php
โ”‚       โ”œโ”€โ”€ modal.css
โ”‚       โ””โ”€โ”€ modal.js
โ”œโ”€โ”€ core/
โ”‚   โ”œโ”€โ”€ Component.php
โ”‚   โ””โ”€โ”€ ComponentRegistry.php
โ””โ”€โ”€ public/
    โ””โ”€โ”€ index.php

์ด ๊ตฌ์กฐ์˜ ํ•ต์‹ฌ์€ ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž์‹ ๋งŒ์˜ ๋…๋ฆฝ์ ์ธ ํด๋”๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ. PHP ๋กœ์ง, CSS ์Šคํƒ€์ผ, JavaScript ๋™์ž‘์ด ๋ชจ๋‘ ํ•œ ๊ณณ์— ๋ชจ์—ฌ์žˆ์–ด์„œ ๊ด€๋ฆฌ๊ฐ€ ์ •๋ง ํŽธํ•ด์ ธ. ๐Ÿ“

๊ธฐ๋ณธ Component ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ

๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ๋ถ€๋ชจ๊ฐ€ ๋  ๊ธฐ๋ณธ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ์ด ํด๋ž˜์Šค๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ๊ณตํ†ต ๊ธฐ๋Šฅ๋“ค์„ ๋‹ด๋‹นํ•  ๊ฑฐ์•ผ.

<?php
// core/Component.php

abstract class Component {
    protected $props = [];
    protected $children = [];
    protected $attributes = [];
    
    public function __construct(array $props = []) {
        $this->props = $props;
        $this->init();
    }
    
    // ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™” (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function init() {}
    
    // ๋ Œ๋”๋ง ๋ฉ”์„œ๋“œ (ํ•„์ˆ˜ ๊ตฌํ˜„)
    abstract public function render(): string;
    
    // Props ์„ค์ •
    public function setProp(string $key, $value): self {
        $this->props[$key] = $value;
        return $this;
    }
    
    // Props ๊ฐ€์ ธ์˜ค๊ธฐ
    public function getProp(string $key, $default = null) {
        return $this->props[$key] ?? $default;
    }
    
    // HTML ์†์„ฑ ์ถ”๊ฐ€
    public function setAttribute(string $key, string $value): self {
        $this->attributes[$key] = $value;
        return $this;
    }
    
    // ์†์„ฑ๋“ค์„ HTML ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
    protected function getAttributesString(): string {
        $attrs = [];
        foreach ($this->attributes as $key => $value) {
            $attrs[] = sprintf('%s="%s"', 
                htmlspecialchars($key), 
                htmlspecialchars($value)
            );
        }
        return implode(' ', $attrs);
    }
    
    // ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
    public function addChild(Component $child): self {
        $this->children[] = $child;
        return $this;
    }
    
    // ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค ๋ Œ๋”๋ง
    protected function renderChildren(): string {
        return implode('', array_map(
            fn($child) => $child->render(), 
            $this->children
        ));
    }
    
    // ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ ์‹œ ์ž๋™ ๋ Œ๋”๋ง
    public function __toString(): string {
        return $this->render();
    }
}

์ด ๊ธฐ๋ณธ ํด๋ž˜์Šค๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ๋ณด๋ฉด:


โœ… Props ๊ด€๋ฆฌ - ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋˜๋Š” ๋ฐ์ดํ„ฐ
โœ… Attributes ๊ด€๋ฆฌ - HTML ์†์„ฑ๋“ค (class, id, data-* ๋“ฑ)
โœ… Children ๊ด€๋ฆฌ - ์ค‘์ฒฉ๋œ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ
โœ… ๋ฉ”์„œ๋“œ ์ฒด์ด๋‹ - ์œ ์ฐฝํ•œ ์ธํ„ฐํŽ˜์ด์Šค ์ œ๊ณต
โœ… ์ž๋™ ๋ Œ๋”๋ง - echo ์‹œ ์ž๋™์œผ๋กœ render() ํ˜ธ์ถœ

์‹ค์ „ Button ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” Button ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ. ์ด ๋ฒ„ํŠผ์€ ๋‹ค์–‘ํ•œ ์Šคํƒ€์ผ๊ณผ ํฌ๊ธฐ๋ฅผ ์ง€์›ํ•˜๊ณ , ์•„์ด์ฝ˜๋„ ๋„ฃ์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด. ๐ŸŽจ

<?php
// components/Button/Button.php

class Button extends Component {
    private $validVariants = ['primary', 'secondary', 'success', 'danger', 'warning'];
    private $validSizes = ['sm', 'md', 'lg'];
    
    protected function init() {
        // ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
        $this->props = array_merge([
            'text' => 'Button',
            'variant' => 'primary',
            'size' => 'md',
            'icon' => null,
            'iconPosition' => 'left',
            'disabled' => false,
            'type' => 'button',
            'onClick' => null
        ], $this->props);
        
        // ์œ ํšจ์„ฑ ๊ฒ€์ฆ
        $this->validateProps();
        
        // ๊ธฐ๋ณธ ํด๋ž˜์Šค ์„ค์ •
        $this->buildClasses();
    }
    
    private function validateProps() {
        $variant = $this->getProp('variant');
        if (!in_array($variant, $this->validVariants)) {
            throw new InvalidArgumentException(
                "Invalid variant: {$variant}. Must be one of: " . 
                implode(', ', $this->validVariants)
            );
        }
        
        $size = $this->getProp('size');
        if (!in_array($size, $this->validSizes)) {
            throw new InvalidArgumentException(
                "Invalid size: {$size}. Must be one of: " . 
                implode(', ', $this->validSizes)
            );
        }
    }
    
    private function buildClasses() {
        $classes = [
            'btn',
            'btn-' . $this->getProp('variant'),
            'btn-' . $this->getProp('size')
        ];
        
        if ($this->getProp('disabled')) {
            $classes[] = 'btn-disabled';
        }
        
        // ๊ธฐ์กด class ์†์„ฑ๊ณผ ๋ณ‘ํ•ฉ
        $existingClass = $this->attributes['class'] ?? '';
        $this->setAttribute('class', 
            trim($existingClass . ' ' . implode(' ', $classes))
        );
    }
    
    public function render(): string {
        $text = htmlspecialchars($this->getProp('text'));
        $icon = $this->getProp('icon');
        $iconPosition = $this->getProp('iconPosition');
        $disabled = $this->getProp('disabled');
        $type = $this->getProp('type');
        $onClick = $this->getProp('onClick');
        
        // disabled ์†์„ฑ ์ถ”๊ฐ€
        if ($disabled) {
            $this->setAttribute('disabled', 'disabled');
        }
        
        // type ์†์„ฑ ์ถ”๊ฐ€
        $this->setAttribute('type', $type);
        
        // onClick ์ด๋ฒคํŠธ ์ถ”๊ฐ€
        if ($onClick) {
            $this->setAttribute('onclick', $onClick);
        }
        
        // ์•„์ด์ฝ˜ ๋ Œ๋”๋ง
        $iconHtml = '';
        if ($icon) {
            $iconHtml = sprintf(
                '<span class="btn-icon">%s</span>',
                $icon
            );
        }
        
        // ๋ฒ„ํŠผ ๋‚ด์šฉ ๊ตฌ์„ฑ
        $content = $iconPosition === 'left' 
            ? $iconHtml . '<span class="btn-text">' . $text . '</span>'
            : '<span class="btn-text">' . $text . '</span>' . $iconHtml;
        
        return sprintf(
            '<button %s>%s</button>',
            $this->getAttributesString(),
            $content
        );
    }
    
    // ํŽธ์˜ ๋ฉ”์„œ๋“œ๋“ค
    public function primary(): self {
        return $this->setProp('variant', 'primary');
    }
    
    public function secondary(): self {
        return $this->setProp('variant', 'secondary');
    }
    
    public function large(): self {
        return $this->setProp('size', 'lg');
    }
    
    public function small(): self {
        return $this->setProp('size', 'sm');
    }
    
    public function disable(): self {
        return $this->setProp('disabled', true);
    }
}

์™€! ์ฝ”๋“œ๊ฐ€ ์ข€ ๊ธธ์–ด ๋ณด์ด์ง€๋งŒ, ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ •๋ง ๊ฐ„๋‹จํ•ด. ํ•œ๋ฒˆ ๋ณผ๊นŒ? ๐Ÿ˜Ž

<?php
// ์‚ฌ์šฉ ์˜ˆ์‹œ

// ๊ธฐ๋ณธ ๋ฒ„ํŠผ
$btn1 = new Button(['text' => 'ํด๋ฆญํ•˜์„ธ์š”']);
echo $btn1;

// ๋ฉ”์„œ๋“œ ์ฒด์ด๋‹์œผ๋กœ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
$btn2 = (new Button(['text' => '์ €์žฅ']))
    ->primary()
    ->large()
    ->setAttribute('id', 'save-btn');
echo $btn2;

// ์•„์ด์ฝ˜์ด ์žˆ๋Š” ๋ฒ„ํŠผ
$btn3 = new Button([
    'text' => '์‚ญ์ œ',
    'variant' => 'danger',
    'icon' => '๐Ÿ—‘๏ธ',
    'onClick' => 'confirmDelete()'
]);
echo $btn3;

// ๋น„ํ™œ์„ฑํ™”๋œ ๋ฒ„ํŠผ
$btn4 = (new Button(['text' => '์ œ์ถœ']))
    ->disable()
    ->secondary();
echo $btn4;

๐Ÿ’ก ํ”„๋กœ ํŒ: ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ๋•Œ๋Š” ํ•ญ์ƒ "์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ฐ€์žฅ ํŽธํ•˜๊ฒŒ ๋А๋‚„ ๋ฐฉ๋ฒ•์ด ๋ญ˜๊นŒ?"๋ฅผ ๊ณ ๋ฏผํ•ด๋ด. API ๋””์ž์ธ์ด ์ง๊ด€์ ์ผ์ˆ˜๋ก ํŒ€ ์ „์ฒด์˜ ์ƒ์‚ฐ์„ฑ์ด ์˜ฌ๋ผ๊ฐ€๊ฑฐ๋“ !

๐ŸŽญ Web Components์™€์˜ ํ†ตํ•ฉ

์ž, ์ด์ œ ์ง„์งœ ์žฌ๋ฏธ์žˆ๋Š” ๋ถ€๋ถ„์ด์•ผ! PHP๋กœ ๋งŒ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ Web Components์™€ ์—ฐ๊ฒฐํ•ด์„œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์—์„œ๋„ ๋™์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ณด์ž. ์ด๊ฒŒ ๋ฐ”๋กœ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜์˜ ํ•ต์‹ฌ์ด์•ผ. ๐Ÿš€

๐Ÿค” Web Components๊ฐ€ ๋ญ์•ผ?


Web Components๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ๊ธฐ์ˆ ์ด์•ผ. React๋‚˜ Vue ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ ์—†์ด๋„ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปค์Šคํ…€ HTML ์š”์†Œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€. ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ๊ธฐ์ˆ ๋กœ ๊ตฌ์„ฑ๋ผ:


1. Custom Elements - ์ž์‹ ๋งŒ์˜ HTML ํƒœ๊ทธ ์ •์˜
2. Shadow DOM - ์Šคํƒ€์ผ๊ณผ ๋งˆํฌ์—… ์บก์Аํ™”
3. HTML Templates - ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋งˆํฌ์—… ์กฐ๊ฐ

PHP์™€ Web Components ๋ธŒ๋ฆฟ์ง€ ๋งŒ๋“ค๊ธฐ

PHP ์ปดํฌ๋„ŒํŠธ๊ฐ€ Web Component๋กœ๋„ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๋ฉด, JavaScript ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•ด. ์ด๊ฑธ ์œ„ํ•œ ํŠธ๋ ˆ์ดํŠธ(Trait)๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

<?php
// core/WebComponentTrait.php

trait WebComponentTrait {
    protected $webComponentName;
    protected $shadowDOM = true;
    
    // Web Component ์ด๋ฆ„ ์„ค์ •
    public function setWebComponentName(string $name): self {
        // Custom element ์ด๋ฆ„์€ ๋ฐ˜๋“œ์‹œ ํ•˜์ดํ”ˆ์„ ํฌํ•จํ•ด์•ผ ํ•จ
        if (!preg_match('/^[a-z]+-[a-z-]+$/', $name)) {
            throw new InvalidArgumentException(
                "Web component name must contain a hyphen: {$name}"
            );
        }
        $this->webComponentName = $name;
        return $this;
    }
    
    // Web Component JavaScript ์ƒ์„ฑ
    public function generateWebComponent(): string {
        if (!$this->webComponentName) {
            throw new RuntimeException(
                'Web component name not set. Call setWebComponentName() first.'
            );
        }
        
        $className = $this->getWebComponentClassName();
        $template = $this->getWebComponentTemplate();
        $styles = $this->getWebComponentStyles();
        
        return <<<JS
class {$className} extends HTMLElement {
    constructor() {
        super();
        
        // Shadow DOM ์ƒ์„ฑ
        const shadow = this.attachShadow({ mode: 'open' });
        
        // ์Šคํƒ€์ผ ์ถ”๊ฐ€
        const style = document.createElement('style');
        style.textContent = `{$styles}`;
        shadow.appendChild(style);
        
        // ํ…œํ”Œ๋ฆฟ ์ถ”๊ฐ€
        const template = document.createElement('template');
        template.innerHTML = `{$template}`;
        shadow.appendChild(template.content.cloneNode(true));
        
        // ์ดˆ๊ธฐํ™”
        this.init();
    }
    
    // ๊ด€์ฐฐํ•  ์†์„ฑ๋“ค
    static get observedAttributes() {
        return {$this->getObservedAttributes()};
    }
    
    // ์†์„ฑ ๋ณ€๊ฒฝ ๊ฐ์ง€
    attributeChangedCallback(name, oldValue, newValue) {
        if (oldValue !== newValue) {
            this.handleAttributeChange(name, newValue);
        }
    }
    
    // ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ
    init() {
        {$this->getInitScript()}
    }
    
    // ์†์„ฑ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ
    handleAttributeChange(name, value) {
        {$this->getAttributeChangeScript()}
    }
    
    // ์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์— ์ถ”๊ฐ€๋  ๋•Œ
    connectedCallback() {
        {$this->getConnectedScript()}
    }
    
    // ์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์—์„œ ์ œ๊ฑฐ๋  ๋•Œ
    disconnectedCallback() {
        {$this->getDisconnectedScript()}
    }
}

// Custom Element ๋“ฑ๋ก
customElements.define('{$this->webComponentName}', {$className});
JS;
    }
    
    // ํด๋ž˜์Šค ์ด๋ฆ„ ์ƒ์„ฑ (PascalCase)
    protected function getWebComponentClassName(): string {
        $parts = explode('-', $this->webComponentName);
        return implode('', array_map('ucfirst', $parts)) . 'Element';
    }
    
    // ํ…œํ”Œ๋ฆฟ HTML ๋ฐ˜ํ™˜ (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function getWebComponentTemplate(): string {
        return $this->render();
    }
    
    // ์Šคํƒ€์ผ CSS ๋ฐ˜ํ™˜ (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function getWebComponentStyles(): string {
        return '';
    }
    
    // ๊ด€์ฐฐํ•  ์†์„ฑ ๋ชฉ๋ก (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function getObservedAttributes(): string {
        $attrs = array_keys($this->props);
        return json_encode($attrs);
    }
    
    // ์ดˆ๊ธฐํ™” ์Šคํฌ๋ฆฝํŠธ (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function getInitScript(): string {
        return '// Component initialized';
    }
    
    // ์†์„ฑ ๋ณ€๊ฒฝ ์Šคํฌ๋ฆฝํŠธ (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function getAttributeChangeScript(): string {
        return '// Attribute changed';
    }
    
    // ์—ฐ๊ฒฐ ์Šคํฌ๋ฆฝํŠธ (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function getConnectedScript(): string {
        return '// Component connected';
    }
    
    // ์—ฐ๊ฒฐ ํ•ด์ œ ์Šคํฌ๋ฆฝํŠธ (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function getDisconnectedScript(): string {
        return '// Component disconnected';
    }
    
    // Web Component ์Šคํฌ๋ฆฝํŠธ ์ถœ๋ ฅ
    public function renderWebComponentScript(): string {
        return sprintf(
            '<script>%s</script>',
            $this->generateWebComponent()
        );
    }
}

์ด์ œ ์ด ํŠธ๋ ˆ์ดํŠธ๋ฅผ Button ์ปดํฌ๋„ŒํŠธ์— ์ ์šฉํ•ด๋ณด์ž! ๐ŸŽฏ

<?php
// components/Button/Button.php (ํ™•์žฅ ๋ฒ„์ „)

class Button extends Component {
    use WebComponentTrait;
    
    // ... ๊ธฐ์กด ์ฝ”๋“œ ...
    
    protected function getWebComponentStyles(): string {
        return <<<CSS
:host {
    display: inline-block;
}

.btn {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 0.375rem;
    font-size: 1rem;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}

.btn:hover:not(:disabled) {
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.btn:active:not(:disabled) {
    transform: translateY(0);
}

.btn:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.btn-primary {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
}

.btn-secondary {
    background: linear-gradient(135deg, #a8b5c8 0%, #8a9bb0 100%);
    color: white;
}

.btn-success {
    background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
    color: white;
}

.btn-danger {
    background: linear-gradient(135deg, #ef5350 0%, #e53935 100%);
    color: white;
}

.btn-sm {
    padding: 0.375rem 0.75rem;
    font-size: 0.875rem;
}

.btn-lg {
    padding: 0.75rem 1.5rem;
    font-size: 1.125rem;
}

.btn-icon {
    display: inline-flex;
    align-items: center;
}
CSS;
    }
    
    protected function getInitScript(): string {
        return <<<JS
// ๋ฒ„ํŠผ ์š”์†Œ ์ฐพ๊ธฐ
this.button = this.shadowRoot.querySelector('.btn');

// ํด๋ฆญ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
this.button.addEventListener('click', (e) => {
    if (this.hasAttribute('disabled')) {
        e.preventDefault();
        e.stopPropagation();
        return;
    }
    
    // ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
    this.dispatchEvent(new CustomEvent('btn-click', {
        bubbles: true,
        composed: true,
        detail: { button: this }
    }));
});
JS;
    }
    
    protected function getAttributeChangeScript(): string {
        return <<<JS
switch(name) {
    case 'text':
        const textEl = this.shadowRoot.querySelector('.btn-text');
        if (textEl) textEl.textContent = value;
        break;
        
    case 'disabled':
        this.button.disabled = value !== null;
        break;
        
    case 'variant':
        // ๊ธฐ์กด variant ํด๋ž˜์Šค ์ œ๊ฑฐ
        this.button.className = this.button.className
            .replace(/btn-(primary|secondary|success|danger|warning)/g, '');
        // ์ƒˆ variant ํด๋ž˜์Šค ์ถ”๊ฐ€
        this.button.classList.add('btn-' + value);
        break;
}
JS;
    }
}

์ด์ œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•ด๋ณด์ž! PHP์—์„œ ๋ Œ๋”๋งํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” Web Component๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ๐ŸŽ‰

<?php
// ์‚ฌ์šฉ ์˜ˆ์‹œ

$button = new Button([
    'text' => '์ข‹์•„์š”',
    'variant' => 'primary'
]);

// Web Component ์ด๋ฆ„ ์„ค์ •
$button->setWebComponentName('custom-button');

// Web Component ์Šคํฌ๋ฆฝํŠธ ์ถœ๋ ฅ (ํ•œ ๋ฒˆ๋งŒ)
echo $button->renderWebComponentScript();

// ์ด์ œ HTML์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
?>

<!-- PHP๋กœ ๋ Œ๋”๋ง๋œ ๋ฒ„ํŠผ -->
<?= $button ?>

<!-- Web Component๋กœ ์‚ฌ์šฉ -->
<custom-button text="๊ณต์œ ํ•˜๊ธฐ" variant="secondary"></custom-button>

<!-- JavaScript๋กœ ๋™์  ์ƒ์„ฑ -->
<script>
// ๋™์ ์œผ๋กœ ๋ฒ„ํŠผ ์ถ”๊ฐ€
const btn = document.createElement('custom-button');
btn.setAttribute('text', '๋™์  ๋ฒ„ํŠผ');
btn.setAttribute('variant', 'success');
btn.addEventListener('btn-click', (e) => {
    console.log('๋ฒ„ํŠผ ํด๋ฆญ๋จ!', e.detail);
});
document.body.appendChild(btn);

// ์†์„ฑ ๋ณ€๊ฒฝ๋„ ๊ฐ€๋Šฅ
setTimeout(() => {
    btn.setAttribute('text', '๋ณ€๊ฒฝ๋œ ํ…์ŠคํŠธ');
    btn.setAttribute('variant', 'danger');
}, 2000);
</script>

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: Web Components๋Š” ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›๋˜์ง€๋งŒ, IE11์€ ํด๋ฆฌํ•„์ด ํ•„์š”ํ•ด. ํ”„๋กœ์ ํŠธ์˜ ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„๋ฅผ ํ™•์ธํ•˜๊ณ , ํ•„์š”ํ•˜๋ฉด @webcomponents/webcomponentsjs ํด๋ฆฌํ•„์„ ์ถ”๊ฐ€ํ•ด์ค˜.

๐ŸŽจ ๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ: Card ๋งŒ๋“ค๊ธฐ

์ด์ œ ์ข€ ๋” ๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ? Card ์ปดํฌ๋„ŒํŠธ๋Š” ํ—ค๋”, ๋ณธ๋ฌธ, ํ‘ธํ„ฐ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๊ณ , ์ด๋ฏธ์ง€๋„ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด. ์ค‘์ฒฉ๋œ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๋‹ค๋ฃจ๋Š”์ง€ ๋ณด์—ฌ์ค„๊ฒŒ! ๐Ÿƒ

<?php
// components/Card/Card.php

class Card extends Component {
    use WebComponentTrait;
    
    private $header;
    private $body;
    private $footer;
    private $image;
    
    protected function init() {
        $this->props = array_merge([
            'title' => null,
            'subtitle' => null,
            'image' => null,
            'imagePosition' => 'top', // top, left, right, background
            'variant' => 'default',
            'elevated' => true,
            'hoverable' => false,
            'clickable' => false
        ], $this->props);
        
        $this->buildClasses();
    }
    
    private function buildClasses() {
        $classes = ['card'];
        
        if ($this->getProp('elevated')) {
            $classes[] = 'card-elevated';
        }
        
        if ($this->getProp('hoverable')) {
            $classes[] = 'card-hoverable';
        }
        
        if ($this->getProp('clickable')) {
            $classes[] = 'card-clickable';
        }
        
        $classes[] = 'card-' . $this->getProp('variant');
        $classes[] = 'card-image-' . $this->getProp('imagePosition');
        
        $existingClass = $this->attributes['class'] ?? '';
        $this->setAttribute('class', 
            trim($existingClass . ' ' . implode(' ', $classes))
        );
    }
    
    // ํ—ค๋” ์„ค์ •
    public function setHeader($content): self {
        if ($content instanceof Component) {
            $this->header = $content;
        } else {
            $this->header = $content;
        }
        return $this;
    }
    
    // ๋ณธ๋ฌธ ์„ค์ •
    public function setBody($content): self {
        if ($content instanceof Component) {
            $this->body = $content;
        } else {
            $this->body = $content;
        }
        return $this;
    }
    
    // ํ‘ธํ„ฐ ์„ค์ •
    public function setFooter($content): self {
        if ($content instanceof Component) {
            $this->footer = $content;
        } else {
            $this->footer = $content;
        }
        return $this;
    }
    
    // ์ด๋ฏธ์ง€ ์„ค์ •
    public function setImage(string $src, string $alt = ''): self {
        $this->image = [
            'src' => $src,
            'alt' => $alt
        ];
        return $this;
    }
    
    public function render(): string {
        $parts = [];
        
        // ์ด๋ฏธ์ง€ ๋ Œ๋”๋ง
        if ($this->image || $this->getProp('image')) {
            $imgData = $this->image ?? ['src' => $this->getProp('image'), 'alt' => ''];
            $parts[] = sprintf(
                '<div class="card-image"><img src="%s" alt="%s" /></div>',
                htmlspecialchars($imgData['src']),
                htmlspecialchars($imgData['alt'])
            );
        }
        
        // ํ—ค๋” ๋ Œ๋”๋ง
        if ($this->header || $this->getProp('title')) {
            $headerContent = $this->header ?? $this->renderDefaultHeader();
            $parts[] = '<div class="card-header">' . $headerContent . '</div>';
        }
        
        // ๋ณธ๋ฌธ ๋ Œ๋”๋ง
        if ($this->body || !empty($this->children)) {
            $bodyContent = $this->body ?? $this->renderChildren();
            $parts[] = '<div class="card-body">' . $bodyContent . '</div>';
        }
        
        // ํ‘ธํ„ฐ ๋ Œ๋”๋ง
        if ($this->footer) {
            $parts[] = '<div class="card-footer">' . $this->footer . '</div>';
        }
        
        return sprintf(
            '<div %s>%s</div>',
            $this->getAttributesString(),
            implode('', $parts)
        );
    }
    
    private function renderDefaultHeader(): string {
        $title = htmlspecialchars($this->getProp('title'));
        $subtitle = $this->getProp('subtitle');
        
        $html = '<h3 class="card-title">' . $title . '</h3>';
        
        if ($subtitle) {
            $html .= '<p class="card-subtitle">' . 
                     htmlspecialchars($subtitle) . 
                     '</p>';
        }
        
        return $html;
    }
    
    protected function getWebComponentStyles(): string {
        return <<<CSS
:host {
    display: block;
}

.card {
    background: white;
    border-radius: 12px;
    overflow: hidden;
    transition: all 0.3s ease;
}

.card-elevated {
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.card-hoverable:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}

.card-clickable {
    cursor: pointer;
}

.card-clickable:active {
    transform: scale(0.98);
}

.card-image {
    width: 100%;
    overflow: hidden;
}

.card-image img {
    width: 100%;
    height: auto;
    display: block;
    object-fit: cover;
}

.card-image-top .card-image {
    order: -1;
}

.card-image-background {
    position: relative;
}

.card-image-background .card-image {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
}

.card-image-background .card-image img {
    height: 100%;
    filter: brightness(0.7);
}

.card-image-background .card-header,
.card-image-background .card-body,
.card-image-background .card-footer {
    position: relative;
    z-index: 1;
    color: white;
}

.card-header {
    padding: 1.5rem;
    border-bottom: 1px solid #f0f0f0;
}

.card-title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 600;
    color: #2c3e50;
}

.card-subtitle {
    margin: 0.5rem 0 0 0;
    font-size: 0.875rem;
    color: #7f8c8d;
}

.card-body {
    padding: 1.5rem;
}

.card-footer {
    padding: 1rem 1.5rem;
    background: #f8f9fa;
    border-top: 1px solid #f0f0f0;
}

.card-default {
    border: 1px solid #e0e0e0;
}

.card-primary {
    border: 2px solid #667eea;
}

.card-success {
    border: 2px solid #81c784;
}
CSS;
    }
    
    protected function getInitScript(): string {
        return <<<JS
// ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์นด๋“œ์ธ ๊ฒฝ์šฐ
if (this.hasAttribute('clickable')) {
    this.addEventListener('click', () => {
        this.dispatchEvent(new CustomEvent('card-click', {
            bubbles: true,
            composed: true,
            detail: { card: this }
        }));
    });
}
JS;
    }
}

Card ์ปดํฌ๋„ŒํŠธ์˜ ์‚ฌ์šฉ ์˜ˆ์‹œ๋ฅผ ๋ณผ๊นŒ? ์ •๋ง ๋‹ค์–‘ํ•˜๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ

<?php
// ๊ธฐ๋ณธ ์นด๋“œ
$card1 = new Card([
    'title' => '์žฌ๋Šฅ๋„ท์—์„œ ๊ฐœ๋ฐœ์ž ์ฐพ๊ธฐ',
    'subtitle' => '์ „๋ฌธ PHP ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ํ•จ๊ป˜ํ•˜์„ธ์š”'
]);
$card1->setBody('
    <p>์žฌ๋Šฅ๋„ท์€ ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ฐ€์ง„ ์ „๋ฌธ๊ฐ€๋“ค์ด ๋ชจ์—ฌ์žˆ๋Š” ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.
    ํ”„๋กœ์ ํŠธ์— ํ•„์š”ํ•œ ๊ฐœ๋ฐœ์ž๋ฅผ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด์š”!</p>
');
$card1->setFooter('
    <div style="display: flex; gap: 10px;">
        ' . new Button(['text' => '์ž์„ธํžˆ ๋ณด๊ธฐ', 'variant' => 'primary']) . '
        ' . new Button(['text' => '๊ณต์œ ํ•˜๊ธฐ', 'variant' => 'secondary']) . '
    </div>
');

echo $card1;

// ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š” ์นด๋“œ
$card2 = (new Card([
    'title' => 'PHP ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ',
    'hoverable' => true
]))
    ->setImage('https://example.com/php-components.jpg', 'PHP Components')
    ->setBody('<p>์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ฐœ๋ฐœ ์†๋„๋ฅผ 2๋ฐฐ ๋†’์ด์„ธ์š”!</p>');

echo $card2;

// ์ค‘์ฒฉ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ€์ง„ ์นด๋“œ
$card3 = new Card(['title' => 'ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก']);

// ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋กœ ์—ฌ๋Ÿฌ ์นด๋“œ ์ถ”๊ฐ€
for ($i = 1; $i <= 3; $i++) {
    $innerCard = new Card([
        'title' => "ํ”„๋กœ์ ํŠธ #{$i}",
        'variant' => 'default',
        'clickable' => true
    ]);
    $innerCard->setBody("<p>ํ”„๋กœ์ ํŠธ ์„ค๋ช… #{$i}</p>");
    $card3->addChild($innerCard);
}

echo $card3;

// ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์นด๋“œ
$card4 = new Card([
    'title' => 'ํŠน๋ณ„ ์ด๋ฒคํŠธ',
    'subtitle' => '์ง€๊ธˆ ๋ฐ”๋กœ ์ฐธ์—ฌํ•˜์„ธ์š”',
    'imagePosition' => 'background',
    'image' => 'https://example.com/event-bg.jpg'
]);
$card4->setBody('<p>ํ•œ์ •๋œ ์‹œ๊ฐ„ ๋™์•ˆ๋งŒ ์ œ๊ณต๋˜๋Š” ํŠน๋ณ„ ํ˜œํƒ!</p>');
$card4->setFooter(
    new Button(['text' => '์ฐธ์—ฌํ•˜๊ธฐ', 'variant' => 'success', 'size' => 'lg'])
);

echo $card4;

๐Ÿ”„ ์ปดํฌ๋„ŒํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ

์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์†Œ๋ฉธ๋˜๋Š” ๊ณผ์ •์„ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฑด ์ •๋ง ์ค‘์š”ํ•ด. ํŠนํžˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ, ํƒ€์ด๋จธ ๊ฐ™์€ ๋ฆฌ์†Œ์Šค๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ๋”์šฑ ๊ทธ๋ ‡์ง€. ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž! ๐Ÿ”„

<?php
// core/Component.php (ํ™•์žฅ ๋ฒ„์ „)

abstract class Component {
    // ... ๊ธฐ์กด ์ฝ”๋“œ ...
    
    private $lifecycleHooks = [
        'beforeRender' => [],
        'afterRender' => [],
        'beforeDestroy' => [],
        'afterDestroy' => []
    ];
    
    private $isRendered = false;
    private $isDestroyed = false;
    
    // ๋ผ์ดํ”„์‚ฌ์ดํด ํ›… ๋“ฑ๋ก
    public function on(string $hook, callable $callback): self {
        if (!isset($this->lifecycleHooks[$hook])) {
            throw new InvalidArgumentException("Invalid lifecycle hook: {$hook}");
        }
        
        $this->lifecycleHooks[$hook][] = $callback;
        return $this;
    }
    
    // ํ›… ์‹คํ–‰
    protected function triggerHook(string $hook, ...$args): void {
        if (isset($this->lifecycleHooks[$hook])) {
            foreach ($this->lifecycleHooks[$hook] as $callback) {
                call_user_func_array($callback, array_merge([$this], $args));
            }
        }
    }
    
    // ๋ Œ๋”๋ง (ํ›… ํฌํ•จ)
    public function render(): string {
        if ($this->isDestroyed) {
            throw new RuntimeException('Cannot render destroyed component');
        }
        
        // beforeRender ํ›…
        $this->triggerHook('beforeRender');
        
        // ์‹ค์ œ ๋ Œ๋”๋ง
        $output = $this->doRender();
        
        $this->isRendered = true;
        
        // afterRender ํ›…
        $this->triggerHook('afterRender', $output);
        
        return $output;
    }
    
    // ์‹ค์ œ ๋ Œ๋”๋ง ๋กœ์ง (์ž์‹ ํด๋ž˜์Šค์—์„œ ๊ตฌํ˜„)
    abstract protected function doRender(): string;
    
    // ์ปดํฌ๋„ŒํŠธ ํŒŒ๊ดด
    public function destroy(): void {
        if ($this->isDestroyed) {
            return;
        }
        
        // beforeDestroy ํ›…
        $this->triggerHook('beforeDestroy');
        
        // ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค๋„ ํŒŒ๊ดด
        foreach ($this->children as $child) {
            if (method_exists($child, 'destroy')) {
                $child->destroy();
            }
        }
        
        // ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ
        $this->cleanup();
        
        $this->isDestroyed = true;
        
        // afterDestroy ํ›…
        $this->triggerHook('afterDestroy');
    }
    
    // ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ (์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œ)
    protected function cleanup(): void {
        // ๊ธฐ๋ณธ์ ์œผ๋กœ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š์Œ
    }
    
    // ์ƒํƒœ ํ™•์ธ
    public function isRendered(): bool {
        return $this->isRendered;
    }
    
    public function isDestroyed(): bool {
        return $this->isDestroyed;
    }
}

์ด์ œ ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…์„ ํ™œ์šฉํ•œ ์‹ค์ „ ์˜ˆ์ œ๋ฅผ ๋ณด์ž. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” UserCard ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ! ๐Ÿ‘ค

<?php
// components/UserCard/UserCard.php

class UserCard extends Card {
    private $userId;
    private $userData;
    private $db;
    
    public function __construct(array $props = []) {
        parent::__construct($props);
        
        $this->userId = $props['userId'] ?? null;
        
        // beforeRender ํ›…: ๋ฐ์ดํ„ฐ ๋กœ๋“œ
        $this->on('beforeRender', function($component) {
            $this->loadUserData();
        });
        
        // afterRender ํ›…: ๋กœ๊น…
        $this->on('afterRender', function($component, $output) {
            error_log("UserCard rendered for user: {$this->userId}");
        });
        
        // beforeDestroy ํ›…: ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ
        $this->on('beforeDestroy', function($component) {
            if ($this->db) {
                // DB ์—ฐ๊ฒฐ ์ข…๋ฃŒ
                $this->db = null;
            }
        });
    }
    
    private function loadUserData(): void {
        if (!$this->userId) {
            throw new RuntimeException('User ID is required');
        }
        
        // ์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์ ธ์˜ค๊ฒ ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ๋กœ
        // (์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” PDO๋‚˜ ORM ์‚ฌ์šฉ)
        $this->userData = [
            'id' => $this->userId,
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'avatar' => 'https://example.com/avatar.jpg',
            'bio' => 'PHP ๊ฐœ๋ฐœ์ž์ด์ž ์ปดํฌ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜ ์ „๋ฌธ๊ฐ€',
            'skills' => ['PHP', 'JavaScript', 'MySQL', 'Web Components']
        ];
    }
    
    protected function doRender(): string {
        if (!$this->userData) {
            return '<div class="user-card-error">์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</div>';
        }
        
        // ์นด๋“œ ์„ค์ •
        $this->setProp('title', $this->userData['name']);
        $this->setProp('subtitle', $this->userData['email']);
        $this->setImage($this->userData['avatar'], $this->userData['name']);
        
        // ๋ณธ๋ฌธ ๊ตฌ์„ฑ
        $skillsHtml = implode('', array_map(function($skill) {
            return '<span class="skill-badge">' . htmlspecialchars($skill) . '</span>';
        }, $this->userData['skills']));
        
        $bodyHtml = sprintf('
            <p class="user-bio">%s</p>
            <div class="user-skills">
                <strong>๊ธฐ์ˆ  ์Šคํƒ:</strong>
                <div class="skills-container">%s</div>
            </div>
        ', 
            htmlspecialchars($this->userData['bio']),
            $skillsHtml
        );
        
        $this->setBody($bodyHtml);
        
        // ํ‘ธํ„ฐ์— ์•ก์…˜ ๋ฒ„ํŠผ๋“ค
        $this->setFooter('
            <div style="display: flex; gap: 10px; justify-content: space-between;">
                ' . new Button(['text' => 'ํ”„๋กœํ•„ ๋ณด๊ธฐ', 'variant' => 'primary']) . '
                ' . new Button(['text' => '๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ', 'variant' => 'secondary']) . '
            </div>
        ');
        
        // ๋ถ€๋ชจ ํด๋ž˜์Šค์˜ ๋ Œ๋”๋ง ํ˜ธ์ถœ
        return parent::doRender();
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$userCard = new UserCard(['userId' => 123]);

// ์ถ”๊ฐ€ ํ›… ๋“ฑ๋ก๋„ ๊ฐ€๋Šฅ
$userCard->on('afterRender', function($component, $output) {
    // ๋ถ„์„ ์ถ”์ 
    // Analytics::track('user_card_viewed', ['user_id' => $component->userId]);
});

echo $userCard;

// ์‚ฌ์šฉ ์™„๋ฃŒ ํ›„ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ
$userCard->destroy();

๐Ÿ’ก ์‹ค์ „ ํŒ: ๋ผ์ดํ”„์‚ฌ์ดํด ํ›…์€ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋„ ์—„์ฒญ ์œ ์šฉํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด, beforeRender ํ›…์—์„œ ๋ชฉ(mock) ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ์ž…ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—†์ด๋„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด!

๐ŸŽช ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์™€ ์ž๋™ ๋กœ๋”ฉ

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ˆ˜์‹ญ, ์ˆ˜๋ฐฑ ๊ฐœ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด. ๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ requireํ•˜๋Š” ๊ฑด ๋น„ํšจ์œจ์ ์ด์ง€. ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž๋™์œผ๋กœ ๋“ฑ๋กํ•˜๊ณ  ๋กœ๋“œํ•˜๋Š” ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿš€

<?php
// core/ComponentRegistry.php

class ComponentRegistry {
    private static $instance = null;
    private $components = [];
    private $paths = [];
    private $aliases = [];
    
    private function __construct() {
        // ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด
    }
    
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    // ์ปดํฌ๋„ŒํŠธ ๊ฒฝ๋กœ ๋“ฑ๋ก
    public function registerPath(string $path): self {
        $this->paths[] = rtrim($path, '/');
        return $this;
    }
    
    // ์ปดํฌ๋„ŒํŠธ ๋ณ„์นญ ๋“ฑ๋ก
    public function registerAlias(string $alias, string $className): self {
        $this->aliases[$alias] = $className;
        return $this;
    }
    
    // ์ปดํฌ๋„ŒํŠธ ์ž๋™ ๊ฒ€์ƒ‰
    public function discover(): self {
        foreach ($this->paths as $path) {
            if (!is_dir($path)) {
                continue;
            }
            
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($path)
            );
            
            foreach ($iterator as $file) {
                if ($file->isFile() && $file->getExtension() === 'php') {
                    $this->loadComponentFile($file->getPathname());
                }
            }
        }
        
        return $this;
    }
    
    private function loadComponentFile(string $filepath): void {
        require_once $filepath;
        
        // ํŒŒ์ผ์—์„œ ์ •์˜๋œ ํด๋ž˜์Šค ์ฐพ๊ธฐ
        $classes = get_declared_classes();
        $beforeCount = count($this->components);
        
        foreach ($classes as $class) {
            if (is_subclass_of($class, Component::class) && 
                !isset($this->components[$class])) {
                $this->register($class);
            }
        }
    }
    
    // ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก
    public function register(string $className): self {
        if (!class_exists($className)) {
            throw new RuntimeException("Class not found: {$className}");
        }
        
        if (!is_subclass_of($className, Component::class)) {
            throw new RuntimeException(
                "{$className} must extend Component class"
            );
        }
        
        $this->components[$className] = [
            'class' => $className,
            'registered_at' => time()
        ];
        
        return $this;
    }
    
    // ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
    public function create(string $name, array $props = []): Component {
        // ๋ณ„์นญ์œผ๋กœ ์ฐพ๊ธฐ
        $className = $this->aliases[$name] ?? $name;
        
        // ๋“ฑ๋ก๋œ ์ปดํฌ๋„ŒํŠธ์ธ์ง€ ํ™•์ธ
        if (!isset($this->components[$className])) {
            // ์ž๋™ ๋กœ๋“œ ์‹œ๋„
            if (class_exists($className)) {
                $this->register($className);
            } else {
                throw new RuntimeException(
                    "Component not found: {$name}"
                );
            }
        }
        
        return new $className($props);
    }
    
    // ๋“ฑ๋ก๋œ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก
    public function getRegistered(): array {
        return array_keys($this->components);
    }
    
    // ์ปดํฌ๋„ŒํŠธ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
    public function has(string $name): bool {
        $className = $this->aliases[$name] ?? $name;
        return isset($this->components[$className]);
    }
    
    // ํ—ฌํผ ํ•จ์ˆ˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก
    public function __call(string $name, array $arguments): Component {
        // ๋ฉ”์„œ๋“œ ์ด๋ฆ„์„ ํด๋ž˜์Šค ์ด๋ฆ„์œผ๋กœ ๋ณ€ํ™˜
        // ์˜ˆ: button() -> Button
        $className = ucfirst($name);
        $props = $arguments[0] ?? [];
        
        return $this->create($className, $props);
    }
}

// ์ „์—ญ ํ—ฌํผ ํ•จ์ˆ˜
function component(string $name, array $props = []): Component {
    return ComponentRegistry::getInstance()->create($name, $props);
}

function c(string $name, array $props = []): Component {
    // ์งง์€ ๋ฒ„์ „
    return component($name, $props);
}

์ด์ œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ์„ค์ •ํ•˜๊ณ  ์‚ฌ์šฉํ•ด๋ณด์ž! ์ •๋ง ํŽธ๋ฆฌํ•ด์งˆ ๊ฑฐ์•ผ. ๐Ÿ˜Ž

<?php
// bootstrap.php - ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™”

require_once 'core/Component.php';
require_once 'core/ComponentRegistry.php';
require_once 'core/WebComponentTrait.php';

// ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ธ์Šคํ„ด์Šค ๊ฐ€์ ธ์˜ค๊ธฐ
$registry = ComponentRegistry::getInstance();

// ์ปดํฌ๋„ŒํŠธ ๊ฒฝ๋กœ ๋“ฑ๋ก
$registry
    ->registerPath(__DIR__ . '/components')
    ->registerPath(__DIR__ . '/custom-components');

// ๋ณ„์นญ ๋“ฑ๋ก (์„ ํƒ์‚ฌํ•ญ)
$registry
    ->registerAlias('btn', 'Button')
    ->registerAlias('user-card', 'UserCard');

// ์ž๋™ ๊ฒ€์ƒ‰ ์‹คํ–‰
$registry->discover();

// ๋“ฑ๋ก๋œ ์ปดํฌ๋„ŒํŠธ ํ™•์ธ
echo "๋“ฑ๋ก๋œ ์ปดํฌ๋„ŒํŠธ: " . implode(', ', $registry->getRegistered()) . "\n";

// ===== ์‚ฌ์šฉ ์˜ˆ์‹œ =====

// ๋ฐฉ๋ฒ• 1: ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ํ†ตํ•œ ์ƒ์„ฑ
$button1 = $registry->create('Button', ['text' => 'ํด๋ฆญ']);
echo $button1;

// ๋ฐฉ๋ฒ• 2: ๋ณ„์นญ ์‚ฌ์šฉ
$button2 = $registry->create('btn', ['text' => '์ €์žฅ']);
echo $button2;

// ๋ฐฉ๋ฒ• 3: ํ—ฌํผ ํ•จ์ˆ˜ ์‚ฌ์šฉ
$card = component('Card', [
    'title' => 'ํ—ฌํผ ํ•จ์ˆ˜๋กœ ์ƒ์„ฑ',
    'hoverable' => true
]);
echo $card;

// ๋ฐฉ๋ฒ• 4: ์งง์€ ํ—ฌํผ ํ•จ์ˆ˜
$userCard = c('UserCard', ['userId' => 456]);
echo $userCard;

// ๋ฐฉ๋ฒ• 5: ๋งค์ง ๋ฉ”์„œ๋“œ (๊ฐ€์žฅ ํŽธ๋ฆฌ!)
$button3 = $registry->button(['text' => '๋งค์ง!']);
$card2 = $registry->card(['title' => '๋งค์ง ์นด๋“œ']);

echo $button3;
echo $card2;

๐ŸŽฏ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํŒจํ„ด์˜ ์žฅ์ :


1. ์ž๋™ ๋กœ๋”ฉ - ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ requireํ•  ํ•„์š” ์—†์Œ
2. ์ค‘์•™ ๊ด€๋ฆฌ - ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌ
3. ๋ณ„์นญ ์ง€์› - ๊ธด ํด๋ž˜์Šค ์ด๋ฆ„ ๋Œ€์‹  ์งง์€ ๋ณ„์นญ ์‚ฌ์šฉ
4. ์œ ์—ฐ์„ฑ - ๋Ÿฐํƒ€์ž„์— ์ปดํฌ๋„ŒํŠธ ๊ต์ฒด ๊ฐ€๋Šฅ
5. ํ…Œ์ŠคํŠธ ์šฉ์ด - ๋ชฉ(mock) ์ปดํฌ๋„ŒํŠธ๋กœ ์‰ฝ๊ฒŒ ๊ต์ฒด

๐ŸŽจ ์Šคํƒ€์ผ ๊ด€๋ฆฌ์™€ ํ…Œ๋งˆ ์‹œ์Šคํ…œ

์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์Šคํƒ€์ผ ๊ด€๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์ ธ. ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ CSS๋ฅผ ์–ด๋–ป๊ฒŒ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ํ…Œ๋งˆ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ? ์Šคํƒ€์ผ ๋งค๋‹ˆ์ €๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๐ŸŽจ

<?php
// core/StyleManager.php

class StyleManager {
    private static $instance = null;
    private $styles = [];
    private $themes = [];
    private $currentTheme = 'default';
    private $compiled = false;
    
    private function __construct() {}
    
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    // ์Šคํƒ€์ผ ๋“ฑ๋ก
    public function register(string $name, string $css): self {
        $this->styles[$name] = $css;
        $this->compiled = false;
        return $this;
    }
    
    // ์Šคํƒ€์ผ ํŒŒ์ผ ๋“ฑ๋ก
    public function registerFile(string $name, string $filepath): self {
        if (!file_exists($filepath)) {
            throw new RuntimeException("Style file not found: {$filepath}");
        }
        
        $css = file_get_contents($filepath);
        return $this->register($name, $css);
    }
    
    // ํ…Œ๋งˆ ๋“ฑ๋ก
    public function registerTheme(string $name, array $variables): self {
        $this->themes[$name] = $variables;
        return $this;
    }
    
    // ํ˜„์žฌ ํ…Œ๋งˆ ์„ค์ •
    public function setTheme(string $name): self {
        if (!isset($this->themes[$name])) {
            throw new RuntimeException("Theme not found: {$name}");
        }
        
        $this->currentTheme = $name;
        $this->compiled = false;
        return $this;
    }
    
    // CSS ๋ณ€์ˆ˜ ์น˜ํ™˜
    private function replaceVariables(string $css): string {
        if (!isset($this->themes[$this->currentTheme])) {
            return $css;
        }
        
        $theme = $this->themes[$this->currentTheme];
        
        foreach ($theme as $key => $value) {
            // CSS ๋ณ€์ˆ˜ ํ˜•์‹: var(--variable-name)
            $css = str_replace("var(--{$key})", $value, $css);
            // ๋˜๋Š” PHP ๋ณ€์ˆ˜ ํ˜•์‹: {{variable-name}}
            $css = str_replace("{{{$key}}}", $value, $css);
        }
        
        return $css;
    }
    
    // ๋ชจ๋“  ์Šคํƒ€์ผ ์ปดํŒŒ์ผ
    public function compile(): string {
        if ($this->compiled) {
            return $this->getCompiled();
        }
        
        $compiled = [];
        
        foreach ($this->styles as $name => $css) {
            $processed = $this->replaceVariables($css);
            $compiled[] = "/* Component: {$name} */\n{$processed}\n";
        }
        
        $this->compiled = implode("\n", $compiled);
        return $this->compiled;
    }
    
    // ์ปดํŒŒ์ผ๋œ ์Šคํƒ€์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
    public function getCompiled(): string {
        return $this->compiled ?: $this->compile();
    }
    
    // ์Šคํƒ€์ผ ํƒœ๊ทธ๋กœ ์ถœ๋ ฅ
    public function render(): string {
        return sprintf(
            '<style>.tx-header{background:linear-gradient(135deg,#f5f7fa 0%,#e8ecf1 100%);
border-radius:16px;
padding:40px;
margin:30px 0;
box-shadow:0 4px 20px rgba(0,0,0,0.08);
text-align:center;}
.tx-section{margin:40px 0;
line-height:1.9;}
.tx-box{background:linear-gradient(135deg,#faf9f7 0%,#f0eeeb 100%);
border-left:4px solid #a8b5c8;
padding:20px;
margin:25px 0;
border-radius:8px;
box-shadow:0 2px 12px rgba(0,0,0,0.05);}
.tx-highlight{background:linear-gradient(120deg,#fef5e7 0%,#fdebd0 100%);
padding:3px 8px;
border-radius:4px;
font-weight:600;}
.tx-tip{background:linear-gradient(135deg,#e8f5e9 0%,#dcedc8 100%);
border-left:4px solid #81c784;
padding:18px;
margin:25px 0;
border-radius:8px;}
.tx-warning{background:linear-gradient(135deg,#fff3e0 0%,#ffe0b2 100%);
border-left:4px solid #ffb74d;
padding:18px;
margin:25px 0;
border-radius:8px;}
.tx-table{width:100%;
border-collapse:collapse;
margin:25px 0;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 12px rgba(0,0,0,0.06);}
.tx-table th{background:linear-gradient(135deg,#b8c5d6 0%,#a3b1c6 100%);
color:#2c3e50;
padding:15px;
text-align:left;
font-weight:600;}
.tx-table td{padding:15px;
border-bottom:1px solid #ecf0f1;}
.tx-subtitle{color:#5d6d7e;
font-size:1.1em;
margin:30px 0 15px 0;
font-weight:600;
padding-left:12px;
border-left:3px solid #a8b5c8;}</style>',
            $this->compile()
        );
    }
    
    // ํŒŒ์ผ๋กœ ์ €์žฅ
    public function saveToFile(string $filepath): bool {
        $css = $this->compile();
        return file_put_contents($filepath, $css) !== false;
    }
    
    // ์••์ถ• (minify)
    public function minify(string $css): string {
        // ์ฃผ์„ ์ œ๊ฑฐ
        $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
        // ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ ์ œ๊ฑฐ
        $css = str_replace(["\r\n", "\r", "\n", "\t", '  ', '    '], '', $css);
        $css = preg_replace('/\s+/', ' ', $css);
        $css = preg_replace('/\s*([{}:;,])\s*/', '$1', $css);
        
        return trim($css);
    }
    
    // ์••์ถ•๋œ ๋ฒ„์ „ ์ถœ๋ ฅ
    public function renderMinified(): string {
        return sprintf(
            '<style>.tx-header{background:linear-gradient(135deg,#f5f7fa 0%,#e8ecf1 100%);
border-radius:16px;
padding:40px;
margin:30px 0;
box-shadow:0 4px 20px rgba(0,0,0,0.08);
text-align:center;}
.tx-section{margin:40px 0;
line-height:1.9;}
.tx-box{background:linear-gradient(135deg,#faf9f7 0%,#f0eeeb 100%);
border-left:4px solid #a8b5c8;
padding:20px;
margin:25px 0;
border-radius:8px;
box-shadow:0 2px 12px rgba(0,0,0,0.05);}
.tx-highlight{background:linear-gradient(120deg,#fef5e7 0%,#fdebd0 100%);
padding:3px 8px;
border-radius:4px;
font-weight:600;}
.tx-tip{background:linear-gradient(135deg,#e8f5e9 0%,#dcedc8 100%);
border-left:4px solid #81c784;
padding:18px;
margin:25px 0;
border-radius:8px;}
.tx-warning{background:linear-gradient(135deg,#fff3e0 0%,#ffe0b2 100%);
border-left:4px solid #ffb74d;
padding:18px;
margin:25px 0;
border-radius:8px;}
.tx-table{width:100%;
border-collapse:collapse;
margin:25px 0;
background:#fff;
border-radius:8px;
overflow:hidden;
box-shadow:0 2px 12px rgba(0,0,0,0.06);}
.tx-table th{background:linear-gradient(135deg,#b8c5d6 0%,#a3b1c6 100%);
color:#2c3e50;
padding:15px;
text-align:left;
font-weight:600;}
.tx-table td{padding:15px;
border-bottom:1px solid #ecf0f1;}
.tx-subtitle{color:#5d6d7e;
font-size:1.1em;
margin:30px 0 15px 0;
font-weight:600;
padding-left:12px;
border-left:3px solid #a8b5c8;}</style>',
            $this->minify($this->compile())
        );
    }
}

์ด์ œ ํ…Œ๋งˆ ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•ด๋ณด์ž! ๋ผ์ดํŠธ ๋ชจ๋“œ์™€ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์‰ฝ๊ฒŒ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŒ“

<?php
// themes.php - ํ…Œ๋งˆ ์ •์˜

$styleManager = StyleManager::getInstance();

// ๋ผ์ดํŠธ ํ…Œ๋งˆ
$styleManager->registerTheme('light', [
    'primary-color' => '#667eea',
    'secondary-color' => '#a8b5c8',
    'success-color' => '#81c784',
    'danger-color' => '#ef5350',
    'warning-color' => '#ffb74d',
    'bg-color' => '#ffffff',
    'text-color' => '#2c3e50',
    'border-color' => '#e0e0e0',
    'shadow' => '0 2px 8px rgba(0,0,0,0.1)'
]);

// ๋‹คํฌ ํ…Œ๋งˆ
$styleManager->registerTheme('dark', [
    'primary-color' => '#8b9dc3',
    'secondary-color' => '#6b7c93',
    'success-color' => '#66bb6a',
    'danger-color' => '#e57373',
    'warning-color' => '#ffd54f',
    'bg-color' => '#1e1e1e',
    'text-color' => '#e0e0e0',
    'border-color' => '#3a3a3a',
    'shadow' => '0 2px 8px rgba(0,0,0,0.3)'
]);

// ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ ๋“ฑ๋ก (๋ณ€์ˆ˜ ์‚ฌ์šฉ)
$styleManager->register('button', '
.btn {
    background: var(--primary-color);
    color: var(--text-color);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow);
}

.btn-secondary {
    background: var(--secondary-color);
}

.btn-success {
    background: var(--success-color);
}

.btn-danger {
    background: var(--danger-color);
}
');

$styleManager->register('card', '
.card {
    background: var(--bg-color);
    color: var(--text-color);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow);
}

.card-header {
    border-bottom: 1px solid var(--border-color);
}
');

// ์‚ฌ์šฉ ์˜ˆ์‹œ
// ๋ผ์ดํŠธ ๋ชจ๋“œ
$styleManager->setTheme('light');
echo $styleManager->render();

// ๋˜๋Š” ๋‹คํฌ ๋ชจ๋“œ
$styleManager->setTheme('dark');
echo $styleManager->renderMinified();

// ํŒŒ์ผ๋กœ ์ €์žฅ
$styleManager->setTheme('light');
$styleManager->saveToFile('public/css/components-light.css');

$styleManager->setTheme('dark');
$styleManager->saveToFile('public/css/components-dark.css');

์ปดํฌ๋„ŒํŠธ์—์„œ ์Šคํƒ€์ผ ๋งค๋‹ˆ์ €๋ฅผ ์ž๋™์œผ๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ํ†ตํ•ฉํ•  ์ˆ˜๋„ ์žˆ์–ด! ๐Ÿ”—

<?php
// core/Component.php์— ์ถ”๊ฐ€

abstract class Component {
    // ... ๊ธฐ์กด ์ฝ”๋“œ ...
    
    // ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ ์ž๋™ ๋“ฑ๋ก
    protected function registerStyles(): void {
        $className = get_class($this);
        $styleManager = StyleManager::getInstance();
        
        // ์ด๋ฏธ ๋“ฑ๋ก๋˜์–ด ์žˆ์œผ๋ฉด ์Šคํ‚ต
        if ($styleManager->has($className)) {
            return;
        }
        
        // ์ปดํฌ๋„ŒํŠธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ CSS ํŒŒ์ผ ์ฐพ๊ธฐ
        $reflection = new ReflectionClass($className);
        $dir = dirname($reflection->getFileName());
        $cssFile = $dir . '/' . strtolower($className) . '.css';
        
        if (file_exists($cssFile)) {
            $styleManager->registerFile($className, $cssFile);
        }
    }
    
    public function __construct(array $props = []) {
        $this->props = $props;
        $this->registerStyles(); // ์ž๋™ ์Šคํƒ€์ผ ๋“ฑ๋ก
        $this->init();
    }
}

๐Ÿงช ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŒ…

์ข‹์€ ์ปดํฌ๋„ŒํŠธ๋Š” ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ์•ผ! PHPUnit์„ ์‚ฌ์šฉํ•ด์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ๐Ÿงช

<?php
// tests/ButtonTest.php

use PHPUnit\Framework\TestCase;

class ButtonTest extends TestCase {
    private $button;
    
    protected function setUp(): void {
        $this->button = new Button(['text' => 'Test Button']);
    }
    
    protected function tearDown(): void {
        if ($this->button) {
            $this->button->destroy();
        }
    }
    
    public function testButtonRendersCorrectly() {
        $output = $this->button->render();
        
        $this->assertStringContainsString('<button', $output);
        $this->assertStringContainsString('Test Button', $output);
        $this->assertStringContainsString('btn', $output);
    }
    
    public function testButtonVariants() {
        $variants = ['primary', 'secondary', 'success', 'danger', 'warning'];
        
        foreach ($variants as $variant) {
            $button = new Button([
                'text' => 'Test',
                'variant' => $variant
            ]);
            
            $output = $button->render();
            $this->assertStringContainsString("btn-{$variant}", $output);
            
            $button->destroy();
        }
    }
    
    public function testButtonSizes() {
        $sizes = ['sm', 'md', 'lg'];
        
        foreach ($sizes as $size) {
            $button = new Button([
                'text' => 'Test',
                'size' => $size
            ]);
            
            $output = $button->render();
            $this->assertStringContainsString("btn-{$size}", $output);
            
            $button->destroy();
        }
    }
    
    public function testDisabledButton() {
        $button = new Button([
            'text' => 'Test',
            'disabled' => true
        ]);
        
        $output = $button->render();
        $this->assertStringContainsString('disabled', $output);
        $this->assertStringContainsString('btn-disabled', $output);
    }
    
    public function testButtonWithIcon() {
        $button = new Button([
            'text' => 'Test',
            'icon' => '๐Ÿš€'
        ]);
        
        $output = $button->render();
        $this->assertStringContainsString('๐Ÿš€', $output);
        $this->assertStringContainsString('btn-icon', $output);
    }
    
    public function testMethodChaining() {
        $output = $this->button
            ->primary()
            ->large()
            ->setAttribute('id', 'test-btn')
            ->render();
        
        $this->assertStringContainsString('btn-primary', $output);
        $this->assertStringContainsString('btn-lg', $output);
        $this->assertStringContainsString('id="test-btn"', $output);
    }
    
    public function testInvalidVariantThrowsException() {
        $this->expectException(InvalidArgumentException::class);
        
        new Button([
            'text' => 'Test',
            'variant' => 'invalid'
        ]);
    }
    
    public function testLifecycleHooks() {
        $beforeRenderCalled = false;
        $afterRenderCalled = false;
        
        $button = new Button(['text' => 'Test']);
        
        $button->on('beforeRender', function() use (&$beforeRenderCalled) {
            $beforeRenderCalled = true;
        });
        
        $button->on('afterRender', function() use (&$afterRenderCalled) {
            $afterRenderCalled = true;
        });
        
        $button->render();
        
        $this->assertTrue($beforeRenderCalled);
        $this->assertTrue($afterRenderCalled);
    }
    
    public function testComponentDestruction() {
        $button = new Button(['text' => 'Test']);
        
        $this->assertFalse($button->isDestroyed());
        
        $button->destroy();
        
        $this->assertTrue($button->isDestroyed());
        
        $this->expectException(RuntimeException::class);
        $button->render(); // ํŒŒ๊ดด๋œ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ Œ๋”๋ง ๋ถˆ๊ฐ€
    }
}

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋„ ์ž‘์„ฑํ•ด๋ณด์ž! ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•จ๊ป˜ ๋™์ž‘ํ•˜๋Š” ๊ฑธ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฑฐ์•ผ. ๐Ÿ”—

<?php
// tests/ComponentIntegrationTest.php

use PHPUnit\Framework\TestCase;

class ComponentIntegrationTest extends TestCase {
    public function testCardWithButtons() {
        $card = new Card(['title' => 'Test Card']);
        
        $button1 = new Button(['text' => 'Action 1', 'variant' => 'primary']);
        $button2 = new Button(['text' => 'Action 2', 'variant' => 'secondary']);
        
        $card->setFooter(
            '<div>' . $button1 . $button2 . '</div>'
        );
        
        $output = $card->render();
        
        $this->assertStringContainsString('Test Card', $output);
        $this->assertStringContainsString('Action 1', $output);
        $this->assertStringContainsString('Action 2', $output);
        $this->assertStringContainsString('btn-primary', $output);
        $this->assertStringContainsString('btn-secondary', $output);
    }
    
    public function testNestedComponents() {
        $parentCard = new Card(['title' => 'Parent']);
        
        for ($i = 1; $i <= 3; $i++) {
            $childCard = new Card(['title' => "Child {$i}"]);
            $childCard->setBody("<p>Content {$i}</p>");
            $parentCard->addChild($childCard);
        }
        
        $output = $parentCard->render();
        
        $this->assertStringContainsString('Parent', $output);
        $this->assertStringContainsString('Child 1', $output);
        $this->assertStringContainsString('Child 2', $output);
        $this->assertStringContainsString('Child 3', $output);
    }
    
    public function testComponentRegistry() {
        $registry = ComponentRegistry::getInstance();
        $registry->register('Button');
        
        $button = $registry->create('Button', ['text' => 'Registry Test']);
        
        $this->assertInstanceOf(Button::class, $button);
        
        $output = $button->render();
        $this->assertStringContainsString('Registry Test', $output);
    }
    
    public function testStyleManager() {
        $styleManager = StyleManager::getInstance();
        
        $styleManager->registerTheme('test', [
            'primary-color' => '#ff0000'
        ]);
        
        $styleManager->register('test-component', '
            .test { color: var(--primary-color); }
        ');
        
        $styleManager->setTheme('test');
        $compiled = $styleManager->compile();
        
        $this->assertStringContainsString('#ff0000', $compiled);
        $this->assertStringNotContainsString('var(--primary-color)', $compiled);
    }
}

๐Ÿ’ก ํ…Œ์ŠคํŠธ ํŒ: ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋Š” "๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ• ๊นŒ?"๋ณด๋‹ค "์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ• ๊นŒ?"๋ฅผ ๋จผ์ € ์ƒ๊ฐํ•ด๋ด. ์‚ฌ์šฉ์ž ๊ด€์ ์—์„œ ํ…Œ์ŠคํŠธํ•˜๋ฉด ๋” ์˜๋ฏธ ์žˆ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿš€ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์ถ•ํ•˜๊ธฐ

์ด์ œ ๋ฐฐ์šด ๋ชจ๋“  ๊ฑธ ํ™œ์šฉํ•ด์„œ ์‹ค์ œ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์˜ ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ƒ์ƒํ•ด๋ด. ๐Ÿ“Š

<?php
// components/Dashboard/StatCard.php

class StatCard extends Card {
    protected function init() {
        parent::init();
        
        $this->props = array_merge([
            'label' => '',
            'value' => 0,
            'icon' => '๐Ÿ“Š',
            'trend' => null, // 'up', 'down', null
            'trendValue' => null,
            'color' => 'primary'
        ], $this->props);
    }
    
    protected function doRender(): string {
        $label = htmlspecialchars($this->getProp('label'));
        $value = htmlspecialchars($this->getProp('value'));
        $icon = $this->getProp('icon');
        $trend = $this->getProp('trend');
        $trendValue = $this->getProp('trendValue');
        $color = $this->getProp('color');
        
        $trendHtml = '';
        if ($trend && $trendValue) {
            $trendIcon = $trend === 'up' ? '๐Ÿ“ˆ' : '๐Ÿ“‰';
            $trendClass = $trend === 'up' ? 'trend-up' : 'trend-down';
            $trendHtml = sprintf(
                '<span class="stat-trend %s">%s %s</span>',
                $trendClass,
                $trendIcon,
                htmlspecialchars($trendValue)
            );
        }
        
        $this->setBody(sprintf('
            <div class="stat-card-content stat-card-%s">
                <div class="stat-icon">%s</div>
                <div class="stat-info">
                    <div class="stat-label">%s</div>
                    <div class="stat-value">%s</div>
                    %s
                </div>
            </div>
        ', $color, $icon, $label, $value, $trendHtml));
        
        return parent::doRender();
    }
}

// components/Dashboard/ChartCard.php

class ChartCard extends Card {
    protected function init() {
        parent::init();
        
        $this->props = array_merge([
            'chartType' => 'line', // line, bar, pie
            'data' => [],
            'labels' => [],
            'chartId' => 'chart-' . uniqid()
        ], $this->props);
    }
    
    protected function doRender(): string {
        $chartId = $this->getProp('chartId');
        $chartType = $this->getProp('chartType');
        $data = json_encode($this->getProp('data'));
        $labels = json_encode($this->getProp('labels'));
        
        $this->setBody(sprintf('
            <div class="chart-container">
                <canvas id="%s"></canvas>
            </div>
            <script>
                (function() {
                    const ctx = document.getElementById("%s").getContext("2d");
                    new Chart(ctx, {
                        type: "%s",
                        data: {
                            labels: %s,
                            datasets: [{
                                data: %s,
                                backgroundColor: [
                                    "rgba(102, 126, 234, 0.8)",
                                    "rgba(129, 199, 132, 0.8)",
                                    "rgba(255, 183, 77, 0.8)",
                                    "rgba(239, 83, 80, 0.8)"
                                ]
                            }]
                        },
                        options: {
                            responsive: true,
                            maintainAspectRatio: false
                        }
                    });
                })();
            </script>
        ', $chartId, $chartId, $chartType, $labels, $data));
        
        return parent::doRender();
    }
}

// pages/dashboard.php - ๋Œ€์‹œ๋ณด๋“œ ํŽ˜์ด์ง€

require_once 'bootstrap.php';

// ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ (์‹ค์ œ๋กœ๋Š” DB์—์„œ ๊ฐ€์ ธ์˜ด)
$stats = [
    [
        'label' => '์ด ์‚ฌ์šฉ์ž',
        'value' => '12,345',
        'icon' => '๐Ÿ‘ฅ',
        'trend' => 'up',
        'trendValue' => '+12%',
        'color' => 'primary'
    ],
    [
        'label' => 'ํ™œ์„ฑ ํ”„๋กœ์ ํŠธ',
        'value' => '89',
        'icon' => '๐Ÿ“',
        'trend' => 'up',
        'trendValue' => '+5%',
        'color' => 'success'
    ],
    [
        'label' => '์ด๋ฒˆ ๋‹ฌ ์ˆ˜์ต',
        'value' => 'โ‚ฉ2,450,000',
        'icon' => '๐Ÿ’ฐ',
        'trend' => 'up',
        'trendValue' => '+23%',
        'color' => 'warning'
    ],
    [
        'label' => '๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ',
        'value' => '23',
        'icon' => 'โฐ',
        'trend' => 'down',
        'trendValue' => '-8%',
        'color' => 'danger'
    ]
];

?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>์žฌ๋Šฅ๋„ท ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <?= StyleManager::getInstance()->renderMinified() ?>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f7fa;
        }
        .dashboard-header {
            margin-bottom: 30px;
        }
        .dashboard-header h1 {
            margin: 0 0 10px 0;
            color: #2c3e50;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        .stat-card-content {
            display: flex;
            align-items: center;
            gap: 20px;
        }
        .stat-icon {
            font-size: 3rem;
        }
        .stat-info {
            flex: 1;
        }
        .stat-label {
            font-size: 0.875rem;
            color: #7f8c8d;
            margin-bottom: 5px;
        }
        .stat-value {
            font-size: 2rem;
            font-weight: 700;
            color: #2c3e50;
        }
        .stat-trend {
            font-size: 0.875rem;
            margin-top: 5px;
            display: inline-block;
        }
        .trend-up {
            color: #81c784;
        }
        .trend-down {
            color: #ef5350;
        }
        .charts-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
            gap: 20px;
        }
        .chart-container {
            height: 300px;
        }
    </style>
</head>
<body>
    <div class="dashboard-header">
        <h1>๐Ÿ“Š ์žฌ๋Šฅ๋„ท ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ</h1>
        <p>์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ๋ฐ ๋ถ„์„ ๋ฐ์ดํ„ฐ</p>
    </div>
    
    <div class="stats-grid">
        <?php foreach ($stats as $stat): ?>
            <?= new StatCard($stat) ?>
        <?php endforeach; ?>
    </div>
    
    <div class="charts-grid">
        <?= new ChartCard([
            'title' => '์›”๋ณ„ ์‚ฌ์šฉ์ž ์ฆ๊ฐ€',
            'chartType' => 'line',
            'labels' => ['1์›”', '2์›”', '3์›”', '4์›”', '5์›”', '6์›”'],
            'data' => [1200, 1900, 3000, 5000, 8000, 12345]
        ]) ?>
        
        <?= new ChartCard([
            'title' => '์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ”„๋กœ์ ํŠธ',
            'chartType' => 'pie',
            'labels' => ['๊ฐœ๋ฐœ', '๋””์ž์ธ', '๋งˆ์ผ€ํŒ…', '๊ธฐํƒ€'],
            'data' => [45, 25, 20, 10]
        ]) ?>
    </div>
    
    <?php
    // ์ตœ๊ทผ ํ™œ๋™ ์นด๋“œ
    $activityCard = new Card([
        'title' => '์ตœ๊ทผ ํ™œ๋™',
        'subtitle' => '์ง€๋‚œ 24์‹œ๊ฐ„'
    ]);
    
    $activities = [
        ['user' => '๊น€๊ฐœ๋ฐœ', 'action' => '์ƒˆ ํ”„๋กœ์ ํŠธ ๋“ฑ๋ก', 'time' => '5๋ถ„ ์ „'],
        ['user' => '์ด๋””์ž์ธ', 'action' => 'ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ', 'time' => '15๋ถ„ ์ „'],
        ['user' => '๋ฐ•๋งˆ์ผ€ํŒ…', 'action' => '๋ฆฌ๋ทฐ ์ž‘์„ฑ', 'time' => '1์‹œ๊ฐ„ ์ „'],
    ];
    
    $activityHtml = '<div class="activity-list">';
    foreach ($activities as $activity) {
        $activityHtml .= sprintf('
            <div class="activity-item">
                <strong>%s</strong>๋‹˜์ด <em>%s</em> - <span class="time">%s</span>
            </div>
        ', 
            htmlspecialchars($activity['user']),
            htmlspecialchars($activity['action']),
            htmlspecialchars($activity['time'])
        );
    }
    $activityHtml .= '</div>';
    
    $activityCard->setBody($activityHtml);
    echo $activityCard;
    ?>
</body>
</html>

์™€! ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์™„์ „ํžˆ ๋™์ž‘ํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ๋งŒ๋“ค์–ด์ ธ. ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋…๋ฆฝ์ ์ด๋ฉด์„œ๋„ ์กฐํ™”๋กญ๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ์ง€? ๐ŸŽ‰

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์ด ์ž˜ ๋™์ž‘ํ•œ๋‹ค๊ณ  ํ•ด์„œ ๋์ด ์•„๋‹ˆ์•ผ. ์„ฑ๋Šฅ๋„ ์ค‘์š”ํ•˜๊ฑฐ๋“ ! ๋ช‡ ๊ฐ€์ง€ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์•Œ์•„๋ณด์ž. โšก

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ:


1. ์ปดํฌ๋„ŒํŠธ ์บ์‹ฑ
์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œํ•ด๋‘๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ.

<?php
// core/CacheableTrait.php

trait CacheableTrait {
    private $cacheKey;
    private $cacheDuration = 3600; // 1์‹œ๊ฐ„
    
    public function setCacheKey(string $key): self {
        $this->cacheKey = $key;
        return $this;
    }
    
    public function setCacheDuration(int $seconds): self {
        $this->cacheDuration = $seconds;
        return $this;
    }
    
    public function render(): string {
        if (!$this->cacheKey) {
            return parent::render();
        }
        
        // ์บ์‹œ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„
        $cached = $this->getFromCache($this->cacheKey);
        if ($cached !== null) {
            return $cached;
        }
        
        // ๋ Œ๋”๋ง
        $output = parent::render();
        
        // ์บ์‹œ์— ์ €์žฅ
        $this->saveToCache($this->cacheKey, $output, $this->cacheDuration);
        
        return $output;
    }
    
    private function getFromCache(string $key): ?string {
        // ์‹ค์ œ๋กœ๋Š” Redis, Memcached ๋“ฑ ์‚ฌ์šฉ
        // ์—ฌ๊ธฐ์„œ๋Š” ํŒŒ์ผ ์บ์‹œ ์˜ˆ์‹œ
        $cacheFile = sys_get_temp_dir() . '/component_cache_' . md5($key);
        
        if (!file_exists($cacheFile)) {
            return null;
        }
        
        $data = unserialize(file_get_contents($cacheFile));
        
        if ($data['expires'] < time()) {
            unlink($cacheFile);
            return null;
        }
        
        return $data['content'];
    }
    
    private function saveToCache(string $key, string $content, int $duration): void {
        $cacheFile = sys_get_temp_dir() . '/component_cache_' . md5($key);
        
        $data = [
            'content' => $content,
            'expires' => time() + $duration
        ];
        
        file_put_contents($cacheFile, serialize($data));
    }
    
    public function clearCache(): void {
        if ($this->cacheKey) {
            $cacheFile = sys_get_temp_dir() . '/component_cache_' . md5($this->cacheKey);
            if (file_exists($cacheFile)) {
                unlink($cacheFile);
            }
        }
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
class ExpensiveComponent extends Component {
    use CacheableTrait;
    
    protected function doRender(): string {
        // ๋ณต์žกํ•œ ๊ณ„์‚ฐ์ด๋‚˜ DB ์ฟผ๋ฆฌ
        sleep(2); // ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        return '<div>Expensive content</div>';
    }
}

$component = new ExpensiveComponent();
$component
    ->setCacheKey('expensive_component_123')
    ->setCacheDuration(1800); // 30๋ถ„

// ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ: 2์ดˆ ์†Œ์š”
echo $component->render();

// ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ: ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (์บ์‹œ์—์„œ)
echo $component->render();

2. ์ง€์—ฐ ๋กœ๋”ฉ (Lazy Loading) ํ•„์š”ํ•  ๋•Œ๋งŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋กœ๋“œํ•˜๋ฉด ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ๊ฐ„์„ ์ค„์ผ ์ˆ˜ ์žˆ์–ด.

<?php
// core/LazyComponent.php

class LazyComponent {
    private $className;
    private $props;
    private $loaded = false;
    private $instance;
    
    public function __construct(string $className, array $props = []) {
        $this->className = $className;
        $this->props = $props;
    }
    
    private function load(): Component {
        if (!$this->loaded) {
            $this->instance = new $this->className($this->props);
            $this->loaded = true;
        }
        return $this->instance;
    }
    
    public function render(): string {
        return $this->load()->render();
    }
    
    public function __call(string $method, array $args) {
        return call_user_func_array(
            [$this->load(), $method],
            $args
        );
    }
    
    public function __toString(): string {
        return $this->render();
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$lazyCard = new LazyComponent('Card', [
    'title' => 'Lazy Loaded Card'
]);

// ์ด ์‹œ์ ์—๋Š” Card๊ฐ€ ์•„์ง ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ
// render()๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ ๋น„๋กœ์†Œ ์ƒ์„ฑ๋จ
echo $lazyCard; // ์ด์ œ ์ƒ์„ฑ๋จ

3. ์ถœ๋ ฅ ๋ฒ„ํผ๋ง ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•œ ๋ฒˆ์— ๋ Œ๋”๋งํ•  ๋•Œ ์ถœ๋ ฅ ๋ฒ„ํผ๋ง์„ ์‚ฌ์šฉํ•˜๋ฉด ํšจ์œจ์ ์ด์•ผ.

<?php
// core/ComponentRenderer.php

class ComponentRenderer {
    private $components = [];
    
    public function add(Component $component): self {
        $this->components[] = $component;
        return $this;
    }
    
    public function renderAll(): string {
        ob_start();
        
        foreach ($this->components as $component) {
            echo $component->render();
        }
        
        return ob_get_clean();
    }
    
    public function renderToFile(string $filepath): bool {
        $content = $this->renderAll();
        return file_put_contents($filepath, $content) !== false;
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$renderer = new ComponentRenderer();

for ($i = 1; $i <= 100; $i++) {
    $renderer->add(new Card(['title' => "Card {$i}"]));
}

// ํ•œ ๋ฒˆ์— ๋ Œ๋”๋ง
$output = $renderer->renderAll();

// ๋˜๋Š” ํŒŒ์ผ๋กœ ์ €์žฅ
$renderer->renderToFile('output.html');

๐ŸŽ“ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์™€ ํŒ

๋งˆ์ง€๋ง‰์œผ๋กœ, ์‹ค์ „์—์„œ ์ •๋ง ์œ ์šฉํ•œ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ! ์ด๊ฑด ๋‚ด๊ฐ€ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฐฐ์šด ๊ฒƒ๋“ค์ด์•ผ. ๐Ÿ’Ž

1. ์ปดํฌ๋„ŒํŠธ ๋„ค์ด๋ฐ ๊ทœ์น™


โœ… DO: ๋ช…ํ™•ํ•˜๊ณ  ์„ค๋ช…์ ์ธ ์ด๋ฆ„ ์‚ฌ์šฉ
UserProfileCard, ProductListItem, NavigationMenu

โŒ DON'T: ๋ชจํ˜ธํ•˜๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ์งง์€ ์ด๋ฆ„
Card1, Item, Thing

2. Props ์„ค๊ณ„ ์›์น™


โœ… ํ•„์ˆ˜ props๋Š” ์ตœ์†Œํ™” - ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•˜๋ผ
โœ… ํƒ€์ž… ํžŒํŒ… ์‚ฌ์šฉ - PHP 7.4+ ํƒ€์ž… ํ”„๋กœํผํ‹ฐ ํ™œ์šฉ
โœ… ์œ ํšจ์„ฑ ๊ฒ€์ฆ - ์ž˜๋ชป๋œ ๊ฐ’์€ ์ฆ‰์‹œ ์—๋Ÿฌ ๋ฐœ์ƒ
โœ… ๋ฌธ์„œํ™” - PHPDoc์œผ๋กœ ๊ฐ prop ์„ค๋ช…

<?php
class WellDesignedComponent extends Component {
    /**
     * @param array $props
     *   - title (string, required): ์ปดํฌ๋„ŒํŠธ ์ œ๋ชฉ
     *   - subtitle (string, optional): ๋ถ€์ œ๋ชฉ
     *   - variant (string, optional): ์Šคํƒ€์ผ ๋ณ€ํ˜• (default, primary, success)
     *   - size (string, optional): ํฌ๊ธฐ (sm, md, lg)
     */
    public function __construct(array $props = []) {
        // ํ•„์ˆ˜ props ํ™•์ธ
        if (!isset($props['title'])) {
            throw new InvalidArgumentException('title prop is required');
        }
        
        // ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
        $props = array_merge([
            'subtitle' => '',
            'variant' => 'default',
            'size' => 'md'
        ], $props);
        
        parent::__construct($props);
    }
}
3. ์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ (Composition) vs ์ƒ์† (Inheritance)

๊ฐ€๋Šฅํ•˜๋ฉด ์ƒ์†๋ณด๋‹ค๋Š” ๊ตฌ์„ฑ์„ ์„ ํ˜ธํ•ด. ๋” ์œ ์—ฐํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์›Œ!

<?php
// โŒ ๋‚˜์œ ์˜ˆ: ๊ณผ๋„ํ•œ ์ƒ์†
class BaseCard extends Component {}
class UserCard extends BaseCard {}
class AdminUserCard extends UserCard {}
class SuperAdminUserCard extends AdminUserCard {} // ๋„ˆ๋ฌด ๊นŠ์Œ!

// โœ… ์ข‹์€ ์˜ˆ: ๊ตฌ์„ฑ ์‚ฌ์šฉ
class Card extends Component {
    private $header;
    private $body;
    private $footer;
    
    public function withHeader(Component $header): self {
        $this->header = $header;
        return $this;
    }
    
    public function withBody(Component $body): self {
        $this->body = $body;
        return $this;
    }
    
    public function withFooter(Component $footer): self {
        $this->footer = $footer;
        return $this;
    }
}

// ์‚ฌ์šฉ
$card = (new Card())
    ->withHeader(new CardHeader(['title' => 'User Profile']))
    ->withBody(new UserProfile(['userId' => 123]))
    ->withFooter(new CardActions(['buttons' => [...]]));
4. ์—๋Ÿฌ ์ฒ˜๋ฆฌ

์ปดํฌ๋„ŒํŠธ์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด!

<?php
// core/ErrorBoundary.php

class ErrorBoundary extends Component {
    private $fallback;
    
    public function __construct(Component $child, $fallback = null) {
        $this->addChild($child);
        $this->fallback = $fallback ?? $this->getDefaultFallback();
    }
    
    protected function doRender(): string {
        try {
            return $this->renderChildren();
        } catch (Exception $e) {
            // ์—๋Ÿฌ ๋กœ๊น…
            error_log("Component error: " . $e->getMessage());
            
            // ํด๋ฐฑ UI ๋ Œ๋”๋ง
            if (is_callable($this->fallback)) {
                return call_user_func($this->fallback, $e);
            }
            
            return $this->fallback;
        }
    }
    
    private function getDefaultFallback(): string {
        return '<div class="error-boundary">
            <p>โš ๏ธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</p>
        </div>';
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$errorBoundary = new ErrorBoundary(
    new ProblematicComponent(),
    function($error) {
        return sprintf(
            '<div class="error-message">์˜ค๋ฅ˜: %s</div>',
            htmlspecialchars($error->getMessage())
        );
    }
);

echo $errorBoundary;
5. ์ ‘๊ทผ์„ฑ (Accessibility)

๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ ‘๊ทผ์„ฑ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด!

<?php
class AccessibleButton extends Button {
    protected function doRender(): string {
        // ARIA ์†์„ฑ ์ž๋™ ์ถ”๊ฐ€
        if ($this->getProp('disabled')) {
            $this->setAttribute('aria-disabled', 'true');
        }
        
        // ์•„์ด์ฝ˜๋งŒ ์žˆ๋Š” ๋ฒ„ํŠผ์€ aria-label ํ•„์ˆ˜
        if ($this->getProp('icon') && !$this->getProp('text')) {
            if (!$this->attributes['aria-label'] ?? false) {
                throw new RuntimeException(
                    'Icon-only buttons must have aria-label'
                );
            }
        }
        
        // role ์†์„ฑ
        $this->setAttribute('role', 'button');
        
        // ํ‚ค๋ณด๋“œ ์ ‘๊ทผ์„ฑ
        if (!isset($this->attributes['tabindex'])) {
            $this->setAttribute('tabindex', '0');
        }
        
        return parent::doRender();
    }
}

โš ๏ธ ์ฃผ์˜: ์ ‘๊ทผ์„ฑ์€ ์„ ํƒ์‚ฌํ•ญ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜์•ผ! ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž, ํ‚ค๋ณด๋“œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์šฉ์ž, ์ƒ‰๊ฐ ์ด์ƒ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž ๋“ฑ ๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ด.

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜… ์šฐ๋ฆฌ๋Š” PHP์™€ Web Components๋ฅผ ํ™œ์šฉํ•ด์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋งŒ๋“ค์–ด๋ดค์–ด. ๊ธฐ๋ณธ Component ํด๋ž˜์Šค๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ, Web Components ํ†ตํ•ฉ, ์Šคํƒ€์ผ ๊ด€๋ฆฌ, ํ…Œ์ŠคํŒ…, ๊ทธ๋ฆฌ๊ณ  ์‹ค์ „ ๋Œ€์‹œ๋ณด๋“œ๊นŒ์ง€!

์ด ์‹œ์Šคํ…œ์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ํ™•์žฅ์„ฑ์ด์•ผ. ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ ธ๋„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ๊ฐ€ ์‰ฝ๊ฑฐ๋“ . ๊ทธ๋ฆฌ๊ณ  PHP์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง๊ณผ Web Components์˜ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ธํ„ฐ๋ž™์…˜์ด ์™„๋ฒฝํ•˜๊ฒŒ ์กฐํ™”๋ฅผ ์ด๋ฃจ์ง€.

๐Ÿ’ก ๋‹ค์Œ ๋‹จ๊ณ„:
1. ์ƒํƒœ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ - ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ณต์œ 
2. ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ ํ†ตํ•ฉ - SPA ๊ฐ™์€ ๊ฒฝํ—˜ ์ œ๊ณต
3. ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ์ตœ์ ํ™” - ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„ ๊ฐœ์„ 
4. ๋นŒ๋“œ ์‹œ์Šคํ…œ ๊ตฌ์ถ• - ์ž๋™ํ™”๋œ ์ปดํฌ๋„ŒํŠธ ๋ฒˆ๋“ค๋ง
5. ์Šคํ† ๋ฆฌ๋ถ ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ ๋ฌธ์„œํ™” ๋„๊ตฌ ๋„์ž…

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์„ ๊ฐœ๋ฐœํ•  ๋•Œ, ์ด๋Ÿฐ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์ด ์žˆ์œผ๋ฉด ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ์ •๋ง ๋นจ๋ผ์ ธ. ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋‘” ์ปดํฌ๋„ŒํŠธ๋ฅผ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ๋””์ž์ธ ์ผ๊ด€์„ฑ๋„ ์ž๋™์œผ๋กœ ์œ ์ง€๋˜๊ฑฐ๋“ . ๐Ÿš€

๊ธฐ์–ตํ•ด! ์ข‹์€ ์ปดํฌ๋„ŒํŠธ๋Š”:


โœจ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ  (Reusable)
๐Ÿ”’ ์บก์Аํ™”๋˜์–ด ์žˆ์œผ๋ฉฐ (Encapsulated)
๐Ÿงฉ ์กฐํ•ฉ ๊ฐ€๋Šฅํ•˜๊ณ  (Composable)
๐Ÿ“ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๋ฉฐ (Testable)
โ™ฟ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์•ผ ํ•ด (Accessible)

์ด ์›์น™๋“ค์„ ์ง€ํ‚ค๋ฉด์„œ ๊ฐœ๋ฐœํ•˜๋ฉด, ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฝ๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ํ–‰์šด์„ ๋นŒ์–ด! ํ™”์ดํŒ…! ๐Ÿ’ชโœจ

๐ŸŽจ โšก ๐Ÿš€ Happy Component Building! ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ ๋” ๋‚˜์€ ์›น์„ ๋งŒ๋“ค์–ด๊ฐ€์„ธ์š”
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.