<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>comely_15.log</title>
        <link>https://velog.io/</link>
        <description>App, Web Developer</description>
        <lastBuildDate>Mon, 09 Jun 2025 14:37:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>comely_15.log</title>
            <url>https://velog.velcdn.com/images/comely_15/profile/1c225aa7-f24c-4852-bf3a-316dc9e7c719/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. comely_15.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/comely_15" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[SQL&Database] Stored Procedure, 변수, 날짜
]]></title>
            <link>https://velog.io/@comely_15/SQLDatabase-Stored-Procedure-%EB%B3%80%EC%88%98-%EB%82%A0%EC%A7%9C</link>
            <guid>https://velog.io/@comely_15/SQLDatabase-Stored-Procedure-%EB%B3%80%EC%88%98-%EB%82%A0%EC%A7%9C</guid>
            <pubDate>Mon, 09 Jun 2025 14:37:06 GMT</pubDate>
            <description><![CDATA[<h2 id="stored-procedure---코드-재사용의-핵심"><strong>Stored Procedure - 코드 재사용의 핵심</strong></h2>
<h3 id="stored-procedure란"><strong>Stored Procedure란?</strong></h3>
<p>자주 사용하는 SQL 코드 덩어리를 저장해두고 필요할 때마다 호출하여 사용할 수 있는 기능입니다. 프로그래밍의 함수와 비슷한 개념입니다.</p>
<h3 id="stored-procedure-생성-방법"><strong>Stored Procedure 생성 방법</strong></h3>
<p><strong>DBeaver에서 생성</strong>:</p>
<ol>
<li>데이터베이스의 <code>Procedures</code> 메뉴에서 우클릭</li>
<li>새로운 Procedure 생성 선택</li>
<li><code>source</code> 메뉴에서 <code>BEGIN/END</code> 사이에 코드 작성</li>
</ol>
<p><strong>SQL로 직접 생성</strong>:</p>
<pre><code class="language-sql">DROP PROCEDURE IF EXISTS 데이터베이스명.get_all;

DELIMITER $$
CREATE PROCEDURE 데이터베이스명.get_all()
BEGIN
  SELECT * FROM product WHERE 가격 &gt; 5000;
END 
$$
DELIMITER ;</code></pre>
<p><strong>주요 구성 요소</strong>:</p>
<ul>
<li><code>DROP PROCEDURE IF EXISTS</code>: 기존 프로시저가 있으면 삭제</li>
<li><code>DELIMITER $$</code>: 구분자를 <code>$$</code>로 변경 (내부 <code>;</code> 때문)</li>
<li><code>BEGIN/END</code>: 실행할 코드 블록</li>
<li><code>DELIMITER ;</code>: 구분자를 다시 <code>;</code>로 복원</li>
</ul>
<h3 id="stored-procedure-실행"><strong>Stored Procedure 실행</strong></h3>
<pre><code class="language-sql">CALL procedure이름();
-- 또는
CALL 데이터베이스명.procedure이름();</code></pre>
<h3 id="성능-및-생산성-향상"><strong>성능 및 생산성 향상</strong></h3>
<p><strong>성능 측면</strong>:</p>
<ul>
<li>캐싱된 실행 계획 재사용으로 약간의 속도 향상</li>
<li>하지만 실제 실행 속도는 직접 SQL과 큰 차이 없음</li>
</ul>
<p><strong>생산성 측면</strong>:</p>
<ol>
<li><strong>코드 재사용</strong>: 반복되는 긴 코드를 간단히 호출</li>
<li><strong>팀 협업</strong>: 개발자가 아닌 사용자도 복잡한 쿼리 활용 가능</li>
<li><strong>유지보수</strong>: 중앙 집중식 코드 관리</li>
</ol>
<hr>
<h2 id="변수-variables---데이터-임시-저장"><strong>변수 (Variables) - 데이터 임시 저장</strong></h2>
<h3 id="user-variable-변수"><strong>User Variable (@변수)</strong></h3>
<p>세션이 종료될 때까지 유지되는 전역 변수입니다.</p>
<pre><code class="language-sql">-- 변수 생성 및 할당
SET @age = 20;
SELECT @age := 20;  -- 동일한 결과

-- 변수 사용
SELECT @age;

-- 계산과 함께 사용
SET @price = 6000;
SELECT * FROM product WHERE 가격 = @price;

-- 서브쿼리 결과 저장
SET @count = (SELECT COUNT(*) FROM product WHERE 가격 = 5000);

-- 변수 값 증가
SET @age = @age + 1;</code></pre>
<h3 id="local-variable-declare-변수"><strong>Local Variable (DECLARE 변수)</strong></h3>
<p>Procedure 내부에서만 사용 가능한 지역 변수입니다.</p>
<pre><code class="language-sql">CREATE PROCEDURE var_test() 
BEGIN 
  DECLARE 변수1 INT;
  DECLARE 변수2 VARCHAR(100);
  DECLARE 변수3 INT DEFAULT 123;  -- 기본값 설정

  SET 변수1 = 10;
  SET 변수1 = 변수1 + 1;
  SELECT 변수1;  -- 결과: 11
END</code></pre>
<h3 id="user-variable-vs-local-variable-비교"><strong>User Variable vs Local Variable 비교</strong></h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>User Variable (@변수)</th>
<th>Local Variable (DECLARE)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>사용 범위</strong></td>
<td>전역 (모든 SQL에서 사용)</td>
<td>Procedure 내부만</td>
</tr>
<tr>
<td><strong>생존 기간</strong></td>
<td>세션 종료까지</td>
<td>Procedure 실행 종료까지</td>
</tr>
<tr>
<td><strong>선언 위치</strong></td>
<td>어디서나</td>
<td>BEGIN 바로 다음</td>
</tr>
<tr>
<td><strong>안전성</strong></td>
<td>중복 선언 위험</td>
<td>안전함</td>
</tr>
</tbody></table>
<hr>
<h2 id="parameter---유연한-procedure-만들기"><strong>Parameter - 유연한 Procedure 만들기</strong></h2>
<h3 id="입력-parameter-in"><strong>입력 Parameter (IN)</strong></h3>
<p>Procedure 실행 시 외부에서 값을 전달받을 수 있습니다.</p>
<pre><code class="language-sql">-- 단일 파라미터
CREATE PROCEDURE get_products(price_limit INT)
BEGIN
    SELECT * FROM product WHERE 가격 &gt; price_limit;
END;

-- 사용법
CALL get_products(6000);  -- 6000원 이상 상품 조회
CALL get_products(8000);  -- 8000원 이상 상품 조회

-- 다중 파라미터
CREATE PROCEDURE search_products(price_min INT, name_part VARCHAR(100))
BEGIN
    SELECT * FROM product 
    WHERE 가격 &gt; price_min OR 상품명 LIKE CONCAT(&#39;%&#39;, name_part, &#39;%&#39;);
END;

-- 사용법
CALL search_products(5000, &#39;김치&#39;);</code></pre>
<h3 id="출력-parameter-out"><strong>출력 Parameter (OUT)</strong></h3>
<p>Procedure 내부의 값을 외부로 전달할 수 있습니다.</p>
<pre><code class="language-sql">CREATE PROCEDURE calculate_total(OUT total_price INT)
BEGIN
    SET total_price = (SELECT SUM(가격) FROM product);
END;

-- 사용법
CALL calculate_total(@result);
SELECT @result;  -- 총 가격 출력</code></pre>
<hr>
<h2 id="날짜시간-데이터-처리"><strong>날짜/시간 데이터 처리</strong></h2>
<h3 id="날짜-데이터-타입"><strong>날짜 데이터 타입</strong></h3>
<table>
<thead>
<tr>
<th>데이터 타입</th>
<th>형식</th>
<th>범위</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DATE</strong></td>
<td>YYYY-MM-DD</td>
<td>1000-9999년</td>
<td>날짜만 저장</td>
</tr>
<tr>
<td><strong>DATETIME</strong></td>
<td>YYYY-MM-DD HH:MM:SS</td>
<td>1000-9999년</td>
<td>날짜와 시간 저장</td>
</tr>
<tr>
<td><strong>TIMESTAMP</strong></td>
<td>YYYY-MM-DD HH:MM:SS</td>
<td>1970-2038년</td>
<td>현재 시간 기록용</td>
</tr>
</tbody></table>
<p><strong>정밀도 설정</strong>:</p>
<pre><code class="language-sql">DATETIME(6)  -- 마이크로초(6자리)까지 저장</code></pre>
<h3 id="날짜-조회-및-필터링"><strong>날짜 조회 및 필터링</strong></h3>
<p><strong>기본 날짜 조회</strong>:</p>
<pre><code class="language-sql">-- 특정 날짜/시간 이후
SELECT * FROM blog WHERE 발행일 &gt; &#39;2022-03-10 08:24:25&#39;;

-- 정확한 날짜/시간
SELECT * FROM blog WHERE 발행일 = &#39;2022-03-10 08:24:25&#39;;</code></pre>
<p><strong>특정 날짜의 모든 데이터 조회</strong>:</p>
<pre><code class="language-sql">-- 2022년 3월 10일 하루 전체
SELECT * FROM blog 
WHERE 발행일 &gt;= &#39;2022-03-10 00:00:00&#39; 
  AND 발행일 &lt; &#39;2022-03-11 00:00:00&#39;;

-- 현재 시간까지의 범위
SELECT * FROM blog 
WHERE 발행일 &gt; &#39;2022-03-10 00:00:00&#39; 
  AND 발행일 &lt;= NOW();</code></pre>
<h3 id="날짜-함수-활용"><strong>날짜 함수 활용</strong></h3>
<p><strong>현재 시간 조회</strong>:</p>
<pre><code class="language-sql">SELECT NOW();           -- 현재 날짜와 시간
SELECT NOW(6);          -- 마이크로초까지
SELECT CURDATE();       -- 현재 날짜만</code></pre>
<p><strong>날짜 형식 변경</strong>:</p>
<pre><code class="language-sql">SELECT DATE_FORMAT(NOW(), &#39;%Y년 %m월 %d일&#39;);
SELECT DATE_FORMAT(NOW(), &#39;%H:%i:%s&#39;);</code></pre>
<p><strong>주요 형식 지정자</strong>:</p>
<ul>
<li><code>%Y</code>: 4자리 년도, <code>%y</code>: 2자리 년도</li>
<li><code>%m</code>: 월(01-12), <code>%d</code>: 일(01-31)</li>
<li><code>%H</code>: 시간(00-23), <code>%i</code>: 분(00-59), <code>%s</code>: 초(00-59)</li>
</ul>
<p><strong>날짜 연산</strong>:</p>
<pre><code class="language-sql">-- 1년 추가
SELECT DATE_ADD(NOW(), INTERVAL 1 YEAR);
SELECT NOW() + INTERVAL 1 YEAR;

-- 다양한 간격
SELECT DATE_ADD(&#39;2022-01-01&#39;, INTERVAL 1 MONTH);
SELECT DATE_ADD(&#39;2022-01-01&#39;, INTERVAL 7 DAY);</code></pre>
<p><strong>날짜 부분 추출</strong>:</p>
<pre><code class="language-sql">SELECT YEAR(NOW());     -- 년도
SELECT MONTH(NOW());    -- 월
SELECT DAY(NOW());      -- 일
SELECT HOUR(NOW());     -- 시간</code></pre>
<h3 id="날짜-데이터-삽입"><strong>날짜 데이터 삽입</strong></h3>
<pre><code class="language-sql">INSERT INTO blog (제목, 발행일) 
VALUES (&#39;새 글&#39;, &#39;2024-01-15 14:30:00&#39;);

-- 현재 시간으로 삽입
INSERT INTO blog (제목, 발행일) 
VALUES (&#39;새 글&#39;, NOW());</code></pre>
<hr>
<h2 id="실전-활용-예제"><strong>실전 활용 예제</strong></h2>
<h3 id="월간-활성-사용자mau-계산"><strong>월간 활성 사용자(MAU) 계산</strong></h3>
<pre><code class="language-sql">-- 2022년 11월 MAU
SELECT COUNT(*) FROM login_record 
WHERE last_login &gt;= &#39;2022-11-01 00:00:00&#39; 
  AND last_login &lt; &#39;2022-12-01 00:00:00&#39;;</code></pre>
<h3 id="조건부-날짜-조회"><strong>조건부 날짜 조회</strong></h3>
<pre><code class="language-sql">-- 9월의 짝수일 데이터 조회
SELECT * FROM login_record 
WHERE MONTH(last_login) = 9 
  AND DAY(last_login) % 2 = 0;</code></pre>
<h3 id="복합-procedure-예제"><strong>복합 Procedure 예제</strong></h3>
<pre><code class="language-sql">CREATE PROCEDURE analyze_sales(
    IN start_date DATE,
    IN end_date DATE,
    OUT total_sales INT,
    OUT avg_daily_sales DECIMAL(10,2)
)
BEGIN
    DECLARE day_count INT;

    -- 총 매출 계산
    SELECT SUM(가격) INTO total_sales
    FROM sales s
    JOIN product p ON s.상품id = p.id
    WHERE s.구매날짜 BETWEEN start_date AND end_date;

    -- 일수 계산
    SET day_count = DATEDIFF(end_date, start_date) + 1;

    -- 일평균 매출 계산
    SET avg_daily_sales = total_sales / day_count;
END;

-- 사용법
CALL analyze_sales(&#39;2022-01-01&#39;, &#39;2022-01-31&#39;, @total, @avg);
SELECT @total as 총매출, @avg as 일평균매출;</code></pre>
<h3 id="시간대별-분석-procedure"><strong>시간대별 분석 Procedure</strong></h3>
<pre><code class="language-sql">CREATE PROCEDURE hourly_analysis(analysis_date DATE)
BEGIN
    SELECT 
        HOUR(구매시간) as 시간대,
        COUNT(*) as 구매건수,
        SUM(가격) as 시간대매출
    FROM sales s
    JOIN product p ON s.상품id = p.id
    WHERE DATE(구매시간) = analysis_date
    GROUP BY HOUR(구매시간)
    ORDER BY 시간대;
END;

CALL hourly_analysis(&#39;2022-03-15&#39;);</code></pre>
<p>이러한 Stored Procedure, 변수, 날짜 처리 기능들을 조합하면 복잡한 비즈니스 로직도 효율적으로 구현할 수 있으며, 코드의 재사용성과 유지보수성을 크게 향상시킬 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL&Database] JOIN과 데이터 조작
]]></title>
            <link>https://velog.io/@comely_15/SQLDatabase-JOIN%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%EC%9E%91</link>
            <guid>https://velog.io/@comely_15/SQLDatabase-JOIN%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%EC%9E%91</guid>
            <pubDate>Mon, 09 Jun 2025 14:25:44 GMT</pubDate>
            <description><![CDATA[<h2 id="다중-테이블-조회의-기본-개념"><strong>다중 테이블 조회의 기본 개념</strong></h2>
<h3 id="여러-테이블의-데이터를-함께-출력하는-방법"><strong>여러 테이블의 데이터를 함께 출력하는 방법</strong></h3>
<p>정규화된 데이터베이스에서는 데이터가 여러 테이블에 분산되어 있어 관련 정보를 함께 조회해야 하는 경우가 많습니다.</p>
<p><strong>예시 테이블 구조</strong></p>
<p><strong>program 테이블</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>프로그램</th>
<th>가격</th>
<th>강사id</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>스쿼시</td>
<td>5000</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>헬스</td>
<td>6000</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>골프</td>
<td>8000</td>
<td>3</td>
</tr>
<tr>
<td>4</td>
<td>골프중급</td>
<td>9000</td>
<td>3</td>
</tr>
<tr>
<td>5</td>
<td>개인피티</td>
<td>15000</td>
<td>4</td>
</tr>
</tbody></table>
<p><strong>teacher 테이블</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>강사</th>
<th>출신대학</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>2</td>
<td>박덕팔</td>
<td>연세대</td>
</tr>
<tr>
<td>3</td>
<td>이상구</td>
<td>고려대</td>
</tr>
</tbody></table>
<hr>
<h2 id="기본-다중-테이블-조회"><strong>기본 다중 테이블 조회</strong></h2>
<h3 id="from-절에-여러-테이블-지정"><strong>FROM 절에 여러 테이블 지정</strong></h3>
<pre><code class="language-sql">-- 여러 테이블 동시 조회
SELECT 프로그램, 가격, 강사, 출신대학
FROM program, teacher;

-- 테이블명.컬럼명 형태로 명시 (권장)
SELECT program.프로그램, program.가격, teacher.강사, teacher.출신대학
FROM program, teacher;</code></pre>
<h3 id="cross-join의-문제점"><strong>CROSS JOIN의 문제점</strong></h3>
<p>조건 없이 여러 테이블을 조회하면 <strong>카르테시안 곱(Cartesian Product)</strong>이 발생합니다:</p>
<p><strong>CROSS JOIN 결과 예시</strong></p>
<table>
<thead>
<tr>
<th>프로그램</th>
<th>가격</th>
<th>강사id</th>
<th>강사</th>
<th>출신대학</th>
</tr>
</thead>
<tbody><tr>
<td>스쿼시</td>
<td>5000</td>
<td>1</td>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>스쿼시</td>
<td>5000</td>
<td>1</td>
<td>박덕팔</td>
<td>연세대</td>
</tr>
<tr>
<td>스쿼시</td>
<td>5000</td>
<td>1</td>
<td>이상구</td>
<td>고려대</td>
</tr>
<tr>
<td>헬스</td>
<td>6000</td>
<td>2</td>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody></table>
<ul>
<li>program 테이블 5행 × teacher 테이블 3행 = <strong>15행 출력</strong></li>
<li>대부분이 의미 없는 조합</li>
</ul>
<h3 id="올바른-조회-방법---where-조건-추가"><strong>올바른 조회 방법 - WHERE 조건 추가</strong></h3>
<pre><code class="language-sql">SELECT program.프로그램, program.가격, teacher.강사, teacher.출신대학
FROM program, teacher
WHERE program.강사id = teacher.id;</code></pre>
<p><strong>올바른 결과</strong></p>
<table>
<thead>
<tr>
<th>프로그램</th>
<th>가격</th>
<th>강사</th>
<th>출신대학</th>
</tr>
</thead>
<tbody><tr>
<td>스쿼시</td>
<td>5000</td>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>헬스</td>
<td>6000</td>
<td>박덕팔</td>
<td>연세대</td>
</tr>
<tr>
<td>골프</td>
<td>8000</td>
<td>이상구</td>
<td>고려대</td>
</tr>
<tr>
<td>골프중급</td>
<td>9000</td>
<td>이상구</td>
<td>고려대</td>
</tr>
</tbody></table>
<hr>
<h2 id="inner-join---권장되는-문법"><strong>INNER JOIN - 권장되는 문법</strong></h2>
<h3 id="inner-join-기본-문법"><strong>INNER JOIN 기본 문법</strong></h3>
<pre><code class="language-sql">SELECT 출력할컬럼들
FROM 테이블1 INNER JOIN 테이블2
ON 조건문;</code></pre>
<h3 id="실제-사용-예시"><strong>실제 사용 예시</strong></h3>
<pre><code class="language-sql">SELECT program.프로그램, program.가격, teacher.강사, teacher.출신대학
FROM program INNER JOIN teacher
ON program.강사id = teacher.id;</code></pre>
<p><strong>INNER JOIN의 장점</strong>:</p>
<ul>
<li>의도가 명확히 드러남 (&quot;테이블을 연결한다&quot;)</li>
<li>가독성이 좋음</li>
<li>표준 SQL 문법</li>
</ul>
<h3 id="3개-이상-테이블-join"><strong>3개 이상 테이블 JOIN</strong></h3>
<pre><code class="language-sql">-- 방법 1: FROM에 여러 테이블
SELECT *
FROM 테이블1, 테이블2, 테이블3
WHERE 조건1 AND 조건2;

-- 방법 2: 연속된 INNER JOIN (권장)
SELECT *
FROM 테이블1 
INNER JOIN 테이블2 ON 조건1
INNER JOIN 테이블3 ON 조건2;</code></pre>
<hr>
<h2 id="다양한-join-유형"><strong>다양한 JOIN 유형</strong></h2>
<h3 id="left-join---왼쪽-테이블-전체-포함"><strong>LEFT JOIN - 왼쪽 테이블 전체 포함</strong></h3>
<pre><code class="language-sql">SELECT * 
FROM program 
LEFT JOIN teacher
ON program.강사id = teacher.id;</code></pre>
<p><strong>LEFT JOIN 결과</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>프로그램</th>
<th>가격</th>
<th>강사id</th>
<th>id</th>
<th>강사</th>
<th>출신대학</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>스쿼시</td>
<td>5000</td>
<td>1</td>
<td>1</td>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>2</td>
<td>헬스</td>
<td>6000</td>
<td>2</td>
<td>2</td>
<td>박덕팔</td>
<td>연세대</td>
</tr>
<tr>
<td>3</td>
<td>골프</td>
<td>8000</td>
<td>3</td>
<td>3</td>
<td>이상구</td>
<td>고려대</td>
</tr>
<tr>
<td>4</td>
<td>골프중급</td>
<td>9000</td>
<td>3</td>
<td>3</td>
<td>이상구</td>
<td>고려대</td>
</tr>
<tr>
<td>5</td>
<td>개인피티</td>
<td>15000</td>
<td>4</td>
<td>NULL</td>
<td>NULL</td>
<td>NULL</td>
</tr>
</tbody></table>
<ul>
<li><strong>특징</strong>: 왼쪽 테이블(program)의 모든 행이 포함됨</li>
<li><strong>NULL</strong>: 매칭되는 오른쪽 테이블 데이터가 없는 경우</li>
</ul>
<h3 id="right-join---오른쪽-테이블-전체-포함"><strong>RIGHT JOIN - 오른쪽 테이블 전체 포함</strong></h3>
<pre><code class="language-sql">SELECT * 
FROM program 
RIGHT JOIN teacher
ON program.강사id = teacher.id;</code></pre>
<h3 id="null-값-활용한-분석"><strong>NULL 값 활용한 분석</strong></h3>
<pre><code class="language-sql">-- 매칭되지 않는 데이터 찾기
SELECT * 
FROM program 
LEFT JOIN teacher
ON program.강사id = teacher.id
WHERE teacher.id IS NULL;  -- 강사 정보가 없는 프로그램</code></pre>
<h3 id="join-유형-비교"><strong>JOIN 유형 비교</strong></h3>
<table>
<thead>
<tr>
<th>JOIN 유형</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><strong>INNER JOIN</strong></td>
<td>양쪽 테이블에 모두 있는 행만</td>
</tr>
<tr>
<td><strong>LEFT JOIN</strong></td>
<td>왼쪽 테이블 전체 + 매칭되는 오른쪽 행</td>
</tr>
<tr>
<td><strong>RIGHT JOIN</strong></td>
<td>오른쪽 테이블 전체 + 매칭되는 왼쪽 행</td>
</tr>
<tr>
<td><strong>FULL JOIN</strong></td>
<td>양쪽 테이블의 모든 행 (MySQL 미지원)</td>
</tr>
<tr>
<td><strong>CROSS JOIN</strong></td>
<td>모든 조합 (조건 없음)</td>
</tr>
</tbody></table>
<hr>
<h2 id="데이터-삽입-insert"><strong>데이터 삽입 (INSERT)</strong></h2>
<h3 id="기본-insert-문법"><strong>기본 INSERT 문법</strong></h3>
<pre><code class="language-sql">INSERT INTO 테이블명 (컬럼명1, 컬럼명2, ...) VALUES (값1, 값2, ...);</code></pre>
<h3 id="실제-사용-예시-1"><strong>실제 사용 예시</strong></h3>
<pre><code class="language-sql">-- 테이블 생성
CREATE TABLE product (
    id INT AUTO_INCREMENT PRIMARY KEY,
    상품명 VARCHAR(100),
    가격 INT
);

-- 데이터 삽입
INSERT INTO product (id, 상품명, 가격) VALUES (1, &#39;김치&#39;, 500);

-- AUTO_INCREMENT 컬럼 생략
INSERT INTO product (상품명, 가격) VALUES (&#39;두부&#39;, 1000);

-- 모든 컬럼 입력 시 컬럼명 생략 가능
INSERT INTO product VALUES (3, &#39;수박&#39;, 1500);

-- 여러 행 동시 삽입
INSERT INTO product VALUES 
    (4, &#39;참외&#39;, 2000), 
    (5, &#39;배추&#39;, 2500);</code></pre>
<h3 id="다른-테이블-데이터-복사"><strong>다른 테이블 데이터 복사</strong></h3>
<pre><code class="language-sql">-- SELECT 결과를 다른 테이블에 삽입
INSERT INTO product (id, 상품명, 가격)
SELECT id, 상품명, 가격 FROM product2;

-- 테이블 전체 복사 (MySQL)
CREATE TABLE 새테이블 SELECT * FROM 기존테이블;

-- 임시 테이블 생성
CREATE TEMPORARY TABLE 임시테이블 SELECT * FROM 기존테이블;</code></pre>
<hr>
<h2 id="데이터-수정-update"><strong>데이터 수정 (UPDATE)</strong></h2>
<h3 id="기본-update-문법"><strong>기본 UPDATE 문법</strong></h3>
<pre><code class="language-sql">UPDATE 테이블명 
SET 컬럼1 = 값, 컬럼2 = 값
WHERE 조건식;</code></pre>
<h3 id="실제-사용-예시-2"><strong>실제 사용 예시</strong></h3>
<pre><code class="language-sql">-- 특정 행 수정
UPDATE product
SET 가격 = 5000, 상품명 = &#39;단무지&#39;
WHERE id = 1;

-- 기존 값 활용한 수정
UPDATE product
SET 가격 = 가격 + 100  -- 기존 가격에 100원 추가
WHERE id = 1;

-- 조건부 수정 (IF 함수 활용)
UPDATE user_sales 
SET email = IF(first_name = &#39;Solly&#39;, &#39;admin@test.com&#39;, &#39;test@test.com&#39;) 
WHERE email = &#39;&#39;;</code></pre>
<h3 id="join과-함께-사용하는-update"><strong>JOIN과 함께 사용하는 UPDATE</strong></h3>
<pre><code class="language-sql">UPDATE A INNER JOIN B 
ON 조인조건
SET A.컬럼 = 값, B.컬럼 = 값
WHERE 조건식;</code></pre>
<hr>
<h2 id="데이터-삭제-delete"><strong>데이터 삭제 (DELETE)</strong></h2>
<h3 id="기본-delete-문법"><strong>기본 DELETE 문법</strong></h3>
<pre><code class="language-sql">DELETE FROM 테이블명 WHERE 조건식;</code></pre>
<h3 id="실제-사용-예시-3"><strong>실제 사용 예시</strong></h3>
<pre><code class="language-sql">-- 특정 조건의 행 삭제
DELETE FROM product WHERE id = 5;

-- NULL 값을 가진 행 삭제
DELETE FROM user_sales WHERE sales IS NULL;</code></pre>
<h3 id="join과-함께-사용하는-delete"><strong>JOIN과 함께 사용하는 DELETE</strong></h3>
<pre><code class="language-sql">DELETE A 
FROM A INNER JOIN B 
ON 조인조건
WHERE 조건식;

-- A 테이블에서만 삭제: DELETE A
-- B 테이블에서만 삭제: DELETE B  
-- 양쪽 모두 삭제: DELETE A, B</code></pre>
<p><strong>중요한 주의사항</strong>:</p>
<ul>
<li><code>WHERE</code> 조건 빼먹으면 <strong>모든 행이 삭제됨</strong></li>
<li>Foreign Key 제약이 있는 데이터는 삭제 시 오류 발생</li>
<li>삭제 전 반드시 백업 고려</li>
</ul>
<hr>
<h2 id="union---결과-합치기"><strong>UNION - 결과 합치기</strong></h2>
<h3 id="union-기본-문법"><strong>UNION 기본 문법</strong></h3>
<pre><code class="language-sql">SELECT 컬럼1, 컬럼2 FROM 테이블1
UNION
SELECT 컬럼1, 컬럼2 FROM 테이블2;</code></pre>
<h3 id="실제-사용-예시-4"><strong>실제 사용 예시</strong></h3>
<pre><code class="language-sql">-- 두 테이블의 모든 데이터 합치기
SELECT * FROM stock
UNION
SELECT * FROM stock2;

-- 같은 테이블의 다른 조건 결과 합치기
SELECT * FROM stock WHERE id = 1
UNION
SELECT * FROM stock WHERE id = 2;</code></pre>
<h3 id="union-vs-union-all"><strong>UNION vs UNION ALL</strong></h3>
<pre><code class="language-sql">-- UNION: 중복 제거
SELECT * FROM stock
UNION
SELECT * FROM stock2;

-- UNION ALL: 중복 포함
SELECT * FROM stock
UNION ALL
SELECT * FROM stock2;</code></pre>
<p><strong>UNION 사용 조건</strong>:</p>
<ul>
<li>컬럼 개수가 동일해야 함</li>
<li>데이터 타입이 호환되어야 함</li>
</ul>
<p><strong>JOIN vs UNION 비교</strong>:</p>
<ul>
<li><strong>JOIN</strong>: 테이블을 좌우로 결합 (컬럼 확장)</li>
<li><strong>UNION</strong>: 테이블을 상하로 결합 (행 확장)</li>
</ul>
<hr>
<h2 id="view---가상-테이블"><strong>VIEW - 가상 테이블</strong></h2>
<h3 id="view-개념"><strong>VIEW 개념</strong></h3>
<ul>
<li>자주 사용하는 복잡한 SELECT 문을 저장해둔 <strong>가상 테이블</strong></li>
<li>실제 데이터를 저장하지 않고 쿼리만 저장</li>
<li>테이블처럼 사용 가능</li>
</ul>
<h3 id="view-생성"><strong>VIEW 생성</strong></h3>
<pre><code class="language-sql">CREATE VIEW 뷰이름 AS
SELECT 컬럼1, 컬럼2, ...
FROM 테이블명
WHERE 조건;

-- 실제 예시
CREATE VIEW sales_summary AS
SELECT s.id, s.구매날짜, p.상품명, p.가격, u.고객명
FROM sales s
INNER JOIN product p ON s.상품id = p.id
INNER JOIN user_table u ON s.고객번호 = u.id;</code></pre>
<h3 id="view-활용"><strong>VIEW 활용</strong></h3>
<pre><code class="language-sql">-- VIEW를 테이블처럼 사용
SELECT * FROM sales_summary;
SELECT * FROM sales_summary WHERE 가격 &gt; 10000;

-- VIEW 기반 추가 집계
SELECT 구매날짜, SUM(가격) as 일별매출
FROM sales_summary
GROUP BY 구매날짜;</code></pre>
<p><strong>VIEW의 장점</strong>:</p>
<ol>
<li><strong>재사용성</strong>: 복잡한 JOIN 쿼리를 간단히 재사용</li>
<li><strong>보안</strong>: 필요한 컬럼만 노출하여 데이터 보안 강화</li>
<li><strong>용량 절약</strong>: 실제 데이터 복사 없이 가상으로 제공</li>
<li><strong>실험용</strong>: 테이블 구조 변경 전 테스트 용도</li>
</ol>
<hr>
<h2 id="실전-예제"><strong>실전 예제</strong></h2>
<h3 id="매출-분석-종합-예시"><strong>매출 분석 종합 예시</strong></h3>
<pre><code class="language-sql">-- 1. 기본 매출 조회 (3개 테이블 JOIN)
SELECT s.id, s.구매날짜, p.상품명, p.가격, u.고객명
FROM sales s
INNER JOIN product p ON s.상품id = p.id
INNER JOIN user_table u ON s.고객번호 = u.id;

-- 2. 날짜별 매출 집계
SELECT 구매날짜, SUM(가격) as 일별매출
FROM sales s
INNER JOIN product p ON s.상품id = p.id
GROUP BY 구매날짜
ORDER BY 구매날짜;

-- 3. 매출에 없는 상품 찾기 (LEFT JOIN 활용)
SELECT p.*
FROM product p
LEFT JOIN sales s ON p.id = s.상품id
WHERE s.상품id IS NULL;

-- 4. 고객별 구매 내역과 총액
SELECT u.고객명, COUNT(*) as 구매횟수, SUM(p.가격) as 총구매액
FROM sales s
INNER JOIN product p ON s.상품id = p.id
INNER JOIN user_table u ON s.고객번호 = u.id
GROUP BY u.id, u.고객명
ORDER BY 총구매액 DESC;</code></pre>
<p>이러한 JOIN과 데이터 조작 기법들을 마스터하면 복잡한 비즈니스 요구사항도 효율적으로 처리할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL&Database] 테이블 관리 및 정규화]]></title>
            <link>https://velog.io/@comely_15/SQLDatabase-%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B4%80%EB%A6%AC-%EB%B0%8F-%EC%A0%95%EA%B7%9C%ED%99%94</link>
            <guid>https://velog.io/@comely_15/SQLDatabase-%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B4%80%EB%A6%AC-%EB%B0%8F-%EC%A0%95%EA%B7%9C%ED%99%94</guid>
            <pubDate>Mon, 09 Jun 2025 14:15:59 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터베이스와-테이블-기본-관리"><strong>데이터베이스와 테이블 기본 관리</strong></h2>
<h3 id="데이터베이스-생성과-삭제"><strong>데이터베이스 생성과 삭제</strong></h3>
<pre><code class="language-sql">-- 데이터베이스 생성
CREATE DATABASE 데이터베이스이름;

-- 데이터베이스 삭제 (되돌릴 수 없음!)
DROP DATABASE 데이터베이스이름;</code></pre>
<h3 id="테이블-생성"><strong>테이블 생성</strong></h3>
<p>테이블을 만들 때는 컬럼명과 데이터 타입을 지정해야 합니다.</p>
<pre><code class="language-sql">-- 기본 테이블 생성
CREATE TABLE 테이블명 (
    컬럼1이름 datatype,
    컬럼2이름 datatype,
    컬럼3이름 datatype
    -- 주의: 마지막 줄에는 콤마(,) 금지!
);

-- 실제 예시
CREATE TABLE new_table (
    id INT,
    이름 VARCHAR(100),
    나이 INT
);</code></pre>
<p><strong>기본값 설정</strong></p>
<pre><code class="language-sql">CREATE TABLE new_table2 (
    id INT,
    이름 VARCHAR(100) DEFAULT &#39;홍길동&#39;,  -- 기본값 설정
    나이 INT
);</code></pre>
<ul>
<li>데이터가 입력되지 않을 때 자동으로 채워질 기본값을 설정할 수 있습니다.</li>
</ul>
<h3 id="테이블-삭제"><strong>테이블 삭제</strong></h3>
<pre><code class="language-sql">DROP TABLE 테이블명;  -- 되돌릴 수 없음!</code></pre>
<hr>
<h2 id="컬럼-관리-alter-table"><strong>컬럼 관리 (ALTER TABLE)</strong></h2>
<h3 id="컬럼-추가"><strong>컬럼 추가</strong></h3>
<pre><code class="language-sql">ALTER TABLE 테이블명
ADD 컬럼명 VARCHAR(100);

-- 기본값과 함께 추가
ALTER TABLE 테이블명
ADD 컬럼명 INT DEFAULT 0;</code></pre>
<h3 id="컬럼-데이터-타입-변경"><strong>컬럼 데이터 타입 변경</strong></h3>
<pre><code class="language-sql">ALTER TABLE 테이블명
MODIFY COLUMN 컬럼명 datatype;

-- 예시
ALTER TABLE member 
MODIFY COLUMN 나이 BIGINT;</code></pre>
<p><strong>주의사항</strong>: 이미 문자 데이터가 있는 컬럼을 숫자 타입으로 변경하는 것은 불가능</p>
<h3 id="컬럼-삭제"><strong>컬럼 삭제</strong></h3>
<pre><code class="language-sql">ALTER TABLE 테이블명
DROP COLUMN 컬럼명;</code></pre>
<hr>
<h2 id="제약조건-constraints"><strong>제약조건 (Constraints)</strong></h2>
<h3 id="1-not-null---필수-입력-설정"><strong>1. NOT NULL - 필수 입력 설정</strong></h3>
<pre><code class="language-sql">CREATE TABLE new_table (
    id INT NOT NULL,        -- NULL 값 입력 금지
    이름 VARCHAR(100) NOT NULL,
    나이 INT
);</code></pre>
<ul>
<li>해당 컬럼에 데이터를 반드시 입력해야 함</li>
<li>빈 값으로 두면 저장이 거부됨</li>
</ul>
<h3 id="2-unique---중복-값-방지"><strong>2. UNIQUE - 중복 값 방지</strong></h3>
<pre><code class="language-sql">-- 단일 컬럼에 UNIQUE 설정
CREATE TABLE new_table (
    id INT UNIQUE,          -- 모든 값이 달라야 함
    이름 VARCHAR(100),
    나이 INT
);

-- 복수 컬럼 조합으로 UNIQUE 설정
CREATE TABLE new_table (
    id INT,
    이름 VARCHAR(100),
    나이 INT,
    UNIQUE(이름, 나이)      -- 이름+나이 조합이 중복되면 안됨
);</code></pre>
<p><strong>UNIQUE 조합 예시</strong>:
| id | 이름 | 나이 |
|---|---|---|
| 1 | aaa | 20 |
| 2 | aaa | 21 |  ← 허용 (조합이 다름)
| 3 | aaa | 20 |  ← 거부 (조합이 중복)</p>
<h3 id="3-check---값의-범위나-조건-제한"><strong>3. CHECK - 값의 범위나 조건 제한</strong></h3>
<pre><code class="language-sql">CREATE TABLE new_table (
    id INT,
    이름 VARCHAR(100),
    나이 INT CHECK (나이 &gt; 0),      -- 양수만 입력 가능
    성별 VARCHAR(10) CHECK (성별 IN (&#39;남&#39;, &#39;여&#39;))  -- 특정 값만 허용
);</code></pre>
<h3 id="4-primary-key---기본-키-설정"><strong>4. PRIMARY KEY - 기본 키 설정</strong></h3>
<pre><code class="language-sql">CREATE TABLE new_table (
    id INT PRIMARY KEY,     -- 자동으로 NOT NULL, UNIQUE 포함
    이름 VARCHAR(100),
    나이 INT
);</code></pre>
<p><strong>PRIMARY KEY의 역할</strong>:</p>
<ul>
<li>각 행을 고유하게 식별하는 역할</li>
<li>NOT NULL과 UNIQUE 제약이 자동으로 적용</li>
<li>테이블당 하나만 설정 가능</li>
</ul>
<h3 id="5-auto_increment---자동-증가"><strong>5. AUTO_INCREMENT - 자동 증가</strong></h3>
<pre><code class="language-sql">-- MySQL 방식
CREATE TABLE new_table (
    id INT AUTO_INCREMENT PRIMARY KEY,  -- 1, 2, 3... 자동 증가
    이름 VARCHAR(100),
    나이 INT
);

-- Oracle/PostgreSQL 표준 방식
CREATE TABLE new_table (
    id NUMBER GENERATED BY DEFAULT ALWAYS AS IDENTITY PRIMARY KEY,
    이름 VARCHAR(100)
);</code></pre>
<h3 id="6-제약조건-명명과-관리"><strong>6. 제약조건 명명과 관리</strong></h3>
<pre><code class="language-sql">CREATE TABLE new_table (
    id INT,
    이름 VARCHAR(100),
    나이 INT,
    CONSTRAINT pk_new_table PRIMARY KEY (id),           -- 기본키 제약
    CONSTRAINT chk_age CHECK(나이 &gt; 10),                -- 나이 체크 제약
    CONSTRAINT uk_name UNIQUE(이름)                     -- 이름 유일성 제약
);</code></pre>
<p><strong>기존 테이블에 제약조건 추가</strong>:</p>
<pre><code class="language-sql">ALTER TABLE 테이블명 MODIFY 컬럼명 INT NOT NULL;</code></pre>
<hr>
<h2 id="데이터베이스-정규화-normalization"><strong>데이터베이스 정규화 (Normalization)</strong></h2>
<h3 id="정규화의-목적"><strong>정규화의 목적</strong></h3>
<p>데이터 중복을 줄이고 무결성을 보장하며, 수정/삭제 시 발생할 수 있는 문제를 방지하기 위한 과정입니다.</p>
<hr>
<h2 id="제1정규형-1nf---원자값-보장"><strong>제1정규형 (1NF) - 원자값 보장</strong></h2>
<h3 id="문제-상황"><strong>문제 상황</strong></h3>
<p>체육센터 수강등록 시스템에서 한 회원이 여러 프로그램을 신청하는 경우:</p>
<table>
<thead>
<tr>
<th>회원번호</th>
<th>회원이름</th>
<th>프로그램</th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>강호동</td>
<td>스쿼시초급</td>
</tr>
<tr>
<td>102</td>
<td>손흥민</td>
<td>헬스</td>
</tr>
<tr>
<td>103</td>
<td>김민수</td>
<td>헬스, 골프초급</td>
</tr>
</tbody></table>
<h3 id="제1정규화-해결"><strong>제1정규화 해결</strong></h3>
<p>하나의 셀에는 하나의 값만 저장하도록 분리:</p>
<table>
<thead>
<tr>
<th>회원번호</th>
<th>회원이름</th>
<th>프로그램</th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>강호동</td>
<td>스쿼시초급</td>
</tr>
<tr>
<td>102</td>
<td>손흥민</td>
<td>헬스</td>
</tr>
<tr>
<td>103</td>
<td>김민수</td>
<td>헬스</td>
</tr>
<tr>
<td>103</td>
<td>김민수</td>
<td>골프초급</td>
</tr>
</tbody></table>
<p><strong>장점</strong>:</p>
<ul>
<li>데이터 검색, 수정, 삭제가 용이해짐</li>
<li>성능 문제 해결</li>
<li>추가 컬럼 확장이 쉬워짐</li>
</ul>
<hr>
<h2 id="제2정규형-2nf---부분-종속성-제거"><strong>제2정규형 (2NF) - 부분 종속성 제거</strong></h2>
<h3 id="문제-상황-1"><strong>문제 상황</strong></h3>
<p>복합 기본키에서 일부 컬럼에만 종속되는 속성이 있는 경우:</p>
<table>
<thead>
<tr>
<th>회원번호</th>
<th>회원이름</th>
<th>프로그램</th>
<th>가격</th>
<th>납부여부</th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>강호동</td>
<td>스쿼시초급</td>
<td>5000</td>
<td>0</td>
</tr>
<tr>
<td>102</td>
<td>손흥민</td>
<td>헬스</td>
<td>6000</td>
<td>1</td>
</tr>
<tr>
<td>103</td>
<td>김민수</td>
<td>헬스</td>
<td>6000</td>
<td>1</td>
</tr>
<tr>
<td>103</td>
<td>김민수</td>
<td>골프초급</td>
<td>8000</td>
<td>0</td>
</tr>
</tbody></table>
<p><strong>문제점</strong>: &#39;가격&#39;은 &#39;프로그램&#39;에만 의존하므로, 헬스 가격 변경 시 모든 헬스 수강생 행을 수정해야 함</p>
<h3 id="제2정규화-해결"><strong>제2정규화 해결</strong></h3>
<p>부분 종속성이 있는 컬럼을 별도 테이블로 분리:</p>
<p><strong>수강등록현황 테이블</strong></p>
<table>
<thead>
<tr>
<th>회원번호</th>
<th>회원이름</th>
<th>프로그램</th>
<th>납부여부</th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>강호동</td>
<td>스쿼시초급</td>
<td>0</td>
</tr>
<tr>
<td>102</td>
<td>손흥민</td>
<td>헬스</td>
<td>1</td>
</tr>
<tr>
<td>103</td>
<td>김민수</td>
<td>헬스</td>
<td>1</td>
</tr>
<tr>
<td>103</td>
<td>김민수</td>
<td>골프초급</td>
<td>0</td>
</tr>
</tbody></table>
<p><strong>프로그램 테이블</strong></p>
<table>
<thead>
<tr>
<th>프로그램</th>
<th>가격</th>
</tr>
</thead>
<tbody><tr>
<td>스쿼시초급</td>
<td>5000</td>
</tr>
<tr>
<td>헬스</td>
<td>6000</td>
</tr>
<tr>
<td>골프초급</td>
<td>8000</td>
</tr>
</tbody></table>
<h3 id="부분-종속성-판단-연습"><strong>부분 종속성 판단 연습</strong></h3>
<p><strong>문제 1</strong>: 책대여내역 테이블에서 책가격 컬럼</p>
<table>
<thead>
<tr>
<th>회원아이디</th>
<th>책이름</th>
<th>날짜</th>
<th>회원등급</th>
<th>책가격</th>
<th>반납여부</th>
</tr>
</thead>
<tbody><tr>
<td>lee</td>
<td>디자인책</td>
<td>1월1일</td>
<td>우수</td>
<td>1000</td>
<td>1</td>
</tr>
<tr>
<td>kim</td>
<td>만화책</td>
<td>1월2일</td>
<td>일반</td>
<td>2000</td>
<td>0</td>
</tr>
</tbody></table>
<ul>
<li><strong>복합 기본키</strong>: (회원아이디 + 책이름 + 날짜)</li>
<li><strong>책가격</strong>: &#39;책이름&#39;에만 종속 → <strong>이동 필요</strong></li>
</ul>
<p><strong>문제 2</strong>: 회원등급 컬럼</p>
<ul>
<li><strong>회원등급</strong>: &#39;회원아이디&#39;에만 종속 → <strong>이동 필요</strong></li>
</ul>
<p><strong>문제 3</strong>: 반납여부 컬럼</p>
<ul>
<li><strong>반납여부</strong>: 복합키 전체에 종속 → <strong>이동 불필요</strong></li>
</ul>
<hr>
<h2 id="제3정규형-3nf---이행적-종속성-제거"><strong>제3정규형 (3NF) - 이행적 종속성 제거</strong></h2>
<h3 id="문제-상황-2"><strong>문제 상황</strong></h3>
<p>기본키가 아닌 컬럼에 종속되는 컬럼이 있는 경우</p>
<p><strong>체육센터 프로그램 테이블</strong></p>
<table>
<thead>
<tr>
<th>프로그램</th>
<th>가격</th>
<th>강사</th>
<th>출신대학</th>
</tr>
</thead>
<tbody><tr>
<td>스쿼시</td>
<td>5000</td>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>헬스</td>
<td>6000</td>
<td>박덕팔</td>
<td>연세대</td>
</tr>
<tr>
<td>골프</td>
<td>8000</td>
<td>이상구</td>
<td>고려대</td>
</tr>
<tr>
<td>골프중급</td>
<td>9000</td>
<td>이상구</td>
<td>고려대</td>
</tr>
</tbody></table>
<p><strong>문제점</strong>: &#39;출신대학&#39;은 &#39;프로그램&#39;(기본키)이 아닌 &#39;강사&#39;에 종속됨</p>
<h3 id="제3정규화-해결"><strong>제3정규화 해결</strong></h3>
<p>이행적 종속성이 있는 컬럼을 별도 테이블로 분리:</p>
<p><strong>체육센터 프로그램 테이블</strong></p>
<table>
<thead>
<tr>
<th>프로그램</th>
<th>가격</th>
<th>강사</th>
</tr>
</thead>
<tbody><tr>
<td>스쿼시</td>
<td>5000</td>
<td>김을용</td>
</tr>
<tr>
<td>헬스</td>
<td>6000</td>
<td>박덕팔</td>
</tr>
<tr>
<td>골프</td>
<td>8000</td>
<td>이상구</td>
</tr>
<tr>
<td>골프중급</td>
<td>9000</td>
<td>이상구</td>
</tr>
</tbody></table>
<p><strong>강사정보 테이블</strong></p>
<table>
<thead>
<tr>
<th>강사</th>
<th>출신대학</th>
</tr>
</thead>
<tbody><tr>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>박덕팔</td>
<td>연세대</td>
</tr>
<tr>
<td>이상구</td>
<td>고려대</td>
</tr>
</tbody></table>
<hr>
<h2 id="foreign-key-외래키-관리"><strong>Foreign Key (외래키) 관리</strong></h2>
<h3 id="foreign-key의-개념"><strong>Foreign Key의 개념</strong></h3>
<p>다른 테이블의 기본키를 참조하는 컬럼으로, 테이블 간의 관계를 명확하게 설정합니다.</p>
<h3 id="개선된-테이블-설계"><strong>개선된 테이블 설계</strong></h3>
<p><strong>프로그램 테이블</strong> (Primary Key와 Foreign Key 적용)</p>
<table>
<thead>
<tr>
<th>id</th>
<th>프로그램</th>
<th>가격</th>
<th>강사id</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>스쿼시</td>
<td>5000</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>헬스</td>
<td>6000</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>골프</td>
<td>8000</td>
<td>3</td>
</tr>
</tbody></table>
<p><strong>강사정보 테이블</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>강사</th>
<th>출신대학</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>김을용</td>
<td>서울대</td>
</tr>
<tr>
<td>2</td>
<td>박덕팔</td>
<td>연세대</td>
</tr>
<tr>
<td>3</td>
<td>이상구</td>
<td>고려대</td>
</tr>
</tbody></table>
<h3 id="foreign-key-생성-방법"><strong>Foreign Key 생성 방법</strong></h3>
<p><strong>테이블 생성 시 설정</strong></p>
<pre><code class="language-sql">CREATE TABLE program (
    id INT PRIMARY KEY,
    프로그램 VARCHAR(100),
    가격 INT,
    강사id INT REFERENCES teacher(id)  -- Foreign Key 설정
);

-- 또는 CONSTRAINT 사용
CREATE TABLE program (
    id INT PRIMARY KEY,
    프로그램 VARCHAR(100),
    가격 INT,
    강사id INT,
    CONSTRAINT fk_teacher FOREIGN KEY (강사id) REFERENCES teacher(id)
);</code></pre>
<p><strong>기존 테이블에 추가</strong>:</p>
<pre><code class="language-sql">ALTER TABLE program ADD 
CONSTRAINT fk_teacher FOREIGN KEY (강사id) REFERENCES teacher(id);</code></pre>
<h3 id="foreign-key의-장점"><strong>Foreign Key의 장점</strong></h3>
<ol>
<li><strong>참조 무결성 보장</strong>: 존재하지 않는 강사ID 입력 방지</li>
<li><strong>삭제 방지</strong>: 참조되고 있는 강사 데이터 삭제 차단</li>
<li><strong>관계 시각화</strong>: DBeaver 등에서 테이블 관계를 화살표로 표시</li>
</ol>
<hr>
<h2 id="정규화-실습-예제"><strong>정규화 실습 예제</strong></h2>
<h3 id="쇼핑몰-구매내역-정규화"><strong>쇼핑몰 구매내역 정규화</strong></h3>
<p><strong>원본 테이블</strong></p>
<table>
<thead>
<tr>
<th>구매자</th>
<th>상품명</th>
<th>수량</th>
<th>날짜</th>
<th>회원이름</th>
<th>상품카테고리</th>
<th>가격</th>
<th>무료배송여부</th>
</tr>
</thead>
<tbody><tr>
<td>user1</td>
<td>노트북</td>
<td>1</td>
<td>2024-01-01</td>
<td>김철수</td>
<td>전자제품</td>
<td>1000000</td>
<td>Y</td>
</tr>
<tr>
<td>user2</td>
<td>티셔츠</td>
<td>2</td>
<td>2024-01-02</td>
<td>이영희</td>
<td>의류</td>
<td>50000</td>
<td>N</td>
</tr>
</tbody></table>
<p><strong>정규화 후</strong></p>
<p><strong>1. 구매내역 테이블</strong> (복합키: 구매자+상품명+수량+날짜)</p>
<table>
<thead>
<tr>
<th>구매자id</th>
<th>상품id</th>
<th>수량</th>
<th>날짜</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
<td>1</td>
<td>2024-01-01</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>2</td>
<td>2024-01-02</td>
</tr>
</tbody></table>
<p><strong>2. 회원정보 테이블</strong></p>
<table>
<thead>
<tr>
<th>구매자id</th>
<th>회원이름</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>김철수</td>
</tr>
<tr>
<td>2</td>
<td>이영희</td>
</tr>
</tbody></table>
<p><strong>3. 상품정보 테이블</strong></p>
<table>
<thead>
<tr>
<th>상품id</th>
<th>상품명</th>
<th>가격</th>
<th>카테고리id</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>노트북</td>
<td>1000000</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>티셔츠</td>
<td>50000</td>
<td>2</td>
</tr>
</tbody></table>
<p><strong>4. 카테고리정보 테이블</strong> (제3정규화)</p>
<table>
<thead>
<tr>
<th>카테고리id</th>
<th>상품카테고리</th>
<th>무료배송여부</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>전자제품</td>
<td>Y</td>
</tr>
<tr>
<td>2</td>
<td>의류</td>
<td>N</td>
</tr>
</tbody></table>
<p>이러한 정규화를 통해 데이터 중복을 제거하고, 수정 시 일관성을 보장하며, 데이터베이스의 효율성을 높일 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL&Database] 서브쿼리와 그룹화 함수]]></title>
            <link>https://velog.io/@comely_15/SQLDatabase-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC%EC%99%80-%EA%B7%B8%EB%A3%B9%ED%99%94-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@comely_15/SQLDatabase-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC%EC%99%80-%EA%B7%B8%EB%A3%B9%ED%99%94-%ED%95%A8%EC%88%98</guid>
            <pubDate>Mon, 09 Jun 2025 14:12:49 GMT</pubDate>
            <description><![CDATA[<h2 id="서브쿼리-subquery-기본-개념"><strong>서브쿼리 (Subquery) 기본 개념</strong></h2>
<h3 id="서브쿼리란"><strong>서브쿼리란?</strong></h3>
<p>SELECT 쿼리 안에 다른 SELECT 쿼리를 넣는 문법으로, 복잡한 조건을 한 번에 처리할 수 있게 해줍니다.</p>
<p><strong>기본 예제: 평균보다 높은 사용금액 조회</strong></p>
<pre><code class="language-sql">-- 1단계: 평균 구하기
SELECT AVG(사용금액) FROM card;  -- 결과: 245000

-- 2단계: 평균보다 높은 값 조회
SELECT * FROM card WHERE 사용금액 &gt; 245000;

-- 서브쿼리로 한번에 처리
SELECT * FROM card WHERE 사용금액 &gt; 
  (SELECT AVG(사용금액) FROM card);</code></pre>
<h3 id="서브쿼리-사용-규칙"><strong>서브쿼리 사용 규칙</strong></h3>
<ol>
<li><strong>괄호 필수</strong>: 서브쿼리는 반드시 <code>( )</code> 괄호로 감싸야 함</li>
<li><strong>단일 값 반환</strong>: 대부분의 경우 하나의 값만 반환해야 함</li>
<li><strong>데이터 위치</strong>: 문자나 숫자가 들어갈 곳에만 사용 가능</li>
</ol>
<h3 id="다양한-위치에서-서브쿼리-사용"><strong>다양한 위치에서 서브쿼리 사용</strong></h3>
<p><strong>WHERE 절에서 사용</strong></p>
<pre><code class="language-sql">-- 평균 사용금액보다 높은 고객 조회
SELECT * FROM card 
WHERE 사용금액 &gt; (SELECT AVG(사용금액) FROM card);</code></pre>
<p><strong>SELECT 절에서 사용</strong></p>
<pre><code class="language-sql">-- 각 고객의 사용금액과 평균 사용금액 함께 출력
SELECT 고객명, 사용금액, (SELECT AVG(사용금액) FROM card) AS 평균금액
FROM card;

-- 평균과의 차이 계산
SELECT 고객명, 사용금액, 
       사용금액 - (SELECT AVG(사용금액) FROM card) AS 평균대비차이
FROM card;</code></pre>
<p><strong>IN 절에서 사용</strong></p>
<pre><code class="language-sql">-- 블랙리스트 테이블 생성 후 해당 고객들의 사용금액 조회
SELECT 사용금액 FROM card 
WHERE 고객명 IN (SELECT 이름 FROM blacklist);</code></pre>
<h3 id="실무-활용-예제"><strong>실무 활용 예제</strong></h3>
<pre><code class="language-sql">-- 고객등급이 &#39;패밀리&#39;인 사람들의 평균 연체횟수보다 연체횟수가 높은 사람 수
SELECT COUNT(*) FROM card 
WHERE 연체횟수 &gt; (
  SELECT AVG(연체횟수) FROM card WHERE 고객등급 = &#39;패밀리&#39;
);</code></pre>
<hr>
<h2 id="group-by---그룹화를-통한-집계"><strong>GROUP BY - 그룹화를 통한 집계</strong></h2>
<h3 id="group-by-기본-문법"><strong>GROUP BY 기본 문법</strong></h3>
<p>같은 값을 가진 행들을 그룹으로 묶어서 집계 함수를 적용할 때 사용합니다.</p>
<pre><code class="language-sql">-- 고객등급별로 그룹화
SELECT 고객등급 FROM card GROUP BY 고객등급;

-- 고객등급별 고객 수 계산
SELECT 고객등급, COUNT(고객명) FROM card GROUP BY 고객등급;

-- 고객등급별 평균 사용금액
SELECT 고객등급, AVG(사용금액) FROM card GROUP BY 고객등급;</code></pre>
<h3 id="집계-함수와-group-by-조합"><strong>집계 함수와 GROUP BY 조합</strong></h3>
<pre><code class="language-sql">-- 고객등급별 다양한 통계
SELECT 고객등급,
       COUNT(*) AS 고객수,
       AVG(사용금액) AS 평균사용금액,
       MAX(사용금액) AS 최대사용금액,
       MIN(사용금액) AS 최소사용금액,
       SUM(사용금액) AS 총사용금액
FROM card 
GROUP BY 고객등급;</code></pre>
<h3 id="having---group-by-결과-필터링"><strong>HAVING - GROUP BY 결과 필터링</strong></h3>
<p>GROUP BY로 그룹화된 결과를 필터링할 때 사용합니다.</p>
<pre><code class="language-sql">-- 고객 수가 2명 이상인 등급만 조회
SELECT 고객등급, COUNT(*) AS 고객수
FROM card 
GROUP BY 고객등급 
HAVING COUNT(*) &gt;= 2;

-- VIP 등급만 필터링
SELECT 고객등급, COUNT(고객명) FROM card 
GROUP BY 고객등급 
HAVING 고객등급 = &#39;vip&#39;;</code></pre>
<h3 id="where-vs-having-차이점"><strong>WHERE vs HAVING 차이점</strong></h3>
<ul>
<li><strong>WHERE</strong>: 원본 테이블 데이터를 필터링 (GROUP BY 전에 실행)</li>
<li><strong>HAVING</strong>: GROUP BY 결과를 필터링 (GROUP BY 후에 실행)</li>
</ul>
<pre><code class="language-sql">-- WHERE와 HAVING 함께 사용
SELECT 고객등급, COUNT(고객명) FROM card 
WHERE 연체횟수 = 0           -- 원본 데이터 필터링
GROUP BY 고객등급 
HAVING 고객등급 = &#39;vip&#39;;     -- 그룹화 결과 필터링</code></pre>
<h3 id="sql-실행-순서"><strong>SQL 실행 순서</strong></h3>
<pre><code class="language-sql">SELECT 고객등급, AVG(사용금액)  -- 5. 최종 결과 선택
FROM card                      -- 1. 테이블 선택
WHERE 연체횟수 = 0            -- 2. 원본 데이터 필터링
GROUP BY 고객등급             -- 3. 그룹화
HAVING AVG(사용금액) &gt; 200000 -- 4. 그룹 결과 필터링
ORDER BY 고객등급;           -- 6. 정렬</code></pre>
<hr>
<h2 id="조건부-값-처리-if와-case"><strong>조건부 값 처리: IF와 CASE</strong></h2>
<h3 id="if-함수"><strong>IF 함수</strong></h3>
<p>두 가지 경우만 처리할 때 사용하는 간단한 조건문입니다.</p>
<pre><code class="language-sql">-- 기본 문법
IF(조건식, 조건이_참일때_값, 조건이_거짓일때_값)

-- 사용금액이 20만원 이상이면 &#39;우수&#39;, 미만이면 &#39;거지&#39;
SELECT 고객명, 사용금액, 
       IF(사용금액 &gt; 200000, &#39;우수&#39;, &#39;거지&#39;) AS 등급
FROM card;

-- 연체 여부 확인
SELECT 고객명, 연체횟수,
       IF(연체횟수 = 0, &#39;정상&#39;, &#39;연체&#39;) AS 상태
FROM card;</code></pre>
<h3 id="case-문법"><strong>CASE 문법</strong></h3>
<p>세 가지 이상의 경우를 처리할 때 사용하는 확장된 조건문입니다.</p>
<pre><code class="language-sql">-- 기본 문법
CASE 
  WHEN 조건식1 THEN 값1
  WHEN 조건식2 THEN 값2
  WHEN 조건식3 THEN 값3
  ELSE 기본값
END

-- 사용금액에 따른 등급 분류
SELECT 고객명, 사용금액,
CASE 
  WHEN 사용금액 &gt;= 300000 THEN &#39;vip&#39;
  WHEN 사용금액 &gt;= 200000 THEN &#39;로열&#39;
  WHEN 사용금액 &gt;= 100000 THEN &#39;준수&#39;
  ELSE &#39;그지&#39;
END AS 신규등급
FROM card;</code></pre>
<h3 id="집계-함수와-case-조합"><strong>집계 함수와 CASE 조합</strong></h3>
<pre><code class="language-sql">-- 등급별 점수 합계 (vip=3점, 로열=2점, 패밀리=1점)
SELECT SUM(
  CASE
    WHEN 고객등급 = &#39;vip&#39; THEN 3 
    WHEN 고객등급 = &#39;로열&#39; THEN 2 
    ELSE 1
  END
) AS 총점수
FROM card;

-- 조건별 집계
SELECT 
  SUM(CASE WHEN 사용금액 &gt;= 300000 THEN 1 ELSE 0 END) AS 고액고객수,
  SUM(CASE WHEN 연체횟수 &gt; 0 THEN 1 ELSE 0 END) AS 연체고객수
FROM card;</code></pre>
<hr>
<h2 id="실전-활용-예제"><strong>실전 활용 예제</strong></h2>
<h3 id="문제-1-장부-조작하기"><strong>문제 1: 장부 조작하기</strong></h3>
<p>사용금액 30만원 이상은 50% 증액, 미만은 10% 증액하여 총합 계산</p>
<pre><code class="language-sql">SELECT SUM(
  CASE 
    WHEN 사용금액 &gt;= 300000 THEN 사용금액 * 1.5
    ELSE 사용금액 * 1.1
  END
) AS 조작된총합
FROM card;</code></pre>
<h3 id="문제-2-등급-변동-대상자-찾기"><strong>문제 2: 등급 변동 대상자 찾기</strong></h3>
<p>현재 등급과 새로운 등급 기준이 다른 고객들만 조회</p>
<pre><code class="language-sql">SELECT 고객명, 사용금액, 고객등급,
CASE 
  WHEN 사용금액 &gt;= 300000 THEN &#39;vip&#39;
  WHEN 사용금액 &gt;= 200000 THEN &#39;로열&#39;
  ELSE &#39;패밀리&#39;
END AS 신규등급
FROM card
WHERE 고객등급 != CASE 
  WHEN 사용금액 &gt;= 300000 THEN &#39;vip&#39;
  WHEN 사용금액 &gt;= 200000 THEN &#39;로열&#39;
  ELSE &#39;패밀리&#39;
END;</code></pre>
<h3 id="문제-3-연체횟수별-고객-분포"><strong>문제 3: 연체횟수별 고객 분포</strong></h3>
<pre><code class="language-sql">-- 연체횟수별 고객 수 (1명인 경우 제외)
SELECT 연체횟수, COUNT(*) AS 고객수
FROM card 
GROUP BY 연체횟수 
HAVING COUNT(*) &gt; 1
ORDER BY 연체횟수;</code></pre>
<h3 id="문제-4-등급별-사용금액-격차"><strong>문제 4: 등급별 사용금액 격차</strong></h3>
<pre><code class="language-sql">-- 등급별 최대/최소 사용금액 비율
SELECT 고객등급, 
       MAX(사용금액) AS 최대금액,
       MIN(사용금액) AS 최소금액,
       ROUND(MAX(사용금액) / MIN(사용금액), 2) AS 격차비율
FROM card 
GROUP BY 고객등급 
ORDER BY 고객등급;</code></pre>
<hr>
<h2 id="고급-활용-팁"><strong>고급 활용 팁</strong></h2>
<h3 id="서브쿼리-최적화"><strong>서브쿼리 최적화</strong></h3>
<pre><code class="language-sql">-- 비효율적: 매번 서브쿼리 실행
SELECT *, (SELECT AVG(사용금액) FROM card) FROM card;

-- 효율적: 변수나 임시 테이블 활용 고려
-- (실제로는 대부분의 DBMS가 자동 최적화)</code></pre>
<h3 id="group-by와-order-by-조합"><strong>GROUP BY와 ORDER BY 조합</strong></h3>
<pre><code class="language-sql">-- 등급별 평균 사용금액을 높은 순으로 정렬
SELECT 고객등급, AVG(사용금액) AS 평균사용금액
FROM card 
GROUP BY 고객등급 
ORDER BY 평균사용금액 DESC;</code></pre>
<h3 id="복잡한-조건의-case-문"><strong>복잡한 조건의 CASE 문</strong></h3>
<pre><code class="language-sql">-- 여러 컬럼을 고려한 등급 산정
SELECT 고객명,
CASE 
  WHEN 사용금액 &gt;= 300000 AND 연체횟수 = 0 THEN &#39;VIP&#39;
  WHEN 사용금액 &gt;= 200000 AND 연체횟수 &lt;= 1 THEN &#39;우수&#39;
  WHEN 연체횟수 &gt; 3 THEN &#39;주의&#39;
  ELSE &#39;일반&#39;
END AS 종합등급
FROM card;</code></pre>
<p>이러한 기능들을 조합하면 복잡한 비즈니스 로직을 SQL로 구현할 수 있으며, 데이터 분석과 보고서 작성에 매우 유용합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL&Database] 검색 및 데이터 처리
]]></title>
            <link>https://velog.io/@comely_15/SQLDatabase-%EA%B2%80%EC%83%89-%EB%B0%8F-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@comely_15/SQLDatabase-%EA%B2%80%EC%83%89-%EB%B0%8F-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Mon, 09 Jun 2025 14:07:17 GMT</pubDate>
            <description><![CDATA[<h2 id="like-문법을-이용한-패턴-검색"><strong>LIKE 문법을 이용한 패턴 검색</strong></h2>
<h3 id="기본-like-문법"><strong>기본 LIKE 문법</strong></h3>
<p>특정 단어가 포함된 데이터를 검색할 때 사용하는 문법입니다.</p>
<pre><code class="language-sql">-- 정확히 일치하는 경우
SELECT * FROM product WHERE 상품명 LIKE &#39;소파&#39;;

-- 패턴 검색 (% 와일드카드 사용)
SELECT * FROM product WHERE 상품명 LIKE &#39;%소파%&#39;;</code></pre>
<h3 id="와일드카드-종류"><strong>와일드카드 종류</strong></h3>
<p><strong>% (퍼센트) - 0개 이상의 아무 문자</strong></p>
<pre><code class="language-sql">-- &#39;소파&#39;가 포함된 모든 상품
SELECT * FROM product WHERE 상품명 LIKE &#39;%소파%&#39;;

-- &#39;소파&#39;로 시작하는 상품
SELECT * FROM product WHERE 상품명 LIKE &#39;소파%&#39;;

-- &#39;소파&#39;로 끝나는 상품  
SELECT * FROM product WHERE 상품명 LIKE &#39;%소파&#39;;</code></pre>
<p><strong>_ (언더스코어) - 정확히 1개의 아무 문자</strong></p>
<pre><code class="language-sql">-- &#39;아무글자 2개 + 소파&#39;와 일치하는 상품
SELECT * FROM product WHERE 상품명 LIKE &#39;__소파&#39;;</code></pre>
<h3 id="like-사용-시-주의사항"><strong>LIKE 사용 시 주의사항</strong></h3>
<ul>
<li><strong>성능 저하</strong>: <code>%</code> 기호를 많이 사용하면 인덱스 활용이 어려워 속도가 느려질 수 있음</li>
<li><strong>CHAR 타입</strong>: 고정 길이로 공백이 자동 추가되어 예상과 다른 결과가 나올 수 있음</li>
<li>가능하면 <code>=</code>, <code>&gt;</code>, <code>&lt;</code> 등의 연산자 우선 사용 권장</li>
</ul>
<h3 id="복합-조건-검색"><strong>복합 조건 검색</strong></h3>
<pre><code class="language-sql">-- &#39;소파&#39; 또는 &#39;chair&#39;가 포함된 상품
SELECT * FROM product 
WHERE 상품명 LIKE &#39;%소파%&#39; OR 상품명 LIKE &#39;%chair%&#39;;

-- &#39;소파&#39;는 포함하지만 &#39;나무&#39;는 포함하지 않는 상품
SELECT * FROM product 
WHERE 상품명 LIKE &#39;%소파%&#39; AND NOT 상품명 LIKE &#39;%나무%&#39;;

-- &#39;Green&#39;으로 시작해서 &#39;chair&#39;로 끝나는 상품
SELECT * FROM product WHERE 상품명 LIKE &#39;Green%chair&#39;;</code></pre>
<hr>
<h2 id="집계-함수-aggregate-functions"><strong>집계 함수 (Aggregate Functions)</strong></h2>
<h3 id="기본-집계-함수"><strong>기본 집계 함수</strong></h3>
<p>숫자 데이터의 통계를 계산할 때 사용하는 함수들입니다.</p>
<pre><code class="language-sql">-- 최댓값
SELECT MAX(사용금액) FROM card;

-- 최솟값  
SELECT MIN(사용금액) FROM card;

-- 평균값
SELECT AVG(연체횟수) FROM card;

-- 합계
SELECT SUM(사용금액) FROM card;

-- 개수 (행의 수)
SELECT COUNT(사용금액) FROM card;
SELECT COUNT(*) FROM card;  -- 같은 결과</code></pre>
<h3 id="as를-이용한-컬럼명-변경"><strong>AS를 이용한 컬럼명 변경</strong></h3>
<pre><code class="language-sql">-- 컬럼명을 더 읽기 쉽게 변경
SELECT MAX(사용금액) AS 최대사용금액 FROM card;

-- 여러 집계 함수 동시 사용
SELECT MAX(결제횟수), MIN(결제횟수) FROM card;</code></pre>
<h3 id="조건부-집계"><strong>조건부 집계</strong></h3>
<p>특정 조건을 만족하는 데이터만으로 통계를 계산할 수 있습니다.</p>
<pre><code class="language-sql">-- VIP 고객의 평균 사용금액
SELECT AVG(사용금액) FROM card WHERE 고객등급 = &#39;vip&#39;;

-- VIP 고객의 평균 결제횟수와 사용금액 총합
SELECT AVG(결제횟수), SUM(사용금액) 
FROM card WHERE 고객등급 = &#39;vip&#39;;

-- 연체횟수 1회 이하인 고객 수
SELECT COUNT(연체횟수) FROM card WHERE 연체횟수 &lt;= 1;</code></pre>
<h3 id="distinct와-집계-함수"><strong>DISTINCT와 집계 함수</strong></h3>
<p>중복을 제거한 후 집계를 수행할 수 있습니다.</p>
<pre><code class="language-sql">-- 중복 제거 후 출력
SELECT DISTINCT 연체횟수 FROM card;

-- 중복 제거 후 평균 계산
SELECT AVG(DISTINCT 연체횟수) FROM card;</code></pre>
<h3 id="limit을-이용한-대안"><strong>LIMIT을 이용한 대안</strong></h3>
<p>큰 데이터셋에서 MAX/MIN 대신 정렬과 LIMIT을 사용할 수 있습니다.</p>
<pre><code class="language-sql">-- MAX() 대신 정렬 + LIMIT 사용
SELECT * FROM card ORDER BY 사용금액 DESC LIMIT 1;

-- MIN() 대신 정렬 + LIMIT 사용  
SELECT * FROM card ORDER BY 사용금액 ASC LIMIT 1;</code></pre>
<hr>
<h2 id="사칙연산과-컬럼-계산"><strong>사칙연산과 컬럼 계산</strong></h2>
<h3 id="숫자-컬럼의-사칙연산"><strong>숫자 컬럼의 사칙연산</strong></h3>
<p>컬럼 값에 직접 연산을 적용할 수 있습니다.</p>
<pre><code class="language-sql">-- 부가세 제외한 사용금액 (10% 할인)
SELECT 사용금액 * 0.9 FROM card;

-- 컬럼명 변경과 함께 사용
SELECT 사용금액 * 0.9 AS 부가세제외, 연체횟수 + 100 FROM card;</code></pre>
<p><strong>사용 가능한 연산자</strong></p>
<ul>
<li><code>+</code> : 덧셈</li>
<li><code>-</code> : 뺄셈  </li>
<li><code>*</code> : 곱셈</li>
<li><code>/</code> : 나눗셈</li>
</ul>
<h3 id="컬럼-간-연산"><strong>컬럼 간 연산</strong></h3>
<p>서로 다른 컬럼 값들을 연산할 수 있습니다.</p>
<pre><code class="language-sql">-- 결제당 평균 사용금액 계산
SELECT 사용금액 / 결제횟수 FROM card;

-- 여러 컬럼 조합 연산
SELECT 사용금액, 결제횟수, 사용금액 / 결제횟수 AS 평균결제금액 FROM card;</code></pre>
<hr>
<h2 id="문자열-처리-함수"><strong>문자열 처리 함수</strong></h2>
<h3 id="concat---문자열-합치기"><strong>CONCAT - 문자열 합치기</strong></h3>
<p>여러 문자열을 연결하여 하나로 만드는 함수입니다.</p>
<pre><code class="language-sql">-- 고객명과 등급 합치기
SELECT CONCAT(고객명, 고객등급) FROM card;

-- 중간에 텍스트 추가
SELECT CONCAT(고객명, &#39; is &#39;, 고객등급) FROM card;

-- 숫자도 문자로 변환하여 합치기
SELECT CONCAT(고객명, &#39; used &#39;, 사용금액) FROM card;</code></pre>
<p><strong>DBMS별 차이점</strong></p>
<pre><code class="language-sql">-- MySQL/MariaDB
SELECT CONCAT(고객명, &#39; is &#39;, 고객등급) FROM card;

-- PostgreSQL/Oracle  
SELECT 고객명 || &#39; is &#39; || 고객등급 FROM card;</code></pre>
<h3 id="문자열-조작-함수들"><strong>문자열 조작 함수들</strong></h3>
<p><strong>TRIM - 공백 제거</strong></p>
<pre><code class="language-sql">-- 좌우 공백 제거
SELECT TRIM(컬럼명) FROM 테이블명;</code></pre>
<p><strong>REPLACE - 문자 교체</strong></p>
<pre><code class="language-sql">-- &#39;서울&#39;을 &#39;경기&#39;로 모두 교체
SELECT REPLACE(&#39;서울에사는 서울맨&#39;, &#39;서울&#39;, &#39;경기&#39;);

-- 모든 공백 제거
SELECT REPLACE(컬럼명, &#39; &#39;, &#39;&#39;) FROM 테이블명;</code></pre>
<p><strong>SUBSTR - 문자 추출</strong></p>
<pre><code class="language-sql">-- 3번째부터 2글자 추출
SELECT SUBSTR(&#39;abcdef&#39;, 3, 2);  -- 결과: &#39;cd&#39;

-- 휴대폰 번호 뒷자리 4글자
SELECT SUBSTR(번호, 10, 4) FROM 테이블명;</code></pre>
<p><strong>RIGHT/LEFT - 좌우에서 문자 추출</strong></p>
<pre><code class="language-sql">-- 오른쪽에서 4글자 추출
SELECT RIGHT(번호, 4) FROM 테이블명;</code></pre>
<p><strong>INSERT - 문자 일부 교체</strong></p>
<pre><code class="language-sql">-- 1번째부터 4글자를 &#39;hello&#39;로 교체
SELECT INSERT(&#39;test@naver.com&#39;, 1, 4, &#39;hello&#39;);  -- 결과: &#39;hello@naver.com&#39;</code></pre>
<hr>
<h2 id="수학-함수"><strong>수학 함수</strong></h2>
<h3 id="최댓값최솟값-비교"><strong>최댓값/최솟값 비교</strong></h3>
<pre><code class="language-sql">-- 여러 값 중 최댓값
SELECT GREATEST(5, 3, 2, 1, 4);  -- 결과: 5

-- 여러 값 중 최솟값  
SELECT LEAST(5, 3, 2, 1, 4);     -- 결과: 1</code></pre>
<h3 id="소수점-처리"><strong>소수점 처리</strong></h3>
<pre><code class="language-sql">-- 내림 (소수점 버림)
SELECT FLOOR(10.1);  -- 결과: 10
SELECT FLOOR(10.9);  -- 결과: 10

-- 올림
SELECT CEIL(10.1);   -- 결과: 11
SELECT CEIL(10.9);   -- 결과: 11

-- 반올림 (지정 자릿수까지)
SELECT ROUND(10.777, 2);    -- 결과: 10.78

-- 내림 (지정 자릿수까지)  
SELECT TRUNCATE(10.777, 2); -- 결과: 10.77</code></pre>
<h3 id="기타-수학-함수"><strong>기타 수학 함수</strong></h3>
<pre><code class="language-sql">-- 거듭제곱
SELECT POWER(4, 2);  -- 결과: 16 (4의 2승)

-- 절댓값
SELECT ABS(-100);    -- 결과: 100</code></pre>
<hr>
<h2 id="실전-활용-예제"><strong>실전 활용 예제</strong></h2>
<h3 id="복합-쿼리-예제"><strong>복합 쿼리 예제</strong></h3>
<pre><code class="language-sql">-- VIP 고객의 할인된 평균 사용금액
SELECT AVG(사용금액 * 0.9) AS VIP할인평균금액 
FROM card 
WHERE 고객등급 = &#39;vip&#39;;

-- 고객 정보를 보기 좋게 포맷팅
SELECT CONCAT(고객명, &#39; (&#39;, 고객등급, &#39;) - &#39;, 사용금액, &#39;원&#39;) AS 고객정보
FROM card;

-- 사용금액이 높은 상위 5명
SELECT * FROM card 
ORDER BY 사용금액 DESC 
LIMIT 5;</code></pre>
<h3 id="데이터-정제-예제"><strong>데이터 정제 예제</strong></h3>
<pre><code class="language-sql">-- 연체 없는 고객들의 통계
SELECT 
    COUNT(*) AS 고객수,
    AVG(사용금액) AS 평균사용금액,
    SUM(사용금액) AS 총사용금액
FROM card 
WHERE 연체횟수 = 0;

-- 전화번호 마스킹 처리
SELECT 
    고객명,
    CONCAT(SUBSTR(번호, 1, 3), &#39;-****-&#39;, RIGHT(번호, 4)) AS 마스킹번호
FROM card;</code></pre>
<p><strong>함수 사용 시 팁</strong></p>
<ol>
<li><strong>성능 고려</strong>: 함수 사용 시 인덱스 활용이 어려울 수 있으므로 대용량 데이터에서는 주의</li>
<li><strong>DBMS별 차이</strong>: MySQL, PostgreSQL, Oracle 등에서 함수명이나 문법이 다를 수 있음</li>
<li><strong>실무 활용</strong>: 모든 함수를 외우지 말고 필요할 때 검색하여 사용하는 것이 효율적</li>
<li><strong>데이터 검증</strong>: 함수 적용 전후 결과를 반드시 확인하여 의도한 대로 작동하는지 검증</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL&Database] MySQL 설치 및 기본 사용법]]></title>
            <link>https://velog.io/@comely_15/SQLDatabase-MySQL-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@comely_15/SQLDatabase-MySQL-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Mon, 09 Jun 2025 14:03:11 GMT</pubDate>
            <description><![CDATA[<h2 id="mysql-설치하기"><strong>MySQL 설치하기</strong></h2>
<h3 id="macos-설치"><strong>macOS 설치</strong></h3>
<p><strong>방법 1: 설치 프로그램 사용 (권장)</strong></p>
<ol>
<li><a href="https://dev.mysql.com/downloads/mysql/">https://dev.mysql.com/downloads/mysql/</a> 접속</li>
<li>MySQL Server Community Edition macOS 버전 다운로드<ul>
<li><strong>M1 이상 프로세서</strong>: ARM 버전 선택</li>
<li><strong>Intel 프로세서</strong>: x86 버전 선택</li>
</ul>
</li>
<li>설치 과정에서 <strong>MySQL root 계정 비밀번호</strong> 설정 (반드시 기억!)</li>
</ol>
<p><strong>방법 2: Homebrew 사용</strong></p>
<pre><code class="language-bash">brew install mysql</code></pre>
<h3 id="환경-변수-설정-macos"><strong>환경 변수 설정 (macOS)</strong></h3>
<p><strong>1. 터미널 종류 확인 후 설정 파일 열기</strong></p>
<pre><code class="language-bash"># zsh 사용 시
open ~/.zshrc

# bash 사용 시  
open ~/.bash_profile</code></pre>
<p><strong>2. PATH 추가</strong></p>
<pre><code class="language-bash">export PATH=&quot;$PATH:/usr/local/mysql/bin/&quot;</code></pre>
<ul>
<li>MySQL 설치 경로가 다를 경우 해당 경로로 수정</li>
</ul>
<p><strong>3. 터미널 재시작 후 MySQL 접속</strong></p>
<pre><code class="language-bash">mysql -u root -p</code></pre>
<ul>
<li>root 계정 비밀번호 입력하여 MySQL 서버 실행</li>
</ul>
<hr>
<h2 id="dbeaver-설치-및-연결"><strong>DBeaver 설치 및 연결</strong></h2>
<h3 id="dbeaver-설치"><strong>DBeaver 설치</strong></h3>
<ol>
<li>구글에서 &quot;DBeaver&quot; 검색하여 공식 사이트 접속</li>
<li>운영체제에 맞는 버전 다운로드<ul>
<li><strong>M1 Mac</strong>: Apple Silicon 버전</li>
<li><strong>Intel Mac/Windows</strong>: 해당 버전</li>
</ul>
</li>
</ol>
<h3 id="mysql-연결-설정"><strong>MySQL 연결 설정</strong></h3>
<ol>
<li>DBeaver 실행 후 <strong>&quot;새 데이터베이스 연결&quot;</strong> 클릭</li>
<li><strong>MySQL</strong> 선택</li>
<li>연결 정보 입력:<ul>
<li><strong>Host</strong>: localhost (로컬 설치 시)</li>
<li><strong>Username</strong>: root</li>
<li><strong>Password</strong>: MySQL 설치 시 설정한 비밀번호</li>
</ul>
</li>
<li><strong>&quot;연결 테스트&quot;</strong> 후 연결 완료</li>
</ol>
<hr>
<h2 id="자주-발생하는-문제-해결"><strong>자주 발생하는 문제 해결</strong></h2>
<h3 id="windows-환경"><strong>Windows 환경</strong></h3>
<p><strong>MySQL Workbench Local Instance 시작 실패</strong></p>
<ol>
<li>윈도우 검색 → &quot;환경 변수 편집&quot;</li>
<li>Path 변수에 <code>C:\Windows\System32</code> 추가</li>
</ol>
<p><strong>DBeaver MySQL 연결 실패</strong></p>
<ol>
<li>윈도우 검색 → &quot;서비스&quot;</li>
<li>MYSQL80 서비스 찾아서 우클릭 → 시작</li>
<li>MySQL Workbench에서도 서버 실행 상태 확인</li>
</ol>
<h3 id="macos-환경"><strong>macOS 환경</strong></h3>
<pre><code class="language-bash"># 터미널에서 MySQL 서버 실행 여부 확인
mysql -u root -p</code></pre>
<hr>
<h2 id="database와-table-생성"><strong>Database와 Table 생성</strong></h2>
<h3 id="database-생성"><strong>Database 생성</strong></h3>
<pre><code class="language-sql">-- DBeaver에서 Databases 우클릭 → Create New Database
-- 또는 SQL로 생성
CREATE DATABASE mart;</code></pre>
<h3 id="table-생성과-column-설정"><strong>Table 생성과 Column 설정</strong></h3>
<pre><code class="language-sql">-- product 테이블 생성 예시
CREATE TABLE product (
    번호 INT,
    상품명 VARCHAR(100),
    카테고리 VARCHAR(50),
    가격 INT
);</code></pre>
<p><strong>Column 설정 시 고려사항</strong></p>
<ul>
<li><strong>Data Type</strong>: 저장할 데이터의 종류에 맞게 선택</li>
<li><strong>용량</strong>: 예상되는 최대 크기보다 여유있게 설정</li>
<li><strong>제약조건</strong>: NULL 허용 여부, 기본값 등</li>
</ul>
<hr>
<h2 id="mysql-data-types"><strong>MySQL Data Types</strong></h2>
<h3 id="문자형-데이터"><strong>문자형 데이터</strong></h3>
<table>
<thead>
<tr>
<th>Data Type</th>
<th>저장 가능한 양</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>CHAR</strong></td>
<td>0~255자</td>
<td>고정 길이, CHAR(10)은 항상 10자 용량 차지</td>
</tr>
<tr>
<td><strong>VARCHAR</strong></td>
<td>0~65,535자</td>
<td>가변 길이, 실제 데이터 크기만큼 용량 차지</td>
</tr>
<tr>
<td><strong>TEXT</strong></td>
<td>0~65,535자</td>
<td>긴 텍스트용</td>
</tr>
<tr>
<td><strong>MEDIUMTEXT</strong></td>
<td>0~1,600만자</td>
<td>매우 긴 텍스트용 (블로그 글 등)</td>
</tr>
</tbody></table>
<p><strong>사용 팁</strong></p>
<ul>
<li>일반적인 문자 저장: <code>VARCHAR</code> 사용</li>
<li>긴 글 저장: <code>MEDIUMTEXT</code> 사용</li>
<li><code>VARCHAR(300)</code> 설정 후 10자만 저장해도 10자+1byte만 차지</li>
</ul>
<h3 id="숫자형-데이터"><strong>숫자형 데이터</strong></h3>
<table>
<thead>
<tr>
<th>Data Type</th>
<th>저장 범위</th>
<th>용량</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SMALLINT</strong></td>
<td>-32,768 ~ 32,767</td>
<td>2byte</td>
<td>작은 숫자용</td>
</tr>
<tr>
<td><strong>INT</strong></td>
<td>-21억 ~ 21억</td>
<td>4byte</td>
<td>가장 많이 사용</td>
</tr>
<tr>
<td><strong>BIGINT</strong></td>
<td>-900경 ~ 900경</td>
<td>8byte</td>
<td>매우 큰 숫자용</td>
</tr>
<tr>
<td><strong>FLOAT</strong></td>
<td>-10^38 ~ 10^38</td>
<td>4byte</td>
<td>소수점 7자리, 약간의 오차</td>
</tr>
<tr>
<td><strong>DOUBLE</strong></td>
<td>-10^308 ~ 10^308</td>
<td>8byte</td>
<td>소수점 14자리, 약간의 오차</td>
</tr>
<tr>
<td><strong>DECIMAL</strong></td>
<td>최대 65자리</td>
<td>가변</td>
<td>소수점 30자리, 오차 없음</td>
</tr>
</tbody></table>
<p><strong>사용 팁</strong></p>
<ul>
<li>일반적인 정수: <code>INT</code> 사용</li>
<li>양수만 저장: <code>UNSIGNED</code> 옵션 추가 (범위가 2배로 확장)</li>
<li>정확한 소수점 계산: <code>DECIMAL</code> 사용 (금융 데이터 등)</li>
</ul>
<h3 id="날짜시간형-데이터"><strong>날짜/시간형 데이터</strong></h3>
<table>
<thead>
<tr>
<th>Data Type</th>
<th>저장 범위</th>
<th>형식</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DATE</strong></td>
<td>1000년~9999년</td>
<td>YYYY-MM-DD</td>
<td>날짜만 저장</td>
</tr>
<tr>
<td><strong>TIME</strong></td>
<td>-839~+838시간</td>
<td>HH:MM:SS</td>
<td>시간의 양 저장</td>
</tr>
<tr>
<td><strong>DATETIME</strong></td>
<td>1000년~9999년</td>
<td>YYYY-MM-DD HH:MM:SS</td>
<td>날짜와 시간 모두 저장</td>
</tr>
<tr>
<td><strong>TIMESTAMP</strong></td>
<td>1970년~2038년</td>
<td>YYYY-MM-DD HH:MM:SS</td>
<td>2038년 문제로 권장하지 않음</td>
</tr>
</tbody></table>
<p><strong>사용 팁</strong></p>
<ul>
<li>일반적인 날짜/시간 저장: <code>DATETIME</code> 사용</li>
<li>현재 시간 자동 기록: <code>TIMESTAMP</code> 간혹 사용</li>
</ul>
<hr>
<h2 id="기본-sql-문법"><strong>기본 SQL 문법</strong></h2>
<h3 id="select-문법-데이터-조회"><strong>SELECT 문법 (데이터 조회)</strong></h3>
<p><strong>기본 조회</strong></p>
<pre><code class="language-sql">-- 모든 컬럼 조회
SELECT * FROM 테이블명;

-- 특정 컬럼만 조회
SELECT 컬럼명 FROM 테이블명;

-- 여러 컬럼 조회
SELECT 컬럼명1, 컬럼명2 FROM 테이블명;

-- 다른 데이터베이스의 테이블 조회
SELECT * FROM 데이터베이스명.테이블명;</code></pre>
<h3 id="order-by-정렬"><strong>ORDER BY (정렬)</strong></h3>
<p><strong>기본 정렬</strong></p>
<pre><code class="language-sql">-- 오름차순 정렬 (ASC는 생략 가능)
SELECT * FROM product ORDER BY 가격 ASC;

-- 내림차순 정렬
SELECT * FROM product ORDER BY 가격 DESC;

-- 여러 컬럼 기준 정렬
SELECT * FROM product ORDER BY 카테고리 ASC, 가격 DESC;

-- 컬럼 번호로 정렬 (3번째 컬럼 기준)
SELECT * FROM product ORDER BY 3 DESC;</code></pre>
<p><strong>정렬 순서</strong></p>
<ul>
<li><strong>ASC (오름차순)</strong>: A→Z, 1→9</li>
<li><strong>DESC (내림차순)</strong>: Z→A, 9→1</li>
</ul>
<h3 id="where-조건-필터링"><strong>WHERE (조건 필터링)</strong></h3>
<p><strong>기본 조건</strong></p>
<pre><code class="language-sql">-- 문자 조건 (따옴표 필수)
SELECT * FROM product WHERE 카테고리 = &#39;가구&#39;;

-- 숫자 조건 (따옴표 불필요)
SELECT * FROM product WHERE 가격 &gt; 50000;

-- 범위 조건
SELECT * FROM product WHERE 가격 BETWEEN 5000 AND 8000;</code></pre>
<p><strong>비교 연산자</strong></p>
<ul>
<li><code>=</code> : 같음</li>
<li><code>!=</code> : 같지 않음  </li>
<li><code>&gt;</code>, <code>&lt;</code> : 크다, 작다</li>
<li><code>&gt;=</code>, <code>&lt;=</code> : 크거나 같다, 작거나 같다</li>
</ul>
<h3 id="복합-조건-and-or-not"><strong>복합 조건 (AND, OR, NOT)</strong></h3>
<p><strong>AND 조건 (모든 조건 만족)</strong></p>
<pre><code class="language-sql">SELECT * FROM product 
WHERE 카테고리 = &#39;가구&#39; AND 가격 = 5000;</code></pre>
<p><strong>OR 조건 (하나 이상 조건 만족)</strong></p>
<pre><code class="language-sql">SELECT * FROM product 
WHERE 카테고리 = &#39;가구&#39; OR 가격 = 5000;</code></pre>
<p><strong>NOT 조건 (조건 제외)</strong></p>
<pre><code class="language-sql">SELECT * FROM product 
WHERE NOT 카테고리 = &#39;가구&#39;;</code></pre>
<p><strong>괄호를 이용한 조건 그룹화</strong></p>
<pre><code class="language-sql">SELECT * FROM product 
WHERE (카테고리 = &#39;가구&#39; OR 카테고리 = &#39;옷&#39;) AND 가격 = 5000;</code></pre>
<h3 id="in-문법-여러-값-중-일치"><strong>IN 문법 (여러 값 중 일치)</strong></h3>
<p><strong>OR 조건을 IN으로 간단히 표현</strong></p>
<pre><code class="language-sql">-- 기존 방식 (OR 여러 개)
SELECT * FROM product 
WHERE 카테고리 = &#39;신발&#39; OR 카테고리 = &#39;가전&#39; OR 카테고리 = &#39;식품&#39;;

-- IN 사용 (더 간단)
SELECT * FROM product 
WHERE 카테고리 IN (&#39;신발&#39;, &#39;가전&#39;, &#39;식품&#39;);

-- NOT IN (제외)
SELECT * FROM product 
WHERE 상품명 NOT IN (&#39;셔츠&#39;, &#39;반팔티&#39;, &#39;운동화&#39;);</code></pre>
<p><strong>IN 사용 조건</strong></p>
<ul>
<li>같은 컬럼에서 여러 값 중 하나와 일치하는 경우</li>
<li>다른 컬럼 간의 OR 조건에는 사용 불가</li>
</ul>
<hr>
<h2 id="실습-예제"><strong>실습 예제</strong></h2>
<h3 id="기본-조회-실습"><strong>기본 조회 실습</strong></h3>
<pre><code class="language-sql">-- 1. 재고가 20 이하인 상품을 상품명 가나다 순으로 조회
SELECT * FROM product 
WHERE 재고 &lt;= 20 
ORDER BY 상품명;

-- 2. 가격이 3000원 미만이거나 6000원 초과인 상품 조회
SELECT * FROM product 
WHERE 가격 &lt; 3000 OR 가격 &gt; 6000;

-- 3. 카테고리가 &#39;옷&#39;이 아니면서 가격이 5000원인 상품 조회
SELECT * FROM product 
WHERE 카테고리 != &#39;옷&#39; AND 가격 = 5000;</code></pre>
<h3 id="csv-파일-불러오기"><strong>CSV 파일 불러오기</strong></h3>
<ol>
<li>DBeaver에서 테이블 우클릭 → <strong>&quot;데이터 가져오기&quot;</strong></li>
<li><strong>&quot;CSV에서 가져오기&quot;</strong> 선택</li>
<li>한글 데이터의 경우 인코딩을 <strong>EUC-KR</strong>로 설정</li>
<li>파일명과 동일한 테이블 생성하여 데이터 입력</li>
</ol>
<p><strong>주요 문법 정리</strong></p>
<ul>
<li><strong>SELECT FROM</strong>: 데이터 조회의 기본 (&quot;셀프&quot;로 기억)</li>
<li><strong>ORDER BY</strong>: 정렬 (ASC/DESC)</li>
<li><strong>WHERE</strong>: 조건 필터링</li>
<li><strong>AND/OR/NOT</strong>: 복합 조건</li>
<li><strong>IN</strong>: 여러 값 중 일치 조건</li>
<li><strong>BETWEEN</strong>: 범위 조건</li>
</ul>
<p>이러한 기본 문법들을 조합하면 복잡한 데이터 조회와 분석이 가능합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node.js] 실시간 채팅
]]></title>
            <link>https://velog.io/@comely_15/Node.js-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85</link>
            <guid>https://velog.io/@comely_15/Node.js-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85</guid>
            <pubDate>Mon, 09 Jun 2025 13:56:57 GMT</pubDate>
            <description><![CDATA[<h2 id="채팅-기능-동작-원리"><strong>채팅 기능 동작 원리</strong></h2>
<p><strong>기본 흐름</strong></p>
<ol>
<li>사용자가 메시지 작성 후 전송</li>
<li>서버가 메시지를 받아서 같은 채팅방의 다른 사용자들에게 전달</li>
<li>모든 사용자가 실시간으로 메시지 확인</li>
</ol>
<p><strong>Room 개념 활용</strong></p>
<ul>
<li>전체 사용자가 아닌 <strong>특정 채팅방(Room)</strong> 사용자들에게만 메시지 전송</li>
<li>채팅방별로 독립적인 대화 공간 구성</li>
</ul>
<hr>
<h2 id="채팅-기능-구현-단계"><strong>채팅 기능 구현 단계</strong></h2>
<h3 id="1-채팅방-입장-시-room-조인"><strong>1. 채팅방 입장 시 Room 조인</strong></h3>
<p><strong>클라이언트에서 서버에 Room 입장 요청</strong></p>
<pre><code class="language-html">&lt;!-- chatDetail.ejs --&gt;
&lt;script&gt;
  const socket = io()
  socket.emit(&#39;ask-join&#39;, &#39;&lt;%= result._id %&gt;&#39;)
&lt;/script&gt;</code></pre>
<ul>
<li>채팅방 상세페이지 접속 시 자동으로 해당 채팅방 Room에 입장 요청</li>
<li>채팅방의 고유 ID(<code>_id</code>)를 Room 이름으로 사용</li>
</ul>
<p><strong>서버에서 Room 조인 처리</strong></p>
<pre><code class="language-javascript">// server.js
socket.on(&#39;ask-join&#39;, async (data) =&gt; {
  socket.join(data)
})</code></pre>
<ul>
<li>클라이언트 요청을 받아 해당 Room에 사용자 추가</li>
<li>이제 같은 Room의 사용자들끼리만 메시지 송수신 가능</li>
</ul>
<p><strong>보안 강화 (선택사항)</strong></p>
<pre><code class="language-javascript">socket.on(&#39;ask-join&#39;, async (data) =&gt; {
  // 현재 로그인된 사용자 정보 확인
  let currentUser = socket.request.session.passport.user.id

  // DB에서 해당 채팅방에 권한이 있는지 확인
  let chatRoom = await db.collection(&#39;chatroom&#39;).findOne({
    _id: new ObjectId(data),
    member: new ObjectId(currentUser)
  })

  if (chatRoom) {
    socket.join(data)
  }
})</code></pre>
<hr>
<h3 id="2-메시지-전송-기능"><strong>2. 메시지 전송 기능</strong></h3>
<p><strong>클라이언트에서 메시지 전송</strong></p>
<pre><code class="language-html">&lt;script&gt;
  document.querySelector(&#39;.chat-button&#39;).addEventListener(&#39;click&#39;, function () {
    let 작성한거 = document.querySelector(&#39;.chat-input&#39;).value
    socket.emit(&#39;message-send&#39;, { 
      room: &#39;&lt;%= result._id %&gt;&#39;, 
      msg: 작성한거 
    })
  })
&lt;/script&gt;</code></pre>
<ul>
<li>전송 버튼 클릭 시 메시지 내용과 채팅방 ID를 함께 서버로 전송</li>
<li>Object 형태로 여러 데이터를 한번에 전달</li>
</ul>
<p><strong>서버에서 메시지 분배</strong></p>
<pre><code class="language-javascript">// server.js
socket.on(&#39;message-send&#39;, async (data) =&gt; {
  console.log(&#39;유저가 보낸거 : &#39;, data) // { room: ~~, msg: ~~~ }
  io.to(data.room).emit(&#39;message-broadcast&#39;, data.msg)
})</code></pre>
<ul>
<li><code>io.to(room이름)</code>: 특정 Room의 모든 사용자에게 메시지 전송</li>
<li>메시지를 보낸 사용자를 포함한 모든 Room 멤버가 수신</li>
</ul>
<hr>
<h3 id="3-메시지-수신-및-화면-표시"><strong>3. 메시지 수신 및 화면 표시</strong></h3>
<p><strong>클라이언트에서 메시지 수신 처리</strong></p>
<pre><code class="language-html">&lt;!-- chatDetail.ejs --&gt;
&lt;script&gt;
  socket.on(&#39;message-broadcast&#39;, (data) =&gt; {
    document.querySelector(&#39;.chat-screen&#39;)
      .insertAdjacentHTML(&#39;beforeend&#39;, `&lt;div class=&quot;chat-box&quot;&gt;&lt;span&gt;${data}&lt;/span&gt;&lt;/div&gt;`)
  })
&lt;/script&gt;</code></pre>
<ul>
<li><code>insertAdjacentHTML()</code>: 기존 HTML에 새로운 요소 추가</li>
<li><code>beforeend</code>: 지정된 요소의 마지막 자식으로 추가</li>
<li>실시간으로 채팅 화면에 새 메시지가 표시됨</li>
</ul>
<hr>
<h2 id="채팅-내용-영구-저장"><strong>채팅 내용 영구 저장</strong></h2>
<p><strong>문제점</strong>: 새로고침 시 채팅 내용이 모두 사라짐
<strong>해결책</strong>: 채팅 메시지를 DB에 저장하고 페이지 로드 시 불러오기</p>
<p><strong>메시지 DB 저장</strong></p>
<pre><code class="language-javascript">socket.on(&#39;message-send&#39;, async (data) =&gt; {
  // DB에 채팅 메시지 저장
  await db.collection(&#39;chatMessage&#39;).insertOne({
    parentRoom: new ObjectId(data.room),
    content: data.msg,
    who: new ObjectId(socket.request.session.passport.user.id),
    timestamp: new Date()
  })

  // 실시간으로 다른 사용자들에게 전송
  io.to(data.room).emit(&#39;message-broadcast&#39;, data.msg)
})</code></pre>
<p><strong>페이지 로드 시 기존 채팅 불러오기</strong></p>
<pre><code class="language-javascript">app.get(&#39;/chat/detail/:id&#39;, async (요청, 응답) =&gt; {
  let chatRoom = await db.collection(&#39;chatroom&#39;).findOne({
    _id: new ObjectId(요청.params.id)
  })

  let messages = await db.collection(&#39;chatMessage&#39;).find({
    parentRoom: new ObjectId(요청.params.id)
  }).toArray()

  응답.render(&#39;chatDetail.ejs&#39;, {
    result: chatRoom,
    messages: messages
  })
})</code></pre>
<hr>
<h2 id="socketio-고급-설정"><strong>Socket.io 고급 설정</strong></h2>
<p><strong>DB Adapter 사용</strong></p>
<ul>
<li>웹소켓 연결 정보가 메모리에만 저장되어 서버 재시작 시 모든 연결이 끊어짐</li>
<li>MongoDB Adapter를 사용하면 연결 정보를 DB에 안전하게 저장</li>
</ul>
<pre><code class="language-javascript">// MongoDB Adapter 설정 예시
const { MongoStore } = require(&#39;@socket.io/mongo-adapter&#39;);

io.adapter(new MongoStore({
  uri: &#39;mongodb://localhost:27017/myapp&#39;
}));</code></pre>
<hr>
<h2 id="server-sent-events-sse-실시간-업데이트"><strong>Server Sent Events (SSE) 실시간 업데이트</strong></h2>
<h3 id="sse-기본-개념"><strong>SSE 기본 개념</strong></h3>
<ul>
<li><strong>기존 HTTP</strong>: 1회 요청 → 1회 응답 → 연결 종료</li>
<li><strong>SSE</strong>: 연결을 유지하며 서버에서 클라이언트로 지속적인 데이터 전송</li>
</ul>
<h3 id="sse-서버-설정"><strong>SSE 서버 설정</strong></h3>
<pre><code class="language-javascript">app.get(&#39;/stream/list&#39;, (요청, 응답) =&gt; {
  응답.writeHead(200, {
    &quot;Connection&quot;: &quot;keep-alive&quot;,
    &quot;Content-Type&quot;: &quot;text/event-stream&quot;,
    &quot;Cache-Control&quot;: &quot;no-cache&quot;,
  });

  응답.write(&#39;event: msg\n&#39;);
  응답.write(&#39;data: 바보\n\n&#39;);
});</code></pre>
<p><strong>주요 헤더 설명</strong></p>
<ul>
<li><code>Connection: keep-alive</code>: 연결 지속 유지</li>
<li><code>Content-Type: text/event-stream</code>: SSE 형식임을 명시</li>
<li><code>Cache-Control: no-cache</code>: 캐싱 방지</li>
</ul>
<h3 id="클라이언트-sse-연결"><strong>클라이언트 SSE 연결</strong></h3>
<pre><code class="language-html">&lt;script&gt;
  let eventSource = new EventSource(&#39;/stream/list&#39;)
  eventSource.addEventListener(&#39;msg&#39;, function (e){
    console.log(e.data);
  });
&lt;/script&gt;</code></pre>
<hr>
<h2 id="mongodb-change-stream-활용"><strong>MongoDB Change Stream 활용</strong></h2>
<h3 id="change-stream-기본-설정"><strong>Change Stream 기본 설정</strong></h3>
<pre><code class="language-javascript">let 찾을문서 = [
  { $match: { operationType: &#39;insert&#39; } }
]

const changeStream = db.collection(&#39;post&#39;).watch(찾을문서)

changeStream.on(&#39;change&#39;, (result) =&gt; {
  console.log(result)
  console.log(&#39;새 글:&#39;, result.fullDocument)
});</code></pre>
<p><strong>주요 기능</strong></p>
<ul>
<li><code>operationType</code>: insert, update, delete 등 작업 유형 필터링</li>
<li><code>result.fullDocument</code>: 변경된 문서의 전체 내용</li>
<li>실시간으로 DB 변화 감지 및 처리</li>
</ul>
<h3 id="실시간-게시물-업데이트"><strong>실시간 게시물 업데이트</strong></h3>
<pre><code class="language-javascript">app.get(&#39;/stream/post&#39;, (요청, 응답) =&gt; {
  응답.writeHead(200, {
    &quot;Connection&quot;: &quot;keep-alive&quot;,
    &quot;Content-Type&quot;: &quot;text/event-stream&quot;,
    &quot;Cache-Control&quot;: &quot;no-cache&quot;,
  })

  const 찾을문서 = [
    { $match: { operationType: &#39;insert&#39; } }
  ]

  let changeStream = db.collection(&#39;post&#39;).watch(찾을문서)
  changeStream.on(&#39;change&#39;, (result) =&gt; {
    응답.write(&#39;event: msg\n&#39;)
    응답.write(`data: ${JSON.stringify(result.fullDocument)}\n\n`)
  })
});</code></pre>
<h3 id="클라이언트에서-실시간-업데이트-처리"><strong>클라이언트에서 실시간 업데이트 처리</strong></h3>
<pre><code class="language-html">&lt;script&gt;
  let eventSource = new EventSource(&#39;/stream/post&#39;)
  eventSource.addEventListener(&#39;msg&#39;, function (e){
    let 가져온거 = JSON.parse(e.data)
    document.querySelector(&#39;.white-bg&#39;)
      .insertAdjacentHTML(&#39;afterbegin&#39;, 
        `&lt;div class=&quot;list-box&quot;&gt;&lt;h4&gt;${가져온거.title}&lt;/h4&gt;&lt;/div&gt;`
      )
  })
&lt;/script&gt;</code></pre>
<h3 id="성능-최적화"><strong>성능 최적화</strong></h3>
<pre><code class="language-javascript">let changeStream

connectDB.then((client) =&gt; {
  db = client.db(&#39;forum&#39;)

  // Change Stream을 한 번만 생성하여 성능 향상
  changeStream = db.collection(&#39;post&#39;).watch([
    { $match: { operationType: &#39;insert&#39; } }
  ])

  server.listen(process.env.PORT, () =&gt; {
    console.log(&#39;서버 실행중&#39;)
  })
})</code></pre>
<hr>
<h2 id="고급-채팅-기능-구현"><strong>고급 채팅 기능 구현</strong></h2>
<h3 id="추가-구현-아이디어"><strong>추가 구현 아이디어</strong></h3>
<p><strong>1. 채팅 메시지 DB 저장 및 불러오기</strong></p>
<ul>
<li>메시지를 <code>chatMessage</code> 컬렉션에 저장</li>
<li>채팅방 입장 시 기존 메시지 불러와서 표시</li>
</ul>
<p><strong>2. 발신자별 메시지 스타일 구분</strong></p>
<pre><code class="language-javascript">// 내가 보낸 메시지 우측 정렬
if (message.who === currentUserId) {
  messageHTML = `&lt;div class=&quot;chat-box mine&quot;&gt;&lt;span&gt;${message.content}&lt;/span&gt;&lt;/div&gt;`
} else {
  messageHTML = `&lt;div class=&quot;chat-box&quot;&gt;&lt;span&gt;${message.content}&lt;/span&gt;&lt;/div&gt;`
}</code></pre>
<p><strong>3. 채팅방 접근 권한 검증</strong></p>
<ul>
<li>로그인하지 않은 사용자 접근 차단</li>
<li>채팅방 멤버가 아닌 사용자 접근 차단</li>
<li>Socket 연결 시와 페이지 접속 시 모두 검증</li>
</ul>
<p><strong>4. 실시간 타이핑 표시</strong></p>
<pre><code class="language-javascript">// 타이핑 중임을 다른 사용자에게 알림
socket.emit(&#39;typing&#39;, { room: roomId, user: username })
socket.broadcast.to(roomId).emit(&#39;user-typing&#39;, username)</code></pre>
<p>이러한 기능들을 통해 완전한 실시간 채팅 시스템을 구축할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node.js] React 연동, 댓글 기능]]></title>
            <link>https://velog.io/@comely_15/Node.js-React-%EC%97%B0%EB%8F%99-%EB%8C%93%EA%B8%80-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@comely_15/Node.js-React-%EC%97%B0%EB%8F%99-%EB%8C%93%EA%B8%80-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Mon, 09 Jun 2025 13:49:18 GMT</pubDate>
            <description><![CDATA[<h2 id="서버-기본-설정"><strong>서버 기본 설정</strong></h2>
<p><strong>Node.js 서버 생성</strong></p>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const path = require(&#39;path&#39;);
const app = express();

app.listen(8080, function () {
  console.log(&#39;listening on 8080&#39;)
});</code></pre>
<p><strong>설치 과정</strong></p>
<ol>
<li>Node.js 설치</li>
<li>작업폴더 생성 후 에디터로 오픈</li>
<li><code>server.js</code> 파일 생성</li>
<li>터미널에서 <code>npm init -y</code> 입력</li>
<li><code>npm install express</code> 입력</li>
<li><code>nodemon server.js</code> 또는 <code>node server.js</code>로 서버 실행</li>
</ol>
<hr>
<h2 id="react의-역할과-특징"><strong>React의 역할과 특징</strong></h2>
<p><strong>React가 필요한 이유</strong></p>
<ul>
<li><strong>SPA(Single Page Application)</strong>: 새로고침 없이 페이지 전환이 가능한 앱처럼 부드러운 웹사이트 제작</li>
<li><strong>기존 방식</strong>: 페이지 이동마다 전체 페이지를 새로 불러옴</li>
<li><strong>React 방식</strong>: JavaScript로 페이지 내용만 교체하여 부드러운 사용자 경험 제공</li>
</ul>
<p><strong>React 프로젝트 생성</strong></p>
<pre><code class="language-bash">npx create-react-app 프로젝트명
cd 프로젝트명
npm run start  # 개발 서버 실행
npm run build  # 배포용 파일 생성</code></pre>
<hr>
<h2 id="react와-nodejs-서버-연동"><strong>React와 Node.js 서버 연동</strong></h2>
<p><strong>폴더 구조 예시</strong></p>
<pre><code>프로젝트 폴더/
├── server.js
└── react-project/
    └── build/
        ├── index.html
        ├── static/
        └── ...</code></pre><p><strong>서버에서 React 파일 제공</strong></p>
<pre><code class="language-javascript">// server.js
app.use(express.static(path.join(__dirname, &#39;react-project/build&#39;)));

app.get(&#39;/&#39;, function (요청, 응답) {
  응답.sendFile(path.join(__dirname, &#39;/react-project/build/index.html&#39;));
});</code></pre>
<p><strong>정적 파일 제공 설정</strong></p>
<ul>
<li><code>express.static()</code>: CSS, JS, 이미지 등 정적 파일들을 자동으로 제공</li>
<li>React 빌드 폴더의 모든 파일에 접근 가능하게 만드는 설정</li>
</ul>
<hr>
<h2 id="라우팅-처리"><strong>라우팅 처리</strong></h2>
<p><strong>React 라우터 사용 시 주의사항</strong></p>
<ul>
<li>브라우저 URL 직접 입력 시 서버로 요청이 전달됨</li>
<li>React 라우터가 처리하지 못하는 상황 발생</li>
</ul>
<p><strong>React 라우팅 전권 위임</strong></p>
<pre><code class="language-javascript">// 모든 경로를 React가 처리하도록 설정 (가장 하단에 배치)
app.get(&#39;*&#39;, function (요청, 응답) {
  응답.sendFile(path.join(__dirname, &#39;/react-project/build/index.html&#39;));
});</code></pre>
<hr>
<h2 id="데이터-통신-방식"><strong>데이터 통신 방식</strong></h2>
<p><strong>Server-side Rendering vs Client-side Rendering</strong></p>
<p><strong>Server-side Rendering</strong></p>
<ol>
<li>서버에서 DB 데이터 조회</li>
<li>HTML에 데이터 삽입</li>
<li>완성된 HTML을 클라이언트에 전송</li>
</ol>
<p><strong>Client-side Rendering (React 방식)</strong></p>
<ol>
<li>React에서 서버에 AJAX 요청으로 데이터 요청</li>
<li>서버가 JSON 형태로 데이터만 전송</li>
<li>React가 받은 데이터로 동적으로 HTML 생성</li>
</ol>
<p><strong>React-서버 통신 설정</strong></p>
<pre><code class="language-javascript">// server.js 상단에 추가
app.use(express.json());
var cors = require(&#39;cors&#39;);
app.use(cors());</code></pre>
<p><strong>설치 필요</strong></p>
<pre><code class="language-bash">npm install cors</code></pre>
<ul>
<li><code>express.json()</code>: 클라이언트가 보낸 JSON 데이터 파싱</li>
<li><code>cors</code>: 다른 도메인 간의 AJAX 요청 허용</li>
</ul>
<hr>
<h2 id="개발-환경과-배포"><strong>개발 환경과 배포</strong></h2>
<p><strong>개발 시 권장 방식</strong></p>
<ul>
<li>React: <code>npm run start</code>로 별도 서버 실행</li>
<li>Node.js: <code>nodemon server.js</code>로 별도 서버 실행</li>
<li>각각 다른 포트에서 동시 실행</li>
</ul>
<p><strong>React에서 서버 통신</strong></p>
<pre><code class="language-javascript">// 전체 URL 명시
fetch(&#39;http://localhost:8080/api/data&#39;)

// 또는 package.json에 proxy 설정
&quot;proxy&quot;: &quot;http://localhost:8080&quot;</code></pre>
<p><strong>배포 시</strong></p>
<ul>
<li><code>npm run build</code>로 최종 빌드 파일 생성</li>
<li>빌드된 파일을 서버에서 정적 파일로 제공</li>
</ul>
<hr>
<h2 id="서브디렉토리-배포"><strong>서브디렉토리 배포</strong></h2>
<p><strong>여러 앱을 다른 경로에서 제공</strong></p>
<pre><code class="language-javascript">// server.js
app.use(&#39;/&#39;, express.static(path.join(__dirname, &#39;public&#39;)))
app.use(&#39;/react&#39;, express.static(path.join(__dirname, &#39;react-project/build&#39;)))

app.get(&#39;/&#39;, function(요청, 응답){
  응답.sendFile(path.join(__dirname, &#39;public/main.html&#39;))
}) 

app.get(&#39;/react&#39;, function(요청, 응답){
  응답.sendFile(path.join(__dirname, &#39;react-project/build/index.html&#39;))
})</code></pre>
<p><strong>React 프로젝트 설정</strong></p>
<pre><code class="language-json">// react-project/package.json
{
  &quot;homepage&quot;: &quot;/react&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;
}</code></pre>
<hr>
<h2 id="회원-기능이-포함된-게시판-구현"><strong>회원 기능이 포함된 게시판 구현</strong></h2>
<p><strong>글 발행 시 작성자 정보 저장</strong></p>
<pre><code class="language-javascript">app.post(&#39;/add&#39;, upload.single(&#39;img1&#39;), async (요청, 응답) =&gt; {
  await db.collection(&#39;post&#39;).insertOne({
    title: 요청.body.title,
    content: 요청.body.content,
    user: 요청.user._id,           // 현재 로그인 유저 ID
    username: 요청.user.username   // 현재 로그인 유저명
  })
})</code></pre>
<p><strong>비정규화 vs 정규화</strong></p>
<p><strong>정규화 방식 (관계형 DB)</strong></p>
<ul>
<li>사용자 정보는 별도 테이블에 저장</li>
<li>게시글에는 사용자 ID만 저장</li>
<li>필요시 JOIN으로 데이터 결합</li>
<li><strong>장점</strong>: 데이터 정확성, <strong>단점</strong>: 조회 속도 느림</li>
</ul>
<p><strong>비정규화 방식 (MongoDB 권장)</strong></p>
<ul>
<li>게시글에 사용자명도 함께 저장</li>
<li>데이터 중복 허용</li>
<li><strong>장점</strong>: 조회 속도 빠름, <strong>단점</strong>: 데이터 불일치 가능성</li>
</ul>
<hr>
<h2 id="권한-기반-기능-구현"><strong>권한 기반 기능 구현</strong></h2>
<p><strong>본인 글만 삭제 가능</strong></p>
<pre><code class="language-javascript">app.delete(&#39;/delete&#39;, async (요청, 응답) =&gt; {
  await db.collection(&#39;post&#39;).deleteOne({
    _id: new ObjectId(요청.query.docid),
    user: 요청.user._id  // 현재 로그인 유저와 작성자가 일치하는 경우만
  })
  응답.send(&#39;삭제완료&#39;)
})</code></pre>
<p><strong>추가 구현 아이디어</strong></p>
<ol>
<li><strong>본인 글만 수정 가능</strong>: 삭제와 동일한 방식으로 조건 추가</li>
<li><strong>본인 글에만 삭제 버튼 표시</strong>: EJS에서 조건문으로 버튼 노출 제어</li>
<li><strong>삭제 성공 시에만 UI 업데이트</strong>: AJAX 응답 처리로 성공 여부 확인</li>
</ol>
<hr>
<h2 id="댓글-시스템-설계"><strong>댓글 시스템 설계</strong></h2>
<p><strong>데이터 저장 방식 비교</strong></p>
<p><strong>방식 1: 게시글 내 배열로 저장</strong></p>
<pre><code class="language-javascript">{
  title: &#39;게시글 제목&#39;,
  content: &#39;게시글 내용&#39;,
  comments: [&#39;댓글1&#39;, &#39;댓글2&#39;, &#39;댓글3&#39;]
}</code></pre>
<ul>
<li><strong>단점</strong>: 댓글 개별 수정/삭제 어려움, 16MB 용량 제한, 부분 조회 불가</li>
</ul>
<p><strong>방식 2: 별도 컬렉션으로 관리</strong></p>
<pre><code class="language-javascript">// comment 컬렉션
{
  content: &#39;댓글 내용&#39;,
  writerId: ObjectId(&#39;작성자ID&#39;),
  writer: &#39;작성자명&#39;,
  parentId: ObjectId(&#39;부모게시글ID&#39;)
}</code></pre>
<ul>
<li><strong>장점</strong>: 개별 수정/삭제 용이, 용량 제한 없음, 필요한 댓글만 조회 가능</li>
</ul>
<hr>
<h2 id="댓글-기능-구현"><strong>댓글 기능 구현</strong></h2>
<p><strong>1. 댓글 UI 생성</strong></p>
<pre><code class="language-html">&lt;!-- detail.ejs --&gt;
&lt;div class=&quot;detail-bg&quot;&gt;
  &lt;h4&gt;&lt;%= result.title %&gt;&lt;/h4&gt;
  &lt;p&gt;&lt;%= result.content %&gt;&lt;/p&gt;
  &lt;hr style=&quot;margin-top: 60px&quot;&gt;

  &lt;!-- 댓글 목록 --&gt;
  &lt;% for (let i = 0; i &lt; result2.length; i++) { %&gt;
    &lt;p&gt;&lt;strong&gt;&lt;%= result2[i].writer %&gt;&lt;/strong&gt; &lt;%= result2[i].content %&gt;&lt;/p&gt;
  &lt;% } %&gt;

  &lt;!-- 댓글 작성 폼 --&gt;
  &lt;form action=&quot;/comment&quot; method=&quot;POST&quot;&gt;
    &lt;input name=&quot;content&quot; placeholder=&quot;댓글을 입력하세요&quot;&gt;
    &lt;input name=&quot;parentId&quot; value=&quot;&lt;%= result._id %&gt;&quot; style=&quot;display: none&quot;&gt;
    &lt;button type=&quot;submit&quot;&gt;댓글작성&lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;</code></pre>
<p><strong>2. 댓글 저장 API</strong></p>
<pre><code class="language-javascript">app.post(&#39;/comment&#39;, async (요청, 응답) =&gt; {
  let result = await db.collection(&#39;comment&#39;).insertOne({
    content: 요청.body.content,
    writerId: new ObjectId(요청.user._id),
    writer: 요청.user.username,
    parentId: new ObjectId(요청.body.parentId)
  })

  응답.redirect(&#39;back&#39;)  // 이전 페이지로 이동
})</code></pre>
<p><strong>3. 댓글과 함께 상세페이지 표시</strong></p>
<pre><code class="language-javascript">app.get(&#39;/detail/:id&#39;, async (요청, 응답) =&gt; {
  let result = await db.collection(&#39;post&#39;).findOne({
    _id: new ObjectId(요청.params.id)
  })
  let result2 = await db.collection(&#39;comment&#39;).find({
    parentId: new ObjectId(요청.params.id)
  }).toArray()

  응답.render(&#39;detail.ejs&#39;, {result: result, result2: result2})
})</code></pre>
<p><strong>고급 기능 추가</strong></p>
<ul>
<li><strong>AJAX 댓글</strong>: 새로고침 없이 댓글 추가</li>
<li><strong>실시간 업데이트</strong>: WebSocket을 활용한 실시간 댓글 반영</li>
<li><strong>댓글 페이지네이션</strong>: 댓글이 많을 때 페이지 단위로 로딩</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node.js] 검색 기능2
]]></title>
            <link>https://velog.io/@comely_15/Node.js-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A52</link>
            <guid>https://velog.io/@comely_15/Node.js-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A52</guid>
            <pubDate>Mon, 09 Jun 2025 13:44:06 GMT</pubDate>
            <description><![CDATA[<h2 id="검색-기능-구현-과정"><strong>검색 기능 구현 과정</strong></h2>
<p><strong>기본 흐름</strong></p>
<ol>
<li>검색 UI 생성 → 사용자가 검색어 입력</li>
<li>서버에서 DB 검색 → 검색어가 포함된 게시물 조회</li>
<li>결과를 EJS로 전송 → 검색 결과 페이지 표시</li>
</ol>
<hr>
<h2 id="1-검색-ui-만들기"><strong>1. 검색 UI 만들기</strong></h2>
<p><strong>HTML 구조</strong></p>
<pre><code class="language-html">&lt;!-- list.ejs --&gt;
&lt;input class=&quot;search&quot;&gt;
&lt;button class=&quot;search-send&quot;&gt;검색&lt;/button&gt;</code></pre>
<p><strong>CSS 스타일링</strong></p>
<pre><code class="language-css">.search {
  margin-left: 20px;
  padding: 5px;
}
.search-send {
  padding: 6px 10px;
  background: lightgray;
  border: none;
  border-radius: 5px;
  vertical-align: middle;
}</code></pre>
<p><strong>JavaScript로 검색 요청</strong></p>
<pre><code class="language-html">&lt;!-- list.ejs 하단 --&gt;
&lt;script&gt;
  document.querySelector(&#39;.search-send&#39;).addEventListener(&#39;click&#39;, function(){
    let 입력한거 = document.querySelector(&#39;.search&#39;).value
    location.href = &#39;/search?val=&#39; + 입력한거
  })
&lt;/script&gt;</code></pre>
<ul>
<li><strong>location.href</strong>: 페이지 이동과 동시에 GET 요청 전송</li>
<li><strong>Query String</strong>: URL에 검색어를 파라미터로 전달</li>
</ul>
<hr>
<h2 id="2-서버에서-검색-처리"><strong>2. 서버에서 검색 처리</strong></h2>
<p><strong>기본 검색 (정확히 일치하는 제목)</strong></p>
<pre><code class="language-javascript">app.get(&#39;/search&#39;, async (요청, 응답) =&gt; {
  let result = await db.collection(&#39;post&#39;).find({title: 요청.query.val}).toArray()
  응답.render(&#39;search.ejs&#39;, { 글목록: result })
})</code></pre>
<p><strong>정규식을 활용한 부분 검색</strong></p>
<pre><code class="language-javascript">app.get(&#39;/search&#39;, async (요청, 응답) =&gt; {
  let result = await db.collection(&#39;post&#39;).find({
    title: { $regex: 요청.query.val }
  }).toArray()
  응답.render(&#39;search.ejs&#39;, { 글목록: result })
})</code></pre>
<ul>
<li><strong>$regex 연산자</strong>: 특정 문자를 포함하는 모든 결과 검색</li>
<li><strong>단점</strong>: 모든 문서를 하나씩 검사하므로 속도가 느림</li>
</ul>
<hr>
<h2 id="database-검색-최적화"><strong>Database 검색 최적화</strong></h2>
<p><strong>일반적인 검색 방식의 문제점</strong></p>
<ul>
<li>DB가 모든 문서를 하나씩 검사</li>
<li>문서가 많을수록 검색 속도 급격히 저하</li>
<li>1억 개 문서가 있으면 1억 개 모두 확인</li>
</ul>
<p><strong>Index의 필요성</strong></p>
<ul>
<li><strong>Binary Search</strong>: 정렬된 데이터에서 반씩 나누어 검색</li>
<li><strong>Index</strong>: 정렬된 컬렉션 복사본으로 빠른 검색 지원</li>
</ul>
<hr>
<h2 id="index-만들기와-활용"><strong>Index 만들기와 활용</strong></h2>
<p><strong>MongoDB에서 Index 생성</strong></p>
<ol>
<li>Collection → Indexes → Create Index</li>
<li>필드명과 데이터 타입 설정<ul>
<li>문자: <code>&quot;title&quot;: &quot;text&quot;</code></li>
<li>숫자: <code>&quot;title&quot;: 1</code></li>
</ul>
</li>
</ol>
<p><strong>Text Index 활용한 검색</strong></p>
<pre><code class="language-javascript">db.collection().find({
  $text: { $search: &#39;안녕&#39; }
}).toArray()</code></pre>
<p><strong>성능 확인</strong></p>
<pre><code class="language-javascript">// 성능 비교
db.collection().find({title: &#39;안녕&#39;}).explain(&#39;executionStats&#39;)
db.collection().find({$text: {$search: &#39;안녕&#39;}}).explain(&#39;executionStats&#39;)</code></pre>
<ul>
<li><strong>COLLSCAN</strong>: 전체 컬렉션 스캔 (느림)</li>
<li><strong>Index 사용</strong>: 특정 문서만 검사 (빠름)</li>
</ul>
<p><strong>Index의 단점</strong></p>
<ol>
<li><strong>용량 증가</strong>: 컬렉션을 복사하여 저장하므로 용량 2배</li>
<li><strong>수정 시 부담</strong>: 문서 추가/수정/삭제 시 Index도 함께 수정</li>
<li><strong>정확한 단어만 검색</strong>: 한국어 조사나 어미 변화 검색 불가</li>
</ol>
<hr>
<h2 id="search-index-full-text-search"><strong>Search Index (Full Text Search)</strong></h2>
<p><strong>동작 원리</strong></p>
<ol>
<li><strong>전처리</strong>: 불용어, 조사 제거 (은/는/이/가/을/를 등)</li>
<li><strong>단어 추출 및 정렬</strong>: 의미있는 단어들만 정렬하여 저장</li>
<li><strong>문서 위치 기록</strong>: 각 단어가 어떤 문서에 있는지 매핑</li>
<li><strong>점수 계산</strong>: 검색 정확도에 따른 Score 부여</li>
</ol>
<p><strong>Search Index 생성</strong></p>
<ol>
<li>Collection → Search Indexes → Create Index</li>
<li><strong>Analyzer</strong>: <code>lucene.korean</code> 선택 (한국어 불용어 제거)</li>
<li><strong>Dynamic Mapping</strong>: 끄기 (수동으로 필드 선택)</li>
<li><strong>Field Mapping</strong>: 검색할 필드 지정 (예: title)</li>
</ol>
<p><strong>서버에서 Search Index 활용</strong></p>
<pre><code class="language-javascript">app.get(&#39;/search&#39;, async (요청, 응답) =&gt; {
  let 검색조건 = [
    {
      $search: {
        index: &#39;사용할 인덱스 이름&#39;,
        text: { 
          query: 요청.query.val, 
          path: &#39;title&#39; 
        }
      }
    }
  ]
  let result = await db.collection(&#39;post&#39;).aggregate(검색조건).toArray()
  응답.render(&#39;search.ejs&#39;, { 글목록: result })
})</code></pre>
<hr>
<h2 id="고급-검색-옵션"><strong>고급 검색 옵션</strong></h2>
<p><strong>다양한 검색 조건 조합</strong></p>
<pre><code class="language-javascript">let 검색조건 = [
  {
    $search: {
      index: &#39;사용할 인덱스 이름&#39;,
      text: { query: &#39;검색어&#39;, path: &#39;검색할 필드이름&#39; }
    }
  },
  { $sort: { _id: 1 } },        // 정렬 (기본값: score 순)
  { $limit: 10 },               // 결과 개수 제한
  { $skip: 5 },                 // 앞의 5개 건너뛰기
  { $project: { 제목: 1, _id: 0 } }  // 특정 필드만 반환
]</code></pre>
<p><strong>주요 연산자 설명</strong></p>
<ul>
<li><strong>$sort</strong>: 검색 결과 정렬 (기본적으로 relevance score 순)</li>
<li><strong>$limit</strong>: 결과 개수 제한 (페이지네이션에 유용)</li>
<li><strong>$skip</strong>: 지정된 개수만큼 건너뛰기</li>
<li><strong>$project</strong>: 반환할 필드 선택 (1: 포함, 0: 제외)</li>
</ul>
<p><strong>검색 결과 페이지네이션 예시</strong></p>
<pre><code class="language-javascript">// 첫 번째 페이지: 3개씩, skip 0
{ $skip: 0 }, { $limit: 3 }

// 두 번째 페이지: 3개씩, skip 3  
{ $skip: 3 }, { $limit: 3 }

// 세 번째 페이지: 3개씩, skip 6
{ $skip: 6 }, { $limit: 3 }</code></pre>
<p><strong>Search Index의 장점</strong></p>
<ul>
<li><strong>자동 불용어 제거</strong>: 한국어 조사, 어미 자동 처리</li>
<li><strong>유연한 검색</strong>: &#39;안녕&#39; 검색 시 &#39;안녕하세요&#39;, &#39;안녕하십니까&#39; 모두 검색</li>
<li><strong>자동 점수 계산</strong>: 검색 정확도에 따른 자동 순위 매김</li>
<li><strong>빠른 성능</strong>: 대용량 데이터에서도 빠른 검색 속도</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node.js] Pagination & Session]]></title>
            <link>https://velog.io/@comely_15/Node.js-Pagination-Session</link>
            <guid>https://velog.io/@comely_15/Node.js-Pagination-Session</guid>
            <pubDate>Mon, 09 Jun 2025 11:52:43 GMT</pubDate>
            <description><![CDATA[<h2 id="pagination-페이지네이션"><strong>Pagination (페이지네이션)</strong></h2>
<p><strong>기본 개념</strong></p>
<ul>
<li>많은 데이터를 여러 페이지로 나누어 보여주는 기능</li>
<li>DB 부담과 브라우저 성능 최적화를 위해 필요</li>
</ul>
<p><strong>URL 구조 설계</strong></p>
<pre><code>/list/1  → 1~5번째 글
/list/2  → 6~10번째 글  
/list/3  → 11~15번째 글</code></pre><p><strong>기본 구현 (하드코딩)</strong></p>
<pre><code class="language-javascript">app.get(&#39;/list/1&#39;, async (요청, 응답) =&gt; {
  let result = await db.collection(&#39;post&#39;).find().skip(0).limit(5).toArray()
  응답.render(&#39;list.ejs&#39;, { 글목록 : result })
})</code></pre>
<p><strong>URL 파라미터 활용</strong></p>
<pre><code class="language-javascript">app.get(&#39;/list/:id&#39;, async (요청, 응답) =&gt; {
  let result = await db.collection(&#39;post&#39;).find()
    .skip((요청.params.id - 1) * 5)
    .limit(5)
    .toArray()
  응답.render(&#39;list.ejs&#39;, { 글목록 : result })
})</code></pre>
<p><strong>MongoDB 쿼리 메소드</strong></p>
<ul>
<li><code>.limit(5)</code> : 최대 5개 문서만 가져오기</li>
<li><code>.skip(5)</code> : 앞의 5개 문서 건너뛰기</li>
<li><code>.toArray()</code> : 결과를 배열로 변환</li>
</ul>
<p><strong>skip() 방식의 한계</strong></p>
<ul>
<li>큰 숫자(100만 이상) skip 시 성능 저하</li>
<li>문서를 하나씩 세어가며 처리하기 때문</li>
</ul>
<hr>
<h2 id="성능-최적화된-pagination"><strong>성능 최적화된 Pagination</strong></h2>
<p><strong>_id 기반 방식</strong></p>
<pre><code class="language-javascript">await db.collection(&#39;post&#39;).find({
  _id: { $gt: 마지막게시물_id }
}).limit(5).toArray()</code></pre>
<p><strong>장점과 단점</strong></p>
<ul>
<li><strong>장점</strong>: 매우 빠른 성능 (_id 기반 검색은 DB가 가장 빠르게 처리)</li>
<li><strong>단점</strong>: 순차적 탐색만 가능, 특정 페이지로 점프 불가</li>
</ul>
<p><strong>다음 버튼 구현</strong></p>
<pre><code class="language-javascript">// 서버 코드
app.get(&#39;/list/next/:id&#39;, async (요청, 응답) =&gt; {
  let result = await db.collection(&#39;post&#39;).find({
    _id: { $gt: new ObjectId(요청.params.id) }
  }).limit(5).toArray()
  응답.render(&#39;list.ejs&#39;, { 글목록 : result })
})</code></pre>
<pre><code class="language-html">&lt;!-- EJS 템플릿 --&gt;
&lt;a href=&quot;/list/next/&lt;%= 글목록[글목록.length - 1]._id %&gt;&quot;&gt;다음&lt;/a&gt;</code></pre>
<p><strong>Auto Increment 구현</strong></p>
<ul>
<li>MongoDB는 기본 auto increment 미지원</li>
<li>Counter 컬렉션 활용하여 구현</li>
<li>Trigger 기능 사용 권장</li>
</ul>
<hr>
<h2 id="session-기반-회원-인증"><strong>Session 기반 회원 인증</strong></h2>
<p><strong>기본 동작 원리</strong></p>
<p><strong>회원가입</strong></p>
<ol>
<li>유저가 아이디/비번 입력</li>
<li>서버가 DB에 저장</li>
</ol>
<p><strong>로그인</strong>  </p>
<ol>
<li>유저가 로그인 시도</li>
<li>DB의 정보와 대조 확인</li>
<li>성공 시 <strong>세션 생성</strong> 및 <strong>쿠키 발급</strong></li>
</ol>
<p><strong>인증이 필요한 기능</strong></p>
<ol>
<li>유저가 요청 시 쿠키 자동 전송</li>
<li>서버가 세션 확인 후 서비스 제공</li>
</ol>
<p><strong>입장권(세션) 저장 방식</strong></p>
<ul>
<li>브라우저 쿠키에 자동 저장</li>
<li>GET/POST 요청 시 자동으로 함께 전송</li>
</ul>
<hr>
<h2 id="session-vs-jwt-비교"><strong>Session vs JWT 비교</strong></h2>
<p><strong>Session 방식</strong></p>
<ul>
<li>DB에 세션 정보 저장</li>
<li>매 요청마다 DB 조회 필요</li>
<li><strong>장점</strong>: 엄격한 사용자 검증 가능</li>
<li><strong>단점</strong>: DB 부담 증가</li>
</ul>
<p><strong>JWT(Token) 방식</strong>  </p>
<ul>
<li>사용자 정보를 암호화하여 토큰에 저장</li>
<li>DB 조회 없이 토큰 검증</li>
<li><strong>장점</strong>: DB 부담 적음, 확장성 좋음</li>
<li><strong>단점</strong>: 토큰 탈취 시 강제 로그아웃 어려움</li>
</ul>
<hr>
<h2 id="passport-라이브러리-설정"><strong>Passport 라이브러리 설정</strong></h2>
<p><strong>설치</strong></p>
<pre><code class="language-bash">npm install express-session passport passport-local</code></pre>
<p><strong>기본 설정</strong></p>
<pre><code class="language-javascript">const session = require(&#39;express-session&#39;)
const passport = require(&#39;passport&#39;)
const LocalStrategy = require(&#39;passport-local&#39;)

app.use(passport.initialize())
app.use(session({
  secret: &#39;암호화에 쓸 비번&#39;,
  resave: false,
  saveUninitialized: false
}))
app.use(passport.session())</code></pre>
<p><strong>로그인 검증 로직</strong></p>
<pre><code class="language-javascript">passport.use(new LocalStrategy(async (입력한아이디, 입력한비번, cb) =&gt; {
  let result = await db.collection(&#39;user&#39;).findOne({ username: 입력한아이디 })
  if (!result) {
    return cb(null, false, { message: &#39;아이디 DB에 없음&#39; })
  }
  if (result.password == 입력한비번) {
    return cb(null, result)
  } else {
    return cb(null, false, { message: &#39;비번불일치&#39; })
  }
}))</code></pre>
<p><strong>로그인 API</strong></p>
<pre><code class="language-javascript">app.post(&#39;/login&#39;, async (요청, 응답, next) =&gt; {
  passport.authenticate(&#39;local&#39;, (error, user, info) =&gt; {
    if (error) return 응답.status(500).json(error)
    if (!user) return 응답.status(401).json(info.message)
    요청.logIn(user, (err) =&gt; {
      if (err) return next(err)
      응답.redirect(&#39;/&#39;)
    })
  })(요청, 응답, next)
})</code></pre>
<p><strong>세션 생성</strong></p>
<pre><code class="language-javascript">passport.serializeUser((user, done) =&gt; {
  process.nextTick(() =&gt; {
    done(null, { id: user._id, username: user.username })
  })
})</code></pre>
<p><strong>세션 검증</strong></p>
<pre><code class="language-javascript">passport.deserializeUser(async (user, done) =&gt; {
  let result = await db.collection(&#39;user&#39;).findOne({
    _id: new ObjectId(user.id)
  })
  delete result.password
  process.nextTick(() =&gt; {
    return done(null, result)
  })
})</code></pre>
<p><strong>세션 유효기간 설정</strong></p>
<pre><code class="language-javascript">app.use(session({
  secret: &#39;암호화에 쓸 비번&#39;,
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 60 * 60 * 1000 } // 1시간
}))</code></pre>
<p><strong>로그인 상태 확인</strong></p>
<ul>
<li>API 내에서 <code>요청.user</code>로 현재 로그인된 사용자 정보 확인 가능</li>
<li>브라우저 개발자도구 → Application → Cookies에서 세션 쿠키 확인</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript-09] 조건부 타입, infer, d.ts 파일, implements, keyof, Mapped Types]]></title>
            <link>https://velog.io/@comely_15/TypeScript-09-%EC%A1%B0%EA%B1%B4%EB%B6%80-%ED%83%80%EC%9E%85-infer-d.ts-%ED%8C%8C%EC%9D%BC-implements-keyof-Mapped-Types</link>
            <guid>https://velog.io/@comely_15/TypeScript-09-%EC%A1%B0%EA%B1%B4%EB%B6%80-%ED%83%80%EC%9E%85-infer-d.ts-%ED%8C%8C%EC%9D%BC-implements-keyof-Mapped-Types</guid>
            <pubDate>Fri, 06 Jun 2025 19:26:17 GMT</pubDate>
            <description><![CDATA[<h2 id="1-조건부-타입-conditional-types">1. 조건부 타입 (Conditional Types)</h2>
<h3 id="삼항연산자-기본-개념">삼항연산자 기본 개념</h3>
<p>JavaScript에서 사용하는 조건부 연산자를 TypeScript 타입에서도 활용 가능합니다.</p>
<pre><code class="language-javascript">// JavaScript 삼항연산자
조건문 ? 참일때실행할코드 : 거짓일때실행할코드
3 &gt; 1 ? console.log(&#39;맞아요&#39;) : console.log(&#39;아님&#39;)</code></pre>
<h3 id="typescript-조건부-타입">TypeScript 조건부 타입</h3>
<pre><code class="language-typescript">type Age&lt;T&gt; = T extends string ? string : unknown;

let age: Age&lt;string&gt;;   // string 타입
let age2: Age&lt;number&gt;;  // unknown 타입</code></pre>
<p><strong>핵심</strong>: <code>extends</code> 키워드로 조건을 만들고, 삼항연산자로 결과 타입을 지정합니다.</p>
<h3 id="실용적인-예제">실용적인 예제</h3>
<pre><code class="language-typescript">type FirstItem&lt;T&gt; = T extends any[] ? T[0] : any;

let age1: FirstItem&lt;string[]&gt;;  // string 타입
let age2: FirstItem&lt;number&gt;;    // any 타입</code></pre>
<h2 id="2-infer-키워드">2. infer 키워드</h2>
<h3 id="기본-문법">기본 문법</h3>
<p>조건부 타입에서 타입을 추출하여 변수로 사용할 수 있는 키워드입니다.</p>
<pre><code class="language-typescript">type Person&lt;T&gt; = T extends infer R ? R : unknown;
type 새타입 = Person&lt;string&gt;;  // string 타입</code></pre>
<h3 id="배열-타입-추출">배열 타입 추출</h3>
<pre><code class="language-typescript">type 타입추출&lt;T&gt; = T extends (infer R)[] ? R : unknown;
type NewType = 타입추출&lt;boolean[]&gt;;  // boolean 타입</code></pre>
<h3 id="함수-return-타입-추출">함수 Return 타입 추출</h3>
<pre><code class="language-typescript">type 타입추출&lt;T&gt; = T extends (() =&gt; infer R) ? R : unknown;
type NewType = 타입추출&lt;() =&gt; number&gt;;  // number 타입</code></pre>
<p><strong>참고</strong>: <code>ReturnType&lt;&gt;</code> 내장 타입으로도 함수의 return 타입 추출 가능합니다.</p>
<h2 id="3-declare-키워드와-외부-파일-연동">3. declare 키워드와 외부 파일 연동</h2>
<h3 id="declare-키워드-사용법">declare 키워드 사용법</h3>
<p>외부 JavaScript 파일의 변수를 TypeScript에서 사용할 때 타입 에러를 방지합니다.</p>
<pre><code class="language-typescript">// data.js
var a = 10;
var b = {name: &#39;kim&#39;};

// index.ts
declare let a: number;
console.log(a + 1);  // 에러 없이 사용 가능</code></pre>
<p><strong>특징</strong>: </p>
<ul>
<li><code>declare</code>가 붙은 코드는 JavaScript로 변환되지 않음</li>
<li>컴파일러에게 힌트를 주는 역할</li>
</ul>
<h3 id="ambient-module">Ambient Module</h3>
<p>TypeScript의 특별한 기능으로, import/export 없이도 같은 폴더의 타입을 전역으로 사용 가능합니다.</p>
<pre><code class="language-typescript">// data.ts
type Age = number;
let 나이: Age = 20;

// index.ts (import 없이 사용 가능)
console.log(나이 + 1);   // 가능
let 철수: Age = 30;      // 가능</code></pre>
<h3 id="global-모듈-만들기">Global 모듈 만들기</h3>
<pre><code class="language-typescript">// 로컬 모듈에서 전역 타입 선언
declare global {
  type Dog = string;
}</code></pre>
<h2 id="4-dts-파일-활용">4. d.ts 파일 활용</h2>
<h3 id="dts-파일이란">d.ts 파일이란?</h3>
<ul>
<li><strong>Definition의 약자</strong>로 타입 정의만 저장하는 파일</li>
<li>JavaScript로 컴파일되지 않음</li>
<li>타입 정의 전용 파일</li>
</ul>
<h3 id="타입-정의-파일-만들기">타입 정의 파일 만들기</h3>
<pre><code class="language-typescript">// types.d.ts
export type Age = number;
export type multiply = (x: number, y: number) =&gt; number;
export interface Person { name: string }</code></pre>
<h3 id="자동-생성-설정">자동 생성 설정</h3>
<pre><code class="language-json">// tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;declaration&quot;: true
  }
}</code></pre>
<p>이 설정으로 <code>.ts</code> 파일 저장 시 자동으로 <code>.d.ts</code> 파일이 생성됩니다.</p>
<h3 id="외부-라이브러리-타입-설치">외부 라이브러리 타입 설치</h3>
<pre><code class="language-bash">npm install --save @types/jquery</code></pre>
<h2 id="5-implements-키워드">5. implements 키워드</h2>
<h3 id="기본-사용법">기본 사용법</h3>
<p>클래스가 특정 인터페이스의 구조를 갖고 있는지 확인하는 키워드입니다.</p>
<pre><code class="language-typescript">interface CarType {
  model: string;
  price: number;
}

class Car implements CarType {
  model: string;
  price: number = 1000;
  constructor(a: string) {
    this.model = a;
  }
}</code></pre>
<h3 id="implements-vs-extends">implements vs extends</h3>
<ul>
<li><strong>implements</strong>: 구조 확인만 (타입 할당 안됨)</li>
<li><strong>extends</strong>: 상속 (속성과 메서드 복사)</li>
</ul>
<pre><code class="language-typescript">class Car implements CarType {
  model;  // any 타입 (implements는 타입 할당 안함)
  // ...
}</code></pre>
<h2 id="6-index-signatures">6. Index Signatures</h2>
<h3 id="기본-문법-1">기본 문법</h3>
<p>객체의 모든 속성에 대한 타입을 한 번에 지정하는 방법입니다.</p>
<pre><code class="language-typescript">interface StringOnly {
  [key: string]: string;
}

let obj: StringOnly = {
  name: &#39;kim&#39;,
  age: &#39;20&#39;,        // 모든 값이 string이어야 함
  location: &#39;seoul&#39;
};</code></pre>
<h3 id="다른-속성과-함께-사용">다른 속성과 함께 사용</h3>
<pre><code class="language-typescript">interface StringOnly {
  age: string;                    // 특정 속성 지정
  [key: string]: string;          // 나머지 모든 속성
}</code></pre>
<h3 id="숫자-키-사용">숫자 키 사용</h3>
<pre><code class="language-typescript">interface StringOnly {
  [key: number]: string;
}

let obj: StringOnly = {
  0: &#39;kim&#39;,
  1: &#39;20&#39;,
  2: &#39;seoul&#39;
};</code></pre>
<h3 id="recursive-index-signatures">Recursive Index Signatures</h3>
<p>중첩된 객체를 위한 재귀적 타입 정의입니다.</p>
<pre><code class="language-typescript">interface MyType {
  &#39;font-size&#39;: MyType | number;
}

let obj: MyType = {
  &#39;font-size&#39;: {
    &#39;font-size&#39;: {
      &#39;font-size&#39;: 14
    }
  }
};</code></pre>
<h2 id="7-keyof-연산자와-mapped-types">7. keyof 연산자와 Mapped Types</h2>
<h3 id="keyof-연산자">keyof 연산자</h3>
<p>객체 타입의 모든 키를 union type으로 추출합니다.</p>
<pre><code class="language-typescript">interface Person {
  age: number;
  name: string;
}

type PersonKeys = keyof Person;  // &quot;age&quot; | &quot;name&quot;</code></pre>
<h3 id="mapped-types">Mapped Types</h3>
<p>기존 타입의 모든 속성을 다른 타입으로 변환하는 타입 변환기입니다.</p>
<pre><code class="language-typescript">type Car = {
  color: boolean;
  model: boolean;
  price: boolean | number;
};

type TypeChanger&lt;MyType&gt; = {
  [key in keyof MyType]: string;
};

type 새로운타입 = TypeChanger&lt;Car&gt;;
// { color: string; model: string; price: string; }</code></pre>
<h3 id="범용-타입-변환기">범용 타입 변환기</h3>
<pre><code class="language-typescript">type TypeChanger&lt;MyType, T&gt; = {
  [key in keyof MyType]: T;
};

type NewBus = TypeChanger&lt;Bus, boolean&gt;;    // 모든 속성이 boolean
type NewBus2 = TypeChanger&lt;Bus, string[]&gt;;  // 모든 속성이 string[]</code></pre>
<h2 id="8-실무-예제">8. 실무 예제</h2>
<h3 id="숙제-1-조건부-타입">숙제 1: 조건부 타입</h3>
<pre><code class="language-typescript">type Age&lt;T&gt; = T extends [string, ...any] ? T[0] : unknown;

let age1: Age&lt;[string, number]&gt;;   // string
let age2: Age&lt;[boolean, number]&gt;;  // unknown</code></pre>
<h3 id="숙제-2-infer로-함수-파라미터-추출">숙제 2: infer로 함수 파라미터 추출</h3>
<pre><code class="language-typescript">type 타입뽑기&lt;T&gt; = T extends (x: infer R) =&gt; any ? R : any;
type a = 타입뽑기&lt;(x: number) =&gt; void&gt;;  // number</code></pre>
<h3 id="숙제-3-index-signatures-활용">숙제 3: Index Signatures 활용</h3>
<pre><code class="language-typescript">// 자동차 정보 타입
interface CarInfo {
  [key: string]: string | number;
}

// 중첩 객체 타입
interface MyType {
  &#39;font-size&#39;: number;
  [key: string]: number | MyType;
}</code></pre>
<h3 id="숙제-4-mapped-types-활용">숙제 4: Mapped Types 활용</h3>
<pre><code class="language-typescript">// Bus 타입 변환
type Bus = {
  color: string;
  model: boolean;
  price: number;
};

type TypeChanger&lt;MyType&gt; = {
  [key in keyof MyType]: string | number;
};

type NewBus = TypeChanger&lt;Bus&gt;;</code></pre>
<h2 id="정리">정리</h2>
<h3 id="조건부-타입--infer">조건부 타입 &amp; infer</h3>
<ul>
<li><strong>목적</strong>: 타입 파라미터에 따라 다른 타입 반환</li>
<li><strong>방법</strong>: <code>extends</code>와 삼항연산자, <code>infer</code>로 타입 추출</li>
</ul>
<h3 id="declare--dts">declare &amp; d.ts</h3>
<ul>
<li><strong>목적</strong>: 외부 JavaScript 파일과의 연동, 타입 정의 분리</li>
<li><strong>방법</strong>: <code>declare</code>로 타입 힌트, <code>.d.ts</code>로 타입 정의 파일</li>
</ul>
<h3 id="index-signatures--mapped-types">Index Signatures &amp; Mapped Types</h3>
<ul>
<li><strong>목적</strong>: 동적 객체 타입 정의, 타입 변환</li>
<li><strong>방법</strong>: <code>[key: type]: type</code> 문법, <code>keyof</code>와 <code>in</code> 연산자</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript-08] React + TypeScript + Redux]]></title>
            <link>https://velog.io/@comely_15/TypeScript-08-React-TypeScript-Redux</link>
            <guid>https://velog.io/@comely_15/TypeScript-08-React-TypeScript-Redux</guid>
            <pubDate>Fri, 06 Jun 2025 19:21:55 GMT</pubDate>
            <description><![CDATA[<h2 id="1-react-typescript-프로젝트-설정">1. React TypeScript 프로젝트 설정</h2>
<h3 id="새-프로젝트-생성">새 프로젝트 생성</h3>
<pre><code class="language-bash">npx create-react-app 프로젝트명 --template typescript</code></pre>
<h3 id="기존-프로젝트에-typescript-추가">기존 프로젝트에 TypeScript 추가</h3>
<pre><code class="language-bash">npm install --save typescript @types/node @types/react @types/react-dom @types/jest</code></pre>
<p><strong>핵심</strong>: 컴포넌트 파일은 <code>.tsx</code> 확장자 사용 (JSX 문법 지원)</p>
<h2 id="2-react에서-typescript-사용법">2. React에서 TypeScript 사용법</h2>
<h3 id="jsx-타입-지정">JSX 타입 지정</h3>
<pre><code class="language-typescript">let 박스: JSX.Element = &lt;div&gt;&lt;/div&gt;;
let 버튼: JSX.Element = &lt;button&gt;&lt;/button&gt;;</code></pre>
<h3 id="함수-컴포넌트-타입-지정">함수 컴포넌트 타입 지정</h3>
<pre><code class="language-typescript">type AppProps = {
  name: string;
};

function App(props: AppProps): JSX.Element {
  return &lt;div&gt;{props.name}&lt;/div&gt;;
}</code></pre>
<h3 id="props로-jsx-전달하기">Props로 JSX 전달하기</h3>
<pre><code class="language-typescript">type ContainerProps = {
  a: JSX.IntrinsicElements[&#39;h4&#39;];
};

function Container(props: ContainerProps) {
  return &lt;div&gt;{props.a}&lt;/div&gt;;
}

// 사용법
&lt;Container a={&lt;h4&gt;안녕&lt;/h4&gt;} /&gt;</code></pre>
<h3 id="state-타입-지정">State 타입 지정</h3>
<pre><code class="language-typescript">const [user, setUser] = useState&lt;string | null&gt;(&#39;kim&#39;);</code></pre>
<h3 id="type-assertion-주의사항">Type Assertion 주의사항</h3>
<pre><code class="language-typescript">// React에서는 &lt;&gt; 대신 as 사용
let code: any = 123;
let employeeCode = code as number;  // ✅ 올바름
let employeeCode2 = &lt;number&gt;code;   // ❌ React에서 금지</code></pre>
<h2 id="3-tuple-타입">3. Tuple 타입</h2>
<h3 id="기본-문법">기본 문법</h3>
<p>배열의 각 위치별로 정확한 타입 지정</p>
<pre><code class="language-typescript">let 멍멍이: [string, boolean];
멍멍이 = [&#39;dog&#39;, true];  // ✅ 올바름
멍멍이 = [true, &#39;dog&#39;];  // ❌ 순서 틀림</code></pre>
<h3 id="rest-parameter와-tuple">Rest Parameter와 Tuple</h3>
<pre><code class="language-typescript">function 함수(...x: [string, number]) {
  console.log(x);
}

함수(&#39;kim&#39;, 123);        // ✅ 가능
함수(&#39;kim&#39;, 123, 456);   // ❌ 에러</code></pre>
<h3 id="옵션-요소-뒤에서만-가능">옵션 요소 (뒤에서만 가능)</h3>
<pre><code class="language-typescript">type Num = [number, number?, number?];
let 변수1: Num = [10];
let 변수2: Num = [10, 20];
let 변수3: Num = [10, 20, 30];</code></pre>
<h3 id="spread-연산자와-tuple">Spread 연산자와 Tuple</h3>
<pre><code class="language-typescript">let arr = [1, 2, 3];
let arr2: [number, number, ...number[]] = [4, 5, ...arr];</code></pre>
<h2 id="4-redux-typescript-설정">4. Redux TypeScript 설정</h2>
<h3 id="redux를-사용하는-이유">Redux를 사용하는 이유</h3>
<ol>
<li><strong>중앙 집중식 state 관리</strong> - props 없이도 state 공유 가능</li>
<li><strong>안전한 state 수정</strong> - reducer 함수로 수정 방법을 미리 정의하여 버그 방지</li>
</ol>
<h3 id="설치">설치</h3>
<pre><code class="language-bash">npm install redux react-redux
npm install @reduxjs/toolkit  # 신규 방식 사용시</code></pre>
<h2 id="5-전통-방식-redux--typescript">5. 전통 방식 Redux + TypeScript</h2>
<h3 id="store-설정">Store 설정</h3>
<pre><code class="language-typescript">import { Provider } from &#39;react-redux&#39;;
import { createStore } from &#39;redux&#39;;

interface Counter {
  count: number;
}

const 초기값: Counter = { count: 0 };

function reducer(state = 초기값, action: {type: string}) {
  if (action.type === &#39;증가&#39;) {
    return { count: state.count + 1 };
  } else if (action.type === &#39;감소&#39;) {
    return { count: state.count - 1 };
  } else {
    return state;
  }
}

const store = createStore(reducer);

// Store 타입 export
export type RootState = ReturnType&lt;typeof store.getState&gt;;</code></pre>
<h3 id="컴포넌트에서-사용">컴포넌트에서 사용</h3>
<pre><code class="language-typescript">import { useDispatch, useSelector } from &#39;react-redux&#39;;
import { Dispatch } from &#39;redux&#39;;
import { RootState } from &#39;./index&#39;;

function App() {
  const 꺼내온거 = useSelector((state: RootState) =&gt; state);
  const dispatch: Dispatch = useDispatch();

  return (
    &lt;div&gt;
      {꺼내온거.count}
      &lt;button onClick={() =&gt; dispatch({type: &#39;증가&#39;})}&gt;
        버튼
      &lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h2 id="6-신규-방식-redux-toolkit--typescript">6. 신규 방식 Redux Toolkit + TypeScript</h2>
<h3 id="store-설정-1">Store 설정</h3>
<pre><code class="language-typescript">import { createSlice, configureStore, PayloadAction } from &#39;@reduxjs/toolkit&#39;;

const 초기값 = { count: 0, user: &#39;kim&#39; };

const counterSlice = createSlice({
  name: &#39;counter&#39;,
  initialState: 초기값,
  reducers: {
    increment(state) {
      state.count += 1;
    },
    decrement(state) {
      state.count -= 1;
    },
    incrementByAmount(state, action: PayloadAction&lt;number&gt;) {
      state.count += action.payload;
    }
  }
});

const store = configureStore({
  reducer: {
    counter1: counterSlice.reducer
  }
});

export type RootState = ReturnType&lt;typeof store.getState&gt;;
export const { increment, decrement, incrementByAmount } = counterSlice.actions;</code></pre>
<h3 id="컴포넌트에서-사용-1">컴포넌트에서 사용</h3>
<pre><code class="language-typescript">import { useDispatch, useSelector } from &#39;react-redux&#39;;
import { RootState, increment } from &#39;./index&#39;;

function App() {
  const 꺼내온거 = useSelector((state: RootState) =&gt; state);
  const dispatch = useDispatch();

  return (
    &lt;div&gt;
      {꺼내온거.counter1.count}
      &lt;button onClick={() =&gt; dispatch(increment())}&gt;
        버튼
      &lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<h2 id="7-실무-예제">7. 실무 예제</h2>
<h3 id="숙제-1-음식-정보-tuple">숙제 1: 음식 정보 Tuple</h3>
<pre><code class="language-typescript">let 음식: [string, number, boolean] = [&#39;동서녹차&#39;, 4000, true];</code></pre>
<h3 id="숙제-2-가변-길이-tuple">숙제 2: 가변 길이 Tuple</h3>
<pre><code class="language-typescript">let arr: [string, number, ...boolean[]] = [&#39;동서녹차&#39;, 4000, true, false, true];</code></pre>
<h3 id="숙제-3-rest-parameter--tuple">숙제 3: Rest Parameter + Tuple</h3>
<pre><code class="language-typescript">function 함수(...rest: [string, boolean, ...(number | string)[]]) {
  // 첫째는 문자, 둘째는 boolean, 나머지는 숫자 또는 문자
}

함수(&#39;a&#39;, true, 6, 3, &#39;1&#39;, 4);</code></pre>
<h3 id="숙제-4-문자숫자-분류기">숙제 4: 문자/숫자 분류기</h3>
<pre><code class="language-typescript">function 함수(...rest: (string | number)[]): [string[], number[]] {
  let result: [string[], number[]] = [[], []];

  rest.forEach((a) =&gt; {
    if (typeof a === &#39;string&#39;) {
      result[0].push(a);
    } else {
      result[1].push(a);
    }
  });

  return result;
}

// 사용 예시
함수(&#39;b&#39;, 5, 6, 8, &#39;a&#39;);  // [[&#39;b&#39;, &#39;a&#39;], [5, 6, 8]]</code></pre>
<h2 id="정리">정리</h2>
<h3 id="react--typescript-핵심">React + TypeScript 핵심</h3>
<ul>
<li><strong>파일 확장자</strong>: <code>.tsx</code> 사용</li>
<li><strong>JSX 타입</strong>: <code>JSX.Element</code></li>
<li><strong>Props/State</strong>: 명확한 타입 지정</li>
<li><strong>Assertion</strong>: <code>as</code> 키워드만 사용</li>
</ul>
<h3 id="redux--typescript-핵심">Redux + TypeScript 핵심</h3>
<ul>
<li><strong>Store 타입</strong>: <code>RootState</code> export로 재사용</li>
<li><strong>Action 타입</strong>: <code>PayloadAction&lt;T&gt;</code> 사용 (신규 방식)</li>
<li><strong>전통 vs 신규</strong>: 코드 길이와 복잡도 차이</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript-07] Import, Generic]]></title>
            <link>https://velog.io/@comely_15/TypeScript-07-Import-Generic</link>
            <guid>https://velog.io/@comely_15/TypeScript-07-Import-Generic</guid>
            <pubDate>Fri, 06 Jun 2025 19:19:49 GMT</pubDate>
            <description><![CDATA[<h2 id="1-importexport-기본-문법">1. Import/Export 기본 문법</h2>
<h3 id="변수와-함수-내보내기가져오기">변수와 함수 내보내기/가져오기</h3>
<p><strong>a.ts (내보내는 파일)</strong></p>
<pre><code class="language-typescript">export var 이름 = &#39;kim&#39;;
export var 나이 = 30;</code></pre>
<p><strong>b.ts (가져오는 파일)</strong></p>
<pre><code class="language-typescript">import {이름, 나이} from &#39;./a&#39;;
console.log(이름);  // &#39;kim&#39;</code></pre>
<p><strong>핵심 규칙</strong>:</p>
<ul>
<li><code>export</code>: 다른 파일에서 사용할 수 있도록 내보내기</li>
<li><code>import</code>: 다른 파일에서 내보낸 것을 가져오기</li>
<li>파일 경로는 <code>./</code>로 시작 (현재 경로)</li>
<li><code>.ts</code> 확장자는 생략</li>
</ul>
<h3 id="전체-가져오기">전체 가져오기</h3>
<pre><code class="language-typescript">import * from &#39;./a&#39;;  // a.ts의 모든 export를 가져옴
console.log(이름);
console.log(나이);</code></pre>
<h3 id="타입-내보내기가져오기">타입 내보내기/가져오기</h3>
<p><strong>a.ts</strong></p>
<pre><code class="language-typescript">export type Name = string | boolean;
export type Age = (a: number) =&gt; number;</code></pre>
<p><strong>b.ts</strong></p>
<pre><code class="language-typescript">import {Name, Age} from &#39;./a&#39;;

let 이름: Name = &#39;kim&#39;;
let 함수: Age = (a) =&gt; { return a + 10 };</code></pre>
<p><strong>로컬 타입</strong>: <code>export</code>를 붙이지 않으면 해당 파일에서만 사용 가능</p>
<h2 id="2-namespace-구버전-방식">2. Namespace (구버전 방식)</h2>
<h3 id="기본-문법">기본 문법</h3>
<p>타입스크립트 1.5 이전에 사용되던 방식. 타입명 중복을 방지</p>
<p><strong>a.ts</strong></p>
<pre><code class="language-typescript">namespace MyNamespace {
  export interface PersonInterface { age: number };
  export type NameType = number | string;
}</code></pre>
<p><strong>b.ts</strong></p>
<pre><code class="language-typescript">/// &lt;reference path=&quot;./a.ts&quot; /&gt;

let 이름: MyNamespace.NameType = &#39;민수&#39;;
let 나이: MyNamespace.PersonInterface = { age: 10 };</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li><code>/// &lt;reference path=&quot;&quot;/&gt;</code> 태그로 파일 연결</li>
<li><code>네임스페이스명.타입명</code> 형식으로 사용</li>
<li>타입명 중복 문제 해결</li>
</ul>
<hr>
<h1 id="generic-타입">Generic 타입</h1>
<h2 id="1-generic이-필요한-이유">1. Generic이 필요한 이유</h2>
<h3 id="문제-상황">문제 상황</h3>
<pre><code class="language-typescript">function 함수(x: unknown[]) {
  return x[0];  // return 타입이 unknown
}

let a = 함수([4, 2]);
console.log(a + 1);  // 에러! unknown 타입이라 연산 불가</code></pre>
<p><strong>문제점</strong>: TypeScript는 타입을 자동으로 변경해주지 않음</p>
<h2 id="2-generic-기본-문법">2. Generic 기본 문법</h2>
<h3 id="함수에-generic-적용">함수에 Generic 적용</h3>
<pre><code class="language-typescript">function 함수&lt;MyType&gt;(x: MyType[]): MyType {
  return x[0];
}

let a = 함수&lt;number&gt;([4, 2]);     // a는 number 타입
let b = 함수&lt;string&gt;([&#39;kim&#39;, &#39;park&#39;]);  // b는 string 타입</code></pre>
<p><strong>동작 원리</strong>:</p>
<ul>
<li><code>&lt;MyType&gt;</code>: 타입 파라미터 정의</li>
<li><code>함수&lt;number&gt;()</code>: 사용 시 타입 지정</li>
<li>MyType 자리에 입력한 타입이 대입됨</li>
</ul>
<h3 id="타입-추론">타입 추론</h3>
<pre><code class="language-typescript">let a = 함수([4, 2]);  // &lt;number&gt; 생략 가능, 자동으로 타입 추론</code></pre>
<h2 id="3-generic-제약-조건-constraints">3. Generic 제약 조건 (Constraints)</h2>
<h3 id="extends로-타입-제한">extends로 타입 제한</h3>
<pre><code class="language-typescript">function 함수&lt;MyType extends number&gt;(x: MyType) {
  return x - 1;  // number 타입만 허용하므로 연산 가능
}

let a = 함수&lt;number&gt;(100);  // 성공</code></pre>
<h3 id="커스텀-타입으로-제한">커스텀 타입으로 제한</h3>
<pre><code class="language-typescript">interface lengthCheck {
  length: number;
}

function 함수&lt;MyType extends lengthCheck&gt;(x: MyType) {
  return x.length;  // length 속성이 있는 타입만 허용
}

let a = 함수&lt;string&gt;(&#39;hello&#39;);    // 성공 (string에 length 있음)
let b = 함수&lt;number&gt;(1234);       // 에러 (number에 length 없음)</code></pre>
<h2 id="4-다양한-활용법">4. 다양한 활용법</h2>
<h3 id="class에서-generic">Class에서 Generic</h3>
<pre><code class="language-typescript">class Person&lt;T&gt; {
  name: T;
  constructor(a: T) {
    this.name = a;
  }
}

let a = new Person&lt;string&gt;(&#39;어쩌구&#39;);  // name이 string 타입</code></pre>
<h3 id="type에서-generic">Type에서 Generic</h3>
<pre><code class="language-typescript">type Age&lt;MyType&gt; = MyType;</code></pre>
<h2 id="5-실무-예제">5. 실무 예제</h2>
<h3 id="숙제-1-문자배열-길이-측정-함수">숙제 1: 문자/배열 길이 측정 함수</h3>
<pre><code class="language-typescript">function 함수&lt;MyType extends string | string[]&gt;(x: MyType) {
  console.log(x.length);
}

함수&lt;string&gt;(&#39;hello&#39;);           // 5 출력
함수&lt;string[]&gt;([&#39;kim&#39;, &#39;park&#39;]); // 2 출력</code></pre>
<h3 id="숙제-2-json-파싱-함수">숙제 2: JSON 파싱 함수</h3>
<pre><code class="language-typescript">interface Animal {
  name: string;
  age: number;
}

function 함수&lt;Type&gt;(x: string): Type {
  return JSON.parse(x);
}

let data = &#39;{&quot;name&quot;: &quot;dog&quot;, &quot;age&quot;: 1}&#39;;
let result = 함수&lt;Animal&gt;(data);  // Animal 타입으로 변환</code></pre>
<h3 id="숙제-3-타입-중복-해결-namespace">숙제 3: 타입 중복 해결 (Namespace)</h3>
<pre><code class="language-typescript">namespace GoodDog {
  export type Dog = string;
}

namespace BadDog {
  export interface Dog { name: string };
}

let dog1: GoodDog.Dog = &#39;bark&#39;;
let dog2: BadDog.Dog = { name: &#39;paw&#39; };</code></pre>
<h2 id="정리">정리</h2>
<h3 id="importexport">Import/Export</h3>
<ul>
<li><strong>목적</strong>: 파일 간 코드 재사용</li>
<li><strong>방법</strong>: <code>export</code>로 내보내고 <code>import</code>로 가져오기</li>
<li><strong>타입도 동일하게</strong> 적용 가능</li>
</ul>
<h3 id="generic">Generic</h3>
<ul>
<li><strong>목적</strong>: 타입을 파라미터처럼 전달하여 재사용성 높이기</li>
<li><strong>방법</strong>: <code>&lt;타입파라미터&gt;</code> 문법 사용</li>
<li><strong>제약</strong>: <code>extends</code>로 허용할 타입 범위 제한 가능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript-06] public, private
]]></title>
            <link>https://velog.io/@comely_15/TypeScript-06-public-private</link>
            <guid>https://velog.io/@comely_15/TypeScript-06-public-private</guid>
            <pubDate>Fri, 06 Jun 2025 19:16:35 GMT</pubDate>
            <description><![CDATA[<h1 id="typescript-class-키워드-완벽-가이드">TypeScript Class 키워드 완벽 가이드</h1>
<h2 id="1-public-private-키워드">1. public, private 키워드</h2>
<h3 id="public-키워드">public 키워드</h3>
<ul>
<li>클래스 속성을 <strong>어디서든 접근하고 수정 가능</strong>하게 만듦</li>
<li>기본값이므로 생략해도 동일하게 작동</li>
</ul>
<pre><code class="language-typescript">class User {
  public name: string;

  constructor() {
    this.name = &#39;kim&#39;;
  }
}

let 유저1 = new User();
유저1.name = &#39;park&#39;;  // 가능</code></pre>
<h3 id="private-키워드">private 키워드</h3>
<ul>
<li><strong>클래스 내부에서만</strong> 접근하고 수정 가능</li>
<li>자식 객체에서도 접근 불가능</li>
</ul>
<pre><code class="language-typescript">class User {
  public name: string;
  private familyName: string;

  constructor() {
    this.name = &#39;kim&#39;;
    let hello = this.familyName + &#39;안뇽&#39;;  // 가능 (클래스 내부)
  }
}

let 유저1 = new User();
유저1.name = &#39;park&#39;;        // 가능
유저1.familyName = &#39;456&#39;;   // 에러 (외부 접근 불가)</code></pre>
<h3 id="private-속성-수정하기">private 속성 수정하기</h3>
<p>외부에서 private 속성을 수정하려면 <strong>클래스 내부에 함수를 만들어</strong> 간접 접근</p>
<pre><code class="language-typescript">class User {
  private familyName: string;

  changeSecret() {
    this.familyName = &#39;park&#39;;  // 클래스 내부에서 수정
  }
}

let 유저1 = new User();
유저1.changeSecret();  // 함수를 통한 간접 수정</code></pre>
<p><strong>활용법</strong>: 중요한 데이터를 실수로 수정하는 것을 방지하고, 안전장치를 추가한 개발 가능</p>
<h2 id="2-constructor-단축-문법">2. Constructor 단축 문법</h2>
<p>public/private 키워드를 constructor 파라미터에 붙이면 <strong>필드 선언과 할당을 동시에</strong> 처리</p>
<pre><code class="language-typescript">// 기존 방식
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 단축 문법
class Person {
  constructor(public name: string) {
    // this.name = name 자동 생성
  }
}</code></pre>
<h2 id="3-protected-키워드">3. protected 키워드</h2>
<p>private와 비슷하지만 <strong>extends로 상속받은 클래스에서도 사용 가능</strong></p>
<pre><code class="language-typescript">class User {
  protected x = 10;
}

class NewUser extends User {
  doThis() {
    this.x = 20;  // protected라서 가능 (private이면 에러)
  }
}</code></pre>
<ul>
<li><strong>private</strong>: 해당 클래스에서만 사용</li>
<li><strong>protected</strong>: 해당 클래스 + 상속받은 클래스에서 사용</li>
</ul>
<h2 id="4-static-키워드">4. static 키워드</h2>
<p>속성이나 메서드를 <strong>클래스 자체에 부여</strong> (인스턴스가 아닌)</p>
<pre><code class="language-typescript">class User {
  static x = 10;  // 클래스에 직접 부여
  y = 20;         // 인스턴스에 부여
}

let john = new User();
john.x;   // 불가능
User.x;   // 가능 (10)
john.y;   // 가능 (20)</code></pre>
<h3 id="static-활용-예시">static 활용 예시</h3>
<p>클래스의 <strong>기본 설정값이나 공통 데이터</strong> 저장용</p>
<pre><code class="language-typescript">class User {
  static skill = &#39;js&#39;;
  intro = User.skill + &#39; 전문가입니다&#39;;
}

// 설정 변경
User.skill = &#39;python&#39;;
let 민수 = new User();  // &#39;python 전문가입니다&#39;</code></pre>
<h3 id="키워드-조합-사용">키워드 조합 사용</h3>
<pre><code class="language-typescript">class User {
  private static x = 10;     // 클래스 내부에서만 접근 가능한 static
  public static y = 20;      // 어디서든 접근 가능한 static
  protected z = 30;          // 상속 클래스에서도 사용 가능
}</code></pre>
<h2 id="5-실무-활용-예시">5. 실무 활용 예시</h2>
<h3 id="숙제-1-키워드-특징-정리">숙제 1: 키워드 특징 정리</h3>
<pre><code class="language-typescript">class User {
  private static x = 10;    // User 클래스에서만 접근, 수정 가능
  public static y = 20;     // 어디서든 User.y로 접근, 수정 가능
  protected z = 30;         // 클래스 내부 + 상속 클래스에서 사용 가능
}</code></pre>
<h3 id="숙제-2-static-메서드-만들기">숙제 2: static 메서드 만들기</h3>
<pre><code class="language-typescript">class User {
  private static x = 10;
  public static y = 20;

  static addOne(param: number) {
    User.x += param;  // static 속성은 클래스명.속성으로 접근
  }

  static printX() {
    console.log(User.x);
  }
}

User.addOne(3);
User.printX();  // 13</code></pre>
<h3 id="숙제-3-dom-조작-클래스">숙제 3: DOM 조작 클래스</h3>
<pre><code class="language-typescript">class Square {
  constructor(
    public width: number, 
    public height: number, 
    public color: string
  ) {}

  draw() {
    let randomPos = Math.random() * 400;  // 0~400px 랜덤 위치

    let square = `&lt;div style=&quot;
      position: relative;
      top: ${randomPos}px;
      left: ${randomPos}px;
      width: ${this.width}px;
      height: ${this.height}px;
      background: ${this.color}
    &quot;&gt;&lt;/div&gt;`;

    document.body.insertAdjacentHTML(&#39;beforeend&#39;, square);
  }
}

let 네모 = new Square(30, 30, &#39;red&#39;);
네모.draw();  // 랜덤 위치에 빨간 사각형 생성</code></pre>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>키워드</th>
<th>접근 범위</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>public</strong></td>
<td>어디서든</td>
<td>기본값, 자유롭게 접근 가능</td>
</tr>
<tr>
<td><strong>private</strong></td>
<td>클래스 내부만</td>
<td>외부 접근 차단, 보안성 높음</td>
</tr>
<tr>
<td><strong>protected</strong></td>
<td>클래스 + 상속 클래스</td>
<td>private보다 범위가 넓음</td>
</tr>
<tr>
<td><strong>static</strong></td>
<td>클래스 자체</td>
<td>인스턴스가 아닌 클래스에 직접 부여</td>
</tr>
</tbody></table>
<p><strong>핵심</strong>: 각 키워드는 데이터 보안과 코드 구조화를 위한 도구들입니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript-05] 연산자, Destructuring]]></title>
            <link>https://velog.io/@comely_15/TypeScript-05-%EC%97%B0%EC%82%B0%EC%9E%90-Destructuring</link>
            <guid>https://velog.io/@comely_15/TypeScript-05-%EC%97%B0%EC%82%B0%EC%9E%90-Destructuring</guid>
            <pubDate>Fri, 06 Jun 2025 19:15:06 GMT</pubDate>
            <description><![CDATA[<h2 id="1--연산자를-활용한-nullundefined-처리">1. &amp;&amp; 연산자를 활용한 null/undefined 처리</h2>
<h3 id="연산자의-특별한-기능">&amp;&amp; 연산자의 특별한 기능</h3>
<ul>
<li><strong>falsy 값</strong>: <code>null</code>, <code>undefined</code>, <code>NaN</code>, <code>false</code> 등</li>
<li><strong>동작 원리</strong>: 첫 번째 falsy 값을 반환하거나, 모두 truthy면 마지막 값 반환</li>
</ul>
<pre><code class="language-typescript">1 &amp;&amp; null &amp;&amp; 3          // null 반환
undefined &amp;&amp; &#39;안녕&#39; &amp;&amp; 100  // undefined 반환</code></pre>
<h3 id="실무-활용법">실무 활용법</h3>
<pre><code class="language-typescript">function printAll(strs: string | undefined) {
  if (strs &amp;&amp; typeof strs === &quot;string&quot;) {  
    console.log(strs);
  } 
}

// 또는 더 간단하게
if (변수 != null) {
  // null과 undefined 모두 걸러냄
}</code></pre>
<h2 id="2-in-연산자로-object-narrowing">2. in 연산자로 Object Narrowing</h2>
<p>서로 다른 속성을 가진 객체들을 구분할 때 사용</p>
<pre><code class="language-typescript">type Fish = { swim: string };
type Bird = { fly: string };

function 함수(animal: Fish | Bird) {
  if (&quot;swim&quot; in animal) {
    return animal.swim;  // Fish 타입으로 narrowing
  }
  return animal.fly;     // Bird 타입으로 narrowing
}</code></pre>
<p><strong>핵심</strong>: 배타적인 속성(서로 겹치지 않는 속성)이 있어야 narrowing 가능</p>
<h2 id="3-instanceof로-class-객체-구분">3. instanceof로 Class 객체 구분</h2>
<p>클래스로 생성된 객체들을 구분할 때 사용</p>
<pre><code class="language-typescript">let 날짜 = new Date();
if (날짜 instanceof Date) {
  console.log(&#39;Date 객체입니다&#39;);
}</code></pre>
<h2 id="4-literal-type을-활용한-narrowing">4. Literal Type을 활용한 Narrowing</h2>
<p>가장 실용적인 방법으로, 각 타입에 고유한 literal 값을 부여</p>
<pre><code class="language-typescript">type Car = {
  wheel: &#39;4개&#39;,
  color: string
}

type Bike = {
  wheel: &#39;2개&#39;, 
  color: string
}

function 함수(x: Car | Bike) {
  if (x.wheel === &#39;4개&#39;) {
    console.log(&#39;차량: &#39; + x.color);
  } else {
    console.log(&#39;바이크: &#39; + x.color);
  }
}</code></pre>
<p><strong>장점</strong>: 비슷한 구조의 객체들도 쉽게 구분 가능</p>
<hr>
<h1 id="rest-파라미터--destructuring">Rest 파라미터 &amp; Destructuring</h1>
<h2 id="rest-파라미터-기본-개념">Rest 파라미터 기본 개념</h2>
<p>개수가 정해지지 않은 파라미터를 받을 때 사용</p>
<pre><code class="language-typescript">function 전부더하기(...a: number[]) {
  console.log(a);  // 배열로 받아짐
}

전부더하기(1, 2, 3, 4, 5);  // [1, 2, 3, 4, 5]</code></pre>
<p><strong>주의사항</strong>:</p>
<ul>
<li>다른 파라미터 뒤에만 올 수 있음</li>
<li>항상 배열 형태로 전달됨</li>
</ul>
<h2 id="spread-vs-rest-구분">Spread vs Rest 구분</h2>
<pre><code class="language-typescript">// Spread: 배열/객체 펼치기
let arr = [3, 4, 5];
let arr2 = [1, 2, ...arr];  // [1, 2, 3, 4, 5]

// Rest: 파라미터 여러 개 받기  
function func(...params) { }</code></pre>
<h2 id="destructuring-문법">Destructuring 문법</h2>
<h3 id="객체-destructuring">객체 Destructuring</h3>
<pre><code class="language-typescript">let { student, age } = { student: true, age: 20 };</code></pre>
<h3 id="배열-destructuring">배열 Destructuring</h3>
<pre><code class="language-typescript">let [a, b] = [&#39;안녕&#39;, 100];</code></pre>
<h3 id="함수-파라미터에서-destructuring">함수 파라미터에서 Destructuring</h3>
<pre><code class="language-typescript">// 객체 파라미터
type UserType = {
  user: string,
  comment: number[],
  admin: boolean
}

function 함수({user, comment, admin}: UserType): void {
  console.log(user, comment, admin);
}

// 배열 파라미터
type 어레이 = (number | string | boolean)[];

function 함수2([a, b, c]: 어레이) {
  console.log(a, b, c);
}</code></pre>
<hr>
<h1 id="never-type-완벽-이해">Never Type 완벽 이해</h1>
<h2 id="never-type-기본-조건">Never Type 기본 조건</h2>
<p>함수에 never 타입을 사용하려면:</p>
<ol>
<li><strong>절대 return하지 않음</strong></li>
<li><strong>함수 실행이 끝나지 않음</strong> (endpoint가 없음)</li>
</ol>
<h2 id="never-type-사용-예시">Never Type 사용 예시</h2>
<h3 id="무한-루프">무한 루프</h3>
<pre><code class="language-typescript">function 함수(): never {
  while (true) {
    console.log(123);
  }
}</code></pre>
<h3 id="에러-발생">에러 발생</h3>
<pre><code class="language-typescript">function 함수(): never {
  throw new Error(&#39;에러메세지&#39;);
}</code></pre>
<h2 id="never가-자동으로-나타나는-경우">Never가 자동으로 나타나는 경우</h2>
<h3 id="1-잘못된-narrowing">1. 잘못된 Narrowing</h3>
<pre><code class="language-typescript">function 함수(parameter: string) {
  if (typeof parameter === &quot;string&quot;) {
    parameter + 1;
  } else {
    parameter;  // never 타입이 됨 (있을 수 없는 경우)
  }
}</code></pre>
<h3 id="2-함수-표현식-vs-함수-선언문">2. 함수 표현식 vs 함수 선언문</h3>
<pre><code class="language-typescript">// 함수 선언문 → void 타입
function 함수1() {
  throw new Error();
}

// 함수 표현식 → never 타입  
let 함수2 = function() {
  throw new Error();
}</code></pre>
<h3 id="3-strict-모드에서-빈-배열">3. Strict 모드에서 빈 배열</h3>
<pre><code class="language-typescript">let arr = [];  // strict 모드에서 never[] 타입</code></pre>
<h2 id="실무에서는">실무에서는?</h2>
<p>Never 타입은 직접 사용할 일이 거의 없음. 주로 <strong>코드 오류 감지</strong>용으로 자동 등장하므로, never가 보이면 코드를 점검해봅시다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript-02] Type Aliases
]]></title>
            <link>https://velog.io/@comely_15/TypeScript-02-Type-Aliases</link>
            <guid>https://velog.io/@comely_15/TypeScript-02-Type-Aliases</guid>
            <pubDate>Fri, 06 Jun 2025 18:56:07 GMT</pubDate>
            <description><![CDATA[<h2 id="type-aliases-타입-별칭">Type Aliases (타입 별칭)</h2>
<h3 id="기본-개념">기본 개념</h3>
<p>긴 타입 정의를 변수처럼 저장하여 재사용하는 기능</p>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-typescript">// 기본 Type Alias
type Animal = string | number | undefined
let 동물: Animal = &#39;cat&#39;

// 복잡한 Union 타입을 간단하게
type StringOrNumber = string | number
let value: StringOrNumber = 123</code></pre>
<h3 id="object-타입-저장">Object 타입 저장</h3>
<pre><code class="language-typescript">type 사람 = {
  name: string,
  age: number,
}

let teacher: 사람 = { name: &#39;john&#39;, age: 20 }

// Type Alias 없이 쓰면
let teacher: {
  name: string,
  age: number,
} = { name: &#39;john&#39;, age: 20 }  // 복잡하고 가독성 떨어짐</code></pre>
<h3 id="readonly-속성">readonly 속성</h3>
<pre><code class="language-typescript">type Girlfriend = {
  readonly name: string,
  age: number
}

let 여친: Girlfriend = {
  name: &#39;엠버&#39;,
  age: 25
}

여친.name = &#39;유라&#39;  // 에러! readonly 속성
여친.age = 26       // 정상</code></pre>
<h3 id="선택적-속성-optional-properties">선택적 속성 (Optional Properties)</h3>
<pre><code class="language-typescript">type Square = {
  color?: string,    // 선택적 속성
  width: number,     // 필수 속성
}

let 네모1: Square = { width: 100 }              // 정상
let 네모2: Square = { color: &#39;red&#39;, width: 100 } // 정상</code></pre>
<p><strong>중요</strong>: <code>color?</code>는 <code>color: string | undefined</code>와 동일</p>
<hr>
<h2 id="type-aliases-확장">Type Aliases 확장</h2>
<h3 id="union으로-합치기">Union으로 합치기</h3>
<pre><code class="language-typescript">type Name = string
type Age = number
type Person = Name | Age  // string | number

let info: Person = &#39;kim&#39;  // 정상
info = 25                 // 정상</code></pre>
<h3 id="intersection으로-합치기-">Intersection으로 합치기 (&amp;)</h3>
<pre><code class="language-typescript">type PositionX = { x: number }
type PositionY = { y: number }
type XandY = PositionX &amp; PositionY  // { x: number, y: number }

let 좌표: XandY = { x: 1, y: 2 }</code></pre>
<h3 id="복합-확장">복합 확장</h3>
<pre><code class="language-typescript">type BasicInfo = { name: string, age: number }
type ContactInfo = { phone: string, email: string }
type UserInfo = BasicInfo &amp; ContactInfo &amp; { isActive: boolean }

let user: UserInfo = {
  name: &#39;kim&#39;,
  age: 25,
  phone: &#39;010-1234-5678&#39;,
  email: &#39;kim@email.com&#39;,
  isActive: true
}</code></pre>
<h3 id="type-aliases-제한사항">Type Aliases 제한사항</h3>
<pre><code class="language-typescript">type Name = string
type Name = number  // 에러! 재정의 불가능</code></pre>
<hr>
<h2 id="type-narrowing-타입-좁히기">Type Narrowing (타입 좁히기)</h2>
<h3 id="문제-상황">문제 상황</h3>
<pre><code class="language-typescript">function 내함수(x: number | string) {
  return x + 1  // 에러! Union 타입에서는 연산 불가
}</code></pre>
<h3 id="typeof를-사용한-narrowing">typeof를 사용한 Narrowing</h3>
<pre><code class="language-typescript">function 내함수(x: number | string) {
  if (typeof x === &#39;number&#39;) {
    return x + 1        // x는 number 타입
  } else if (typeof x === &#39;string&#39;) {
    return x + &#39;1&#39;      // x는 string 타입
  } else {
    return 0            // 모든 경우 처리 (권장)
  }
}</code></pre>
<h3 id="다양한-narrowing-방법">다양한 Narrowing 방법</h3>
<pre><code class="language-typescript">// 1. typeof 사용
function checkType(x: string | number) {
  if (typeof x === &#39;string&#39;) {
    return x.toUpperCase()  // string 메서드 사용 가능
  }
  return x * 2  // number 연산 가능
}

// 2. in 연산자 사용
type Dog = { bark: string }
type Cat = { meow: string }

function makeSound(animal: Dog | Cat) {
  if (&#39;bark&#39; in animal) {
    console.log(animal.bark)  // Dog 타입
  } else {
    console.log(animal.meow)  // Cat 타입
  }
}

// 3. instanceof 사용
function processDate(date: Date | string) {
  if (date instanceof Date) {
    return date.getFullYear()  // Date 메서드 사용 가능
  }
  return new Date(date).getFullYear()
}

// 4. Array.isArray() 사용
function processValue(value: string | string[]) {
  if (Array.isArray(value)) {
    return value.join(&#39;, &#39;)    // 배열 메서드 사용 가능
  }
  return value.toUpperCase()   // 문자열 메서드 사용 가능
}</code></pre>
<hr>
<h2 id="type-assertion-타입-단언">Type Assertion (타입 단언)</h2>
<h3 id="기본-사용법-1">기본 사용법</h3>
<pre><code class="language-typescript">function 내함수(x: number | string) {
  return (x as number) + 1
}

console.log(내함수(123))  // 124
console.log(내함수(&#39;123&#39;))  // &#39;1231&#39; (문자열 연결)</code></pre>
<h3 id="type-assertion-특징">Type Assertion 특징</h3>
<ol>
<li><strong>타입 강제 변환</strong>: Union 타입을 특정 타입으로 단언</li>
<li><strong>컴파일 타임만 적용</strong>: 실제 런타임 동작은 변경되지 않음</li>
<li><strong>제한적 사용</strong>: 관련 있는 타입끼리만 단언 가능</li>
</ol>
<h3 id="안전한-사용-예시">안전한 사용 예시</h3>
<pre><code class="language-typescript">// DOM 요소 타입 단언
let button = document.querySelector(&#39;#btn&#39;) as HTMLButtonElement
button.disabled = true  // HTMLButtonElement 메서드 사용 가능

// API 응답 타입 단언
let response = JSON.parse(&#39;{&quot;name&quot;:&quot;kim&quot;,&quot;age&quot;:25}&#39;) as {name: string, age: number}
console.log(response.name)  // 타입 안전</code></pre>
<h3 id="type-assertion-vs-narrowing">Type Assertion vs Narrowing</h3>
<pre><code class="language-typescript">// Narrowing (권장)
function safeAdd(x: number | string) {
  if (typeof x === &#39;number&#39;) {
    return x + 1
  }
  return parseFloat(x) + 1
}

// Assertion (주의해서 사용)
function riskyAdd(x: number | string) {
  return (x as number) + 1
}</code></pre>
<hr>
<h2 id="literal-types-리터럴-타입">Literal Types (리터럴 타입)</h2>
<h3 id="기본-개념-1">기본 개념</h3>
<p>특정 값만 가질 수 있도록 제한하는 타입</p>
<h3 id="문자열-literal-type">문자열 Literal Type</h3>
<pre><code class="language-typescript">let john: &#39;대머리&#39; = &#39;대머리&#39;
let kim: &#39;솔로&#39; = &#39;솔로&#39;

// john = &#39;기혼&#39;  // 에러! &#39;대머리&#39;만 가능</code></pre>
<h3 id="union-literal-type">Union Literal Type</h3>
<pre><code class="language-typescript">type Direction = &#39;left&#39; | &#39;right&#39; | &#39;up&#39; | &#39;down&#39;
let 방향: Direction = &#39;left&#39;

type Status = &#39;loading&#39; | &#39;success&#39; | &#39;error&#39;
let 현재상태: Status = &#39;loading&#39;</code></pre>
<h3 id="숫자-literal-type">숫자 Literal Type</h3>
<pre><code class="language-typescript">type DiceNumber = 1 | 2 | 3 | 4 | 5 | 6
let 주사위: DiceNumber = 3

function rollDice(): DiceNumber {
  return Math.floor(Math.random() * 6) + 1 as DiceNumber
}</code></pre>
<h3 id="함수에서-literal-type-활용">함수에서 Literal Type 활용</h3>
<pre><code class="language-typescript">// 매개변수와 반환값 모두 Literal Type
function 가위바위보(choice: &#39;가위&#39; | &#39;바위&#39; | &#39;보&#39;): (&#39;가위&#39; | &#39;바위&#39; | &#39;보&#39;)[] {
  return [&#39;가위&#39;, &#39;바위&#39;, &#39;보&#39;]
}

// 템플릿 리터럴 타입 (고급)
type Theme = &#39;light&#39; | &#39;dark&#39;
type Color = &#39;red&#39; | &#39;blue&#39; | &#39;green&#39;
type ThemedColor = `${Theme}-${Color}`  // &#39;light-red&#39; | &#39;light-blue&#39; | ... 등</code></pre>
<hr>
<h2 id="as-const-문법">as const 문법</h2>
<h3 id="문제-상황-1">문제 상황</h3>
<pre><code class="language-typescript">var 자료 = {
  name: &#39;kim&#39;
}

function 내함수(a: &#39;kim&#39;) {
  console.log(a)
}

내함수(자료.name)  // 에러! string 타입을 &#39;kim&#39; 타입에 할당 불가</code></pre>
<h3 id="as-const로-해결">as const로 해결</h3>
<pre><code class="language-typescript">var 자료 = {
  name: &#39;kim&#39;
} as const

function 내함수(a: &#39;kim&#39;) {
  console.log(a)
}

내함수(자료.name)  // 정상! 자료.name은 &#39;kim&#39; 타입</code></pre>
<h3 id="as-const-효과">as const 효과</h3>
<ol>
<li><strong>Literal 타입 적용</strong>: 값 자체가 타입이 됨</li>
<li><strong>readonly 적용</strong>: 모든 속성이 변경 불가능</li>
</ol>
<pre><code class="language-typescript">const config = {
  apiUrl: &#39;https://api.example.com&#39;,
  timeout: 5000,
  retries: 3
} as const

// config.apiUrl = &#39;other&#39;  // 에러! readonly 속성

type ApiUrl = typeof config.apiUrl  // &#39;https://api.example.com&#39; 타입</code></pre>
<h3 id="배열에서-as-const">배열에서 as const</h3>
<pre><code class="language-typescript">const colors = [&#39;red&#39;, &#39;green&#39;, &#39;blue&#39;] as const
type Color = typeof colors[number]  // &#39;red&#39; | &#39;green&#39; | &#39;blue&#39;

function setColor(color: Color) {
  console.log(color)
}

setColor(&#39;red&#39;)    // 정상
setColor(&#39;yellow&#39;) // 에러!</code></pre>
<hr>
<h2 id="실전-예제">실전 예제</h2>
<h3 id="예제-1-데이터-클리닝-함수">예제 1: 데이터 클리닝 함수</h3>
<pre><code class="language-typescript">function 클리닝함수(a: (number | string)[]): number[] {
  let 클리닝완료된거: number[] = []

  a.forEach((b) =&gt; {
    if (typeof b === &#39;string&#39;) {
      클리닝완료된거.push(parseFloat(b))
    } else {
      클리닝완료된거.push(b)
    }
  })

  return 클리닝완료된거
}

console.log(클리닝함수([123, &#39;456&#39;, 789, &#39;012&#39;]))  // [123, 456, 789, 12]</code></pre>
<h3 id="예제-2-선생님-과목-조회-함수">예제 2: 선생님 과목 조회 함수</h3>
<pre><code class="language-typescript">type Teacher = {
  subject: string | string[]
}

function 마지막과목찾기(teacher: Teacher): string {
  if (typeof teacher.subject === &#39;string&#39;) {
    return teacher.subject
  } else if (Array.isArray(teacher.subject)) {
    return teacher.subject[teacher.subject.length - 1]
  } else {
    return &#39;과목 없음&#39;
  }
}

let 철수쌤 = { subject: &#39;math&#39; }
let 영희쌤 = { subject: [&#39;science&#39;, &#39;english&#39;] }
let 민수쌤 = { subject: [&#39;science&#39;, &#39;art&#39;, &#39;korean&#39;] }

console.log(마지막과목찾기(철수쌤))  // &#39;math&#39;
console.log(마지막과목찾기(영희쌤))  // &#39;english&#39;
console.log(마지막과목찾기(민수쌤))  // &#39;korean&#39;</code></pre>
<h3 id="예제-3-사용자-타입-확장">예제 3: 사용자 타입 확장</h3>
<pre><code class="language-typescript">// 기본 사용자 정보
type User = {
  name: string,
  email?: string,
  phone: string
}

// 성인 여부 추가
type Adult = {
  adult: boolean
}

// 두 타입 결합
type NewUser = User &amp; Adult

let 회원가입정보: NewUser = {
  name: &#39;kim&#39;,
  adult: false,
  phone: &#39;010-1234-5678&#39;
  // email은 선택사항이므로 생략 가능
}</code></pre>
<hr>
<h2 id="고급-패턴">고급 패턴</h2>
<h3 id="1-조건부-타입-활용">1. 조건부 타입 활용</h3>
<pre><code class="language-typescript">type ApiResponse&lt;T&gt; = {
  success: true,
  data: T
} | {
  success: false,
  error: string
}

function handleResponse&lt;T&gt;(response: ApiResponse&lt;T&gt;): T | null {
  if (response.success) {
    return response.data  // 타입 안전
  } else {
    console.error(response.error)
    return null
  }
}</code></pre>
<h3 id="2-discriminated-union">2. Discriminated Union</h3>
<pre><code class="language-typescript">type Shape = 
  | { kind: &#39;circle&#39;, radius: number }
  | { kind: &#39;square&#39;, size: number }
  | { kind: &#39;rectangle&#39;, width: number, height: number }

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case &#39;circle&#39;:
      return Math.PI * shape.radius ** 2
    case &#39;square&#39;:
      return shape.size ** 2
    case &#39;rectangle&#39;:
      return shape.width * shape.height
    default:
      // 모든 케이스를 처리했는지 컴파일 타임에 확인
      const exhaustiveCheck: never = shape
      throw new Error(`Unhandled shape: ${exhaustiveCheck}`)
  }
}</code></pre>
<h3 id="3-type-guard-함수">3. Type Guard 함수</h3>
<pre><code class="language-typescript">function isString(value: unknown): value is string {
  return typeof value === &#39;string&#39;
}

function isNumber(value: unknown): value is number {
  return typeof value === &#39;number&#39;
}

function processValue(value: unknown) {
  if (isString(value)) {
    console.log(value.toUpperCase())  // string 메서드 사용 가능
  } else if (isNumber(value)) {
    console.log(value.toFixed(2))     // number 메서드 사용 가능
  }
}</code></pre>
<hr>
<h2 id="모범-사례-및-팁">모범 사례 및 팁</h2>
<h3 id="1-type-alias-네이밍">1. Type Alias 네이밍</h3>
<pre><code class="language-typescript">// 좋은 예
type UserProfile = { name: string, age: number }
type ApiResponse&lt;T&gt; = { data: T, status: number }
type HttpMethod = &#39;GET&#39; | &#39;POST&#39; | &#39;PUT&#39; | &#39;DELETE&#39;

// 피해야 할 예
type user = { name: string, age: number }  // 소문자 시작
type Data = any  // 너무 일반적</code></pre>
<h3 id="2-union-vs-intersection-선택">2. Union vs Intersection 선택</h3>
<pre><code class="language-typescript">// Union (OR): 여러 타입 중 하나
type StringOrNumber = string | number

// Intersection (AND): 모든 타입 속성 포함
type UserWithProfile = User &amp; Profile</code></pre>
<h3 id="3-narrowing-vs-assertion-선택">3. Narrowing vs Assertion 선택</h3>
<pre><code class="language-typescript">// Narrowing 선호 (안전)
function processId(id: string | number) {
  if (typeof id === &#39;string&#39;) {
    return id.toUpperCase()
  }
  return id.toString()
}

// Assertion 최소화 (위험)
function processId(id: string | number) {
  return (id as string).toUpperCase()  // 런타임 에러 가능성
}</code></pre>
<hr>
<h2 id="실전-체크리스트">실전 체크리스트</h2>
<h3 id="type-aliases">Type Aliases</h3>
<ul>
<li><input disabled="" type="checkbox"> 복잡한 타입을 Type Alias로 분리</li>
<li><input disabled="" type="checkbox"> 의미 있는 이름으로 명명</li>
<li><input disabled="" type="checkbox"> readonly와 선택적 속성 적절히 활용</li>
<li><input disabled="" type="checkbox"> Intersection으로 타입 확장</li>
</ul>
<h3 id="type-narrowing">Type Narrowing</h3>
<ul>
<li><input disabled="" type="checkbox"> Union 타입 사용 시 적절한 타입 가드 구현</li>
<li><input disabled="" type="checkbox"> typeof, in, instanceof 등 적절한 방법 선택</li>
<li><input disabled="" type="checkbox"> 모든 케이스 처리 확인</li>
</ul>
<h3 id="type-assertion">Type Assertion</h3>
<ul>
<li><input disabled="" type="checkbox"> 최소한으로 사용</li>
<li><input disabled="" type="checkbox"> 타입 안전성이 확실할 때만 사용</li>
<li><input disabled="" type="checkbox"> DOM 조작이나 외부 라이브러리 사용 시 활용</li>
</ul>
<h3 id="literal-types">Literal Types</h3>
<ul>
<li><input disabled="" type="checkbox"> 제한된 값만 허용해야 하는 경우 활용</li>
<li><input disabled="" type="checkbox"> 상수나 설정값에 활용</li>
<li><input disabled="" type="checkbox"> as const로 불변 객체 생성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Node.js-10] 웹 서비스 추가 기능]]></title>
            <link>https://velog.io/@comely_15/Node.js-10-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%B6%94%EA%B0%80-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@comely_15/Node.js-10-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%B6%94%EA%B0%80-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Fri, 06 Jun 2025 18:34:45 GMT</pubDate>
            <description><![CDATA[<h2 id="결제-기능-구현">결제 기능 구현</h2>
<h3 id="결제-시스템-개요">결제 시스템 개요</h3>
<ul>
<li><strong>PG사 연동</strong>: 직접 결제 처리 X, 전문 업체 활용</li>
<li><strong>결제 대행사</strong>: 다날, 이니시스, KG이니시스 등</li>
<li><strong>통합 솔루션</strong>: 포트원(구 아임포트) 활용 권장</li>
</ul>
<h3 id="포트원portone-결제-구현">포트원(PortOne) 결제 구현</h3>
<h4 id="1-포트원-설정">1. 포트원 설정</h4>
<pre><code class="language-javascript">// 포트원 라이브러리 로드
&lt;script src=&quot;https://cdn.iamport.kr/v1/iamport.js&quot;&gt;&lt;/script&gt;

// 초기화
const IMP = window.IMP;
IMP.init(&#39;imp_code&#39;); // 발급받은 가맹점 식별코드</code></pre>
<h4 id="2-결제-요청">2. 결제 요청</h4>
<pre><code class="language-javascript">// 결제 요청 함수
function requestPay() {
  IMP.request_pay({
    pg: &quot;html5_inicis&quot;,              // PG사 코드
    pay_method: &quot;card&quot;,              // 결제 방법
    merchant_uid: &quot;order_&quot; + new Date().getTime(), // 주문번호
    name: &quot;상품명&quot;,                   // 상품명
    amount: 1000,                    // 결제 금액
    buyer_email: &quot;buyer@example.com&quot;, // 구매자 이메일
    buyer_name: &quot;구매자명&quot;,           // 구매자 이름
    buyer_tel: &quot;010-1234-5678&quot;,      // 구매자 전화번호
    buyer_addr: &quot;서울특별시&quot;,         // 구매자 주소
    buyer_postcode: &quot;123-456&quot;        // 구매자 우편번호
  }, function(rsp) {
    if (rsp.success) {
      // 결제 성공시
      fetch(&#39;/payment/complete&#39;, {
        method: &#39;POST&#39;,
        headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
        body: JSON.stringify({
          imp_uid: rsp.imp_uid,        // 결제 고유번호
          merchant_uid: rsp.merchant_uid, // 주문번호
          amount: rsp.paid_amount      // 결제 금액
        })
      })
      .then(response =&gt; response.json())
      .then(data =&gt; {
        if (data.success) {
          alert(&#39;결제가 완료되었습니다.&#39;);
          location.href = &#39;/payment/success&#39;;
        }
      });
    } else {
      // 결제 실패시
      alert(&#39;결제에 실패하였습니다. &#39; + rsp.error_msg);
    }
  });
}</code></pre>
<h4 id="3-서버에서-결제-검증">3. 서버에서 결제 검증</h4>
<pre><code class="language-javascript">// 결제 완료 API
app.post(&#39;/payment/complete&#39;, async (요청, 응답) =&gt; {
  try {
    const { imp_uid, merchant_uid, amount } = 요청.body;

    // 포트원에서 결제 정보 조회 (결제 검증)
    const getToken = await fetch(&#39;https://api.iamport.kr/users/getToken&#39;, {
      method: &#39;POST&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify({
        imp_key: &#39;your_api_key&#39;,
        imp_secret: &#39;your_api_secret&#39;
      })
    });

    const { access_token } = await getToken.json();

    // 결제 정보 조회
    const getPaymentData = await fetch(`https://api.iamport.kr/payments/${imp_uid}`, {
      headers: { &#39;Authorization&#39;: access_token }
    });

    const paymentData = await getPaymentData.json();
    const { amount: paidAmount, status } = paymentData.response;

    // 결제 금액 및 상태 검증
    if (paidAmount === amount &amp;&amp; status === &#39;paid&#39;) {
      // DB에 결제 정보 저장
      await db.collection(&#39;payments&#39;).insertOne({
        imp_uid: imp_uid,
        merchant_uid: merchant_uid,
        amount: amount,
        status: &#39;completed&#39;,
        user_id: 요청.user._id,
        created_at: new Date()
      });

      응답.json({ success: true, message: &#39;결제가 완료되었습니다.&#39; });
    } else {
      응답.json({ success: false, message: &#39;결제 검증 실패&#39; });
    }
  } catch (error) {
    console.error(&#39;결제 처리 오류:&#39;, error);
    응답.status(500).json({ success: false, message: &#39;서버 오류&#39; });
  }
});</code></pre>
<h3 id="해외-결제-솔루션">해외 결제 솔루션</h3>
<h4 id="stripe-결제-구현">Stripe 결제 구현</h4>
<pre><code class="language-javascript">// Stripe 라이브러리 로드
&lt;script src=&quot;https://js.stripe.com/v3/&quot;&gt;&lt;/script&gt;

// Stripe 초기화
const stripe = Stripe(&#39;pk_test_your_publishable_key&#39;);

// 결제 요청
async function createPayment() {
  const response = await fetch(&#39;/create-payment-intent&#39;, {
    method: &#39;POST&#39;,
    headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
    body: JSON.stringify({ amount: 1000 }) // 금액 (센트 단위)
  });

  const { client_secret } = await response.json();

  const result = await stripe.confirmCardPayment(client_secret, {
    payment_method: {
      card: cardElement,
      billing_details: {
        name: &#39;Customer Name&#39;
      }
    }
  });

  if (result.error) {
    console.error(result.error.message);
  } else {
    console.log(&#39;결제 성공!&#39;);
  }
}</code></pre>
<h4 id="paypal-결제-구현">PayPal 결제 구현</h4>
<pre><code class="language-javascript">// PayPal 버튼 생성
paypal.Buttons({
  createOrder: function(data, actions) {
    return actions.order.create({
      purchase_units: [{
        amount: {
          value: &#39;10.00&#39;
        }
      }]
    });
  },
  onApprove: function(data, actions) {
    return actions.order.capture().then(function(details) {
      alert(&#39;결제가 완료되었습니다. &#39; + details.payer.name.given_name);
    });
  }
}).render(&#39;#paypal-button-container&#39;);</code></pre>
<hr>
<h2 id="리치-텍스트-에디터-구현">리치 텍스트 에디터 구현</h2>
<h3 id="에디터의-역할">에디터의 역할</h3>
<ul>
<li><strong>WYSIWYG</strong>: What You See Is What You Get</li>
<li><strong>기능</strong>: 텍스트 서식, 이미지 삽입, 링크 생성 등</li>
<li><strong>데이터 변환</strong>: 사용자 입력 → HTML/JSON 형태로 변환</li>
</ul>
<h3 id="quilljs-에디터-구현">Quill.js 에디터 구현</h3>
<h4 id="1-기본-설정">1. 기본 설정</h4>
<pre><code class="language-html">&lt;!-- Quill 라이브러리 로드 --&gt;
&lt;link href=&quot;https://cdn.quilljs.com/1.3.6/quill.snow.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;script src=&quot;https://cdn.quilljs.com/1.3.6/quill.min.js&quot;&gt;&lt;/script&gt;

&lt;!-- 에디터 컨테이너 --&gt;
&lt;div id=&quot;editor&quot;&gt;
  &lt;p&gt;여기에 내용을 입력하세요...&lt;/p&gt;
&lt;/div&gt;</code></pre>
<h4 id="2-에디터-초기화">2. 에디터 초기화</h4>
<pre><code class="language-javascript">// Quill 에디터 초기화
const quill = new Quill(&#39;#editor&#39;, {
  theme: &#39;snow&#39;,
  modules: {
    toolbar: [
      [{ &#39;header&#39;: [1, 2, 3, false] }],
      [&#39;bold&#39;, &#39;italic&#39;, &#39;underline&#39;, &#39;strike&#39;],
      [{ &#39;color&#39;: [] }, { &#39;background&#39;: [] }],
      [{ &#39;list&#39;: &#39;ordered&#39;}, { &#39;list&#39;: &#39;bullet&#39; }],
      [{ &#39;indent&#39;: &#39;-1&#39;}, { &#39;indent&#39;: &#39;+1&#39; }],
      [&#39;link&#39;, &#39;image&#39;, &#39;video&#39;],
      [&#39;clean&#39;]
    ]
  }
});

// 내용 가져오기
function getEditorContent() {
  const htmlContent = quill.root.innerHTML;
  const deltaContent = quill.getContents();

  return {
    html: htmlContent,
    delta: deltaContent
  };
}

// 폼 제출시 에디터 내용 포함
document.querySelector(&#39;form&#39;).addEventListener(&#39;submit&#39;, function(e) {
  const content = getEditorContent();

  // 숨겨진 input에 내용 저장
  document.querySelector(&#39;input[name=&quot;content&quot;]&#39;).value = content.html;
  document.querySelector(&#39;input[name=&quot;contentDelta&quot;]&#39;).value = JSON.stringify(content.delta);
});</code></pre>
<h4 id="3-서버에서-에디터-데이터-처리">3. 서버에서 에디터 데이터 처리</h4>
<pre><code class="language-javascript">app.post(&#39;/add-post&#39;, async (요청, 응답) =&gt; {
  try {
    const { title, content, contentDelta } = 요청.body;

    // HTML 내용 검증 및 정제 (XSS 방지)
    const cleanContent = sanitizeHtml(content, {
      allowedTags: [&#39;h1&#39;, &#39;h2&#39;, &#39;h3&#39;, &#39;p&#39;, &#39;br&#39;, &#39;strong&#39;, &#39;em&#39;, &#39;u&#39;, &#39;ol&#39;, &#39;ul&#39;, &#39;li&#39;, &#39;a&#39;, &#39;img&#39;],
      allowedAttributes: {
        &#39;a&#39;: [&#39;href&#39;],
        &#39;img&#39;: [&#39;src&#39;, &#39;alt&#39;]
      }
    });

    await db.collection(&#39;posts&#39;).insertOne({
      title: title,
      content: cleanContent,
      contentDelta: JSON.parse(contentDelta),
      author: 요청.user._id,
      createdAt: new Date()
    });

    응답.redirect(&#39;/posts&#39;);
  } catch (error) {
    console.error(&#39;게시글 저장 오류:&#39;, error);
    응답.status(500).send(&#39;서버 오류&#39;);
  }
});</code></pre>
<h3 id="toast-ui-editor-구현">Toast UI Editor 구현</h3>
<h4 id="1-기본-설정-1">1. 기본 설정</h4>
<pre><code class="language-html">&lt;!-- Toast UI Editor 라이브러리 --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://uicdn.toast.com/editor/latest/toastui-editor.min.css&quot; /&gt;
&lt;script src=&quot;https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js&quot;&gt;&lt;/script&gt;

&lt;!-- 에디터 컨테이너 --&gt;
&lt;div id=&quot;editor&quot;&gt;&lt;/div&gt;</code></pre>
<h4 id="2-에디터-초기화-1">2. 에디터 초기화</h4>
<pre><code class="language-javascript">// Toast UI Editor 초기화
const editor = new toastui.Editor({
  el: document.querySelector(&#39;#editor&#39;),
  height: &#39;500px&#39;,
  initialEditType: &#39;wysiwyg&#39;,
  previewStyle: &#39;vertical&#39;,
  placeholder: &#39;내용을 입력하세요...&#39;,
  hooks: {
    addImageBlobHook: (blob, callback) =&gt; {
      // 이미지 업로드 처리
      uploadImage(blob).then(imageUrl =&gt; {
        callback(imageUrl, &#39;alt text&#39;);
      });
    }
  }
});

// 내용 가져오기
function getEditorContent() {
  const htmlContent = editor.getHTML();
  const markdownContent = editor.getMarkdown();

  return {
    html: htmlContent,
    markdown: markdownContent
  };
}

// 이미지 업로드 함수
async function uploadImage(blob) {
  const formData = new FormData();
  formData.append(&#39;image&#39;, blob);

  const response = await fetch(&#39;/upload-image&#39;, {
    method: &#39;POST&#39;,
    body: formData
  });

  const result = await response.json();
  return result.imageUrl;
}</code></pre>
<h3 id="에디터-보안-처리">에디터 보안 처리</h3>
<h4 id="html-검증-및-정제">HTML 검증 및 정제</h4>
<pre><code class="language-javascript">// HTML 정제 라이브러리 설치
npm install sanitize-html

// 서버에서 HTML 정제
const sanitizeHtml = require(&#39;sanitize-html&#39;);

function sanitizeContent(content) {
  return sanitizeHtml(content, {
    allowedTags: [
      &#39;h1&#39;, &#39;h2&#39;, &#39;h3&#39;, &#39;h4&#39;, &#39;h5&#39;, &#39;h6&#39;,
      &#39;p&#39;, &#39;br&#39;, &#39;hr&#39;,
      &#39;strong&#39;, &#39;em&#39;, &#39;u&#39;, &#39;s&#39;, &#39;sup&#39;, &#39;sub&#39;,
      &#39;ol&#39;, &#39;ul&#39;, &#39;li&#39;,
      &#39;a&#39;, &#39;img&#39;,
      &#39;blockquote&#39;, &#39;code&#39;, &#39;pre&#39;,
      &#39;table&#39;, &#39;thead&#39;, &#39;tbody&#39;, &#39;tr&#39;, &#39;th&#39;, &#39;td&#39;
    ],
    allowedAttributes: {
      &#39;a&#39;: [&#39;href&#39;, &#39;target&#39;],
      &#39;img&#39;: [&#39;src&#39;, &#39;alt&#39;, &#39;width&#39;, &#39;height&#39;],
      &#39;table&#39;: [&#39;border&#39;, &#39;cellpadding&#39;, &#39;cellspacing&#39;],
      &#39;td&#39;: [&#39;colspan&#39;, &#39;rowspan&#39;],
      &#39;th&#39;: [&#39;colspan&#39;, &#39;rowspan&#39;]
    },
    allowedSchemes: [&#39;http&#39;, &#39;https&#39;, &#39;mailto&#39;]
  });
}</code></pre>
<hr>
<h2 id="파일-업로드-에디터-연동">파일 업로드 에디터 연동</h2>
<h3 id="이미지-업로드-서버-구현">이미지 업로드 서버 구현</h3>
<pre><code class="language-javascript">const multer = require(&#39;multer&#39;);
const path = require(&#39;path&#39;);

// 파일 저장 설정
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, &#39;uploads/editor-images/&#39;);
  },
  filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + &#39;-&#39; + Math.round(Math.random() * 1E9);
    cb(null, &#39;image-&#39; + uniqueSuffix + path.extname(file.originalname));
  }
});

const upload = multer({ 
  storage: storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB 제한
  fileFilter: function (req, file, cb) {
    // 이미지 파일만 허용
    if (file.mimetype.startsWith(&#39;image/&#39;)) {
      cb(null, true);
    } else {
      cb(new Error(&#39;이미지 파일만 업로드 가능합니다.&#39;));
    }
  }
});

// 이미지 업로드 API
app.post(&#39;/upload-image&#39;, upload.single(&#39;image&#39;), (요청, 응답) =&gt; {
  if (!요청.file) {
    return 응답.status(400).json({ error: &#39;파일이 업로드되지 않았습니다.&#39; });
  }

  const imageUrl = `/uploads/editor-images/${요청.file.filename}`;
  응답.json({ imageUrl: imageUrl });
});

// 정적 파일 서빙
app.use(&#39;/uploads&#39;, express.static(&#39;uploads&#39;));</code></pre>
<hr>
<h2 id="에디터-고급-기능">에디터 고급 기능</h2>
<h3 id="1-자동-저장-기능">1. 자동 저장 기능</h3>
<pre><code class="language-javascript">let autoSaveInterval;

function startAutoSave() {
  autoSaveInterval = setInterval(() =&gt; {
    const content = getEditorContent();
    const postId = getCurrentPostId();

    fetch(&#39;/auto-save&#39;, {
      method: &#39;POST&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify({
        postId: postId,
        content: content.html,
        contentDelta: content.delta
      })
    })
    .then(response =&gt; response.json())
    .then(data =&gt; {
      if (data.success) {
        showAutoSaveIndicator();
      }
    });
  }, 30000); // 30초마다 자동 저장
}

function showAutoSaveIndicator() {
  const indicator = document.querySelector(&#39;#auto-save-indicator&#39;);
  indicator.textContent = &#39;자동 저장됨 &#39; + new Date().toLocaleTimeString();
  indicator.style.opacity = &#39;1&#39;;

  setTimeout(() =&gt; {
    indicator.style.opacity = &#39;0&#39;;
  }, 3000);
}</code></pre>
<h3 id="2-글자-수-제한">2. 글자 수 제한</h3>
<pre><code class="language-javascript">// Quill 에디터 글자 수 제한
quill.on(&#39;text-change&#39;, function() {
  const text = quill.getText();
  const maxLength = 10000;

  if (text.length &gt; maxLength) {
    quill.deleteText(maxLength, text.length);
    alert(`최대 ${maxLength}자까지 입력 가능합니다.`);
  }

  // 글자 수 표시
  document.querySelector(&#39;#char-count&#39;).textContent = 
    `${text.length} / ${maxLength}`;
});</code></pre>
<h3 id="3-에디터-테마-커스터마이징">3. 에디터 테마 커스터마이징</h3>
<pre><code class="language-css">/* Quill 에디터 커스텀 스타일 */
.ql-editor {
  min-height: 300px;
  font-size: 16px;
  line-height: 1.6;
}

.ql-toolbar {
  border-top: 1px solid #ccc;
  border-left: 1px solid #ccc;
  border-right: 1px solid #ccc;
}

.ql-container {
  border-bottom: 1px solid #ccc;
  border-left: 1px solid #ccc;
  border-right: 1px solid #ccc;
}

/* 다크 테마 */
.dark-theme .ql-editor {
  background-color: #2d3748;
  color: #e2e8f0;
}

.dark-theme .ql-toolbar {
  background-color: #4a5568;
  border-color: #718096;
}</code></pre>
<hr>
<h2 id="실전-통합-예시">실전 통합 예시</h2>
<h3 id="완전한-게시글-작성-폼">완전한 게시글 작성 폼</h3>
<pre><code class="language-html">&lt;form id=&quot;post-form&quot; method=&quot;POST&quot; action=&quot;/posts&quot;&gt;
  &lt;div class=&quot;form-group&quot;&gt;
    &lt;label for=&quot;title&quot;&gt;제목&lt;/label&gt;
    &lt;input type=&quot;text&quot; id=&quot;title&quot; name=&quot;title&quot; required&gt;
  &lt;/div&gt;

  &lt;div class=&quot;form-group&quot;&gt;
    &lt;label for=&quot;category&quot;&gt;카테고리&lt;/label&gt;
    &lt;select id=&quot;category&quot; name=&quot;category&quot;&gt;
      &lt;option value=&quot;notice&quot;&gt;공지사항&lt;/option&gt;
      &lt;option value=&quot;free&quot;&gt;자유게시판&lt;/option&gt;
      &lt;option value=&quot;qna&quot;&gt;Q&amp;A&lt;/option&gt;
    &lt;/select&gt;
  &lt;/div&gt;

  &lt;div class=&quot;form-group&quot;&gt;
    &lt;label&gt;내용&lt;/label&gt;
    &lt;div id=&quot;editor&quot;&gt;&lt;/div&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;content&quot; id=&quot;content-input&quot;&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;contentDelta&quot; id=&quot;content-delta-input&quot;&gt;
  &lt;/div&gt;

  &lt;div class=&quot;form-group&quot;&gt;
    &lt;div id=&quot;char-count&quot;&gt;0 / 10000&lt;/div&gt;
    &lt;div id=&quot;auto-save-indicator&quot;&gt;&lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;form-actions&quot;&gt;
    &lt;button type=&quot;button&quot; onclick=&quot;saveDraft()&quot;&gt;임시저장&lt;/button&gt;
    &lt;button type=&quot;submit&quot;&gt;발행하기&lt;/button&gt;
  &lt;/div&gt;
&lt;/form&gt;

&lt;script&gt;
// 에디터 초기화
const quill = new Quill(&#39;#editor&#39;, {
  theme: &#39;snow&#39;,
  modules: {
    toolbar: [
      [{ &#39;header&#39;: [1, 2, 3, false] }],
      [&#39;bold&#39;, &#39;italic&#39;, &#39;underline&#39;],
      [{ &#39;color&#39;: [] }, { &#39;background&#39;: [] }],
      [{ &#39;list&#39;: &#39;ordered&#39;}, { &#39;list&#39;: &#39;bullet&#39; }],
      [&#39;link&#39;, &#39;image&#39;],
      [&#39;clean&#39;]
    ]
  }
});

// 폼 제출 처리
document.getElementById(&#39;post-form&#39;).addEventListener(&#39;submit&#39;, function(e) {
  const content = quill.root.innerHTML;
  const delta = quill.getContents();

  document.getElementById(&#39;content-input&#39;).value = content;
  document.getElementById(&#39;content-delta-input&#39;).value = JSON.stringify(delta);
});

// 자동 저장 시작
startAutoSave();
&lt;/script&gt;</code></pre>
<hr>
<h2 id="결제-및-에디터-비교표">결제 및 에디터 비교표</h2>
<h3 id="결제-솔루션-비교">결제 솔루션 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>포트원</th>
<th>Stripe</th>
<th>PayPal</th>
</tr>
</thead>
<tbody><tr>
<td><strong>대상 지역</strong></td>
<td>한국</td>
<td>전세계</td>
<td>전세계</td>
</tr>
<tr>
<td><strong>수수료</strong></td>
<td>2.9%~</td>
<td>2.9%~</td>
<td>3.4%~</td>
</tr>
<tr>
<td><strong>구현 난이도</strong></td>
<td>쉬움</td>
<td>보통</td>
<td>쉬움</td>
</tr>
<tr>
<td><strong>지원 결제수단</strong></td>
<td>카드, 계좌이체, 간편결제</td>
<td>카드 중심</td>
<td>PayPal 계정, 카드</td>
</tr>
<tr>
<td><strong>개발자 친화성</strong></td>
<td>우수</td>
<td>매우 우수</td>
<td>우수</td>
</tr>
</tbody></table>
<h3 id="에디터-라이브러리-비교">에디터 라이브러리 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>Quill.js</th>
<th>Toast UI Editor</th>
<th>TinyMCE</th>
</tr>
</thead>
<tbody><tr>
<td><strong>크기</strong></td>
<td>작음</td>
<td>보통</td>
<td>큼</td>
</tr>
<tr>
<td><strong>기능</strong></td>
<td>기본적</td>
<td>풍부</td>
<td>매우 풍부</td>
</tr>
<tr>
<td><strong>커스터마이징</strong></td>
<td>쉬움</td>
<td>보통</td>
<td>어려움</td>
</tr>
<tr>
<td><strong>마크다운 지원</strong></td>
<td>X</td>
<td>O</td>
<td>플러그인</td>
</tr>
<tr>
<td><strong>무료 사용</strong></td>
<td>O</td>
<td>O</td>
<td>제한적</td>
</tr>
</tbody></table>
<hr>
<h2 id="실전-체크리스트">실전 체크리스트</h2>
<h3 id="결제-기능">결제 기능</h3>
<ul>
<li><input disabled="" type="checkbox"> PG사 계약 및 테스트 계정 발급</li>
<li><input disabled="" type="checkbox"> 결제 라이브러리 연동</li>
<li><input disabled="" type="checkbox"> 결제 검증 로직 구현</li>
<li><input disabled="" type="checkbox"> 결제 내역 DB 저장</li>
<li><input disabled="" type="checkbox"> 환불 처리 로직</li>
</ul>
<h3 id="에디터-기능">에디터 기능</h3>
<ul>
<li><input disabled="" type="checkbox"> 에디터 라이브러리 선택 및 연동</li>
<li><input disabled="" type="checkbox"> 이미지 업로드 기능</li>
<li><input disabled="" type="checkbox"> HTML 보안 처리</li>
<li><input disabled="" type="checkbox"> 자동 저장 기능</li>
<li><input disabled="" type="checkbox"> 반응형 디자인</li>
</ul>
<h3 id="보안-고려사항">보안 고려사항</h3>
<ul>
<li><input disabled="" type="checkbox"> XSS 공격 방지</li>
<li><input disabled="" type="checkbox"> 파일 업로드 보안</li>
<li><input disabled="" type="checkbox"> 결제 데이터 암호화</li>
<li><input disabled="" type="checkbox"> API 보안 인증</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS-11] Admin Dashboard, 3D 변환]]></title>
            <link>https://velog.io/@comely_15/CSS-11-Admin-Dashboard-3D-%EB%B3%80%ED%99%98</link>
            <guid>https://velog.io/@comely_15/CSS-11-Admin-Dashboard-3D-%EB%B3%80%ED%99%98</guid>
            <pubDate>Fri, 06 Jun 2025 17:47:51 GMT</pubDate>
            <description><![CDATA[<h1 id="admin-dashboard-제작--3d-flip-카드-완벽-가이드">Admin Dashboard 제작 &amp; 3D Flip 카드 완벽 가이드</h1>
<h2 id="🎯-admin-dashboard-실습-개요">🎯 Admin Dashboard 실습 개요</h2>
<p>관리자 페이지(Admin Dashboard)는 웹 개발에서 자주 만들게 되는 실무 프로젝트입니다. Bootstrap 5와 Font Awesome 5를 활용하여 효율적으로 제작해보겠습니다.</p>
<h3 id="필요한-준비물">필요한 준비물</h3>
<ul>
<li><strong>Bootstrap 5</strong> CDN 또는 다운로드</li>
<li><strong>Font Awesome 5</strong> 아이콘 라이브러리</li>
<li>커스텀 CSS 파일</li>
</ul>
<h2 id="🔝-1-상단-네비게이션-바-제작">🔝 1. 상단 네비게이션 바 제작</h2>
<h3 id="기본-구조">기본 구조</h3>
<pre><code class="language-html">&lt;nav class=&quot;navbar navbar-expand-lg navbar-light bg-light&quot;&gt;
  &lt;div class=&quot;container-fluid&quot;&gt;
    &lt;button class=&quot;navbar-toggler ms-auto&quot; type=&quot;button&quot; data-bs-toggle=&quot;collapse&quot; data-bs-target=&quot;#navbar&quot;&gt;
      &lt;span class=&quot;navbar-toggler-icon&quot;&gt;&lt;/span&gt;
    &lt;/button&gt;

    &lt;div class=&quot;collapse navbar-collapse&quot; id=&quot;navbar&quot;&gt;
      &lt;!-- 검색창 --&gt;
      &lt;form class=&quot;d-flex&quot;&gt;
        &lt;input class=&quot;form-control me-2 input-animation&quot; type=&quot;search&quot; placeholder=&quot;Search&quot;&gt;
        &lt;button class=&quot;btn btn-outline-secondary&quot; type=&quot;submit&quot;&gt;Search&lt;/button&gt;
      &lt;/form&gt;

      &lt;!-- 우측 메뉴 --&gt;
      &lt;ul class=&quot;navbar-nav ms-auto mb-2 mb-lg-0&quot;&gt;
        &lt;li class=&quot;nav-item&quot;&gt;
          &lt;a class=&quot;nav-link position-relative&quot; href=&quot;#&quot;&gt;
            &lt;span class=&quot;badge bg-danger notification-badge&quot;&gt;5&lt;/span&gt;
            &lt;i class=&quot;fas fa-bell&quot;&gt;&lt;/i&gt;
          &lt;/a&gt;
        &lt;/li&gt;
        &lt;li class=&quot;nav-item&quot;&gt;
          &lt;a class=&quot;nav-link&quot; href=&quot;#&quot;&gt;
            &lt;i class=&quot;fas fa-envelope me-1&quot;&gt;&lt;/i&gt;
          &lt;/a&gt;
        &lt;/li&gt;
        &lt;li class=&quot;nav-item&quot;&gt;
          &lt;a class=&quot;nav-link&quot; href=&quot;#&quot;&gt;Minny Park&lt;/a&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;</code></pre>
<h3 id="bootstrap-유틸리티-클래스-활용">Bootstrap 유틸리티 클래스 활용</h3>
<p><strong>정렬 클래스:</strong></p>
<ul>
<li><code>ms-auto</code>: margin-start auto (오른쪽 정렬)</li>
<li><code>me-auto</code>: margin-end auto (왼쪽 정렬)</li>
<li><code>d-flex</code>: display flex</li>
<li><code>justify-content-between</code>: 양쪽 끝으로 정렬</li>
</ul>
<h3 id="알림-배지-스타일링">알림 배지 스타일링</h3>
<pre><code class="language-css">.nav-link {
  position: relative;
}

.notification-badge {
  position: absolute;
  top: -2px;
  left: 15px;
  font-size: 0.7rem;
  padding: 2px 6px;
  border-radius: 50%;
}</code></pre>
<h3 id="검색창-애니메이션">검색창 애니메이션</h3>
<pre><code class="language-css">.input-animation {
  width: 150px;
  transition: width 0.8s ease;
}

.input-animation:focus {
  width: 300px;
  outline: none;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}</code></pre>
<h2 id="📊-2-summary-카드-섹션">📊 2. Summary 카드 섹션</h2>
<h3 id="그리드-레이아웃">그리드 레이아웃</h3>
<pre><code class="language-html">&lt;div class=&quot;container&quot;&gt;
  &lt;h4 class=&quot;my-4&quot;&gt;Dashboard&lt;/h4&gt;
  &lt;div class=&quot;row g-4&quot;&gt;
    &lt;div class=&quot;col-lg-3 col-md-6&quot;&gt;
      &lt;div class=&quot;summary-card earnings&quot;&gt;
        &lt;div class=&quot;card&quot;&gt;
          &lt;div class=&quot;card-body d-flex justify-content-between align-items-center&quot;&gt;
            &lt;div&gt;
              &lt;p class=&quot;card-text text-muted mb-1&quot;&gt;Earnings (Monthly)&lt;/p&gt;
              &lt;h4 class=&quot;fw-bold text-primary&quot;&gt;$40,000&lt;/h4&gt;
            &lt;/div&gt;
            &lt;div class=&quot;icon-wrapper&quot;&gt;
              &lt;i class=&quot;fas fa-calendar fa-2x text-primary&quot;&gt;&lt;/i&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;col-lg-3 col-md-6&quot;&gt;
      &lt;div class=&quot;summary-card revenue&quot;&gt;
        &lt;div class=&quot;card border-left-success&quot;&gt;
          &lt;div class=&quot;card-body d-flex justify-content-between align-items-center&quot;&gt;
            &lt;div&gt;
              &lt;p class=&quot;card-text text-muted mb-1&quot;&gt;Revenue (Annual)&lt;/p&gt;
              &lt;h4 class=&quot;fw-bold text-success&quot;&gt;$215,000&lt;/h4&gt;
            &lt;/div&gt;
            &lt;div class=&quot;icon-wrapper&quot;&gt;
              &lt;i class=&quot;fas fa-dollar-sign fa-2x text-success&quot;&gt;&lt;/i&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;col-lg-3 col-md-6&quot;&gt;
      &lt;div class=&quot;summary-card tasks&quot;&gt;
        &lt;div class=&quot;card border-left-info&quot;&gt;
          &lt;div class=&quot;card-body d-flex justify-content-between align-items-center&quot;&gt;
            &lt;div&gt;
              &lt;p class=&quot;card-text text-muted mb-1&quot;&gt;Tasks&lt;/p&gt;
              &lt;h4 class=&quot;fw-bold text-info&quot;&gt;50%&lt;/h4&gt;
            &lt;/div&gt;
            &lt;div class=&quot;icon-wrapper&quot;&gt;
              &lt;i class=&quot;fas fa-clipboard-list fa-2x text-info&quot;&gt;&lt;/i&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;col-lg-3 col-md-6&quot;&gt;
      &lt;div class=&quot;summary-card requests&quot;&gt;
        &lt;div class=&quot;card border-left-warning&quot;&gt;
          &lt;div class=&quot;card-body d-flex justify-content-between align-items-center&quot;&gt;
            &lt;div&gt;
              &lt;p class=&quot;card-text text-muted mb-1&quot;&gt;Pending Requests&lt;/p&gt;
              &lt;h4 class=&quot;fw-bold text-warning&quot;&gt;18&lt;/h4&gt;
            &lt;/div&gt;
            &lt;div class=&quot;icon-wrapper&quot;&gt;
              &lt;i class=&quot;fas fa-comments fa-2x text-warning&quot;&gt;&lt;/i&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="카드-커스텀-스타일">카드 커스텀 스타일</h3>
<pre><code class="language-css">.summary-card .card {
  border: none;
  box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
  transition: all 0.3s ease;
}

.summary-card .card:hover {
  transform: translateY(-5px);
  box-shadow: 0 0.25rem 2rem 0 rgba(58, 59, 69, 0.2);
}

.border-left-success {
  border-left: 4px solid #1cc88a !important;
}

.border-left-info {
  border-left: 4px solid #36b9cc !important;
}

.border-left-warning {
  border-left: 4px solid #f6c23e !important;
}

.icon-wrapper {
  opacity: 0.7;
}</code></pre>
<h2 id="📈-3-차트--태스크-섹션">📈 3. 차트 &amp; 태스크 섹션</h2>
<h3 id="레이아웃-구조">레이아웃 구조</h3>
<pre><code class="language-html">&lt;div class=&quot;container mt-5&quot;&gt;
  &lt;div class=&quot;row&quot;&gt;
    &lt;!-- 차트 영역 --&gt;
    &lt;div class=&quot;col-lg-8&quot;&gt;
      &lt;div class=&quot;card&quot;&gt;
        &lt;div class=&quot;card-header d-flex justify-content-between align-items-center&quot;&gt;
          &lt;h6 class=&quot;fw-bold text-primary mb-0&quot;&gt;Earnings Overview&lt;/h6&gt;
          &lt;div class=&quot;dropdown&quot;&gt;
            &lt;button class=&quot;btn btn-sm btn-outline-secondary dropdown-toggle&quot; type=&quot;button&quot; data-bs-toggle=&quot;dropdown&quot;&gt;
              Monthly
            &lt;/button&gt;
            &lt;ul class=&quot;dropdown-menu&quot;&gt;
              &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&gt;Daily&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&gt;Weekly&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a class=&quot;dropdown-item&quot; href=&quot;#&quot;&gt;Monthly&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;card-body&quot;&gt;
          &lt;canvas id=&quot;myChart&quot; width=&quot;400&quot; height=&quot;200&quot;&gt;&lt;/canvas&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- 태스크 리스트 영역 --&gt;
    &lt;div class=&quot;col-lg-4&quot;&gt;
      &lt;div class=&quot;card&quot;&gt;
        &lt;div class=&quot;card-header&quot;&gt;
          &lt;h6 class=&quot;fw-bold text-primary mb-0&quot;&gt;Tasks&lt;/h6&gt;
        &lt;/div&gt;
        &lt;div class=&quot;card-body&quot;&gt;
          &lt;div class=&quot;task-item&quot;&gt;
            &lt;div class=&quot;form-check&quot;&gt;
              &lt;input class=&quot;form-check-input&quot; type=&quot;checkbox&quot; id=&quot;task1&quot;&gt;
              &lt;label class=&quot;form-check-label&quot; for=&quot;task1&quot;&gt;
                Design new landing page
              &lt;/label&gt;
            &lt;/div&gt;
            &lt;small class=&quot;text-muted&quot;&gt;Due: 2024-01-15&lt;/small&gt;
          &lt;/div&gt;

          &lt;div class=&quot;task-item&quot;&gt;
            &lt;div class=&quot;form-check&quot;&gt;
              &lt;input class=&quot;form-check-input&quot; type=&quot;checkbox&quot; id=&quot;task2&quot; checked&gt;
              &lt;label class=&quot;form-check-label text-muted&quot; for=&quot;task2&quot;&gt;
                &lt;s&gt;Update user documentation&lt;/s&gt;
              &lt;/label&gt;
            &lt;/div&gt;
            &lt;small class=&quot;text-muted&quot;&gt;Completed&lt;/small&gt;
          &lt;/div&gt;

          &lt;div class=&quot;task-item&quot;&gt;
            &lt;div class=&quot;form-check&quot;&gt;
              &lt;input class=&quot;form-check-input&quot; type=&quot;checkbox&quot; id=&quot;task3&quot;&gt;
              &lt;label class=&quot;form-check-label&quot; for=&quot;task3&quot;&gt;
                Fix mobile responsive issues
              &lt;/label&gt;
            &lt;/div&gt;
            &lt;small class=&quot;text-danger&quot;&gt;Overdue&lt;/small&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="태스크-아이템-스타일">태스크 아이템 스타일</h3>
<pre><code class="language-css">.task-item {
  padding: 12px 0;
  border-bottom: 1px solid #eaecf4;
}

.task-item:last-child {
  border-bottom: none;
}

.task-item .form-check-label {
  font-weight: 500;
  cursor: pointer;
}

.task-item small {
  display: block;
  margin-top: 4px;
}</code></pre>
<h2 id="🎨-4-슬라이딩-사이드바">🎨 4. 슬라이딩 사이드바</h2>
<h3 id="html-구조">HTML 구조</h3>
<pre><code class="language-html">&lt;body&gt;
  &lt;!-- 사이드바 --&gt;
  &lt;div class=&quot;sidebar&quot;&gt;
    &lt;div class=&quot;sidebar-header p-3&quot;&gt;
      &lt;h5 class=&quot;text-white mb-0&quot;&gt;
        &lt;i class=&quot;fas fa-tachometer-alt me-2&quot;&gt;&lt;/i&gt;
        &lt;span class=&quot;sidebar-text&quot;&gt;Admin&lt;/span&gt;
      &lt;/h5&gt;
    &lt;/div&gt;

    &lt;div class=&quot;sidebar-menu&quot;&gt;
      &lt;div class=&quot;menu-item&quot;&gt;
        &lt;i class=&quot;fas fa-home&quot;&gt;&lt;/i&gt;
        &lt;span class=&quot;sidebar-text&quot;&gt;Dashboard&lt;/span&gt;
      &lt;/div&gt;
      &lt;div class=&quot;menu-item&quot;&gt;
        &lt;i class=&quot;fas fa-users&quot;&gt;&lt;/i&gt;
        &lt;span class=&quot;sidebar-text&quot;&gt;Users&lt;/span&gt;
      &lt;/div&gt;
      &lt;div class=&quot;menu-item&quot;&gt;
        &lt;i class=&quot;fas fa-chart-bar&quot;&gt;&lt;/i&gt;
        &lt;span class=&quot;sidebar-text&quot;&gt;Analytics&lt;/span&gt;
      &lt;/div&gt;
      &lt;div class=&quot;menu-item&quot;&gt;
        &lt;i class=&quot;fas fa-cog&quot;&gt;&lt;/i&gt;
        &lt;span class=&quot;sidebar-text&quot;&gt;Settings&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- 메인 컨텐츠 --&gt;
  &lt;div class=&quot;main-content&quot;&gt;
    &lt;!-- 여기에 네비게이션과 대시보드 내용 --&gt;
  &lt;/div&gt;
&lt;/body&gt;</code></pre>
<h3 id="사이드바-스타일--애니메이션">사이드바 스타일 &amp; 애니메이션</h3>
<pre><code class="language-css">.sidebar {
  width: 200px;
  height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1000;
  transition: all 0.5s ease;
  transform: translateX(-150px);
  overflow: hidden;
}

.sidebar:hover {
  transform: translateX(0);
}

.sidebar-header {
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.menu-item {
  padding: 15px 20px;
  color: rgba(255, 255, 255, 0.8);
  cursor: pointer;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.menu-item:hover {
  background: rgba(255, 255, 255, 0.1);
  color: white;
  padding-left: 30px;
}

.menu-item i {
  width: 20px;
  margin-right: 15px;
  transition: all 0.5s ease;
  transform: translateX(120px);
}

.sidebar:hover .menu-item i {
  transform: translateX(0);
}

.sidebar-text {
  transition: all 0.5s ease;
  opacity: 0;
  transform: translateX(20px);
}

.sidebar:hover .sidebar-text {
  opacity: 1;
  transform: translateX(0);
}

.main-content {
  margin-left: 50px;
  transition: margin-left 0.5s ease;
  min-height: 100vh;
  background: #f8f9fc;
}

.sidebar:hover ~ .main-content {
  margin-left: 200px;
}</code></pre>
<h2 id="🔄-3d-transform-기초">🔄 3D Transform 기초</h2>
<h3 id="3d-회전축-이해">3D 회전축 이해</h3>
<pre><code class="language-css">.demo-box {
  width: 100px;
  height: 100px;
  background: #007bff;
  margin: 50px;
  transition: transform 0.5s ease;
}

/* X축 회전 (가로축 중심) */
.rotate-x:hover {
  transform: rotateX(180deg);
}

/* Y축 회전 (세로축 중심) */
.rotate-y:hover {
  transform: rotateY(180deg);
}

/* Z축 회전 (화면 수직축 중심) */
.rotate-z:hover {
  transform: rotateZ(180deg);
}

/* 복합 회전 */
.rotate-combo:hover {
  transform: rotateX(45deg) rotateY(45deg) rotateZ(45deg);
}</code></pre>
<h3 id="축-설명">축 설명</h3>
<ul>
<li><strong>X축 (가로)</strong>: 좌우로 그어진 가상의 선을 중심으로 회전</li>
<li><strong>Y축 (세로)</strong>: 위아래로 그어진 가상의 선을 중심으로 회전  </li>
<li><strong>Z축 (깊이)</strong>: 화면을 뚫고 나오는 가상의 선을 중심으로 회전</li>
</ul>
<h2 id="🃏-3d-flip-카드-제작">🃏 3D Flip 카드 제작</h2>
<h3 id="html-구조-1">HTML 구조</h3>
<pre><code class="language-html">&lt;div class=&quot;flip-container&quot;&gt;
  &lt;div class=&quot;flip-outer&quot;&gt;
    &lt;div class=&quot;flip-inner&quot;&gt;
      &lt;div class=&quot;front&quot;&gt;
        &lt;img src=&quot;profile.jpg&quot; alt=&quot;Profile&quot;&gt;
      &lt;/div&gt;
      &lt;div class=&quot;back&quot;&gt;
        &lt;div class=&quot;back-content&quot;&gt;
          &lt;h4&gt;개발자 김철용&lt;/h4&gt;
          &lt;p&gt;Frontend Developer&lt;/p&gt;
          &lt;p&gt;React, Vue, Angular&lt;/p&gt;
          &lt;div class=&quot;social-links&quot;&gt;
            &lt;a href=&quot;#&quot;&gt;&lt;i class=&quot;fab fa-github&quot;&gt;&lt;/i&gt;&lt;/a&gt;
            &lt;a href=&quot;#&quot;&gt;&lt;i class=&quot;fab fa-linkedin&quot;&gt;&lt;/i&gt;&lt;/a&gt;
            &lt;a href=&quot;#&quot;&gt;&lt;i class=&quot;fab fa-twitter&quot;&gt;&lt;/i&gt;&lt;/a&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="css-스타일링">CSS 스타일링</h3>
<pre><code class="language-css">.flip-container {
  perspective: 1000px;
  display: inline-block;
  margin: 20px;
}

.flip-outer {
  width: 300px;
  height: 300px;
}

.flip-inner {
  width: 100%;
  height: 100%;
  position: relative;
  transition: transform 0.8s ease;
  transform-style: preserve-3d;
  cursor: pointer;
}

.flip-inner:hover {
  transform: rotateY(180deg);
}

.front,
.back {
  width: 100%;
  height: 100%;
  position: absolute;
  backface-visibility: hidden;
  border-radius: 15px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  overflow: hidden;
}

.front {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  align-items: center;
  justify-content: center;
}

.front img {
  width: 80%;
  height: 80%;
  object-fit: cover;
  border-radius: 50%;
  border: 5px solid white;
}

.back {
  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  transform: rotateY(180deg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  text-align: center;
}

.back-content h4 {
  font-size: 1.5rem;
  margin-bottom: 10px;
  font-weight: bold;
}

.back-content p {
  margin-bottom: 8px;
  opacity: 0.9;
}

.social-links {
  margin-top: 20px;
}

.social-links a {
  color: white;
  font-size: 1.5rem;
  margin: 0 10px;
  transition: transform 0.3s ease;
}

.social-links a:hover {
  transform: scale(1.2);
}</code></pre>
<h3 id="핵심-속성-설명">핵심 속성 설명</h3>
<h4 id="transform-style-preserve-3d"><code>transform-style: preserve-3d</code></h4>
<ul>
<li>3D 변환을 자식 요소에도 적용</li>
<li>없으면 평면적으로 보임</li>
</ul>
<h4 id="backface-visibility-hidden"><code>backface-visibility: hidden</code></h4>
<ul>
<li>뒷면이 보이지 않도록 설정</li>
<li>카드 뒤집기 효과의 핵심</li>
</ul>
<h4 id="perspective"><code>perspective</code></h4>
<ul>
<li>3D 효과의 깊이감 설정</li>
<li>값이 클수록 멀리서 보는 효과</li>
</ul>
<h2 id="🎨-고급-3d-효과">🎨 고급 3D 효과</h2>
<h3 id="다중-카드-그리드">다중 카드 그리드</h3>
<pre><code class="language-html">&lt;div class=&quot;card-grid&quot;&gt;
  &lt;div class=&quot;flip-container&quot;&gt;
    &lt;div class=&quot;flip-outer&quot;&gt;
      &lt;div class=&quot;flip-inner&quot;&gt;
        &lt;div class=&quot;front team-card&quot;&gt;
          &lt;img src=&quot;team1.jpg&quot; alt=&quot;Team Member 1&quot;&gt;
          &lt;h5&gt;Alice Johnson&lt;/h5&gt;
        &lt;/div&gt;
        &lt;div class=&quot;back&quot;&gt;
          &lt;div class=&quot;back-content&quot;&gt;
            &lt;h4&gt;Alice Johnson&lt;/h4&gt;
            &lt;p&gt;UI/UX Designer&lt;/p&gt;
            &lt;p&gt;5년 경력&lt;/p&gt;
            &lt;p&gt;Figma, Sketch, Adobe XD&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- 더 많은 카드들... --&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 30px;
  padding: 40px;
  max-width: 1200px;
  margin: 0 auto;
}

.team-card {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding: 30px;
  text-align: center;
  color: white;
}

.team-card img {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  margin-bottom: 20px;
  border: 4px solid white;
}

.team-card h5 {
  margin: 0;
  font-weight: 600;
}</code></pre>
<h3 id="호버-시-전체-그리드-효과">호버 시 전체 그리드 효과</h3>
<pre><code class="language-css">.card-grid:hover .flip-container:not(:hover) {
  opacity: 0.7;
  transform: scale(0.95);
}

.flip-container {
  transition: all 0.3s ease;
}</code></pre>
<h2 id="📱-반응형-대응">📱 반응형 대응</h2>
<h3 id="모바일-최적화">모바일 최적화</h3>
<pre><code class="language-css">@media (max-width: 768px) {
  .sidebar {
    width: 100%;
    height: auto;
    position: static;
    transform: none;
  }

  .sidebar:hover {
    transform: none;
  }

  .main-content {
    margin-left: 0;
  }

  .flip-outer {
    width: 250px;
    height: 250px;
  }

  .card-grid {
    grid-template-columns: 1fr;
    padding: 20px;
  }

  .input-animation:focus {
    width: 200px;
  }
}

@media (max-width: 480px) {
  .flip-outer {
    width: 200px;
    height: 200px;
  }

  .summary-card .card-body {
    flex-direction: column;
    text-align: center;
  }

  .icon-wrapper {
    margin-top: 15px;
  }
}</code></pre>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="성능-최적화">성능 최적화</h3>
<pre><code class="language-css">/* GPU 가속 활용 */
.flip-inner {
  will-change: transform;
}

/* 애니메이션 완료 후 will-change 제거 */
.flip-inner.animation-complete {
  will-change: auto;
}</code></pre>
<h3 id="접근성-고려">접근성 고려</h3>
<pre><code class="language-html">&lt;!-- 키보드 네비게이션 지원 --&gt;
&lt;div class=&quot;flip-inner&quot; tabindex=&quot;0&quot; role=&quot;button&quot; aria-label=&quot;프로필 카드 뒤집기&quot;&gt;
  &lt;!-- 카드 내용 --&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.flip-inner:focus {
  outline: 3px solid #007bff;
  outline-offset: 2px;
}

.flip-inner:focus,
.flip-inner:hover {
  transform: rotateY(180deg);
}</code></pre>
<h3 id="브라우저-호환성">브라우저 호환성</h3>
<pre><code class="language-css">/* 구형 브라우저 대응 */
.flip-inner {
  -webkit-transform-style: preserve-3d;
  -moz-transform-style: preserve-3d;
  transform-style: preserve-3d;

  -webkit-transition: transform 0.8s ease;
  -moz-transition: transform 0.8s ease;
  transition: transform 0.8s ease;
}

.front,
.back {
  -webkit-backface-visibility: hidden;
  -moz-backface-visibility: hidden;
  backface-visibility: hidden;
}</code></pre>
<p>Admin Dashboard와 인상적인 3D Flip 카드를 만들어보았습니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS-10] Grid & Position Sticky]]></title>
            <link>https://velog.io/@comely_15/CSS-10-Grid-Position-Sticky</link>
            <guid>https://velog.io/@comely_15/CSS-10-Grid-Position-Sticky</guid>
            <pubDate>Fri, 06 Jun 2025 17:46:31 GMT</pubDate>
            <description><![CDATA[<h2 id="🏗️-css-grid-기초">🏗️ CSS Grid 기초</h2>
<h3 id="브라우저-호환성">브라우저 호환성</h3>
<table>
<thead>
<tr>
<th>브라우저</th>
<th>지원 버전</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chrome</strong></td>
<td>57+ ✅</td>
</tr>
<tr>
<td><strong>Firefox</strong></td>
<td>52+ ✅</td>
</tr>
<tr>
<td><strong>Safari</strong></td>
<td>10.1+ ✅</td>
</tr>
<tr>
<td><strong>Edge</strong></td>
<td>16+ ✅</td>
</tr>
<tr>
<td><strong>IE</strong></td>
<td>❌ (구 문법으로만 부분 지원)</td>
</tr>
</tbody></table>
<p><strong>참고</strong>: <a href="https://caniuse.com">caniuse.com</a>에서 정확한 호환성 정보 확인 가능</p>
<h3 id="grid-기본-개념">Grid 기본 개념</h3>
<p>CSS Grid는 <strong>격자(Grid) 기반 레이아웃 시스템</strong>으로, 복잡한 2차원 레이아웃을 쉽게 구현할 수 있습니다.</p>
<h2 id="📐-기본-grid-레이아웃">📐 기본 Grid 레이아웃</h2>
<h3 id="간단한-grid-생성">간단한 Grid 생성</h3>
<pre><code class="language-html">&lt;div class=&quot;grid-container&quot;&gt;
  &lt;div class=&quot;item&quot;&gt;1&lt;/div&gt;
  &lt;div class=&quot;item&quot;&gt;2&lt;/div&gt;
  &lt;div class=&quot;item&quot;&gt;3&lt;/div&gt;
  &lt;div class=&quot;item&quot;&gt;4&lt;/div&gt;
  &lt;div class=&quot;item&quot;&gt;5&lt;/div&gt;
  &lt;div class=&quot;item&quot;&gt;6&lt;/div&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.grid-container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;  /* 4개 컬럼 */
  grid-template-rows: 100px 100px 100px;   /* 3개 행 */
  grid-gap: 10px;                         /* 간격 */
  padding: 20px;
}

.item {
  background: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
}</code></pre>
<h3 id="grid-단위-설명">Grid 단위 설명</h3>
<pre><code class="language-css">.grid-example {
  display: grid;

  /* 다양한 단위 사용 */
  grid-template-columns: 
    200px          /* 고정 크기 */
    1fr            /* 비율 (1배) */
    2fr            /* 비율 (2배) */
    minmax(100px, 300px)  /* 최소/최대 크기 */
    auto;          /* 내용에 맞춤 */

  /* repeat 함수 사용 */
  grid-template-columns: repeat(4, 1fr);      /* 1fr을 4번 반복 */
  grid-template-columns: repeat(3, 200px);    /* 200px을 3번 반복 */
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* 반응형 */
}</code></pre>
<h2 id="🎯-grid-레이아웃-방법-1-아이템-크기-조정">🎯 Grid 레이아웃 방법 1: 아이템 크기 조정</h2>
<h3 id="그리드-라인-기반-배치">그리드 라인 기반 배치</h3>
<pre><code class="language-html">&lt;div class=&quot;layout-container&quot;&gt;
  &lt;div class=&quot;header&quot;&gt;헤더&lt;/div&gt;
  &lt;div class=&quot;sidebar&quot;&gt;사이드바&lt;/div&gt;
  &lt;div class=&quot;main&quot;&gt;메인 컨텐츠&lt;/div&gt;
  &lt;div class=&quot;footer&quot;&gt;푸터&lt;/div&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.layout-container {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: 80px 400px 80px;
  grid-gap: 10px;
  min-height: 100vh;
}

.header {
  grid-column: 1 / 5;  /* 컬럼 1번 라인부터 5번 라인까지 */
  background: #333;
  color: white;
}

.sidebar {
  grid-column: 1 / 2;  /* 컬럼 1번 라인부터 2번 라인까지 */
  grid-row: 2 / 3;     /* 행 2번 라인부터 3번 라인까지 */
  background: #f8f9fa;
}

.main {
  grid-column: 2 / 5;  /* 컬럼 2번 라인부터 5번 라인까지 */
  grid-row: 2 / 3;
  background: white;
  padding: 20px;
}

.footer {
  grid-column: 1 / 5;
  background: #6c757d;
  color: white;
}</code></pre>
<h3 id="그리드-라인-번호-이해">그리드 라인 번호 이해</h3>
<pre><code>  1   2   3   4   5  ← 세로 라인 번호
1 ┌───┬───┬───┬───┐
  │   │   │   │   │
2 ├───┼───┼───┼───┤
  │   │   │   │   │
3 ├───┼───┼───┼───┤
  │   │   │   │   │
4 └───┴───┴───┴───┘
  ↑ 가로 라인 번호</code></pre><h3 id="단축-속성">단축 속성</h3>
<pre><code class="language-css">.item {
  /* 기본 방식 */
  grid-column-start: 1;
  grid-column-end: 3;
  grid-row-start: 1;
  grid-row-end: 2;

  /* 단축 속성 */
  grid-column: 1 / 3;
  grid-row: 1 / 2;

  /* span 키워드 사용 */
  grid-column: span 2;  /* 2칸 차지 */
  grid-row: span 1;     /* 1칸 차지 */
}</code></pre>
<h2 id="🏷️-grid-레이아웃-방법-2-영역-이름-지정">🏷️ Grid 레이아웃 방법 2: 영역 이름 지정</h2>
<h3 id="grid-areas-사용">Grid Areas 사용</h3>
<pre><code class="language-html">&lt;div class=&quot;page-layout&quot;&gt;
  &lt;header class=&quot;page-header&quot;&gt;헤더&lt;/header&gt;
  &lt;aside class=&quot;page-sidebar&quot;&gt;사이드바&lt;/aside&gt;
  &lt;main class=&quot;page-main&quot;&gt;메인 컨텐츠&lt;/main&gt;
  &lt;footer class=&quot;page-footer&quot;&gt;푸터&lt;/footer&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.page-layout {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: 80px 1fr 60px;
  grid-template-areas: 
    &quot;header  header  header&quot;
    &quot;sidebar main    aside&quot;
    &quot;footer  footer  footer&quot;;
  min-height: 100vh;
  grid-gap: 10px;
}

.page-header {
  grid-area: header;
  background: #007bff;
  color: white;
}

.page-sidebar {
  grid-area: sidebar;
  background: #f8f9fa;
}

.page-main {
  grid-area: main;
  background: white;
  padding: 20px;
}

.page-footer {
  grid-area: footer;
  background: #6c757d;
  color: white;
}</code></pre>
<h3 id="빈-공간-표시">빈 공간 표시</h3>
<pre><code class="language-css">.grid-with-gaps {
  grid-template-areas: 
    &quot;header header header header&quot;
    &quot;sidebar main main .&quot;        /* . = 빈 공간 */
    &quot;sidebar main main .&quot;
    &quot;footer footer footer footer&quot;;
}</code></pre>
<h2 id="📱-반응형-grid-레이아웃">📱 반응형 Grid 레이아웃</h2>
<h3 id="기본-반응형-패턴">기본 반응형 패턴</h3>
<pre><code class="language-css">.responsive-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  grid-gap: 20px;
  padding: 20px;
}

/* 수동 반응형 */
@media (max-width: 768px) {
  .page-layout {
    grid-template-columns: 1fr;
    grid-template-areas: 
      &quot;header&quot;
      &quot;main&quot;
      &quot;sidebar&quot;
      &quot;footer&quot;;
  }
}

@media (min-width: 769px) and (max-width: 1024px) {
  .page-layout {
    grid-template-columns: 150px 1fr;
    grid-template-areas: 
      &quot;header header&quot;
      &quot;sidebar main&quot;
      &quot;footer footer&quot;;
  }
}</code></pre>
<h2 id="🖼️-실전-예제-이미지-갤러리">🖼️ 실전 예제: 이미지 갤러리</h2>
<h3 id="html-구조">HTML 구조</h3>
<pre><code class="language-html">&lt;div class=&quot;gallery-grid&quot;&gt;
  &lt;div class=&quot;gallery-item featured&quot;&gt;
    &lt;img src=&quot;https://picsum.photos/600/400?random=1&quot; alt=&quot;Featured Image&quot;&gt;
  &lt;/div&gt;
  &lt;div class=&quot;gallery-item&quot;&gt;
    &lt;img src=&quot;https://picsum.photos/300/300?random=2&quot; alt=&quot;Image 2&quot;&gt;
  &lt;/div&gt;
  &lt;div class=&quot;gallery-item&quot;&gt;
    &lt;img src=&quot;https://picsum.photos/300/300?random=3&quot; alt=&quot;Image 3&quot;&gt;
  &lt;/div&gt;
  &lt;div class=&quot;gallery-item&quot;&gt;
    &lt;img src=&quot;https://picsum.photos/300/300?random=4&quot; alt=&quot;Image 4&quot;&gt;
  &lt;/div&gt;
  &lt;div class=&quot;gallery-item&quot;&gt;
    &lt;img src=&quot;https://picsum.photos/300/300?random=5&quot; alt=&quot;Image 5&quot;&gt;
  &lt;/div&gt;
  &lt;div class=&quot;gallery-item&quot;&gt;
    &lt;img src=&quot;https://picsum.photos/300/300?random=6&quot; alt=&quot;Image 6&quot;&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="css-스타일">CSS 스타일</h3>
<pre><code class="language-css">.gallery-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 200px;
  grid-gap: 15px;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.gallery-item {
  background: #f8f9fa;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s ease;
}

.gallery-item:hover {
  transform: translateY(-5px);
}

.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* 첫 번째 이미지를 2x2로 확대 */
.gallery-item.featured {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}

/* 반응형 */
@media (max-width: 768px) {
  .gallery-grid {
    grid-template-columns: 1fr;
    grid-auto-rows: 250px;
  }

  .gallery-item.featured {
    grid-column: 1 / 2;
    grid-row: 1 / 2;
  }
}

@media (min-width: 769px) and (max-width: 1024px) {
  .gallery-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .gallery-item.featured {
    grid-column: 1 / 3;
    grid-row: 1 / 2;
  }
}</code></pre>
<h2 id="🏢-실전-예제-블로그-레이아웃">🏢 실전 예제: 블로그 레이아웃</h2>
<pre><code class="language-html">&lt;div class=&quot;blog-layout&quot;&gt;
  &lt;header class=&quot;blog-header&quot;&gt;
    &lt;h1&gt;My Blog&lt;/h1&gt;
    &lt;nav&gt;Navigation Menu&lt;/nav&gt;
  &lt;/header&gt;
  &lt;aside class=&quot;blog-sidebar&quot;&gt;
    &lt;h3&gt;Sidebar&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;Recent Posts&lt;/li&gt;
      &lt;li&gt;Categories&lt;/li&gt;
      &lt;li&gt;Archives&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/aside&gt;
  &lt;main class=&quot;blog-content&quot;&gt;
    &lt;article&gt;
      &lt;h2&gt;Blog Post Title&lt;/h2&gt;
      &lt;p&gt;Blog post content goes here...&lt;/p&gt;
    &lt;/article&gt;
  &lt;/main&gt;
  &lt;footer class=&quot;blog-footer&quot;&gt;
    &lt;p&gt;&amp;copy; 2024 My Blog. All rights reserved.&lt;/p&gt;
  &lt;/footer&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.blog-layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas: 
    &quot;header header&quot;
    &quot;sidebar content&quot;
    &quot;footer footer&quot;;
  min-height: 100vh;
  grid-gap: 20px;
  padding: 20px;
}

.blog-header {
  grid-area: header;
  background: #343a40;
  color: white;
  padding: 20px;
  border-radius: 8px;
}

.blog-sidebar {
  grid-area: sidebar;
  background: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
}

.blog-content {
  grid-area: content;
  background: white;
  padding: 30px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.blog-footer {
  grid-area: footer;
  background: #6c757d;
  color: white;
  padding: 15px;
  border-radius: 8px;
  text-align: center;
}

/* 태블릿 */
@media (max-width: 1024px) {
  .blog-layout {
    grid-template-columns: 200px 1fr;
  }
}

/* 모바일 */
@media (max-width: 768px) {
  .blog-layout {
    grid-template-columns: 1fr;
    grid-template-areas: 
      &quot;header&quot;
      &quot;content&quot;
      &quot;sidebar&quot;
      &quot;footer&quot;;
  }
}</code></pre>
<h2 id="📌-position-sticky">📌 Position Sticky</h2>
<h3 id="기본-개념">기본 개념</h3>
<p><code>position: sticky</code>는 <strong>스크롤 기반 고정 요소</strong>를 만드는 속성입니다.</p>
<h3 id="브라우저-지원">브라우저 지원</h3>
<table>
<thead>
<tr>
<th>브라우저</th>
<th>지원 버전</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Chrome</strong></td>
<td>56+ ✅</td>
</tr>
<tr>
<td><strong>Firefox</strong></td>
<td>32+ ✅</td>
</tr>
<tr>
<td><strong>Safari</strong></td>
<td>6.1+ ✅</td>
</tr>
<tr>
<td><strong>Edge</strong></td>
<td>16+ ✅</td>
</tr>
<tr>
<td><strong>IE</strong></td>
<td>❌</td>
</tr>
</tbody></table>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-html">&lt;body style=&quot;height: 3000px; background: #f0f0f0;&quot;&gt;
  &lt;div class=&quot;content-section&quot;&gt;
    &lt;div class=&quot;sticky-sidebar&quot;&gt;
      &lt;img src=&quot;product.jpg&quot; alt=&quot;Product Image&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;main-text&quot;&gt;
      &lt;h2&gt;Product Description&lt;/h2&gt;
      &lt;p&gt;Long product description that requires scrolling...&lt;/p&gt;
      &lt;!-- 많은 텍스트 내용 --&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/body&gt;</code></pre>
<pre><code class="language-css">.content-section {
  background: white;
  max-width: 1200px;
  margin: 100px auto;
  padding: 40px;
  display: flex;
  gap: 40px;
  min-height: 2000px; /* 스크롤 가능하도록 */
}

.sticky-sidebar {
  flex: 0 0 400px;
  position: sticky;
  top: 20px;          /* viewport 상단에서 20px 위치에 고정 */
  height: fit-content;
}

.sticky-sidebar img {
  width: 100%;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.main-text {
  flex: 1;
  line-height: 1.8;
}</code></pre>
<h3 id="sticky-동작-원리">Sticky 동작 원리</h3>
<ol>
<li><strong>일반 상태</strong>: 요소가 원래 위치에 있음</li>
<li><strong>스크롤 시</strong>: 지정된 위치(<code>top</code>, <code>bottom</code> 등)에 도달하면 고정</li>
<li><strong>부모 범위</strong>: 부모 요소를 벗어나면 다시 일반 요소로 동작</li>
</ol>
<h3 id="고급-sticky-예제">고급 Sticky 예제</h3>
<h4 id="네비게이션-메뉴">네비게이션 메뉴</h4>
<pre><code class="language-html">&lt;header class=&quot;main-header&quot;&gt;
  &lt;h1&gt;Website Header&lt;/h1&gt;
&lt;/header&gt;

&lt;nav class=&quot;sticky-nav&quot;&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a href=&quot;#home&quot;&gt;Home&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#about&quot;&gt;About&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#services&quot;&gt;Services&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#contact&quot;&gt;Contact&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/nav&gt;

&lt;main class=&quot;main-content&quot;&gt;
  &lt;!-- 페이지 내용 --&gt;
&lt;/main&gt;</code></pre>
<pre><code class="language-css">.main-header {
  background: #343a40;
  color: white;
  padding: 60px 20px;
  text-align: center;
}

.sticky-nav {
  position: sticky;
  top: 0;
  background: white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  z-index: 100;
}

.sticky-nav ul {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: 1200px;
  margin: 0 auto;
}

.sticky-nav li {
  flex: 1;
}

.sticky-nav a {
  display: block;
  padding: 20px;
  text-decoration: none;
  color: #333;
  text-align: center;
  transition: background 0.3s ease;
}

.sticky-nav a:hover {
  background: #f8f9fa;
}

.main-content {
  max-width: 1200px;
  margin: 0 auto;
  padding: 40px 20px;
  min-height: 2000px;
}</code></pre>
<h4 id="사이드바-목차">사이드바 목차</h4>
<pre><code class="language-html">&lt;div class=&quot;article-layout&quot;&gt;
  &lt;aside class=&quot;table-of-contents&quot;&gt;
    &lt;h3&gt;목차&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#section1&quot;&gt;섹션 1&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#section2&quot;&gt;섹션 2&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#section3&quot;&gt;섹션 3&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#section4&quot;&gt;섹션 4&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/aside&gt;

  &lt;article class=&quot;article-content&quot;&gt;
    &lt;section id=&quot;section1&quot;&gt;
      &lt;h2&gt;섹션 1&lt;/h2&gt;
      &lt;p&gt;섹션 1의 내용...&lt;/p&gt;
    &lt;/section&gt;
    &lt;!-- 더 많은 섹션들 --&gt;
  &lt;/article&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.article-layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  gap: 40px;
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.table-of-contents {
  position: sticky;
  top: 20px;
  height: fit-content;
  background: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #e9ecef;
}

.table-of-contents h3 {
  margin-top: 0;
  color: #495057;
}

.table-of-contents ul {
  list-style: none;
  padding: 0;
}

.table-of-contents li {
  margin-bottom: 8px;
}

.table-of-contents a {
  color: #007bff;
  text-decoration: none;
  padding: 8px 0;
  display: block;
  border-radius: 4px;
  padding-left: 12px;
  transition: all 0.3s ease;
}

.table-of-contents a:hover {
  background: #e9ecef;
  color: #0056b3;
}

.article-content {
  line-height: 1.8;
}

.article-content section {
  margin-bottom: 60px;
  min-height: 600px;
}

@media (max-width: 768px) {
  .article-layout {
    grid-template-columns: 1fr;
  }

  .table-of-contents {
    position: static;
    order: -1;
  }
}</code></pre>
<h2 id="🎯-grid-vs-flexbox-비교">🎯 Grid vs Flexbox 비교</h2>
<h3 id="언제-grid를-사용할까">언제 Grid를 사용할까?</h3>
<pre><code class="language-css">/* Grid가 적합한 경우 */
.grid-suitable {
  /* 2차원 레이아웃 (행과 열 모두 제어) */
  display: grid;
  grid-template-areas: 
    &quot;header header header&quot;
    &quot;sidebar main aside&quot;
    &quot;footer footer footer&quot;;
}

/* Flexbox가 적합한 경우 */
.flex-suitable {
  /* 1차원 레이아웃 (한 방향만 제어) */
  display: flex;
  justify-content: space-between;
  align-items: center;
}</code></pre>
<h3 id="특징-비교">특징 비교</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>Grid</th>
<th>Flexbox</th>
</tr>
</thead>
<tbody><tr>
<td><strong>차원</strong></td>
<td>2차원 (행 + 열)</td>
<td>1차원 (행 또는 열)</td>
</tr>
<tr>
<td><strong>용도</strong></td>
<td>전체 레이아웃</td>
<td>컴포넌트 내부 정렬</td>
</tr>
<tr>
<td><strong>복잡성</strong></td>
<td>복잡한 레이아웃</td>
<td>간단한 정렬</td>
</tr>
<tr>
<td><strong>반응형</strong></td>
<td>강력한 반응형 기능</td>
<td>유연한 크기 조절</td>
</tr>
</tbody></table>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="grid-네이밍-규칙">Grid 네이밍 규칙</h3>
<pre><code class="language-css">.layout-grid {
  grid-template-areas: 
    &quot;header header header&quot;
    &quot;nav main aside&quot;
    &quot;footer footer footer&quot;;
}

/* 명확한 이름 사용 */
.page-header { grid-area: header; }
.page-nav { grid-area: nav; }
.page-main { grid-area: main; }
.page-aside { grid-area: aside; }
.page-footer { grid-area: footer; }</code></pre>
<h3 id="성능-최적화">성능 최적화</h3>
<pre><code class="language-css">/* 불필요한 재계산 방지 */
.grid-container {
  contain: layout style;
}

/* 이미지 최적화 */
.grid-item img {
  object-fit: cover;
  width: 100%;
  height: 100%;
}</code></pre>
<h3 id="접근성-고려">접근성 고려</h3>
<pre><code class="language-css">/* 스크린 리더를 위한 순서 유지 */
.accessible-grid {
  display: grid;
  grid-template-areas: 
    &quot;header&quot;
    &quot;main&quot;
    &quot;sidebar&quot;
    &quot;footer&quot;;
}

@media (min-width: 768px) {
  .accessible-grid {
    grid-template-areas: 
      &quot;header header&quot;
      &quot;sidebar main&quot;
      &quot;footer footer&quot;;
  }
}</code></pre>
<p>CSS Grid와 Position Sticky을 활용하여 반응형인 웹 레이아웃을 구현할 수 있습니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS-09] 비디오 & 애니메이션]]></title>
            <link>https://velog.io/@comely_15/CSS-09-%EB%B9%84%EB%94%94%EC%98%A4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@comely_15/CSS-09-%EB%B9%84%EB%94%94%EC%98%A4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98</guid>
            <pubDate>Fri, 06 Jun 2025 17:43:59 GMT</pubDate>
            <description><![CDATA[<h2 id="🎬-html-비디오-태그">🎬 HTML 비디오 태그</h2>
<h3 id="기본-비디오-삽입">기본 비디오 삽입</h3>
<pre><code class="language-html">&lt;video&gt;
  &lt;source src=&quot;video/sample.mp4&quot; type=&quot;video/mp4&quot;&gt;
  &lt;source src=&quot;video/sample.webm&quot; type=&quot;video/webm&quot;&gt;
  &lt;source src=&quot;video/sample.ogg&quot; type=&quot;video/ogg&quot;&gt;
  브라우저가 비디오를 지원하지 않습니다.
&lt;/video&gt;</code></pre>
<p><strong>멀티 포맷 지원</strong>: 여러 형식을 제공하면 브라우저가 최적화된 포맷을 자동 선택</p>
<h3 id="비디오-속성">비디오 속성</h3>
<pre><code class="language-html">&lt;video 
  autoplay 
  muted 
  loop 
  controls
  poster=&quot;thumbnail.jpg&quot; 
  preload=&quot;metadata&quot;
  width=&quot;800&quot; 
  height=&quot;450&quot;
&gt;
  &lt;source src=&quot;video.mp4&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;</code></pre>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
<th>참고사항</th>
</tr>
</thead>
<tbody><tr>
<td><code>autoplay</code></td>
<td>자동 재생</td>
<td><strong>muted와 함께 사용해야 작동</strong></td>
</tr>
<tr>
<td><code>muted</code></td>
<td>음소거 상태</td>
<td>자동재생 정책상 필수</td>
</tr>
<tr>
<td><code>loop</code></td>
<td>반복 재생</td>
<td>배경 비디오에 유용</td>
</tr>
<tr>
<td><code>controls</code></td>
<td>재생 컨트롤 표시</td>
<td>사용자 제어 가능</td>
</tr>
<tr>
<td><code>poster</code></td>
<td>썸네일 이미지</td>
<td>비디오 로드 전 표시</td>
</tr>
<tr>
<td><code>preload</code></td>
<td>사전 로딩 설정</td>
<td><code>auto</code>, <code>metadata</code>, <code>none</code></td>
</tr>
</tbody></table>
<h3 id="preload-옵션">Preload 옵션</h3>
<pre><code class="language-html">&lt;!-- 전체 비디오 미리 다운로드 --&gt;
&lt;video preload=&quot;auto&quot;&gt;
  &lt;source src=&quot;video.mp4&quot;&gt;
&lt;/video&gt;

&lt;!-- 메타데이터만 미리 로드 (권장) --&gt;
&lt;video preload=&quot;metadata&quot;&gt;
  &lt;source src=&quot;video.mp4&quot;&gt;
&lt;/video&gt;

&lt;!-- 사용자가 재생할 때까지 로드하지 않음 --&gt;
&lt;video preload=&quot;none&quot;&gt;
  &lt;source src=&quot;video.mp4&quot;&gt;
&lt;/video&gt;</code></pre>
<h2 id="📺-배경-비디오-구현">📺 배경 비디오 구현</h2>
<h3 id="html-구조">HTML 구조</h3>
<pre><code class="language-html">&lt;div class=&quot;video-box&quot;&gt;
  &lt;video class=&quot;video-container&quot; autoplay muted loop&gt;
    &lt;source src=&quot;video/background.mp4&quot; type=&quot;video/mp4&quot;&gt;
    &lt;source src=&quot;video/background.webm&quot; type=&quot;video/webm&quot;&gt;
  &lt;/video&gt;
  &lt;div class=&quot;video-content&quot;&gt;
    &lt;h1&gt;Welcome to Our Site&lt;/h1&gt;
    &lt;p&gt;Amazing content over video background&lt;/p&gt;
    &lt;button class=&quot;cta-button&quot;&gt;Get Started&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="css-스타일링">CSS 스타일링</h3>
<pre><code class="language-css">.video-box {
  position: relative;
  height: 100vh;
  width: 100%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.video-container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  min-width: 100%;
  min-height: 100%;
  width: auto;
  height: auto;
  z-index: -1;
  background-size: cover;
}

.video-content {
  position: relative;
  z-index: 1;
  text-align: center;
  color: white;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}

.video-content h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
}

.video-content p {
  font-size: 1.2rem;
  margin-bottom: 2rem;
}

.cta-button {
  padding: 15px 30px;
  font-size: 1.1rem;
  background: rgba(255, 255, 255, 0.2);
  color: white;
  border: 2px solid white;
  border-radius: 5px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.cta-button:hover {
  background: white;
  color: #333;
}</code></pre>
<h3 id="반응형-배경-비디오">반응형 배경 비디오</h3>
<pre><code class="language-css">@media (max-width: 768px) {
  .video-container {
    /* 모바일에서는 이미지로 대체 */
    display: none;
  }

  .video-box {
    background-image: url(&#39;video-poster.jpg&#39;);
    background-size: cover;
    background-position: center;
  }

  .video-content h1 {
    font-size: 2rem;
  }
}</code></pre>
<h2 id="🎨-transform-속성">🎨 Transform 속성</h2>
<h3 id="기본-transform-속성들">기본 Transform 속성들</h3>
<pre><code class="language-css">.transform-demo {
  /* 회전 */
  transform: rotate(45deg);

  /* 이동 */
  transform: translate(50px, 100px);
  transform: translateX(50px);
  transform: translateY(100px);

  /* 크기 조절 */
  transform: scale(1.5);
  transform: scaleX(2);
  transform: scaleY(0.5);

  /* 비틀기 */
  transform: skew(15deg, 5deg);
  transform: skewX(15deg);
  transform: skewY(5deg);

  /* 여러 속성 조합 */
  transform: rotate(45deg) scale(1.2) translateX(20px);
}</code></pre>
<h3 id="transform의-장점">Transform의 장점</h3>
<pre><code class="language-css">/* 나쁜 예: 레이아웃 재계산 발생 */
.bad-animation {
  transition: margin-left 0.3s;
}
.bad-animation:hover {
  margin-left: 100px; /* 레이아웃 재계산 */
}

/* 좋은 예: GPU 가속 사용 */
.good-animation {
  transition: transform 0.3s;
}
.good-animation:hover {
  transform: translateX(100px); /* 효율적 */
}</code></pre>
<h2 id="🎬-keyframes-애니메이션">🎬 Keyframes 애니메이션</h2>
<h3 id="기본-문법">기본 문법</h3>
<pre><code class="language-css">/* 1. 키프레임 정의 */
@keyframes slideIn {
  0% {
    transform: translateX(-100px);
    opacity: 0;
  }
  50% {
    transform: translateX(20px);
    opacity: 0.8;
  }
  100% {
    transform: translateX(0);
    opacity: 1;
  }
}

/* 2. 애니메이션 적용 */
.slide-element {
  animation: slideIn 0.8s ease-out;
}</code></pre>
<h3 id="animation-속성-상세">Animation 속성 상세</h3>
<pre><code class="language-css">.complex-animation {
  animation-name: myAnimation;           /* 키프레임 이름 */
  animation-duration: 2s;                /* 지속 시간 */
  animation-timing-function: ease-in-out; /* 타이밍 함수 */
  animation-delay: 0.5s;                 /* 시작 지연 */
  animation-iteration-count: 3;          /* 반복 횟수 */
  animation-direction: alternate;        /* 방향 */
  animation-fill-mode: forwards;         /* 종료 후 상태 */
  animation-play-state: running;         /* 재생/일시정지 */

  /* 축약형 */
  animation: myAnimation 2s ease-in-out 0.5s 3 alternate forwards;
}</code></pre>
<h3 id="animation-fill-mode">Animation Fill Mode</h3>
<pre><code class="language-css">/* 애니메이션 종료 후 원래 상태로 복귀 (기본값) */
.animation-none {
  animation-fill-mode: none;
}

/* 애니메이션 종료 후 마지막 상태 유지 */
.animation-forwards {
  animation-fill-mode: forwards;
}

/* 애니메이션 시작 전 첫 번째 상태 적용 */
.animation-backwards {
  animation-fill-mode: backwards;
}

/* 시작 전과 종료 후 모두 적용 */
.animation-both {
  animation-fill-mode: both;
}</code></pre>
<h2 id="🚀-실전-애니메이션-예제">🚀 실전 애니메이션 예제</h2>
<h3 id="1-흔들리는-버튼">1. 흔들리는 버튼</h3>
<pre><code class="language-css">.shake-button {
  padding: 15px 30px;
  font-size: 18px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.3s ease;
}

.shake-button:hover {
  animation: shake 0.6s ease-in-out;
}

@keyframes shake {
  0%, 100% { 
    transform: rotate(0deg); 
  }
  25% { 
    transform: rotate(-5deg); 
  }
  50% { 
    transform: rotate(5deg); 
  }
  75% { 
    transform: rotate(-5deg); 
  }
}</code></pre>
<h3 id="2-회전하는-플러스-버튼">2. 회전하는 플러스 버튼</h3>
<pre><code class="language-css">.plus-button {
  font-size: 48px;
  color: #333;
  cursor: pointer;
  display: inline-block;
  transition: transform 0.3s ease;
}

.plus-button:hover {
  animation: rotatePlus 0.8s ease-out forwards;
}

@keyframes rotatePlus {
  0% { 
    transform: rotate(0deg) scale(1); 
  }
  25% { 
    transform: rotate(-15deg) scale(1.1); 
  }
  100% { 
    transform: rotate(45deg) scale(1.3); 
  }
}</code></pre>
<h3 id="3-슬라이딩-메뉴">3. 슬라이딩 메뉴</h3>
<pre><code class="language-html">&lt;nav class=&quot;sliding-menu&quot;&gt;
  &lt;h4&gt;Menu&lt;/h4&gt;
  &lt;p class=&quot;menu-item&quot;&gt;Home&lt;/p&gt;
  &lt;p class=&quot;menu-item&quot;&gt;About&lt;/p&gt;
  &lt;p class=&quot;menu-item&quot;&gt;Contact&lt;/p&gt;
&lt;/nav&gt;</code></pre>
<pre><code class="language-css">.sliding-menu {
  position: fixed;
  left: 0;
  top: 0;
  width: 250px;
  height: 100vh;
  background: #333;
  color: white;
  padding: 30px 20px;
  transform: translateX(-200px);
  transition: all 0.5s ease;
  z-index: 1000;
}

.sliding-menu:hover {
  transform: translateX(0);
}

.sliding-menu h4 {
  text-align: right;
  transition: text-align 0.5s ease;
}

.sliding-menu:hover h4 {
  text-align: center;
}

.menu-item {
  cursor: pointer;
  padding: 10px 0;
  transition: all 0.3s ease;
}

.sliding-menu:hover .menu-item {
  animation: slideText 0.8s ease-out;
}

@keyframes slideText {
  0% { 
    transform: translateX(-100px); 
    opacity: 0;
  }
  50% { 
    transform: translateX(30px) skewX(-20deg); 
    opacity: 0.7;
  }
  100% { 
    transform: translateX(0) skewX(0deg); 
    opacity: 1;
  }
}</code></pre>
<h3 id="4-페이드-인-카드-애니메이션">4. 페이드 인 카드 애니메이션</h3>
<pre><code class="language-css">.card {
  background: white;
  border-radius: 10px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 20px;
  opacity: 0;
  transform: translateY(50px);
  animation: fadeInUp 0.6s ease-out forwards;
}

.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.4s; }
.card:nth-child(4) { animation-delay: 0.6s; }

@keyframes fadeInUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}</code></pre>
<h3 id="5-로딩-스피너">5. 로딩 스피너</h3>
<pre><code class="language-css">.loader {
  width: 50px;
  height: 50px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}</code></pre>
<h3 id="6-펄스-효과">6. 펄스 효과</h3>
<pre><code class="language-css">.pulse-button {
  position: relative;
  padding: 15px 30px;
  background: #ff6b6b;
  color: white;
  border: none;
  border-radius: 50px;
  cursor: pointer;
}

.pulse-button::before {
  content: &#39;&#39;;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 50px;
  background: #ff6b6b;
  animation: pulse 2s infinite;
  z-index: -1;
}

@keyframes pulse {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  100% {
    transform: scale(1.3);
    opacity: 0;
  }
}</code></pre>
<h2 id="⚡-성능-최적화">⚡ 성능 최적화</h2>
<h3 id="will-change-속성">Will-Change 속성</h3>
<pre><code class="language-css">/* 애니메이션 성능 향상 */
.animated-element {
  will-change: transform;
}

/* 애니메이션 완료 후 제거 */
.animated-element.animation-complete {
  will-change: auto;
}</code></pre>
<h3 id="gpu-가속-활용">GPU 가속 활용</h3>
<pre><code class="language-css">.gpu-accelerated {
  /* 3D 변환으로 GPU 가속 활성화 */
  transform: translate3d(0, 0, 0);

  /* 또는 */
  transform: translateZ(0);

  /* 더 명시적인 방법 */
  will-change: transform;
  transform: translate3d(0, 0, 0);
}</code></pre>
<h3 id="효율적인-애니메이션-속성">효율적인 애니메이션 속성</h3>
<pre><code class="language-css">/* 권장: 레이아웃에 영향 없는 속성들 */
.efficient-animation {
  transition: transform 0.3s, opacity 0.3s;
}

/* 비권장: 레이아웃 재계산 발생 */
.inefficient-animation {
  transition: width 0.3s, height 0.3s, margin 0.3s;
}</code></pre>
<h2 id="🎯-반응형-애니메이션">🎯 반응형 애니메이션</h2>
<h3 id="미디어-쿼리와-애니메이션">미디어 쿼리와 애니메이션</h3>
<pre><code class="language-css">/* 데스크톱 애니메이션 */
@media (min-width: 768px) {
  .desktop-animation {
    animation: complexAnimation 2s ease-in-out;
  }
}

/* 모바일에서는 단순한 애니메이션 */
@media (max-width: 767px) {
  .desktop-animation {
    animation: simpleAnimation 1s ease-out;
  }
}

/* 사용자가 애니메이션을 원하지 않는 경우 */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}</code></pre>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="애니메이션-디버깅">애니메이션 디버깅</h3>
<pre><code class="language-css">/* 개발 중 애니메이션 슬로우 모션 */
* {
  animation-duration: 10s !important;
}

/* 특정 요소만 슬로우 모션 */
.debug-animation {
  animation-duration: 5s !important;
}</code></pre>
<h3 id="재사용-가능한-애니메이션-클래스">재사용 가능한 애니메이션 클래스</h3>
<pre><code class="language-css">/* 유틸리티 애니메이션 클래스 */
.fade-in {
  animation: fadeIn 0.5s ease-out forwards;
}

.slide-up {
  animation: slideUp 0.6s ease-out forwards;
}

.bounce-in {
  animation: bounceIn 0.8s ease-out forwards;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slideUp {
  from { 
    transform: translateY(30px); 
    opacity: 0; 
  }
  to { 
    transform: translateY(0); 
    opacity: 1; 
  }
}

@keyframes bounceIn {
  0% { 
    transform: scale(0.3); 
    opacity: 0; 
  }
  50% { 
    transform: scale(1.05); 
    opacity: 0.8; 
  }
  70% { 
    transform: scale(0.9); 
    opacity: 0.9; 
  }
  100% { 
    transform: scale(1); 
    opacity: 1; 
  }
}</code></pre>
<p>HTML 비디오 삽입부터 고급 CSS 애니메이션까지 정리해보았습니다.</p>
]]></description>
        </item>
    </channel>
</rss>