<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>g_c0916.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 31 Mar 2025 11:20:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>g_c0916.log</title>
            <url>https://velog.velcdn.com/images/g_c0916/profile/322e56a9-cff2-4b3b-84d3-03b5db5f9128/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. g_c0916.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/g_c0916" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA['.gif', '.jpg'... 모듈 또는 해당 형식 선언을 찾을 수 없습니다 ts(2307) 오류]]></title>
            <link>https://velog.io/@g_c0916/.gif-.jpg...-%EB%AA%A8%EB%93%88-%EB%98%90%EB%8A%94-%ED%95%B4%EB%8B%B9-%ED%98%95%EC%8B%9D-%EC%84%A0%EC%96%B8%EC%9D%84-%EC%B0%BE%EC%9D%84-%EC%88%98-%EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4-ts2307-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@g_c0916/.gif-.jpg...-%EB%AA%A8%EB%93%88-%EB%98%90%EB%8A%94-%ED%95%B4%EB%8B%B9-%ED%98%95%EC%8B%9D-%EC%84%A0%EC%96%B8%EC%9D%84-%EC%B0%BE%EC%9D%84-%EC%88%98-%EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4-ts2307-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Mon, 31 Mar 2025 11:20:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>assets/loading.gif&#39; 모듈 또는 해당 형식 선언을 찾을 수 없습니다.ts(2307)</p>
</blockquote>
<p>TypeScript에서 이미지를 import하려고 하는데 에러가 발생했는데 이미지 파일의 타입이 정의되지 않아서 그렇다고 한다
타입 정의 파일(.d.ts)을 생성하여 사용하려는 이미지 파일의 확장자에 타입을 정의해주면 된다.</p>
<p>프로젝트 root에 declarations.d.ts라는 파일을 만들고 아래 코드를 입력 후 저장 해 준다.</p>
<hr>
<pre><code class="language-js">declare module &#39;*.gif&#39;;
declare module &#39;*.png&#39;;
declare module &#39;*.jpg&#39;;
declare module &#39;*.jpeg&#39;;
declare module &#39;*.svg&#39;;
</code></pre>
<hr>
<p>저장 후에도 해당 오류가 해결되지 않는 경우</p>
<p><code>tsconfig.json</code>파일을 살펴보자.</p>
<pre><code class="language-js">&quot;include&quot;: [&quot;src&quot;, &quot;react-app-env.d.ts&quot;, &quot;*.d.ts&quot;],</code></pre>
<p>include 옵션에 <code>&quot;*.d.ts&quot;</code> 을 추가 해주도록 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Query] 리액트 쿼리 알아보기(1)]]></title>
            <link>https://velog.io/@g_c0916/React-Query-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BF%BC%EB%A6%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B01</link>
            <guid>https://velog.io/@g_c0916/React-Query-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BF%BC%EB%A6%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B01</guid>
            <pubDate>Mon, 13 Nov 2023 08:30:59 GMT</pubDate>
            <description><![CDATA[<h2 id="react-query">React Query</h2>
<p>React에서 서버 상태를 가져오고, 캐싱하고, 동기화하고, 업데이트할 수 있는 라이브러리. 비동기 로직을 쉽게 처리할 수 있다.</p>
<h3 id="상태">상태</h3>
<ul>
<li>Local State : React 컴포넌트 안에서 사용되는 state</li>
<li>Global State : Global Store에 정의되어 프로젝트 어디서나 접근할 수 있는 state</li>
<li>Server State : 서버로부터 받아오는 state</li>
</ul>
<hr>
<p>간단한 예제를 만들어 react query가 어떤식으로 동작하는지 먼저 확인해보자.</p>
<h3 id="테스트용-서버">테스트용 서버</h3>
<p>client에서 get 요청시 데이터가 저장된 배열을 응답으로 보내준다.
client에서 post 요청시 전달받은 id와 title을 배열에 추가한다.</p>
<pre><code class="language-js">// server.js

const express = require(&quot;express&quot;);
const app = express();
const cors = require(&quot;cors&quot;); // CORS 미들웨어 추가

// JSON 파싱 미들웨어를 사용하여 POST 요청 본문을 파싱합니다.
app.use(express.json());

// CORS 설정
app.use(cors());

// 메뉴 데이터를 저장할 배열
const menuData = [
  { id: 1, title: &quot;noodle&quot; },
  { id: 2, title: &quot;risotto&quot; },
];

// GET 요청을 처리하는 엔드포인트
app.get(&quot;/menu&quot;, (req, res) =&gt; {
  res.json(menuData);
});

// POST 요청을 처리하는 엔드포인트
app.post(&quot;/menu&quot;, (req, res) =&gt; {
  // POST 요청에서 전송된 데이터를 메뉴 데이터 배열에 추가
  const newItem = req.body.suggest;
  console.log(newItem);
  menuData.push(newItem);

  res.status(201).json(newItem); // 새로운 항목을 반환하고 상태 코드 201(Created)를 전송
});

// 서버를 특정 포트에서 시작
const port = process.env.PORT || 4000;
app.listen(port, () =&gt; {
  console.log(`서버가 ${port} 포트에서 실행 중입니다.`);
});
</code></pre>
<hr>
<h3 id="테스트용-클라이언트">테스트용 클라이언트</h3>
<p>server에 있는 메뉴 리스트를 불러와 li 태그로 나열해주고 suggest 버튼을 누르면 Toowoomba Pasta가 계속 추가된다.</p>
<pre><code class="language-js">// ./src/App.js
import axios from &quot;axios&quot;;
import &quot;./App.css&quot;;
import { useMutation, useQuery, useQueryClient } from &quot;react-query&quot;;

function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;Menus /&gt;
    &lt;/div&gt;
  );
}

function Menus() {
  const queryClient = useQueryClient();

  const { data } = useQuery(&quot;getMenu&quot;, () =&gt;
    axios.get(&quot;http://localhost:4000/menu&quot;).then(({ data }) =&gt; data)
  );

  const { mutate } = useMutation(
    (suggest) =&gt; axios.post(&quot;http://localhost:4000/menu&quot;, { suggest }),
    {
      onSuccess: () =&gt; queryClient.invalidateQueries(&quot;getMenu&quot;),
    }
  );
  return (
    &lt;div&gt;
      &lt;h1&gt;Tomorrow&#39;s Lunch candidates! &lt;/h1&gt;
      &lt;button
        onClick={() =&gt; mutate({ id: Date.now(), title: &quot;Toowoomba Pasta&quot; })}
      &gt;
        Suggest Tomorrow&#39;s Menu
      &lt;/button&gt;
      &lt;ul&gt;{data &amp;&amp; data.map((item) =&gt; &lt;li key={item.id}&gt;{item.title}&lt;/li&gt;)}&lt;/ul&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<hr>
<p>브라우저를 두개 띄워놓고 왼쪽만 suggest 버튼을 눌러본다.</p>
<center>
  <img src="https://velog.velcdn.com/images/g_c0916/post/e89f2712-9585-4686-8350-05bd9cf6621c/image.png" width="700" />
</center>


<p>다음으로 오른쪽 브라우저를 클릭해 포커스가 되는 순간 아무것도 하지않아도 리스트가 최신 server data로 리랜더링 된다.</p>
<center>
  <img src="https://velog.velcdn.com/images/g_c0916/post/fa0a50b3-53dc-4e2a-9875-e15f33355425/image.png" width="700" />
</center>

<p>버튼을 누르지도, 새로고침을 하지 않았음에도 왼쪽 브라우저에서 추가한 server의 data 변화를 알아채고 최신 상태로 업데이트 해준다.</p>
<p>여기까지 봤을때 코드를 몇줄 작성하지도 않았는데 server state 관리가 되고 그에맞게 화면이 랜더링 되는 상황이 꽤 신기하다.</p>
<p>다음장에서 useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider에 대해 알아보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[엑셀(.xlsx) 데이터를 firebase database에 넣기]]></title>
            <link>https://velog.io/@g_c0916/%EC%97%91%EC%85%80.xlsx-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-firebase-database%EC%97%90-%EB%84%A3%EA%B8%B0</link>
            <guid>https://velog.io/@g_c0916/%EC%97%91%EC%85%80.xlsx-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-firebase-database%EC%97%90-%EB%84%A3%EA%B8%B0</guid>
            <pubDate>Fri, 15 Sep 2023 12:48:14 GMT</pubDate>
            <description><![CDATA[<p>로또번호를 이용한 토이프로젝트를 하면서 전회차 당첨번호 리스트가 필요했는데
동행복권 홈페이지에서 데이터를 엑셀파일로 받을 수 있었다.
해당 엑셀파일의 내용을 js에서 사용할 수 있게끔 데이터로 바꿀 수 있지않을까 찾아봤다.</p>
<p>먼저 xlsx 모듈을 설치해준다.</p>
<pre><code>$ npm install xlsx</code></pre><p>사용방법</p>
<pre><code>모듈 사용

const xlsx = require(&#39;xlsx&#39;);

or

import * as xlsx from &#39;xlsx&#39;;



* 파일읽기
const fileData = xlsx.read(data, read_opts);

* json 형태로 데이터 표시
const data = xlsx.utils.sheet_to_json(sheet);
</code></pre><hr>
<pre><code class="language-js">pages/addServerData.js

import React from &quot;react&quot;;
import db from &quot;../firebaseConfig&quot;;
import * as XLSX from &quot;xlsx&quot;;
import { doc, setDoc } from &quot;firebase/firestore&quot;;


export default function AddServerData() {
/* 
FlieReader 객체를 이용하여 업로드된 파일을 읽을 수 있다.

FileReader.readAsArrayBuffer()
  - 지정된 내용을 읽기 시작한다. 완료되면 속성에 파일의 데이터를 나타내는 result가 포함된다 

fileReader.onload
 - load이벤트 핸들러
*/ 
  const readExcel = async (file) =&gt; {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onload = async (e) =&gt; {
      if (!e.target) return;
      const bufferArray = e.target.result;
      const fileInformation = XLSX.read(bufferArray, {
        type: &quot;buffer&quot;,
        cellText: false,
        cellDates: true,
      });

      const sheetName = fileInformation.SheetNames[0];
      const rawData = fileInformation.Sheets[sheetName];
      const data = XLSX.utils.sheet_to_json(rawData);
      try {
        for (let i = 0; i &lt; data.length; i++) {
          const {
            drwNo,
            date,
            drwNo1,
            drwNo2,
            drwNo3,
            drwNo4,
            drwNo5,
            drwNo6,
          } = data[i];
          await setDoc(
            doc(db, &quot;firstNums&quot;, &quot;data&quot;),
            {
              [drwNo]: [drwNo1, drwNo2, drwNo3, drwNo4, drwNo5, drwNo6],
            },
            { merge: true }
          );
        }
      } catch (error) {
        console.error(&quot;Error posting data to Firestore:&quot;, error);
      }
    };
  };

  const handleExcelFileChange = (e) =&gt; {
    if (!e.target.files) return;
    const file = e.target.files[0];
    readExcel(file);
  };

  return (
    &lt;&gt;
      &lt;div className=&quot;flex-center&quot;&gt;
        &lt;input type=&quot;file&quot; onChange={(e) =&gt; handleExcelFileChange(e)} /&gt;
      &lt;/div&gt;
      &lt;style jsx&gt;{`
        .flex-center {
          display: flex;
          width: 100%;
          justify-content: center;
        }
      `}&lt;/style&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>addServerData 페이지에서 엑셀파일을 업로드하게되면 엑셀파일의 데이터를 읽어 json 형태로 데이터를 변환해준다.</p>
<p>변환된 데이터를 이용해 firebase database에 각 회차별로 데이터를 넣어주었다.
database에 데이터가 덮어 씌워지지않도록 <strong>{merge : true}</strong>  옵션을 추가 해줬다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Markdown파일로 post 업데이트 하기]]></title>
            <link>https://velog.io/@g_c0916/Next.js-Markdown%ED%8C%8C%EC%9D%BC%EB%A1%9C-post-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@g_c0916/Next.js-Markdown%ED%8C%8C%EC%9D%BC%EB%A1%9C-post-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 14 Sep 2023 14:48:43 GMT</pubDate>
            <description><![CDATA[<p>next 버전은 13입니다.</p>
<h3 id="mdxremote">MDXRemote</h3>
<p>next-mdx-remote를 활용하여 markdown 파일을 HTML 코드로 변환해준다.</p>
<pre><code>npm install next-mdx-remote
npm install gray-matter
</code></pre><p>위의 두개를 설치해주고</p>
<p>먼저, 동적라우팅으로 post 별로 페이지를 렌더링할 수 있도록 
app/post/[id]/page.tsx 경로로 폴더와 파일을 만들어주자.</p>
<pre><code class="language-js">// app/post/[id]/page.tsx

export default function Page({ params }) {
  return &lt;div&gt;My Post&lt;/div&gt;;
}</code></pre>
<h3 id="route-example-url-params">Route Example URL params</h3>
<pre><code>app/blog/[id]/page.js    </code></pre><p>/blog/1  =&gt;     { id: &#39;1&#39; }
/blog/2  =&gt;     { id: &#39;2&#39; }
/blog/3     =&gt;     { id: &#39;3&#39; }</p>
<p><a href="http://localhost:3000/post/1">http://localhost:3000/post/1</a> 로 접속할 경우 
params의 id 값은 1</p>
<p><a href="http://localhost:3000/post/2">http://localhost:3000/post/2</a> 의 경우 
id값은 2가 된다.</p>
<p>--</p>
<hr>
<p>gray-matter를 사용하면 md파일의 내용을 데이터로 쉽게 변환이 가능하다.</p>
<pre><code class="language-md">
// md파일
---
title: 글제목
date: 2023-09-12
tag: [&quot;md&quot;, &quot;gray-matter&quot;]
---
(블로그 글 내용) 




// gray-matter 사용시 데이터
{
  content: &#39;글내용&#39;,
  data: { 
    title: &#39;글제목&#39;, 
    date: &#39;2023-09-12&#39; 
    tag: &#39;[&quot;md&quot;, &quot;gray-matter&quot;]&#39; 
  }
}</code></pre>
<hr>
<h3 id="generatestaticparams">generateStaticParams</h3>
<p>여기서 SSG를 구현하기 위해서는 예전 getStaticPaths 함수 같은 게 필요한데</p>
<p>getStaticPaths처럼 서버사이드 라우팅의 모든 경우의 수를 지정할 필요가 있다.</p>
<p>Next.js 13 버전에서는 generateStaticParams 함수를 사용하면 된다.</p>
<hr>
<blockquote>
<p>위의 내용들을 바탕으로 app/post/[id]/page.tsx를 다시 한번 정리해주면 다음과 같다.</p>
</blockquote>
<pre><code class="language-js">// app/post/[id]/page.tsx

import fs from &quot;fs&quot;;
import path from &quot;path&quot;;
import matter from &quot;gray-matter&quot;;

import { MDXRemote } from &quot;next-mdx-remote/rsc&quot;;

// fs모듈을 이용해 최상위 디렉토리의 posts폴더 파일들을 불러와 블로그 경로의 경우를 지정한다.

export async function generateStaticParams() {
  const files = fs.readdirSync(path.join(&quot;posts&quot;));

  const paths = files.map((filename) =&gt; ({
    id: filename.replace(&quot;.mdx&quot;, &quot;&quot;),
  }));

  return paths;
}


// params로 받은 post id를 전달하고 md파일내용을 gray-matter로 데이터로 바꾼다
function getPost({ id }: { id: string }) {
  const markdownFile = fs.readFileSync(
    path.join(&quot;posts&quot;, decodeURIComponent(id) + &quot;.mdx&quot;),
    &quot;utf-8&quot;
  );

  const { data: frontMatter, content } = matter(markdownFile);

  return {
    frontMatter,
    id,
    content,
  };
}

export default function Post({ params }: any) {
  const props = getPost(params);

  return (
    &lt;&gt;
      &lt;head&gt;
        &lt;title&gt;{props.frontMatter.title}&lt;/title&gt;
        &lt;meta name=&quot;description&quot; content={props.frontMatter.description} /&gt;
        &lt;meta name=&quot;keyword&quot; content=&quot;page keyword&quot; /&gt;
      &lt;/head&gt;
      &lt;article style={{ color: &quot;#e1e1e1&quot;, padding: &quot;20px 0&quot; }}&gt;
        &lt;h1&gt;{props.frontMatter.title}&lt;/h1&gt;
        &lt;MDXRemote source={props.content} /&gt;
      &lt;/article&gt;
    &lt;/&gt;
  );
}</code></pre>
<hr>
<h3 id="폴더경로">폴더경로</h3>
<center>
    <img src="https://velog.velcdn.com/images/g_c0916/post/8f939856-3edb-4dd2-aff2-9fb94be9dd86/image.png" alt="경로이미지" width="300">
</center>
]]></description>
        </item>
        <item>
            <title><![CDATA[검색엔진 등록]]></title>
            <link>https://velog.io/@g_c0916/%EA%B2%80%EC%83%89%EC%97%94%EC%A7%84-%EB%93%B1%EB%A1%9D</link>
            <guid>https://velog.io/@g_c0916/%EA%B2%80%EC%83%89%EC%97%94%EC%A7%84-%EB%93%B1%EB%A1%9D</guid>
            <pubDate>Wed, 21 Jun 2023 18:31:13 GMT</pubDate>
            <description><![CDATA[<p>Next.js로 프로젝트를 다 만들고 배포를 하고나서 몇일이 지나도 검색이 되지않았다. 크롤링이 오래 걸리는건가 싶었다가 뭔가 아닌거같아서 찾아보니 검색엔진등록을 해야된다고 한다.</p>
<p>대표적으로 google, naver에 검색엔진을 등록해보기로 했다.</p>
<p>google은 <a href="https://search.google.com/search-console">https://search.google.com/search-console</a>
사이트에 들어가서 하면된다.</p>
<p>로그인을하면 소유권을 도메인으로 등록할지 URL 접두어로 등록할지 뜬다.</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/0b554482-442f-49d1-8319-870153901275/image.png" alt=""></p>
<p>도메인 소유권 등록은 하위 도메인을 포함한 전체에 대한 소유권을 확인하겠다는 뜻이다.</p>
<p>random.com 이라는 도메인을 가지고 있으면 하위 도메인 a.random.com, b.random.com 등을 한번에 모두 내 소유로 확인하게 된다.</p>
<p>URL 접두어 소유권 등록은 해당하는 주소에 대한 소유권을 확인하겠다는 뜻이다.</p>
<p>예를 들면 <a href="https://random.com">https://random.com</a> 사이트를 가지고 있으면 해당 주소에 대한 소유권을 확인하게 된다. <a href="http://random.com%EB%A1%9C%EB%8F%84">http://random.com로도</a> 접속이 가능하다면 각각 등록해줘야한다.</p>
<p>나는 vercel로 배포를 했고 URL 접두어 방식이 간편해보여서 해당 방식으로 진행했다.
화면 오른쪽에 내가 배포한 사이트 주소를 입력하면</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/7d0c8638-3ed9-4366-9a0e-c87354a91c46/image.png" alt=""></p>
<p>html head태그에 넣을 수 있는 meta 태그가 나오게 되는데 해당 코드를 내 프로젝트의 head 태그에 넣어주고 배포 후 확인 진행한다.</p>
<hr>
<p>naver는 <a href="https://searchadvisor.naver.com">https://searchadvisor.naver.com</a> 사이트에 접속 후 로그인을 하고
웹 마스터 도구로 들어가준다.</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/cb262301-49f9-4329-be78-e246cd0668c0/image.png" alt=""></p>
<p>그러면 사이트등록 화면이 나오는데 내 사이트 주소를 입력해준다
<img src="https://velog.velcdn.com/images/g_c0916/post/18ba9cb8-ae03-4603-9514-aee1e93a0c40/image.png" alt=""></p>
<p>google search console에서와 마찬가지로 meta태그를 준다. 해당 태그를 내 프로젝트 head 태그에 넣어주고 확인을 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/f6998765-20df-4691-9626-876df5f81d5e/image.png" alt=""></p>
<p>색인 생성에 몇일정도 걸린다고 하는데 블로그 작성 시점에서는 아직 완료가 되지않아서 좀 더 기다려봐야 결과를 알 수 있을것같다.
검색엔진등록이 모두 완료되면 구글에 쉽게 확인해보는 방법은
site:<a href="https://random-num.vercel.app">https://random-num.vercel.app</a> 와같이 site:[내 웹사이트주소]로 검색하면 검색결과가 나온다고해서 생각날때마다 확인을 한번씩 하고있다.</p>
<hr>
<p>추가로 웹 주소를 카카오 대화방에 공유하면 썸네일이 뜨게 되는데 해당 내용을 meta 태그에서 og(오픈그래프)를 통해 수정할 수 있다.</p>
<pre><code class="language-html">&lt;meta property=&quot;og:title&quot; content=&quot;로또 번호 랜덤 생성&quot; /&gt;
&lt;meta property=&quot;og:url&quot; content=&quot;https://random-num.vercel.app/&quot; /&gt;
&lt;meta property=&quot;og:type&quot; content=&quot;website&quot; /&gt;
&lt;meta property=&quot;og:image&quot; content=&quot;./ogImage.png&quot; /&gt;
&lt;meta
      property=&quot;og:description&quot;
      content=&quot;제외하고 싶은 숫자를 빼고 로또 번호를 랜덤으로 생성할 수 있습니다.&quot;
      /&gt;</code></pre>
<center>
  <img src="https://velog.velcdn.com/images/g_c0916/post/835c496d-        7253-4807-9e53-bf491832ed64/image.jpeg" width="400px" /> 
</center>

<p>해당 형식으로 썸네일 이미지를 변경하고 웹사이트 설명등을 추가할 수 있다.</p>
<h3 id="추가-참고">추가 참고</h3>
<pre><code class="language-html">&lt;!-- 필수 og 태그 --&gt;
&lt;meta property=&quot;og:type&quot; content=&quot;website&quot;&gt;
&lt;meta property=&quot;og:url&quot; content=&quot;https://example.com/page.html&quot;&gt;
&lt;meta property=&quot;og:title&quot; content=&quot;Content Title&quot;&gt;
&lt;meta property=&quot;og:image&quot; content=&quot;https://example.com/image.jpg&quot;&gt;
&lt;meta property=&quot;og:description&quot; content=&quot;Description Here&quot;&gt;
&lt;meta property=&quot;og:site_name&quot; content=&quot;Site Name&quot;&gt;
&lt;meta property=&quot;og:locale&quot; content=&quot;en_US&quot;&gt;

&lt;!-- 필수는 아니지만, 추천하는 og 태그 --&gt;
&lt;meta property=&quot;og:image:width&quot; content=&quot;1200&quot;&gt;
&lt;meta property=&quot;og:image:height&quot; content=&quot;630&quot;&gt;

&lt;!-- naver 블로그, 카카오톡 미리보기 설정 --&gt;
&lt;meta property=&quot;og:title&quot; content=&quot;콘텐츠 제목&quot; /&gt; 
&lt;meta property=&quot;og:url&quot; content=&quot;웹페이지 URL&quot; /&gt;
&lt;meta property=&quot;og:type&quot; content=&quot;웹페이지 타입(blog, website 등)&quot; /&gt;
&lt;meta property=&quot;og:image&quot; content=&quot;표시되는 이미지&quot; /&gt; 
&lt;meta property=&quot;og:title&quot; content=&quot;웹사이트 이름&quot; /&gt; 
&lt;meta property=&quot;og:description&quot; content=&quot;웹페이지 설명&quot; /&gt;

&lt;!-- 트위터 미리보기 --&gt;
&lt;meta name=&quot;twitter:card&quot; content=&quot;트위터 카드 타입(요약정보, 사진, 비디오)&quot; /&gt; 
&lt;meta name=&quot;twitter:title&quot; content=&quot;콘텐츠 제목&quot; /&gt; 
&lt;meta name=&quot;twitter:description&quot; content=&quot;웹페이지 설명&quot; /&gt; 
&lt;meta name=&quot;twitter:image&quot; content=&quot;표시되는 이미지 &quot; /&gt;

&lt;!-- 모바일 앱 미리보기 --&gt;
&lt;--iOS--&gt;
&lt;meta property=&quot;al:ios:url&quot; content=&quot; ios 앱 URL&quot; /&gt;
&lt;meta property=&quot;al:ios:app_store_id&quot; content=&quot;ios 앱스토어 ID&quot; /&gt; 
&lt;meta property=&quot;al:ios:app_name&quot; content=&quot;ios 앱 이름&quot; /&gt; 

&lt;!-- Android --&gt;
&lt;meta property=&quot;al:android:url&quot; content=&quot;안드로이드 앱 URL&quot; /&gt;
&lt;meta property=&quot;al:android:app_name&quot; content=&quot;안드로이드 앱 이름&quot; /&gt;
&lt;meta property=&quot;al:android:package&quot; content=&quot;안드로이드 패키지 이름&quot; /&gt; 
&lt;meta property=&quot;al:web:url&quot; content=&quot;안드로이드 앱 URL&quot; /&gt;

&lt;!-- 출처 https://seons-dev.tistory.com/entry/html-Meta-OG --&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] useContext]]></title>
            <link>https://velog.io/@g_c0916/Next.js-useContext</link>
            <guid>https://velog.io/@g_c0916/Next.js-useContext</guid>
            <pubDate>Wed, 21 Jun 2023 17:36:01 GMT</pubDate>
            <description><![CDATA[<p>Next.js로 토이프로젝트를 만들면서 전역상태관리가 필요해서 useContext를 사용해봤다.</p>
<p>평소에 Redux-toolkit만 사용했는데 이번엔 관리할 데이터가 적어서 굳이 redux를 사용하는것보단 react에서 제공되는 useContext를 사용하는게 간편하기도하고 코드도 간단해 보여서 사용하게 됐다.</p>
<p>내가 관리할 데이터는 [[1,2,3,4], [5,6,7,8]]과 같은 number 배열로 이루어진 배열이었다.</p>
<pre><code>createContext를 import해서 context를 만들어준다. 초기값을 지정해주고 context에 전달한다.</code></pre><pre><code class="language-tsx">// context/ResultContext.tsx
import { createContext } from &quot;react&quot;;

interface ResultContextType {
  numbers: number[][];
  addNumbers: (arr: number[]) =&gt; void;
  resetNumbers: () =&gt; void;
  deleteNumbers: (idx: number) =&gt; void;
}

const ResultContext = createContext&lt;ResultContextType&gt;({
  numbers: [],
  addNumbers: (arr: number[]) =&gt; {},
  resetNumbers: () =&gt; {},
  deleteNumbers: (idx: number) =&gt; {},
});

export default ResultContext;</code></pre>
<hr>
<pre><code> _app.tsx에서 useState를 이용해 상태값과 필요한 업데이트 함수를 작성해주고 
 ResultContext.Provider의 value에 전달해주면 된다. 
 context provider를 통해 value를 전달해주게되면 
 해당 컴포넌트의 하위컴포넌트에서 전역상태값에 접근할 수 있다.
_app.tsx에서 구성했기 때문에 모든 페이지에서 접근이 가능하다.</code></pre><pre><code class="language-tsx">// _app.tsx

import ResultContext from &quot;../components/context/ResultContext&quot;;
import { useState } from &quot;react&quot;;
import { AppProps } from &quot;next/app&quot;;

function App({ Component, pageProps }: AppProps) {

const [numbers, setNumbers] = useState&lt;number[][]&gt;([]);

const addNumbers = (arr: number[]) =&gt; {
  setNumbers((prev) =&gt; [arr, ...prev]);
};

const resetNumbers = () =&gt; {
  setNumbers([]);
};

const deleteNumbers = (idx: number): void =&gt; {
  setNumbers(numbers.filter((el, index) =&gt; idx !== index));
};


...

  return (
      &lt;ResultContext.Provider
              value={{ numbers, addNumbers, resetNumbers, deleteNumbers }}
            &gt;
          &lt;Component {...pageProps} /&gt;
      &lt;/ResultContext.Provider&gt;
  )
};

export default App;
</code></pre>
<hr>
<pre><code>필요한곳에서 useContext를 이용하면 상태값을 불러올 수 있다. 상태값을 업데이트 함수도 마찬가지로 
const { addNumbers, deleteNumbers } = useContext(resultContext);
와 같이 불러와서 사용하면 된다.</code></pre><pre><code class="language-tsx">import { useContext } from &quot;react&quot;;
import resultContext from &quot;../components/context/ResultContext&quot;;
import Num from &quot;./Num&quot;;

function ResultList({ isSearch, search, searchId, findDrwNo }: Props) {
  const { numbers } = useContext(resultContext);

  return (
    &lt;&gt;
      {numbers.map((el, idx) =&gt; (
        &lt;div className=&quot;result-number&quot;&gt;
          {el.map((num, idx) =&gt; (
            &lt;Num key={idx} num={num} row={idx} /&gt;
          ))}
        &lt;/div&gt;
      ))}
    &lt;/&gt;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 접근성에 대해서]]></title>
            <link>https://velog.io/@g_c0916/%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@g_c0916/%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Mon, 22 May 2023 11:41:25 GMT</pubDate>
            <description><![CDATA[<p>웹의 창시자 팀 버너스리(Tim Berners-Lee)는 &quot;웹의 힘은 그것의 보편성에 있다. 장애에 구애없이 모든 사람이 접근할 수 있는 것이 필수적인 요소이다.&quot; 라고 했다.</p>
<p>즉, 제약을 가진 사용자(장애인, 노인 등), 혹은 어떠한 기술환경에서도 전문적인 능력 없이 웹 사이트에서 제공하는 모든 정보에 접근할 수 있도록 보장하는 것이다.</p>
<hr>
<h3 id="웹-접근성">웹 접근성</h3>
<p>장애를 가진 사람이든 가지지 않은 사람이든 모두가 웹 사이트를 이용할 수 있게 하는 방식을 의미한다.</p>
<p>장애를 가진 사람들 에게는 스크린 리더기를 이용하여 웹을 이용하는 것이 대표적인 예시다.</p>
<p>맥에서는 voiceover를 이용하여 스크린 리더를 이용할 수 있다.</p>
<h3 id="웹-접근성-예시">웹 접근성 예시</h3>
<ul>
<li><p>의미가 있는 이미지의 경우 의미나 기능이 동일한 대체 텍스트를 제공해야한다(alt=&quot;글쓰기 &quot;).</p>
</li>
<li><p>의미가 없는 이미지(글머리 기호, 장식용 배경 이미지 등)의 경우에는 대체 텍스트를 공백(alt=&quot;&quot;)으로 제공해야 한다.</p>
</li>
<li><p>동영상, 음성 등 멀티미디어 콘텐츠 제공 시에는 청각 장애인 등이 해당 콘텐츠를 이해 할 
수 있도록 자막, 원고 또는 수화를 제공해야 한다.</p>
</li>
<li><p>색상을 배제하여도 원하는 내용을 전달 할 수 있도록, 색상 이외에도 명암이나 패턴 등으로 콘텐츠 구분이 가능해야 한다.</p>
</li>
<li><p>프레임에 대한 적절한 제목 제공(내용이 없는 빈 프레임의 경우에도 title=&quot;빈 프레임&quot; 과 같이 제공해야한다)</p>
</li>
</ul>
<p>이외에도 다양한 웹 접근성 향상을 위한 표준 기술 가이드 라인이 존재한다.</p>
<hr>
<h3 id="웹-표준">웹 표준</h3>
<p>사용자가 어떤 브라우저나 기기를 사용하더라도 내용을 동일하게 볼 수 있도록 하는 것이 웹 표준이다. 표준화 단체인 W3C가 권고한 표준안에 따라 웹사이트를 작성할 때 이용하는 HTML, CSS, JavaScript 등에 대한 규정이 담겨 있다.</p>
<p>국내 웹은 특정 브라우저(IE)와 사용자 등의 이용환경, 비표준 페이지, 과도한 플러그인 사용으로 모든 사용자들에게 운영체제 및 웹 브라우저 등의 정보접근 제약이 있다. 따라서 브라우저의 종류나 버전에 상관없이 모든 사용자들이 동일한 웹사이트를 볼 수 있도록 웹 표준기술 작업이 필요하며, 웹 표준 준수는 웹 접근성 준수를 위한 핵심이라고 할 수 있다. (즉, 웹 접근성이 더 큰 개념이며, 웹 표준은 웹 접근성을 구현하기 위한 부분적 요소다)</p>
<h3 id="웹-표준으로-얻는-이득">웹 표준으로 얻는 이득</h3>
<p>1) CSS와 HTML이 분리되어 유지보수에 들어가는 시간이 단축되고, 불필요한 마크업이 최소화되어 페이지 로딩속도가 향상된다.</p>
<p>2) 오래된 브라우저에서도 컨텐츠가 적절하게 표시되고 호환성과 운용성이 확보된다.</p>
<p>3) 논리적이고 효율적으로 작성된 웹 문서는 코드의 양이 줄어 파일 크기가 줄고 서버부담의 감소로 이어질 수 있다.</p>
<p>4) 스크린리더기 등 보조공학 기기 사용자들이 조금 더 정확한 정보를 얻을 수 있도록 돕는다.</p>
<hr>
<h3 id="시맨틱-마크업">시맨틱 마크업</h3>
<p>시멘틱 태그는 의미를 가진 태그이다.
<code>&lt;header&gt;, &lt;footer&gt;, &lt;nav&gt;, &lt;main&gt;, &lt;section&gt;, &lt;article&gt;</code> 과 같은 태그들이 있다.</p>
<blockquote>
<p>시맨틱 태그 사용 이유</p>
</blockquote>
<ul>
<li><ol>
<li>웹 접근성에 효율적
시각 장애가 있는 사용자가 스크린 리더기로 페이지를 탐색할 때 의미론적 마크업을 푯말로 사용할 수 있다.</li>
</ol>
</li>
<li><ol start="2">
<li>코드 가독성에 따른 유지 보수의 용이
끊임없는 div 들을 탐색하는 것보다, 의미 있는 코드 블록을 찾는 것이 훨씬 쉽다.</li>
</ol>
</li>
<li><ol start="3">
<li>검색엔진 최적화(SEO)에 유리
의미가 명확한 태그는 크롤러에게 알맞은 정보를 가져갈 수 있도록 도움을 준다.</li>
</ol>
</li>
</ul>
<hr>
<p>사용중인 브라우저가 웹 표준을 얼만큼 준수하는지 확인 (<a href="http://html5test.com">http://html5test.com</a>)
웹 접근성을 체험해 볼 수 있는 사이트 (<a href="https://nax.naver.com/index">https://nax.naver.com/index</a>)</p>
<hr>
<ul>
<li>크로스 브라우징 : 한개의 브라우저가 아닌 여러 브라우저에서 동등한 정보를 보여주는 것</li>
<li>웹 표준 : W3C 등의 표준화 기구에서 정의 해준 명세에 맞게 마크업 하는 것.</li>
<li>시맨틱마크업 : 요소의 정의에 맞게 사용하는 것</li>
<li>웹 접근성 : 이용자, 이용자의 장비에 관계없이 이용할 수 있는 웹 사이트를 구성하는 것(시각 장애인 등도 이용할 수 있으며, 다양한 pc, 장비에서도 접근 할 수 있는 웹 사이트)</li>
</ul>
<hr>
<p>책을 보다가 그동안 웹 접근성 부분을 가볍게 지나치지 않았나하는 생각에 알아보았다. 내가 만드는 웹사이트를 이용하는 사용자들에게 편리함을 제공하는것은 중요하다. 그러나 그 사용자들이 다양한 사람들이라는것을 알아둬야한다. 모두가 제약없이 쉽게 이용할 수 있도록 접근성에 대한 고민도 많이 해봐야하는것 같다. 특히나 누군가에게는 공평한 정보를 제공해주지 못할수도, 누군가에겐 해가 될수도있다(광과민성발작증세를 가진 사람들을 위해 깜박이는 콘텐츠 제공 시 사전에 회피할수 있는 수단을 제공해야한다)는 것을 알아두자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 타이핑효과 setTimeout]]></title>
            <link>https://velog.io/@g_c0916/React-%ED%83%80%EC%9D%B4%ED%95%91%ED%9A%A8%EA%B3%BC-setTimeout</link>
            <guid>https://velog.io/@g_c0916/React-%ED%83%80%EC%9D%B4%ED%95%91%ED%9A%A8%EA%B3%BC-setTimeout</guid>
            <pubDate>Mon, 15 May 2023 07:07:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>커스텀훅으로 useTypingEffect 만들기</p>
</blockquote>
<pre><code class="language-js">import { useState, useEffect } from &quot;react&quot;;

function useTypingEffect(text, delay) {
  const [typedText, setTypedText] = useState(&quot;&quot;);

  useEffect(() =&gt; {
    let currentIndex = 0;
    const intervalId = setInterval(() =&gt; {
      const currentLetter = text[currentIndex];
      setTypedText((prevTypedText) =&gt; prevTypedText + currentLetter);
      currentIndex++;
      if (currentIndex === text.length) {
        clearInterval(intervalId);
      }
    }, delay);

    return () =&gt; {
      clearInterval(intervalId);
      setTypedText(&quot;&quot;);
    };
  }, [text, delay]);

  return typedText;
}

export default useTypingEffect;</code></pre>
<p>text와 delay를 전달받고 setInterval을 이용해 delay 마다 빈문자열에 text를 한글자씩 붙여주는 방식이다.</p>
<hr>
<h3 id="setstate-함수의-인자에-콜백함수-사용">setState 함수의 인자에 콜백함수 사용</h3>
<pre><code class="language-js">setState((prevState) =&gt; {
    return()
});</code></pre>
<p>useState 세터 함수의 인자로 콜백함수를 넣어주게 되면</p>
<ol>
<li>콜백함수의 인자는 이전 상태값을 가지게 된다.</li>
<li>콜백함수의 return 값에는 새로운 state를 지정할 수 있다.</li>
</ol>
<h3 id="콜백-형식의-usestate-초기값-지정">콜백 형식의 useState 초기값 지정</h3>
<pre><code class="language-js">useState(() =&gt; {
    return()
});</code></pre>
<p>useState를 콜백 형식으로 초기값을 지정하면 첫 렌더링 시에만 한 번 콜백을 실행해서 초기값을 만들고, 그 이후에는 콜백함수를 실행하지 않는다.</p>
<p>따라서 useState 초기값 지정시 오래걸리는 작업이 있을때 사용해주면 좋다.
단, 콜백 함수가 리턴할 때까지 React가 렌더링을 하지 않고 기다리게 된다.</p>
<hr>
<h3 id="사용예시">사용예시</h3>
<pre><code class="language-js">const TerminalLine = React.memo(function TerminalLine({ text }) {
  let typingText = useTypingEffect(text, 60);

  return (
    &lt;div&gt;
      {typingText}
    &lt;/div&gt;
  );
});

export default TerminalLine;</code></pre>
<p>나의 경우 타이핑을 한번만 하고 끝내는것이 아니라 여러문장을 나타내야했는데 이전 문장이 완료되는 시점에 다음줄의 타이핑이 시작되어야했다.
그렇기 때문에 useTypingEffect을 이용해 첫번째 타이핑이 끝나는 시간을 다음줄의 시작 시간으로 설정하고 그 다음줄은 이전 모든 줄의 타이핑이 끝나는 시간으로 시작 시간을 설정한다.</p>
<pre><code class="language-js">
  useEffect(() =&gt; {
    let timeoutId;
    const arrTimeoutId = [];

    const startTyping = () =&gt; {
      let delay = 500;
      for (let i = 0; i &lt; text.length; i++) {
        const textLine = text[i];
        timeoutId = setTimeout(() =&gt; {
          if (i &gt; 0) {
            // 완료된 이전 문장들을 담을 state
            setCompleteTyping(text.slice(0, i));
          }
          // useTypingEffect에 전달할 text state
          setTypingText(textLine);
        }, delay);

        arrTimeoutId.push(timeoutId);
        // 이전 문장의 길이 * 타이핑 속도에 여유시간(500ms)를 더해 다음 타이핑 시작 시간을 조절한다.
        delay += text[i].length * 60 + 500;
      }
    };


    startTyping();


    return () =&gt; {
      // clearTimeout(timeoutId);
      arrTimeoutId.forEach((id) =&gt; clearTimeout(id));
    };
  }, [text]);</code></pre>
<p><strong>참고</strong>
setTimeout을 이렇게 여러번 사용해본적이 처음이라 clear를 잘못하고 있었다는걸 몰랐었다. 타이핑 이펙트 주는 화면이 여러개라 전환을 하면 이전 화면 텍스트가 잠깐 나타났다가 사라지곤 했는데 마지막 주석처리 한것처럼 컴포넌트 언마운트 시 타이머를 클리어 하도록 timeoutId 하나만 clearTimeout 함수의 인자로 전달하면 제일 마지막 setTimeout만 클리어된다.(for문에서 setTimeout 호출할때마다 반환하는 id를 timeoudId 변수에 할당하는데 for문이 끝나고 난 다음에는 제일 마지막 id만 할당되어있는 상태다.)
setTimeout을 호출하면 타이머를 식별하는 id값을 반환하는데 실행 대기중인 모든 타이머 id를 clear해줘야한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[웹성능 최적화] font 최적화]]></title>
            <link>https://velog.io/@g_c0916/%EC%9B%B9%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-font-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@g_c0916/%EC%9B%B9%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-font-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Fri, 05 May 2023 05:44:06 GMT</pubDate>
            <description><![CDATA[<h3 id="최적화-전">최적화 전</h3>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/6bf752da-d1aa-4597-9bf2-743ce71bf0ba/image.gif" alt=""></p>
<h3 id="최적화-후">최적화 후</h3>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/4fd4c0c0-641e-44b5-a6c2-d16a873d9a66/image.gif" alt=""></p>
<p>포트폴리오 첫 화면에 텍스트를 넣게되면서 구글폰트를 사용하게 됐는데 캐싱되기전에 기본 폰트상태로 노출되는 상태를 확인했다.</p>
<p> 먼저 FOUT 와 FOIT에 대해 알아야하는데 </p>
<blockquote>
<p>FOUT는 Flash Of Unstyled Text의 약자로 먼저 텍스트를 보여주고 폰트의 다운이 완료되면 폰트를 적용해 보여준다.</p>
</blockquote>
<blockquote>
<p>FOIT는 Flash Of Invisible Text의 약자로 폰트의 다운이 완료될때까지 텍스트를 노출하지 않다가 다운이 완료되면 폰트를 적용하고 보여준다.</p>
</blockquote>
<h2 id="폰트-적용시점">폰트 적용시점</h2>
<p>먼저 import 하는 구글폰트를 확인해보자</p>
<center>
  <img src="https://velog.velcdn.com/images/g_c0916/post/075657ad-88cb-4446-a294-72ec834f607f/image.png" width="600" height="400"/>
</center>

<p> 잘 보면 폰트의 diplay 속성이 swap 이다. css속성으로 폰트의 적용시점을 컨트롤 할 수 있는데 여러가지 속성이있다.</p>
<blockquote>
</blockquote>
<ul>
<li>auto : 브라우저 기본 동작(IE, edge = FOUT, chorme, safari = FOIT)</li>
<li>block : FOIT (timeout = 3s)</li>
<li>swap : FOUT</li>
<li>fallback : FOIT(timeout=0.1s) 3초 후에도 불러오지 못했을 시 기분 폰트로 유지, 이후 캐시</li>
<li>optional : FOIT(timeout=0.1s) 이후 네트워크 상태에 따라 기본폰트로 유지할지 웹폰트를 적용할지 결정, 이후 캐시</li>
</ul>
<p>display 속성이 swap이라서 FOUT방식으로 동작하고 있었기 때문에 <strong>display 속성</strong>을 <strong>block</strong>으로만 해도 상당히 개선된 모습을 보일것이다. 하지만 개발자도구 network탭에서 slow 3g 환경으로 낮추고 페이지를 로드하면 폰트 리소스 다운이 3초를 넘겨 기본폰트가 잠깐 노출되는 경우도 있다.</p>
<h2 id="폰트-파일-크기-줄이기">폰트 파일 크기 줄이기</h2>
<p>폰트의 파일이 큰 경우 사이즈를 줄여 리소스 다운 시간을 줄일 수 있다. 보통 ttf &gt; woff &gt; woff2 순으로 파일의 크기 차이가 있다. 일단 나는 woff2로 변환해서 preload까지 해보려고한다.</p>
<p>ttf확장자의 폰트를 woff2로 변환하는 방법은  <a href="https://cloudconvert.com/">https://cloudconvert.com/</a> 해당 사이트에서 가능하다.  </p>
<p>폰트를 변환후에 프로젝트에 넣고 font-face로 설정을 해주면 사용이 가능하다.</p>
<pre><code class="language-css">/* app.css */

@font-face {
  font-family: Oswald;
  src: url(&#39;./assets/fonts/Oswald-Bold.woff2&#39;);
  font-weight: 700;
  font-display: block;  
}
</code></pre>
<p>여기까지만해도 파일 크기가 큰 ttf 폰트를 사용중이었다면 눈에띄는 효과가 있다.</p>
<blockquote>
<p>이외에도 몇가지 방법이 더 있는데</p>
</blockquote>
<ul>
<li>몇종류의 글자만 사용하는경우 예를들어 portfoilo 라는 텍스트에만 폰트를 적용한다고 하면 폰트파일에서 p, o, r, t, f, i, l 만 추출해서 파일 사이즈를 줄이는 subset 방법</li>
<li>font-face의 unicode-range 속성으로 텍스트 코드를 지정해 해당 텍스트가 없는 경우 폰트 리소스를 불러오지않도록 설정하는 방법</li>
<li>base64 encode를 통해 data-uri로 변환하여 css에 직접 넣고 css 리소스에 포함하여 불러올 수 있는 방법<br>위 내용에 대해서는 <a href="https://transfonter.org/">https://transfonter.org/</a> 사용방법을 알아보자</li>
</ul>
<hr>
<h2 id="preload">preload</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;link
      rel=&quot;preload&quot;
      href=&quot;Oswald-Bold.woff2&quot;
      as=&quot;font&quot;
      type=&quot;font/woff2&quot;
      crossorigin
    /&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>index.html의 헤더에 link태그로 위와같이 설정을 해주면 되는데(href와 type만 수정하면 된다.) 
이후에 build를 해보면</p>
<center>
  <img src="https://velog.velcdn.com/images/g_c0916/post/a9aabdfd-0f3b-4b0e-a413-f74d37125f98/image.png" width="600" height="400"/>
</center>

<p>폰트가 리소스이기 때문에 그대로 가져다 쓰는게 아니라 폰트 이름에 해시값이 붙는다.</p>
<p>그렇기 때문에 build된 index.html 안에 href 경로를 수정해줘야한다.</p>
<pre><code class="language-html">&lt;link
  rel=&quot;preload&quot;
  href=&quot;./static/media/Oswald-Bold.e7fa7e72b7c318133311.woff2&quot;
  as=&quot;font&quot;
  type=&quot;font/woff2&quot;
  crossorigin
/&gt;</code></pre>
<p>이후 개발자도구 performance탭에서 새로고침을 해보면
<img src="https://velog.velcdn.com/images/g_c0916/post/376f31c6-24c0-4d3e-9a67-6d3b733523d2/image.png" alt=""></p>
<p>html파일이 불리고 js파일보다도 먼저 font가 불리는것을 확인 할 수 있다.
<img src="https://velog.velcdn.com/images/g_c0916/post/68e60ffd-fa04-40c7-a946-9642d63bc703/image.png" alt=""></p>
<p>여기서 중요한것은 html 파일에 link 태그만 넣어도 호출은 하는데 폰트의 경로와 파일이름이 일치해야 한다는것이다. 그런데 build 할때마다 index.html 파일을 수정해주는것은 무리가 있기 때문에 웹팩 설정을 통해 font를 preload 하도록 구성 해줄 수 있는 방법이 있는데</p>
<p>현재 CRA를 사용하기 때문에 웹팩을 직접 커스텀 할 수 없어 웹팩을 커스텀 할 수 있는 react-wired 라는 라이브러리를 설치 하여 웹팩 설정을 override 할 수 있는 환경을 구성하고 preload를 할 수 있게 도와주는 preload-webpack-plugin을 설치하여 세팅해주는 방법이 있다.</p>
<p>(해당 방법은 추후에 업데이트 예정. 포트폴리오에 설치한 라이브러리중 하나와 react-wired가 충돌을 일으켜서 설정을 아직 못했다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버 배포(EC2) ]]></title>
            <link>https://velog.io/@g_c0916/%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%ACEC2</link>
            <guid>https://velog.io/@g_c0916/%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%ACEC2</guid>
            <pubDate>Thu, 04 May 2023 09:01:05 GMT</pubDate>
            <description><![CDATA[<center><img src="https://velog.velcdn.com/images/g_c0916/post/6792bfe3-c45e-41ea-a159-9557b30717a3/image.png" width=500px height=400px /></center>


<p>EC2란 아마존 웹 서비스에서 제공하는 클라우드 컴퓨팅 서비스다. 클라우드 컴퓨팅은 인터넷(클라우드)을 통해 서버, 스토리지, 데이터베이스 등의 컴퓨팅 서비스를 제공하는 서비스다. 즉, 아마존에서 가상의 컴퓨터를 한 대 빌리는 것과 같다.</p>
<p>EC2는 컴퓨터를 한 대 빌리는 것이므로 컴퓨터로 할 수 있는 모든 일을 할 수 있다. 빌린 컴퓨터는 직접 사용하는 컴퓨터와 다르게 아마존이 전 세계에 만들어 놓은 데이터 센터(인프라)에 만들어져 있기 때문에 컴퓨터를 조작하기 위해 네트워크(인터넷)를 통해 컴퓨터를 제어해야 한다는 차이점이 있을 뿐 일반적인 컴퓨터와 다른 점은 없다.</p>
<p>아마존 EC2를 통해서 할 수 있는 가장 기본적인 일은 웹서버를 설치하고 웹 서버를 통해서 사용자가 웹 브라우저를 통해 요청하는 서비스를 제공하는 것이다. 인스턴스는 1대의 컴퓨터를 의미하는 단위이고 AWS에서 컴퓨터를 빌리는 것을 인스턴스를 생성한다고 한다.</p>
<center>
  <img src="https://velog.velcdn.com/images/g_c0916/post/a8af0979-294c-4756-a216-0f4025fe6d3b/image.png" width=800px height=500px />
인스턴스 시작
 </center>


<center>
  <img src="https://velog.velcdn.com/images/g_c0916/post/54bc584d-65d3-4334-9abe-276fd969d68d/image.png" width=800px height=500px />
이 예시에서는 Ubuntu 18버전으로 인스턴스를 생성할거다.
 </center>



<p><img src="https://velog.velcdn.com/images/g_c0916/post/c5e997f3-3fe9-43f4-acb4-a5478454be26/image.png" alt="">
우리는 돈이 부족한 취준생이니까 프리티어로 만들어준다. 참고로 EC2 프리티어는 월 750시간에 대해 과금이 없는데 프리티어 인스턴스를 2개 만들어서 24시간 31일 돌리면 24 * 31 * 2 = 1488시간이 되어서 무료에 해당하는 750시간을 제외한 738시간에 대해 과금이 청구된다.  2개를 돌리고싶다면 12시간씩 / 3개를 돌리고싶다면 8시간씩 시간을 줄여서 돌리거나 인스턴스별로 시간분배해서 750시간만 넘기지 않는다면 인스턴스를 여러개 만들어도 상관은없다.</p>
<p>아래에 검토및 시작을 누르고 시작하기를 누르면
<img src="https://velog.velcdn.com/images/g_c0916/post/bf53d05e-b060-49a1-8ffe-822c09c7d884/image.png" alt=""></p>
<p>인스턴스에 연결할 때 사용 할 키 페어 다운로드 하는것이 나온다 이름을 지어주고 키 페어를 다운로드 해준 다음 인스턴스 시작을 누른다.</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/51629ea9-0aec-46d9-baeb-adc40577994b/image.png" alt=""></p>
<p>다운로드 한 파일은 SSH 통신을 위한 키 페어 중 프라이빗 키가 기록된 파일이다. 해당 키 페어 파일은 EC2 인스턴스에 연결을 할 때 사용하는 암호가 담긴 파일이다. 따라서 pem 파일은 관리에 유의해야 한다.</p>
<p> <img src="https://velog.velcdn.com/images/g_c0916/post/39da8a15-8cb8-4ebc-85d7-f97cb2f91620/image.png" alt=""></p>
<p>인스턴스가 실행 중으로 바뀌고 이제 연결을 해볼 차례이다.  </p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/f472254b-d972-41c1-a85c-f1fac07ab643/image.png" alt=""></p>
<p>SSH 연결을 이용해볼건데 터미널에 좀 전에 다운받았던 .pem 파일이 있는 경로에서 chmod 400 [name].pem으로 권한을 먼저 수정해준다. </p>
<p>그 후 아래 ssh -i &quot;yeollin.pem&quot; <a href="mailto:ubuntu@ec2-52-79-227-43.ap-northeast-2.compute.amazonaws.com">ubuntu@ec2-52-79-227-43.ap-northeast-2.compute.amazonaws.com</a> 명령어를 통해 인스턴스에 접속이 가능할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/b02e9b01-029e-44cf-86b6-92391b7183d3/image.png" alt=""></p>
<p>처음 접속할 때 경고메세지가 나타나는데  해당 메시지는 EC2 인스턴스에 처음 접속할 시 나오는 경고 메시지다. &#39;yes&#39;를 입력하여 다음 과정으로 넘어간다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/7639aaba-8123-4f14-b8a3-82cf0aca4f19/image.png" alt=""></p>
<p>위와같이 나오면 SSH 프로토콜을 이용한 원격접속이 성공적으로 됐다. 이제 명령어를 통해 AWS에서 빌린 가상PC를 사용 할 수 있다.</p>
<p>EC2 인스턴스를 생성하는 것은 가상 PC 한 대를 임대하는 것이고, 컴퓨터 운영체제를 처음 구입하면 필요한 프로그램을 설치해야 하듯이, EC2 인스턴스에 처음 접속하면 서버를 구동하는 데 필요한 개발 환경을 구축하는 것부터 시작해야 한다.</p>
<p>패키지 매니저가 관리하는 패키지의 정보를 최신 상태로 업데이트하기 위해서 아래 명령어를 사용한다.</p>
<pre><code>$ sudo apt update</code></pre><p>업데이트 과정이 끝나면 nvm, node.js를 설치해야 한다. nvm 설치는 NVM GitHub 페이지의 Install &amp; Update Script 부분을 참조하여 진행한다.</p>
<pre><code>$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
$ source ~/.bashrc</code></pre><p>설치 과정이 마무리되면 터미널에 nvm --version 명령어를 입력하여 nvm 설치가 정상적으로 끝났는지 확인한다.</p>
<p>다음으로는 node.js를 설치한다</p>
<pre><code>$ nvm install node</code></pre><p>node.js의 설치가 끝나면 npm 명령어가 정상적으로 입력되지 않는 상황을 방지하기 위해 터미널에</p>
<pre><code>$ sudo apt install npm</code></pre><p>명령어를 입력해서 npm 설치를 진행한다. 위 과정이 모두 끝나면 node.js 기반 서버를 실행하는 데 필요한 개발 환경 구축이 완료된다.</p>
<p>배포할 서버가 들어있는 깃헙 레포지토리 주소를 복사하고, git clone 명령어를 통해 EC2 인스턴스에 해당 프로젝트 클론을 받는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 더 쉽게 kakao-maps-sdk 사용하기 - 검색기능을 개선해보자]]></title>
            <link>https://velog.io/@g_c0916/React%EC%97%90%EC%84%9C-%EB%8D%94-%EC%89%BD%EA%B2%8C-kakao-maps-sdk-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-keyword-category-%EA%B2%80%EC%83%89-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@g_c0916/React%EC%97%90%EC%84%9C-%EB%8D%94-%EC%89%BD%EA%B2%8C-kakao-maps-sdk-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-keyword-category-%EA%B2%80%EC%83%89-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 27 Apr 2023 13:28:25 GMT</pubDate>
            <description><![CDATA[<p>저번 내용에 이어서 검색기능을 개선해 보았다.</p>
<p>일단 내가 원했던건 검색을 통해 특정위치로 이동해 음식점을 검색해보고난 후 지도를 확대해서 드래그 해가며 주변에 있는 다른 음식점 정보도 제공 받고싶다 였다.</p>
<p>먼저 kakao-maps-sdk 설명에서 카테고리 검색에 관한 내용을 못찾았었기 때문에 검색어로 찾는 방향으로 진행을 했었다. 맵을 드래그 했을때는 맵 중앙의 좌표를 주소로 변환하고 재검색하는 방향을 생각해봤다.</p>
<center><img src="https://velog.velcdn.com/images/g_c0916/post/fb59ac12-4bed-4e8e-9b6e-7622f8cf2e5e/image.png" width="600" height="400"/></center>

<p>주소검색으로도 해당 주소 주변 음식점이 잘 나오길래 써먹을 수 있겠구나했다. 먼저 이전 코드를 보면</p>
<pre><code class="language-js">// dragEnd 이벤트로인한 중앙 좌표(position)의 상태가 업데이트 될때마다 실행
useEffect(() =&gt; {
    var geocoder = new kakao.maps.services.Geocoder();
    geocoder.coord2Address(position.lng, position.lat, displayCenterInfo);
  }, [position]);


// 좌표를 이용해 실제 주소를 받는 콜백함수.
  function displayCenterInfo(result, status) {
    if (status === kakao.maps.services.Status.OK) {
      let detailAddr = !!result[0].road_address
        ? result[0].road_address.address_name
        : &quot;&quot;;
      detailAddr += result[0].address.address_name;
      setKeyword(detailAddr);
    }
  }
</code></pre>
<p>위와같은 방법으로 좌표를 주소로 변환해 검색 keyword를 업데이트 해준다. 만약 &quot;기흥역&quot;에 대한 검색을 하면 &quot;기흥역 음식점&quot; 에 대한 결과를 보여주고 맵을 드래그하면 &quot;경기 용인시 기흥구 상갈동 123-22 음식점&quot;에 대한 검색을 하게 된다. </p>
<p>한식이나 일식, 중식 처럼 카테고리를 바꾼다면?</p>
<ul>
<li>&quot;경기 용인시 기흥구 상갈동 123-22 일식&quot;</li>
<li>&quot;경기 용인시 기흥구 상갈동 123-22 한식&quot;</li>
</ul>
<p>이런식으로 검색을 하게 된다.
전혀 안되는것은 아니지만 검색 디폴트가 정확도순 이기 때문에 결과가 없거나 드래그로 맵 이동을 했지만 결과가 똑같은 경우도 많이 발생한다.</p>
<p>무엇보다 세 경우 모두 검색마다 지도 범위를 재설정하게 되는데 검색 결과의 영향인지 맵의 확대 Level이 들쑥날쑥해서 보기 너무 불편했다. 
특히나 좀 더 잦은 검색을 하게 되는 드래그, 메뉴탭의 경우 조금 움직였는데 갑자기 지도가 확대됐다가 축소됐다가 하면서 정신이 없었고, 그러다보니 몇번 드래그하고 메뉴탭을 눌러보면 내가 처음 보고자 했던 위치에서 벗어난 장소를 보고있는 경우도 있다.</p>
<hr>
<p>수정을 해보자</p>
<p>기존 keywordSearch만을 사용했던거에서 categorySearch 추가하고 하나의 함수로 묶었다. (한식, 중식, 양식 같은 type으로도 검색해야해서 총 3가지경우의 검색을 해야하는데 공통부분이 많아서 하나로 묶고 if문으로 분기했다)</p>
<h1 id="search">Search</h1>
<p>먼저 getSearchResult를 호출할때 type(상수)을 인자로 받아 if문을 나눠 검색의 타입과 옵션을 따로 지정했다. 공통옵션은 아래와같다.</p>
<ul>
<li>음식점(&quot;FD6&quot;) 만을 검색하는 category_group_code</li>
<li>중심 좌표. 특정 지역을 기준으로 검색하는 location</li>
</ul>
<pre><code class="language-js">// search
const getSearchResult = (type) =&gt; {
    if (!map) return;
  // 공통 옵션
    const options = {
      category_group_code: &quot;FD6&quot;,
      location: new kakao.maps.LatLng(
        map.getCenter().getLat(),
        map.getCenter().getLng()
      ),
    };
  // category 검색(drag 검색에 사용)
    if (type === CATEGORY) {
      isUseBounds = false;
      const copyOptions = {
        ...options,
        sort: kakao.maps.services.SortBy.DISTANCE,
      };
      ps.categorySearch(&quot;FD6&quot;, kakaoMapCallback, copyOptions);
    }
  // selcet 검색 (메뉴탭 클릭 시 메뉴 종류를 keyword로 검색을 한다)
      else if (type === SELECT) {
      isUseBounds = false;
      ps.keywordSearch(selectType, kakaoMapCallback, options);
    } 
      // keyword 검색 (검색어 입력시 해당 위치를 keyword로 검색한다)
      else {
      isUseBounds = true;
      ps.keywordSearch(searchInputValue, kakaoMapCallback, options);
    }
  };
</code></pre>
<blockquote>
<p>category</p>
</blockquote>
<ul>
<li>ps.categorySearch를 이용한다. </li>
<li>isUseBounds 변수를 이용해 bounds 할지 안할지 정해주었다.(bounds=지도범위재설정 / 이렇게 하지 않고 options 이용해서도 할 수 있을거라 보는데 축소, 확대, drag 등 화면 영역의 좌표가 계속 변하는 상황에 어떻게 쓸 수 있을지 감이 안와서 아예 bounds를 막았다)</li>
<li>확대해서 조금씩 주변을 검색한다는 가정하에 마커가 map 화면 밖으로 벗어나지 않게 sort : distance 옵션을 추가 해주었다(거리순 검색).</li>
</ul>
<blockquote>
<p>select</p>
</blockquote>
<ul>
<li>ps.keywordSearch를 이용한다.</li>
<li>CATEGORY와 마찬가지로 bounds 하지 않는다.</li>
<li>ps.keywordSearch의 검색어로 props로 전달받은 &quot;한식&quot;, &quot;중식&quot;, &quot;일식&quot; 같은 종류를 검색한다. (음식점을 kakao map api에서 제공하는 최대 페이지까지 데이터를 모아서 따로 필터링 하는것보다 기본적으로 음식점만을 검색하기 때문에 검색어로 필터링 하는게 더 낫다고 생각했다)</li>
</ul>
<blockquote>
<p>keyword</p>
</blockquote>
<ul>
<li>SELECT와 거의 동일하고 검색어는 input의 value를 사용한다.</li>
<li>bounds 한다. 예를 들면 기흥역을 보고있다가 강남역을 검색하면 지도 범위를 강남역으로 재설정한다.</li>
</ul>
<hr>
<h1 id="callback">CallBack</h1>
<pre><code class="language-js">// callback
  const kakaoMapCallback = (data, status, _pagination) =&gt; {
    if (status === kakao.maps.services.Status.OK) {
      // 여기 data에 검색 결과가 들어있다.
      // console.log(data)
      getIsMapResult(true);
      const bounds = new kakao.maps.LatLngBounds();
      let markers = [];
      getPlace(data);
      for (var i = 0; i &lt; data.length; i++) {
        markers.push({
          position: {
            lat: data[i].y,
            lng: data[i].x,
          },
          content: data[i].place_name,
          data: data[i],
        });
        bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x));
      }
      setMarkers(markers);
      // bounds!!!!!!!!!!!!!!!!!!!
      if (isUseBounds) map.setBounds(bounds);
    } else if (status === kakao.maps.services.Status.ZERO_RESULT) {
      getIsMapResult(false);
      setMarkers([]);
    } else if (status === kakao.maps.services.Status.ERROR) {
    }
  };
</code></pre>
<p>검색결과를 callback 함수로 받아올 수 있다. 검색결과를 잘 받았을때, 결과가 없을때, 에러가 났을때 처리할 로직을 작성하면 되고, sample에서 크게 달라지는 부분은 없다.
bounds라고 주석 처리한 부분만 참고하면 될듯 하다. isUseBounds = true 일때만 지도 범위를 재설정하게 만들어뒀다.</p>
<hr>
<h1 id="전체코드">전체코드</h1>
<pre><code class="language-js">
// search 
const getSearchResult = (type) =&gt; {
    if (!map) return;
    const options = {
      category_group_code: &quot;FD6&quot;,
      location: new kakao.maps.LatLng(
        map.getCenter().getLat(),
        map.getCenter().getLng()
      ),
    };
    if (type === CATEGORY) {
      isUseBounds = false;
      const copyOptions = {
        ...options,
        sort: kakao.maps.services.SortBy.DISTANCE,
      };
      ps.categorySearch(&quot;FD6&quot;, kakaoMapCallback, copyOptions);
    } else if (type === SELECT) {
      isUseBounds = false;
      ps.keywordSearch(selectType, kakaoMapCallback, options);
    } else {
      isUseBounds = true;
      ps.keywordSearch(searchInputValue, kakaoMapCallback, options);
    }
  };


// callback
  const kakaoMapCallback = (data, status, _pagination) =&gt; {
    if (status === kakao.maps.services.Status.OK) {
      // 여기 data에 검색 결과가 들어있다.
      // console.log(data)
      getIsMapResult(true);
      const bounds = new kakao.maps.LatLngBounds();
      let markers = [];
      getPlace(data);
      for (var i = 0; i &lt; data.length; i++) {
        markers.push({
          position: {
            lat: data[i].y,
            lng: data[i].x,
          },
          content: data[i].place_name,
          data: data[i],
        });
        bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x));
      }
      setMarkers(markers);
      if (isUseBounds) map.setBounds(bounds);
    } else if (status === kakao.maps.services.Status.ZERO_RESULT) {
      getIsMapResult(false);
      setMarkers([]);
    } else if (status === kakao.maps.services.Status.ERROR) {
    }
  };

  useEffect(() =&gt; {
    getSearchResult(CATEGORY);
  }, [map, position]);

  useEffect(() =&gt; {
    getSearchResult(SELECT);
  }, [selectType]);

  const handleSearchButton = () =&gt; {
    getSearchResult();
  };</code></pre>
<hr>
<ul>
<li><p>처음 map이 그려질때 아무 음식점 리스트가 없으면 심심해보여서 우리동네 좌표를 기본값으로 categorySearch하여 리스트를 보여주게끔 해놨다.</p>
</li>
<li><p>dragEnd 이벤트로 position state가 변경되면 마찬가지로 categorySearch를 한다.</p>
</li>
<li><p>메뉴탭에 음식종류 선택을 하면 음식종류(한식, 중식, 일식)를 value로 keywordSearch를 한다.</p>
</li>
<li><p>onClick 이벤트(검색버튼), onKeyPress 이벤트(엔터키) 에는 input value로 keywordSearch를 한다.</p>
</li>
</ul>
<hr>
<h1 id="비교하기">비교하기</h1>
<h2 id="수정-전">(수정 전)</h2>
<center>
<img src="https://velog.velcdn.com/images/g_c0916/post/a1e952bb-0011-4fa2-b859-b6c39257cba6/image.gif" width="500" />
위치 검색 </center>

<hr>
<center>
<img src="https://velog.velcdn.com/images/g_c0916/post/32af6026-be93-4514-85d2-1d9bb26332e9/image.gif" width="500" />
드래그 검색 (지도를 확대후에 드래그했는데 지도를 재구성하면서 축소되어서 나온다)
</center>

<hr>
<center>
<img src="https://velog.velcdn.com/images/g_c0916/post/5da2d4a0-fa7f-4dc0-9fe6-36d6bb88778c/image.gif" width="500" />
메뉴탭 검색 (마찬가지로 확대 level이 들쑥날쑥하다)
</center>

<hr>
<h2 id="수정-후">(수정 후)</h2>
<center>
<img src="https://velog.velcdn.com/images/g_c0916/post/a1e952bb-0011-4fa2-b859-b6c39257cba6/image.gif" width="500" />
위치 검색 </center>

<hr>
<center>
<img src="https://velog.velcdn.com/images/g_c0916/post/da6dd50a-3756-46a2-af3d-305863fd88a5/image.gif" width="500" />
드래그 검색
</center>

<hr>
<center>
<img src="https://velog.velcdn.com/images/g_c0916/post/51ad3fbb-65bb-4a2a-aea8-7bdcc7ab999a/image.gif" width="500" />
메뉴탭 검색
</center>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[토이 프로젝트 - GPT API (1)]]></title>
            <link>https://velog.io/@g_c0916/%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-chat-GPT-API</link>
            <guid>https://velog.io/@g_c0916/%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-chat-GPT-API</guid>
            <pubDate>Wed, 26 Apr 2023 14:40:48 GMT</pubDate>
            <description><![CDATA[<p>gpt API를 이용해서 뭔가 만들어 볼 수 없을까 찾아보다가 gpt가 문장의 긍정적 부정적인 판단이 가능하다는것에 대해 알았다. 엄청 짧은 문장이나 단어뿐만 아니라 좀 더 길고 일상적인 문법에서 전체 내용이 얼마나 긍정적인지 부정적인지 수치상으로 파악할 수 있을까 궁금했다. 테스트 해보기 좋은 음식점 리뷰를 먼저 chat gpt에 돌려봤다.</p>
<pre><code>매장 굉장히 깨끗했고 좋은 분위기에서 식사했습니다. 메뉴는 조금 낯선 감은 있었지만 맛있게 잘 먹었습니다.
</code></pre><p>실제로 어느 식당의 한 이용자의 리뷰이고 그 이용자는 별점 4점을 주었다. 전체적으로 좋은평의 리뷰이지만 메뉴가 조금 낯선 감이 있었다고 하니 4점 정도의 별점이 납득이 간다.</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/1c0938b8-9e11-4748-935a-f05de115b43c/image.png" alt=""></p>
<p>다만, 이라는 부분부터 조금 불안하긴하지만 잘 파악한것같다. 이정도면 될거같다.</p>
<p>일단 gpt에 대한 사전 조사를 해본 결과</p>
<ul>
<li>gpt api 무료 크레딧이 5달러밖에 안된다는 점</li>
<li>한글이 영어보다 한글자당 token을 많이 잡아먹는다는 점</li>
<li>fine-tuning으로 좀 더 똑똑하게 만들 수 있을까라는 생각이 들었는데 fine-tuning은 1000 토큰당 가격이 상당히 나간다는 점</li>
<li>fine-tuning(davinci) &gt; text-davinci-003 &gt; gtp-3.5-turbo 순으로 비싸다</li>
</ul>
<p>어느정도의 텍스트를 보냈을때 얼마만큼의 토큰이 들지 가늠하기 힘들었고, 몇번의 테스트를 거치게 될지 알 수 없었기때문에 가격이 문제였다. 일단 더 긴 문장까지는 가지말고 음식점 리뷰정도의 텍스트 길이가 좋겠다고 생각하고 리뷰를 작성하면 gpt가 자동으로 그에맞는 별점을 매겨주는 시스템으로 프로젝트를 만들어 보기로 했다.</p>
<p>( 아직 수정할 부분이 좀 더 있지만 현재 배포중인 주소이다 <a href="https://www.autoratingai.shop">https://www.autoratingai.shop</a>)</p>
<hr>
<p>map API를 이용해 가게들의 리뷰 데이터를 가지고 있어야 이전에 남긴 리뷰들을 보여줄 수 있겠다 생각하고 미리 server를 간단하게 만들기로 했다.</p>
<pre><code class="language-js">// server/index.js
const OpenAI = require(&quot;openai&quot;);
const { Configuration, OpenAIApi } = OpenAI;
const axios = require(&quot;axios&quot;);
const express = require(&quot;express&quot;);
const bodyParser = require(&quot;body-parser&quot;);
const cors = require(&quot;cors&quot;);
const app = express();
const port = 5001;
const GPT_KEY = &quot;my api key&quot;

const configuration = new Configuration({
  organization: &quot;org-N5YhVJchrFKSEZu1kn1CQPsu&quot;,
  apiKey: GPT_KEY,
});

const openai = new OpenAIApi(configuration);

app.use(bodyParser.json());
app.use(cors());


app.post(&quot;/&quot;, async (req, res) =&gt; {
  const { message } = req.body;
  const question = `너는 음식점 소비자야 너는 가게에 다음과같은 리뷰를 남겼어 ${message} 점수는 1점이상 5점이하중에 몇점을 줬을까? 점수 : 몇점, 이유: ~입니다 라고 대답해`;
  const response = await axios.post(
      &quot;https://api.openai.com/v1/chat/completions&quot;,
      {
        model: &quot;gpt-3.5-turbo&quot;,
        messages: [{ role: &quot;user&quot;, content: question }],
        temperature: 0.6,
      },
      {
        headers: {
          &quot;Content-Type&quot;: &quot;application/json&quot;,
          Authorization: `Bearer ${GPT_KEY}`,
        },
      }
    );

    const response2 = await openai.createCompletion({
      model: &quot;text-davinci-003&quot;,
      prompt: question,
      max_tokens: 2048,
      temperature: 0.6,
    });

    if (response2.data) {
      if (response2.data.choices) {
        res.json({
          turbo: response.data.choices[0].message.content,
          davinci: response2.data.choices[0].text,
        });
      }
    }
  } catch (error) {
    console.error(error);
  }
})


app.listen(port, () =&gt; {
  console.log(&quot;Example app port: &quot; + port);
});
</code></pre>
<pre><code>server 디렉토리에서 node index.js로 서버 실행 후 클라이언트에서 post 요청을 보냈다.</code></pre><blockquote>
<p>GPT API 사용하기</p>
</blockquote>
<p>axios로 호출하는 방법도 있고 openai 라이브러리를 설치해서 사용하는 방법이 있는데 이상하게 gpt-3.5-turbo 모델은 openai 라이브러리로 구현이 안돼서 axios를 이용해 통신했다.
둘다 방법은 쉽다.</p>
<h2 id="text-davinci-003">text-davinci-003</h2>
<pre><code class="language-js">
const OpenAI = require(&quot;openai&quot;);
const { Configuration, OpenAIApi } = OpenAI;
const GPT_KEY = &quot;my api key&quot;

const configuration = new Configuration({
  organization: &quot;org-N5YhVJchrFKSEZu1kn1CQPsu&quot;,
  apiKey: GPT_KEY,
});

const openai = new OpenAIApi(configuration);

 const response2 = await openai.createCompletion({
      model: &quot;text-davinci-003&quot;,
      prompt: question,
      max_tokens: 2048,
      temperature: 0.6,
 });</code></pre>
<p>openai 라이브러리를 설치해서 호출하는 방법이다. text-davinci-003은 model만 필수라고 나온다. prompt는 선택사항인데 일단 뭔가 답변을 받을 메세지를 보내야하기 때문에 필수항목이라고 보면 될거같다. string과 array 타입이 다 된다고 나오는데</p>
<pre><code class="language-js">const myArray = [&quot;오늘은&quot;, &quot;날씨가&quot;, &quot;매우&quot;, &quot;덥습니다.&quot;];
const prompt = myArray;</code></pre>
<p>이 경우, prompt에 전달되는 값은 [&quot;오늘은&quot;, &quot;날씨가&quot;, &quot;매우&quot;, &quot;덥습니다.&quot;]가 아니라, 배열의 요소들이 공백으로 구분된 문자열인 &quot;오늘은 날씨가 매우 덥습니다.&quot;가 된다고 한다.</p>
<hr>
<h2 id="gpt-35-turbo">gpt-3.5-turbo</h2>
<pre><code class="language-js">  const response = await axios.post(
      &quot;https://api.openai.com/v1/chat/completions&quot;,
      {
        model: &quot;gpt-3.5-turbo&quot;,
        messages: [{ role: &quot;user&quot;, content: question }],
        temperature: 0.6,
      },
      {
        headers: {
          &quot;Content-Type&quot;: &quot;application/json&quot;,
          Authorization: `Bearer ${GPT_KEY}`,
        },
      }
    );</code></pre>
<p>헤더에 gpt 홈페이지에서 발급받은 api 키를 넣어주고 바디에는 model, messages, temperature 을 넣어주면 된다. model, messages는 필수 항목이고 temperature은 선택 항목이다. 그외에도 선택항목은 다양하게 있다. <a href="https://platform.openai.com/docs/api-reference/chat/create">https://platform.openai.com/docs/api-reference/chat/create</a> (_여기서 확인)</p>
<hr>
<p> <strong><em>gpt-3.5-turbo</em></strong> 의 경우 대화형 모델이기 때문에 messages에는 배열 형식으로 role이 부여된다.</p>
<pre><code class="language-js">messages=[
        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a helpful assistant.&quot;},
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Who won the world series in 2020?&quot;},
        {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;The Los Angeles Dodgers won the World Series in 2020.&quot;},
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Where was it played?&quot;}
    ]
</code></pre>
<p>예시로는 이렇다. 
일반적으로 대화는 먼저 시스템 메시지로 형식화되고 그 다음에 user 및 assistant 메시지가 번갈아 표시된다. 대화 기록을 포함하면 user의 content를 전달할때, 이전 메시지를 참조하는데 있어 도움이 된다. </p>
<p>위의 예에서 Where was it played? 라는 user의 마지막 질문은 2020년 월드 시리즈에 대한 이전 메시지의 맥락이 있어야 의미가 있다.
<img src="https://velog.velcdn.com/images/g_c0916/post/7890cb1f-6531-4d1a-bf72-78c2b3237e4d/image.png" alt=""></p>
<p>어디서 경기가 펼쳐졌는지에 대한 물음에 이전 대화 맥락을 알고있기 때문에 좌측의 gpt-3.5-turbo 모델은 월드시리즈가 어디서 경기했는지 답변을 해주고, 우측의 text-davinci-003 모델은 최초의 월드컵 이야기를 한다.</p>
<p>나같은경우 대화를 이어나가는것이 아닌 text-davinci-003 모델과 같은 수준의 단일 답변만 필요했기 때문에 messages에 user role만 전달한다.</p>
<hr>
<h1 id="gpt-35-turbo-vs-text-davinci-003">[gpt-3.5-turbo] vs [text-davinci-003]</h1>
<p>좌 : gpt-3.5-turbo ------------------ 우 : text-davinci-003</p>
<h2 id="테스트1">테스트1</h2>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/b67920c1-63b2-4468-b169-5990f9160523/image.png" alt=""></p>
<h2 id="테스트2">테스트2</h2>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/e2071b12-af21-4ae1-a86b-f6d47d5ca054/image.png" alt=""></p>
<p>(두 리뷰 다 아쉬운 부분을 언급한점을 파악해 별점 1점씩 내린 부분이 신기하다)
이외에도 테스트를 많이 해봤지만 같은 요청에 답변의 퀄리티가 gpt-3.5-turbo가 대체로 마음에 들었다. </p>
<p>다만, gpt-3.5-turbo는 fine-tuning을 지원하지 않는다고 나와있다. fine-tuning을 하려면 davinci 모델을 기반으로 training 해야 하는데 일단은 기본 제공되는 모델도 충분히 재밌게 써먹을 수 있다고 생각해 프로젝트를 만들어보고 fine-tuning은 따로 테스트 해보는 방식으로 프로젝트를 진행했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 더 쉽게 kakao maps-sdk 사용하기 - 마커]]></title>
            <link>https://velog.io/@g_c0916/React%EC%97%90%EC%84%9C-%EB%8D%94-%EC%89%BD%EA%B2%8C-kakao-maps-sdk-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EB%A7%88%EC%BB%A4</link>
            <guid>https://velog.io/@g_c0916/React%EC%97%90%EC%84%9C-%EB%8D%94-%EC%89%BD%EA%B2%8C-kakao-maps-sdk-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EB%A7%88%EC%BB%A4</guid>
            <pubDate>Wed, 26 Apr 2023 09:21:24 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>키워드 검색을 할때 data를 이용해 marker들의 위치를 담은 배열을 생성해주고 setMarkers로 상태를 업데이트 한다.</p>
</li>
<li><p>jsx에서 컴포넌트의 props를 전달해 쉽게 여러개의 마커와 커스텀오버레이 설정이 가능하다</p>
</li>
<li><p>주석으로 설명을 덧붙인다</p>
</li>
</ul>
<pre><code class="language-jsx">const { kakao } = window;
const [info, setInfo] = useState()
const [markers, setMarkers] = useState([])
const [map, setMap] = useState()
const [searchInputValue, setSearchInputValue] = useState(&quot;&quot;);
const [keyword, setKeyword] = useState(&quot;&quot;);

useEffect(() =&gt; {
    if (!map) return
    const ps = new kakao.maps.services.Places()

    ps.keywordSearch(`${keyword}`, (data, status, _pagination) =&gt; {
      if (status === kakao.maps.services.Status.OK) {
        const bounds = new kakao.maps.LatLngBounds()


        // 마커 위치를 담은 배열을 생성한다        
        let markers = []
        for (var i = 0; i &lt; data.length; i++) {
          markers.push({
            position: {
              lat: data[i].y,
              lng: data[i].x,
            },
            content: data[i].place_name,
          })

          bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x))
        }
        setMarkers(markers)


        map.setBounds(bounds)
      }
    })
  }, [map, keyword])

const handleKeyPress = (e) =&gt; {
  if (e.key === &quot;Enter&quot;) setKeyword(searchInputValue);
};

// 현재 클릭한 마커로 업데이트 및 센터 이동
const handleClickMarker = (e, marker) =&gt; {
    setInfo(marker);
    map.panTo(e.getPosition());
  };

...

return (
    &lt;...&gt;
        &lt;Map
        center={{
          lat: 37.566826,
          lng: 126.9786567,
        }}
        style={{
          width: &quot;100%&quot;,
          height: &quot;200px&quot;,
          borderRadius: &quot;5px&quot;,
          border: &quot;2px solid #e2e2e2&quot;,
        }}
        onCreate={setMap}
        level={3}
      &gt;
        // 마커를 맵에 생성 
        // position 속성으로 마커의 위치를 정해주고
        // 클릭이벤트 지정한다.
        {markers.map((marker, idx) =&gt; (
          &lt;div key={idx}&gt;
            &lt;MapMarker
              position={{ lat: marker.position.lat, lng: marker.position.lng }}
              onClick={(e) =&gt; handleClickMarker(e, marker)}
              // 마커 이미지 변경이 가능하다
              image={{
                src: &quot;./marker.png&quot;,
                size: {
                  width: 30,
                  height: 30,
                },
              }}
            /&gt;

            // 클릭한 마커의 커스텀오버레이만 보이게끔 조건을 붙인다
            {info &amp;&amp; info.content === marker.content &amp;&amp; (
              &lt;CustomOverlayMap
                position={{
                  lat: marker.position.lat,
                  lng: marker.position.lng,
                }}
                // 커스텀오버레이 위치 조정
                yAnchor={2.1}
              &gt;
                &lt;div className=&quot;customoverlay&quot;&gt;
                  &lt;span className=&quot;title&quot;&gt;{marker.content}&lt;/span&gt;
                &lt;/div&gt;
              &lt;/CustomOverlayMap&gt;
            )}
          &lt;/div&gt;
        ))}
      &lt;/Map&gt;
    &lt;...&gt;
)</code></pre>
<p>map 검색 결과의 특정위치의 좌표를 <code>MapMarker</code> 컴포넌트를 이용해 전달해주면 된다.</p>
<p>커스텀오버레이가 필요하면 <code>CustomOverlayMap</code> 컴포넌트를 통해 마커와 같은 좌표를 전달해주고 xAnchor, yAnchor를 통해 위치 조정이 가능하다. 기본값은 0.5이고 0~1 사이의 값을 받는다고 나와있다. 생각보다 위치가 좀 차이나서 2.1까지 올렸는데 잘 된다. 이부분은 좀 더 알아보고 수정해야겠다. (kakao map 컴포넌트 전체코드도 같이 수정)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React에서 더 쉽게 kakao maps-sdk 사용하기 - 키워드 검색]]></title>
            <link>https://velog.io/@g_c0916/React%EC%97%90%EC%84%9C-%EB%8D%94-%EC%89%BD%EA%B2%8C-kakao-maps-sdk-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@g_c0916/React%EC%97%90%EC%84%9C-%EB%8D%94-%EC%89%BD%EA%B2%8C-kakao-maps-sdk-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Apr 2023 07:34:21 GMT</pubDate>
            <description><![CDATA[<p><a href="https://react-kakao-maps-sdk.jaeseokim.dev/">https://react-kakao-maps-sdk.jaeseokim.dev/</a></p>
<ul>
<li>Kakao Maps API를 React에 맞게 변경한 라이브러리</li>
</ul>
<blockquote>
<p>index.html에 sdk script태그 추가</p>
</blockquote>
<pre><code class="language-html">&lt;script
  type=&quot;text/javascript&quot;
  src=&quot;//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다.&amp;libraries=services,clusterer&quot;
&gt;&lt;/script&gt;</code></pre>
<blockquote>
<p>install</p>
</blockquote>
<pre><code>npm install react-kakao-maps-sdk
or
yarn add react-kakao-maps-sdk</code></pre><blockquote>
<p>지도 생성하기</p>
</blockquote>
<pre><code class="language-js">import import { Map } from &quot;react-kakao-maps-sdk&quot;;

export default function KakaoMap(){
  return (
    &lt;Map // 지도를 표시할 Container
      center={{
        // 지도의 중심좌표
        lat: 33.450701,
        lng: 126.570667,
      }}
      style={{
        // 지도의 크기
        width: &quot;100%&quot;,
        height: &quot;450px&quot;,
      }}
      level={3} // 지도의 확대 레벨
    /&gt;
  );
}</code></pre>
<hr>
<p>나의 경우 음식점들만 지도에 나타내야했는데 react-kakao-maps-sdk에서 제공하는 api와 kakao developers에서 제공하는 api와는 차이가 있어서 카테고리별 검색 기능을 찾지 못해 처음에는 키워드 검색을 기준으로 map api를 구현하였다.</p>
<pre><code class="language-js">import React, { useState, useEffect } from &quot;react&quot;;
import { Map, MapMarker, CustomOverlayMap } from &quot;react-kakao-maps-sdk&quot;;


function(){
  const { kakao } = window;
  const [info, setInfo] = useState()
  const [markers, setMarkers] = useState([])
  const [map, setMap] = useState()

  useEffect(() =&gt; {
    if (!map) return
    const ps = new kakao.maps.services.Places()

    ps.keywordSearch(&quot;이태원 맛집&quot;, (data, status, _pagination) =&gt; {
      if (status === kakao.maps.services.Status.OK) {
        // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
        // LatLngBounds 객체에 좌표를 추가합니다
        const bounds = new kakao.maps.LatLngBounds()
        let markers = []

        for (var i = 0; i &lt; data.length; i++) {
          // @ts-ignore
          markers.push({
            position: {
              lat: data[i].y,
              lng: data[i].x,
            },
            content: data[i].place_name,
          })
          // @ts-ignore
          bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x))
        }
        setMarkers(markers)

        // 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다
        map.setBounds(bounds)
      }
    })
  }, [map])

  return (
    &lt;Map // 로드뷰를 표시할 Container
      center={{
        lat: 37.566826,
        lng: 126.9786567,
      }}
      style={{
        width: &quot;100%&quot;,
        height: &quot;350px&quot;,
      }}
      level={3}
      onCreate={setMap}
    &gt;
      {markers.map((marker) =&gt; (
        &lt;MapMarker
          key={`marker-${marker.content}-${marker.position.lat},${marker.position.lng}`}
          position={marker.position}
          onClick={() =&gt; setInfo(marker)}
        &gt;
          {info &amp;&amp;info.content === marker.content &amp;&amp; (
            &lt;div style={{color:&quot;#000&quot;}}&gt;{marker.content}&lt;/div&gt;
          )}
        &lt;/MapMarker&gt;
      ))}
    &lt;/Map&gt;
  )
}</code></pre>
<p>위 검색어 입력 지도 생성하기 sample에 input을 추가해 이태원 맛집 항목에 input의 value로 변경해 주었다.</p>
<pre><code class="language-jsx">const { kakao } = window;
const [info, setInfo] = useState()
const [markers, setMarkers] = useState([])
const [map, setMap] = useState()
const [searchInputValue, setSearchInputValue] = useState(&quot;&quot;);
const [keyword, setKeyword] = useState(&quot;&quot;);

useEffect(() =&gt; {
    if (!map) return
    const ps = new kakao.maps.services.Places()

    ps.keywordSearch(`${keyword} 음식점`, (data, status, _pagination) =&gt; {
      if (status === kakao.maps.services.Status.OK) {
        // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
        // LatLngBounds 객체에 좌표를 추가합니다
        const bounds = new kakao.maps.LatLngBounds()
        let markers = []

        for (var i = 0; i &lt; data.length; i++) {
          markers.push({
            position: {
              lat: data[i].y,
              lng: data[i].x,
            },
            content: data[i].place_name,
          })
          bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x))
        }
        setMarkers(markers)

        // 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다
        map.setBounds(bounds)
      }
    })
  }, [map, keyword])

const handleKeyPress = (e) =&gt; {
  if (e.key === &quot;Enter&quot;) setKeyword(searchInputValue);
};


...

return (
  &lt;...&gt;
    &lt;SearchInput
        onChange={(e) =&gt; setSearchInputValue(e.target.value)}
        onKeyPress={(e) =&gt; handleKeyPress(e)}
        value={searchInputValue}
        placeholder={&quot;주소를 입력해주세요 ex)강남역 or 서울특별시 역삼동&quot;}
    /&gt;
    &lt;SearchButton onClick={() =&gt; setKeyword(searchInputValue)}&gt;
      검색
    &lt;/SearchButton&gt;
&lt;...&gt;
)
</code></pre>
<p>이런 형식으로 <code>keyword</code> 상태가 업데이트 되면 useEffect 안의 내용이 다시 실행 되게끔하고 검색어도 <code>${keyword} 음식점</code> 이라는 형식으로 음식점만을 검색하게 만들었다.</p>
<p>검색결과는 ps.keywordSearch의 data 파라미터로 받아 볼 수 있다.</p>
<hr>
<h2 id="이렇게-사용해도-되는걸까">이렇게 사용해도 되는걸까</h2>
<p>하지만 뭔가 만족스럽지 못하다. <a href="https://react-kakao-maps-sdk.jaeseokim.dev/">https://react-kakao-maps-sdk.jaeseokim.dev/</a> 해당 사이트에 카테고리별 검색에 대한 안내가 없지만 기본적으로 kakao developers에서 서비스중인 api를 베이스로 만든 것일 텐데 기능이 생각보다 너무 적다.
그래서 모듈안에 있는 Map 컴포넌트와 keywordSearch 함수에 대한 코드를 살펴봤다. 잘 읽어보면 사이트 내 api나 sample 항목에서 미처 설명못한 기능들을 구현할 방법들이 생각날거다.</p>
<blockquote>
<p>Map 컴포넌트</p>
</blockquote>
<pre><code class="language-jsx">/// &lt;reference types=&quot;kakao.maps.d.ts&quot; /&gt;
/// &lt;reference types=&quot;kakao.maps.d.ts&quot; /&gt;
/// &lt;reference types=&quot;kakao.maps.d.ts&quot; /&gt;
import React from &quot;react&quot;;
import { PolymorphicComponentPropsWithOutRef } from &quot;../types&quot;;
export declare const KakaoMapContext: React.Context&lt;kakao.maps.Map&gt;;
export declare type MapProps = {
    /**
     * 중심으로 설정할 위치 입니다.
     */
    center: {
        lat: number;
        lng: number;
    } | {
        x: number;
        y: number;
    };
    /**
     * 중심을 이동시킬때 Panto를 사용할지 정합니다.
     * @default false
     */
    isPanto?: boolean;
    /**
     * 중심 좌표를 지정한 좌표 또는 영역으로 부드럽게 이동한다. 필요하면 확대 또는 축소도 수행한다.
     * 만약 이동할 거리가 지도 화면의 크기보다 클 경우 애니메이션 없이 이동한다.
     * padding 만큼 제외하고 영역을 계산하며, padding 을 지정하지 않으면 기본값으로 32가 사용된다.
     */
    padding?: number;
    /**
     * 확대 수준 (기본값: 3)
     */
    level?: number;
    /**
     * 최대 확대 수준
     */
    maxLevel?: number;
    /**
     * 최소 확대 수준
     */
    minLevel?: number;
    /**
     * 지도 종류 (기본값: 일반 지도)
     */
    mapTypeId?: kakao.maps.MapTypeId;
    /**
     * 마우스 드래그, 휠, 모바일 터치를 이용한 시점 변경(이동, 확대, 축소) 가능 여부
     */
    draggable?: boolean;
    /**
     * 마우스 휠이나 멀티터치로 지도 확대, 축소 기능을 막습니다. 상황에 따라 지도 확대, 축소 기능을 제어할 수 있습니다.
     */
    zoomable?: boolean;
    /**
     * 마우스 휠, 모바일 터치를 이용한 확대 및 축소 가능 여부
     */
    scrollwheel?: boolean;
    /**
     * 더블클릭 이벤트 및 더블클릭 확대 가능 여부
     */
    disableDoubleClick?: boolean;
    /**
     * 더블클릭 확대 가능 여부
     */
    disableDoubleClickZoom?: boolean;
    /**
     * 투영법 지정 (기본값: kakao.maps.ProjectionId.WCONG)
     */
    projectionId?: string;
    /**
     * 지도 타일 애니메이션 설정 여부 (기본값: true)
     */
    tileAnimation?: boolean;
    /**
     * 키보드의 방향키와 +, – 키로 지도 이동,확대,축소 가능 여부 (기본값: false)
     */
    keyboardShortcuts?: boolean | {
        /**
         * 지도 이동 속도
         */
        speed: number;
    };
    /**
     * map 생성 후 해당 객체를 전달하는 함수
     */
    onCreate?: (map: kakao.maps.Map) =&gt; void;
    /**
     * 중심 좌표가 변경되면 발생한다.
     */
    onCenterChanged?: (target: kakao.maps.Map) =&gt; void;
    /**
     * 확대 수준이 변경되기 직전 발생한다.
     */
    onZoomStart?: (target: kakao.maps.Map) =&gt; void;
    /**
     * 확대 수준이 변경되면 발생한다.
     */
    onZoomChanged?: (target: kakao.maps.Map) =&gt; void;
    /**
     * 지도 영역이 변경되면 발생한다.
     */
    onBoundsChanged?: (target: kakao.maps.Map) =&gt; void;
    /**
     * 지도를 클릭하면 발생한다.
     */
    onClick?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) =&gt; void;
    /**
     * 지도를 더블클릭하면 발생한다.
     */
    onDoubleClick?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) =&gt; void;
    /**
     * 지도를 마우스 오른쪽 버튼으로 클릭하면 발생한다.
     */
    onRightClick?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) =&gt; void;
    /**
     * 지도에서 마우스 커서를 이동하면 발생한다.
     */
    onMouseMove?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) =&gt; void;
    /**
     * 드래그를 시작할 때 발생한다.
     */
    onDragStart?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) =&gt; void;
    /**
     * 드래그를 하는 동안 발생한다.
     */
    onDrag?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) =&gt; void;
    /**
     * 드래그가 끝날 때 발생한다.
     */
    onDragEnd?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) =&gt; void;
    /**
     * 중심 좌표나 확대 수준이 변경되면 발생한다.
     * 단, 애니메이션 도중에는 발생하지 않는다.
     */
    onIdle?: (target: kakao.maps.Map) =&gt; void;
    /**
     * 확대수준이 변경되거나 지도가 이동했을때 타일 이미지 로드가 모두 완료되면 발생한다.
     * 지도이동이 미세하기 일어나 타일 이미지 로드가 일어나지 않은경우 발생하지 않는다.
     */
    onTileLoaded?: (target: kakao.maps.Map) =&gt; void;
    /**
     * 지도 기본 타일(일반지도, 스카이뷰, 하이브리드)이 변경되면 발생한다.
     */
    onMaptypeidChanged?: (target: kakao.maps.Map) =&gt; void;
    children?: React.ReactNode | undefined;
};
declare type MapComponent = &lt;T extends React.ElementType = &quot;div&quot;&gt;(props: PolymorphicComponentPropsWithOutRef&lt;T, MapProps&gt; &amp; React.RefAttributes&lt;kakao.maps.Map&gt;) =&gt; React.ReactElement | null;
/**
 * 기본적인 Map 객체를 생성하는 Comeponent 입니다.
 * props로 받는 `on*` 이벤트는 해당 `kakao.maps.Map` 객체를 함께 인자로 전달 합니다.
 *
 * `ref`를 통해 `map` 객체에 직접 접근하여 사용 또는 onCreate 이벤트를 이용하여 접근이 가능합니다.
 *
 * &gt; *주의 사항* `Map`, `RoadView` 컴포넌트에 한하여, ref 객체가 컴포넌트 마운트 시점에 바로 초기화가 안될 수 있습니다.
 * &gt;
 * &gt; 컴포넌트 마운트 시점에 `useEffect` 를 활용하여, 특정 로직을 수행하고 싶은 경우 `ref` 객체를 사용하는 것보다
 * &gt; `onCreate` 이벤트와 `useState`를 함께 활용하여 제어하는 것을 추천 드립니다.
 */
declare const Map: MapComponent;
export default Map;</code></pre>
<blockquote>
<p>keywordSearch</p>
</blockquote>
<pre><code class="language-jsx">/// &lt;reference path=&quot;index.d.ts&quot; /&gt;

declare namespace kakao.maps.services {
  /**
   * 장소 검색 서비스.
   *
   * @see [Places](http://apis.map.kakao.com/web/documentation/#services_Places)
   */
  export class Places {
    /**
     * 장소 검색 서비스 객체를 생성한다.
     *
     * @param map 중심 좌표를 Places 객체의 location으로 설정할 지도 객체
     */
    constructor(map?: Map);

    /**
     * 지도 객체를 설정한다. 이미 설정되어 있는 지도는 `setMap(null)` 로 해제 가능하다.
     *
     * @param map 지도 객체
     */
    public setMap(map: Map | null): void;

    /**
     * 입력한 키워드로 검색한다.
     *
     * @param keyword 검색할 키워드
     * @param callback 검색 결과를 받을 콜백함수
     * @param options
     */
    public keywordSearch(
      keyword: string,
      callback: (
        result: PlacesSearchResult,
        status: Status,
        pagination: Pagination
      ) =&gt; void,
      options?: PlacesSearchOptions
    ): void;

    /**
     * 주어진 카테고리 코드로 검색한다.
     * 카테고리 검색은 영역 검색이 기본이므로
     * 옵션에 명세된 `x`, `y` 또는 `rect` 를 직접 지정하거나,
     * `location` 또는 `bounds` 값을 넣어 주어야 한다.
     * 아니면 지정한 `Map` 객체를 이용하는 옵션인 `useMapCenter` 또는
     * `useMapBounds` 을 참으로 설정하여 지도의 영역이 자동으로
     * 관련 값에 할당되도록 해도 된다.
     *
     * @param code 검색할 카테고리 코드
     * @param callback 검색 결과를 받을 콜백함수
     * @param options
     */
    public categorySearch(
      code: CategoryCode | `${CategoryCode}`,
      callback: (
        result: PlacesSearchResult,
        status: Status,
        pagination: Pagination
      ) =&gt; void,
      options?: PlacesSearchOptions
    ): void;
  }

  export type PlacesSearchResult = PlacesSearchResultItem[];

  export interface PlacesSearchResultItem {
    /**
     * 장소 ID
     */
    id: string;

    /**
     * 장소명, 업체명
     */
    place_name: string;

    /**
     * 카테고리 이름
     * 예) 음식점 &gt; 치킨
     */
    category_name: string;

    /**
     * 중요 카테고리만 그룹핑한 카테고리 그룹 코드
     * 예) FD6
     */
    category_group_code?: `${CategoryCode}` | `${Exclude&lt;CategoryCode, &quot;&quot;&gt;}`[];

    /**
     * 중요 카테고리만 그룹핑한 카테고리 그룹명
     * 예) 음식점
     */
    category_group_name: string;

    /**
     * 전화번호
     */
    phone: string;

    /**
     * 전체 지번 주소
     */
    address_name: string;

    /**
     * 전체 도로명 주소
     */
    road_address_name: string;

    /**
     * X 좌표값 혹은 longitude
     */
    x: string;

    /**
     * Y 좌표값 혹은 latitude
     */
    y: string;

    /**
     * 장소 상세페이지 URL
     */
    place_url: string;

    /**
     * 중심좌표까지의 거리(x,y 파라미터를 준 경우에만 존재). 단위 meter
     */
    distance: string;
  }

  export interface PlacesSearchOptions {
    /**
     * 키워드 필터링을 위한 카테고리 코드
     */
    category_group_code?: `${CategoryCode}` | `${Exclude&lt;CategoryCode, &quot;&quot;&gt;}`[];

    /**
     * 중심 좌표. 특정 지역을 기준으로 검색한다.
     */
    location?: LatLng;

    /**
     * x 좌표, longitude, `location` 값이 있으면 무시된다.
     */
    x?: number;

    /**
     * y 좌표, latitude, `location` 값이 있으면 무시된다.
     */
    y?: number;

    /**
     * 중심 좌표로부터의 거리(반경) 필터링 값. `location` / `x`, `y` / `useMapCenter` 중 하나와 같이 써야 의미가 있음. 미터(m) 단위. 기본값은 5000, 0~20000까지 가능
     */
    radius?: number;

    /**
     * 검색할 사각형 영역
     */
    bounds?: LatLngBounds;

    /**
     * 사각 영역. 좌x,좌y,우x,우y 형태를 가짐. `bounds` 값이 있으면 무시된다.
     */
    rect?: string;

    /**
     * 한 페이지에 보여질 목록 개수. 기본값은 15, 1~15까지 가능
     */
    size?: number;

    /**
     * 검색할 페이지. 기본값은 1, `size` 값에 따라 1~45까지 가능
     */
    page?: number;

    /**
     * 정렬 옵션. `DISTANCE` 일 경우 지정한 좌표값에 기반하여 동작함. 기본값은 `ACCURACY` (정확도 순)
     */
    sort?: SortBy;

    /**
     * 지정한 Map 객체의 중심 좌표를 사용할지의 여부. 참일 경우, `location` 속성은 무시된다. 기본값은 false
     */
    useMapCenter?: boolean;

    /**
     * 지정한 Map 객체의 영역을 사용할지의 여부. 참일 경우, `bounds` 속성은 무시된다. 기본값은 false
     */
    useMapBounds?: boolean;
  }
}</code></pre>
<hr>
<p>역시나 생각했던 기능들이 다 들어있었다.
굳이 <code>${keyword} 음식점</code> 으로 검색할 필요없이 장소만 검색어로 전달하고 keywordSearch 함수의 options 항목으로 음식점 코드(FD6)을 전달해주면 음식점만을 검색하게 된다.</p>
<pre><code class="language-jsx">
     // ...생략

  const options = {
    category_group_code: &quot;FD6&quot;, // 음식점만 검색한다
    page : 2,    // 2페이지의 검색 결과를 받는다
  };

  ps.keywordSearch(
    searchInputValue,
    (data, status, _pagination) =&gt; {},
    options
  );

    // ...생략
</code></pre>
<hr>
<p>map drag 재검색도 kakao developes 참고해서</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
    var geocoder = new kakao.maps.services.Geocoder();
    geocoder.coord2Address(position.lng, position.lat, displayCenterInfo);
  }, [position]);

  function displayCenterInfo(result, status) {
    if (status === kakao.maps.services.Status.OK) {
      let detailAddr = !!result[0].road_address
        ? result[0].road_address.address_name
        : &quot;&quot;;
      detailAddr += result[0].address.address_name;
      setKeyword(detailAddr);
    }
  }</code></pre>
<p>위와같이 중심좌표를 먼저 구한다음 행정주소로 변환해서 keyword를 업데이트하는 방식으로 복잡하게 구현했었는데 그럴필요없이 Map 컴포넌트의 </p>
<pre><code class="language-jsx">&lt;Map
onDragEnd={(map) =&gt; {
  setPosition({
    lat: map.getCenter().getLat(),
    lng: map.getCenter().getLng(),
  });
&gt;
  ...
&lt;/Map&gt;</code></pre>
<p>onDragEnd 이벤트로 중심좌표를 업데이트하고 keywordSearch options의 location 속성을 이용해보면 쉽게 수정이 가능 할 것 같다.</p>
<p>결론 : 좀 더 빨리 생각했으면 두번 고생 안할텐데..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] s3 자동배포(code pipeline)]]></title>
            <link>https://velog.io/@g_c0916/AWS-s3-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%ACcode-pipeline</link>
            <guid>https://velog.io/@g_c0916/AWS-s3-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%ACcode-pipeline</guid>
            <pubDate>Wed, 26 Apr 2023 02:59:39 GMT</pubDate>
            <description><![CDATA[<p>S3 버킷을 생성하고 난 뒤 자동배포를 위한 작업이다.</p>
<p>배포하려는 프로젝트 최상위 디렉토리에 buildspec.yml 파일을 생성해준다.</p>
<pre><code class="language-yml">version: 0.2

phases:
  pre_build:
    commands:
      - cd client
      - npm install
  build:
    commands:
      - npm run build

artifacts:
  files:
    - &#39;**/*&#39;
  base-directory: client/build</code></pre>
<p>yml 내용을 자세히 보면 내 프로젝트 구성에 맞게 build 사전 작업으로 client 디렉토리에 접근해 npm install을 먼저 해준다. 그 뒤 build 작업에서는 npm run build 명령어를 통해 빌드를 해주게 되고 build 된 파일들의 base-directory가 들어간다.</p>
<p>변경사항을 저장하고 commit 후 push 한다.</p>
<hr>
<blockquote>
<p>이제 aws의 CodePipeline 콘솔에 접속한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/32205e3b-37bd-4551-bbb3-313b108bb3e4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/5f45e844-772d-45fa-98d5-ee9993d85247/image.png" alt=""></p>
<blockquote>
<p>자신이 구별할 수 있는 이름으로 파이프라인 이름을 적어주고 다음으로 넘어간다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/e6aafa53-27a1-4774-a0c5-75e417cdf13f/image.png" alt=""></p>
<blockquote>
<p>소스공급자는 github(버전 2)로 선택해준다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/b2cd62d1-0305-456e-a9c8-28f3b36c2b9f/image.png" alt=""></p>
<blockquote>
<p>GitHub에 연결 클릭</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/e36b9097-8434-4cc9-baa3-455010fabbb9/image.png" alt=""></p>
<blockquote>
<p>연결이름을 정하고 새앱 설치 클릭</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/7a59fa2d-87c0-4479-ab0e-b2f6aa86c698/image.png" alt=""></p>
<blockquote>
<p>깃헙 앱 새창이 뜰텐데 맨 아래로 내려보면 연결 할 프로젝트 레포 선택할 수 있는 부분이 보인다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/0e6b63a9-7126-498b-87fe-33577070a3b6/image.png" alt=""></p>
<blockquote>
<p>연결 후 리포지토리 이름에 해당 프로젝트를 선택할 수 있고 배포할 브랜치도 선택 후에 다음으로 넘어간다. 이후 해당 브랜치에 커밋이 발생하면 자동으로 빌드-배포가 진행 된다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/461555c9-e1ac-4650-a564-1038b45832de/image.png" alt=""></p>
<blockquote>
<p>빌드공급자로 AWS CodeBuild 선택, 다음으로 넘어가 프로젝트 생성을 눌러준다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/4dcd7ddd-919e-4d43-968f-7cbea99c1265/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/fd8a6ef4-b11f-4942-9a63-acc77de80b4c/image.png" alt=""></p>
<blockquote>
<p>운영체제는 Ubuntu 다른 운영체제 선택 시 터미널 명령어가 buildspec.yml 파일에 담긴 명령어가 정상적으로 작동하지 않을 가능성이 있다. 
런타임은 standard
이미지는 <a href="https://docs.aws.amazon.com/codebuild/latest/userguide/available-runtimes.html">https://docs.aws.amazon.com/codebuild/latest/userguide/available-runtimes.html</a> 위 링크에서 지원하는 버전을 참고해 선택한다.
이미지의 버전은 이 런타임 버전에 항상 최신 버전 사용을 선택한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/123f0fbf-9a13-437a-b40c-0852f686d8c7/image.png" alt=""></p>
<blockquote>
<p>로그 파일을 저장하는 서비스로 CloudWatch 혹은 S3를 이용할 수 있다. S3에 버킷이 이미 하나 생성되어 있다면, 버킷의 수가 많아져 과금이 될 가능성이 있으니 CloudWatch 서비스를 빌드 출력 로그 저장을 위한 서비스로 선택하여 리소스를 분산할 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/fa12430c-a927-4e94-8389-eb79c0060211/image.png" alt=""></p>
<blockquote>
<p>프로젝트 생성이 다 끝났다면 다시 전 화면으로 다시 돌아와 프로젝트에서 사용하는 환경변수를 설정해준다. 추후 pipeline 생성이 끝난후에 추가해줄 수 도 있다.(맨 아래 참고)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/c1ae563f-50b3-4299-9030-df30f472c031/image.png" alt=""></p>
<blockquote>
<p>다음으로 넘어가서 배포공급자는 S3로 만들어두었던 버킷을 선택한다. 배포하기전에는 파일 압축을 풀어줄수 있도록 체크 표시한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/c45a6999-66df-43a6-9b8e-92216be181b7/image.png" alt=""></p>
<blockquote>
<p>다음으로 넘어가 파이프라인을 생성한다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/b8a65e71-690b-451b-94a0-233a5ec55c1d/image.png" alt=""></p>
<p>이렇게 3개 다 성공하면 끝!</p>
<p>S3 서비스에 접속하면 &#39;codepipeline&#39;으로 시작하는 버킷이 생성된 것을 확인할 수 있는데 이는 정상적이므로 해당 버킷은 무시해도 좋다.</p>
<p>만약 빌드 과정에서 &#39;실패&#39; 메시지가 보일 경우 빌드 출력 로그를 확인하여 문제점을 찾아야 한다.</p>
<hr>
<h3 id="환경변수-추가-하는-방법">환경변수 추가 하는 방법</h3>
<blockquote>
<p>생성한 파이프라인에 들어간다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/3def2278-379d-4ce2-80ac-ce08e81afd54/image.png" alt=""></p>
<blockquote>
<p>편집
<img src="https://velog.velcdn.com/images/g_c0916/post/a5e8180d-dcb6-4fe2-87e6-2c7974a4459f/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>build의 스테이지 편집</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/0e39041d-421c-4fc3-90e3-2d4637c8db7e/image.png" alt=""></p>
<blockquote>
<p>현재 작업중인 그룹의 편집버튼 클릭하면 환경변수를 추가할 수 있다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/3e55c543-a104-4f85-8bf4-5f14248ca007/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/b8a12154-f4ad-42a5-b590-09c191961459/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[웹 성능 최적화] Chrome Lighthouse (AUDITS)]]></title>
            <link>https://velog.io/@g_c0916/%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Chrome-Lighthouse-AUDITS</link>
            <guid>https://velog.io/@g_c0916/%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-Chrome-Lighthouse-AUDITS</guid>
            <pubDate>Mon, 10 Apr 2023 14:36:38 GMT</pubDate>
            <description><![CDATA[<h3 id="왜-성능-최적화를-해야하는가">왜 성능 최적화를 해야하는가?</h3>
<ul>
<li><p>페이지가 뜨는 시간이 늦을수록 사용자가 서비스를 떠날 확률이 높다. 성능 최적화를 통해 사용자가 쾌적한 환경에서 서비스를 이용할 수 있다면 그로 인해 사용자가 웹사이트를 자주 방문하거나 추천할 가능성이 높아지고 그만큼 수익증대의 기대도 할 수 있다.</p>
</li>
<li><p>어디서나 프론트엔드는 비슷한 일을 하게 된다. 하지만 거기에 더해 브라우저를 이해하고 최적화를 할줄 안다면 같은 프론트엔드 개발자 내에서도 상당한 경쟁력을 갖출 수 있다.</p>
</li>
</ul>
<hr>
<h3 id="웹-성능-결정-요소">웹 성능 결정 요소</h3>
<ul>
<li><p>로딩 성능 : 각 리소스를 불러오는 성능</p>
</li>
<li><p>렌더링 성능 : 불러온 리소스들을 화면에 보여주는 성능</p>
</li>
</ul>
<p>어떻게 더 빠르게 리소스를 로드할것인지, 어떻게 화면을 더 빠르게 렌더링 할 것인지 초점을 두고 최적화를 해나가야 한다. 그러기 위해서는 브라우저가 서버와 어떻게 통신하는지, 브라우저는 화면을 그릴때 어떻게 그리는지 알아야한다.</p>
<hr>
<h3 id="성능-최적화-keyword">성능 최적화 KeyWord</h3>
<p><code>로딩 성능 최적화</code></p>
<ul>
<li><p>이미지 사이즈 최적화 : 웹서비스에서 다양한 이미지를 사용하는데 이미지의 사이즈가 너무 크면 서비스가 무거워지고 너무 작으면 사용자가 낮은 해상도의 이미지를 보게 돼 불편함을 겪을 수 있다.</p>
</li>
<li><p>code Split : 코드를 분할할 때 어떻게해야 코드를 효율적으로 분할 할 수 있는지, 언제 코드를 분할해야하는지 파악한다.</p>
</li>
<li><p>텍스트 압축 : 웹페이지에 접속하면 HTML, JavaScript, CSS 등 다양한 리소스들을 다운받게되는데 이런 리소스들을 서버에서 미리 압축해서 다운받게한다.</p>
</li>
</ul>
<p><code>렌더링 성능 최적화</code></p>
<ul>
<li>Bottleneck 코드 최적화 : 특정 JavaScript 코드 때문에 서비스가 느리게 동작할 때, 해당 병목현상을 일으키는 코드를 찾아내고 최적화 한다.</li>
</ul>
<hr>
<blockquote>
<h2 id="chrome-lighthouse-audits">Chrome lighthouse (AUDITS)</h2>
</blockquote>
<p><strong>Chrome lighthouse</strong>는 Chrome 브라우저 내 개발자도구에 내장된 퍼포먼스 측정 도구이다.</p>
<p>크롬 개발자 도구(F12)를 열어 탭에서 Lighthouse를 찾는다. 들어가면 Category와 Device를 선택할 수 있는데 각 카테고리의 의미는 아래와 같다.</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Desc</th>
</tr>
</thead>
<tbody><tr>
<td>Performance</td>
<td>웹 페이지의 로딩 속도 등 실제 성능을 측정한다.</td>
</tr>
<tr>
<td>Progressive Web App</td>
<td>PWA로 부르며, 웹과 네이티브 앱의 기능 모두의 이점을 가지도록 만들어진 서비스인지 체크한다.</td>
</tr>
<tr>
<td>Best practices</td>
<td>Best practices를 따라 개발되었는지 체크한다.</td>
</tr>
<tr>
<td>Accessibility</td>
<td>접근성. 보통 폰트 사이즈, 메뉴간 간격 등을 측정한다.</td>
</tr>
<tr>
<td>SEO</td>
<td>Search Engine Optimization의 약자로 검색 엔진 수집 최적화에 관련된 부분이다.</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/4170fcb6-a7b9-4788-b12e-9c81a83e11ab/image.png" alt=""></p>
<p>체크 후에 <strong>Analyze page load</strong> 버튼을 이용해 스캔해보면, 각각 항목에 대한 결과 점수와 상세 내용을 볼 수 있다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/99284e18-54a6-4c1d-8af3-ced63b071055/image.png" alt=""></p>
<blockquote>
<h4 id="first-contentful-paint">First Contentful Paint</h4>
</blockquote>
<p>초기 DOM 콘텐츠를 렌더링하는데 걸리는 시간을 측정한다. 첫번째 텍스트 또는 이미지가 표시되는 시간을 가리킨다. FCP 측정 시점을 시작점으로 하는 다른 Metric들이 있기 때문에, 가장 먼저 찍히는 FCP는 다른 항목들의 점수 및 전체 점수에 큰 영향을 줄 수 있다.</p>
<blockquote>
<h4 id="largest-contentful-paint">Largest Contentful Paint</h4>
</blockquote>
<p>가장 큰 콘텐츠를 렌더링 하는데 걸리는 시간이다. 보통 중요한 콘텐츠일수록 크기가 크기 때문에, Lighthouse는 LCP 점수에 가장 높은 가중치인 25%를 주어 계산한다.</p>
<p><strong>LCP로 간주되는 요소</strong></p>
<ul>
<li><code>&lt;img&gt; 요소</code></li>
<li><code>&lt;svg&gt; 안에 있는 &lt;image&gt; 요소</code></li>
<li><code>&lt;video&gt; 요소</code></li>
<li><code>background-image 속성의 url()로 로드되는 요소</code></li>
<li><code>block 레벨 요소 (텍스트 노드 또는 기타 인라인 수준의 텍스트 요소 자식을 포함하는 요소)</code></li>
</ul>
<p><strong>LCP의 크기로 간주되려면</strong></p>
<p>일반적으로 뷰포트(viewport) 내에서 사용자에게 표시되는 요소 중 가장 크기가 큰 요소로 결정된다.</p>
<blockquote>
<h4 id="total-blocking-time">Total Blocking Time</h4>
</blockquote>
<p>마우스 클릭, 화면 탭 또는 키보드 누름과 같은 사용자 입력에 페이지가 응답하지 못하도록 차단된 총 시간을 의미한다. FCP와 TTI 사이의 모든 Long Task 블로킹 타임을 추가하여 계산된다.</p>
<p><strong>TBT 점수를 높이는 방법 (</strong>= Long Task의 근본적인 원인을 제거하는 방법)</p>
<ul>
<li>불필요한 JavaScript 로드, 실행 코드 제거</li>
<li>코드 분할을 통해 JavsScript payload 감소시키기</li>
<li>사용하지 않는 코드 삭제</li>
</ul>
<blockquote>
<h4 id="cumulative-layout-shift">Cumulative Layout Shift</h4>
</blockquote>
<p>사용자가 예상치 못한 레이아웃 이동을 경험하는 것에 대한 점수이다. 레이아웃 불안정이 사용자에게 미치는 부정적인 영향에 대해 검사해 볼 수 있다.</p>
<blockquote>
<h4 id="speed-index">Speed Index</h4>
</blockquote>
<p>콘텐츠가 시각적으로 표시되는 진행 속도를 측정한다. 리로드되는 페이지의 비디오를 캡쳐하여 프레임 간의 속도를 계산한다.</p>
<p><strong>SI 점수를 높이는 방법</strong></p>
<ul>
<li>메인 스레드 작업 최소화</li>
<li>JavaScript 실행 시간 단축</li>
<li>폰트가 로드되는 중에도 텍스트가 계속 표시되도록</li>
</ul>
<blockquote>
<h4 id="time-to-interactive">Time to Interactive</h4>
</blockquote>
<p>&quot;Time to Interactive (TTI)&quot;는 사용자가 페이지와 상호작용할 수 있는 상태가 되는 시점을 나타내는 지표이다. 이 지표는 웹 페이지가 모두 로드되어 브라우저에서 실행될 때까지 걸리는 시간을 포함하여 계산된다.</p>
<p>하지만, &quot;Time to Interactive (TTI)&quot; 항목이 LightHouse에서 표시되지 않는 경우도 있다. 이는 대개 LightHouse에서 사용되는 기본적인 측정 시간 제한과 관련이 있다. LightHouse는 기본적으로 5초 이내에 페이지를 로드하고 측정을 마쳐야 하기 때문에, TTI가 5초 이상 걸리는 페이지의 경우에는 TTI 항목이 표시되지 않을 수 있다.</p>
<p>또한, LightHouse는 TTI 항목을 계산하기 위해 페이지 상호작용을 검사하고, 이를 예측하기 위해 다양한 요소를 고려한다. 때로는 LightHouse가 이를 정확하게 계산하지 못할 수도 있다. 이 경우에는 TTI 항목이 표시되지 않거나, 잘못된 결과를 보여줄 수 있다.</p>
<p>따라서, &quot;Time to Interactive (TTI)&quot; 항목이 LightHouse에서 표시되지 않는다고 해서 반드시 측정이 이루어지지 않는 것은 아니다. 그러나, TTI를 측정하고자 한다면, LightHouse를 사용하는 것 외에도 다른 도구나 방법을 활용하여 확인하는 것이 좋다.</p>
<hr>
<h3 id="diagnostics">DIAGNOSTICS</h3>
<p>상세 부분에는 해당 웹페이지의 문제점과, 문제점을 해결하기 위한 가이드들을 알려준다.
<img src="https://velog.velcdn.com/images/g_c0916/post/ffc63b0c-fdda-4391-860e-3604f5e52fda/image.png" alt=""></p>
<h3 id="passed-audits">PASSED AUDITS</h3>
<p>이미 잘 적용하고 있는 항목들을 나타내준다.
<img src="https://velog.velcdn.com/images/g_c0916/post/ff1e635f-364e-4690-96a8-8e3cf9b8d7f0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조_String] JavaScript 문자열 (1)]]></title>
            <link>https://velog.io/@g_c0916/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-JavaScript-%EB%AC%B8%EC%9E%90%EC%97%B4-1</link>
            <guid>https://velog.io/@g_c0916/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-JavaScript-%EB%AC%B8%EC%9E%90%EC%97%B4-1</guid>
            <pubDate>Mon, 10 Apr 2023 04:08:01 GMT</pubDate>
            <description><![CDATA[<hr>
<h3 id="문자열-접근">문자열 접근</h3>
<p>.charAt(index)는 0부터 시작하는 인덱스를 입력 값으로 받고 <strong>문자열의 해당 인덱스 위치에 있는 문자를 반환</strong>한다.</p>
<pre><code class="language-js">&#39;dog&#39;.charAt(1); // &quot;o&quot;를 반환한다.</code></pre>
<hr>
<p>문자열(여러 문자들) 접근을 위해 <strong>지정된 인덱스 사이의 문자들을 반환</strong>하는 .substring(startIndex, endIndex)를 사용한다.</p>
<pre><code class="language-js">&#39;YouTube&#39;.substring(1,2); // &#39;o&#39;를 반환한다.
&#39;YouTube&#39;.substring(3,7); // &#39;Tube&#39;를 반환한다.</code></pre>
<p>두번째 매개변수(endIndex)를 전달하지않으면 지정된 시작 위치부터 끝까지의 모든 문자 값들을 반환한다.</p>
<pre><code class="language-js">&#39;YouTube&#39;.substring(1); // &#39;ouTube&#39;를 반환한다.</code></pre>
<hr>
<h3 id="문자열-비교">문자열 비교</h3>
<p>대부분의 프로그래밍 언어에는 문자열을 비교할 수 있는 함수가 존재한다. 자바스크립트에서는 &lt;, &gt; 연산자를 사용해 쉽게 수행 할 수 있다.</p>
<pre><code class="language-js">let a = &#39;a&#39;;
let b = &#39;b&#39;;

console.log(a &lt; b); // true</code></pre>
<hr>
<p>길이가 다른 두 문자열을 비교한다면 <strong>문자열의 시작부터 비교하기 시작해 더 짧은 길이의 문자열 길이만큼까지만 비교</strong>한다.</p>
<pre><code class="language-js">let a = &#39;add&#39;;
let b = &#39;b&#39;;

console.log(a &lt; b); // true</code></pre>
<pre><code class="language-js">let a = &#39;add&#39;;
let b = &#39;bb&#39;;

console.log(a &lt; b); // false</code></pre>
<p>&#39;a&#39;와 &#39;b&#39;를 비교한다음 &#39;d&#39;와 &#39;b&#39;를 비교한다. &#39;bb&#39;의 모든 문자를 비교했기 때문에 비교 처리는 중단된다.</p>
<hr>
<h3 id="문자열-검색">문자열 검색</h3>
<p>문자열 내에 특정 문자열을 찾기 위해 .indexOf(searchValue [ , fromIndex ])를 사용한다. 검색 하고자 하는 문자열을 매개변수로 받는다. 선택적으로 검색 시작 인덱스를 지정하는 매개변수도 받는다.
.indexOf 함수는 일치하는 문자열의 위치를 반환한다. 발견하지 못한 경우 -1이 반환된다.</p>
<pre><code class="language-js">&#39;Red Dragon&#39;.indexOf(&#39;Red&#39;); // 0 반환
&#39;Red Dragon&#39;.indexOf(&#39;RedScale&#39;); // -1 반환
&#39;Red Dragon&#39;.indexOf(&#39;Dragon&#39;, 0); // 4 반환
&#39;Red Dragon&#39;.indexOf(&#39;Dragon&#39;, 4); // 4 반환
&#39;Red Dragon&#39;.indexOf(&#39;&#39;, 9); // 9 반환</code></pre>
<hr>
<p>문자열 내에 특정 문자들이 몇번 등장하는지 셀 수 있다.</p>
<pre><code class="language-js">const str = &quot;He&#39;s my king from this day until his last day&quot;;
let count = 0;
let pos = str.indexOf(&#39;a&#39;);

while(pos !== -1) {
  count++;
  pos = str.indexOf(&#39;a&#39;, pos + 1);
}

console.log(count); // 3 반환
</code></pre>
<hr>
<p>마지막으로 startsWith은 문자열이 특정 입력으로 시작하면 true(불리언)를 반환하고 endsWith은 문자열이 특정 입력으로 끝나면 true를 반환한다.</p>
<pre><code class="language-js">&#39;Red Dragon&#39;.startsWith(&#39;Red&#39;); // true
&#39;Red Dragon&#39;.endsWith(&#39;Dragon&#39;); // true
&#39;Red Dragon&#39;.startsWith(&#39;Dragon&#39;); // false
&#39;Red Dragon&#39;.endsWith(&#39;Red&#39;); // false</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조_Number] 소수,  소인수분해]]></title>
            <link>https://velog.io/@g_c0916/%EC%86%8C%EC%88%98-%EC%86%8C%EC%9D%B8%EC%88%98%EB%B6%84%ED%95%B4</link>
            <guid>https://velog.io/@g_c0916/%EC%86%8C%EC%88%98-%EC%86%8C%EC%9D%B8%EC%88%98%EB%B6%84%ED%95%B4</guid>
            <pubDate>Tue, 28 Mar 2023 12:51:05 GMT</pubDate>
            <description><![CDATA[<h3 id="소수-판별">소수 판별</h3>
<pre><code class="language-js">function isPrime(n) {
  if (n &lt;= 1) {
    return false;
  }

  // 2부터 n-1까지의 수로 나눈다
  for (let i = 2; i &lt; n; i++) {
    if (n % i === 0) {
      return false;
    }
  }

  return true;
}</code></pre>
<p>위의 코드의 시간복잡도는 O(N)이다. 하지만 소수 판별 시 기본적으로 2의 배수는 무시해도 되며 2,3을 제외한 모든 소수가 6k-1, 6k+1(k는 정수)의 형태인것을 알고나면 다음과 같이 개선을 더 할 수 있다.</p>
<blockquote>
<p>5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47...</p>
</blockquote>
<h4 id="시간-복잡도-osqrtn">시간 복잡도 O(sqrt(N))</h4>
<pre><code class="language-js">function isPrime(n) {
  // 1보다 작은 수와, 2,3의 예외 처리를 해준다.
  if (n &lt;= 1) return false;
  if (n &lt;= 3) return true;


  // 2와 3의 합성수인 경우를 처리 해준다.
  if (n % 2 === 0 || n % 3 === 0) return false;


  // 2,3의 합성수에 대한 예외처리를 했기때문에 
  // 5부터 시작해 n의 제곱근까지 
  // 6k-1, 6k+1 소수로 분해가 가능한지 검사하면 된다. ( i+=6 )
  // 소수로 나눠진다면 합성수이고, 나눠지지 않는다면 소수이다.
  for (let i = 5; i ** 2 &lt;= n; i += 6) {
    if (n % i === 0 || n % (i + 2) === 0) {
      return false;
    }
  }

  return true;
}</code></pre>
<blockquote>
<p>합성수는 소수들의 합성으로 이루어져 있기 때문에 자연수에서 소수를 제외하면 모두 합성수이다. 
 그렇기 때문에 소수는 1과 자기자신밖에 나눠지지않고, 소수를 제외한 합성수는 소수로 분해가 가능하다. (이렇게 합성수를 소수로 분해하는것이 소인수분해이다)</p>
</blockquote>
<hr>
<h3 id="소인수분해">소인수분해</h3>
<h4 id="시간-복잡도-osqrtn-1">시간 복잡도 O(sqrt(N))</h4>
<pre><code class="language-js">function primeFactors(n) {
  const arr = [];
  // n이 2로 나눠질 수 있는 만큼 2를 배열에 추가하고 2로 나눠준다.
  while (n % 2 === 0) {
    arr.push(2);
    n = n / 2;
  }

  // 이 지점에서 n은 홀수임이 확실하다.
  // 따라서 수를 2씩 증가시키면서 확인할 수 있다. (i += 2)
  for (let i = 3; i ** 2 &lt;= n; i += 2) {
    while (n % i === 0) {
      arr.push(i);
      n = n / i;
    }
  }

  // 위의 반복문에서 제곱근까지 확인했음에도 나눠떨어지는 수가 없다면 남은 값은 소수이다. 
  // 해당값을 배열에 추가해준다. (남은 값이 2보다 큰 소수인 경우만 처리)
  if (n &gt; 2) {
    arr.push(n);
  }

  return console.log(arr);
}

primeFactors(10); // [2, 5]
primeFactors(2000334214140); // [ 2, 2, 3, 5, 193, 172740433 ]</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[expo] eas build (Android) 에러]]></title>
            <link>https://velog.io/@g_c0916/expo-eas-build-Android-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@g_c0916/expo-eas-build-Android-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Tue, 21 Feb 2023 08:38:25 GMT</pubDate>
            <description><![CDATA[<p>expo에서 최근 expo build의 지원이 중단되고 eas build 로 앱을 빌드하도록 변경 되었다. 
expo 공식 홈페이지에 eas init 설정하는 점이 있지만 크게 어려운 것은 없고 이전에 
<code>expo build:android</code> 명령어로 빌드해서 구글 플레이스토어에 업로드 한 프로젝트이기 때문에 설정을 마치고 <code>eas build -p android --profile preview</code> 명령어로 빌드를 해보았지만 에러가 발생했다. 내가 겪었던 에러는 다음과 같다.</p>
<pre><code>Build failed: Gradle build failed with unknown error. 
See logs for the &quot;Run gradlew&quot; phase for more information.</code></pre><p>알 수 없는 이유 때문이라니 원인을 어떻게 찾아야할지 막막했다. 몇일동안 원인을 찾지 못하고 손을 놓아버렸다가 다시 정신차리고 구글링해보고 겨우 해결을 했다.</p>
<p>먼저 해당 오류의 자세한 내용은 <a href="https://expo.dev/">https://expo.dev/</a> 에서 자신의 프로젝트에 들어가면 실패한 빌드에 대한 정보가 나오는데 logs를 살펴볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/g_c0916/post/0bb7856d-fe3e-4571-9c74-5f2bb55f8302/image.png" alt="">
<strong>Run gradlew</strong> 탭을 눌러보면 상당히 많은 곳에 [stderr] 표시가 있을 수 있다. 그 중에 원인을 찾아 나가보면 될 것이다.</p>
<p>나는 위의 에러코드를 볼 수 있는 dev tool이 있는지 모르고 몇일간 구글링을 통해 위 에러를 해결해보았다.</p>
<p>나와 같은 에러를 겪은 사람들이 몇몇 있었고, 누군가가 제시해 준 해결방법도 다양했지만 내 상황에는 적용되지 않는 방법들이었다.</p>
<ul>
<li>expo sdk 버전 문제</li>
<li>dependencies 라이브러리 충돌 문제</li>
<li>env 설정 문제</li>
</ul>
<p>expo 버전도 변경해보고, 문제가 될만한 라이브러리도 삭제해보고 환경변수도 eas.json에 설정 해봤지만 문제가 해결되지는 않았다.</p>
<h3 id="단계별로-원인을-찾기-위해-새로운-프로젝트를-생성했다">단계별로 원인을 찾기 위해 새로운 프로젝트를 생성했다.</h3>
<ol>
<li>기존 프로젝트와 같이 eas init 설정을 하고 eas build 키워드를 통해 빌드를 했다. <strong>(eas 설정 문제x)</strong></li>
<li>기존 프로젝트에 종속성으로 포함한 라이브러리들을 새로운 프로젝트에 조금씩 추가하면서 빌드를 해보고 에러가 발생하는 것이 있는지 찾아보았다. <strong>(라이브러리 문제x)</strong></li>
<li>환경변수를 eas.json에 설정하고 빌드를 했다. <strong>(env 문제x)</strong></li>
<li>여기까지 아무런 에러가 발생하지않았다. <strong>(설정에는 문제가 없다.)</strong></li>
<li>프로젝트를 구현한 코드 중 어딘가에서 에러가 발생하고 있다는 판단이 들었다.</li>
<li>dev tool에서 빌드 에러에 관련된 내용들 발견. 여러 에러들중에 firebase에 대한 에러가 눈에 띄었다.</li>
<li>구글링 해본결과 firebase app 초기화 코드에 문제가 있다는 것을 찾았다.</li>
</ol>
<pre><code class="language-js">import firebase from &quot;firebase/compat/app&quot;;
import &quot;firebase/compat/database&quot;;
import &quot;firebase/compat/storage&quot;;

const firebaseConfig = {
  apiKey: process.env.API_KEY,
  authDomain: process.env.AUTH_DOMAIN,
  projectId: process.env.PROJECT_ID,
  storageBucket: process.env.STORAGE_BUCKET,
  messagingSenderId: process.env.MESSAGING_SENDER_ID,
  appId: process.env.APP_ID,
  measurementId: process.env.MEASUREMENT_ID,
  databaseURL: process.env.DATABASE_URL,
};

// 기존 코드
// if (!firebase.apps.length) {
//   firebase.initializeApp(firebaseConfig);
//}

if (!firebase.apps.length) {
  app = firebase.initializeApp(firebaseConfig);
} else {
  app = firebase.app();
}

export const db = firebase.database();</code></pre>
<p>expo build 로 할땐 나타나지 않았던 에러인데 eas build 에서 발생한 이유는 확실하지 않지만, 앱을 초기화하기 전에 실행 중인 앱 목록을 확인하여 실행중인 앱이 없을때 초기화하고 실행중인 앱이 있으면 초기화하지않고 실행하게끔 수정을했다.
현재는 <code>eas build -p android --profile preview</code> 도 잘 되고 구글 플레이스토어에 업데이트도 잘 되었다.</p>
<hr>
<h2 id="결론">결론</h2>
<p>나와같은 문제가 발생한 경우라면 위와같이 firebase 코드를 수정하면 해결하면 될거라고 생각하고, 그 외의 문제는 <a href="https://expo.dev/">https://expo.dev/</a> 에서 실패한 빌드에 대한 logs를 볼 수 있으니 해당 내용을 먼저 확인해보길! </p>
<pre><code>Build failed: Gradle build failed with unknown error. 
See logs for the &quot;Run gradlew&quot; phase for more information.</code></pre><p>해당 에러메세지로 구글링하게되면 원인이 너무 다양하고 expo 커뮤니티가 크지 않기때문에 정보도 많지 않아서 시간을 엄청나게 소비하게 될 가능성이 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[심심해서 만들어본 몬티홀 시뮬레이터]]></title>
            <link>https://velog.io/@g_c0916/%EC%8B%AC%EC%8B%AC%ED%95%B4%EC%84%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%EB%AA%AC%ED%8B%B0%ED%99%80-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@g_c0916/%EC%8B%AC%EC%8B%AC%ED%95%B4%EC%84%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%EB%AA%AC%ED%8B%B0%ED%99%80-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Mon, 13 Feb 2023 07:47:18 GMT</pubDate>
            <description><![CDATA[<h2>심심해서 만들어본 몬티홀 시뮬레이터</h2>

<p><a href="https://choigicheol.github.io/montyHall/">https://choigicheol.github.io/montyHall/</a></p>
<p>몬티홀 문제는 예전에 DP라는 드라마에서 처음 들어봤는데 당시에 드라마가 너무 재밌어서 남은거 보느라 크게 신경안쓰고 넘어갔었다.</p>
<p>그러다 몇일전 인터넷 검색 중에 몬티홀 문제를 우연히 본 김에 나무위키에서 자세하게 읽어봤는데 머리로는 바로 이해가능하지만 이상하게 좀 신기해서 직접 표본을 여러번 돌려보고 싶어 알고리즘 푼다고 생각하고 시뮬레이터로 간단하게 만들어봤다.</p>
<hr>
<p>몬티홀 문제 캐나다-미국 TV 프로그램 사회자가 진행하던 미국 오락 프로그램 《Let&#39;s Make a Deal》에서 유래한 확률 문제다.</p>
<hr>
<p>규칙은 다음과 같다.</p>
<pre><code>- 닫혀 있는 문 3개가 있다.
- 한 문 뒤에는 상품(=자동차)이 있고, 나머지 두 문은 꽝(=염소)이다.
- 참가자는 이 3가지 문 중 하나를 골라야 상품을 얻을 수 있다.
- 참가자가 문 하나를 고르면, 사회자는 남은 2가지 문 중에 염소가 있는 꽝인 문을 하나 열고
  그게 &#39;꽝&#39;이라는 사실을 밝힌다.
- 여기서 참가자에게 다른 문으로 바꿀 수 있는 기회가 주어진다.</code></pre><hr>
<p>남은 문은 2개이고 그 중 하나를 고르는 것이나 마찬가지니 선택을 바꾸나 안바꾸나 50:50으로 자동차를 고를 확률은 똑같을 것이라고 생각하는 사람이 많을 수 있는데, <code>이때 선택을 바꾸는 것이 훨씬 이득</code> 이다.  심지어 문을 바꾸지 않았을때에 비해 문을 바꿨을 때 상품이 자동차일 확률이 2배나 높다.</p>
<p>간단하게 설명하면 </p>
<h3 id="선택을-바꾼다고-했을-때"><strong>선택을 바꾼다고 했을 때</strong></h3>
<p>최초의 3개의 문에서 내가 선택한 문이 염소면 사회자가 공개한 염소를 제외한 다른 문을 선택할 것이기 때문에 자동차를 얻는다. 
반대로 최초 선택에서 자동차를 골랐다면 선택을 바꿈으로써 염소를 얻게 된다.</p>
<p>즉, 3개의 문에서 최초 선택으로 염소를 고를 확률은 2/3 = 66.67% 이기 때문에 선택을 바꾸면 자동차일 확률이 66.67%가 된다. </p>
<p>선택을 바꾸지 않고 같은 문을 선택한다고 하면, 최초 선택에서 꼭 자동차를 골랐어야 하니 1/3 = 33.33%로 선택을 바꾸냐 바꾸지 않느냐에 따라 자동차를 고를 확률이 2배나 차이나게 된다.</p>
<hr>
<p>조건이 사회자가 정답을 알고있어야하고, 꽝 문을 열어주고, 다시 선택할 기회를 주고 여러 조건들이 붙긴하지만 그냥 봤을때는 반반의 확률처럼 보이는것이 알고보면 훨씬 높은 확률로 정답을 선택할 수 있다는 점이 뭔가 신기했다.</p>
<p>그래봤자 실전은 될될안될안이라고 10%의 확률이 연달아 맞을 수도 있는게 확률이니까... 
첫선택으로 자동차를 고를 확률이 그래도 1/3은 있으니 그 확률에 자신이 있다면 선택을 바꾸지 않는것도 뭐~ 
이상 취미활동 끝</p>
]]></description>
        </item>
    </channel>
</rss>