<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>choii_ii.log</title>
        <link>https://velog.io/</link>
        <description>퍼블리셔 / 작업 기로끄v</description>
        <lastBuildDate>Wed, 18 Jun 2025 16:25:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>choii_ii.log</title>
            <url>https://velog.velcdn.com/images/choii_ii/profile/4eef8c9d-4bb6-4b30-baf1-21bd7458d3ee/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. choii_ii.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/choii_ii" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[React] Create-React-App (CRA)으로 리액트 프로젝트 시작하기 ③]]></title>
            <link>https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-cht0gmzn</link>
            <guid>https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-cht0gmzn</guid>
            <pubDate>Wed, 18 Jun 2025 16:25:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>📌 지난 포스트
<a href="https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0" target="_blank">[React] Create-React-App (CRA)으로 리액트 프로젝트 시작하기 ①</a>
<a href="https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-3jwvinxf" target="_blank">[React] Create-React-App (CRA)으로 리액트 프로젝트 시작하기 ②</a></p>
<hr>
<h1 id="🧩-useeffect로-최초-렌더링-시-한-번만-실행되는-코드-만들기">🧩 useEffect()로 최초 렌더링 시 한 번만 실행되는 코드 만들기</h1>
<p>🪄 리액트에서는 <strong>상태(state)가 변경될 때마다 컴포넌트가 다시 렌더링된다.</strong> <strong>그런데 만약 API를 통해 데이터를 가져오는 작업이 렌더링 때마다 반복된다면 비효율적일 것이다.</strong>
화면을 새로고침하면 컴포넌트가 최초로 렌더링되며 데이터를 불러온다. 그런데 딱 여기까지. 데이터 요청 등 특수한 작업은 최초 렌더링 될 때 한 번만 되길 원할 것이다.
그래서 <strong>이번 포스트에서는 최초 렌더링 시 한 번만 실행</strong>하는 방법을 배우고자 한다.</p>
<p>📌 create-react-app을 사용하면 React.useState() 대신 <strong>useState()만 써도 된다.</strong> 다만, 이렇게 사용하려면 <strong>react에서 useState를 미리 불러와야 한다!</strong> <code>import {useState} from &#39;react&#39;;</code> 이렇게!</p>
<h3 id="1️⃣-상태가-변경될-때마다-실행되는-코드-비효율적">1️⃣ 상태가 변경될 때마다 실행되는 코드 (비효율적)</h3>
<p>🔹 아래 코드의 경우 상태가 변경될 때 App 함수 전체 코드가 렌더링되기 때문에 <code>console.log(&#39;I run all the time&#39;);</code>는 여러번 실행 될 것이다. 말이 console.log이지, 다른 코드였으면 정말 비효율적이고 성능도 많이 떨어질 것이다. </p>
<pre><code>&lt;script&gt;
import {useState} from &#39;react&#39;;

function App() {
  const [counter, setValue] = useState(0);
  const onClick = () =&gt; setValue((prev) =&gt; prev+1);
  console.log(&#39;I run all the time&#39;); // 상태 변화될 때마다 실행될 것 (비효율적!!!)
  return (
    &lt;div&gt;
      &lt;h2&gt;Total Click : {counter}&lt;/h2&gt;
      &lt;button onClick={onClick}&gt;Click me!!!&lt;/button&gt;
    &lt;/div&gt;
  );
}
&lt;/script&gt;</code></pre><h2 id="🌟-그래서-배우는-것이-useeffect-함수">🌟 그래서 배우는 것이 useEffect 함수!</h2>
<p>🔹 <strong><code>useEffect(실행할 함수, [의존성 배열(dependencyList)])</code></strong> 처럼 두개의 인수를 받고, <strong>dependency가 존재하면 해당 배열 상태가 변경될 때마다 함수가 실행</strong>되는 것이다. <strong>배열을 비워두면 최초 한 번만 실행</strong>된다. 이게 전부다. 진짜 쉽다.</p>
<h3 id="1️⃣-초기-렌더링-1회만-실행되는-코드">1️⃣ 초기 렌더링 1회만 실행되는 코드</h3>
<p>🔹 <strong>배열을 비워두면 최초 한 번만 실행</strong>된다.</p>
<pre><code>&lt;script&gt;
    import {useState, useEffect} from &#39;react&#39;;
    // 코드 중략...
    useEffect(() =&gt; {
        console.log(&#39;I run only once&#39;);
    }, []);
&lt;/script&gt;</code></pre><h3 id="1️⃣-특정-값이-바뀔-때마다-실행되는-코드">1️⃣ 특정 값이 바뀔 때마다 실행되는 코드</h3>
<p>🔹 <strong>상태 변화를 감시 받아야하는 dependency를 명시</strong>하면, 상태가 변경될 때마다 실행된다.</p>
<pre><code>&lt;script&gt;
    useEffect(() =&gt; {
        console.log(&#39;SEARCH FOR&#39;, keyword);
      }, [keyword]); 
&lt;/script&gt;</code></pre><h3 id="2️⃣-특정-값이-바뀔-때마다-실행되는-코드-조건부">2️⃣ 특정 값이 바뀔 때마다 실행되는 코드 (조건부)</h3>
<p>🔹 예시로 영화 검색 API를 활용한다고 했을 때, input 요소에 텍스트를 한글자 한글자 입력할 때마다 API를 호출한다면 정말 끔찍할 것이다. 물론 특정 값이 바뀔 때마다 실행되도록 할 수 있지만, 더 디테일한 조건부를 추가해야한다면..? <strong>값이 바뀔 때마다 실행되긴 해야하지만, <code>공백이 아니어야하고, 글자 수가 5자 이상일 때</code>의 조건을 덧붙여야 한다면..!</strong> 이렇게 if문으로 작성해볼 수 있다.</p>
<pre><code>&lt;script&gt;
    useEffect(() =&gt; {
        if(keyword !== &quot;&quot;, keyword.length &gt; 5){
          console.log(&#39;SEARCH FOR&#39;, keyword);
        }
      }, [keyword]);
&lt;/script&gt;</code></pre><h3 id="3️⃣-특정-값이-바뀔-때마다-실행되는-코드-다수의-컴포넌트">3️⃣ 특정 값이 바뀔 때마다 실행되는 코드 (다수의 컴포넌트?!)</h3>
<p>🔹 만약 버튼 컴포넌트와 input 컴포넌트가 있다. <strong>버튼을 클릭할 때는 input 관련 함수가 실행되어서는 안되고, 버튼 관련 함수만 실행되어야한다.</strong> input 동작때도 마찬가지. <strong>이럴 때는 위에서 본 것처럼 dependency만 각각 설정해주면 된다.</strong>
🔹 그런데, <strong>버튼 클릭과 input 동작 시 공통적으로 실행시키고자하는 함수가 있다면?</strong> 복잡하게 생각할 것 없이 <strong>dependenty를 나란히 적어주면 된다. <code>[counter, keyword]</code>처럼!!</strong></p>
<pre><code>&lt;script&gt;
    // 1회만 최초 실행
    useEffect(() =&gt; {
        console.log(&#39;I run only once.&#39;);
     }, []);

    // counter 상태 값이 변경될 때마다 실행
    useEffect(() =&gt; {
        console.log(&#39;I run when &quot;counter&quot; changes&#39;);
      }, [counter]);

    // keyword 상태 값이 변경될 때마다 실행
     useEffect(() =&gt; {
        console.log(&#39;I run when &quot;keyword&quot; changes&#39;)
      }, [keyword]);    

    // counter, keyword 상태 값이 변경될 때마다 실행
    useEffect(() =&gt; {
        console.log(&#39;I run when &quot;keyword &amp; counter&quot; changes&#39;);
      },[keyword, counter]);
&lt;/script&gt;</code></pre><hr>
<h2 id="🌟-잠깐만요-진짜-마지막이에요-cleanup함수">🌟 잠깐만요, 진짜 마지막이에요. cleanup함수!</h2>
<p>🪄 <code>useEffect</code> 안에서 이벤트 리스너나 타이머 같은 작업을 설정했다면, <strong>컴포넌트가 사라지거나(destroy) 다시 실행될 때(create) 이전 작업을 정리해 주어야 한다. 이때 사용하는 것이 바로 cleanup 함수</strong>이다. </p>
<p>🔹 cleanup 함수라고 해서 복잡하거나 어려운 문법은 절대 아니다. <strong>function에서 return의 원리만 알고 있으면 충분히 사용</strong>할 수 있다.</p>
<p>🔹 <strong>&quot;특정 조건일 때 실행, 조건에서 벗어나면 종료&quot;</strong> 이 원리를 바탕으로 <strong>useEffect 안에서는 return 뒤에 오는 함수가 cleanup 함수</strong>가 되며, <strong>컴포넌트가 사라질 때(destroy) 호출</strong>된다. </p>
<p>🔹 사용방법은 다양하다. <strong>destroy 함수, create 함수를 각각 선언한 후 호출하는 방법</strong>도 있고, <strong>화살표 함수 표기로 한 번에 작성하는 방법</strong>도 있다. 코드가 간결하다면 한 번에 작성하는게 편할텐데, 이건 정말 기초 중의 기초라 화살표 함수 표기법이 쉬워보이는 거겠지..ㅎ</p>
<pre><code>&lt;script&gt;
    // 화살표 함수 표기법
      useEffect(() =&gt; {
        console.log(&quot;hi&quot;);
          return () =&gt; console.log(&#39;bye&#39;);
      }, []);

    // 함수 선언 후 실행법
    function Hello(){
          function byFn(){
            console.log(&#39;bye :(&#39;);
         }
        function hiFn(){
            console.log(&#39;create :)&#39;);
            return byFn; //destroy될 때
         }
    }
&lt;/script&gt;</code></pre><br>

<p>🪄 지금까지의 포스트를 통해 리액트 기초를 다져보았다. 다음 포스트부터는 기초를 바탕으로 프로젝트를 시작할텐데, 당황하지 않고 천천히 차근차근 배워나가며 리액트를 부셔버리겠다...후하 아디오스!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] Create-React-App (CRA)으로 리액트 프로젝트 시작하기 ②]]></title>
            <link>https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-3jwvinxf</link>
            <guid>https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-3jwvinxf</guid>
            <pubDate>Wed, 18 Jun 2025 08:56:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>📌 지난 포스트
<a href="https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0" target="_blank">[React] Create-React-App (CRA)으로 리액트 프로젝트 시작하기 ①</a></p>
<hr>
<h1 id="🧩-리액트-컴포넌트-분리와-모듈-스타일링">🧩 리액트 컴포넌트 분리와 모듈 스타일링</h1>
<p>🪄 이번 포스트에서는 그동안 배워왔던 컴포넌트 분리에 각각의 컴포넌트 스타일을 입히는 방법을 배워보기로 한다. 진짜 어메이징하고 클래스 명을 기억하거나 고민하지 않아도 되어서 최고였음</p>
<h2 id="1️⃣-buttonjs-컴포넌트-파일-만들기">1️⃣ Button.js 컴포넌트 파일 만들기</h2>
<p>🔹 src 폴더 안에 <strong>Button.js 파일을 새로 만들어 컴포넌트를 작성</strong>
🔹 <strong><code>export default</code>는 다른 파일에서 해당 컴포넌트를 불러올 수 있게 해주는 역할</strong>을 한다.
예를 들어, 렌더링을 할 <strong>App.js파일에서 Button 컴포넌트를 정상적으로 가져올 수 있게 하기 위해 export와 import는 필수</strong>이다.
🔹 Button 함수의 props로 text={&quot;continue&quot;}를 써주면 이런 버튼이 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/b4f6e58b-6a75-43e3-92e6-4692f6f56fb8/image.png" alt="Button.js 컴포넌트 파일 만들기"></p>
<pre><code>&lt;script&gt;
// Button 컴포넌트
function Button({text}){
    return &lt;button&gt;{text}&lt;/button&gt;
}
export default Button; // App.js에서 Button을 가져올 수 있게 하기 위해 내보내기


// App 컴포넌트
import Button from &#39;./Button&#39;;

function App() {
  return (
    &lt;div&gt;
      &lt;h1&gt;Welcome back!&lt;/h1&gt;
      &lt;Button text={&quot;Continue&quot;}/&gt;  // text props로 버튼에 text 삽입
    &lt;/div&gt;
  );
}

export default App;

&lt;/script&gt;</code></pre><br>

<h2 id="2️⃣-컴포넌트에-스타일-적용하는-법">2️⃣ 컴포넌트에 스타일 적용하는 법</h2>
<h3 id="✅-방법-1--인라인-스타일-비추천">✅ 방법 1 : 인라인 스타일 (비추천)</h3>
<p>🔹 코드와 스타일을 한 번에 관리할 수 있어 간단하지만, <strong>style 속성이 길어지면 코드가 지저분해지고 재사용이 어렵다는 큰 단점이 있어 비추천</strong>한다.</p>
<pre><code>&lt;script&gt;
function Button({text}){
    return &lt;button
                style={{
                backgroundColor:&quot;skyblue&quot;,
                padding:&quot;10px&quot;
                }}
            &gt;{text}&lt;/button&gt;
}
&lt;/script&gt;</code></pre><h3 id="✅-방법-2--전역-css-파일">✅ 방법 2 : 전역 CSS 파일</h3>
<p>🔹 index.js 또는 App.js 등에서 css파일을 import하여 적용할 수 있다. 하지만 이 방식은 <strong>모든 컴포넌트 스타일에 영향을 줄 수 있어 충돌의 위험이 있다.</strong></p>
<pre><code>&lt;script&gt;
    import &#39;./styles.css&#39;;
&lt;/script&gt;</code></pre><h3 id="✅-방법-3--css-모듈-사용-추천">✅ 방법 3 : CSS 모듈 사용 (추천!)</h3>
<p>🔹 예시로 <strong>Button.module.css라는 모듈 파일을 생성</strong>해서, <strong>해당 컴포넌트 전용 스타일을 작성</strong>할 수 있다.
🔹 Button.module.css을 <strong>import로 불러오고, 컴포넌트에 className 속성을 설정</strong>한다.  🔹 <strong><code>styles.btn</code>처럼 표기하는 이유</strong>는 <code>Button.module.css</code>를 <code>import styles from &quot;./Button.module.css&quot;</code>처럼 불러오면, <strong>CSS 클래스들이 자바스크립트 객체 형태로 변환되어 styles 안에 담긴다. <code>{btn: &quot;Button_btn__1a2b3&quot;}</code> 이런식으로.</strong> 그래서 컴포넌트에서 사용할 때는 <code>&lt;button className={styles.btn}&gt;Click&lt;/button&gt;</code> 이렇게 적어 <strong>객체 안에 있는 속성을 꺼내 쓰는 것</strong>이다.
🔹 이 방법은 <strong>CSS 모듈의 고유한 클래스명이 자동으로 생성</strong>되기 때문에 <strong>클래스명이 충돌하지 않는다.</strong></p>
<pre><code>&lt;style&gt;
    /* Button.module.css */
    .btn{
        background-color:skyblue;
        padding:10px;
    }
&lt;/style&gt;</code></pre><pre><code>// Button.js 컴포넌트 파일
&lt;script&gt;
    import styles from &#39;./Button.module.css&#39;;

    function Button({text}){
        return &lt;button className={styles.btn}&gt;{text}&lt;/button&gt;
    }
    export default Button;
&lt;/script&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[React] Create-React-App (CRA)으로 리액트 프로젝트 시작하기 ①]]></title>
            <link>https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choii_ii/React-Create-React-App-CRA%EC%9C%BC%EB%A1%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 18 Jun 2025 07:34:53 GMT</pubDate>
            <description><![CDATA[<h1 id="🌟-create-react-app-cra란">🌟 Create-React-App (CRA)란?</h1>
<p>🪄 이전 포스트에서도 언급했듯,, 지금까지 배운 건 현재 실무에서 사용되지 않는 cdn 작성법이라 기본 개념만 익히기로 하고, 오늘 포스트에서는 본격적으로 <strong>실무에서도 활용되고 가장 간편하며 효과적으로 작업할 수 있는 Create-React-App(CRA) 환경</strong>에 대해 배워보기로 한다.</p>
<p>🪄 CRA는 <strong>복잡한 설정 없이 빠르게 리액트 개발 환경을 구성할 수 있도록 도와주는 도구</strong>로, 다양한 사전설정과 편리한 기능을 제공한다.</p>
<h3 id="👍🏻-react-reactdom-webpack-babel-등-필수-패키지-자동-설치">👍🏻 React, ReactDOM, Webpack, Babel 등 필수 패키지 자동 설치</h3>
<p>🔹 리액트 컴포넌트와 렌더링, 최신 자바스크립트 변환을 위한 Babel 같은 필수 도구들을 따로 설치하거나 CDN으로 연결하지 않아도 알아서 세팅해준다.</p>
<h3 id="👍🏻-개발-서버-실행-및-코드-변경-시-자동-새로고침-지원">👍🏻 개발 서버 실행 및 코드 변경 시, 자동 새로고침 지원</h3>
<p>🔹 코드를 수정하면 별도 새로고침 없이 바로 브라우저에 변경 사항이 반영되어 개발 속도가 빨라진다.</p>
<hr>
<h1 id="🌟-create-react-app-cra-시작하기">🌟 Create-React-App (CRA) 시작하기</h1>
<p><a href="https://github.com/facebook/create-react-app
">📍Create React App Github 바로가기</a></p>
<h2 id="1️⃣-nodejs-설치">1️⃣ Node.js 설치</h2>
<p><strong><a href="https://nodejs.org/ko
">📍Node.js 공식 사이트</a></strong>에서 운영체제에 맞는 버전 설치
🔹 설치 후, 명령 프롬프트(cmd)를 열고 <strong><code>node -v</code> 명령어로 설치 여부와 버전 확인</strong>
🔹 <code>npx</code> 입력을 통해 <strong>npx가 정상적으로 동작하는지 확인</strong> 
🔸 <strong>npx : 필요한 패키지를 따로 설치하지 않고도 바로 실행할 수 있게 도와주는 도구</strong>로, 제대로 작동하면 CRA 프로젝트 생성도 문제없이 할 수 있다.
<img src="https://velog.velcdn.com/images/choii_ii/post/86041f47-d770-4313-aed9-6b3e03a361cd/image.png" alt="Node.js 설치"></p>
<h2 id="2️⃣-프로젝트-생성">2️⃣ 프로젝트 생성</h2>
<p>🔹 명령 프롬프트(cmd)를 열고 <strong><code>npx create-react-app my-app</code> 명령어를 입력하여 my-app 이라는 프로젝트를 생성</strong> (my-app은 본인이 설정할 프로젝트명으로 설정하면 됨)
🔹 프로젝트 생성이 완료되면 <strong>React, ReactDOM 등 필수 패키지가 자동 설치되고, 프로젝트 폴더가 만들어진다.</strong>
🔹 <strong>VS Code로 바로 열고 싶다면 <code>code my-app</code> 명령어를 입력</strong></p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/f507377c-7ce9-45ce-aec7-8aafaff9fc8f/image.png" alt="프로젝트 생성"></p>
<p style="font-size:15px; color:#999; text-align:center;">나는 react-for-beginners라는 프로젝트 명으로 시작하지롱~</p>


<h2 id="3️⃣-개발-서버-실행">3️⃣ 개발 서버 실행</h2>
<p>🔹 VS Code 내 <strong>터미널에서 <code>npm start</code> 명령어를 실행하면 개발용 서버가 실행</strong>되며, 자동으로 브라우저가 열린다.</p>
<h2 id="4️⃣-프로젝트-구조-및-초기-정리">4️⃣ 프로젝트 구조 및 초기 정리</h2>
<p>🔹 <strong>src 폴더</strong> : 애플리케이션 소스 코드가 위치하는 곳으로 <strong>프로젝트의 모든 파일을 관리할 폴더</strong>
🔹 기본 생성되는 파일 중 필요 없는 파일들은 삭제해도 무방 <span style="font-size:15px; color:#999; text-align:center;">ex) App.test.js, App.css, index.css, logo.svg, reportWebVitals.js, setupTests.js</span>
🔹 index.js와 App.js 파일을 중심으로 깔끔하게 코드 작성이 가능</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 이것만 알아도 반은 성공 ③ (React로 재사용 가능한 버튼 컴포넌트 만들기)]]></title>
            <link>https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5-React%EB%A1%9C-%EC%9E%AC%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%B2%84%ED%8A%BC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5-React%EB%A1%9C-%EC%9E%AC%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%B2%84%ED%8A%BC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 17 Jun 2025 17:18:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p><strong>📌 지난 포스트</strong>
<a href="https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5" target="_blank">[React] 이것만 알아도 반은 성공 ①</a>
<a href="https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5-szjaqm0g" target="_blank">[React] 이것만 알아도 반은 성공 ②</a></p>
<hr>
<h1 id="🧩-react로-재사용-가능한-버튼-컴포넌트-만들기">🧩 React로 재사용 가능한 버튼 컴포넌트 만들기</h1>
<p>🪄 <strong>보통 버튼은 1-2가지 유형으로 만들어놓고 프로젝트 전반에 걸쳐 재사용하게 된다.</strong> 이렇게 UI 요소를 더욱 효율적으로 재사용하기 위해 사용하는 것이 React.js인 점을 기억하고 있을 것이다. 그래서 React를 배우고 있는 것이고! 
오늘은 React 학습 목표에 조금 더 가까워지기 위한 예제를 만들어 볼 것이다.
핵심은 <strong>primary 용도로 사용할 공통 버튼 컴포넌트를 하나 만들고</strong>, <strong>props라는 것을 통해 버튼마다 텍스트, 크기 등의 차이를 유연하게 줄 수 있게 구성</strong>해보는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/ba7329e4-f6c5-48a5-bc1a-659eb6146415/image.png" alt="React로 재사용 가능한 버튼 컴포넌트 만들기"></p>
<h2 id="🔸-reactjs-🔸">🔸 React.js 🔸</h2>
<h3 id="👍🏻-공통-버튼-컴포넌트btn-작성">👍🏻 공통 버튼 컴포넌트(Btn()) 작성</h3>
<p>🔹 click 이벤트나, style, id, class 등 <strong>html 속성을 작성</strong>할 수 있다. <strong>style의 경우에는 object 형식으로 나열한다.</strong>
🔹 <strong>인수로 text, changeValue를 받는 것은 버튼 안에서 어떤 글자를 보여줄 것인지 또는 어떤 동작을 해야하는지 알기 위해서이다.</strong> 예를들어, 주문 받은 피자의 토핑(text)과 굽는 방식(changeValue)을 주방(컴포넌트)에 알려주는 것과 같다. 
🔹 그런데 이때, 인수를 <strong>props 자체로 받아도 되고, 구조분해할당으로 바로 받는 방법이 있다.</strong> props는 object라 {props.text} 처럼 써야한다. 하지만 <strong>구조분해할당({text, changeValue})으로 바로 받는다면 {text} 처럼 간결하고 직관적인 사용이 가능해서 이 방법을 권장한다.</strong></p>
<h3 id="👍🏻-reactmemo">👍🏻 React.memo()</h3>
<p>🔹 <code>const MemorizedBtn = React.memo(Btn);</code> : Btn 컴포넌트를 기억하고 있다가 <strong>props가 이전과 동일하다면, re-render하지 않겠다는 코드</strong>
🔹 <strong>요소 상태의 변경이 있을 때마다, re-render 되는데 이것은 상당히 비효율적이다.</strong> <strong>React가 변경된 부분만 빠르게 업데이트 시켜서 효율성을 증대시킨다는 장점이 있으니 배우고 있는 것이 아니겠는가?</strong> 따라서 <strong>우리는 props가 변경되지 않는다면 re-render 시키지 않을 것이라는 memo 속성을 사용해줄 것이다.</strong>
🔹 즉, text나 changeValue가 변하지 않으면 <MemorizedBtn />은 re-render 되지 않음.</p>
<h3 id="👍🏻-렌더링-함수-props란">👍🏻 렌더링 함수 (+props란?)</h3>
<p>🔹 <code>const [value, setValue] = React.useState(&quot;Save Changes&quot;);</code> : value(상태 관리), setValue(변경 될 값), value의 초기값으로 Save Changes 설정
🔹 <code>const changeValue = () =&gt; { setValue(&quot;Revert Changes&quot;) };</code> : 버튼 클릭 시, Revert Changes로 변경
🔹 <code>&lt;MemorizedBtn text={value} big={true} changeValue={changeValue} /&gt;</code> <strong>: text와 changeValue를 MemorizedBtn 컴포넌트에 props로 전달</strong>
🔹 여기서 <strong>props란, 컴포넌트에서 직접 실행되지 않고, function에서 props를 전달받아 실행되는 것!</strong> 즉, changeValue라는 props는 <strong>Btn컴포넌트에서 onClick 이벤트에 연결되어 실행</strong>되고, 이로 인해 <strong>상태가 변경되며 value 값이 Revert Change로 바뀐다.</strong> <strong>상태가 변경되면 컴포넌트가 다시 렌더링</strong> 되고, <strong>버튼 내부에 표시되는 {text} 값도 변경된 Revert Change로 업데이트</strong> 되어 나타난다.</p>
<pre><code>&lt;script type=&quot;text/babel&quot;&gt;
    const root = document.querySelector(&#39;#root&#39;);

    // primary btn 컴포넌트
    function Btn({ text, changeValue }){
        return (
            &lt;button
                onClick={changeValue}
                style={{
                    //object 형식으로 씀
                    backgroundColor: &quot;tomato&quot;,
                    color: &quot;white&quot;,
                    padding: &quot;10px 20px&quot;,
                    borderRadius: 10
                }}&gt;
                {text}
            &lt;/button&gt;
        )
    }

    const MemorizedBtn = React.memo(Btn);

    // 컴포넌트 렌더링
    function App() {
        const [value, setValue] = React.useState(&quot;Save Changes&quot;);
        const changeValue = () =&gt; { setValue(&quot;Revert Changes&quot;) };
        return (
            &lt;div&gt;
                // Continue 버튼 1개 + Save Changes -&gt; Revert Changes 변경되는 버튼 1개
                &lt;MemorizedBtn text={value} big={true} changeValue={changeValue} /&gt;
                &lt;MemorizedBtn text=&quot;Continue&quot; /&gt;
            &lt;/div&gt;
        );
    }

    ReactDOM.render(&lt;App /&gt;, root);
&lt;/script&gt;</code></pre><br>

<p>🪄 와.. 갑자기 머리가 새하얘지기 시작한 것 같다. 근데 너무 재밌어...!! 근데 어려워!!!! 혼란스럽다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 이것만 알아도 반은 성공 ②]]></title>
            <link>https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5-szjaqm0g</link>
            <guid>https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5-szjaqm0g</guid>
            <pubDate>Tue, 17 Jun 2025 15:58:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p><strong>📌 지난 포스트</strong>
<a href="https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5" target="_blank">[React] 이것만 알아도 반은 성공 ①</a></p>
<hr>
<h1 id="🧩-예제로-알아보는-react">🧩 예제로 알아보는 React</h1>
<p>🪄 지난 포스트에서 미리 본 <strong>&#39;클릭이벤트를 통한 숫자 카운팅&#39;</strong> 예제를 분석하며 React 속성을 빠르게 알아보겠다.</p>
<h2 id="🔸-html5-🔸">🔸 HTML5 🔸</h2>
<p>🔹 HTML에서 React root 영역 지정 (렌더링 영역)</p>
<pre><code>&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
&lt;/body&gt;</code></pre><h2 id="🔸-reactjs-🔸">🔸 React.js 🔸</h2>
<h3 id="👍🏻-react-필수-스크립트">👍🏻 React 필수 스크립트</h3>
<p>🔹 <code>react.production.min.js</code> : 컴포넌트 생성, 상태관리(useState) 등 기능을 활용할 수 있는 리액트 핵심 라이브러리
🔹 <code>react-dom.production.min.js</code> : 리액트로 만든 UI를 실제 HTML에 그려주는 라이브러리로 render() 기능을 활용할 수 있다.
🔹 <code>babel.min.js</code> : JSX 문법을 브라우저가 이해할 수 있도록 실시간으로 변환해주는 도구. 본문에 반드시 <code>type=&quot;text/babel&quot;</code>을  같이 써주어야 한다.</p>
<pre><code>// React 필수 스크립트
&lt;script src=&quot;https://unpkg.com/react@17.0.2/umd/react.production.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://unpkg.com/@babel/standalone/babel.min.js&quot;&gt;&lt;/script&gt;</code></pre><br>

<h3 id="👍🏻-reactusestate">👍🏻 React.useState()</h3>
<p>🔹 <strong>useState는 컴포넌트 안에서 상태(state)를 관리</strong>하기 위해 사용하는 <strong>함수</strong>이다.
🔹 (<strong>현재 상태 값(state), 상태를 바꿔주는 함수(modifier))라는 두 개의 인수를 갖고</strong>, 배열구조분해할당을 통해 <code>const [state, setState] = useState(초기값);</code> 이렇게 꺼내 쓸 수 있다. state : 현재 상태 값, setState : 상태를 바꿔주는 함수 이렇게 되는 것.. <strong>이건 React 쓰면서 정말 자주 보게 될 거니까 매우 중요바리한 개념이겠쥬?</strong>
🔹 아래 코드에서는 <code>const [counter, setCounter] = React.useState(0);</code> <strong>counter라는 상태 변수</strong>와 <strong>상태를 변경할 수 있는 setCounter 함수를 선언</strong>했고, <strong>counter의 초기값을 0으로 설정</strong>한 것이다.
🔹 <strong>상태를 바꿔주는 함수(modifier)는 상태가 변경될 때, 새로운 값을 갖고 컴포넌트가 재생성</strong>된다. <strong>즉, 여러번 render을 하지 않고도 효율적인 코드 작성이 가능하다는 뜻!</strong></p>
<h3 id="👍🏻-컴포넌트-함수는-반드시-대문자">👍🏻 컴포넌트 함수는 반드시 대문자!</h3>
<p>🔹 <code>&lt;App /&gt;</code> 처럼 컴포넌트 함수는 반드시 대문자로 시작한다. 그 이유는 React가 JSX 코드 내에서 태그가 대문자로 시작하는 경우, 이를 HTML 태그가 아닌 사용자 정의 컴포넌트로 인식하기 때문이다. 즉, <code>&lt;app/&gt;</code>처럼 <strong>소문자로 작성하면 React는 이것을 HTML 태그로 읽어버린다.</strong> 따라서 <code>&lt;App /&gt;</code>, <code>App()</code> 대문자로 작성하는 것이 규칙!</p>
<h3 id="👍🏻-이벤트-함수-선언">👍🏻 이벤트 함수 선언</h3>
<p>🔹 click, change 등 이벤트 발생 시, 상태를 변경하는 함수를 실행
🔹 <code>setCounter((current) =&gt; current + 1);</code> 그런데 여기서 <strong>current를 인수로 받는 이유는 이전 상태값을 기준으로 새로운 상태값을 <u>정확하게 계산하기 위해서</u>이다. 앞으로도 이런 업데이트 함수 방식을 사용할 것이다.</strong></p>
<h3 id="👍🏻-jsx-작성-팁">👍🏻 JSX 작성 팁</h3>
<p>🔹 <code>&lt;h3&gt;Total clicks : {counter}&lt;/h3&gt;</code> : <strong>카운팅 될 숫자 data를 받기 위해 { }를 활용하여 변수 값을 출력</strong>한다.
🔹 <code>&lt;button onClick={onClick}&gt;Click me!&lt;/button&gt;</code> : <strong>onClick은 클릭 이벤트를 호출</strong>하는 것이고, <strong>{onClick}은 onClick 이름을 가진 함수를 불러오는 것</strong>이다.</p>
<h3 id="👍🏻-reactdomrender">👍🏻 ReactDOM.render()</h3>
<p>🔹 <strong>리액트 요소(컴포넌트)를 실제 화면에 출력</strong>해주는 함수
🔹 <code>ReactDOM.render(&lt;App /&gt;, root);</code> :  (컴포넌트, 요소를 보여줄 HTML 요소) 형태로 사용</p>
<pre><code>&lt;script type=&quot;text/babel&quot;&gt;
    const root = document.querySelector(&#39;#root&#39;);

    // App() : 컴포넌트 함수 (재사용 가능한 UI 조각)
    function App(){
        // 컴포넌트 상태 관리
        const [counter, setCounter] = React.useState(0);
        // click 이벤트 정의
        const onClick = () =&gt; setCounter((current) =&gt; current + 1);
         return (
            // JSX 문법 : 상태가 변경되면 자동으로 렌더링됨
             &lt;div&gt;
                &lt;h3&gt;Total clicks : {counter}&lt;/h3&gt;
                &lt;button onClick={onClick}&gt;Click me!&lt;/button&gt;
            &lt;/div&gt;
         );
     }

     ReactDOM.render(&lt;App /&gt;, root); // App 컴포넌트를 root 요소에 렌더링(그리기)
&lt;/script&gt;</code></pre><br>

<p>🪄 React 입문 시, 배운다는 이 내용은 cdn방식 또는 vanilla 방식이라고 하는데, <strong>요즘엔 진짜 거의 안쓴다고 합니다..ㅎ</strong> 대신 <strong>대부분 Create React App(CRA) 또는 Vite, Next.js 같은 모던 개발 환경을 쓴다고 하네요?</strong> 그럼 이걸 도대체 왜 배운건가 싶겠지만, 뭐든 기초가 탄탄해야 써먹을 수 있지 않겠습니까..ㅎ_ㅎ 눈에서 왜 땀이 나지 증말
cdn방식을 활용해서 저는 단위를 변환하는 Convert 토이 프로젝트도 진행을 해보았는데요! 이건 다음..? 다다음..? 포스팅에서 작성해보겠습니다요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 이것만 알아도 반은 성공 ①]]></title>
            <link>https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5</link>
            <guid>https://velog.io/@choii_ii/React-%EC%9D%B4%EA%B2%83%EB%A7%8C-%EC%95%8C%EC%95%84%EB%8F%84-%EB%B0%98%EC%9D%80-%EC%84%B1%EA%B3%B5</guid>
            <pubDate>Tue, 17 Jun 2025 12:57:39 GMT</pubDate>
            <description><![CDATA[<h1 id="📢-react를-배우기-전끄적끄적">📢 React를 배우기 전,,(끄적끄적)</h1>
<p>요즘 웹 개발 세계에서 React는 빠지질 않는다.
도대체 React가 뭐길래 다들 배우라고 난리일까?</p>
<p>그리고 &quot;퍼블리셔가 이걸 꼭 알아야 해?&quot;라는 생각은 이제 옛말이 되었다.</p>
<p>나도 한 2년 전쯤? 회사 다닐 때 틈틈이 공부해보려고 했는데, 퇴근하고 공부한다는 게 말처럼 쉽진 않았다. 진짜 웬만한 의지랑 광기가 없으면 꾸준히 하기 힘든 것이었음!!
그래서 그땐 그냥 &#39;포트폴리오부터 만들고, 이직 후 천천히 배우자&#39;하고 React는 잠깐 미뤄뒀었는데…
지금 생각하면, 그때 바로 시작할 걸 그랬다.
진작에 익혀뒀으면 더 효율적으로 웹사이트 만들고 운영할 수 있었을 텐데 말이지...
<span style="font-size:15px; color:#999">(그리고 취업도 더 빨랐겠지..🤦🏻‍♀️)</span></p>
<p>여기까진 그냥 나의 넋두리였고, 이제 본격적으로 React가 뭔지 알아가보자</p>
<hr>
<h1 id="🌟-react란">🌟 React란?</h1>
<p>🪄 React는 사용자 인터페이스(UI)를 만들기 위한 자바스크립트 라이브러리로, HTML요소들을 조립하듯 구성하여 동적인 웹 화면을 만들 수 있게 도와주는 도구라고 생각하면 된다. <strong>즉, 기존에는 HTML, CSS, JS 파일단위로 나눠 작업하던 반면, React는 컴포넌트(Component) 단위로 화면을 조각조각 나눠 코드를 작성한다.</strong> 그래서 <strong>유지보수나 재사용이 훨씬 쉬워지고, 복잡한 UI도 깔끔하게 구현이 가능하다는 장점</strong>이 있다.</p>
<h3 id="👍🏻-컴포넌트component-기반-구조">👍🏻 컴포넌트(Component) 기반 구조</h3>
<p>🔹 화면을 button, card, list 같은 작은 조각(=컴포넌트)으로 나눠서 만들어 유지보수나 재사용이 쉬워진다.</p>
<h3 id="👍🏻-빠른-화면-업데이트-virtual-dom">👍🏻 빠른 화면 업데이트 (Virtual DOM)</h3>
<p>🔹 React는 가상 DOM을 먼저 바꾼 다음 실제 DOM과 비교해서 <strong>변경된 부분만 업데이트</strong>하여 빠르고 부드러운 성능을 보장한다.</p>
<h3 id="👍🏻-재사용성-좋은-코드">👍🏻 재사용성 좋은 코드</h3>
<p>🔹 <strong>한 번 만든 컴포넌트를 props(속성)만 변경하여 여기저기서 쉽게 사용</strong>할 수 있다.</p>
<hr>
<h1 id="🌟-jsx-리액트를-위한-필수-문법">🌟 JSX! 리액트를 위한 필수 문법</h1>
<p>🪄 React를 공부하다보면 꼭 듣게 되는 것이 바로 JSX이다. <strong>JSX는 쉽게 말해서 JS안에서 HTML처럼 보이는 코드를 쓸 수 있게 해주는 문법</strong>이다. </p>
<h3 id="👍🏻-직관적인-ui--쉬운-유지보수">👍🏻 직관적인 UI + 쉬운 유지보수</h3>
<p>🔹 HTML 구조가 한눈에 보이고, 화면구성 + 동작 코드를 한 곳에서 작성할 수 있어서 유지보수가 쉽다.</p>
<h3 id="👍🏻-조건부반복-렌더링도-문제없음">👍🏻 조건부/반복 렌더링도 문제없음</h3>
<p>🔹 JSX는 JS문법을 그대로 쓸 수 있어서, <strong>삼항 연산자, &amp;&amp;, if문 등을 사용</strong>해 조건에 따라 다른 UI를 보여줄 수 있다. 또, map() 같은 <strong>배열 메서드를 활용해서 리스트나 카드 UI를 구성할 때 반복 렌더링하는 것도 가능</strong>하다.</p>
<p>*<em>🔻 JSX 예시 🔻
*</em></p>
<pre><code>&lt;script&gt;
    function App(){
        // JSX 문법
        return &lt;h1&gt;안녕, 리액트!&lt;/h1&gt;;
    }
&lt;/script&gt;</code></pre><h2 id="🧩-jsx문법은-이렇게">🧩 JSX문법은 이렇게!</h2>
<h3 id="👍🏻-태그는-꼭-하나의-부모-요소로-감싸기">👍🏻 태그는 꼭 하나의 부모 요소로 감싸기!</h3>
<p>🔹 여러 개의 태그를 그냥 나열하면 오류가 발생하게 된다. 따라서 <strong><code>&lt;div&gt;</code> 태그로 감싸주거나 <code>&lt;&gt; &lt;/&gt;</code> 같은 Fragment 문법을 써야한다.</strong></p>
<h3 id="👍🏻-js스타일의-html-속성-사용하기">👍🏻 JS스타일의 HTML 속성 사용하기!</h3>
<p>🔹 <code>&lt;script type=&quot;text/babel&quot;&gt;</code> : 브라우저에서 JSX 문법을 사용 가능하게 만들어주는 설정이다. <strong>JSX는 브라우저가 직접 이해할 수 없기 때문에, Babel이 중간에서 JSX를 일반 JS 코드로 변환해주어야 한다.</strong>
🔹 <code>class -&gt; className</code>, <code>for -&gt; htmlFor</code>
🔹 JSX는 자바스크립트 기반이기 때문에 <strong>HTML 속성도 위처럼 JS 스타일로 작성해야한다.</strong></p>
<h2 id="🧩-바닐라js-vs-jsxreact">🧩 바닐라JS VS JSX(React)</h2>
<h3 id="👍🏻-바닐라js">👍🏻 바닐라JS</h3>
<p>🔹 HTML 작성 -&gt; JS에서 요소 가져오기 -&gt; 이벤트 감지 -&gt; 데이터 업데이트 및 이벤트 실행</p>
<p>🔻 클릭이벤트를 통한 숫자 카운팅 스크립트 (바닐라) 🔻</p>
<pre><code>&lt;body&gt;
    &lt;span&gt;Total clicks : 0&lt;/span&gt;
    &lt;button id=&quot;btn&quot;&gt;Click me!&lt;/button&gt;
&lt;/body&gt;</code></pre><pre><code>&lt;script&gt;
    let counter = 0;
    const span = document.querySelector(&#39;span&#39;);
    const button = document.querySelector(&#39;#btn&#39;);
    button.addEventListener(&#39;click&#39;, handleClick);

    function handleClick() {
        counter = counter + 1;
        span.innerText = `Total clicks : ${counter}`;
    }
&lt;/script&gt;</code></pre><h3 id="👍🏻-jsxreact">👍🏻 JSX(React)</h3>
<p>🔹 컴포넌트 생성 -&gt; 상태 정의 -&gt; 렌더링</p>
<p>🔻 클릭이벤트를 통한 숫자 카운팅 스크립트 (JSX(React)) 🔻</p>
<pre><code>&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
&lt;/body&gt;</code></pre><pre><code>&lt;script type=&quot;text/babel&quot;&gt;
    const root = document.querySelector(&#39;#root&#39;);

    // App() : 컴포넌트 함수 (재사용 가능한 UI 조각)
    function App(){
        // 컴포넌트 상태 관리
        const [counter, setCounter] = React.useState(0);
        // click 이벤트 정의
        const onClick = () =&gt; setCounter((current) =&gt; current + 1);
         return (
            // JSX 문법 : 상태가 변경되면 자동으로 렌더링됨
             &lt;div&gt;
                &lt;h3&gt;Total clicks : {counter}&lt;/h3&gt;
                &lt;button onClick={onClick}&gt;Click me!&lt;/button&gt;
            &lt;/div&gt;
         );
     }

     ReactDOM.render(&lt;App /&gt;, root); // App 컴포넌트를 root 요소에 렌더링(그리기)
&lt;/script&gt;</code></pre><br>

<p>🪄 정확하게 React 속성에 대해 몰라도, 코드만 보았을 때 HTML 구조와 동적 코드가 한눈에 보여 유지보수가 쉬울 것으로 예상되지 않는가?! 다음 포스팅 때는 조금 더 디테일한 설명으로 돌아오겠읍니다!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[mouse effect] 마우스 방향에 따라 요소 움직이기 (+rotate 효과)]]></title>
            <link>https://velog.io/@choii_ii/mouse-effect-%EB%A7%88%EC%9A%B0%EC%8A%A4-%EB%B0%A9%ED%96%A5%EC%97%90-%EB%94%B0%EB%9D%BC-%EC%9A%94%EC%86%8C-%EC%9B%80%EC%A7%81%EC%9D%B4%EA%B8%B0-rotate-%ED%9A%A8%EA%B3%BC</link>
            <guid>https://velog.io/@choii_ii/mouse-effect-%EB%A7%88%EC%9A%B0%EC%8A%A4-%EB%B0%A9%ED%96%A5%EC%97%90-%EB%94%B0%EB%9D%BC-%EC%9A%94%EC%86%8C-%EC%9B%80%EC%A7%81%EC%9D%B4%EA%B8%B0-rotate-%ED%9A%A8%EA%B3%BC</guid>
            <pubDate>Wed, 07 May 2025 16:12:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/f3f3b897-3ab4-4768-a5de-a19ca6adb65f/image.gif" alt="마우스 방향에 따라 요소 움직이기"></p>
<h2 id="📌-key-point">📌 Key point</h2>
<p>✅ <strong>mousemove 이벤트 속성</strong>을 통해 <strong>clientX, clientY 마우스 좌표 가져오기</strong>
✅ <strong>기준점으로부터 마우스 위치에 따른 이동 거리 계산</strong> -&gt; 요소의 움직임 각도로 사용
✅ <strong>GSAP</strong> 라이브러리를 활용하여 <strong>움직임 모션 적용</strong>
✅ <strong>정육각형을 입체감</strong> 있게 구현하기</p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 정육각형 구현</p>
<pre><code>&lt;body&gt;
&lt;div class=&quot;wrapper&quot;&gt;
    &lt;div class=&quot;box js-3dbox&quot;&gt;
        Back
        &lt;div class=&quot;right&quot;&gt;&lt;span class=&quot;label&quot;&gt;Right&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;left&quot;&gt;&lt;span class=&quot;label&quot;&gt;Left&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;top&quot;&gt;&lt;span class=&quot;label&quot;&gt;Top&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;bottom&quot;&gt;&lt;span class=&quot;label&quot;&gt;Bottom&lt;/span&gt;&lt;/div&gt;
        &lt;div class=&quot;front&quot;&gt;&lt;span class=&quot;label&quot;&gt;Front&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</code></pre><hr>
<h2 id="👉🏻-스타일은-이렇게-scss">👉🏻 스타일은 이렇게! (SCSS)</h2>
<p>🩵 정육각형을 입체감 있게 스타일 구현</p>
<p>🩵 <strong>transform-style: preserve-3d;</strong> 
🔹 <strong>3D 변환</strong>
🔹 <strong>기본값은 flat</strong>으로, 이는 <strong>자식 요소의 3D 변환이 부모 요소에 영향을 주지 않는</strong> 성격을 가짐
🔹 <strong>preserve-3d</strong>를 사용하면, <strong>부모가 3D로 회전하거나 기울어질 때 자식도 함께 3D로 영향</strong>을 받음</p>
<p>🩵 <strong>transform : perspective(40rem);</strong>
🔹 <strong>원근감</strong>을 설정
🔹 값은 <strong>거리를 나타내는 단위(px, rem, em)</strong>로 지정
🔹 <strong><u>값이 작을수록 깊이감이 강해지고</u>, <u>값이 클수록 깊이감이 약해진다.</u></strong></p>
<p>🩵 <strong>transform-origin : x y z;</strong>
🔹 요소 변환(회전, 크기 변경 등)의 <strong>기준점을 지정</strong>하는 속성
🔹 기본값은 <strong>50% 50% (가로 중앙(x), 세로 중앙(y))</strong>으로, 요소의 중앙에서 변환이 적용
🔹 <strong><code>($size / 2)</code> : Z축 기준</strong>을 설정, 3D 변환에서 <strong>깊은 곳(back)의 중앙을 중심으로 회전하는 깊이감을 줌</strong>
🔹 Z축 (깊이): 화면에서 앞뒤 방향
🔹 Z축 기준이 0일 경우: 요소가 평면 중앙에서 회전 (front)
🔹 Z축 기준이 <code>($size / 2)</code>일 경우: 요소가 back 부분 중앙에서 회전하는 것처럼 보임</p>
<pre><code>&lt;style&gt;
/* BACKGROUND */
.wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background: linear-gradient(30deg, #224, #001);
  overflow: hidden;
}

/* VAR */
$size: 300px;
$m-size: 40vmin;

.box {
  width: $size;
  height: $size;
  transform-style: preserve-3d;  /* 요소의 3D 변환 */
  transform: perspective(40rem); /* 원근법 */
  transform-origin: 50% 50% ($size / 2); /* 요소의 회전 기준점 */
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 90, 0, 0.4);
  background: linear-gradient(45deg, rgba(0, 0, 0, 0.3), rgba(255, 5, 5, 0.3));
  color: #2ab7ca;
  pointer-events: none;
  box-shadow: 0 0 50px 5px rgba(255, 5, 5, 0.3);
  text-shadow: 0 0 20px #000;
  font-size: 2rem;
  transition: transform 0.5s ease-out, font-size 0.3s ease-in-out;

  /* 면 */
  .top,
  .left,
  .right,
  .bottom,
  .front {
      width: $size;
    height: $size;
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(255, 90, 0, 0.4);
    transform-style: preserve-3d; /* 요소의 3D 변환 (텍스트 노출을 위해) */
    background: linear-gradient(45deg, rgba(0, 0, 0, 0.3), rgba(255, 5, 5, 0.3));
    box-shadow: 0 0 50px 5px rgba(255, 5, 5, 0.3);
  }

  .right {
    transform: rotateY(90deg);
    transform-origin: right;
    .label {
      transform: rotateY(-90deg);
    }
  }

  .left {
    transform: rotateY(-90deg);
    transform-origin: left;
    .label {
      transform: rotateY(90deg);
    }
  }

  .bottom {
    transform: rotateX(-90deg);
    transform-origin: bottom;
    .label {
      transform: rotateX(90deg);
    }
  }

  .top {
    transform: rotateX(90deg);
    transform-origin: top;
    .label {
      transform: rotateX(-90deg);
    }
  }

  .front {
    transform: translateZ($size); 
    /* Z축 방향 (깊이 방향)으로 이동 : 사용자에게 가까워짐 (앞으로 나옴) */
    transform-origin: top;
  }
}
&lt;/style&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-js-es6-gsap">👉🏻 스크립트는 이렇게! (JS ES6, GSAP)</h2>
<p>🩵 <strong>객체 구조 분해 할당</strong> : <strong>이벤트 객체에서 필요한 속성만 빠르고 간결하게 가져올 수 있다.</strong>
🔹 e는 전체 마우스 이벤트 객체를 나타내며, 여기에는 다양한 속성이 포함
🔹 <strong>마우스 이벤트 객체에서 clientX와 clientY라는 두 가지 속성만 꺼내어 사용</strong>
<code>const clientX = e.clientX;</code>
<code>const clientY = e.clientY;</code>
이렇게 <strong>따로따로 변수에 저장할 수 있지만, 코드 효율성과 가독성을 향상시키기 위해 구조 분해 할당을 사용</strong>하여 <strong><code>const { clientX, clientY } = e;</code> 이렇게 한 번에 작성</strong>할 수 있다.</p>
<p>🩵 <strong>마우스 위치에 따른 이동 거리 계산</strong>
<code>const rotateY = ((clientX - centerX) / centerX) * movementFactor;</code>
<code>const rotateX = ((centerY - clientY) / centerY) * movementFactor;</code>
🔹 clientX - centerX : 마우스 현재 위치와 중앙의 차이(거리)
🔹 <strong>(clientX - centerX) / centerX</strong> : 중앙을 기준으로 <strong>좌/우/중앙을 -1/+1/0 비율로 변환</strong>. 만약 <strong>centerX로 나누지 않고 절대값을 사용한다면, 화면 크기가 바뀌면 회전 강도도 달라진다.</strong> 따라서 화면 크기에 상관없이 동일한 비율로 동작하기 위한 계산식.
🔹 <strong>movementFactor</strong>를 통해 <strong>회전 강도</strong>를 쉽게 조정</p>
<p>🩵 GSAP을 사용하여 요소의 회전값, 원근값, 기준점, ease 효과 등을 설정하여 부드럽게 움직일 수 있도록 구현</p>
<p>🩵 마우스가 document에서 벗어나면 요소를 초기상태로 돌려놓음</p>
<pre><code>&lt;script&gt;
document.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {
    const wrapper = document.querySelector(&quot;.wrapper&quot;); 
    const box = document.querySelector(&quot;.js-3dbox&quot;);
    const movementFactor = 40; // 회전 강도 (값이 클수록 더 많이 회전)

    //마우스움직임 이벤트
    wrapper.addEventListener(&quot;mousemove&quot;, (e) =&gt; {
    //   const { clientX, clientY } = e; // 마우스 좌표 가져오기(이 값은 마우스를 움직일 때마다 실시간으로 업데이트)
    const clientX = e.clientX;
    const clientY = e.clientY;
      const centerX = window.innerWidth / 2; // 화면 가로 중앙
      const centerY = window.innerHeight / 2; // 화면 세로 중앙

      // 마우스 위치에 따른 이동 거리 계산
      const rotateY = ((clientX - centerX) / centerX) * movementFactor;
      const rotateX = ((centerY - clientY) / centerY) * movementFactor;

      // GSAP을 사용하여 부드럽게 이동
      gsap.to(box, {
        rotationY: rotateY,
        rotationX: rotateX,
        transformPerspective: 800,
        transformOrigin: &quot;center&quot;,
        duration: 0.3,
        ease: &quot;power3.out&quot;
      });
    });

    // 마우스가 영역을 벗어날 때 요소 원위치로 복귀
    wrapper.addEventListener(&quot;mouseleave&quot;, () =&gt; {
      gsap.to(box, {
        rotationY: 0,
        rotationX: 0,
        duration: 0.3,
        ease: &quot;power3.out&quot;
      });
    });
  });
&lt;/script&gt;</code></pre><br>

<hr>
<br>

<h2 id="➕-마우스-움직임에-따른-단순한-요소-움직임-rotate-효과-적용x">➕ 마우스 움직임에 따른 단순한 요소 움직임 (rotate 효과 적용x)</h2>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/0e50e754-bb06-466a-bfab-694e134d5604/image.gif" alt="마우스 방향에 따라 요소 움직이기"></p>
<p>금융사이트에서 많이 보이고, 개인적으로 포트폴리오에서도 많이 사용되는 효과 중 하나라고 생각한다.
첫 번째 예제가 마우스 움직임에 따라 요소가 따라 움직이며 회전도 했던 모션이었다면, 두 번째 예제는 마우스 움직임에 따라 요소가 따라 움직이기만 하는 단순한 모션이다.</p>
<p>기존 스크립트에서 회전값을 설정해주던 코드만 제거하면 된다.
아주 깔끔하고 쉽다.</p>
<h3 id="👉🏻-스크립트는-이렇게-js-es6-gsap-1">👉🏻 스크립트는 이렇게! (JS ES6, GSAP)</h3>
<p>🩵 x: deltaX / y: deltaY
🔹 rotate가 아닌 x값을 변경 / rotate가 아닌 y값을 변경</p>
<pre><code>&lt;script&gt;
document.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {
    const wrapper = document.querySelector(&quot;.wrapper&quot;);
    const box = document.querySelector(&quot;.js-3dbox&quot;);
    const movementFactor = 50; // 이동 강도 설정 (값이 클수록 더 많이 움직임)

    wrapper.addEventListener(&quot;mousemove&quot;, (e) =&gt; {
      const { clientX, clientY } = e;
      const centerX = window.innerWidth / 2;
      const centerY = window.innerHeight / 2;

      const deltaX = ((clientX - centerX) / centerX) * movementFactor;
      const deltaY = ((clientY - centerY) / centerY) * movementFactor;

      gsap.to(box, {
        x: deltaX,  // rotate가 아닌 x값을 변경
        y: deltaY,  // rotate가 아닌 y값을 변경
        duration: 0.3,
        ease: &quot;power3.out&quot;
      });
    });

    wrapper.addEventListener(&quot;mouseleave&quot;, () =&gt; {
      gsap.to(box, {
        x: 0,
        y: 0,
        duration: 0.3,
        ease: &quot;power3.out&quot;
      });
    });
  });
&lt;/script&gt;</code></pre><hr>
<p>자료 참고 : <a href="https://wsss.tistory.com/1148">https://wsss.tistory.com/1148</a>
참고한 스크립트는 이해하기 어려워서, GSAP을 활용한 요소 움직임을 구현하는 스크립트로 개선</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[canvas] canvas 한 번에 알아보기]]></title>
            <link>https://velog.io/@choii_ii/canvas-canvas-%ED%95%9C-%EB%B2%88%EC%97%90-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@choii_ii/canvas-canvas-%ED%95%9C-%EB%B2%88%EC%97%90-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 31 Mar 2025 15:56:46 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-canvas가-뭐야">📌 canvas가 뭐야?!</h2>
<p>🩵 canvas는 HTML5에서 도입된 요소로, <strong>픽셀 기반의 그래픽을 동적으로 그릴 수 있는 영역을 제공</strong>한다. <strong>2D와 3D 그래픽을 그리기 위한 API를 지원</strong>하고, JavaScript를 사용해 <strong>그림을 그리거나 애니메이션을 구현</strong>할 수 있다.
<strong><a href="https://developer.mozilla.org/ko/docs/Web/API/Canvas_API" target="_blank">📍canvas mdn 바로가기</a></strong></p>
<hr>
<h2 id="📌-canvas-특성">📌 canvas 특성</h2>
<p>🩵 canvas는 각 픽셀을 개별적으로 제어하며 <strong>복잡한 도형, 애니메이션, 이미지 등 다양한 시각적 요소를 렌더링 할 수 있다.</strong></p>
<p>🩵 JavaScript의 <strong>2d(context)나 3d(WebGL) 을 이용해 그래픽을 그리거나 조작</strong>할 수 있다.</p>
<p>🩵 캔버스에서 그려지는 그래픽은 <strong>화면 해상도와 관계없이 독립적으로 동작</strong>하여 고해상도의 그래픽을 구현할 수 있으며, <strong>canvas.width/canvas.height 속성으로 크기를 조정</strong>할 수 있다.</p>
<p>🩵 <strong>그림을 그리는 도구들(선, 원, 사각형 등)을 사용하여 복잡한 도형을 그릴 수 있다.</strong> 또한, 이미지를 캔버스에 그린 후 필터 효과(예: 회전, 크기 조정, 색상 변경 등)를 적용할 수 있다.</p>
<p>🩵 주로, 2d 게임에서의 캐릭터 및 배경을 그릴 때, 차트 및 그래프를 구현할 때, 인터랙션을 구현할 때 등 사용된다.</p>
<hr>
<h2 id="📌-canvas-기본-사용법">📌 canvas 기본 사용법</h2>
<h3 id="🩵-html5">🩵 HTML5</h3>
<p>🔹 <code>&lt;canvas id=&quot;myCanvas&quot; width=&quot;500&quot; height=&quot;500&quot;&gt;&lt;/canvas&gt;</code>
🔸 <strong>HTML 문서에 canvas 태그를 추가</strong>하고, <strong>기본 크기(width/height)를 지정</strong>하여 그래픽을 그릴 수 있는 공간을 설정한다. (코드 가독성을 위해 인라인으로 명시하는 편)</p>
<h3 id="🩵-js-es6-canvas에-접근하기">🩵 JS ES6 (canvas에 접근하기!)</h3>
<p>🔹 <code>const canvas = document.querySelector(&#39;canvas&#39;);</code>
🔹 <code>const ctx = canvas.getContext(&#39;2d&#39;);</code>
🔸 <strong>ctx(=context)</strong> : canvas에서 그리기 작업을 처리할 수 있는 객체, <strong>일종의 브러쉬</strong>라고 생각하면 이해가 쉽다.
🔸 <strong>getContext()</strong> : 캔버스의 context를 얻고 <strong>2d와 3d(WebGL) 옵션</strong>이 있다. (대문자 D아니고 소문자 d!)
🔹 <code>canvas.width=800;</code>
🔹 <code>canvas.height=800;</code>
🔸 css로 설정해준 <strong>canvas 크기와 별개로 JS에서 한 번더 설정</strong>해야한다.</p>
<p>즉, 아래와 같이 작성하는 것이 기초 셋팅!</p>
<pre><code>&lt;script&gt;
    const canvas = document.querySelector(&#39;canvas&#39;);
    const ctx = canvas.getContext(&#39;2d&#39;);
    canvas.width=800;
    canvas.height=800;
&lt;/script&gt;</code></pre><hr>
<h2 id="📌-canvas로-그림-그리기">📌 canvas로 그림 그리기</h2>
<h3 id="🩵-사각형-그리기-rectfillrect">🩵 사각형 그리기 (rect/fillRect)</h3>
<p>🔸 <code>rect(x, y, width, height)</code> : <strong>그림을 그릴 좌표(x, y)</strong>와 <strong>도형 크기(width, height)</strong>를 설정
🔹 <strong>rect()는 단순히 허공에 그리는 셈.</strong> 도형, 선을 그릴 때 <strong>항상 stroke 또는 fill로 선 그리기, 채우기</strong>를 해주어야 도형이 정상적으로 그려진다. </p>
<p>🔸 <code>fillRect(x, y, width, height)</code> : <strong>rect()와 fill() 함수를 동시에</strong> 동작! 즉, 도형을 그림과 동시에 색을 채움.</p>
<p>🔸 <code>strokeRect(x, y, width, height)</code> : <strong>rect()와 stroke() 함수를 동시에</strong> 동작! 즉, 도형을 그림과 동시에 선을 그림.</p>
<p>🔸 <strong>context는 순서를 가지기 때문</strong>에 <strong>style을 먼저 설정 후, 그리기를 진행</strong>해야한다.
🔹 <code>ctx.lineWidth=2;</code> : <strong>선 굵기</strong>
🔹 <code>ctx.strokeColor=&quot;#ddd&quot;;</code> : <strong>선 색상 **
🔹 <code>ctx.fillStyle=&quot;pink&quot;;</code> : **채우기 색상</strong></p>
<p><strong>🔸 예제) rect 속성으로 사각형 그리기</strong>
<img src="https://velog.velcdn.com/images/choii_ii/post/9a220170-f51f-4d94-8360-050e667f5f8f/image.png" alt="rect 속성으로 사각형 그리기"></p>
<pre><code>&lt;script&gt;
// canvas 기본세팅
const canvas = document.querySelector(&#39;canvas&#39;);
const ctx = canvas.getContext(&#39;2d&#39;);
canvas.width = 500;
canvas.height = 500;
ctx.fillStyle = &quot;skyblue&quot;;

ctx.rect(50,50,100,100); // 첫 번째 도형
ctx.rect(150,150,100,100); // 두 번째 도형
ctx.rect(250,250,100,100); // 세 번째 도형
ctx.fill(); // 배경색 채우기
// ctx.stroke(); // 선으로 그리기
&lt;/script&gt;</code></pre><h3 id="🩵-직선-그리기-movetolinetostroke">🩵 직선 그리기 (moveTo/lineTo/stroke)</h3>
<p>🔸 moveTo(), lineTo(), stroke() 메서드를 사용하여 직선을 그린다.
🔹 <code>ctx.moveTo(x, y);</code> : <strong>그리기를 시작할 지점</strong>으로 좌표 이동(x축, y축). 즉, 브러쉬를 이동하는 개념이라고 생각하면 이해하기 쉽다.
🔹 <code>ctx.lineTo(x, y);</code> : <strong>선을 그으면서 좌표 이동</strong>(x축, y축)
🔹 <code>ctx.stroke();</code> : 경로를 따라 선 그리기. <strong>해당코드를 반드시 써줘야 선을 그릴 수 있음!</strong>
🔹 <code>ctx.fill();</code> : 직선으로 그린 도형을 채우고 싶다면 fill 속성으로 <strong>배경색을 채울 수 있다.</strong>
🔹 <code>ctx.lineCap=&quot;round&quot;;</code> : <strong>선 끝 모양</strong>을 설정 (butt, round, square)</p>
<p>🔸 <strong>context는 순서를 가지기 때문</strong>에 <strong>style을 먼저 설정 후, 그리기를 진행</strong>해야한다.
🔹 <code>ctx.lineWidth=2;</code> : <strong>선 굵기</strong>
🔹 <code>ctx.strokeColor=&quot;#ddd&quot;;</code> : <strong>선 색상</strong> 
🔹 <code>ctx.fillStyle=&quot;pink&quot;;</code> : <strong>채우기 색상</strong></p>
<p><strong>🔸 예제) 직선으로 사각형 그리기 (stroke)</strong>
<img src="https://velog.velcdn.com/images/choii_ii/post/8cd4bf66-5ec1-490c-bee0-478267b65596/image.png" alt="직선으로 사각형 그리기"></p>
<pre><code>&lt;script&gt;
// canvas 기본세팅
const canvas = document.querySelector(&#39;canvas&#39;);
const ctx = canvas.getContext(&#39;2d&#39;);
canvas.width = 500;
canvas.height = 500;


function onDrawLine(){
    // 선 굵기, 색상 등 style 먼저 설정하기
    ctx.lineWidth=3;
    ctx.strokeStyle=&quot;red&quot;;

    ctx.moveTo(50,50); // (50, 50) 좌표로 이동 (출발점 설정)
    ctx.lineTo(150, 50); // (50,50) → (150,50) 좌표 이동하면서 선 그리기(윗변)
    ctx.lineTo(150, 150); // (150,50) → (150,150) 좌표 이동하면서 선 그리기(우변)
    ctx.lineTo(50, 150); // (150,150) → (50,150) 좌표 이동하면서 선 그리기(밑변)
    ctx.lineTo(50, 50);  // (50,150) → (50,50) 좌표 이동하면서 선 그리기 (시작점으로 돌아옴)
    ctx.stroke(); //선 그리기
}

canvas.addEventListener(&quot;click&quot;, onDrawLine);
&lt;/script&gt;</code></pre><p><strong>🔸 예제) 직선으로 사각형 그리기 + 색상 채우기 (fill)</strong>
<img src="https://velog.velcdn.com/images/choii_ii/post/4ffd4306-ca98-4ab8-ad13-df7f4555479b/image.png" alt="직선으로 사각형 그리기 + 색상 채우기"></p>
<pre><code>&lt;script&gt;
// canvas 기본세팅
const canvas = document.querySelector(&#39;canvas&#39;);
const ctx = canvas.getContext(&#39;2d&#39;);
canvas.width = 500;
canvas.height = 500;


function onDrawLine(){
    // 선 굵기, 색상 등 style 먼저 설정하기
    ctx.lineWidth=3;
    ctx.fillStyle=&quot;pink&quot;;

    ctx.moveTo(50,50); // (50, 50) 좌표로 이동 (출발점 설정)
    ctx.lineTo(150, 50); // (50,50) → (150,50) 좌표 이동하면서 선 그리기(윗변)
    ctx.lineTo(150, 150); // (150,50) → (150,150) 좌표 이동하면서 선 그리기(우변)
    ctx.lineTo(50, 150); // (150,150) → (50,150) 좌표 이동하면서 선 그리기(밑변)
    ctx.lineTo(50, 50);  // (50,150) → (50,50) 좌표 이동하면서 선 그리기 (시작점으로 돌아옴)
    ctx.fill(); //선 채우기
}

canvas.addEventListener(&quot;click&quot;, onDrawLine);
&lt;/script&gt;</code></pre><p><strong>🔸 예제) 도형 그리기(stroke) + 도형 색 채우기(fill)</strong>
🔹 <code>ctx.beginPath();</code> : 하나의 context에서 그려지는 <strong>path를 분리</strong>시키기 위한 속성. <strong>서로 다른 style을 지정하거나 독립시킬 때 사용!</strong> 이전 경로(path)가 초기화 되기 때문에 <strong>moveTo()를 먼저 사용</strong>해야 한다. <strong>그렇지 않으면 새로운 경로의 시작점이 설정되지 않아서 lineTo()가 정상적으로 작동하지 않는다.</strong>
🔹 <strong>beginPath()를 쓰지 않았다면, 모든 도형이 빨간색으로 채워진다.</strong> 이유는 <strong>그림 모두가 같은 경로의 일부이기 때문!</strong></p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/80be5b5d-bc8b-4387-b955-4f9380c8fdb4/image.png" alt="도형 그리기(stroke) + 도형 색 채우기(fill)"></p>
<pre><code>&lt;script&gt;
// canvas 기본세팅
const canvas = document.querySelector(&#39;canvas&#39;);
const ctx = canvas.getContext(&#39;2d&#39;);
canvas.width = 500;
canvas.height = 500;

// 1, 2, 3 stroke 도형
ctx.rect(50,50,100,100);
ctx.rect(150,150,100,100);
ctx.rect(250,250,100,100);
ctx.stroke(); //위 그림에 선으로 채우기

// 빨간색 fill 도형
ctx.beginPath(); // path를 분리하여 새 그림을 그림
ctx.rect(350,350,100,100);
ctx.fillStyle=&quot;red&quot;; 
ctx.fill();
&lt;/script&gt;
</code></pre><p><strong>🔸 예제) 원 그리기 ** 
🔹 <code>ctx.arc(x, y, radius(반지름), starting angle, ending angle);</code> 
🔹 2<em>Math.PI : 완벽한 원을 만드는 공식
🔸 ctx.arc(x, y, radius(반지름), 0, 2</em>Math.PI); : *<em>시작점 0, 끝점 2</em>Math.PI이 가장 완벽한 원을 만드는 공식!</strong>
🔸 ctx.arc(260, 80, 8, 1<em>Math.PI, 2</em>Math.PI); : <strong>반원</strong>을 만드는 공식!</p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/f4418e1b-12bc-4667-992c-1b647804c8dc/image.png" alt="원을 만드는 공식!"></p>
<hr>
<h2 id="📌-canvas-속성">📌 canvas 속성</h2>
<p>🩵 ctx<strong>.font</strong> = &quot;48px serif&quot;; : 폰트 관련 style 설정 (fontWeight, fontSize, fontFamily)</p>
<p>🩵 ctx.save() : ctx의 <strong>기존 상태, 색상, 스타일 등 모든 것을 저장</strong>
🩵 ctx.restore() : <strong>save()와 restore() 사이에 일시적으로 변경할 코드를 쓰고 restore()을 호출하면 수정이 완료</strong>된다. 즉, 기존 설정된 스타일과 다른 스타일을 주고 싶을 때 save()와 restore() 사이에서 설정해줌</p>
<pre><code>&lt;script&gt;
 if(text !== &quot;&quot;){
    ctx.save(); // ctx의 현재 상태, 색상, 스타일 등 모든 것을 저장
    ctx.lineWidth=1;
    ctx.font = &quot;48px serif&quot;; // weight, size, font-family를 지정
    ctx.restore(); // 수정 완료. save와 restore사이의 코드는 변수에 저장되지 않음(임시적이라는..)
}
&lt;/script&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JS ES6] To-do list (+localStorage)]]></title>
            <link>https://velog.io/@choii_ii/JS-ES6-To-do-list-localStorage</link>
            <guid>https://velog.io/@choii_ii/JS-ES6-To-do-list-localStorage</guid>
            <pubDate>Wed, 26 Mar 2025 12:25:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/30b3ab47-7ea5-4df2-bc06-d3d8002e6af2/image.gif" alt="To-do list"></p>
<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 리스트 등록 시, 입력한 input value값을 그대로 리스트에 추가
💚 리스트 삭제 시, 선택한 리스트를 찾아 삭제해야하는 로직
💚 createElement()와 append() 메서드를 활용하여 스크립트로 DOM 요소 생성
💚 선택한 리스트를 매칭 시키기 위해 id값으로 구분이 필요 (Date.now()라는 생성자 함수 사용)
💚 filter 속성을 사용하여 id 값이 매칭되는 리스트 구분
💚 localStorage에 텍스트 타입으로 저장시키기 위해 JSON.stringify() 속성 사용
💚 텍스트 타입으로 저장된 localStorage 데이터 값을 다시 배열로 저장시키기 위해 JSON.parse() 속성 사용</p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 텍스트를 입력할 input 폼 생성</p>
<p>🩵 저장된 list를 노출 시킬 리스트 영역 생성</p>
<pre><code>&lt;body&gt;
 &lt;form id=&quot;todo-form&quot;&gt;
    &lt;input type=&quot;text&quot; placeholder=&quot;Write a To Do and Press Enter&quot; required&gt;
 &lt;/form&gt;
 &lt;ul id=&quot;todo-list&quot;&gt;&lt;/ul&gt;
&lt;/body&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-js-es6">👉🏻 스크립트는 이렇게! (JS ES6)</h2>
<h3 id="1️⃣-painttodo-함수-실행">1️⃣ paintTodo() 함수 실행</h3>
<p>🩵 To-do list에 새로운 항목을 추가
🔹 등록된 리스트는 li &gt; span + button 구조로 마크업되도록 createElement 메서드를 사용</p>
<pre><code>&lt;script&gt;
function paintTodo(newTodo){
    // 전달받을 newTodo는 handleTodoSubmit함수에서 실행시킨 todoInput.value값을 의미

    const li = document.createElement(&quot;li&quot;);
    li.id = newTodo.id;

    const span = document.createElement(&quot;span&quot;);
    span.innerText = newTodo.text;

    const deleteBtn = document.createElement(&quot;button&quot;);
    deleteBtn.innerText = &quot;❌&quot;;
    deleteBtn.addEventListener(&quot;click&quot;, deleteTodo);

    li.append(span);
    li.append(deleteBtn);
    todoList.append(li);
}
&lt;/script&gt;</code></pre><h3 id="2️⃣-handletodosubmit-함수-실행">2️⃣ handleTodoSubmit() 함수 실행</h3>
<p>🩵 input이 submit되면 handleTodoSubmit() 함수 실행
🔹 입력한 value 값을 newTodo 변수에 저장 (리스트에 value 값을 노출시켜야하기 때문)
🔹 input이 submit되면 value 값을 공백으로 리셋
🔹 등록한 리스트(text)를 newTodoObj 객체로 변환하여 todos 배열에 추가
    🔹 <strong>id는 리스트 삭제 시, 클릭한 리스트와 매칭 시켜야 하기 때문에 임의로 Date.now(등록시간) 값을 부여</strong>
🔹 paintTodo()함수 실행 시, 인수로 newTodoObj를 추가 (=새로운 할 일 추가)
🔹 saveTodos()를 호출하여 localStorage에 저장</p>
<pre><code>&lt;script&gt;
function handleTodoSubmit(event){
    event.preventDefault();

    const newTodo = todoInput.value;
    todoInput.value = &quot;&quot;; // 입력 후 공백으로 업데이트

    //newTodo를 todos에 배열(array)로 저장
    const newTodoObj = {
        text:newTodo,
        id:Date.now(),
    }
    todos.push(newTodoObj);

    paintTodo(newTodoObj);  // todos 배열을 list로 생성
    saveTodos();
}

todoForm.addEventListener(&quot;submit&quot;, handleTodoSubmit);
&lt;/script&gt;</code></pre><h3 id="3️⃣-deletetodo-함수-실행">3️⃣ deleteTodo() 함수 실행</h3>
<p>🩵 삭제 버튼을 클릭하면 deleteTodo() 함수 실행 (=리스트 삭제)
🔹 handleTodoSubmit()함수에서는 id는 리스트 삭제 시, 클릭한 리스트와 매칭 시켜야 하기 때문에 임의로 Date.now(등록시간) 값을 부여했었다.
🔹 <strong>클릭한 리스트의 부모요소</strong>를 li라는 변수에 저장
🔹 todos 배열 중 <strong>filter 속성</strong>을 사용하여 <strong>todo.id값과 li.id값이 다른 리스트만 todos에 저장</strong> (즉, 삭제버튼을 클릭한 해당 li의 id와 일치하는 항목을 제외하고 나머지 할 일만 남김)
🔹 saveTodos()를 호출하여 변경된 목록을 저장</p>
<pre><code>&lt;script&gt;
function deleteTodo(event){
    // console.log(&quot;delete click!&quot;);
    const li = event.target.parentElement;
    // event(클릭)대상의 부모요소

    todos = todos.filter(function(todo){
        return todo.id !== parseInt(li.id);
    })
    li.remove();
    // console.log(li.id);
    saveTodos();
};
&lt;/script&gt;
</code></pre><h3 id="4️⃣-savetodos-함수-실행">4️⃣ saveTodos() 함수 실행</h3>
<p>🩵 리스트를 영구적으로 localStorage에 저장시키기 위한 함수
🔹 localStorage.setItem()을 사용하여 todos 배열을 문자열로 변환한 후 저장. (배열은 <strong>JSON.stringify()</strong>를 사용해 텍스트로 변환)</p>
<pre><code>&lt;script&gt;
function saveTodos(){
    localStorage.setItem(TODOS_KEY, JSON.stringify(todos)); 
    //[&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]처럼 텍스트 타입으로 저장됨
}
&lt;/script&gt;</code></pre><h3 id="5️⃣-페이지-로드-시-기존-할-일-목록-가져오기">5️⃣ 페이지 로드 시, 기존 할 일 목록 가져오기</h3>
<p>🩵 페이지가 로드될 때, localStorage.getItem(todos)을 사용하여 저장된 데이터를 가져오고, <strong>JSON.parse()</strong>로 다시 객체 배열로 변환
🔹 JSON.stringify() : 배열/객체데이터를 &quot;텍스트&quot;타입으로 변환
🔹 JSON.parse() : 텍스트를 &quot;배열&quot;로 변환</p>
<p>🩵 localStorage에 저장된 값이 존재한다면
🔹 변환된 데이터를 todos 배열에 할당하고, paintTodo()로 목록을 다시 그립니다.
🔹 forEach()**는 배열의 모든 항목에 대해 반복 작업을 수행할 때 사용. localStorage에서 가져온 할 일 목록을 화면에 표시할 때 사용</p>
<pre><code>&lt;script&gt;
const savedTodos = localStorage.getItem(TODOS_KEY);
// console.log(savedTodos);

if(savedTodos !== null){
    const parsedTodos = JSON.parse(savedTodos); 
    // console.log(parsedTodos);
    todos = parsedTodos;
    parsedTodos.forEach(paintTodo);
}
&lt;/script&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JS ES6] OpenWeather API 활용하기]]></title>
            <link>https://velog.io/@choii_ii/JS-ES6-OpenWeather-API-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choii_ii/JS-ES6-OpenWeather-API-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Mar 2025 09:39:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/76245fc6-94e6-474a-9ecd-a1da1664d1ac/image.png" alt="위치+날씨 API 활용하기"></p>
<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 navigator.geolocation.getCurrentPosition() : 현재 위치(위도, 경도)를 가져오기
💚 OpenWeather API를 통해 해당 위치의 날씨 정보를 가져오기</p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 온도와 지역위치를 노출시킬 요소 생성
🔹 자세한 데이터 값은 weather API를 통해 불러올 것!</p>
<pre><code>&lt;body&gt;
    &lt;div id=&quot;weather&quot;&gt;
        &lt;span&gt;&lt;/span&gt;
        &lt;span&gt;&lt;/span&gt;
    &lt;/div&gt;
&lt;/body&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-js-es6">👉🏻 스크립트는 이렇게! (JS ES6)</h2>
<p>🩵 <strong>navigator.geolocation.getCurrentPosition(성공 함수, 실패 함수)</strong>
🔹 <strong>브라우저에서 제공하는 위치 정보를 가져오는 메서드</strong>
🔹 position.coords<strong>.latitude : 위도</strong>
🔹 position.coords<strong>.longitude : 경도</strong></p>
<p>🩵 <strong>onGeoOk(position) 함수 설명 : 위치를 성공적으로 가져오면 해당 함수 실행</strong>
🔹 <strong>position은 위치 정보를 담고 있는 객체</strong>
🔹 사용자의 위도(latitude), 경도(longitude) 등 위치 관련 정보를 불러와 변수에 저장
🔹 OpenWeather API를 사용하기 위한 <strong>API 키 값을 가져와 변수에 저장</strong>
🔹 OpenWeather API의 <strong>날씨 정보 요청 URL을 만듦</strong>
    🔹 lat=${lat}&amp;lon=${lng} : 사용자 위치(위도, 경도)를 기반으로 날씨 데이터 요청
    🔹 appid=${API_KEY} : API 키 추가
    🔹 units=metric : 섭씨 온도로 데이터 요청
🔹 <strong>fetch(url) : 해당 URL로 API 요청을 보냄</strong> (실제로 URL 이동할 필요 없이 스크립트가 대신 URL을 부름)
🔹 API 요청에 따른 데이터를 받으면, .name (지역명)과 .main.temp (온도) 값을 출력하여 text요소로서 HTML에 반환</p>
<p>🩵 <strong>onGeoError() 함수 설명 : 위치 가져오는 것을 실패했을 때, 해당 함수 실행</strong></p>
<pre><code>&lt;script&gt;
// navigator.geolocation.getCurrentPosition(성공 함수, 실패 함수);

const API_KEY = &quot;OpenWeather에서 부여받은 API 키 값&quot;;

// 성공 함수
function onGeoOk(position){
    // console.log(position);

    const lat = position.coords.latitude; // 위도
    const lng = position.coords.longitude; // 경도

    const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&amp;lon=${lng}&amp;appid=${API_KEY}&amp;units=metric`;

    fetch(url)
    .then(function(response){
        return response.json()
    })
    .then(function(data){
        const weather = document.querySelector(&quot;#weather span:first-child&quot;);
        const city = document.querySelector(&quot;#weather span:last-child&quot;);

        city.innerText = data.name;
        weather.innerText =  `${Math.round(data.main.temp)} / ${data.weather[0].main}`;

    }); 
}

function onGeoError(){
    alert(&quot;Can&#39;t find you. No weather for you.&quot;);
}

navigator.geolocation.getCurrentPosition(onGeoOk, onGeoError);
&lt;/script&gt;</code></pre><p>이렇게 API를 통해 위치와 날씨 정보를 가져오면 아래와 같이 데이터 값이 노출된다.</p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/c662151d-af96-48c1-8381-82eccb3d3587/image.png" alt="위치+날씨 API 활용하기"></p>
<hr>
<h2 id="📌-fetch란">📌 fetch란?</h2>
<p>💚 fetch는 브라우저에서 기본 제공하는 메서드로, <strong>웹 서버와 비동기적으로 데이터를 주고받기 위해 사용</strong>된다.
🔸 <strong>비동기란?</strong> : <strong>데이터를 요청한 후, 결과를 기다리지 않고 다른 작업을 먼저 처리</strong>할 수 있다는 의미이며,이를 통해 사용자 경험이 더 부드럽고, 앱이 더 빠르게 동작할 수 있다.</p>
<p>💚 <strong>fetch 기본 문법</strong>
🔹 url: 데이터를 요청할 서버의 URL
🔹 options: 요청을 더 구체적으로 설정할 수 있는 선택 사항 (예: HTTP 메서드, 헤더 등)</p>
<pre><code>fetch(url, [options])
  .then(response =&gt; response.json())  // 응답 처리
  .then(data =&gt; console.log(data))    // 실제 데이터 처리
  .catch(error =&gt; console.log(error)); // 오류 처리</code></pre><h3 id="✅-fetch-동작-방식">✅ fetch() 동작 방식</h3>
<p>1️⃣ <strong>fetch(url): URL로 요청을 보냄</strong>. 이때 fetch()는 데이터를 바로 반환하지 않고, <strong>&quot;나중에 결과를 줄게!&quot;</strong>라는 약속(Promise)을 먼저 반환한다. 그래서 데이터를 비동기적으로 처리할 수 있는 것!</p>
<p>2️⃣ <strong>response.json()</strong> : 응답이 JSON 형식이라면, <strong>JSON 형태로 파싱</strong>해서 반환</p>
<p>3️⃣ <strong>then(data)</strong> : <strong>요청이 성공</strong>적으로 완료되면 <strong>응답(response)</strong>이 then(data)을 통해 실제 데이터를 처리!</p>
<p>4️⃣ <strong>catch(error)</strong> : <strong>오류가 발생</strong>했을 때, 오류를 처리하는 부분. 예를 들어, 서버 연결이 안 됐거나, 잘못된 URL을 요청했을 때 실행된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS ES6] 명언(문구) 랜덤 출력]]></title>
            <link>https://velog.io/@choii_ii/JS-ES6-%EB%AA%85%EC%96%B8%EB%AC%B8%EA%B5%AC-%EB%9E%9C%EB%8D%A4-%EC%B6%9C%EB%A0%A5</link>
            <guid>https://velog.io/@choii_ii/JS-ES6-%EB%AA%85%EC%96%B8%EB%AC%B8%EA%B5%AC-%EB%9E%9C%EB%8D%A4-%EC%B6%9C%EB%A0%A5</guid>
            <pubDate>Wed, 26 Mar 2025 08:37:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/f9c546a7-0235-49e5-9115-89172c1f7b24/image.gif" alt="명언(문구) 랜덤 출력"></p>
<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 다수의 명언을 랜덤 출력할 것
💚 <strong>Math.random()을 활용하여 인덱스를 랜덤으로 추출</strong></p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 명언과 작가의 정보를 받아올 span 태그 준비! 실질적인 텍스트는 스크립트로 받아올 것임.</p>
<pre><code>&lt;body&gt;
 &lt;div id=&quot;quote&quot;&gt;
        &lt;span&gt;&lt;/span&gt;
        &lt;span&gt;&lt;/span&gt;
    &lt;/div&gt;
&lt;/body&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-js-es6">👉🏻 스크립트는 이렇게! (JS ES6)</h2>
<p>🩵 quotes 라는 <strong>배열에 10개의 명언 객체를 저장</strong></p>
<p>🩵 quotes 배열에서 랜덤한 명언을 선택 (<strong>랜덤한 배열 인덱스 만들기</strong>)
🔹 <strong>Math.random() : 0 이상 1 미만 범위에서 실수를 랜덤으로 반환</strong>
🔹 quotes.length를 곱하는 이유 : Math.random()이 반환하는 값에 quotes.length를 곱하면, <strong>0 이상 quotes.length 미만의 랜덤한 실수가 반환</strong>
🔹 여기서 Math.floor() 를 써주면 내림 처리가 되어 0, 1, 2로 반환되어 변수에 저장된다.
🔹 최종적으로, 랜덤한 텍스트가 todaysQuote에 저장</p>
<h3 id="📌-math-속성-한-번에-주워가기">📌 Math 속성 한 번에 주워가기</h3>
<p>💚 Math.random() : 0 이상 1 미만 사이의 랜덤 숫자를 반환 (소수점 단위)
🔸 Math.random() * 10 : 10 미만의 숫자를 반환하기 위해 곱하기 10</p>
<p>💚 Math<strong>.round</strong>(1.8) : 2로 <strong>반올림</strong>하여 반환
💚 Math<strong>.ceil</strong>(1.1) : 2로 <strong>올림</strong>하여 반환
💚 Math<strong>.floor</strong>(1.6) : 1로 <strong>내림</strong>하여 반환</p>
<pre><code>&lt;script&gt;
const quotes = [
    {
        quote : &quot;The only way to do great work is to love what you do.&quot;,
        author : &quot;Steve Jobs&quot;
    },
    {
        quote : &quot;Do what you can, with what you have, where you are.&quot;,
        author : &quot;Theodore Roosevelt&quot;
    },
    {
        quote : &quot;It always seems impossible until it’s done.&quot;,
        author : &quot;Nelson Mandela&quot;
    },
    {
        quote : &quot;Happiness depends upon ourselves.&quot;,
        author : &quot;Aristotle&quot;
    },
    {
        quote : &quot;Success is not final, failure is not fatal: it is the courage to continue that counts.&quot;,
        author : &quot;Winston Churchill&quot;
    },
    {
        quote : &quot;If you want to go fast, go alone. If you want to go far, go together.&quot;,
        author : &quot;African Proverb&quot;
    },
    {
        quote : &quot;In the middle of every difficulty lies opportunity.&quot;,
        author : &quot;Albert Einstein&quot;
    },
    {
        quote : &quot;Life is really simple, but we insist on making it complicated.&quot;,
        author : &quot;Confucius&quot;
    },
    {
        quote : &quot;Do what you feel in your heart to be right – for you’ll be criticized anyway.&quot;,
        author : &quot;Eleanor Roosevelt&quot;
    },
    {
        quote : &quot;The best way to predict the future is to create it.&quot;,
        author : &quot;Peter Drucker&quot;
    }
]
const quote = document.querySelector(&quot;#quote span:first-child&quot;);
const author = document.querySelector(&quot;#quote span:last-child&quot;);


const todaysQuote = quotes[Math.floor(Math.random() * quotes.length)];

quote.innerText = todaysQuote.quote;
author.innerText = `- ${todaysQuote.author} -`;
&lt;/script&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JS ES6] Background-Image 랜덤 변경]]></title>
            <link>https://velog.io/@choii_ii/JS-ES6-Background-Image-%EB%9E%9C%EB%8D%A4-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@choii_ii/JS-ES6-Background-Image-%EB%9E%9C%EB%8D%A4-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Wed, 26 Mar 2025 06:51:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/234be864-9302-4c4e-98ca-ceca3244b347/image.gif" alt="Background-Image 랜덤 변경"></p>
<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 <strong>Math.random()</strong>을 활용하여 <strong>인덱스를 랜덤으로 추출</strong>하기
💚 <strong>createElement()</strong>를 통해 스크립트로 <strong>요소를 생성</strong>하고 <strong>.append()를 활용하여 문서에 삽입하기</strong></p>
<hr>
<h2 id="👉🏻-스크립트는-이렇게-js-es6">👉🏻 스크립트는 이렇게! (JS ES6)</h2>
<p>🩵 배경으로 사용할 이미지 3장을 준비하고, <strong>파일명을 배열로 나열</strong>한다. <strong>(src 속성 연결을 해야하기 때문에 파일명으로 나열)</strong></p>
<p>🩵 이미지 요소를 랜덤으로 추출하기 위해 인덱스 활용
🔹 <strong>Math.random() : 0 이상 1 미만 범위에서 실수를 랜덤으로 반환</strong>
🔹 images.length를 곱하는 이유 : Math.random()이 반환하는 값에 <strong>images.length를 곱하면, 0 이상 images.length 미만의 랜덤한 실수가 반환</strong>
🔹 여기서 <strong>Math.floor() 를 써주면 내림 처리</strong>가 되어 <strong>0, 1, 2로 반환</strong>되어 변수에 저장된다.
🔹 최종적으로, 랜덤한 이미지가 krImage에 저장</p>
<p>🩵 <strong>createElement() : 새로운 요소를 생성</strong></p>
<p>🩵 bgImage.src : 생성한 img요소의 src(이미지 경로)를 설정하는 속성</p>
<p>🩵 <strong>.append()</strong> : 스크립트로 생성한 img 요소를 <strong>대상요소 뒤에 생성</strong>시키기
🔹 <strong>.pretend()</strong> : <strong>대상요소 앞에 생성</strong></p>
<pre><code>&lt;script&gt;
const images = [&#39;img01.jpg&#39;, &#39;img02.jpg&#39;, &#39;img03.jpg&#39;];

const krImage = images[Math.floor(Math.random() * images.length)];

// .createElement() : 태그 생성
const bgImage = document.createElement(&quot;img&quot;);

bgImage.classList.add(&quot;bgimg&quot;);

// console.log(bgImage);
bgImage.src = `img/${krImage}`;

document.body.append(bgImage);
&lt;/script&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JS ES6] Log-in 효과]]></title>
            <link>https://velog.io/@choii_ii/JS-ES6-Log-in-%ED%9A%A8%EA%B3%BC</link>
            <guid>https://velog.io/@choii_ii/JS-ES6-Log-in-%ED%9A%A8%EA%B3%BC</guid>
            <pubDate>Tue, 25 Mar 2025 09:59:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/2e9449b0-0760-40e7-a757-c78fd1365551/image.gif" alt="Log-in 기능"></p>
<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 input 속성 : required(필수입력), maxlength=&quot;15&quot;(최대 글자수)
💚 input으로 입력한 <strong>이름/아이디 값을 키-값 구조로 localStorage에 저장하여 새로고침하여도 유지될 수 있도록 저장값 활용</strong></p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 이름/아이디를 입력할 input 생성
🔹 login 버튼은 button 태그로 생성해도 된다. 참고로 form 태그 안에서 button 태그는 기본 타입속성이 submit이다.</p>
<p>🩵 그리고 이름/아이디를 입력하고 &#39;Welcome, ~&#39; 문구를 대신 띄울 것이기 때문에 텍스트를 노출시킬 요소도 생성한다.</p>
<pre><code>&lt;script&gt;
&lt;form id=&quot;login-form&quot; class=&quot;hidden&quot;&gt;
        &lt;input required maxlength=&quot;15&quot; type=&quot;text&quot; placeholder=&quot;What is your name?&quot;&gt;
        &lt;input type=&quot;submit&quot; value=&quot;Log In&quot; class=&quot;btn--login&quot;&gt;
&lt;/form&gt;
&lt;h1 id=&quot;greeting&quot; class=&quot;hidden&quot;&gt;&lt;/h1&gt;
&lt;/script&gt;</code></pre><hr>
<h2 id="👉🏻-스타일은-이렇게-css3">👉🏻 스타일은 이렇게! (CSS3)</h2>
<p>🩵 이름/아이디 입력 전과 후를 나누어 입력폼과 텍스트를 각각 노출시켜야 하는데, hidden 이라는 클래스를 스크립트로 컨트롤 할 것이고 따라서 hidden 클래스에 대한 style만 설정해주면 된다.</p>
<pre><code>&lt;style&gt;
.hidden{
    display: none !important;
}
&lt;/style&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-js-es6">👉🏻 스크립트는 이렇게! (JS ES6)</h2>
<h3 id="🩵-onloginsubmit-함수-설명"><strong>🩵 onLoginSubmit() 함수 설명</strong></h3>
<p>🔹 이름/아이디가 입력된 후 submit 상태가 되었을 때, form 영역을 감추고 &#39;Welcome, ~&#39; 문구를 노출시켜야 한다. 따라서 <strong>form 태그에 hidden 클래스를 추가</strong>한다.</p>
<p>🔹 form에 submit이라는 이벤트가 실행되었을 때, <strong>입력한 이름/아이디 값 (=input.value)을 변수와 localStorage에 저장</strong>한다.</p>
<h3 id="🩵-paintgreetings-함수-설명"><strong>🩵 paintGreetings() 함수 설명</strong></h3>
<p>🔹 입력한 이름/아이디 값을 &#39;Welcome, ~&#39;라는 텍스트로 노출시키기 위한 함수
🔹 <strong>localStorage.getItem()</strong>를 통해 <strong>localStorage에 저장된 값을 불러오는 로직</strong>
🔹 기존에 설정되어있던 hidden 클래스를 제거</p>
<h3 id="🩵-localstorage란">🩵 localStorage란?</h3>
<p>🔸 localStorage는 웹 브라우저에서 제공하는 저장소로, <strong>키-값(Key-Value) 쌍 형식</strong>으로 <strong>데이터를 브라우저에 영구적으로 저장</strong>할 수 있는 기능
🔸 localStorage에 저장된 데이터는 세션을 종료해도 계속해서 브라우저를 닫고 다시 열어도 유지된다!
🔹 <strong>localStorage.getItem(USERNAME_KEY)</strong> : 주어진 key에 해당하는 값을 localStorage에서 가져옴
🔹 <strong>localStorage.setItem(USERNAME_KEY, username)</strong> : 주어진 key에 value 값을 localStorage에 저장</p>
<p>🩵 localStorage에 저장된 데이터가 없다면(null) form 노출, 있다면 text 노출</p>
<pre><code>const loginForm = document.querySelector(&quot;#login-form&quot;);
const loginInput = loginForm.querySelector(&quot;input&quot;);
const greeting = document.querySelector(&quot;#greeting&quot;);

//동일한 텍스트를 입력할 일이 많다면 이렇게 KEY 변수로 설정하면 오타 방지, 효율성 짱짱
const HIDDEN_CLASSNAME = &quot;hidden&quot;;
const USERNAME_KEY = &quot;username&quot;;

function onLoginSubmit(event) {
  // preventDefault : 브라우저가 기본 동작을 실행하지 못하게 막아줌
  event.preventDefault();

  loginForm.classList.add(HIDDEN_CLASSNAME);

  const username = loginInput.value; // input에 입력한 이름/아이디(=value)값을 변수에 저장

  // username 값을 username_key와 함께 local storage에 저장
  localStorage.setItem(USERNAME_KEY, username);

  paintGrettings();
}

function paintGreetings(
  // local storage에 저장된 데이터를 username 변수에 저장
  const username = localStorage.getItem(USERNAME_KEY);

  greeting.innerText = `Welcome, ${username}!`; // 데이터보간법을 활용하여 요소의 텍스트로 반환
  greeting.classList.remove(HIDDEN_CLASSNAME); // 앞서 설정되어있던 hidden 클래스 제거
)


// localStorage에 저장된 데이터가 없다면(null) form 노출, 있다면 text 노출
const savedUsername = localStorage.getItem(USERNAME_KEY);

if (savedUsername === null) {
  // show the form
  loginForm.classList.remove(HIDDEN_CLASSNAME);

  // input 요소는 텍스트 입력 필드일 뿐, 폼 제출 이벤트를 처리하는 역할을 하지 않기 때문에 submit 이벤트를 input에 바인딩하는 것보다는 폼 자체인 loginForm에 바인딩하는 것이 일반적인 방식
  loginForm.addEventListener(&#39;submit&#39;, onLoginSubmit);

} else {
  // hide the form
  paintGreetings();
}

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JS ES6] 디지털 시계 만들기 (매우 쉬움)]]></title>
            <link>https://velog.io/@choii_ii/JS-ES6-%EB%94%94%EC%A7%80%ED%84%B8-%EC%8B%9C%EA%B3%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%A7%A4%EC%9A%B0-%EC%89%AC%EC%9B%80</link>
            <guid>https://velog.io/@choii_ii/JS-ES6-%EB%94%94%EC%A7%80%ED%84%B8-%EC%8B%9C%EA%B3%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%A7%A4%EC%9A%B0-%EC%89%AC%EC%9B%80</guid>
            <pubDate>Tue, 25 Mar 2025 07:46:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/c1424db3-c10d-4a82-a337-02667e0f3540/image.gif" alt="디지털 시계 만들기"></p>
<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 new Date() 생성자 함수와 .getHours() 등 시간관련 메서드를 활용하여 시계 셋팅
💚 setInterval() 함수를 통해 1초마다 시간이 흐르는 동작 셋팅</p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 디지털 시계를 노출시킬 요소 생성 후, <strong>00:00:00 (시:분:초) 단위를 맞춰 셋팅</strong>한다.</p>
<pre><code>&lt;body&gt;
    &lt;h2 id=&quot;clock&quot;&gt;00:00:00&lt;/h2&gt;
&lt;/body&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-js-es6">👉🏻 스크립트는 이렇게! (JS ES6)</h2>
<p>🩵 <strong>new Date() : 오늘의 날짜를 불러오는 생성자 함수 호출</strong>
🩵 <strong>.getHours() : 시간</strong>을 반환하는 메서드
🩵 <strong>.getMinutes() : 분</strong>을 반환하는 메서드
🩵 <strong>.getSeconds() : 초</strong>를 반환하는 메서드</p>
<p>🩵 .padStart(길이, 추가할 문자) : 문자열의 길이를 지정하고, 부족한 부분을 <strong>앞쪽에서부터</strong> 지정한 문자로 채운다.
🩵 .padEnd(길이, 추가할 문자) : 문자열의 길이를 지정하고, 부족한 부분을 <strong>뒤쪽에서부터</strong> 지정한 문자로 채운다.</p>
<p>🩵 이렇게 시, 분, 초 데이터를 각 변수에 담은 후, 백틱 기호를 사용한 데이터 보간처리를 하여 innerText로 화면에 출력</p>
<p>🩵 setInterval(실행 함수, intervalTime) : 일정 시간마다 함수를 무한 반복 실행
🔹 1초동안 시간이 바뀌어야하기 때문에 intervalTime을 1000ms로 설정</p>
<pre><code>&lt;script&gt;
const clock = document.querySelector(&quot;#clock&quot;);

function getClock(){
    const date = new Date();
    const hours = String(date.getHours()).padStart(2,&quot;0&quot;);
    const minutes = String(date.getMinutes()).padStart(2,&quot;0&quot;);
    const seconds = String(date.getSeconds()).padStart(2,&quot;0&quot;);

    clock.innerText = `${hours}:${minutes}:${seconds}` 
}
getClock(); // 페이지가 로드되었을 때 바로 현재 시간을 표시하기 위해서 미리 로드!!
setInterval(getClock, 1000);

&lt;/script&gt;</code></pre><hr>
<p>👩🏻‍💻 : 디지털시계 이렇게 쉽게 만드는 것이었다니! 다음엔 D-day 기능 만들어봐야지!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GSAP] ScrollTrigger의 pin 속성 제대로 이해하기!🎯]]></title>
            <link>https://velog.io/@choii_ii/GSAP-ScrollTrigger%EC%9D%98-pin-%EC%86%8D%EC%84%B1-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choii_ii/GSAP-ScrollTrigger%EC%9D%98-pin-%EC%86%8D%EC%84%B1-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 11 Mar 2025 17:11:44 GMT</pubDate>
            <description><![CDATA[<p><em>며칠 전, 면접관님을 통해 알게되었던 새로운 gsap 속성이 있었다.
그거슨 바로 ScrollTrigger 속성 중 하나인 pin 속성!!</em></p>
<p><em>대표적으로 <a href="https://velog.io/@choii_ii/%EC%BD%94%EB%93%9C%EA%B8%B0%EB%A1%9D-%EC%97%94%ED%84%B0%ED%94%84%EB%9D%BC%EC%9D%B4%EC%A6%88%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9" target="_blank">엔터프라이즈블록체인 프로젝트</a>에서 <strong>position:sticky; 를 사용하여 스크롤 시, 특정 요소를 고정시키는 인터랙션을 구현</strong>한 방법이있다.
해당 모션에 대해 <strong>pin 속성을 사용할 수도 있었는데 사용하지 않은 이유에 대해 여쭤보셨다.</strong>
그때의 동공지진이란...🥲 (쫄지말자 초이야.. * 100번 외친듯)
이미 html과 css로 마크업을 다 해둔 상태였긴 했지만, <strong>사실 pin 속성이 정확히 어떤 성질을 갖고 있는 것인지는 숙지하지 못했었다.</strong> 
(괜찮아 이제부터 알면 되지. 원래 <strong>극한의 상황에서 배운 것이 더 기억에 잘 남잖아..🥲</strong>)</em></p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/a757fa1f-c711-49eb-b975-daf7019a6c15/image.jpg" alt=""></p>
<hr>
<h2 id="📌-pin이-도대체-뭔데">📌 pin이 도대체 뭔데?</h2>
<p>🩵 디자인 시안이나 인터랙션 구현에 있어서 <strong>&quot;이 요소는 고정시키고 싶다!&quot;</strong> 하는 순간이 있다면, <strong>이런 순간을 위해 딱 맞는 기능이 바로 pin 속성이다.</strong></p>
<p>🩵 pin 속성은 <strong>단어 그대로 특정 요소를 화면에 고정하는 역할</strong>을 한다. 즉, position:sticky;처럼 <strong>스크롤을 내리더라도 해당 요소가 정해진 영역에서 고정되어있는 것처럼 계속 머무르는 효과를 보여준다.</strong> </p>
<p>🩵 gsap에서는 position:sticky;보다 더 유연하게 조정할 수 있다.</p>
<h3 id="💫-pin-이럴-때-사용해요">💫 pin 이럴 때 사용해요!</h3>
<p>🔸 섹션 강조 : 특정 영역에서 콘텐츠를 고정하면서 강조할 때
🔸 스크롤 인터랙션 : 스크롤 시, 특정 요소가 변하는 효과를 줄 때
🔸 sticky UI : 페이지 네비게이션이나 특정 이미지 등을 고정할 때</p>
<hr>
<h2 id="📌-pin-도대체-어떻게-사용하는건데">📌 pin 도대체 어떻게 사용하는건데?</h2>
<h3 id="🛠️-기본-사용법">🛠️ 기본 사용법</h3>
<p>🩵 기본적으로 <strong>pin:true의 설정만으로도 고정이 가능</strong>하다.</p>
<p>🩵 아래 코드를 보면 <strong>.box는 고정시킬 대상요소</strong>가 되고 <strong>.container는 고정의 기준이 되는 trigger를 의미</strong>한다. <strong>즉, .box 요소는 .container 요소 안에서 스크롤 시 특정 위치에 고정이 되는 것</strong>이다. (특정위치는 start &amp; end로 언제부터 언제까지 고정시킬지 설정한 값에 따라 달라진다.)
🔸 trigger: 어느 요소를 기준으로 할지 설정 (=.container)
🔸 start &amp; end: 언제부터 언제까지 고정할지
🔸 pin: true: 해당 요소를 고정시키는 옵션</p>
<pre><code>// 요소 고정하기
gsap.to(&quot;.box&quot;, {
  scrollTrigger: {
    trigger: &quot;.container&quot;, // 고정할 기준 요소
    start: &quot;top top&quot;, 
    end: &quot;bottom top&quot;,
    pin: true, // 요소 고정!
    markers: true
  }
});</code></pre><h3 id="🛠️-추가-옵션--유용한-기능">🛠️ 추가 옵션 &amp; 유용한 기능</h3>
<p>🩵 <strong>pinSpacing: false</strong> -&gt; <strong>고정될 때 원래 공간만큼 여백이 생기는데</strong>, 그걸 없애고 싶다면?! 이 옵션을 추가하여 <strong>불필요한 공간을 제거</strong>한다.</p>
<p>🩵 pinReparent: true -&gt; 부모 요소의 레이아웃이 깨질 때 해결하는 방법! 이 옵션을 추가하면 특정 레이아웃에서 문제가 생길 때 해결할 수 있다. 하지만 자주 사용되는 옵션은 아니다.</p>
<p>🩵 <strong>anticipatePin : 1</strong> -&gt; 스크롤 성능이 느려지는 게 느껴질 때 <strong>살짝 미리 고정</strong>하는 옵션! <strong>기본값은 0이고 숫자를 올리면 미리 고정</strong>된다. 이 옵션을 조절하면 부드러운 스크롤링을 유지할 수 있다.</p>
<hr>
<h2 id="📌-이제-sticky-들어가시고-pin-나와">📌 이제 sticky 들어가시고, pin 나와!</h2>
<p>🩵 <strong>비주얼 영상/이미지는 고정된 채로 스크롤할 때 텍스트가 바뀌는 효과로 웹 사이트에서 많이 쓰이는 인터랙션이다.</strong> (ex-Apple에서 제품 홍보 시, 정말 많이 볼 수 있는 효과이다.)</p>
<h3 id="🥨-기존-sticky-활용-코드">🥨 기존 sticky 활용 코드</h3>
<p><strong>🩵 [HTML]</strong>
🔸 .sticky-wrap 요소가 고정이 될 대상 요소이다.</p>
<pre><code>&lt;body&gt;
&lt;section class=&quot;sc-intro&quot;&gt;
    &lt;div class=&quot;sticky-wrap&quot;&gt;
        &lt;!-- intro bg video --&gt;
        &lt;div class=&quot;intro__bg&quot;&gt;
            &lt;video id=&quot;video&quot; loop autoplay muted playsinline poster=&quot;./assets/images/intro-poster.jpg&quot;&gt;
                &lt;source src=&quot;./assets/video/Archivo-VariableFont_Medium_C_PC.mp4&quot; type=&quot;video/mp4&quot;&gt;
            &lt;/video&gt;
        &lt;/div&gt;
        &lt;!-- // intro bg video --&gt;

        &lt;!-- intro text --&gt;
        &lt;div class=&quot;group-intro-text&quot;&gt;
            &lt;!-- text 중략 --&gt;
        &lt;/div&gt;
        &lt;!-- // intro text --&gt;
    &lt;/div&gt;
&lt;/section&gt;
&lt;/body&gt;</code></pre><p><strong>🩵 [CSS]</strong>
🔸 <strong>고정의 기준이 되는 .sc-intro 요소에 position:relative;를 설정</strong>해주고 <strong>대략적인 높이도 설정</strong>했다.</p>
<p>🔸 <strong>고정이 되는 .sticky-wrap 요소에 position:sticky;를 설정</strong>했다.</p>
<pre><code>&lt;style&gt;
.sc-intro{
    position: relative;
    width: 100%;
    height: calc(100vh * 8);
}

.sticky-wrap{
    position: sticky;
    width: 100%;
    height: 100vh;
    top: 0;
    left: 0;
    overflow: hidden;
}
&lt;/style&gt;</code></pre><p><strong>🩵 [script (GSAP)]</strong>
🔸 text 애니메이션을 제외한 scrollTrigger만 작성한 스크립트이다. <strong>.sc-intro 영역의 시작점과 끝점을 설정</strong>했다.</p>
<pre><code>&lt;script&gt;
const intro = gsap.timeline({
  scrollTrigger: {
    trigger: &#39;.sc-intro&#39;,
    start: &#39;0% 0%&#39;,
    end: &#39;100%, 100%&#39;,
    // markers: true,
    scrub: true
  }
});
&lt;/script&gt;</code></pre><h3 id="🥨-pin-속성으로-적용한-코드">🥨 pin 속성으로 적용한 코드</h3>
<p><strong>🩵 [css]</strong>
🔸 <strong>position:sticky;를 사용했던 style은 pin 속성으로 대체할 것</strong>이기 때문에 <strong>relative;로 바꿔준다.</strong></p>
<pre><code>&lt;style&gt;
.sticky-wrap {
  position: relative; /* position: sticky를 사용하지 않음 */
  width: 100%;
  height: 100vh;
  overflow: hidden;
}
&lt;/style&gt;</code></pre><p><strong>🩵 [script (GSAP)]</strong>
🔸 <strong>고정시킬 .sticky-wrap 요소에 대한 스크립트를 추가 작성</strong>한다.</p>
<p>🔸 <strong>다른 모션과 달리 css 속성을 별도로 설정할 필요 없이, scrollTrigger만 설정해주면(특히 pin:true)</strong> 스크롤의 기준이 되는 시작점과 끝점에 맞춰 <strong>해당 요소를 고정</strong>시켜준다.</p>
<pre><code>gsap.to(&quot;.sticky-wrap&quot;, {
  scrollTrigger: {
    trigger: &quot;.sc-intro&quot;,  // .sc-intro 영역을 기준으로 고정
    start: &quot;0% 0%&quot;,         // 시작점
    end: &quot;100% 100%&quot;,       // 끝점
    pin: true,              // .sticky-wrap 고정
    scrub: true,            // 스크롤에 따라 부드럽게 애니메이션
    markers: true           // 마커 추가 (디버깅 용)
  }
});
</code></pre><hr>
<p>👩🏻‍💻 : 이제 완벽하게 알아버렸다. ㅎㅎ 반응형 제작할 때에도 유용하고 효율적으로 컨트롤 가능하다고 알려주신 점도 꼭 기억하고 있다가 반응형 테스트 해봐야지!!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SCSS] SCSS 따라하기]]></title>
            <link>https://velog.io/@choii_ii/SCSS-SCSS-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choii_ii/SCSS-SCSS-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 19 Feb 2025 15:44:47 GMT</pubDate>
            <description><![CDATA[<p><em>사실 SCSS 적용 방법에 대해 유튜브를 찾아보기도 하고 강의를 찾아보기도 했지만, 내가 자료를 잘 못찾았던 것인지 마음과는 다르게 쉽게 숙지하지 못했던 스킬 중 하나였다.
고로 이번 마크업 디벨로퍼 과정 수업을 들었던 것이 나의 기술적 역량을 끌어올리는데 큰 몫을 한 것 같다.
주저리주저리 자기 반성은 여기까지 하고, 야무지게 배워온 SCSS의 정석을 잘 메모해보겠다.</em></p>
<h2 id="🩵-scss란">🩵 SCSS란?</h2>
<p>🩵 Sass의 한 문법 형식으로, 기존 CSS의 단점을 보완한 CSS 전처리기이다.
🩵 CSS 문법을 그대로 사용할 수 있으면서도, 변수, 중첩(Nesting), Mixin 등의 강력한 기능을 제공해 유지보수성과 생산성을 높여준다는 장점이 있다.</p>
<hr>
<h2 id="🩵-scss-장점-한-눈에-보기">🩵 SCSS 장점 한 눈에 보기</h2>
<h3 id="1-변수-사용-가능">1. 변수 사용 가능</h3>
<p>🩵 동일한 색상이나 값을 반복적으로 사용할 때, 변수를 활용할 수 있다.
🔹 <code>$primary-color: #3498db;</code> 처럼 $변수명에 값을 할당
🔹 특정 요소에 <code>background-color: $primary-color;</code> 처럼 변수를 불러와 설정해준다. 
🔹 변수 하나만 변경하면 공통 스타일을 쉽게 수정할 수 있다는 장점!</p>
<h3 id="2-중첩nesting-문법-지원">2. 중첩(Nesting) 문법 지원</h3>
<p>🩵 CSS 선택자를 계층 구조 그대로 중첩해서 작성 가능해 가독성이 좋아지는 장점이 있다!
🔹 CSS 구조를 한눈에 파악하기 쉬워지고, 스타일을 그룹화할 수 있다.
🔹 가상요소(before, after), hover 등 선택자는 &amp;를 붙여서 사용한다.</p>
<pre><code>&lt;style&gt;
.navbar {
  background: #333;
  .menu {
    display: flex;
    li {
      list-style: none;
      a {
        color: white;
        text-decoration: none;
      }
    }
  }
}
&lt;/style&gt;</code></pre><p>이렇게 작성 후, 컴파일 하면 아래처럼 변환된다.</p>
<pre><code>&lt;style&gt;
.navbar {
  background: #333;
}
.navbar .menu {
  display: flex;
}
.navbar .menu li {
  list-style: none;
}
.navbar .menu li a {
  color: white;
  text-decoration: none;
}
&lt;/style&gt;</code></pre><h3 id="3-재사용-가능한-mixin-기능">3. 재사용 가능한 Mixin 기능</h3>
<p>🩵 반복되는 스타일을 한 번만 정의하고 여러 곳에서 재사용할 수 있다.
🔹 <code>@mixin 스타일 이름 {}</code> 처럼 정의하고, 요소의 style에서 <code>@include 스타일 이름</code> 처럼 불러와서 여러 곳에서 편하게 재사용할 수 있다.
🔹 반응형 작업할 때, 아주 편하다.</p>
<pre><code>&lt;style&gt;
/* 일반 스타일 */
/* _mixin.scss 파일에서 정리해주면 더욱 좋다. */
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.container {
  @include flex-center;
  height: 100vh;
}


/* 반응형 스타일 */
@mixin mobile-l{
    @media (max-width:767px){
        @content;
    }
}

#cursor {
  display: flex;
  position: fixed;
  align-items: center;
  justify-content: center;
  top: 0;
  left: 0;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 100;
  @include mobile-l {
    display: none;
  }
}
&lt;/style&gt;</code></pre><h3 id="4-모듈화import-use로-코드-관리">4. 모듈화(import, use)로 코드 관리</h3>
<p>🩵 CSS는 모든 스타일을 하나의 파일에 작성해야 하지만, SCSS는 여러 개의 파일로 분리하여 관리할 수 있다. 아래 SCSS 기본 사용법을 참고하자.</p>
<h3 id="5-연산operators-기능-지원">5. 연산(Operators) 기능 지원</h3>
<p>🩵 SCSS에서는 +, -, *, / 등의 연산을 지원한다.</p>
<hr>
<h2 id="🩵-scss-기본-사용법-국룰은-아님">🩵 SCSS 기본 사용법 (국룰은 아님..)</h2>
<p>🩵 <strong>scss라는 디렉토리 생성</strong> -&gt; <strong>base / layout / pages / utils 라는 디렉토리를 생성</strong>하여 각 성격을 가지는 파일들을 관리해줄 것이다.
🔹 <strong>base : reset이나 common, typo(font) 관련</strong> style을 설정해줄 .scss파일을 생성/관리
🔹 <strong>layout : header,footer</strong> style을 설정해줄 .scss파일을 생성/관리
🔹 <strong>pages : 메인, 서브페이지</strong>의 style을 설정해줄 .scss파일을 생성/관리
🔹 <strong>utils : var(css 변수), 반응형(mixin) 등을 정의</strong>해줄 .scss 파일을 생성/관리</p>
<p>🩵 아래 이미지처럼 <strong>_파일명.scss</strong> 처럼 앞에 언더스코어를 붙여줘야한다.
🔹 왜냐하면 <strong>위처럼 생성해준 파일은 다른 SCSS 파일에서 @import나 @use를 통해 불러와서 사용되는 파일</strong>이기 때문에 <strong>컴파일할 때 단독 CSS 파일로 변환하지 않는다.</strong> 즉, _reset.scss 같은 파일을 만들고 main.scss에서 가져다 쓰면, <strong>_reset.scss 자체는 변환되지 않고 main.css에 포함</strong>된다. <strong>= 필요할 때만 불러와서 사용 가능!</strong>
🔹 만약 buttons.scss처럼 <strong>언더스코어없이 만들면, SCSS는 이를 독립적인 SCSS 파일로 인식하고, CSS로 변환할 때 buttons.css 파일을 별도로 생성</strong>한다.</p>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/f0678834-fd80-4468-a5c1-486c079dfca5/image.png" alt="SCSS 기본 사용법"></p>
<p>🩵 scss 디렉토리 하위에 style.scss라는 파일을 생성한다.
🔹 이 파일은 언더스코어가 없으니 css파일로 변환이 된다!</p>
<p>🩵 생성된 style.scss에서 필요한 scss 파일을 import 해준다.
🔹 이때 파일 확장자(.scss)는 안써도 무방하다.
<img src="https://velog.velcdn.com/images/choii_ii/post/2705f5c5-202b-4e37-ab02-796ac958cec2/image.png" alt="SCSS파일 임포트"></p>
<p>🩵 그리고 Live Sass Compiler 라는 확장기능을 설치한다.
🔹 톱니바퀴 클릭 -&gt; 설정 -&gt; Autoprefix 편집
🔹 &quot;liveSassCompile.settings.autoprefix&quot;: [&quot;&gt; 1%&quot;, &quot;last 2 versions&quot;] 추가 (크로스 브라우징 처리를 해주는 코드) </p>
<p>🔹 톱니바퀴 클릭 -&gt; 설정 -&gt; Formats 편집
🔹 &quot;savePath&quot;: &quot;~/../css/&quot;, 경로를 왼쪽과 동일하게 편집 (css파일로 변환된 것을 왼쪽 경로에 저장됨)</p>
<p>🔹 톱니바퀴 클릭 -&gt; 설정 -&gt; Generate Map 편집
🔹 false이면 true로 변경하기 (<em>SCSS는 컴파일 후 CSS로 변환되지만, 이 과정에서 원본 SCSS 파일의 구조를 잃게 된다. source map은 컴파일된 CSS 파일과 원본 SCSS 파일의 매핑 정보를 포함하는 파일로, 개발자가 크롬 개발자 도구(DevTools)에서 스타일을 디버깅할 때 원본 SCSS 파일을 그대로 볼 수 있도록 도와준다.)</em> 
🔹 즉, SCSS 원본을 유지하면서 CSS를 확인할 수 있어 디버깅이 훨씬 쉬워진다.
✅ 특히 여러 개의 SCSS 파일을 모듈화해서 관리할 때 필수적
✅ 프로덕션 환경에서는 성능 최적화를 위해 비활성화할 수도 있음
👉 개발할 때는 true로 설정하고, 배포 시에는 false로 설정하는 것이 일반적인 방법이라고 한다.</p>
<p>🩵 설정이 끝나면 style.scss 파일로 돌아와서 하단의 Watch Sass를 클릭한다. 그리고 저장을 하면 css 디렉토리에 style.css라는 변환 파일이 생성된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[⭐카페24 완벽 정복⭐ (-ing)]]></title>
            <link>https://velog.io/@choii_ii/%EC%B9%B4%ED%8E%9824-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%B3%B5-ing</link>
            <guid>https://velog.io/@choii_ii/%EC%B9%B4%ED%8E%9824-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%B3%B5-ing</guid>
            <pubDate>Mon, 10 Feb 2025 12:37:50 GMT</pubDate>
            <description><![CDATA[<p>👀
<em>내가 보려고 적어두는 카페24 완벽 정복!!</em>
<em>시작은 베이직(기본) 스킨을 바탕으로 디자인을 적용하면서 메모했슴둥</em></p>
<h2 id="💡카페24-시작하기">💡카페24 시작하기</h2>
<p>🔹 로그인, 작업을 진행할 디자인도 추가를 완료했다면 FTP에 연결할 차례!
🔹 코딩을 배운 사람이니까 FTP 연결해서 하드코딩으로 뿌셔줘야지! 솔루션 상에서 클릭클릭한다는 것은 아주.. 가오 상하는 일이잖아요...?</p>
<hr>
<h3 id="🩵-ftp-연결하기">🩵 FTP 연결하기</h3>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/11072a14-4143-4930-b274-ef4111d0ac98/image.png" alt="FTP 연결">
🩵 카페24 -&gt; 디자인 -&gt; <strong>디자인 FTP -&gt; 디자인FTP 권한 신청</strong>
🔹 <strong>만료시간은 최대 7일</strong>이므로 연장 필요!</p>
<p>🩵 <strong>VSCODE -&gt; 확장기능 -&gt; FTP-SIMPLE 설치 -&gt; F1 -&gt; FTP-SIMPLE : CONFIG ~ 클릭</strong>
🔹 <strong>계정, FTP주소, SFTP 접속포트를 적어줌</strong> (디자인FTP 권한신청정보는 카페24 -&gt; 디자인FTP에서 확인)
🔹 VSCODE가 너무 최근 버전이면 호환을 못해서 열리지 않아 다운그레이드 해주어야함</p>
<pre><code>{
    &quot;name&quot;: &quot;localhost&quot;,  // 사용자 이름
    &quot;host&quot;: &quot;&quot;,  // FTP 주소
    &quot;port&quot;: 21,  
    &quot;type&quot;: &quot;ftp&quot;,
    &quot;username&quot;: &quot;&quot;,  // FTP ID
    &quot;password&quot;: &quot;&quot;,  // FTP PW
    &quot;path&quot;: &quot;/skin7/&quot;,  // 나는 스킨 7번이라는 곳에서 작업을 해서 /skin7/를 적었다.
    &quot;autosave&quot;: true,
    &quot;confirm&quot;: false
}</code></pre><p>🩵 <strong>VSCODE (F1) -&gt; FTP-SIMPLE : REMOTE ~ 클릭</strong>
🔹 <strong>접속 할 host, path에서 지정한 디자인경로를 클릭</strong>하면 스킨과 관련된 <strong>작업 스페이스가 열림</strong></p>
<hr>
<h3 id="🩵-indexhtml-이란">🩵 index.html 이란?</h3>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/a3cebd01-fb40-476f-8e51-28427579244a/image.png" alt="메인영역"></p>
<p>🔹 메인 container에 대한 코드를 작성 (<strong>header, footer를 제외한 순수 container</strong>)
🔹 <code>&lt;!--@layout(/layout/basic/main.html)--&gt;</code> 해당 코드 이후로 적힌 모든 내용은 불필요하여 지웠다.
🔹 위 경로는 <strong>main에서만 사용하는 header, footer 등을 다루는 layout</strong>이다. 하지만 <strong>메인과 서브의 layout이 동일하다면 layout.html 파일로 통일해도 무방</strong>하다.
내가 진행했던 프로젝트는 메인과 서브의 layout이 동일하여 layout.html 파일로 통일시켜 작업했다.</p>
<p>🔹 <strong>메인페이지에서만 사용할 css와 js, 라이브러리 cdn 등을 연결</strong>해준다.
<code>&lt;!--@css(/layout/basic/css/main.css)--&gt;</code>
<code>&lt;!--@js(/layout/basic/js/main.js)--&gt;</code></p>
<p>🔹 하나의 html 파일에서 메인페이지의 모든 콘텐츠를 유지관리한다는 것은 매우 비효율적이다. 따라서 <strong>각 section 마다 레고처럼 각각의 조각처럼 쪼개어 작성해주는 것이 가장 효율적</strong>인 방법! 나는 <strong>main이라는 디렉토리를 하나 생성하고 그 안에 section1.html 이라는 규칙을 만들어 영역에 관련된 파일을 생성</strong>했다.
🔹 이렇게 만들어진 각각의 html 파일은 어떻게 불러오냐고? 아주 쉽다.(<strong>import 방식</strong>)
🔸 <em>메인비주얼</em> : <code>&lt;!--@import(/main/_section1.html)--&gt;</code>
🔸 <em>인기꾸끼아</em> : <code>&lt;!--@import(/main/_section2.html)--&gt;</code></p>
<p>🔹 이렇게 불러온 파일은 layout.html에서 include 방식으로 불러오기 때문에 메인페이지에서도 정상적으로 잘 나타나게 되는 것이었다. 그러기 위해서는 index.html 하단에 이와 같은 코드가 있어야 하는 것 같다. <code>&lt;!--@define(cmc_log)--&gt;</code></p>
<hr>
<h3 id="🩵-layouthtml-이란">🩵 layout.html 이란?</h3>
<p>🔹 서브페이지에서 사용되는 header, footer 구조를 다룬다.
🔹 <strong>@contents란, include 방식</strong>으로 *<em>필요한 페이지를 불러오는 개념이다. *</em>
🔹 불필요한 sidebar, quick 영역 등을 지워주고 사용할 필수코드만 남겨둔다.</p>
<p>🔽 layout.html을 리셋하여 정리한 코드</p>
<pre><code>&lt;body&gt;
&lt;!-- 본문 영역 --!&gt;
&lt;header id=&quot;header&quot;&gt;header&lt;/header&gt;

&lt;div id=&quot;wrap&quot;&gt;
    &lt;div id=&quot;container&quot;&gt;
        &lt;div id=&quot;contents&quot;&gt;
            &lt;!-- 아래 코드는 본문 내용을 인크루드 하는 코드 (불러오는 개념) --!&gt;
            &lt;!--@contents--&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;footer id=&quot;footer&quot;&gt;footer&lt;/footer&gt;
&lt;/body&gt;</code></pre><hr>
<h3 id="🩵-layoutbasiccss-공통-css-한-눈에-보기">🩵 layout/basic/~.css (공통 css 한 눈에 보기)</h3>
<p>🔹 common.css : reset style을 내가 자주 쓰는 코드로 덮어주었고, font style도 해당 파일에서 적용시켰다.
🔹 layout.css : 기본으로 꾸며져있던 코드가 작성되어 있다. 불필요한 태그는 삭제한다.</p>
<hr>
<h3 id="🩵-파일-업로드하기-파일-경로-설정">🩵 파일 업로드하기 (파일 경로 설정)</h3>
<p>🔹 font, 이미지 등 파일 경로를 직접 설정해주어야하는 경우가 있다. 이때, <strong>카페24 - 디자인 - 파일업로드</strong>를 통해 <strong>파일을 업로드 후, 경로(주소)복사하여 html, css 등에서 활용</strong>하면 파일이 깨지는 현상없이 정상적으로 잘 노출 될 것이다.</p>
<hr>
<hr>
<h2 id="💡카페24-모듈-활용하기">💡카페24 모듈 활용하기</h2>
<h3 id="🩵-배너-슬라이드-앱-배너매니저">🩵 배너 슬라이드 (앱 배너매니저)</h3>
<p><strong>📍 <a href="https://dfloor.co.kr/services/bannermanager/page/read.html?no=84333&board_no=3001" target="_blank">배너매니저 설치매뉴얼 바로가기</a></strong>
🔹 swiper, fade-in/out 등 메인페이지에는 단순하고도 다양한 배너가 노출되고는 한다. 이때 카페24에서 친히 제공하고 있는 &#39;배너 매니저&#39;라는 앱을 사용해보자. 
🔹 <strong>카페24 - 앱 - 앱스토어 바로가기 - &#39;배너 매니저&#39; 설치</strong>
🔹 <strong>아래 스크립트 코드를 layout.html 하단에 적어주었다.</strong> (전 페이지에 걸쳐서 사용할 수 있어 효율적이다.) <strong>해당 스크립트는 배너매니저 - 프로젝트 코드에서 확인</strong>할 수 있다.</p>
<pre><code>&lt;!-- 배너매니저 스크립트 코드 --&gt;
&lt;style&gt;[df-banner-code][hidden] { display:none !important }&lt;/style&gt;
&lt;script&gt;
if (!document.getElementById(&#39;dfbm-script-46798&#39;)){
  var dfbm_src = &#39;https://ecimg.cafe24img.com/pg1484b56993108007/choii9901/web/upload/appfiles/ZaReJam3QiELznoZeGGkMG/b2abba405e67784eb3a6d7ba38c3e391.js&#39;;
  document.write(&#39;&lt;script id=&quot;dfbm-script-46798&quot; src=&quot;&#39; + dfbm_src + &#39;?v=&#39; + Date.now() + &#39;&quot;&gt;&lt;\/script&gt;&#39;);
}
&lt;/script&gt;</code></pre><p>🔹 그리고 다시 카페24 - 배너매니저로 돌아와서 <strong>배너 그룹을 추가</strong>한다.
🔸 <strong>그룹 코드는 영문으로 작성</strong>하되, <strong>배너 그룹명은 쇼핑몰관리자 등 누구나 알아볼 수 있도록 쉽게 작성</strong>해주어야 한다.
<img src="https://velog.velcdn.com/images/choii_ii/post/86c76d29-90a2-4656-953b-913b19bb2217/image.png" alt="배너매니저"></p>
<p>🔹 <strong>이미지, 링크, target 방법 모두 &#39;배너매니저&#39;를 통해 등록</strong>할 수 있고, <strong>br태그나 클래스 등을 부여해야할 설명 또한 html 문법으로 작성</strong>해주면 된다. </p>
<p>🔹 사용할 배너의 추가가 완료되었다면, 설치매뉴얼에 명시되어있는 대로 <strong>클래스와 구조를 맞게 작성</strong>해주면 된다. 여기서 <strong>그룹코드는 배너 그룹 등록 시, 작성했던 배너 코드명</strong>이고 <strong>복제될 슬라이드 코드에 <code>df-banner-clone</code> 를 적어주면 된다.</strong> 그리고 등록한 정보들은 변수를 통해 불러올 수 있다. (더 많은 변수는 <a href="https://dfloor.co.kr/services/bannermanager/page/read.html?no=84333&board_no=3001" target="_blank">배너매니저 설치매뉴얼</a> 참고)
🔸 df-banner-code=&quot;그룹코드&quot;
🔸 {#href} : 배너 클릭 시, 이동 링크
🔸 {#item} : 등록한 배너 이미지
🔸 {#target} : 설정한 target 방법
🔸 {#html} : 등록한 배너 설명</p>
<pre><code>&lt;body&gt;
&lt;div df-banner-code=&quot;그룹코드&quot; hidden&gt;
    &lt;div df-banner-clone&gt;
        &lt;a href=&quot;{#href}&quot;&gt;{#item}&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</code></pre><p>🔹 <strong>swiper 구조와 접목 시킨 최종 코드</strong>는 아래와 같다.</p>
<pre><code>&lt;body&gt;
&lt;div df-banner-code=&quot;kukka-main-vis&quot; hidden&gt;
    &lt;div class=&quot;swiper&quot;&gt;
        &lt;div class=&quot;swiper-wrapper&quot;&gt;
            &lt;div class=&quot;swiper-slide&quot; df-banner-clone&gt;
                &lt;a href=&quot;{#href}&quot; target=&quot;{#target}&quot;&gt;
                    {#item}
                    &lt;span class=&quot;banner-title&quot;&gt;
                        {#html}
                    &lt;/span&gt;
                &lt;/a&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</code></pre><p>👩🏻‍💻 : 배너매니저 사용법 숙지 완료!</p>
<hr>
<h3 id="💡-쇼핑몰-모듈-꿀팁">💡 쇼핑몰 모듈 꿀팁!</h3>
<p><strong>📍 <a href="https://sdsupport.cafe24.com/product/list.html?cate_no=63" target="_blank">카페24 모듈 리스트 바로가기</a></strong> 
🩵 카페24 모듈 리스트에서 <strong>header(nav), footer, 상품리스트 등 기본 구조와 사용가능한 변수명을 활용하여 구조를 작성</strong>할 수 있다.</p>
<h3 id="🩵-상품-리스트-기초">🩵 상품 리스트 (기초)</h3>
<p>🩵 상품 리스트를 가져올 때에도 카페24에서 제공하는 모듈을 활용하여 불러올 수 있다.
<strong>모듈 코드를 가져오면, 기본상태의 상품목록이 노출</strong>된다.</p>
<p>🩵 이때 <strong>쇼핑몰마다 보여주는 상품 정보들이 다르기 때문에 정보 리스트를 세팅해 주어야 한다.</strong> 
<strong>(카페24 -&gt; 쇼핑몰 설정 -&gt; 상품설정 -&gt; 상품정보표시설정)</strong>
🩵 상품정보표시설정에서 설정해줄 상세분류 항목을 선택하고 <strong>내가 노출시킬 리스트만 체크</strong>하여 저장한다.
꾸까 프로젝트에서는 상품명, 판매가, 할인판매가와 추가옵션항목이 노출되는 구조였다.
<img src="https://velog.velcdn.com/images/choii_ii/post/1caba685-6124-4e25-a215-343fc9fbeafd/image.png" alt="상품정보표시설정"></p>
<p>🩵 기본 css로 굉장히 못생긴 스타일로 노출이 될텐데, 이건 퍼블리싱 고수답게 클래스를 활용해서 스타일을 적용해주면된다. <strong>요소 검사를 해보면 아래 이미지처럼 각 항목마다 클래스가 부여</strong>되어있는 것을 볼 수 있다. 이때 font-size, font-weight, color는 기본으로 셋팅되어있기 때문에 적용해도 바뀌지 않을 때가 있다. 그럴 땐 <strong>!important 속성을 써서 강제로 우선순위가 되도록 하면 설정한대로 바뀐다.</strong>
<img src="https://velog.velcdn.com/images/choii_ii/post/1c3cdcbd-e4c2-4ccd-8ea9-d65ec0bfdc38/image.png" alt=""></p>
<h3 id="🩵-할인상품vs정가상품">🩵 할인상품vs정가상품</h3>
<p>🩵 <strong>할인상품과 정가상품이 있을 때, 노출되는 소비자가의 스타일은 어떻게 다르게 설정할 수 있을까?</strong></p>
<p>🩵 할인상품일 때, <strong>할인판매가를 보여주는 .prd_price_sale이라는 항목</strong>이 생성된다. 이 항목과 :has, :not 선택자 속성을 활용해서 스타일의 차별을 설정해주면 된다.
🔹 .prdList:not(.prd_price_sale) : .prdList 자체가 .prd_price_sale 클래스를 가지지 않는 경우를 의미
🔹 <strong>.prdList:not(:has(.prd_price_sale)) : .prdList 내부에 .prd_price_sale을 가진 요소가 없는 경우를 의미</strong>
따라서 단순히 :not이 아닌 <strong>:not(:has(.prd_price_sale)) 이렇게 써주어야 내부 자식 요소의 조건을 확인한 후 스타일을 적용할 수 있어 정확한 스타일 적용이 가능!</strong></p>
<pre><code>&lt;style&gt;
/* 할인 상품일 때 (.prd_price_sale이 있을 때), 소비자가(.product_price) 스타일 */
.prdList:has(.prd_price_sale) .product_price span {
  line-height: 16px;
  color: #d1d1d1 !important;
  font-weight: normal !important;
}

/* 정가 상품일 때 (.prd_price_sale이 없을 때), 소비자가(.product_price) 스타일 */
.prdList:not(:has(.prd_price_sale)) .product_price span {
  font-size: 16px !important;
  line-height: 24px;
  color: rgb(0, 0, 0) !important;
  font-weight: 600;
}
&lt;/style&gt;</code></pre><h3 id="🩵-상품-추가옵션-설정하기">🩵 상품 추가옵션 설정하기</h3>
<p>🩵 간혹 <strong>상품마다 재입고, 신상품 등 처럼 노출되는 추가옵션</strong>이 있다. 이런 추가옵션은 어떻게 설정할 수 있을까?</p>
<p>🩵 카페24 -&gt; 쇼핑몰 설정 -&gt; 상품설정 -&gt; <strong>상품정보표시설정에서 &#39;항목추가&#39;</strong>를 누르면 어떤 항목을 추가할 것인지 리스트 양식이 나온다. <strong>항목을 구분할 수 있도록 이름을 설정</strong>하고, 추가옵션을 등록할 <strong>상품 등록페이지로 가면 아래 이미지처럼 추가한 항목이 노출</strong>된다.</p>
<p>🩵 이렇게 옵션을 저장하면 <strong>정상적으로 옵션 항목 리스트가 노출</strong>된다. <strong>style을 설정하고 싶다면 역시 항목 리스트에 붙은 클래스명(.custom_option1)을 활용하여 설정</strong>하면된다.
꾸까 프로젝트의 경우 이미지 안에 position으로 위치시켜야 하기 때문에 따로 span 태그를 활용하여 마크업을 해주었다.</p>
<pre><code>&lt;body&gt;
    &lt;span class=&quot;opt&quot;&gt;{$custom_option2}&lt;/span&gt;
&lt;/body&gt;</code></pre><p><img src="https://velog.velcdn.com/images/choii_ii/post/bcc509dc-0f99-442d-b290-39aad88f44a4/image.png" alt="상품 추가옵션"></p>
<p>🩵 아래 이미지처럼 옵션이 잘 뜨는 것을 확인할 수 있다. 그런데 옵션이 없는 상품에서는 노출이 되면 안된다.<strong>텍스트</strong> 따라서 스크립트를 작성해주었다. 
🔹 :not(:has(요소)) 선택자를 통해서 display:none;처리를 하는 방법도 있겠지만, 효율적인 관리를 위해 스크립트로 작성했다.
🔹 <strong>.text().trim() === &#39;&#39;</strong> : <strong>.text() 메서드를 활용하여 옵션의 텍스트를 가져오고, .trim() 메서드를 통해 텍스트의 앞뒤 공백을 제거</strong>한다. <strong>공백까지 제거했을 때의 요소의 텍스트가 완전히 비어있을 때 해당 요소(옵션)을 .hide() 처리</strong>한다.</p>
<pre><code>&lt;script&gt;
// 공통) 상품 옵션(캡션) 없을 때, hide 처리
$(&#39;.prdList .opt&#39;).each(function () {
  if ($(this).text().trim() === &#39;&#39;) {
    $(this).hide();
  }
});
&lt;/script&gt;</code></pre><p><img src="https://velog.velcdn.com/images/choii_ii/post/270efed3-f19b-45fb-90e6-8dfc36231ec6/image.png" alt="상품 추가옵션"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드기록] 꾸까 클론코딩 (카페24/반응형)]]></title>
            <link>https://velog.io/@choii_ii/%EC%BD%94%EB%93%9C%EA%B8%B0%EB%A1%9D-%EA%BE%B8%EA%B9%8C-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-%EC%B9%B4%ED%8E%9824%EB%B0%98%EC%9D%91%ED%98%95</link>
            <guid>https://velog.io/@choii_ii/%EC%BD%94%EB%93%9C%EA%B8%B0%EB%A1%9D-%EA%BE%B8%EA%B9%8C-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-%EC%B9%B4%ED%8E%9824%EB%B0%98%EC%9D%91%ED%98%95</guid>
            <pubDate>Mon, 10 Feb 2025 09:58:52 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/19715748-5c80-4e3b-8ef1-748a15b6f918/image.png" alt="꾸까"></p>
<h4 id="-🔵-꾸까-메인페이지-클론코딩-반응형">** 🔵 꾸까 메인페이지 클론코딩 (반응형)**</h4>
<p>🔸작업기간 : 2025.02.04 - 2025.02.06 (3일 + 유지보수 1일)
🔹기술스택 : HTML, CSS, jQuery, Gsap·Swiper라이브러리
🔸URL : <a href="https://choii9901.cafe24.com/" target="_blank">https://choii9901.cafe24.com/</a></p>
<blockquote>
<p>📌 <strong>Key point</strong>
✅ <strong>특정 슬라이드에서 이벤트 실행 (swiper on 메서드)</strong>
✅ <strong>숫자 카운팅 스크립트 작성법</strong></p>
</blockquote>
<hr>
<h2 id="👀-꾸까-한-눈에-보기">👀 꾸까 한 눈에 보기!</h2>
<h3 id="👉🏻-숫자-카운팅-효과">👉🏻 숫자 카운팅 효과</h3>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/b8ad913c-deb9-4f86-90bd-24174cb8679e/image.gif" alt="숫자 카운팅 효과"></p>
<blockquote>
<p>💡 <strong>숫자 카운팅 실행 원리</strong>
🔸 시작 숫자와 목표 숫자를 설정
🔸 <strong>일정한 간격으로 숫자를 업데이트</strong>하고, <strong>목표 숫자에 도달하면 애니메이션 중지</strong>
🔸 최종적으로 .counter 클래스를 가진 요소의 숫자가 <strong>279,000 → 2,396,445로 2초 동안 증가하는 애니메이션이 실행</strong></p>
</blockquote>
<p><strong>📍 자세한 코드 설명은 <a href="https://velog.io/@choii_ii/JS-ES6-%EC%88%AB%EC%9E%90-%EC%B9%B4%EC%9A%B4%ED%8C%85-%ED%9A%A8%EA%B3%BC-Counting" target="_blank">이 포스팅에서 확인하기!</a></strong></p>
<hr>
<h3 id="👉🏻-특정-slide에서-이벤트-실행-swiper">👉🏻 특정 slide에서 이벤트 실행 (swiper)</h3>
<p>🔸 <strong>Swiper의 on 메서드</strong> : 특정 이벤트가 발생했을 때 실행할 콜백 함수를 등록
🔹 <strong>slideChange : 슬라이드가 변경될 때 실행</strong></p>
<p>🔸 카운팅이 실행되는 슬라이드는 <strong>index 2번째 슬라이드</strong>
🔸 <strong>realIndex 속성</strong>을 통해 <strong>슬라이드의 index를 변수에 저장</strong>하고, <strong>2와 매칭 시 startCounting() 함수 실행</strong></p>
<pre><code>&lt;script&gt;
const mainVis = new Swiper(&#39;.sc-mainvis .swiper&#39;, {
  // 중략
  on: {
    slideChange: function () {
      activeSlideIndex = this.realIndex;
      //   console.log(activeSlideIndex);

      // 슬라이드 인덱스(2) 매칭 시, 카운팅 실행
      if (activeSlideIndex === 2) {
        startCounting(); 
      }

      // 슬라이드 인덱스 업데이트 실행
      updateHeaderClass(activeSlideIndex);      
    }
  }
});

mediaQuery.addEventListener(&#39;change&#39;, function () {
  updateHeaderClass(mainVis.realIndex);
});

&lt;/script&gt;</code></pre><hr>
<h3 id="👉🏻-header-bg-스타일-충돌-방지">👉🏻 header bg 스타일 충돌 방지</h3>
<p><img src="https://velog.velcdn.com/images/choii_ii/post/c54f3867-fb5c-4243-8805-017081ff61c9/image.gif" alt="header bg 스타일 충돌 방지"></p>
<blockquote>
<p>💡 <strong>모션 분석</strong>
🔸 <strong>일정 스크롤에 도달할 때, header요소의 bg 스타일이 변경</strong> <em>(white-theme라는 클래스로 컨트롤)</em>
🔸 <strong>메인비주얼의 슬라이드 인덱스가 2일 때, header 요소의 색상이 #fff로 변경</strong>되고 있음 <em>(white라는 클래스로 컨트롤)</em>
🔸  따라서 두 스타일이 충돌해서는 안되기 때문에 <strong>white-theme 클래스가 추가될 때, white클래스를 제거하는 섬세한 스크립트 작성이 필요</strong></p>
</blockquote>
<p><strong>🩵 1. 슬라이드 인덱스가 2일 때, white 클래스 추가/제거</strong>
🔸 updateHeaderClass() 함수는 위 swiper on 이벤트에서 실행한다.</p>
<p>🔸 index 인수를 사용하는 이유는 <strong>activeSlideIndex 변수가 Swiper 내부에서 할당되기 때문에, Swiper 코드 외부에서 직접 접근하면 오류가 발생</strong>할 수 있기 때문이다.
따라서, <strong>let activeSlideIndex = 0;을 선언하여 현재 슬라이드 인덱스를 저장하는 전역 변수를 만들었다.</strong>
이후, <strong>Swiper 내부에서는 activeSlideIndex = this.realIndex; 코드를 통해 슬라이드 인덱스를 업데이트</strong>한다.
즉, <strong>index라는 인수를 사용하여 함수 호출 시, Swiper의 현재 슬라이드 인덱스를 전달</strong>하면, 올바르게 동작할 수 있다.</p>
<p>🔸 <strong>theme-white 클래스와의 충돌을 방지</strong>하기 위해 조건문을 활용하여 스크립트를 작성했다.
🔹<strong>조건 1) 슬라이드 인덱스가 2와 매칭될 때</strong>
🔹true : <strong>theme-white 클래스가 없다면, white 클래스 추가</strong>
🔹false : white 클래스 무조건 제거</p>
<p><strong>🔹조건 2) 1070px 이상에서는 white 클래스 무조건 제거</strong></p>
<pre><code>&lt;script&gt;
let activeSlideIndex = 0;

function updateHeaderClass(index) {
  if (mediaQuery.matches) {
      // 슬라이드 인덱스(2) 매칭 시
      if (index === 2) {
      // theme-white가 없을 때, white 추가
      if (!$(&#39;#header&#39;).hasClass(&#39;theme-white&#39;)) {
        $(&#39;#header&#39;).addClass(&#39;white&#39;);
      }
    } else {
      // 1070px 이상일 때, 무조건 white 제거
      $(&#39;#header&#39;).removeClass(&#39;white&#39;);
    }
  } else{
      // 1070px 이상일 때, 무조건 white 제거
      $(&#39;#header&#39;).removeClass(&#39;white&#39;);
  }
}
&lt;/script&gt;</code></pre><p><strong>🩵 2. scrollTrigger + theme-white 클래스 추가/제거</strong>
🔸 PC에서는 ScrollTrigger를 사용하지 않음
🔹모바일 전용으로 적용되며, mediaQuery.matches 조건을 통해 <strong>1070px 이상일 때 kill()로 이벤트 제거</strong>
🔹혹시 모를 충돌을 방지하기 위해 <strong>기존 ScrollTrigger가 존재하면 먼저 kill()로 초기화</strong></p>
<p>🔸 ScrollTrigger 이벤트 설정
🔹.sc-mainvis 요소를 기준으로 <strong>스크롤이 90% 지점에 도달하면 theme-white 클래스를 추가하고, white 클래스를 제거</strong>
🔹반대로 <strong>스크롤이 되돌아가면(onLeaveBack), theme-white를 제거</strong>하고, <strong>현재 슬라이드 인덱스(activeSlideIndex)가 2라면, white 클래스를 다시 추가</strong></p>
<pre><code>&lt;script&gt;
let headerTrigger = null;

function setupScrollTrigger() {
    if (mediaQuery.matches) {  
        // 기존 scrollTrigger 제거 (pc와 모바일 충돌 방지)
        if (headerTrigger) {
            headerTrigger.kill();
          }

        headerTrigger = ScrollTrigger.create({
          trigger: &quot;.sc-mainvis&quot;,
          start: &quot;90% 0%&quot;,
          end: &quot;100% 0%&quot;,
          toggleActions: &quot;play none play reverse&quot;,
        //   markers: true,
          onEnter: function () {
            // theme-white 추가 시, white 클래스 제거
              $(&quot;#header&quot;).addClass(&#39;theme-white&#39;).removeClass(&#39;white&#39;);
          },
          onLeaveBack: function () {
            $(&quot;#header&quot;).removeClass(&#39;theme-white&#39;);
            if (activeSlideIndex === 2) {
            //    console.log(activeSlideIndex);
               $(&#39;#header&#39;).addClass(&#39;white&#39;);
              }
          }
        });
      } else {
        // 1070px 이상일 때 ScrollTrigger 비활성화
        if (headerTrigger) {
          headerTrigger.kill();
        }
      }
  }
  setupScrollTrigger();
&lt;/script&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[jQuery] 숫자 카운팅 효과 (Counting)]]></title>
            <link>https://velog.io/@choii_ii/JS-ES6-%EC%88%AB%EC%9E%90-%EC%B9%B4%EC%9A%B4%ED%8C%85-%ED%9A%A8%EA%B3%BC-Counting</link>
            <guid>https://velog.io/@choii_ii/JS-ES6-%EC%88%AB%EC%9E%90-%EC%B9%B4%EC%9A%B4%ED%8C%85-%ED%9A%A8%EA%B3%BC-Counting</guid>
            <pubDate>Tue, 04 Feb 2025 02:46:02 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 <strong>해당 슬라이드에 도달 시, 숫자 카운팅 스크립트 동작</strong>
💚 숫자 증가 과정을 steps로 나누어, 주어진 <strong>duration 내에서 자연스럽고 일정한 속도로 숫자가 증가하도록 세분화</strong>
💚 숫자가 점진적으로 증가하는 애니메이션을 구현할 때 <strong>setInterval()을 활용</strong>하면 쉽게 구현 가능!</p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 마크업 시, <strong>시작값으로 써주는 것이 일반적</strong>
🩵 <strong>초기 로딩 시 자연스러운 상태를 유지하기 위함</strong></p>
<pre><code>&lt;body&gt;
&lt;h2 class=&quot;main-vis__maintitle yellow counter&quot;&gt;279,000&lt;/h2&gt;
&lt;/body&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-jquery">👉🏻 스크립트는 이렇게! (jQuery)</h2>
<h3 id="🩵-코드-미리보기">🩵 코드 미리보기</h3>
<pre><code>&lt;script&gt;
function startCounting() {
    startCounter(&#39;#counter&#39;, 279000, 2396445, 2000);
}

function startCounter(selector, start, end, duration) {
    const counterEl = document.querySelector(selector);

    const totalCount = end - start; // 증가해야 할 숫자의 범위
    const interval = 30; // 숫자가 변경될 간격 (밀리초 단위)
    const steps = Math.floor(duration / interval); // 전체 애니메이션 진행 횟수
    let currentStep = 0; // 현재 진행 중인 스텝 수

    // 선택한 모든 요소에 대해 카운팅 애니메이션 실행
    const counter = setInterval(function() {
        currentStep++;
        const currentCount = start + Math.round(totalCount * (currentStep / steps));  

        counterEl.innerHTML = currentCount.toLocaleString();

        if (currentStep &gt;= steps) {
            clearInterval(counter);
        }
    }, interval);
}
&lt;/script&gt;</code></pre><h3 id="🩵-startcounting-함수">🩵 startCounting() 함수</h3>
<pre><code>&lt;script&gt;
function startCounting() {
    startCounter(&#39;#counter&#39;, 279000, 2396445, 3000);
    // startCounter(&#39;요소&#39;, &#39;시작값&#39;, &#39;종료값&#39;, &#39;지속시간&#39;);
}
&lt;/script&gt;</code></pre><p>🩵 <strong>startCounter() 함수에 전달할 인수를 정의</strong>
🔹 .counter 클래스를 가진 모든 요소에서 카운팅 애니메이션을 시작
🔹 숫자가 279,000부터 2,396,445까지 증가하며, 애니메이션 지속 시간은 2초(2000ms)</p>
<h3 id="🩵-startcounter-함수-선언-카운트-증가-방식">🩵 startCounter() 함수 선언 (카운트 증가 방식)</h3>
<p>🩵 <strong>totalCount = end - start : 증가해야 할 숫자의 범위</strong>
🔹 2,396,445 - 279,000 = 2,117,445</p>
<p>🩵 <strong>interval = 30 : 숫자가 변경될 간격</strong>을 30ms(0.03초)로 설정
🔹 너무 짧은 간격이면 부드럽지 않고, 너무 길면 끊기는 느낌이 들 수 있다.</p>
<p>🩵 <strong>steps = Math.ceil(duration / interval) : 전체 애니메이션을 몇 단계로 나눌 것인지</strong> 계산
🔹 3000ms / 50ms = 60번
🔹 <strong>Math.ceil() : 소수점을 올림</strong>하여 가장 가까운 정수를 반환
🔹 남은 간격을 고려하여 더 많은 단계를 생성할 수 있기 때문에, <strong>카운팅이 종료될 때 더 정확한 값에 도달</strong>할 수 있다고 한다. (소수점이하 자리가 발생할 경우를 대비하여 올림처리)</p>
<p>🚫 숫자가 무조건 1씩 증가하면 너무 느리고, 너무 크게 증가하면 부자연</p>
<p>🚫 <strong>Math.floor()를 사용하여 내림을 하면 소수점 반올림이 일관되지 않게 적용되어 목표 값을 정확히 찍지 못할 수 있어, 남는 시간이 버려지게 되거나 부족하게 계산될 수 있다.</strong> 따라서 end값에 더 정확하게 도달할 수 있는 ceil을 사용하여 올림을 했습니다.</p>
<p>🩵 <strong>currentStep = 0 : 현재 진행 중인 단계</strong></p>
<h3 id="🩵-setinterval을-이용한-숫자-증가">🩵 setInterval()을 이용한 숫자 증가</h3>
<p>🩵 <strong>currentStep++ : 1씩 증가</strong></p>
<p>🩵 <strong>currentCount = start + Math.round(totalCount * currentStep / steps)</strong>
🔹 처음에는 279000 + (2117445 * 1 / 60) = 314907
🔹 두 번째는 279000 + (2117445 * 2 / 60) = 350815
🔹 이렇게 종료값(2,396,445)까지 점점 증가
🔹 <strong>Math.round() : 반올림</strong>하여 오차가 조금씩 발생할 수 있지만, <strong>일반적으로 end 값에 거의 정확히 도달</strong></p>
<p>🚫 <em>duration만으로 카운팅을 조절하면, 정확한 증가 단위, 간격 유지가 어렵고, 설정한 2초보다 긴 시간동안 카운팅이 되었다.</em> 따라서 <strong>애니메이션을 진행시킬 단계를 계산</strong>한 후, <strong>증가해야 할 범위(totalCount)를 steps로 나눠서 일정한 속도로 증가</strong>하도록 계산식을 작성</p>
<p><strong>💡 프레임 단위(interval)로 나누어 숫자가 부드럽게 증가하도록 최적화한 코드!</strong></p>
<p>🩵 <strong>counterEl.innerHTML = currentCount.toLocaleString();</strong>
🔹 <strong>.innerHTML : HTML 요소의 내용을 변경</strong>
🔹 <strong>.toLocaleString()</strong> : 숫자를 <strong>1,234,567과 같은 형식으로 표시</strong>해 가독성 향상</p>
<h3 id="🩵clearinterval을-이용한-카운트-종료">🩵clearInterval()을 이용한 카운트 종료</h3>
<p>🩵 <strong>currentStep이 steps 값에 도달</strong>하면 <strong>clearInterval(counter)를 호출하여 애니메이션을 중지</strong></p>
<hr>
<h2 id="🥨-한-줄-기록">🥨 한 줄 기록</h2>
<p>만약 속도를 더 빠르게 설정하고 싶다면, duration을 낮추고, interval 값을 줄여 결과적으로 steps을 줄이면 된다.
interval 값은 작아질수록 더 자주 숫자가 변경되므로 속도가 체감상 빨라진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[jQuery] click 이벤트를 활용한 To-do List 만들기]]></title>
            <link>https://velog.io/@choii_ii/jQuery-click-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-To-do-List-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@choii_ii/jQuery-click-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-To-do-List-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 31 Jan 2025 17:04:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/choii_ii/post/a4042bec-c356-46a3-9192-7289188c8c0f/image.png" alt=""></p>
<h2 id="📌-key-point">📌 KEY POINT</h2>
<p>💚 input 박스를 통해 텍스트 입력 후 <strong>전송 버튼 클릭 시, &#39;할일&#39; 목록 추가</strong>
💚 <strong>끝/보류 버튼 클릭 시, 각 항목에 추가되며 기존 리스트에서는 사라짐</strong>
💚 <strong>데이터 보간법</strong>을 활용하여 리스트를 추가하는 방법
💚 <strong>동적생성태그의 이벤트 위임</strong></p>
<hr>
<h2 id="👉🏻-마크업은-이렇게-html5">👉🏻 마크업은 이렇게! (HTML5)</h2>
<p>🩵 <strong>리스트 입력 박스 / 할 일 · 끝낸 일 · 보류된 일 리스트</strong> 마크업
🩵 <strong>끝 · 보류 · 삭제 버튼</strong> 마크업</p>
<pre><code>&lt;body&gt;
&lt;!-- 리스트 입력 박스 --!&gt;
&lt;input type=&quot;text&quot; class=&quot;text&quot;&gt;
&lt;button class=&quot;submit&quot;&gt;전송&lt;/button&gt;

&lt;!-- 할 일 --!&gt;
&lt;h2&gt;할 일&lt;/h2&gt;
&lt;ul class=&quot;list1&quot;&gt;
    &lt;li&gt;
        &lt;span class=&quot;work&quot;&gt;빨래&lt;/span&gt;
        &lt;button class=&quot;end&quot;&gt;끝&lt;/button&gt;
        &lt;button class=&quot;wait&quot;&gt;보류&lt;/button&gt;
        &lt;button class=&quot;del&quot;&gt;삭제&lt;/button&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;!-- 끝낸 일 --!&gt;
&lt;h2&gt;끝낸 일&lt;/h2&gt;
&lt;ul class=&quot;list2&quot;&gt;
    &lt;li&gt;
        &lt;span class=&quot;work&quot;&gt;창고정리&lt;/span&gt;
        &lt;button class=&quot;del&quot;&gt;삭제&lt;/button&gt;
    &lt;/li&gt;
&lt;/ul&gt;

&lt;!-- 보류된 일 --!&gt;
&lt;h2&gt;보류된 일&lt;/h2&gt;
&lt;ul class=&quot;list3&quot;&gt;
    &lt;li&gt;
        &lt;span class=&quot;work&quot;&gt;설거지&lt;/span&gt;
        &lt;button class=&quot;end&quot;&gt;끝&lt;/button&gt;
        &lt;button class=&quot;del&quot;&gt;삭제&lt;/button&gt;
    &lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;</code></pre><hr>
<h2 id="👉🏻-스크립트는-이렇게-jquery">👉🏻 스크립트는 이렇게! (jQuery)</h2>
<h3 id="🩵-공통-스크립트-설명">🩵 공통 스크립트 설명</h3>
<p>🩵 <strong>.click() : 클릭 이벤트</strong> 작성
🔹 <code>$(&#39;.submit&#39;).click(function(){})</code>;</p>
<p>🩵 <strong>동적으로 HTML 생성 (백틱을 이용한 문자열 보간 처리)</strong>
🔹 <strong>템플릿 리터럴(`)을 사용하여 HTML 문자열을 생성</strong>
🔹 keyword 변수를 ${} 안에 넣어, <strong>&#39;사용자가 입력한 텍스트&#39;를 태그 안에 삽입</strong></p>
<p>🩵 <strong>.append() : 특정 요소 뒤에 새로운 요소를 추가</strong>하는 메서드
🔹 <code>$(&#39;.list1&#39;).append(html);</code>
🔹 전송 · 보류 · 끝 버튼 클릭 시, 각 항목의 리스트 뒤에 요소가 추가됨</p>
<h3 id="🩵-input-입력-후-전송-버튼-클릭-이벤트">🩵 input 입력 후, &#39;전송&#39; 버튼 클릭 이벤트</h3>
<p>🩵 <strong>.val() : input을 통해 입력한 value 값 가져오기</strong>
🔹 <code>keyword = $(&#39;.text&#39;).val();</code>
🔹 내가 <strong>입력한 내용을 keyword 변수에 저장</strong> (데이터 보간을 위해 변수에 저장)
🔹 <strong>일반 텍스트일 경우 .text() 를 통해 가져옴</strong></p>
<p>🩵 <strong>if 조건문 : 입력값이 있을 때만 실행 (빈 값 방지)</strong>
🔹 입력값(keyword)이 비어 있지 않은 경우(true)에만 코드를 실행
🔹 만약 사용자가 아무것도 입력하지 않았다면(false), if문 내부의 코드는 실행되지 않음</p>
<p>🩵 <strong>입력 필드 초기화 &amp; 포커스 이동</strong>
🔹 <code>$(&#39;.text&#39;).val(&quot;&quot;).focus();</code>
🔹 .val(&quot;&quot;) : 입력창을 비워서 초기화
🔹 .focus() : 입력창에 자동으로 포커스 이동</p>
<pre><code>&lt;script&gt;
// 전송 버튼 클릭 -&gt; 할 일 리스트 추가
$(&#39;.submit&#39;).click(function(){
    keyword=$(&#39;.text&#39;).val();

    if(keyword){
        html =`&lt;li&gt;
        &lt;span class=&quot;work&quot;&gt;${keyword}&lt;/span&gt;
        &lt;button class=&quot;end&quot;&gt;끝&lt;/button&gt;
        &lt;button class=&quot;wait&quot;&gt;보류&lt;/button&gt;
        &lt;button class=&quot;del&quot;&gt;삭제&lt;/button&gt;
    &lt;/li&gt;`

    $(&#39;.list1&#39;).append(html);

    $(&#39;.text&#39;).val(&quot;&quot;).focus()
    }   
});
&lt;/script&gt;</code></pre><h3 id="🩵-동적생성태그의-이벤트-위임">🩵 동적생성태그의 이벤트 위임</h3>
<p>🚫 위 클릭 이벤트처럼 <strong>직접 이벤트를 걸면 초기의 document에 존재하는 요소에만 이벤트가 적용</strong>되어 <strong>동적으로 생성된 태그의 버튼 클릭 시, 이벤트가 적용되지 않는다.</strong></p>
<p>🩵 따라서 <strong>부모 요소(혹은 document)에 이벤트를 걸어 하위 요소에서 발생하는 이벤트를 감지</strong>해야하는데, 이것을 <strong>이벤트 위임이라고 한다.</strong></p>
<h3 id="🩵-삭제-버튼-클릭-이벤트">🩵 &#39;삭제&#39; 버튼 클릭 이벤트</h3>
<p>🩵 <strong>삭제(&#39;del&#39;) 버튼 클릭 시, 해당 리스트를 삭제</strong>
🔹 <code>$(this).parent().remove()</code>
🔹 삭제(&#39;del&#39;) 버튼의 부모인 li(리스트)를 찾아 remove(요소 삭제) 함</p>
<pre><code>&lt;script&gt;
$(document).on(&quot;click&quot;, &quot;.del&quot;, function(){
    $(this).parent().remove()
    //remove : 요소 삭제
});
&lt;/script&gt;</code></pre><h3 id="🩵-끝-버튼-클릭-이벤트">🩵 &#39;끝&#39; 버튼 클릭 이벤트</h3>
<p>🩵 끝(&#39;end&#39;) 버튼 클릭 시, 끝난 일 리스트에 추가하는 방법도 동일하게 이벤트 위임과 템플릿 리터럴을 통해 동적으로 태그를 생성한다.
🔹 단, 끝난 일로 리스트에 추가할 때에는 <strong>클릭한 버튼 리스트 명을 가져와 추가해야하기 때문에 .siblings() 메서드와 .text() 메서드를 활용</strong>해야한다.</p>
<pre><code>&lt;script&gt;
$(document).on(&quot;click&quot;, &quot;.end&quot;, function(){
    // 끝 버튼을 클릭한 요소의 텍스트를 가져옴
    keyword=$(this).siblings(&#39;.work&#39;).text();
    html =`&lt;li&gt;
            &lt;span class=&quot;work&quot;&gt;${keyword}&lt;/span&gt;
         &lt;button class=&quot;del&quot;&gt;삭제&lt;/button&gt;
        &lt;/li&gt;`

$(&#39;.list2&#39;).append(html);
    $(this).parent().remove();
});
&lt;/script&gt;</code></pre><h3 id="🩵-보관-버튼-클릭-이벤트">🩵 &#39;보관&#39; 버튼 클릭 이벤트</h3>
<p>🩵 이하 동일</p>
<pre><code>&lt;script&gt;
$(document).on(&quot;click&quot;, &quot;.wait&quot;, function(){
    keyword=$(this).siblings(&#39;.work&#39;).text();
    html =`&lt;li&gt;
        &lt;span class=&quot;work&quot;&gt;${keyword}&lt;/span&gt;
            &lt;button class=&quot;end&quot;&gt;끝&lt;/button&gt;
        &lt;button class=&quot;del&quot;&gt;삭제&lt;/button&gt;
    &lt;/li&gt;`

$(&#39;.list3&#39;).append(html);
    $(this).parent().remove();
});
&lt;/script&gt;</code></pre>]]></description>
        </item>
    </channel>
</rss>