<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mylee-p.log</title>
        <link>https://velog.io/</link>
        <description>Frontend Developer / Amateur Designer</description>
        <lastBuildDate>Wed, 11 Jan 2023 07:06:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mylee-p.log</title>
            <url>https://velog.velcdn.com/images/mylee-p/profile/021e5000-560a-4c49-a4a7-4e9e6dbb2c76/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mylee-p.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mylee-p" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[디자인부터 개발까지] 날씨 어플 만들기]]></title>
            <link>https://velog.io/@mylee-p/%EB%94%94%EC%9E%90%EC%9D%B8%EB%B6%80%ED%84%B0-%EA%B0%9C%EB%B0%9C%EA%B9%8C%EC%A7%80-%EB%82%A0%EC%94%A8-%EC%96%B4%ED%94%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@mylee-p/%EB%94%94%EC%9E%90%EC%9D%B8%EB%B6%80%ED%84%B0-%EA%B0%9C%EB%B0%9C%EA%B9%8C%EC%A7%80-%EB%82%A0%EC%94%A8-%EC%96%B4%ED%94%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 11 Jan 2023 07:06:15 GMT</pubDate>
            <description><![CDATA[<h2 id="날씨-어플리케이션-만들기">날씨 어플리케이션 만들기</h2>
<img width="691" alt="weather" src="https://user-images.githubusercontent.com/89143892/211734731-ffafeed6-a803-40cf-99a4-be3cdd26494f.png">

<h3 id="figma-이용하여-ui-디자인하기">Figma 이용하여 UI 디자인하기</h3>
<img width="1400" alt="figma_weather" src="https://user-images.githubusercontent.com/89143892/211735083-08509fc1-d050-46f0-bd81-963b3d550b6f.png">

<h3 id="openweathermap-api-날씨-정보-data-받기-무료-api">openweathermap API, 날씨 정보 data 받기 [무료 API]</h3>
<p>Current weather data API
<a href="https://openweathermap.org/current">Current weather data</a></p>
<p>postman 사용하여 데이터 조회
<a href="https://www.postman.com/">Postman API Platform | Sign Up for Free</a></p>
<h3 id="개발환경설정">개발환경설정</h3>
<pre><code>$ npx create-react-app weather-app
$ npm i react-icons --save</code></pre><pre><code class="language-js">//App.js

import WeatherApp from &quot;./WeatherApp&quot;;
import WeatherProvider from &quot;./WeatherProvider/WeatherProvider&quot;;

function App() {
    return (
        &lt;WeatherProvider&gt;
            &lt;WeatherApp /&gt;
        &lt;/WeatherProvider&gt;
    );
}

export default App;</code></pre>
<h3 id="날씨-조회-api-상태관리-context-api">날씨 조회 API 상태관리 [Context API]</h3>
<p>react의 context API를 이용해서 postman으로 호출한 API들을 가지고 불러온 데이터를
context API로 전역상태관리 해주기</p>
<ul>
<li>useState를 이용해서 API가 비정기적으로 데이터를 불러오니까 데이터가 로드되면 그 state를 업데이트해서 Provider에 전달하는 방식으로 구현</li>
</ul>
<pre><code class="language-js">//WeatherProvider.jsx

import React, { createContext, useEffect, useState } from &quot;react&quot;;

export const WeatherContext = createContext({});

function WeatherProvider({ children }) {
    const [weatherInfo, setWeatherInfo] = useState({});
    const getWeatherInfo = async () =&gt; {
        try {
            const currentWeatherInfoAPI =
                &quot;https://api.openweathermap.org/data/2.5/weather?appid=key값입력하기&amp;q=seoul&amp;units=metric&quot;;
            const currentWeatherInfo = await fetch(currentWeatherInfoAPI);
            const {
                name,
                coord: { lat, lon },
                main: {
                    temp,
                    humidity,
                    pressure,
                    feels_like,
                    temp_min,
                    temp_max,
                },
                sys: { sunset, sunrise },
                weather: [{ main: weatherState }],
                wind: { speed, deg },
            } = await currentWeatherInfo.json();
            const hourlyWeatherInfoAPI = `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&amp;lon=${lon}&amp;appid=key값입력하기&amp;units=metric`;
            const hourlyWeatherInfo = await fetch(hourlyWeatherInfoAPI);
            const { hourly } = await hourlyWeatherInfo.json();
            setWeatherInfo({
                name,
                temp,
                humidity,
                pressure,
                weatherState,
                feels_like,
                speed,
                deg,
                hourly,
                sunset,
                sunrise,
                temp_min,
                temp_max,
            });
        } catch (error) {
            console.error(error);
        }
    };
    useEffect(() =&gt; {
        getWeatherInfo();
    }, []);

    return (
        &lt;WeatherContext.Provider value={{ ...weatherInfo }}&gt;
            {children}
        &lt;/WeatherContext.Provider&gt;
    );
}

export default WeatherProvider;</code></pre>
<h3 id="날씨-앱-구조">날씨 앱 구조</h3>
<p>상단과 하단을 나누기 위해 컴포넌트 2개를 만들어줬다.</p>
<ul>
<li>상단 : MainWeather<ul>
<li>도시이름, 현재날씨[아이콘], 현재 온도, 최저기온, 최고기온을 useContext를 이용해서 전역상태를 가져와서 만들어준다.</li>
<li>오늘의 요일과 시간은 day.js 라이브러리를 이용해서 가져와 만들어준다.</li>
<li>날씨 아이콘은 상단, 하단 공통으로 쓰이기 때문에 따로 컴포넌트를 만들어 관리했다.</li>
</ul>
</li>
</ul>
<br/>

<p><em>날씨 아이콘 관리</em></p>
<pre><code class="language-js">//MainWeatherIcon

import React from &quot;react&quot;;
import {
  WiDayCloudy,
  WiDayRain,
  WiDaySunny,
  WiDust,
  WiDaySprinkle,
  WiDayThunderstorm,
  WiDaySnow,
  WiNa,
} from &quot;react-icons/wi&quot;;
function MainWeatherIcon({ weatherState, ...props }) {
  switch (weatherState) {
    case &quot;Thunderstorm&quot;:
      return &lt;WiDayThunderstorm {...props} /&gt;;
    case &quot;Snow&quot;:
      return &lt;WiDaySnow {...props} /&gt;;
    case &quot;Clouds&quot;:
      return &lt;WiDayCloudy {...props} /&gt;;
    case &quot;Clear&quot;:
      return &lt;WiDaySunny {...props} /&gt;;
    case &quot;Haze&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Mist&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Smoke&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Dust&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Fog&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Sand&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Ash&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Squall&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Tornado&quot;:
      return &lt;WiDust {...props} /&gt;;
    case &quot;Rain&quot;:
      return &lt;WiDayRain {...props} /&gt;;
    case &quot;Drizzle&quot;:
      return &lt;WiDaySprinkle {...props} /&gt;;
    default:
      return &lt;WiNa {...props} /&gt;;
  }
}

export default MainWeatherIcon;</code></pre>
<p><em>상단 날씨 부분 개발하기</em></p>
<pre><code class="language-js">//MainWeather

function MainWeather() {
    const { name, temp, weatherState, temp_min, temp_max } =
        useContext(WeatherContext);
    return (
        &lt;div className=&quot;weather&quot;&gt;
            &lt;div className=&quot;city&quot;&gt;{name?.toUpperCase()}&lt;/div&gt;
            &lt;div className=&quot;weather-icon&quot;&gt;
                &lt;MainWeatherIcon
                    weatherState={weatherState}
                    viewBox=&quot;0 0 30 30&quot;
                /&gt;
            &lt;/div&gt;
            &lt;div className=&quot;detail-weather&quot;&gt;
                &lt;div&gt;
                    &lt;div&gt;{parseFloat(temp).toFixed(1)}&amp;deg;&lt;/div&gt;
                    &lt;div className=&quot;detail-item&quot;&gt;
                        &lt;span&gt;
                            &amp;#9663;{parseFloat(temp_min).toFixed(1)}&amp;deg;
                        &lt;/span&gt;
                        &lt;span&gt;
                            &amp;#9653;{parseFloat(temp_max).toFixed(1)}&amp;deg;
                        &lt;/span&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}

export default MainWeather;</code></pre>
<p><em>상단 요일,시간 부분 개발하기</em></p>
<pre><code class="language-js">//MainWeather

function MainWeather() {
    .
    .
    return (
        .
        .
        &lt;div className=&quot;today&quot;&gt;
        &lt;div&gt;{dayjs().format(&#39;dddd&#39;).toUpperCase()}&lt;/div&gt;
      &lt;div&gt;{dayjs().format(&#39;H:mm A&#39;)}&lt;/div&gt;
    &lt;/div&gt;
    )
}</code></pre>
<ul>
<li>하단 : SubWeather</li>
</ul>
<p><em>일출, 풍속, 습도를 useContext 이용해서 전역상태 가져오기</em></p>
<pre><code class="language-js">const { sunrise, speed, humidity } = useContext(WeatherContext);</code></pre>
<p><em>sunrise는 초 단위로 정보가 내려오는데 new객체를 이용해서 밀리세컨드로 변환</em></p>
<pre><code class="language-js">new Date(sunrise * 1000).toLocaleString(&quot;en-US&quot;, {
    hour: &quot;numeric&quot;,
    minute: &quot;numeric&quot;,
    hour12: true,
})</code></pre>
<p><em>전체 구성</em></p>
<pre><code class="language-js">function SubWeather() {
    const { sunrise, speed, humidity } = useContext(WeatherContext);
    return (
        &lt;div className=&quot;sub-weather&quot;&gt;
            &lt;div className=&quot;sub-weather-item&quot;&gt;
                &lt;BsSunrise
                    style={{
                        .
                          .
                    }}
                /&gt;
                &lt;p className=&quot;sub-weather-text&quot;&gt;
                    &lt;span&gt;SUNRISE&lt;/span&gt;
                    {new Date(sunrise * 1000).toLocaleString(&quot;en-US&quot;, {
                        hour: &quot;numeric&quot;,
                        minute: &quot;numeric&quot;,
                        hour12: false,
                    })}
                &lt;/p&gt;
            &lt;/div&gt;
            &lt;div className=&quot;sub-weather-item&quot;&gt;
                &lt;BsWind
                    style={{
                        .
                        .
                    }}
                /&gt;
                &lt;p className=&quot;sub-weather-text&quot;&gt;
                    &lt;span&gt;WIND&lt;/span&gt;
                    {`${speed}m/s`}
                &lt;/p&gt;
            &lt;/div&gt;
            &lt;div className=&quot;sub-weather-item&quot;&gt;
                &lt;WiHumidity
                    style={{
                           .
                        .
                    }}
                /&gt;
                &lt;p className=&quot;sub-weather-text&quot;&gt;
                    &lt;span&gt;HUMIDITY&lt;/span&gt;
                    {`${humidity}%`}
                &lt;/p&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}

export default SubWeather;</code></pre>
<h3 id="수정사항">수정사항</h3>
<p>패스트캠퍼스로 강의를 듣고 난 후 다시 앱 디자인을 하여 만들어봤는데
역시 아쉬움이 많이 남는다. 무료API 종류가 바뀌기도 해서 더욱 아쉽다.</p>
<p>도시 검색 기능과 날씨에 따라 배경이 자동으로 바뀌는 기능도 추가해서
새로 업데이트 해야겠다는 생각이 든다. </p>
<p>나중엔 Heroku를 이용해서 배포도 해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발] 카카오지도 API 응용하기]]></title>
            <link>https://velog.io/@mylee-p/%EA%B0%9C%EB%B0%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4%EC%A7%80%EB%8F%84-API-%EC%9D%91%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@mylee-p/%EA%B0%9C%EB%B0%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4%EC%A7%80%EB%8F%84-API-%EC%9D%91%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 11 Jan 2023 05:52:03 GMT</pubDate>
            <description><![CDATA[<h2 id="카카오지도-api-응용하기">카카오지도 API 응용하기</h2>
<p><img src="https://user-images.githubusercontent.com/89143892/211727861-24546a1a-bda6-4805-ad79-ae94033a37a9.jpg" alt="map"></p>
<p><a href="https://apis.map.kakao.com/web/guide/">Kakao 지도 API</a></p>
<p><a href="https://developers.kakao.com/">Kakao Developers</a></p>
<p>사이트에 들어가서 앱 키를 발급받아야 API를 사용할 수 있다.</p>
<hr>
<h3 id="순수-js-라이브러리를-react에-적용하기">순수 JS 라이브러리를 React에 적용하기</h3>
<pre><code>$ yarn create react-app cacaomap --template typescript
$ yarn start</code></pre><p>&lt;실제 지도를 그리는 Javascript API 불러오기&gt;<br/>
index.html에 있는 head 와 body 가 모두 로딩된 다음, <br/>body의 <code>&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;</code> 안에 작성한 리액트 프로젝트가 들어가게 된다.<br/> index.html에다가 script 태그를 넣으면 리액트 프로젝트가 로딩되기 전에 카카오지도가 install 된다.<br/>
-script에 발급받은 key를 넣어도 인증이 되지 않았다고 오류창이 뜨는 경우 : <br/>브라우저에서 개발자 도구를 이용해 누구나 HTML 코드를 볼 수 있어 다른 사람의 key를 탈취해 <br/> 자신의 앱에 넣을 수 있음으로 카카오맵 이용권한을 뺏는 것을 방지하기 위해 자신이 등록한 사이트에서만 <br/>카카오맵키를 사용하는 것을 허용하고 있다.<br/> -플랫폼 설정하기에서 자신의 사이트를 등록해주면 된다. <code>[ex) http://localhost:3000]</code></p>
<pre><code class="language-html">&lt;!-- public/index.html --&gt;

&lt;script
    type=&quot;text/javascript&quot;
    src=&quot;//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다.&quot;
&gt;&lt;/script&gt;</code></pre>
<h3 id="브라우저에-지도띄우는-코드-작성">브라우저에 지도띄우는 코드 작성</h3>
<p>외부에서 CDN방식으로 <code>&lt;script&gt;</code> 태그로 가져오는 라이브러리들은 <code>window.</code>을 통해 접근 가능하고<br/> 타입스크립트의 경우 <code>window</code>에다가 <code>declare</code>를 통해서 <code>interface</code>안에 새로운 프로퍼티를 정의하여 <br/>함수들을 가져올 수 있다.</p>
<pre><code class="language-tsx">//src/App.tsx

import React, { useEffect } from &quot;react&quot;;
import logo from &quot;./logo.svg&quot;;

//타입스크립트에서 윈도우객체에 kakao 추가하는 방법
declare global {
    interface Window {
        kakao: any;
    }
}

function App() {
    //useEffect를 통해서 마크업들이 로딩된 이후에 실행될 수 있도록 코드 작성
    useEffect(() =&gt; {
        const container = document.getElementById(&quot;map&quot;);
        var options = {
            center: new window.kakao.maps.LatLng(33.450701, 126.570667),
            level: 3,
        };

        var map = new window.kakao.maps.Map(container, options);
    }, []);
    return (
        &lt;div&gt;
            &lt;div
                id=&quot;map&quot;
                style={{
                    width: 300,
                    height: 300,
                }}
            &gt;&lt;/div&gt;
        &lt;/div&gt;
    );
}

export default App;</code></pre>
<p>외부에서 정의된 함수를 가져올 때 코드 작성<br/>-html script 함수를 먼저 작성, App.tsx 파일에서 불러온다.</p>
<pre><code class="language-html">&lt;!-- public/index.html --&gt;

&lt;script
    type=&quot;text/javascript&quot;
    src=&quot;//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다.&quot;
&gt;&lt;/script&gt;

&lt;script&gt;
    function loadMap() {
        const container = document.getElementById(&quot;map&quot;);

        var options = {
            center: new window.kakao.maps.LatLng(33.450701, 126.570667),
            level: 3,
        };

        var map = new window.kakao.maps.Map(container, options);
    }
&lt;/script&gt;</code></pre>
<pre><code class="language-tsx">// src/App.tsx

import React, { useEffect } from &quot;react&quot;;
import logo from &quot;./logo.svg&quot;;

declare global {
    interface Window {
        loadMap: () =&gt; void;
    }
}

function App() {
    useEffect(() =&gt; {
        window.loadMap();
    }, []);
    return (
        &lt;div&gt;
            &lt;div
                id=&quot;map&quot;
                style={{
                    width: 300,
                    height: 300,
                }}
            &gt;&lt;/div&gt;
        &lt;/div&gt;
    );
}

export default App;</code></pre>
<p>함수들이 어떤 타입의 매개변수를 원하는지 미리 파악하여 응용하기<br/><br/>*react에서는 id 쓰는 것을 권장하지 않는다 <br/> -&gt; getElementById 대신 useRef를 통해 엘레먼트 가져오기</p>
<pre><code class="language-tsx">// src/App.tsx

import React, { useEffect, useRef } from &quot;react&quot;;
import logo from &quot;./logo.svg&quot;;

declare global {
    interface Window {
        kakao: any;
    }
}

function App() {
    const mapRef = useRef&lt;HTMLDivElement&gt;(null);

    useEffect(() =&gt; {
        if (mapRef.current) {
            var options = {
                center: new window.kakao.maps.LatLng(33.450701, 126.570667),
                level: 3,
            };
            var map = new window.kakao.maps.Map(mapRef.current, options);
        }
    }, []);
    return (
        &lt;div&gt;
            &lt;div
                ref={mapRef}
                style={{
                    width: 300,
                    height: 300,
                }}
            &gt;&lt;/div&gt;
        &lt;/div&gt;
    );
}

export default App;</code></pre>
<p>리액트스럽게 코드 변경하기<br/><br/>-index.html에 script를 넣게되면 지도를 사용하지 않을 때에도 지도 자바스크립트를 로딩해야되는 <br> 이슈가 있고, index.html에 있는 외부 함수를 window를 통해 접근하는 것이 오류의 원인이기도 한다<br/>[디버깅 할 때 검증하기 어려움] -&gt; 성능 저하, 앱크기 증가 등 악영향이 있을 수 있다.<br/> -동적으로 로딩하게 되면 필요할 때 지도가 보이는 페이지에서만 로딩할 수 있다.<br/> -반드시 script가 로딩 된 이후에 카카오지도가 로딩되는 코드가 실행되어야 한다.</p>
<pre><code class="language-tsx">// src/App.tsx

declare global {
    interface Window {
        kakao: any;
    }
}

function App() {
    const mapRef = useRef&lt;HTMLDivElement&gt;(null);

    useEffect(() =&gt; {
        const script = document.createElement(&quot;script&quot;);
        script.src = document.head.appendChild(script);
        script.onload = () =&gt; {
            window.kakao.maps.load(() =&gt; {
                if (mapRef.current) {
                    var options = {
                        center: new window.kakao.maps.LatLng(
                            33.450701,
                            126.570667
                        ),
                        level: 3,
                    };
                    var map = new window.kakao.maps.Map(
                        mapRef.current,
                        options
                    );
                }
            });
        };
        return () =&gt; script.remove();
    }, []);

    return (
        &lt;div&gt;
            &lt;div
                ref={mapRef}
                style={{
                    width: 300,
                    height: 300,
                }}
            &gt;&lt;/div&gt;
        &lt;/div&gt;
    );
}

export default App;</code></pre>
<h3 id="카카오지도-응용하기">카카오지도 응용하기</h3>
<p>useRef를 이용하여 지역 이동 버튼 구현하기<br/> -값이 새로 설정된다고 해도 리렌더링이 일어나지 않고 UI에 영향을 주지 않게 구현</p>
<pre><code class="language-tsx">// src/App.tsx

.
.
function App() {
    const mapRef = useRef&lt;HTMLDivElement&gt;(null);
    //useRef는 리액트가 리렌더링 되더라도 데이터를 계속 보관하고 있다.
    const map = useRef&lt;any&gt;(null);

    useEffect(() =&gt; {
        const script = document.createElement(&quot;script&quot;);

        script.src =
            &quot;//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다&amp;autoload=false&quot;;

        document.head.appendChild(script);

        script.onload = () =&gt; {
            window.kakao.maps.load(() =&gt; {
                if (mapRef.current) {
                    var options = {
                        center: new window.kakao.maps.LatLng(
                            33.450701,
                            126.570667
                        ),
                        level: 10,
                        draggable: false,
                    };
                    //current안에 있는 값은 리렌더링이 되어도 초기화되지 않는다.
                    map.current = new window.kakao.maps.Map(mapRef.current, options)
                }
            });
        };

        return () =&gt; script.remove();
    }, []);

    return (
        &lt;div&gt;
            {/* 지역이동하는 버튼만들기 */}
            &lt;button
                onClick={() =&gt; {
                    map.current.setCenter(
                        new window.kakao.maps.LatLng(37.5665, 126.9780)
                    );
                }}
            &gt;
                서울
            &lt;/button&gt;
            &lt;button
                onClick={() =&gt; {
                    map.current.setCenter(
                        new window.kakao.maps.LatLng(35.1796, 129.0756)
                    );
                }}
            &gt;
                부산
            &lt;/button&gt;
            &lt;div
                ref={mapRef}
                style={{
                    width: 300,
                    height: 300,
                }}
            &gt;&lt;/div&gt;
        &lt;/div&gt;
    );
}

export default App;</code></pre>
<h3 id="다른-속성-이용하기">다른 속성 이용하기</h3>
<p>-지도에 마커 추가하기<br/>-마커 추가할 때 타이틀 입력 기능 추가<br/>-여러번 추가 했을 때 리스트 생성<br/>-마커 삭제 기능 추가</p>
<pre><code class="language-tsx">// src/App.tsx

function App() {
    const mapRef = useRef&lt;HTMLDivElement&gt;(null);
    //마커들의 정보를 저장해서 아래에 띄워주기
    const [markerList, setMarkerList] = useState&lt;any[]&gt;([]);
    const map = useRef&lt;any&gt;(null);

    useEffect(() =&gt; {
        const script = document.createElement(&quot;script&quot;);

        script.src =
            &quot;//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다&amp;autoload=false&quot;;

        document.head.appendChild(script);

        script.onload = () =&gt; {
            window.kakao.maps.load(() =&gt; {
                if (mapRef.current) {
                    var options = {
                        center: new window.kakao.maps.LatLng(
                            33.450701,
                            126.570667
                        ),
                        level: 10,
                    };
                    map.current = new window.kakao.maps.Map(
                        mapRef.current,
                        options
                    );
                    //마우스 오른쪽 버튼을 클릭하면 지도에 마크 생기게 하는 로직
                    window.kakao.maps.event.addListener(
                        map.current,
                        &quot;rightclick&quot;,
                        (mouseEvent: any) =&gt; {
                            const latlng = mouseEvent.latLng;
                            //우클릭을 했을 때 타이틀 작성해주기
                            const title =
                                prompt(&quot;마커의 타이틀을 입력해주세요.&quot;);

                            //마커 생성하기
                            var marker = new window.kakao.maps.Marker({
                                map: map.current,
                                position: latlng,
                                title,
                            });
                            //마커가 추가될때마다 setMarkerList에 추가
                            setMarkerList((prev) =&gt; [...prev, marker]);
                        }
                    );
                }
            });
        };

        return () =&gt; script.remove();
    }, []);

    return (
        &lt;div&gt;
            &lt;button
                onClick={() =&gt; {
                    map.current.setMapTypeId(
                        window.kakao.maps.MapTypeId.HYBRID
                    );
                }}
            &gt;
                지도 타입 변경
            &lt;/button&gt;

            &lt;div
                ref={mapRef}
                style={{
                    width: 300,
                    height: 300,
                }}
            &gt;&lt;/div&gt;
            {
                // 만들어진 div가 click되었을 때 마커 제거
                markerList.map((value) =&gt; (
                    &lt;div
                        onClick={() =&gt; {
                            value.setMap(null);
                            //state에서도 마커 제거
                            setMarkerList(
                                markerList.filter((v) =&gt; v !== value)
                            );
                        }}
                    &gt;
                        {value.getTitle()}
                    &lt;/div&gt;
                ))
            }
        &lt;/div&gt;
    );
}

export default App;</code></pre>
<pre><code class="language-tsx">// src/App.tsx</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[
TODO LIST 개발]]></title>
            <link>https://velog.io/@mylee-p/TODO-LIST-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@mylee-p/TODO-LIST-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Tue, 10 Jan 2023 09:36:43 GMT</pubDate>
            <description><![CDATA[<h2 id="todo-list">ToDo List</h2>
<img width="1072" alt="todoList" src="https://user-images.githubusercontent.com/89143892/211495564-1b02d23f-a12e-4506-8bc5-889d3f1e9c1a.png">

<p>-input에 할 일 입력 후 + 버튼을 누르면 할 일이 추가되는 기능<br/>-완료 표시, 수정 기능<br/>-삭제 기능 추가<br/>-필터 기능을 사용해서 전체, 해야될 것, 완료된 것을 구분<br/>-todoList에 추가한 내용이 LocalStorage에 저장되어 그대로 내용을 불러오는 기능 구현</p>
<h3 id="개발환경설정---rollupjs">개발환경설정 - Rollup.js</h3>
<pre><code>$ npm init -y
$ npm i -D rollup
$ npm i -D rollup-plugin-scss sass
$ npm i -D rollup-plugin-generate-html-template rollup-plugin-livereload
$ npm i -D rollup-plugin-serve rollup-plugin-terser
$ npm install --save @fortawesome/fontawesome-free</code></pre><p>공통 설정</p>
<pre><code class="language-js">// rollup.common.config.js

import htmlTemplate from &#39;rollup-plugin-generate-html-template&#39;;
import scss from &#39;rollup-plugin-scss&#39;;
import { nodeResolve } from &#39;@rollup/plugin-node-resolve&#39;;
export default {
    input: &#39;src/js/index.js&#39;,
    output: {
        file: &#39;./dist/bundle.js&#39;,
        format: &#39;cjs&#39;,
        sourcemap: true,
    },
    plugins: [
        nodeResolve(),
        scss({
            insert: true,
            sourceMap: true,
        }),
        htmlTemplate({
            template: &#39;src/index.html&#39;,
            target: &#39;index.html&#39;,
        }),
    ],
};</code></pre>
<p>dev server 설정</p>
<pre><code class="language-js">// rollup.dev.config.js

import rollupCommonConfig from &#39;./rollup.common.config&#39;;
import serve from &#39;rollup-plugin-serve&#39;;
//watch가 돌았을 때 dev server를 다시 리로드 시켜주는 플러그인
import livereload from &#39;rollup-plugin-livereload&#39;;

const config = { ...rollupCommonConfig };

config.watch = {
    inclue: &#39;src/**&#39;,
};

config.plugins = [
    ...config.plugins,
    serve({
        host: &#39;localhost&#39;,
        port: 8080,
        open: true,
        contentBase: &#39;dist&#39;,
    }),
    livereload(&#39;dist&#39;),
];

export default config;</code></pre>
<pre><code class="language-js">// rollup.prod.config.js

import rollupCommonConfig from &#39;./rollup.common.config&#39;;

//Uglify/Minify 설정만 넣어주면 됨
const config = { ...rollupCommonConfig };

config.plugins = [...config.plugins, terser()];

export default config;</code></pre>
<p>ESLint / prettier 설정</p>
<pre><code>$ npm i -D eslint
$ npm i -D --save-exact prettier
$ npm i -D eslint-config-prettier eslint-plugin-prettier</code></pre><h3 id="html-구조">HTML 구조</h3>
<pre><code class="language-html">&lt;!-- index.html --&gt;

&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset=&quot;UTF-8&quot; /&gt;
        &lt;title&gt;TodoList&lt;/title&gt;
        &lt;link
            rel=&quot;shortcut icon&quot;
            href=&quot;data:image/x-icon&quot;
            type=&quot;image/x-icon&quot;
        /&gt;
    &lt;/head&gt;

    &lt;body&gt;
        &lt;header&gt;
            &lt;h1&gt;Todo List&lt;/h1&gt;
        &lt;/header&gt;
        &lt;div class=&quot;input-container&quot; id=&quot;input-container&quot;&gt;
            &lt;div class=&quot;input-area&quot; id=&quot;input-area&quot;&gt;
                &lt;input
                    type=&quot;text&quot;
                    class=&quot;todo-input&quot;
                    id=&quot;todo-input&quot;
                    maxlength=&quot;50&quot;
                /&gt;
                &lt;button class=&quot;todo-btn&quot; id=&quot;add-btn&quot; type=&quot;button&quot;&gt;
                    &lt;i class=&quot;fas fa-plus&quot;&gt;&lt;/i&gt;
                &lt;/button&gt;
            &lt;/div&gt;
            &lt;div class=&quot;radio-area&quot; id=&quot;radio-area&quot;&gt;
                &lt;input
                    type=&quot;radio&quot;
                    id=&quot;filter1&quot;
                    name=&quot;filter&quot;
                    value=&quot;ALL&quot;
                    checked
                /&gt;
                &lt;label for=&quot;filter1&quot;&gt;All&lt;/label&gt;
                &lt;input type=&quot;radio&quot; id=&quot;filter2&quot; name=&quot;filter&quot; value=&quot;TODO&quot; /&gt;
                &lt;label for=&quot;filter2&quot;&gt;Todo&lt;/label&gt;
                &lt;input type=&quot;radio&quot; id=&quot;filter3&quot; name=&quot;filter&quot; value=&quot;DONE&quot; /&gt;
                &lt;label for=&quot;filter3&quot;&gt;Done&lt;/label&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;todo-container&quot; id=&quot;todo-container&quot;&gt;
            &lt;div class=&quot;todo-list&quot; id=&quot;todo-list&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="할-일-생성-기능-구현">할 일 생성 기능 구현</h3>
<pre><code class="language-js">// index.js

class TodoList {
    constructor() {
        this.assignElement();
        this.addEvent();
    }
    assignElement() {
        this.inputContainerEl = document.getElementById(&#39;input-container&#39;);
        this.inputAreaEl = this.inputContainerEl.querySelector(&#39;#input-area&#39;);
        this.todoInputEl = this.inputAreaEl.querySelector(&#39;#todo-input&#39;);
        this.addBtnEl = this.inputAreaEl.querySelector(&#39;#add-btn&#39;);
        this.todoContainerEl = document.getElementById(&#39;todo-container&#39;);
        this.todoListEl = this.todoContainerEl.querySelector(&#39;#todo-list&#39;);
    }
    // 이벤트를 추가
    addEvent() {
        //할 일 입력 후 add 버튼을 누르면 그 값을 가져와서 아래에 todoEl를 생성
        this.addBtnEl.addEventListener(&#39;click&#39;, this.onClickAddBtn.bind(this));
    }
    onClickAddBtn() {
        if (this.todoInputEl.value.length === 0) {
            alert(&#39;내용을 입력해주세요.&#39;);
            return;
        }
        this.createTodoElement(this.todoInputEl.value);
    }

    createTodoElement(value) {
        const todoDiv = document.createElement(&#39;div&#39;);
        todoDiv.classList.add(&#39;todo&#39;);

        //input태그를 넣어서 수정에 용이하도록 한다.
        const todoContent = document.createElement(&#39;input&#39;);
        todoContent.value = value;

        //수정버튼을 눌렀을 때만 수정할 수 있도록 하기
        todoContent.readOnly = true;
        todoContent.classList.add(&#39;todo-item&#39;);

        //버튼들 순서대로 만들기 [완료,수정,삭제]
        const fragment = new DocumentFragment();
        fragment.appendChild(todoContent);
        fragment.appendChild(
            this.createButton(&#39;complete-btn&#39;, &#39;complete-btn&#39;, [
                &#39;fas&#39;,
                &#39;fa-check&#39;,
            ]),
        );
        fragment.appendChild(
            this.createButton(&#39;edit-btn&#39;, &#39;edit-btn&#39;, [&#39;fas&#39;, &#39;fa-edit&#39;]),
        );
        fragment.appendChild(
            this.createButton(&#39;delete-btn&#39;, &#39;delete-btn&#39;, [&#39;fas&#39;, &#39;fa-trash&#39;]),
        );
        fragment.appendChild(
            this.createButton(&#39;save-btn&#39;, &#39;save-btn&#39;, [&#39;fas&#39;, &#39;fa-save&#39;]),
        );
        todoDiv.appendChild(fragment);
        this.todoListEl.appendChild(todoDiv);
        //할 일을 추가한 후 input 내용을 지우는 것을 append한다.
        this.todoInputEl.value = &#39;&#39;;
    }

    //버튼은 반복되는 작업이기 때문에 메소드를 이용해 따로 만들어준다
    createButton(btnId, btnClassName, iconClassName) {
        const btn = document.createElement(&#39;button&#39;);
        const icon = document.createElement(&#39;i&#39;);
        icon.classList.add(...iconClassName);
        btn.appendChild(icon);
        btn.id = btnId;
        btn.classList.add(btnClassName);
        return btn;
    }
}

//인스턴스 생성
document.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {
    const todoList = new TodoList();
});</code></pre>
<h3 id="할-일-삭제-기능-구현">할 일 삭제 기능 구현</h3>
<p>-이벤트 버블링 활용, todo-list 안에서 click을 캐치, 해당 버튼에 맞는 실행을 할 수 있도록 로직<br/>-이벤트를 한 번만 걸어서 나중에 동적으로 엘리먼트들이 추가되어도 따로 이벤트를 적용할 필요없는 로직</p>
<pre><code class="language-js">// index.js

class TodoList {
    .
    .
    addEvent() {
        this.addBtnEl.addEventListener(&#39;click&#39;, this.onClickAddBtn.bind(this));
        //todo-list에 이벤트 적용
        this.todoListEl.addEventListener(
            &#39;click&#39;,
            this.onClickTodoList.bind(this),
        );
    }

    onClickTodoList(event) {
        //Destructuring을 사용해서 이벤트 타겟을 뽑아내기
        const { target } = event;
        const btn = target.closest(&#39;button&#39;);
        if (btn.matches(&#39;#delete-btn&#39;)) {
            this.deleteTodo(target);
        }
    }

    //이벤트 타겟을 받기
    deleteTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        //todoDiv에 트랜지션이 끝났을 때의 시점을 캐치해서 지우는 이벤트를 추가
        todoDiv.addEventListener(&#39;transitionend&#39;, () =&gt; {
            //DOM구조에서 존재하는 엘리먼트 자체를 삭제
            todoDiv.remove();
        });
        //안보이게 지우기
        todoDiv.classList.add(&#39;delete&#39;);
    }
    .
    .
};</code></pre>
<h3 id="할-일-수정-완료-기능-구현">할 일 수정, 완료 기능 구현</h3>
<pre><code class="language-js">// index.js

class TodoList {
    .
    .
    onClickTodoList(event) {
        const { target } = event;
        const btn = target.closest(&#39;button&#39;);
        //버튼이 null일때 처리해주기
        if (!btn) return;

        if (btn.matches(&#39;#delete-btn&#39;)) {
            this.deleteTodo(target);
            //edit
        } else if (btn.matches(&#39;#edit-btn&#39;)) {
            this.editTodo(target);
            //save
        } else if (btn.matches(&#39;#save-btn&#39;)) {
            this.saveTodo(target)
            //complate
        } else if (btn.matches(&#39;#complete-btn&#39;)) {
            this.completeTodo(target);
        }
    }

    completeTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        //완료 버튼 클릭시 중간선 생성
        todoDiv.classList.toggle(&#39;done&#39;);

    }

    saveTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        //edit을 지우고
        todoDiv.classList.remove(&#39;edit&#39;);
        //todoInputEl을 찾아서 readonly를 해준다.
        const todoInputEl = todoDiv.querySelector(&#39;input&#39;);
        //save하면 수정할 수 없게 readonly를 true로 해준다.
        todoInputEl.readOnly = true
    }

    //수정버튼
    editTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        const todoInputEl = todoDiv.querySelector(&#39;input&#39;);
        //수정할거니까 입력이 가능하도록 해주기
        todoInputEl.readOnly = false;
        todoInputEl.focus();
        todoDiv.classList.add(&#39;edit&#39;);
    }
    .
    .
};</code></pre>
<h3 id="필터-기능-구현">필터 기능 구현</h3>
<p>-radio 버튼 탐색<br/>-all, todo, done 버튼을 눌렀을 때 value를 캐치해서 필터링 기능 구현<br/>-콜백을 실행하는 방식으로 필터를 실행하는 로직 구현</p>
<pre><code class="language-js">//index.js

class TodoList {
    constructor() {
        this.assignElement();
        this.addEvent();
    }

    assignElement() {
        .
        .
        //radio-area 탐색
        this.radioAreaEl = this.inputContainerEl.querySelector(&#39;#radio-area&#39;);
        //radio 버튼들 탐색
        this.filterRadioBtnEls = this.radioAreaEl.querySelectorAll(
            &#39;input[name=&quot;filter&quot;]&#39;,
        );
    }
    .
    .
    //radio 버튼들에 이벤트를 적용
    addRadioBtnEvent() {
        for (const filterRadioBtnEl of this.filterRadioBtnEls) {
            filterRadioBtnEl.addEventListener(
                &#39;click&#39;,
                this.onClickRadioBtn.bind(this),
            );
        }
    }

    onClickRadioBtn(event) {
        const { value } = event.target;
        console.log(value);
        this.filterTodo(value);
    }

    //필터 메소드는 따로  관리
    filterTodo(status) {
        const todoDivEls = this.todoListEl.querySelectorAll(&#39;div.todo&#39;);
        for (const todoDivEl of todoDivEls) {
            switch (status) {
                case &#39;ALL&#39;:
                    todoDivEl.style.display = &#39;flex&#39;;
                    break;
                case &#39;DONE&#39;:
                    todoDivEl.style.display = todoDivEl.classList.contains(
                        &#39;done&#39;,
                    )
                        ? &#39;flex&#39;
                        : &#39;none&#39;;
                    break;
                case &#39;TODO&#39;:
                    todoDivEl.style.display = todoDivEl.classList.contains(
                        &#39;done&#39;,
                    )
                        ? &#39;none&#39;
                        : &#39;flex&#39;;
                    break;
            }
        }
    }
    .
    .
};</code></pre>
<h3 id="router-개발">Router 개발</h3>
<p>해시를 이용해서 구현</p>
<pre><code class="language-js">// index.js

class Router {
    routes = [];
    notFoundCallback = () =&gt; {};
    addRoute(url, callback) {
        this.routes.push({
            url,
            callback,
        });
        return this;
    }
    checkRoute() {
        const currentRoute = this.routes.find(
            (route) =&gt; route.url === window.location.hash,
        );
        if (!currentRoute) {
            this.notFoundCallback();
            return;
        }
        currentRoute.callback();
    }
    init() {
        window.addEventListener(&#39;hashchange&#39;, this.checkRoute.bind(this));
        if (!window.location.hash) {
            window.location.hash = &#39;#/&#39;;
        }
        this.checkRoute();
    }
    setNotFound(callback) {
        this.notFoundCallback = callback;
        return this;
    }
}
.
.
document.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {
    const router = new Router();
    const todoList = new TodoList();
    const routeCallback = (status) =&gt; () =&gt; {
        todoList.filterTodo(status);
        document.querySelector(
            `input[type=&#39;radio&#39;][value=&#39;${status}&#39;]`,
        ).checked = true;
    };
    router
        .addRoute(&#39;#/all&#39;, routeCallback(&#39;ALL&#39;))
        .addRoute(&#39;#/todo&#39;, routeCallback(&#39;TODO&#39;))
        .addRoute(&#39;#/done&#39;, routeCallback(&#39;DONE&#39;))
        .setNotFound(routeCallback(&#39;ALL&#39;))
        .init();
});</code></pre>
<h3 id="localstorage에-할-일-data-연동">LocalStorage에 할 일 data 연동</h3>
<pre><code class="language-js">// index.js

class Storage {
    saveTodo(id, todoContent) {
        const todosData = this.getTodos();
        todosData.push({ id, content: todoContent, status: &#39;TODO&#39; });
        //만든 것을 다시 todos에 덮어씌워서 LocalStorage에 저장
        localStorage.setItem(&#39;todos&#39;, JSON.stringify(todosData));
    }
    editTodo() {}
    deleteTodo() {}
    getTodos() {
        //JSON.parse를 이용해서 기존데이터 가져오기
        return localStorage.getItem(&#39;todos&#39;) === null
            ? []
            : JSON.parse(localStorage.getItem(&#39;todos&#39;));
    }
}

class TodoList {
    constructor(storage) {
        .
        .
        this.loadSavedData();
    }
    //storage 받아서 TodoList안에 storage 갖고 있는다.
    initStorage(storage) {
        this.storage = storage;
    }
    .
    .
    //로컬스토리지에 담긴 데이터 불러오기
    loadSavedData() {
        const todosData = this.storage.getTodos();
        for (const todoData of todosData) {
            const { id, content, status } = todoData;
            this.createTodoElement(id, content, status);
        }
    }
    .
    .
    onClickAddBtn() {
        if (this.todoInputEl.value.length === 0) {
            alert(&#39;내용을 입력해주세요.&#39;);
            return;
        }
        const id = Date.now();
        //LocalStorage에 데이터 추가
        this.storage.saveTodo(id, this.todoInputEl.value);
        this.createTodoElement(id, this.todoInputEl.value);
    }

    //id, value, status값을 받도록 수정
    createTodoElement(id, value, status = null) {
        const todoDiv = document.createElement(&#39;div&#39;);
        todoDiv.classList.add(&#39;todo&#39;);
        if (status === &#39;DONE&#39;) {
            todoDiv.classList.add(&#39;done&#39;);
        }

        //나중에 수정할 때 사용하기 위해 todoDiv쪽에 dataset속성을 사용해서 id 부여
        todoDiv.dataset.id = id;
        .
        .
    }
    .
    .
}

document.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {
    const router = new Router();
    const todoList = new TodoList(new Storage());
    const routeCallback = (status) =&gt; () =&gt; {
        todoList.filterTodo(status);
        document.querySelector(
            `input[type=&#39;radio&#39;][value=&#39;${status}&#39;]`,
        ).checked = true;
    };
    router
        .addRoute(&#39;#/all&#39;, routeCallback(&#39;ALL&#39;))
        .addRoute(&#39;#/todo&#39;, routeCallback(&#39;TODO&#39;))
        .addRoute(&#39;#/done&#39;, routeCallback(&#39;DONE&#39;))
        .setNotFound(routeCallback(&#39;ALL&#39;))
        .init();
});</code></pre>
<h3 id="status-변경-edit-save도-localstorage에-연동">status 변경, edit, save도 LocalStorage에 연동</h3>
<pre><code class="language-js">// index.js

class Storage {
    saveTodo(id, todoContent) {
        const todosData = this.getTodos();
        todosData.push({ id, content: todoContent, status: &#39;TODO&#39; });
        localStorage.setItem(&#39;todos&#39;, JSON.stringify(todosData));
    }
    editTodo(id, todoContent, status = &#39;TODO&#39;) {
        const todosData = this.getTodos();
        const todoIndex = todosData.findIndex((todo) =&gt; todo.id == id);
        const targetTodoData = todosData[todoIndex];
        const editedTodoData =
            todoContent === &#39;&#39;
                ? { ...targetTodoData, status }
                : { ...targetTodoData, content: todoContent };
        todosData.splice(todoIndex, 1, editedTodoData);
        localStorage.setItem(&#39;todos&#39;, JSON.stringify(todosData));
    }
    deleteTodo(id) {
        const todosData = this.getTodos();
        todosData.splice(
            todosData.findIndex((todo) =&gt; todo.id == id),
            1,
        );
        localStorage.setItem(&#39;todos&#39;, JSON.stringify(todosData));
    }
    getTodos() {
        return localStorage.getItem(&#39;todos&#39;) === null
            ? []
            : JSON.parse(localStorage.getItem(&#39;todos&#39;));
    }
}

class TodoList {
    //클래스변수들 추가
    storage;
    inputContainerEl;
    inputAreaEl;
    todoInputEl;
    addBtnEl;
    todoContainerEl;
    todoListEl;
    radioAreaEl;
    filterRadioBtnEls;
    .
    .
    completeTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        todoDiv.classList.toggle(&#39;done&#39;);
        //storage.editTodo메소드를 불러와서 status 바꿔주기
        const { id } = todoDiv.dataset;
        this.storage.editTodo(
            id,
            &#39;&#39;,
            todoDiv.classList.contains(&#39;done&#39;) ? &#39;DONE&#39; : &#39;TODO&#39;,
        );
    }

    saveTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        todoDiv.classList.remove(&#39;edit&#39;);
        const todoInputEl = todoDiv.querySelector(&#39;input&#39;);
        todoInputEl.readOnly = true;
        const { id } = todoDiv.dataset;
        //editTodo에 id를 넣어주고 todoInputEl 넣어주기
        this.storage.editTodo(id, todoInputEl.value);
    }

    editTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        const todoInputEl = todoDiv.querySelector(&#39;input&#39;);
        todoInputEl.readOnly = false;
        todoInputEl.focus();
        todoDiv.classList.add(&#39;edit&#39;);
    }

    deleteTodo(target) {
        const todoDiv = target.closest(&#39;.todo&#39;);
        todoDiv.addEventListener(&#39;transitionend&#39;, () =&gt; {
            todoDiv.remove();
        });
        todoDiv.classList.add(&#39;delete&#39;);
        //todoDiv의 dataset의 id를 가져와서 삭제하기
        this.storage.deleleTodo(todoDiv.dataset.id);
    }
    .
    .
}
.
.</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 이미지 갤러리 개발]]></title>
            <link>https://velog.io/@mylee-p/React-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B0%A4%EB%9F%AC%EB%A6%AC-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@mylee-p/React-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B0%A4%EB%9F%AC%EB%A6%AC-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Tue, 10 Jan 2023 07:21:11 GMT</pubDate>
            <description><![CDATA[<h2 id="image-gallery">Image Gallery</h2>
<br/>

<img width="960" alt="imageGallery" src="https://user-images.githubusercontent.com/89143892/211480785-b971e430-785a-4ad6-af9f-4909091a32d1.png">

<p>-파일을 선택하면 이미지를 추가할 수 있게 개발<br />-이미지 삭제 기능 추가<br />-반응형으로 개발하여 화면이 작아지면 좌우로 스크롤하여 이미지를 볼 수 있는 기능 추가</p>
<br/>

<h3 id="react-프로젝트-생성">React 프로젝트 생성</h3>
<p>그냥 리액트를 사용하게 되면 따로 리액트, 리액트 돔, 번들러, 바벨 등등을 일일이 설치해야되기 때문에<br /> create react-app 설치 권장</p>
<pre><code>$ npm i -g yarn
$ yarn create react-app image-gallery[폴더명] --template typescript</code></pre><br/>

<h3 id="이미지-갤러리-로직-구현하기">이미지 갤러리 로직 구현하기</h3>
<p>-버튼을 클릭하면 이미지 파일을 드래그해서 내려받을 수 있고 삭제할 수 있는 가능 구현<br/>-이미지 박스들은 무한히 늘어날 수 있다. [이미지를 계속 추가할 수 있기 때문]<br/></p>
<p>*이미지가 들어갈 컴포넌트 만들기</p>
<pre><code class="language-tsx">// src/components/ImageBox.tsx

//타입스크립트 문법으로 props의 타입을 정해준다.[props는 기본적으로 오브젝트여야함 {}]
function ImageBox(props: { src: string }) {
    return (
        &lt;div className=&quot;image-box&quot;&gt;
            {/* 이미지를 받을 수 있게 기능 구현 */}
            &lt;img src={props.src} /&gt;
        &lt;/div&gt;
    );
}

//해당 함수를 다른 곳에서 사용할 것이니까 export를 통해 내보내준다.
export default ImageBox;</code></pre>
<p>*이미지 렌더링</p>
<pre><code class="language-tsx">// App.tsx

import React, { useRef, useState } from &quot;react&quot;;
import &quot;./App.css&quot;;
import ImageBox from &quot;./components/ImageBox&quot;;

function App() {
  const inpRef = useRef&lt;HTMLInputElement&gt;(null);
  const [imageList, setImageList] = useState&lt;string[]&gt;([]);

  console.log(imageList);

  return (
    &lt;div className=&quot;container&quot;&gt;
      &lt;div className={&quot;gallery-box&quot; + (imageList.length &gt; 0 &amp;&amp; &quot;row&quot;)}&gt;
        {imageList.length === 0 &amp;&amp; (
          &lt;div className=&quot;text-center&quot;&gt;
            이미지가 없습니다. &lt;br /&gt;
            이미지를 추가해주세요.
          &lt;/div&gt;
        )}
        &lt;input
          type=&quot;file&quot;
          ref={inpRef}
          onChange={(event) =&gt; {
            if (event.currentTarget.files?.[0]) {
              const file = event.currentTarget.files[0];

              console.log(file.name);

              //파일을 이미지 url로 바꿔주기
              const reader = new FileReader();
              reader.readAsDataURL(file);
              reader.onloadend = (event) =&gt; {
                setImageList((prev) =&gt; [
                  ...prev,
                  event.target?.result as string,
                ]);
              };
            }
          }}
        /&gt;
        {imageList.map((el, idx) =&gt; (
          &lt;ImageBox key={el + idx} src={el} /&gt;
        ))}
        &lt;div
          className=&quot;puls-box mt&quot;
          onClick={() =&gt; {
            inpRef.current?.click();
          }}
        &gt;
          +
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<br/>

<h3 id="드래그-드롭-사용하여-ux-개선하기">드래그 드롭 사용하여 UX 개선하기</h3>
<p>react-dropzone 라이브러리를 사용하여 드래그 드롭 기능 구현</p>
<pre><code>$ yarn add react-dropzone
$ yarn add @types/react-dropzone</code></pre><br/>

<p>파일을 클릭해서 넣는 것이 아닌 폴더에서 드래그 앤 드롭으로 이미지 추가 가능하게 구현</p>
<pre><code class="language-tsx">// App.tsx

import React, { useCallback, useRef, useState } from &#39;react&#39;;
import { useDropzone } from &#39;react-dropzone&#39;;
import &#39;./App.css&#39;;
import ImageBox from &#39;./components/ImageBox&#39;;


function App() {
    const [imageList, setImageList] = useState&lt;string[]&gt;([])

    const onDrop = useCallback(acceptedFiles =&gt; {
        console.log(acceptedFiles)
        if (acceptedFiles.length) {


            for (const file of acceptedFiles) {
                const reader = new FileReader();

                reader.readAsDataURL(file)
                reader.onloadend = (event) =&gt; {
                    setImageList(prev =&gt; [...prev, event.target?.result as string])
                }
            }
        }
    }, [])

    const { getRootProps, getInputProps } = useDropzone({ onDrop })


    return (
        &lt;div className=&#39;container&#39;&gt;
            &lt;div className={&#39;gallery-box &#39; + (imageList.length &gt; 0 &amp;&amp; &#39;row&#39;)}&gt;
                {
                    imageList.length === 0 &amp;&amp;
                    &lt;div className=&#39;text-center&#39;&gt;
                        이미지가 없습니다.&lt;br /&gt;
                        이미지를 추가해주세요.
                    &lt;/div&gt;
                }
                {
                    imageList.map((el, idx) =&gt; &lt;ImageBox key={el + idx} src={el} /&gt;)
                }
                &lt;div className=&#39;plus-box&#39;
                    {...getRootProps()}
                &gt;
                    &lt;input
                        {...getInputProps()}
                    /&gt;
                    +
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}

export default App;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[BMI Calculator]]></title>
            <link>https://velog.io/@mylee-p/BMI-Calculator</link>
            <guid>https://velog.io/@mylee-p/BMI-Calculator</guid>
            <pubDate>Tue, 10 Jan 2023 06:25:26 GMT</pubDate>
            <description><![CDATA[<h2 id="bmi-calculator">BMI Calculator</h2>
<p><img src="https://user-images.githubusercontent.com/89143892/211474264-1c75f399-2f5d-4b5c-82f6-39d00730f092.jpg" alt="bmi"></p>
<p>-BMI : 체질량 지수 <br/> -사용자가 직접 input을 입력하기 때문에 어떤 값이 들어올지 알 수 없다.<br/> -해당 기능을 필터링 하는 로직 구현<br/>-ProgressBar를 사용하여 일반적으로 어느정도 수치인지 확인
-BMI는 소수점 아래 두자리까지 출력</p>
<h3 id="form-태그를-기준으로-html-css를-이용해서-웹페이지-구현">form 태그를 기준으로 HTML, CSS를 이용해서 웹페이지 구현</h3>
<p>form으로 input을 컨트롤해야 사용자 친화적으로 여러 사람의 웹 접근성이 좋아진다.<br/> -사용자 친화적으로 &quot;몸무게&quot;를 클릭해도 input에 커서가 활성화되게 구성</p>
<pre><code class="language-html">&lt;!-- index.html --&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        . .
        &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; media=&quot;screen&quot; href=&quot;main.css&quot; /&gt;
        &lt;script src=&quot;main.js&quot;&gt;&lt;/script&gt;
        &lt;title&gt;Calculator&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;form onsubmit=&quot;onSubmit(event)&quot;&gt;
            &lt;div class=&quot;row&quot;&gt;
                &lt;div class=&quot;form-inp&quot;&gt;
                    &lt;label for=&quot;w&quot;&gt; 몸무게 (kg) &lt;/label&gt;
                    &lt;!-- 몸무게를 클릭해도 input에 커서가 활성화되게 구성하기 --&gt;
                    &lt;!-- 텍스트들이 숫자만 입력되도록 input타입 수정 --&gt;
                    &lt;input
                        class=&quot;inp&quot;
                        type=&quot;number&quot;
                        placeholder=&quot;kg 단위&quot;
                        name=&quot;w&quot;
                        id=&quot;w&quot;
                    /&gt;
                &lt;/div&gt;
                &lt;div class=&quot;form-inp&quot;&gt;
                    &lt;label for=&quot;h&quot;&gt; 신장(m) &lt;/label&gt;
                    &lt;input
                        class=&quot;inp&quot;
                        type=&quot;number&quot;
                        placeholder=&quot;m 단위&quot;
                        name=&quot;h&quot;
                        id=&quot;h&quot;
                    /&gt;
                &lt;/div&gt;
                &lt;!-- 현재 결과 버튼을 누르면 바로 아래에 결과값이 표시 될 수 있도록 
        submit 이벤트를 캔슬한다.  --&gt;
                &lt;button type=&quot;submit&quot; class=&quot;btn&quot;&gt;결과&lt;/button&gt;
            &lt;/div&gt;
        &lt;/form&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="결과값으로-소수점-2자리-까지만-나오게-로직-구성">결과값으로 소수점 2자리 까지만 나오게 로직 구성</h3>
<pre><code class="language-js">//main.js

function onSubmit(event) {
    event.preventDefault();

    const w = parseFloat(event.target[0].value);
    const h = parseFloat(event.target[1].value);

    if (isNaN(w) || isNaN(h) || w &lt;= 0 || h &lt;= 0) {
        alert(&quot;적절한 값이 아닙니다.&quot;);
        return;
    }

    //toFixed(2)를 통해 소수점 2자리만 나오게 수정
    const bmi = w / (h * h);
    console.log(bmi.toFixed(2));

    const res = document.getElementById(&quot;res&quot;);

    // 여기서 블록으로 만들어주지만 reset 할 경우 다시 none이 되어야 하기 때문에
    // html form에서 onreset을 넣어준다.
    res.style.display = &quot;flex&quot;;

    //아이디 가져오기
    document.getElementById(&quot;bmi&quot;).innerText = bmi.toFixed(2);
    //프로그레스바 가져오기
    document.getElementById(&quot;meter&quot;).value = bmi;

    let state = &quot;정상&quot;;
    let common = true;
    //만약에 bmi가 18.5 미만 일 때는 state가 저체중
    if (bmi &lt; 18.5) {
        state = &quot;저체중&quot;;
        common = false;
    }
    //만약에 bmi가 25 이상 일 때는 state가 과체중
    if (bmi &gt; 25) {
        state = &quot;과체중&quot;;
        common = false;
    }
    const stateElement = document.getElementById(&quot;state&quot;);
    stateElement.innerText = state;
    //common이 true 즉, 정상이라면 초록색, 아니라면 빨간색
    //? 앞에 있는 값은 boolean값 즉, true / false 값이어야 한다.
    stateElement.style.color = common ? &quot;#29FF21&quot; : &quot;#FF3A3A&quot;;
}</code></pre>
<h3 id="progressbar와-결과값-화면에-출력">ProgressBar와 결과값 화면에 출력</h3>
<pre><code class="language-html">&lt;!-- index.html --&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;meta charset=&quot;UTF-8&quot; /&gt;
        &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot; /&gt;
        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
        &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; media=&quot;screen&quot; href=&quot;main.css&quot; /&gt;
        &lt;script src=&quot;main.js&quot;&gt;&lt;/script&gt;
        &lt;title&gt;Calculator&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;!-- reset을 한다면 document에서 res를 찾아 스타일의 디스플레이를 none으로 바꿔준다 --&gt;
        &lt;form
            onsubmit=&quot;onSubmit(event)&quot;
            onreset=&quot;document.getElementById(&#39;res&#39;).style.display = &#39;none&#39;&quot;
        &gt;
            &lt;div class=&quot;row&quot;&gt;
                &lt;div class=&quot;form-inp&quot;&gt;
                    &lt;label for=&quot;w&quot;&gt; 몸무게 (kg) &lt;/label&gt;

                    &lt;!-- 최소값 설정해주기 --&gt;
                    &lt;input
                        class=&quot;inp&quot;
                        type=&quot;number&quot;
                        placeholder=&quot;kg 단위&quot;
                        name=&quot;w&quot;
                        id=&quot;w&quot;
                        min=&quot;30&quot;
                    /&gt;
                &lt;/div&gt;
                &lt;div class=&quot;form-inp&quot;&gt;
                    &lt;label for=&quot;h&quot;&gt; 신장(m) &lt;/label&gt;

                    &lt;!-- input에 자연수가 아닌 소수점도 입력할 수 있게 스텝추가하기  --&gt;
                    &lt;input
                        class=&quot;inp&quot;
                        type=&quot;number&quot;
                        step=&quot;0.01&quot;
                        placeholder=&quot;m 단위&quot;
                        name=&quot;h&quot;
                        id=&quot;h&quot;
                        min=&quot;0.5&quot;
                    /&gt;
                &lt;/div&gt;
                &lt;button type=&quot;submit&quot; class=&quot;btn&quot;&gt;결과&lt;/button&gt;
            &lt;/div&gt;

            &lt;div id=&quot;res&quot;&gt;
                &lt;!-- ProgressBar --&gt;
                &lt;div&gt;
                    &lt;!-- 높낮이에 따라 적절하게 프로그레스바 안의 컬러가 변하길 원하기 때문에 progress가 아닌 meter 태그를 사용 --&gt;
                    &lt;meter
                        style=&quot;width: 100%;&quot;
                        min=&quot;10&quot;
                        max=&quot;30&quot;
                        optimum=&quot;22&quot;
                        low=&quot;18.5&quot;
                        high=&quot;25&quot;
                        id=&quot;meter&quot;
                    &gt;&lt;/meter&gt;
                &lt;/div&gt;

                &lt;!-- BMI 결과값 --&gt;
                &lt;div class=&quot;res-text&quot;&gt;
                    &lt;div&gt;당신의 BMI는 &lt;span id=&quot;bmi&quot;&gt;&lt;/span&gt;입니다.&lt;/div&gt;
                    &lt;div&gt;&lt;span id=&quot;state&quot;&gt;&lt;/span&gt;입니다.&lt;/div&gt;
                &lt;/div&gt;

                &lt;!-- 초기화 버튼 --&gt;
                &lt;!-- 현재 display가 flex상태이기 때문에 버튼이 가운데로 오질 않는데 inline-flex로 바꿔주면 된다. --&gt;
                &lt;div style=&quot;text-align: center;&quot;&gt;
                    &lt;!-- form으로 감쌌기 때문에 reset 버튼을 통해 초기화 할 수 있다. --&gt;
                    &lt;button type=&quot;reset&quot; class=&quot;btn&quot;&gt;초기화&lt;/button&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/form&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<pre><code class="language-css">/* main.css */

.btn {
    width: 148px;
    height: 48px;
    background-color: #2699fb;
    color: #fff;
    border-radius: 4px;
    outline: none;
    border: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    font-weight: bold;
}

.btn:active {
    background-color: #007feb;
    outline: auto 2px #0055a0;
}

.inp {
    color: #48aeff;
    box-sizing: border-box;
    width: 204px;
    height: 48px;
    padding: 0 20px;
    background-color: white;
    border: solid 1px #48aeff;
    font-size: 14px;
}

.inp:active {
    outline: auto 2px #0055a0;
}

.inp::placeholder {
    color: #48aeff;
}

.row {
    display: flex;
    flex-direction: row;
    gap: 16px;
    /* 아래에 내려와서 붙어야 하기 때문에 flex-end를 준다 */
    align-items: flex-end;
}

/* inline 레벨인 label을 다시 block 레벨로 바꿔준다. */
.form-inp {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* label 스타일 적용 */
.form-inp label {
    font-size: 12px;
    color: #48aeff;
}

/* 처음에는 숨겨져 있어야 한다. 결과를 눌러야 그 때 출력.*/
#res {
    margin-top: 24px;
    /* display: none; */
    width: 588px;
    display: flex;
    flex-direction: column;
    gap: 24px;
}

.res-text {
    color: #48aeff;
    text-align: center;
}

/* 자식선택자를 이용해서 각각 글씨크기 조정 */
.res-text &gt; div:nth-child(1) {
    font-size: 32px;
}
.res-text &gt; div:nth-child(2) {
    font-size: 24px;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Calculator [계산기 개발]]]></title>
            <link>https://velog.io/@mylee-p/Calculator-%EA%B3%84%EC%82%B0%EA%B8%B0-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@mylee-p/Calculator-%EA%B3%84%EC%82%B0%EA%B8%B0-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Tue, 10 Jan 2023 05:45:52 GMT</pubDate>
            <description><![CDATA[<h2 id="calculator-계산기-개발">Calculator [계산기 개발]</h2>
<p>HTML, CSS, Javascript 만으로 단순하게 개발하기</p>
<img width="248" alt="calculator" src="https://user-images.githubusercontent.com/89143892/211467882-38ead60c-8f61-4601-8072-9f75df5bade4.png">

<br />

<h3 id="두가지-숫자를-받아서-연산자를-활용해-계산하여-값이-나올-수-있도록-구현">두가지 숫자를 받아서 연산자를 활용해 계산하여 값이 나올 수 있도록 구현</h3>
<ol>
<li>아무것도 입력되지 않은 input</li>
<li>결과값이 입력된 input</li>
<li>덧셈 등 연산을 진행하면 + 등 연산자가 생기는 input</li>
<li>숫자를 입력하면 숫자가 바로 입력되는 input</li>
<li>뒤에 입력한 숫자가 음수일 경우 +연산자가 -연산자로 바뀌지 않고 음수가 입력되는 input</li>
<li>*를 하다가 뒤에 음수가 올 수 있기 때문에 5번과 같은 input</li>
<li>=를 입력하면 바로 연산된 값이 보이는 input</li>
<li>연산된 값이 있는 상태에서 Enter를 입력하면 연산수식없이 결과값만 남는 상태의 input</li>
<li>=을 입력한 생태에서 Enter가 아닌 다른 값을 입력하면 리셋 시켜서 해당 값을 넣은 상태로 <br/>출력되는 input</li>
</ol>
<br/>

<h3 id="간단한-html-구조만들기">간단한 HTML 구조만들기</h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        . .
        &lt;script src=&quot;main.js&quot;&gt;&lt;/script&gt;
        &lt;title&gt;Calculator&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;div class=&quot;row&quot;&gt;
            &lt;!-- input을 컨트롤 해야하기 때문에 id를 만든다. --&gt;
            &lt;input class=&quot;inp&quot; disabled id=&quot;top-inp&quot; /&gt;
        &lt;/div&gt;
        &lt;div class=&quot;row&quot;&gt;
            &lt;!-- inputNum 함수를 onClick 이벤트에 넣어서 적용시킨다. --&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(7)&quot;&gt;7&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(8)&quot;&gt;8&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(9)&quot;&gt;9&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputEqu(&#39;=&#39;)&quot;&gt;=&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;row&quot;&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(4)&quot;&gt;4&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(5)&quot;&gt;5&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(6)&quot;&gt;6&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputOper(&#39;+&#39;)&quot;&gt;+&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;row&quot;&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(1)&quot;&gt;1&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(2)&quot;&gt;2&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputNum(3)&quot;&gt;3&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputOper(&#39;-&#39;)&quot;&gt;-&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;row&quot;&gt;
            &lt;div class=&quot;btn btn-lg&quot; onClick=&quot;inputNum(0)&quot;&gt;0&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputOper(&#39;/&#39;)&quot;&gt;/&lt;/div&gt;
            &lt;div class=&quot;btn&quot; onClick=&quot;inputOper(&#39;*&#39;)&quot;&gt;*&lt;/div&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<br />

<h3 id="계산기-로직-구현하기">계산기 로직 구현하기</h3>
<p>글로벌 변수를 만들어서 ture일 경우에는 = 이 눌렸다고 설정해준다 <br /> *원래는 null sefety하게 프로그램을 짜는 것이 좋으나 단순한 로직이기 때문에 이렇게 진행.</p>
<pre><code class="language-js">let left = null,
    right = null,
    oper = null,
    res = flase;

//left, right, oper가 존재할 경우 input에 적용시켜주는 함수
function save() {
    const inp = document.getElementById(&quot;top-inp&quot;);
    let value = &quot;&quot;;
    if (left === null) return;
    //left가 존재할 경우에는 value에다가 left를 더해주고 띄어쓰기를 추가해준다.
    value += left + &quot; &quot;;
    //결과값을 input에 적어준다
    inp.value = value;

    //연산자가 없을 경우에는 리턴을 해주고 value에 oper를 더해주고 띄어쓰기를 해준다.
    if (oper === null) return;
    value += oper + &quot; &quot;;
    inp.value = value;

    if (right === null) return;
    value += right + &quot; &quot;;
    inp.value = value;

    //만약에 res가 true라면 = 을 눌렀다는 뜻이고
    //= 을 눌렀으니까 value에다가 =을 붙여준다.
    if (res) {
        //결과값을 담아줄 res를 선언하고
        let res = &quot;&quot;;
        //= 뒤에다가 연산결과를 만들어 붙여준다
        //switch로 oper를 돌려주고
        //case로 +일 경우에는 left 와 right를 더하는 형식으로 로직을 구성한다.
        switch (oper) {
            case &quot;+&quot;:
                res = parseInt(left) + parseInt(right);
                break;
            case &quot;-&quot;:
                res = parseInt(left) - parseInt(right);
                break;
            case &quot;*&quot;:
                res = parseInt(left) * parseInt(right);
                break;
            case &quot;/&quot;:
                res = parseInt(left) / parseInt(right);
                break;
        }
        value += &quot;=&quot; + res;
        inp.value = value;
    }
}</code></pre>
<br />

<p>input버튼을 클릭했을 때 이벤트를 받기 위해서 숫자를 넣는 함수를 만들어준다.</p>
<pre><code class="language-js">function inputNum(num) {
    //oper가 null이면 즉, 연산자가 null일 경우에는 왼쪽이 아직 다 끝나지 않았다면
    if (oper === null) {
        //그리고 left가 null일 경우 즉, 한번도 input이 되지 않았을 경우
        if (left === null) {
            //left가 null일 경우에는 num을 문자로 바꿔서 넣어준다.
            left = `${num}`;
            //null이 아닐 경우에는 left에 문자열을 더해준다.
        } else {
            //0이 여러번 올 수 없기 때문에 가드를 해준다.
            //num도 0이고 left도 0일 때는 아무것도 더하지 않고 그냥 넘어간다.
            if (num === 0 &amp;&amp; parseInt(left) === 0) return;
            left += `${num}`;
        }
        console.log(&quot;flag1&quot;, left);
    }
    //oper가 null이 아닐 경우, number가 들어왔으면 right에다가 적어준다.
    else {
        //연산자가 입력되지 않았는데 숫자가 들어왔다면 계산기 오른쪽에 텍스트를 적어주고
        //right가 null 경우 즉, 입력이 된 적이 없으면 바로 right에 넣어주고
        if (right === null) {
            right = `${num}`;
        }
        //그게 아니면 더해준다.
        else {
            //마찬가지로 둘 다 0 일 경우 return으로 아래의 명령문을 실행하지 않고 패스
            if (num === 0 &amp;&amp; parseInt(right) === 0) return;
            right += `${num}`;
        }
    }
    //input을 한다음에 바로 save를 호출해서 버튼을 클릭하면 바로바로 input에
    //적절하게 대응될 수 있게 해준다.
    save();
}</code></pre>
<br />

<p>연산자들을 클릭 했을 때 작동하도록 로직 구현</p>
<pre><code class="language-js">.
.
function inputOper(op) {
  //연산자 같은 경우에는 left가 null이고 연산자가 -인 경우에는 input이 안되도록해준다.
  if (left === null &amp;&amp; op === &quot;-&quot;) {
    //left가 null일 경우 음수로 사용하겠다.
    left = &quot;-&quot;
    save()
    return;
  }
  //만약에 left가 null은 아닌데 그냥 -만 있다면 연산자를 쓸 수 없게
  if(left === &quot;-&quot; &amp;&amp; op === &quot;-&quot;) {
    save()
    return;
  }
  //만약에 연산자가 -인데 기존의 값이 존재한고 right가 null이라면
  if(op === &quot;-&quot; &amp;&amp; oper !== null &amp;&amp; right === null) {
    //-를 넣어주고 바로 save 해준다.
    right = &quot;-&quot;
    save()
    return;
  }
  //그게 아니라면 아래 명령문을 실행한다.
  //더해지는게 아니라 = 이기 때문에 언제든지 대체될 수 있다.
  oper = op;
  save()
}</code></pre>
<br>

<p>= 으로 바로 연산되도록 로직 구현</p>
<pre><code class="language-js">.
.
function inputEqu() {
  //만약 기존에 이미 한번 = 으로 계산이 되었고
  if(res) {
    //계산된 상태에서 = 를 한번 더 눌렀다면 left에 resValue을 넣고
    left = resValue
    //초기화를 한 번 해준다.
    right = null
    resValue = null
    oper = null
    res = false
    //만약에 res가 기존에 false 였다면 ture로만 바꿔준다.
  } else {
      res = true
  }
  save()
}</code></pre>
<br />

<p>inputEqu()보강하기</p>
<p>-left, right, oper가 없다면 res에 변화가 가지않도록 블로킹하여 보강하기 <br /> -계산을 할 때 = 을 눌러야만 결과값을 나타내록 보강하기</p>
<pre><code class="language-js">function inputEqu() {
    //left가 null이거나 right가 null이거나 oper가 null일 때
    //return을 통해서 res가 true가 되는 경우를 막는다.
    if (left === null || right === null || !oper) return;

    if (res) {
        left = resValue;
        right = null;
        resValue = null;
        oper = null;
        res = false;
    } else {
        res = true;
    }
    save();
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Date Picker [달력 개발]]]></title>
            <link>https://velog.io/@mylee-p/Date-Picker-%EB%8B%AC%EB%A0%A5-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@mylee-p/Date-Picker-%EB%8B%AC%EB%A0%A5-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Mon, 09 Jan 2023 03:06:57 GMT</pubDate>
            <description><![CDATA[<h2 id="date-picker">Date Picker</h2>
<p>-input box를 클릭하면 달력이 토글로 표시
-현재 날짜를 보라색으로 자동 마크
-선택한 날짜는 주황색으로 마크
-선택한 날짜는 input box에 자동 입력
-토요일은 파란색, 일요일은 빨간색으로 동적으로 표시
-양 옆의 버튼을 통해 이전달, 다음달의 정보를 가져온다.
-선택한 날짜는 월을 넘겼다가 돌아와도 그대로 선택되어 있음(정보저장)
-선택한 날짜는 달력 토글창을 닫았다가 열어도 그대로 선택되어 있음(정보저장)</p>
<img width="1000" alt="datepicker" src="https://user-images.githubusercontent.com/89143892/211232166-cb5b971a-50a8-4d1a-87fe-6ed9d22f1c2e.png">
<br />

<h3 id="개발환경설정---snowpack">개발환경설정 - snowpack</h3>
<pre><code>$ npm init -y
$ npm i -D snowpack
$ npm i -D @snowpack/plugin-sass</code></pre><pre><code class="language-js">//snowpack.config.js

module.exports = {
  mount: {
    public: { url: &#39;/&#39;, static: true }, // &#39;/&#39; : 최상위
    src: { url: &#39;/dist&#39; },
  },
  optimize: {
    minify: true,
  },
  plugins: [
    &#39;@snowpack/plugin-sass&#39;, //설치한 플러그인을 string으로 넣어준다.
  ],
};</code></pre>
<pre><code>$ npm run start

$ npm run build</code></pre><pre><code>$ npm i -D eslint
$ npm i --save-exact prettier
$ npm i -D eslint-config-prettier eslint-plugin-prettier</code></pre><pre><code class="language-json">//.eslintrc.json
{
  &quot;env&quot;: {
    &quot;browser&quot;: true,
    &quot;es2021&quot;: true
  },
  &quot;extends&quot;: [&quot;eslint:recommended&quot;, &quot;plugin:prettier/recommended&quot;],
  &quot;parserOptions&quot;: {
    &quot;ecmaVersion&quot;: 13,
    &quot;sourceType&quot;: &quot;module&quot;
  },
  &quot;rules&quot;: {
    &quot;prettier/prettier&quot;: &quot;error&quot;
  }
}</code></pre>
<pre><code class="language-json">//.eslintignore

/node_modules
/build
snowpack.config.js</code></pre>
<pre><code class="language-json">//.prettierrc.json

{
  &quot;trailingComma&quot;: &quot;all&quot;,
  &quot;bracketSpacing&quot;: true,
  &quot;useTabs&quot;: false,
  &quot;semi&quot;: true,
  &quot;printWidth&quot;: 80,
  &quot;arrowParens&quot;: &quot;avoid&quot;,
  &quot;proseWrap&quot;: &quot;never&quot;,
  &quot;endOfLine&quot;: &quot;auto&quot;,
  &quot;tabWidth&quot;: 2,
  &quot;singleQuote&quot;: true
}</code></pre>
<pre><code class="language-json">//.prettierignore

/node_modules
/build
snowpack.config.js</code></pre>
<pre><code class="language-json">//Open workspace Settings(JSON)

{
  &quot;editor.formatOnSave&quot;: true,
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.fixAll.eslint&quot;: true
  }
}</code></pre>
<h3 id="날짜-조회-기능-개발">날짜 조회 기능 개발</h3>
<blockquote>
<p>클래스 변수로 표시해줄 달에 대한 정보들을 set하는 변수,<br /> string array로 월별의 영문표기를 가져왔다.</p>
</blockquote>
<pre><code class="language-js">class DatePicker {
  monthData = [
    &quot;January&quot;,
    &quot;February&quot;,
    &quot;March&quot;,
    &quot;April&quot;,
    &quot;May&quot;,
    &quot;June&quot;,
    &quot;July&quot;,
    &quot;August&quot;,
    &quot;September&quot;,
    &quot;October&quot;,
    &quot;November&quot;,
    &quot;December&quot;,
  ];</code></pre>
<blockquote>
<p>private 변수 생성<br /> 캘린더 안에서 관리하는 변수, 달력이 보이는데 사용될 날짜들에 대한 정보</p>
</blockquote>
<pre><code class="language-js">
  #calendarDate = {
    data: &quot;&quot;, //js에 new Date()객체를 사용해서 관련 정보를 저장.
    date: 0, //날짜
    month: 0,
    year: 0,
  };

  selectedDate = {
    data: &quot;&quot;,
    date: 0,
    month: 0,
    year: 0,
  };</code></pre>
<blockquote>
<p>각 기능을 하는 엘리먼트를 사용하고 있다는 것을 바로 알 수 있게 필드에 추가해준다.</p>
</blockquote>
<pre><code class="language-js">  datePickerEl;
  dateInputEl;
  calendarMonthEl;
  montnContentEl;
  nextBtnEl;
  prevBtnEl;
  calendarDatesEl;

  //입력 순서대로 메소드를 constructor에 넣어준다.
  constructor() {
    this.initCalendarDate();
    this.assignElement();
    this.addEvent();
  }</code></pre>
<blockquote>
<p>달력의 날짜 정보들을 초기화하는 메소드 생성</p>
</blockquote>
<pre><code class="language-js">  initCalendarDate() {
    const data = new Date(); //따로 파라미터를 넣지 않으면 현재를 기준으로 날짜 정보가 생성된다.
    const date = data.getDate(); //몇일인지 정보를 가져오고
    const month = data.getMonth(); //이번 달이 몇 월인지 정보를 가져오는데 0부터 시작되기때문에 0~11로 리턴이 된다.
    const year = data.getFullYear(); //현재 날짜의 연도 정보를 가져온다.
    this.#calendarDate = {
      data,
      date,
      month,
      year,
    };
  }</code></pre>
<blockquote>
<p>엘리먼트 탐색 메소드</p>
</blockquote>
<pre><code class="language-js">  //엘리먼트 탐색 메소드
  assignElement() {
    this.datePickerEl = document.getElementById(&quot;date-picker&quot;);
    this.dateInputEl = this.datePickerEl.querySelector(&quot;#date-input&quot;); //id를 탐색할 때는 #필수
    this.calendarEl = this.datePickerEl.querySelector(&quot;#calendar&quot;);
    this.calendarMonthEl = this.calendarEl.querySelector(&quot;#month&quot;);
    //지금의 연/월/일을 표시해주는 엘리먼트
    this.monthContentEl = this.calendarMonthEl.querySelector(&quot;#content&quot;);
    //next, prev 버튼 엘리먼트
    this.nextBtnEl = this.calendarMonthEl.querySelector(&quot;#next&quot;);
    this.prevBtnEl = this.calendarMonthEl.querySelector(&quot;#prev&quot;);
    //day[월화수목금토일]은 SCC영역에서 처리했기 때문에 따로 하지 않고 dates엘리먼트를 불러온다.
    this.calendarDatesEl = this.calendarEl.querySelector(&quot;#dates&quot;);
  }</code></pre>
<blockquote>
<p>이벤트 메소드</p>
</blockquote>
<pre><code class="language-js">  addEvent() {
    //상단의 날짜를 눌렀을 때 calendar에 active가 추가되어 달력을 보여주는 메소드
    this.dateInputEl.addEventListener(&quot;click&quot;, this.toggleCalendar.bind(this));
    //next, prev 버튼을 눌러서 다음달, 이전달을 탐색하는 메소드
    this.nextBtnEl.addEventListener(&quot;click&quot;, this.moveNextMonth.bind(this));
    this.prevBtnEl.addEventListener(&quot;click&quot;, this.movePrevtMonth.bind(this));
  }</code></pre>
<blockquote>
<p>동적으로 해당 달의 날짜를 생성해주는 메소드</p>
</blockquote>
<pre><code class="language-js">  moveNextMonth() {
    this.#calendarDate.month++;
    if (this.#calendarDate.month &gt; 11) {
      this.#calendarDate.month = 0;
      this.#calendarDate.year++;
    }
    this.updateMonth();
    this.updateDates();
  }

  movePrevtMonth() {
    this.#calendarDate.month--;
    if (this.#calendarDate.month &lt; 0) {
      this.#calendarDate.month = 11;
      this.#calendarDate.year--;
    }
    this.updateMonth();
    this.updateDates();
  }</code></pre>
<blockquote>
<p>토글로 연도와 월의 영어표기가 노출되는 메소드</p>
</blockquote>
<pre><code class="language-js">toggleCalendar() {
  this.calendarEl.classList.toggle(&quot;active&quot;);
  this.updateMonth();
  this.updateDates();
}</code></pre>
<blockquote>
<p>월별 업데이트</p>
</blockquote>
<pre><code class="language-js">updateMonth() {
  this.monthContentEl.textContent = `${this.#calendarDate.year} ${
    this.monthData[this.#calendarDate.month]
  }`;
}</code></pre>
<blockquote>
<p>업데이트 될 때 마다 새로 동적으로 날짜를 생성하는 메소드</p>
</blockquote>
<pre><code class="language-js">  updateDates() {
    this.calendarDatesEl.innerHTML = &quot;&quot;;
    const numberOfDates = new Date(
      this.#calendarDate.year,
      this.#calendarDate.month + 1,
      0
    ).getDate();
    const fragment = new DocumentFragment();
    for (let i = 0; i &lt; numberOfDates; i++) {
      const dateEl = document.createElement(&quot;div&quot;);
      dateEl.classList.add(&quot;date&quot;);
      dateEl.textContent = i + 1;
      dateEl.dataset.date = i + 1;
      fragment.appendChild(dateEl);
    }
    //그 달의 첫번째 날 즉, 1일이 어느요일에서 시작하는지를 알 수 있는 로직
    fragment.firstChild.style.gridColumnStart =
      new Date(this.#calendarDate.year, this.#calendarDate.month, 1).getDay() +
      1;
    this.calendarDatesEl.appendChild(fragment);

    //토요일과 일요일인 날들을 찾아서 파란색과 빨간색을 표시해주는 메소드 구현
    this.colorSaturday();
    this.colorSunday();

    //업데이트가 되었을 때 오늘 날짜에 마크를 해주는 메소드 구현
    this.markToday();
  }</code></pre>
<blockquote>
<p>업데이트가 되었을 때 오늘 날짜에 마크를 해주는 메소드</p>
</blockquote>
<pre><code class="language-js">  markToday() {
    const currentDate = new Date();
    const currentMonth = currentDate.getMonth(); //0부터 시작
    const currentYear = currentDate.getFullYear(); //이번년도가 몇년인지
    const today = currentDate.getDate(); //오늘이 몇 일인지

    if (
      currentYear === this.#calendarDate.year &amp;&amp;
      currentMonth === this.#calendarDate.month
    ) {
      //calendarDatesEl에서 data-date 속성 탐색을 해서 today를 찾고
      //classList에 today를 넣어준다.
      this.calendarDatesEl
        .querySelector(`[data-date=&#39;${today}&#39;]`)
        .classList.add(&quot;today&quot;);
    }
  }</code></pre>
<blockquote>
<p>토요일에 해당하는 날짜는 모두 파란색으로 표시해주는 메소드</p>
</blockquote>
<pre><code class="language-js">  colorSaturday() {
    const saturdayEls = this.calendarDatesEl.querySelectorAll(
      `.date:nth-child(7n+${
        7 -
        //해당 달에 1일이 무슨 요일인지 알 수 있다.
        new Date(this.#calendarDate.year, this.#calendarDate.month, 1).getDay() //getDay()는 0부터 시작된다.
      })`
    );
    //7n+1의 의미는 n이 0부터 시작하게 되니까
    //1, 8, 15, 22, 29 번째를 찾아서 파란색으로 표시하겠다는 의미
    for (let i = 0; i &lt; saturdayEls.length; i++) {
      saturdayEls[i].style.color = &quot;blue&quot;;
    }
  }</code></pre>
<blockquote>
<p>일요일에 해당하는 날짜는 모두 빨간색으로 표시해주는 메소드</p>
</blockquote>
<pre><code class="language-js">  colorSunday() {
    const sundayEls = this.calendarDatesEl.querySelectorAll(
      //전체를 감싼 다음에 7로 나눠준 것이고,
      `.date:nth-child(7n+${
        (8 -
          new Date(
            this.#calendarDate.year,
            this.#calendarDate.month,
            1
            //나누기 7을해서 나머지를 구한 값을 계산식에 넣어주었다.
          ).getDay()) %
        7
      })`
    );

    //for문을 이용해서 일요일에 해당하는 일들을 빨간색으로 표시해준다.
    for (let i = 0; i &lt; sundayEls.length; i++) {
      sundayEls[i].style.color = &quot;red&quot;;
    }
  }
}</code></pre>
<blockquote>
<p>클래스 생성 시 인스턴스를 생성해야 정상적으로 실행된다.</p>
</blockquote>
<pre><code class="language-js">new DatePicker();</code></pre>
<h3 id="날짜-입력-기능-개발">날짜 입력 기능 개발</h3>
<blockquote>
<p>constructor에 오늘 날짜를 기준으로 date-input에 동적으로 입력되는 메소드를 넣어준다.</p>
</blockquote>
<pre><code class="language-js">constructor() {
    .
    .
    this.setDateInput();
    this.addEvent();
  }</code></pre>
<blockquote>
<p>new Date 객체에서 생성한 날짜 정보를 input에 넣어준다</p>
</blockquote>
<pre><code class="language-js">  initSelectedDate() {
    this.selectedDate = { ...this.#calendarDate };
  }

  setDateInput() {
    //자바스크립트로 동적으로 스트립트를 넣어준다.
    this.dateInputEl.textContent = this.formateDate(this.selectedDate.data);

    this.dateInputEl.dataset.value = this.selectedDate.data;
  }</code></pre>
<blockquote>
<p>원하는 날짜를 클릭하면 date-input에 해당 날짜가 자동으로 입력되는 메소드 <br /> 이벤트 버블링 효과</p>
</blockquote>
<pre><code class="language-js">  addEvent() {
    .
    .
    .
    //calendarDatesEl는 date들의 컨테이너 역할을 하고 이벤트 버블링을 이용해서
    //모든 셀마다 이벤트를 추가하는 것이 아니라 전체를 감싸고 있는 컨테이너에
    //이벤트를 추가해서 그 안에서 어떤 셀을 클릭하는지 캐치할 수 있도록한다.
    this.calendarDatesEl.addEventListener(
      &quot;click&quot;,
      this.onClickSelectorDate.bind(this)
    );
  }</code></pre>
<blockquote>
<p>이벤를 받아서 eventTarget에 dataset에 date가 있으면 조건문을 실행<br /> 옵셔널체이닝</p>
</blockquote>
<pre><code class="language-js">onClickSelectorDate(event) {
  const eventTarget = event.target;
  if (eventTarget.dataset.date) {
    this.calendarDatesEl
      .querySelector(&quot;.selected&quot;)
      ?.classList.remove(&quot;selected&quot;);
    eventTarget.classList.add(&quot;selected&quot;);
    this.selectedDate = {
      data: new Date(
        this.#calendarDate.year,
        this.#calendarDate.month,
        eventTarget.dataset.date
      ),
      //지금 달력으로 보고 있는 날짜의 년 정보
      year: this.#calendarDate.year,
      //지금 달력으로 보고 있는 날짜의 월 정보
      month: this.#calendarDate.month,
      //클릭한 이벤트 타겟의 데이터 정보
      date: eventTarget.dataset.date,
    };
    this.setDateInput();
    //원하는 날짜를 선택하면 캘린더가 안보이게 설정
    this.calendarEl.classList.remove(&quot;active&quot;);
  }
}</code></pre>
<blockquote>
<p>포맷데이터 메소드를 구현해서 날짜 정보를 보여주려는 날짜 형태로 포맷시키는 메소드</p>
</blockquote>
<pre><code class="language-js">  //
  formateDate(dateData) {
    //날짜 정보[dateData]를 가져오는데 date가 10보다 작으면
    //즉, 두자리 수가 아니라면 그냥 0을 붙여준다.
    let date = dateData.getDate();
    if (date &lt; 10) {
      date = `0${date}`;
    }

    //month도 비슷하게 해주는데 index이기 때문에 +를 해서
    //실제 우리가 아는 달의 정보로 바꿔준다.
    let month = dateData.getMonth() + 1;
    if (month &lt; 10) {
      month = `0${month}`;
    }

    let year = dateData.getFullYear();
    return `${year}/${month}/${date}`;
  }</code></pre>
<blockquote>
<p>input 창을 닫은 후 다시 열었을 때 선택한 연/월/일이 있는 캘린더가 보여지게 하는 메소드</p>
</blockquote>
<pre><code class="language-js">  toggleCalendar() {
    if (this.calendarEl.classList.contains(&quot;active&quot;)) {
      //다시 토글을 하게 될때 선택한 날짜의 달력으로 볼 수 있게 된다.
      this.#calendarDate = { ...this.selectedDate };
    }
    this.calendarEl.classList.toggle(&quot;active&quot;);
    this.updateMonth();
    this.updateDates();
  }</code></pre>
<blockquote>
<p>오늘 선택된 날짜를 마크하는 메소드 구현</p>
</blockquote>
<pre><code class="language-js">  markSelectedDate() {
    if (
      //달력에서 보여지는 날짜정보들과 선택된 날짜정보들을 비교해서 조건문을 실행
      this.selectedDate.year === this.#calendarDate.year &amp;&amp;
      this.selectedDate.month === this.#calendarDate.month
    ) {
      this.calendarDatesEl
        .querySelector(`[data-date=&#39;${this.selectedDate.date}&#39;]`)
        .classList.add(&quot;selected&quot;);
    }
  }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 슬라이더 개발]]></title>
            <link>https://velog.io/@mylee-p/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%8D%94-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@mylee-p/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%8D%94-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Sat, 07 Jan 2023 08:41:32 GMT</pubDate>
            <description><![CDATA[<h2 id="이미지-슬라이더">이미지 슬라이더</h2>
<img width="1000" alt="Image" src="https://user-images.githubusercontent.com/89143892/211139891-f44f1721-d49f-459f-a296-19bca594e406.png">

<br />

<h3 id="개발환경설정">개발환경설정</h3>
<blockquote>
<p>Webpack-boilerplate</p>
</blockquote>
<ul>
<li>[font awesome] 아이콘을 사용하기 위해 라이브러리 설치</li>
</ul>
<pre><code>npm install --save @fortawesome/fontawesome-free</code></pre><blockquote>
<p>슬라이드 이미지 관련 설정</p>
</blockquote>
<pre><code class="language-js">//webpack.config.js

module: {
  rules: [
    {
      test: /\.css$/,
      use: [MiniCssExtractPlugin.loader, &quot;css-loader&quot;],
    },
    {
      test: /\.jpeg$/, //jpeg 파일을
      type: &#39;asset/inline&#39; //webpack에 내장된 로더로 읽어들이겠다.
    }
  ],
},</code></pre>
<p>Lodash 문법을 사용해서 입력해준다. <br /> Node.js에서는 require 메서드를 통해 외부 모듈을 가져올 수 있다.</p>
<pre><code class="language-html">&lt;!-- index.html --&gt;

&lt;div class=&quot;slider-wrap&quot; id=&quot;slider-wrap&quot;&gt;
  &lt;ul class=&quot;list slider&quot; id=&quot;slider&quot;&gt;
    &lt;li&gt;
      &lt;img src=&quot;&lt;%= require(&#39;./src/image/red.jpeg&#39;) %&gt;&quot; /&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;img src=&quot;&lt;%= require(&#39;./src/image/orange.jpeg&#39;) %&gt;&quot; /&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;img src=&quot;&lt;%= require(&#39;./src/image/yellow.jpeg&#39;) %&gt;&quot; /&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;img src=&quot;&lt;%= require(&#39;./src/image/green.jpeg&#39;) %&gt;&quot; /&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;img src=&quot;&lt;%= require(&#39;./src/image/blue.jpeg&#39;) %&gt;&quot; /&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;img src=&quot;&lt;%= require(&#39;./src/image/indigo.jpeg&#39;) %&gt;&quot; /&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;img src=&quot;&lt;%= require(&#39;./src/image/violet.jpeg&#39;) %&gt;&quot; /&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;</code></pre>
<h3 id="next-prev-버튼-개발">next, prev 버튼 개발</h3>
<pre><code class="language-js">//imageSlider.js

export default class ImageSlider {
  // 클래스 변수 일부만 private 하게 사용
  #currentPosition = 0; // 현재에 몇 번째 슬라이더에 있는지를 저장

  #slideNumber = 0; // 슬라이드의 개수를 저장

  #slideWidth = 0; // 슬라이드의 너비를 저장

  sliderWrapEl;

  sliderListEl;

  nextBtnEl;

  previousBtnEl;

  // 초기화해준 값을 constructor에 넣어서
  // 클래스에서 인스턴스를 만들 때 실행시켜주도록 함.
  constructor() {
    this.assignElement();
    this.initSliderNumber();
    this.initSlideWidth();
    this.initSliderListWidth();
    this.addEvent();
  }

  // 엘레먼트들을 찾아서 가지고 있는 assignElement()
  assignElement() {
    this.sliderWrapEl = document.getElementById(&#39;slider-wrap&#39;);
    this.sliderListEl = this.sliderWrapEl.querySelector(&#39;#slider&#39;);
    this.nextBtnEl = this.sliderWrapEl.querySelector(&#39;#next&#39;);
    this.previousBtnEl = this.sliderWrapEl.querySelector(&#39;#previous&#39;);
  }

  // 초기화 메소드

  // 슬라이드 리스트들이 몇개 있는지 알려주는 메소드
  initSliderNumber() {
    // length를 이용하여 querySelectorAll이 몇 개를 탐색했는지 알 수 있다.
    this.#slideNumber = this.sliderListEl.querySelectorAll(&#39;li&#39;).length;
  }

  // slideWidth를 초기화하는 메소드
  initSlideWidth() {
    // slideWidth는 sliderWrapEl에 css 1000px을 가지고 오는 코드
    this.#slideWidth = this.sliderWrapEl.clientWidth;
  }

  // 현재 slideList는 float을 써서 List가 옆으로 쭉 이어져 있는 형태
  // 자바스크립트를 이용해 동적으로 리스트를 개수에 따라서 리스트를 설정해주는 로직
  // sliderListWidth를 초기화 하는 메소드
  initSliderListWidth() {
    // 개수 * 높이를 해서 SliderList의 총 Width를 계산해서 다시 sliderListEl에 넣었다.
    // 그러면 css에서 8000px을 넣은 것과 같은 효과가 나타나게 된다.
    this.sliderListEl.style.width = `${this.#slideNumber * this.#slideWidth}px`;
  }

  // 이벤트 추가
  addEvent() {
    // 클릭하면 두번째 파라미터로 넣은 이벤트 핸들러를 실행하는데
    // moveToRight라는 메소드를 따로 만들어서 메소드를 실행하도록 했다.
    // moveToRight 메소드에서 this를 사용할 것이기 때문에 bind(this)를 해준다.
    this.nextBtnEl.addEventListener(&#39;click&#39;, this.moveToRight.bind(this));
    this.previousBtnEl.addEventListener(&#39;click&#39;, this.moveToLeft.bind(this));
  }

  moveToRight() {
    // 현재의 위치값을 1 더해주는 로직
    // eslint-config-airbnb에서는 &#39;++&#39;는 사용하지 못하게 되어있다.
    this.#currentPosition += 1;

    // 경계값 처리해주기
    // 현재 슬라이드가 0~6까지 있는데
    // 0에서 -1로 내려갈때나 6에서 7로 올라갈때를 처리해서 슬라이드가 끊김없이 이어지게.
    // 동적으로 만들어줘야하니까 this.sliedNumber를 이용해서
    // 현재 위치가 슬라이드의 개수와 같아지면 this.#currentPosition = 0; 이렇게 처리해주기
    if (this.#currentPosition === this.#slideNumber) {
      this.#currentPosition = 0;
    }

    // 슬라이드를 넘기는 효과를 준다 -1000, -2000 이런식으로
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;
  }

  moveToLeft() {
    // 현재의 위치값에서 1씩 빼주는 로직
    // currentPosition만 바꿔주면 left에 -값 주는 것을 똑같다.
    this.#currentPosition -= 1;

    // currentPosition이 -1이 되면 currentPosition를 this.#slideNumber - 1; 처리해주기
    if (this.#currentPosition === -1) {
      this.#currentPosition = this.#slideNumber - 1;
    }
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;
  }
}</code></pre>
<h3 id="인디케이터-개발">인디케이터 개발</h3>
<p>동적으로 움직이는 인디케이터 만들기</p>
<pre><code class="language-js">//image-slider.js
export default class ImageSlider {
  .
  .
  constructor() {
    .
    .
    //처음상태의 인디케이터 설정
    this.setIndicator();
  }
  assignElement() {
    .
    .
    //인디케이터 탐색
    this.indicatorWrapEl = this.sliderWrapEl.querySelector(&#39;#indicator-wrap&#39;);
  }
  .
  .
  moveToRight() {
  .
  .
  // 인덱스에 따라서 인덱스가 활성화되는 로직을 붙여준다.
  this.setIndicator();
  }
  moveToLeft() {
  .
  .
  // 인덱스에 따라서 인덱스가 활성화되는 로직을 붙여준다.
  this.setIndicator();
  }
  // 동적으로 인디케이터를 만들어주는 메소드 생성
  createIndicator() {
    // DOM fragment[프래그먼트]를 이용해서 리스트를 한번에 ul태그 안에 넣기
    const docFragment = document.createDocumentFragment();
    // slideNumber를 이용하여 리스트 생성
    for (let i = 0; i &lt; this.#slideNumber; i += 1) {
        const li = document.createElement(&#39;li&#39;);
        // 리스트에 data-를 스크립트로 지정해줄때는 dataset한 다음에 .내용입력
        // 여기서는 index값을 내려준다.
        li.dataset.index = i;
        // 그리고 document fragment에 저장해놓은데에다가 리스트를 하나씩 넣어준다.
        docFragment.appendChild(li);
    }
    // ul태그 안에서 찾아야되니까.
    // 이렇게 하게되면 리스트들이 생성되서 docFragment안에 넣어졌고 docFragment를 ul의 자식으로 넣었다.
    // docFragment는 렌더가 되지 않고 결과물은 ul태그 안에 우리가 추가한 엘레멘트가 생성되고 중간에 다른 엘레멘트가 껴들지 않는다.
    this.indicatorWrapEl.querySelector(&#39;ul&#39;).appendChild(docFragment);
  }

  // 인디케이터 활성화
  // 클릭하면 반투명 -&gt; 흰색 버튼으로 색이 변경되는 로직
  setIndicator() {
  // index 활성화 없음 // li(리스트)에 active된걸 찾고 클래스리스트에서 active를 제거
      // 처음에 초기화 될 때는 active된 게 없을수도 있으니까 옵셔널체이닝(?)을 넣었다.
      this.indicatorWrapEl.querySelector(&#39;li.active&#39;)?.classList.remove(&#39;active&#39;);
  // index에 따라서 활성화
      // 몇번째를 활성화 해줄 건지, ul태그 안에 li:nth-child로 몇번째를 찾는지 정해준다
      // nth-child는 0부터 시작이 아니라 1부터 시작하기 때문에 + 1을 해준다.
      this.indicatorWrapEl
          .querySelector(`ul li:nth-child(${this.#currentPosition + 1})`)
          .classList.add(&#39;active&#39;);
  }
}</code></pre>
<p>인디케이터를 클릭했을 때 해당 로직으로 이동하는 것 구현 [이벤트 버블링 효과]</p>
<pre><code class="language-js">//image-slider.js

export default class ImageSlider {
  .
  .
  addEvent() {
    // 인디케이터를 클릭했을 때 해당 로직으로 이동하는 [ 이벤트 버블링 효과 ]
    this.indicatorWrapEl.addEventListener(
      &#39;click&#39;,
      this.onClickIndicator.bind(this),
    );
  }
  // indicator-wrap부분에 이벤트를 붙여주고 클릭 한 것 중에 데이터인덱스 값이 있으면
  // 해당 인덱스로 이동하는 로직으로 구현.
  onClickIndicator(event) {
    // 이벤트 타겟의 데이터셋을 가져오는데 인덱스로 저장해서 가져온다.
    // 버튼을 누르면 제대로 콘솔에 찍히지만 그 사이를 누르면 undefined가 출력된다.
    // parseInt() : 현재 번호가 string으로 바뀌어져있는데 number로 바꿔줘야한다.
    // 현재 eslint-config-airbnb 룰에 걸려서 10진수 진법을 사용해야한다. = 원래는 안해도 됨
    const indexPosition = parseInt(event.target.dataset.index, 10);
    // parseInt(undefined)를 넣으면 NaN가 뜨는데 이것을 가려내야하기 때문에 조건문을 작성한다.
    // Number에 스테틱메소드인 isInteger를 이용해 indexPosition에 정수가 있으면 다음을 실행.
    if (Number.isInteger(indexPosition)) {
      // 정수를 currentPosition에 넣고 슬라이드를 넘길 수 있게.
      this.#currentPosition = indexPosition;
      this.sliderListEl.style.left = `-${
        this.#slideWidth * this.#currentPosition
      }px`;
      // position이 바뀌었으니까 다시 업데이트를 해준다.
      // 이제 인디케이터 버튼을 클릭하면 해당 인덱스로 이동하게 된다.
      this.setIndicator();
    }
  }
}</code></pre>
<h3 id="autoplay-개발">autoplay 개발</h3>
<p>슬라이드가 자동으로 넘어가는 로직 개발<br /> 일시정지 버튼을 누르면 슬라이더가 멈추는 로직 개발</p>
<pre><code class="language-js">//image-slider.js

export default class ImageSlider {
  .
  .
  // 오토플레이 기능을 멈춰야하는 기능을 구현해야하는데 거기에 사용하기 위해
  // setInterval은 id로 저장해둬야한다.
  // 프라이빗 변수로 IntervalId를 만든다.
  #intervalId;

  // 현재 상태가 오토플레이인지 아닌지 판단하기 위해 프라이빗 변수 생성
  // 페이지를 열면 처음에는 오토플레이가 동작하고 있기 때문에 초기값은 true로 해놓는다.
  #autoPlay = true;

  // 클래스 변수가 어떤 일을 하는지 상단에 미리 작성해놔야
  // 나중에 코드를 리팩토링 할 때나 협업을 할 때 알아보기 용이하다.
  sliderWrapEl;
  .
  .
  assignElement() {
    .
    .
    // 페이지가 떴을 때 오토플레이가 동작하는 기능 구현
    this.controlWrapEl = this.sliderWrapEl.querySelector(&#39;#control-wrap&#39;);
  }
  .
  .
  addEvent() {
    // intervalId을 가지고 오토플레이가 멈추고 다시 재생하는 이벤트 구현
    this.controlWrapEl.addEventListener(&#39;click&#39;, this.togglePlay.bind(this));
  }
  // html 문서에 data- 속성을 미리 넣어뒀다. data-status=&quot;pause&quot;, data-status=&quot;play&quot;
  // 이를 활용해 재생해야되는지 멈춰야되는지를 판단하는 메소드 구현.
  // 현재 controlWrap에 이벤트를 넣어놨기 때문에 버블링을 이용하여 부모에 한번에 이벤트를 걸면된다.
  togglePlay(event) {
    if (event.target.dataset.status === &#39;play&#39;) {
      // status가 tute이면 플레이버튼이 눌린 상태라고 인식하고
      // controlWrapEl.classList에 play 클래스를 넣을 것 이고
      // controlWrapEl.classList에 pause 클래스가 있으면 삭제할 것이다.
      this.#autoPlay = true;
      this.controlWrapEl.classList.add(&#39;paly&#39;);
      this.controlWrapEl.classList.remove(&#39;pause&#39;);
      // 위에는 버튼만 구현했고 동작하는 기능을 불러온다.
      this.initAutoplay();
    } else if (event.target.dataset.status === &#39;pause&#39;) {
      // status가 pause인 경우에는 멈추게 되니까 autoPlay가 false가 된다.
      this.#autoPlay = false;
      // 위와 반대로 동작해야하니까 add와 remove를 변경해준다.
      this.controlWrapEl.classList.remove(&#39;paly&#39;);
      this.controlWrapEl.classList.add(&#39;pause&#39;);
      // pause를 누르면 autoPlay를 멈춰야하니까 clearInterval을 이용해서 멈추게한다.
      clearInterval(this.#intervalId);
    }
  }
  .
  .
  // 3초 interval 즉, autoPlay가 계속 작동되고 있는 상태에서
  // 화살표버튼으로 다음 슬라이드로 넘길 시 3초가 안된 상태에서 다음 슬라이드로 넘어가게된다.
  // 슬라이드를 중간에 넘겨버리지 못하게 처리해주기
  // moveToRight(), moveToLeft()에서 처리해주기
  moveToRight() {
    .
    .
    // pause를 누른 상태에서 버튼을 클릭했을 때 setInterval이 시작되면 안되니까
    // 현재 상태가 play인지 pause인지 즉, autoPlay 상태인지 아닌지 알고
    // autoPlay일 경우에만 해당 로직을 실행시킬 수 있게 조건문으로 감싸준다.
    if (this.#autoPlay) {
      // moveToRight()를 누르게 되면 intervalId가 있다면 얘를 멈추고
      // 즉, clearInterval 하고 다시 setInterval을 시작해주는 로직
      // 이렇게 되면 setInterval이 돌고있는데 moveToRight를 눌렀다면
      // 뒤에서 돌고 있는 setInterval은 멈추게 되고 우리가 버튼을 누름으로 인해서
      // 다음 슬라이드로 넘어가게되고 그 상태에서 다시 setInterval을 시작하게된다.
      clearInterval(this.#intervalId);
      this.#intervalId = setInterval(this.moveToRight.bind(this), 3000);
  }
  moveToLeft() {
    .
    .
    // moveToRight()에서 한 것 같이 똑같이 로직을 구성해준다.
    if (this.#autoPlay) {
      clearInterval(this.#intervalId);
      this.#intervalId = setInterval(this.moveToRight.bind(this), 3000);
    }
    this.setIndicator();
  }
  .
  .
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[가상키보드 만들기]]></title>
            <link>https://velog.io/@mylee-p/%EA%B0%80%EC%83%81%ED%82%A4%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@mylee-p/%EA%B0%80%EC%83%81%ED%82%A4%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 30 Dec 2022 06:55:04 GMT</pubDate>
            <description><![CDATA[<h1 id="가상키보드">가상키보드</h1>
<img width="711" alt="keyboard" src="https://user-images.githubusercontent.com/89143892/210031755-c338abec-c723-443b-8d5a-f4c647d1edd0.png">

<br />

<h3 id="개발환경-설정">개발환경 설정</h3>
<blockquote>
<p>최상의 성능을 위해 어플리케이션을 최적화하는 개발도구 
Webpack 사용</p>
</blockquote>
<pre><code>$ npm i -D webpack webpack-cli webpack-dev-server
$ npm i -D terser-webpack-plugin</code></pre><pre><code class="language-jsx">//webpack.config.js

const path = require(&quot;path&quot;);
const TerserWebpackPlugin = require(&quot;terser-webpack-plugin&quot;);
module.exports = {
  entry: &quot;./src/js/index.js&quot;,
  output: {
    filename: &quot;bundle.js&quot;,
    path: path.resolve(__dirname, &quot;./dist&quot;),
    clean: true,
  },
  devtool: &quot;source-map&quot;,
  mode: &quot;development&quot;,
  optimization: {
    minimizer: [new TerserWebpackPlugin()],
  },
};</code></pre>
<ul>
<li>추가적인 압축 플러그인까지 설치 후 <code>TerserWebpackPlugin</code> 을 생성하여 <br /> <code>minimizer</code> 에 입력 후 <code>$ npx webpack</code> 을 실행한다.</li>
<li><code>dist</code> 경로에 <code>bundle.js</code> , <code>bundle.js.map</code> 파일이 생성된다.</li>
</ul>
<blockquote>
<p>HTML과 CSS파일을 설정해줄 모듈을 설치한다.</p>
</blockquote>
<pre><code>$ npm i -D html-webpack-plugin
$ npm i -D mini-css-extract-plugin css-loader css-minimizer-webpack-plugin</code></pre><blockquote>
<p>Lodash, 객체, 배열 등의 데이터 구조를 쉽게 변환해 사용하게 도와주는 <br /> 자바스크립트 라이브러리</p>
</blockquote>
<pre><code>$ npm i -g npm
$ npm i --save lodash</code></pre><ul>
<li>추가적인 Webapck 설정 사항을 입력한다.</li>
</ul>
<pre><code class="language-js">//webpack.config.js

const path = require(&quot;path&quot;);
const TerserPlugin = require(&quot;terser-webpack-plugin&quot;);
const CssMinimizerPlugin = require(&quot;css-minimizer-webpack-plugin&quot;); /
const HtmlWebpackPlugin = require(&quot;html-webpack-plugin&quot;);
const MiniCssExtractPlugin = require(&quot;mini-css-extract-plugin&quot;);
module.exports = {
  entry: &quot;./src/js/index.js&quot;,
  output: {
    filename: &quot;bundle.js&quot;,
    path: path.resolve(__dirname, &quot;./dist&quot;),
    clean: true,
  },
  devtool: &#39;source-map&#39;,
  mode: &quot;development&quot;,
  devServer: {
    host: &quot;localhost&quot;,
    port: 4200,
    open: true,
    watchFiles: &#39;index.html&#39;,
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: &quot;keyboard&quot;,
      template: &quot;./index.html&quot;,
      inject: &quot;body&quot;,
      favicon: &quot;./favicon.ico&quot;
    }),
    new MiniCssExtractPlugin({ filename: &quot;style.css&quot; }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, &quot;css-loader&quot;],
      },
    ],
  },
  optimization: {
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin()
    ]
  },
};</code></pre>
<ul>
<li><code>build</code>를 <code>production</code> 모드에서 하기</li>
</ul>
<pre><code class="language-json">//package.json

&quot;scripts&quot;: {
    &quot;build&quot;: &quot;webpack --mode=production&quot;, //npm run build
    &quot;dev&quot;: &quot;webpack-dev-server&quot; //npm run dev
  },</code></pre>
<ul>
<li>Webpack Dev Server를 이용해서 개발할 때 Dev server 띄우기
<code>$ npx webpack-dev-server</code> 입력해서 잘 동작하는지 확인</li>
</ul>
<pre><code class="language-js">//webpack.config.js

devServer: {
    host: &quot;localhost&quot;,
    port: 4200,
    open: true,
    watchFiles: &#39;index.html&#39;,
  },</code></pre>
<p>Webpack Dev Server로 인해 개발환경이 설정이 제대로 되었다면
브라우저를 새로고침하지 않아도 자동으로 새로고침되어 개발하기 편한 환경 탄생!
<code>$ npm run dev</code>를 통해 브라우저를 열 수 있다.</p>
<hr>
<h3 id="개발환경-설정2">개발환경 설정2</h3>
<blockquote>
<p>ESLint, 코드 퀄리티를 보장하도록 도와준다.
Prettier, 코드 스타일을 깔끔하게 도와준다.</p>
</blockquote>
<pre><code>$ npm i -D eslint
$ npm install --save-dev --save-exact prettier
$ npm i -D eslint-config-prettier eslint-plugin-prettier
$ npx eslint --init</code></pre><pre><code class="language-json">//.eslintrc.json

{
  &quot;env&quot;: {
    &quot;browser&quot;: true,
    &quot;es2021&quot;: true
  },
  &quot;extends&quot;: [&quot;eslint:recommended&quot;, &quot;plugin:prettier/recommended&quot;],
  &quot;parserOptions&quot;: {
    &quot;ecmaVersion&quot;: 13,
    &quot;sourceType&quot;: &quot;module&quot;
  },
  &quot;rules&quot;: {
    &quot;prettier/prettier&quot;: &quot;error&quot;
  }
}</code></pre>
<pre><code class="language-json">//.prettierrc.json

{
  &quot;arrowParens&quot;: &quot;always&quot;,
  &quot;bracketSameLine&quot;: false,
  &quot;bracketSpacing&quot;: true,
  &quot;embeddedLanguageFormatting&quot;: &quot;auto&quot;,
  &quot;htmlWhitespaceSensitivity&quot;: &quot;css&quot;,
  &quot;insertPragma&quot;: false,
  &quot;jsxSingleQuote&quot;: false,
  &quot;printWidth&quot;: 80,
  &quot;proseWrap&quot;: &quot;preserve&quot;,
  &quot;quoteProps&quot;: &quot;as-needed&quot;,
  &quot;requirePragma&quot;: false,
  &quot;semi&quot;: true,
  &quot;singleQuote&quot;: false,
  &quot;tabWidth&quot;: 2,
  &quot;trailingComma&quot;: &quot;es5&quot;,
  &quot;useTabs&quot;: false,
  &quot;vueIndentScriptAndStyle&quot;: false
}</code></pre>
<pre><code class="language-json">//settings.json

{
    //저장할 때 prettier가 포맷한다.(js.css.html 모두)
  &quot;editor.formatOnSave&quot;: true,
  &quot;editor.codeActionsOnSave&quot;: {
    //저장될 때 eslint 룰대로 fix 한다.
    &quot;source.fixAll.eslint&quot;: true
}</code></pre>
<hr>
<h3 id="다크테마-적용하기">다크테마 적용하기</h3>
<p>다크테마 기능을 사용하기 위한 css 설정</p>
<ul>
<li>filter: invert(100%) 색상 반전 효과 [무채색을 반전]</li>
<li>filter: hue-rotate 컬러 휠을 기준으로 몇 도를 움직이는지 설정 [색상이 있는 색상을 반전]</li>
</ul>
<pre><code class="language-css">/* style.css */

html[theme=&quot;dark-mode&quot;] {
  filter: invert(100%) hue-rotate(180deg);
}</code></pre>
<pre><code class="language-html">&lt;!-- index.html  --&gt;

&lt;!DOCTYPE html&gt;
&lt;html theme=&quot;&quot;&gt;&lt;/html&gt;</code></pre>
<p><code>class Keyboar</code> 안에서 <code>DOM</code>을 가져와서 <code>DOM</code>에 이벤트를 붙이는 형식으로 구성</p>
<ul>
<li><p>#를 붙이면 private변수가 되서 class 외부에서 값을 오해하거나 덮어씌울 수 없다. <br /> [ES2019부터 javascript에서 지원되는 속성]</p>
</li>
<li><p>인스턴스가 생성되었기 때문에 keyboard.js 안의 consteuctor()가 실행되고 <br /> #assignElement(), #addEvent()가 실행되면서 console에 true / false 값이 찍힌다. <br />true 일 때 다크테마모드 적용, false면 해제.</p>
</li>
</ul>
<pre><code class="language-js">//keyboard.js

export class Keyboard {
  #swichEl;
  constructor() {
    this.#assignElement();
    this.#addEvent();
  }
  //엘리먼트 가져오기
  #assignElement() {
    this.#swichEl = document.getElementById(&quot;switch&quot;);
  }

  //이벤트를 가져오는 메소드
  #addEvent() {
    this.#swichEl.addEventListener(&quot;change&quot;, (event) =&gt; {
      document.documentElement.setAttribute(
        &quot;theme&quot;,
        event.target.checked ? &quot;dark-mode&quot; : &quot;&quot;
      );
      console.log(event.target.checked);
    });
  }
}</code></pre>
<h3 id="font-변경-기능">Font 변경 기능</h3>
<p><code>&lt;div class=&quot;select-box&quot;&gt; 에 &lt;select id=&quot;font&quot;&gt;</code> 를 줘서 <br /> 변경될 때 마다 변화를 감지하고 옵션태그 안의 값을 이용해서 폰트를 변경하는 것을 구현</p>
<pre><code class="language-js">//keyboard.js

export class Keyboard {
  #swichEl; //다크모드 스위치 엘레멘트
  #fontSelectEl; //폰트 변경 엘레멘트
  constructor() {
    this.#assignElement();
    this.#addEvent();
  }

  #assignElement() {
    this.#swichEl = document.getElementById(&quot;switch&quot;);
    this.#fontSelectEl = document.getElementById(&quot;font&quot;);
  }

  #addEvent() {
    this.#swichEl.addEventListener(&quot;change&quot;, (event) =&gt; {
      document.documentElement.setAttribute(
        &quot;theme&quot;,
        event.target.checked ? &quot;dark-mode&quot; : &quot;&quot;
      );
      console.log(event.target.checked);
    });
    this.#fontSelectEl.addEventListener(&quot;change&quot;, (event) =&gt; {
      //가져온 폰트값 body에 적용하기
      //body에 있는 모든 폰트들이 선택한 폰트로 바뀌게 된다.
      document.body.style.fontFamily = event.target.value;
      //select-box이기때문에 value를 가져올 수 있다.
    });
  }
}</code></pre>
<h3 id="keyboard-event-적용하기">Keyboard event 적용하기</h3>
<blockquote>
<p>코드 리팩토링 [비용절감 / 이벤트핸들러 분리] <br /></p>
</blockquote>
<ul>
<li>document 에서 switch, font를 찾고 탐색하고 있는데 해당 부분 개선하기 <br /></li>
<li>현재는 메뉴, 인풋그룹, 키보드가 컨테이너에 둘러 쌓여 있는데 해당 부분 개선하기<br />→ 컨테이너를 먼저 찾은 다음에 컨테이너 메뉴, 인풋그룹, 키보드 클래스를 탐색하는 방법</li>
</ul>
<pre><code class="language-js">//keyboard.js
export class Keyboard {
  #swichEl; //다크모드 스위치 엘레멘트
  #fontSelectEl; //폰트 변경 엘레멘트
  #containerEl;
  constructor() {
    this.#assignElement();
    this.#addEvent();
  }

  #assignElement() {
    this.#containerEl = document.getElementById(&quot;container&quot;);
    this.#swichEl = this.#containerEl.querySelector(&quot;#switch&quot;);
    this.#fontSelectEl = this.#containerEl.querySelector(&quot;#font&quot;);
  }

  #addEvent() {
    this.#swichEl.addEventListener(&quot;change&quot;, this.#onChangeTheme);
    this.#fontSelectEl.addEventListener(&quot;change&quot;, this.#onChangeFont);
  }

    //다크테마모드 변경
  #onChangeTheme(event) {
    document.documentElement.setAttribute(
      &quot;theme&quot;,
      event.target.checked ? &quot;dark-mode&quot; : &quot;&quot;
    );
    console.log(event.target.checked);
  }
    //폰트 변경
  #onChangeFont(event) {
    document.body.style.fontFamily = event.target.value;
  }
}</code></pre>
<blockquote>
<p>키보드 이벤트 추가하기 <br />KeyboardEvent { isTrusted: true, key: ‘a’, code: ’KeyA’, …}</p>
</blockquote>
<pre><code class="language-jsx">//keyboard.js

#addEvent() {
    this.#swichEl.addEventListener(&quot;change&quot;, this.#onChangeTheme);
    this.#fontSelectEl.addEventListener(&quot;change&quot;, this.#onChangeFont);

    document.addEventListener(&quot;keydown&quot;, (event) =&gt; {
      console.log(event.code);
      if (this.#keyboardEl.querySelector(`[data-code=${event.code}]`)) {
        this.#keyboardEl
          .querySelector(`[data-code=${event.code}]`)
          .classList.add(&quot;active&quot;);
      }
    });
    document.addEventListener(&quot;keyup&quot;, (event) =&gt; {
      if (this.#keyboardEl.querySelector(`[data-code=${event.code}]`)) {
        this.#keyboardEl
          .querySelector(`[data-code=${event.code}]`)
          .classList.remove(&quot;active&quot;);
      }
    });
  }</code></pre>
<ul>
<li>[ 옵셔널체이닝(optional chaining) ]<br />if문 보다 간단하게 ?. 사용하여 코드 리팩토링</li>
</ul>
<pre><code class="language-jsx">//keyboard.js

#addEvent() {
    this.#swichEl.addEventListener(&quot;change&quot;, this.#onChangeTheme);
    this.#fontSelectEl.addEventListener(&quot;change&quot;, this.#onChangeFont);

    document.addEventListener(&quot;keydown&quot;, (event) =&gt; {
      console.log(event.code);
      this.#keyboardEl
        .querySelector(`[data-code=${event.code}]`)
        ?.classList.add(&quot;active&quot;);
    });

    document.addEventListener(&quot;keyup&quot;, (event) =&gt; {
      this.#keyboardEl
        .querySelector(`[data-code=${event.code}]`)
        ?.classList.remove(&quot;active&quot;);
    });
  }</code></pre>
<blockquote>
<p>한글 입력 불가 메세지 표시 및 한글 입력 막기</p>
</blockquote>
<ul>
<li><code>keydown</code> 즉, 입력된 키가 한글인지 캐치해서 인풋그룹에 에러 클래스를 추가해<br/> 한글 입력 불가 경고 메세지를 띄우는 것</li>
<li><code>input value</code> 중에 한글을 찾아내서 한글을 공백으로 치환해준다. <br /> <code>replace()</code>의 첫번째 파라미터에는 정규식을 넣어주고 두번째는 <code>&quot;&quot;</code>으로 바꿔준다. <br />이렇게 한글을 <code>&quot;&quot;</code>으로 바꾼 값을 다시 <code>inputEl.value</code>에 넣어준다.</li>
</ul>
<pre><code class="language-js">export class Keyboard {
  #swichEl;
  #fontSelectEl;
  #containerEl;
  #keyboardEl;
  #inputGroupEl;
  #inputEl;
  constructor() {
    this.#assignElement();
    this.#addEvent();
  }

.
.
.


  #addEvent() {
    this.#swichEl.addEventListener(&quot;change&quot;, this.#onChangeTheme);
    this.#fontSelectEl.addEventListener(&quot;change&quot;, this.#onChangeFont);
    document.addEventListener(&quot;keydown&quot;, this.#onKeyDown.bind(this));
    document.addEventListener(&quot;keyup&quot;, this.#onKeyUp.bind(this));
    this.#inputEl.addEventListener(&quot;input&quot;, this.#onInput);
  }

  #onInput(event) {
    event.target.value = event.target.value.replace(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/, &quot;&quot;);
  }

  #onKeyDown(event) {
    this.#inputGroupEl.classList.toggle(
      &quot;error&quot;,
      /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(event.key)
    );

    this.#keyboardEl
      .querySelector(`[data-code=${event.code}]`)
      ?.classList.add(&quot;active&quot;);
  }

  #onKeyUp(event) {
    this.#keyboardEl
      .querySelector(`[data-code=${event.code}]`)
      ?.classList.remove(&quot;active&quot;);
  }

  #onChangeTheme(event) {
    document.documentElement.setAttribute(
      &quot;theme&quot;,
      event.target.checked ? &quot;dark-mode&quot; : &quot;&quot;
    );
    console.log(event.target.checked);
  }

  #onChangeFont(event) {
    document.body.style.fontFamily = event.target.value;
  }
}</code></pre>
<blockquote>
<p>마우스 이벤트 추가하기</p>
</blockquote>
<ul>
<li>키보드 레이아웃에서 마우스로 키보드를 눌렀을 때 input에 입력되게 구현</li>
<li>마우스를 클릭할 때는 키보드가 안눌리게 예외처리</li>
<li>키보드를 입력할 때는 마우스 클릭이 안되게 예외처리</li>
</ul>
<pre><code class="language-js">export class Keyboard {
  #swichEl;
  #fontSelectEl;
  #containerEl;
  #keyboardEl;
  #inputGroupEl;
  #inputEl;
  //예외처리
  #keyPress = false;
  #MouseDown = false;
  constructor() {
    this.#assignElement();
    this.#addEvent();
  }

  .
  .
  .


  #onMouseUp(event) {
    //키보드를 누를 때 마우스가 동작하지 않도록 해주고
    //mouseUp상태니까 this.#MouseDown = false;로 .
    if (this.#keyPress) return;
    this.#MouseDown = false;
    const KeyEl = event.target.closest(&quot;div.key&quot;);

    const isActive = !!KeyEl?.classList.contains(&quot;active&quot;);

    const val = KeyEl?.dataset.val;

    if (isActive &amp;&amp; !!val &amp;&amp; val !== &quot;Space&quot; &amp;&amp; val !== &quot;Backspace&quot;) {
      this.#inputEl.value += val;
    }

    if (isActive &amp;&amp; val === &quot;Space&quot;) {
      this.#inputEl.value += &quot; &quot;;
    }

    if (isActive &amp;&amp; val === &quot;Backspace&quot;) {
      this.#inputEl.value = this.#inputEl.value.slice(0, -1);
    }

    this.#keyboardEl.querySelector(&quot;.active&quot;)?.classList.remove(&quot;active&quot;);
  }

  #onMouseDown(event) {
    //키보드를 입력하고 있을 때 마우스핸들러는 작동하지 않게하기
    //메소드 자체가 실행이 안되고 밑에 있는 코드들에 접근하지 못하게 막기
    if (this.#keyPress) return;
    this.#MouseDown = true;
    event.target.closest(&quot;div.key&quot;)?.classList.add(&quot;active&quot;);
  }

  #onInput(event) {
    event.target.value = event.target.value.replace(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/, &quot;&quot;);
  }

  #onKeyDown(event) {
    //#MouseDown 상태면 키보드 이벤트핸들러는 동작할 수 없게하기
    if (this.#MouseDown) return;
    //onKeyDown한 상태니까 keyPress가 true인 상태.
    this.#keyPress = true;

    this.#inputGroupEl.classList.toggle(
      &quot;error&quot;,
      /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(event.key)
    );

    this.#keyboardEl
      .querySelector(`[data-code=${event.code}]`)
      ?.classList.add(&quot;active&quot;);
  }

  #onKeyUp(event) {
    //#MouseDown 상태면 키보드 이벤트핸들러는 동작할 수 없게하기
    if (this.#MouseDown) return;
    //onKeyUp을 하면 키보드가 눌려진 상태가 아니니까 false.
    this.#keyPress = false;

    this.#keyboardEl
      .querySelector(`[data-code=${event.code}]`)
      ?.classList.remove(&quot;active&quot;);
  }
.
.
.

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTML / CSS / JavaScript로 스타벅스 페이지 만들기]]></title>
            <link>https://velog.io/@mylee-p/HTML-CSS-JavaScript%EB%A1%9C-%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@mylee-p/HTML-CSS-JavaScript%EB%A1%9C-%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 30 Dec 2022 02:48:00 GMT</pubDate>
            <description><![CDATA[<h1 id="starbucks">Starbucks</h1>
<p><img src="https://user-images.githubusercontent.com/89143892/209923185-8527e71f-5bde-43f7-9bb6-f2ed8fc5faff.jpg" alt="starbucks"></p>
<hr>
<br />

<h2 id="html--css--javascript를-활용해서-스타벅스-페이지-제작하기">HTML / CSS / JavaScript를 활용해서 스타벅스 페이지 제작하기</h2>
<br />

<h3 id="bemblock-element-modifier-html-클래스-속성의-작명법-사용하기">BEM(Block Element Modifier), HTML 클래스 속성의 작명법 사용하기</h3>
<pre><code class="language-js">&lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;name&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;item&quot;&gt;
        &lt;div class=&quot;name&quot;&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

//위의 코드를 Lodash 기호로 요소의 일부분을 표시
//CSS선택자를 통해 요소를 입력할 때 container 후손선택자로 사용할 경우
//item의 자식요소인 name도 같이 선택될 수 있기 때문에 정확하게 요소를 지칭

&lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;container__name&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;item&quot;&gt;
        &lt;div class=&quot;item__name&quot;&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre>
<h3 id="lodash-cdn-스크롤할-때-생성되는-함수의-갯수-제어-br-">lodash CDN, 스크롤할 때 생성되는 함수의 갯수 제어 <br /></h3>
<ul>
<li>head 태그에 script를 작성</li>
</ul>
<pre><code class="language-js">&lt;script
    src=&quot;https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js&quot;
    integrity=&quot;sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==&quot;
    crossorigin=&quot;anonymous&quot;
    referrerpolicy=&quot;no-referrer&quot;
&gt;&lt;/script&gt;</code></pre>
<ul>
<li>_.throttle(함수, 시간)을 이용해서 스크롤 과부하를 방지한다.</li>
</ul>
<pre><code class="language-js">window.addEventListener(
    &quot;scroll&quot;,
    _.throttle(function () {
        console.log(window.scrollY);
        if (window.scrollY &gt; 500) {
            badgeEl.style.display = &quot;none&quot;;
        } else {
            badgeEl.style.display = &quot;block&quot;;
        }
    }, 300)
);</code></pre>
<h3 id="gsap-cdn-자바스크립트의-애니메이션-br-">gsap CDN, 자바스크립트의 애니메이션 <br /></h3>
<ul>
<li>head 태그에 script를 작성</li>
</ul>
<pre><code class="language-js">&lt;script
    src=&quot;https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js&quot;
    integrity=&quot;sha512-VEBjfxWUOyzl0bAwh4gdLEaQyDYPvLrZql3pw1ifgb6fhEvZl9iDDehwHZ+dsMzA0Jfww8Xt7COSZuJ/slxc4Q==&quot;
    crossorigin=&quot;anonymous&quot;
    referrerpolicy=&quot;no-referrer&quot;
&gt;&lt;/script&gt;</code></pre>
<ul>
<li>gsap.to(요소, 지속시간, 옵션)으로 애니메이션 동작시키기 <br /></li>
</ul>
<pre><code class="language-js">window.addEventListener(&#39;scroll&#39; , _.throttle(function () {
  console.log(window.scrollY);
  if (window.scrollY &gt; 500) {
    gsap.to(badgeEl, .6, {
      opacity: 0
            display: &#39;none&#39;
    });
  } else {
    badgeEl.style.display = &#39;block&#39;;
  }
}, 300));</code></pre>
<h3 id="fade-in-시간차-애니메이션-br-">fade-in, 시간차 애니메이션 <br /></h3>
<pre><code class="language-css">/* css */
.visual .fade-in {
    opacity: 0;
}</code></pre>
<pre><code class="language-js">//js
const fadeEls = document.querySelectorAll(&quot;.visual .fade-in&quot;);
fadeEls.forEach(function (fadeEl, index) {
    gsap.to(fadeEl, 1, {
        delay: (index + 1) * 0.7,
        //0.7, 1.4, 2.1, 2.7 순차적으로 더해져서 반복
        opacity: 1,
    });
});</code></pre>
<h3 id="swiperslide-자바스크립트-라이브러리-활용-br-">Swiper(slide), 자바스크립트 라이브러리 활용 <br /></h3>
<pre><code class="language-js">new Swiper(&quot;.notice-line .swiper-container&quot;, {
    direction: &quot;vertical&quot;, // 수직 슬라이드
    autoplay: true, // 자동 재생 여부
    loop: true, // 반복 재생 여부
});</code></pre>
<h3 id="iframe-player-api-유튜브-영상-삽입-br-">IFrame Player API, 유튜브 영상 삽입 <br /></h3>
<ul>
<li>유트브 영상이 출력될 위치에 요소를 생성한다.</li>
</ul>
<pre><code class="language-html">&lt;!-- html --&gt;

&lt;!-- in HEAD --&gt;
&lt;script defer src=&quot;./js/youtube.js&quot;&gt;&lt;/script&gt;

&lt;!-- in BODY --&gt;
&lt;div id=&quot;player&quot;&gt;&lt;/div&gt;</code></pre>
<ul>
<li>onYouTubePlayerAPIReady 함수 이름은 Youtube IFrame Player API에서 사용하는 이름이기 때문에 다르게 지정하면 동작하지 않는다.</li>
</ul>
<pre><code class="language-js">//js

// Youtube IFrame API를 비동기로 로드
var tag = document.createElement(&quot;script&quot;);
tag.src = &quot;https://www.youtube.com/iframe_api&quot;;
var firstScriptTag = document.getElementsByTagName(&quot;script&quot;)[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

function onYouTubePlayerAPIReady() {
    new YT.Player(&quot;player&quot;, {
        videoId: &quot;An6LvWQuj_8&quot;, // 재생할 유튜브 영상 ID
        playerVars: {
            autoplay: true, // 자동 재생 유무
            loop: true, // 반복 재생 유무
            playlist: &quot;An6LvWQuj_8&quot;, // 반복 재생할 유튜브 영상 ID 목록
        },
        events: {
            // 영상이 준비되었을 때,
            onReady: function (event) {
                event.target.mute(); // 음소거!
            },
        },
    });
}</code></pre>
<h3 id="동영상-앞에-움직이는-애니메이션-삽입br-">동영상 앞에 움직이는 애니메이션 삽입<br /></h3>
<pre><code class="language-js">// 범위 랜덤 함수(소수점 2자리까지)
function random(min, max) {
  return parseFloat((Math.random() * (max - min) + min).toFixed(2))
}
function floatingObject(selector, delay, size) {
  gsap.to(
    selector, 
    random(1.5, 2.5), //애니메이션 동작 시간
    { //옵션
    y: size,
    repeat: -1, //무한반복
    yoyo: true,
    ease: power1.easeInOut,
    delay: ramdom(0, delay) //지연시간
    } 
  );
}
floatingObject(&#39;.floating1&#39;, 1, 15);
floatingObject(&#39;.floating2&#39;, .5, 15);
floatingObject(&#39;.floating3&#39;, 1.5, 20);</code></pre>
<h3 id="scrollmagic-api-br-">ScrollMagic API <br /></h3>
<p>스크롤과 요소의 상호작용을 위한 자바스크립트 라이브러리 활용 <br /></p>
<ul>
<li><p>head 태그에 script를 작성</p>
<pre><code class="language-html">&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.8/ScrollMagic.min.js&quot;&gt;&lt;/script&gt;</code></pre>
</li>
<li><p>대표적으로 어떤 요소가 현재 화면에 보이는 상태인지를 확인할 때 사용</p>
<pre><code class="language-js">new ScrollMagic
.Scene({ 
  triggerElement: spyEl, // 보여짐 여부를 감시할 요소를 지정
  triggerHook: .8 // 화면의 80% 지점에서 보여짐 여부 감시
})
.setClassToggle(spyEl, &#39;show&#39;) // 요소가 화면에 보이면 show 클래스 추가
.addTo(new ScrollMagic.Controller()) // 컨트롤러에 장면을 할당(필수!)</code></pre>
</li>
</ul>
<h3 id="swiper-다중이미지-슬라이드">Swiper, 다중(이미지) 슬라이드</h3>
<pre><code class="language-js">new Swiper(&#39;.awards .swiper-container&#39;, {
  autoplay: true,
  loop: true,
  SpaceBetween: 30,
  slidesPerView: 5, //하나의 화면에 몇 개의 슬라이드들이 보일 것인지
  navigation: {
    prevEl: &#39;.awards .swiper-prev&#39;,
    nextEl: &#39;.awards .swiper-next&#39;
  }
});</code></pre>
<h3 id="scrollto-페이지-상단으로-이동">ScrollTo, 페이지 상단으로 이동</h3>
<pre><code class="language-js">const toTopEl = document.querySelector(&#39;#to-top&#39;);
// window : 브라우저 창, 탭, 윈도우 객체라고 부름. 보고있는 화면 자체
window.addEventListener(&#39;scroll&#39; , _.throttle(function () {
  console.log(window.scrollY);
  if (window.scrollY &gt; 500) {
      .
      .
      .

    //버튼 보이기
    gsap.to(toTopEl, .2, {
      x: 0
    });
  } else {
      .
      .
      .
    //버튼 숨기기
    gsap.to(toTopEl, .2, {
      x: 100
    });
  }
}, 300));
//_.throttle(함수, 시간)

toTopEl.addEventListener(&#39;click&#39;, function () {
    /*window객체는 우리가 출력하는 화면 그 자체를 애니메이션 처리*/ 
  gsap.to(window, .7, { 
    scrollTo: 0
  }); 
});</code></pre>
]]></description>
        </item>
    </channel>
</rss>