<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>pyo-sh.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 24 Feb 2023 09:31:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>pyo-sh.log</title>
            <url>https://images.velog.io/images/pyo-sh/profile/c44021ee-34d8-4ef5-ac16-080aa245b6cf/social.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. pyo-sh.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/pyo-sh" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Yarn Berry 환경과 PnP 기능에서 Prisma 사용]]></title>
            <link>https://velog.io/@pyo-sh/Yarn-Berry-%ED%99%98%EA%B2%BD%EA%B3%BC-PnP-%EA%B8%B0%EB%8A%A5%EC%97%90%EC%84%9C-Prisma-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@pyo-sh/Yarn-Berry-%ED%99%98%EA%B2%BD%EA%B3%BC-PnP-%EA%B8%B0%EB%8A%A5%EC%97%90%EC%84%9C-Prisma-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Fri, 24 Feb 2023 09:31:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Prisma 사용 방법이 아닌 오류 해결에 관한 글입니다.</p>
</blockquote>
<h3 id="yarn-berry-환경-사용-이유">Yarn Berry 환경 사용 이유</h3>
<p>현재 블로그를 개발하고 있는데 나의 프로젝트의 상황은 현재 이러하다</p>
<ol>
<li>하나의 Repository에 여러 프로젝트를 관리하고 있는 상황이다
 (현재는 Front-end와 Back-end를, 이후 직접 서버 인스턴스를 구축할 때 확장 가능성 염두)</li>
<li>하나의 블로그 서비스를 제공하는 것이 목적이기 때문에 Type 지정 시 공통 Type이 있을 수 있다</li>
</ol>
<p>따라서 Yarn Berry 환경으로 구축해 workspace 기능을 손쉽게 사용하고 PnP 기능과 함께 Zero Install 설정으로 배포 속도를 높여보고자 했다.</p>
<h3 id="prisma-사용">Prisma 사용</h3>
<p>Back-end 개발 과정에서 데이터베이스 구축을 어떻게 진행할 것인지 고민을 많이 했었다.</p>
<ul>
<li><code>postgreSQL</code> 과 <code>mysql</code></li>
<li><code>graphQL</code>과 <code>RestAPI</code></li>
<li>어떤 ORM을 사용할까...</li>
</ul>
<p>이러니 저러니 해도 결국 내가 개발하는 블로그였기 때문에 선택의 자유가 높았다.
새로 공부할 수 있으면서도 러닝 커브가 크지 않았으면 했기 때문에 이미 사용해본 바 있는 <code>mysql</code>과 함께 새로운 ORM인 <code>prisma</code>를 사용해보고자 했다.</p>
<h2 id="문제-발생">문제 발생</h2>
<p>그렇게 개발을 진행하던 도중 문제가 발생했다.</p>
<ol>
<li><code>prisma</code> 스키마 파일 생성</li>
<li><code>prisma generate</code> 명령어를 통해 <code>@prisma/client</code> 라이브러리에 스키마 관련 파일을 생성</li>
<li>해당 변경된 파일들을 통해 <code>PrismaClient</code> 코드에 접근 가능</li>
</ol>
<p>위와 같은 로직으로 동작되어야 하는 코드에서 결국 다음과 같은 오류가 발생한다.</p>
<blockquote>
<p>&#39;&quot;@prisma/client&quot;&#39; 모듈에 내보낸 멤버 &#39;PrismaClient&#39;이(가) 없습니다.
    &#39;&quot;@prisma/client&quot;&#39; has no exported member &#39;PrismaClient&#39;.</p>
</blockquote>
<p>혹은 오류가 발생하지 않더라도 Type추론이 되지 않을 가능성이 높다.</p>
<p>현재 나의 프로젝트는 Yarn Berry 환경에서는 node_modules 폴더가 아닌 PnP로 되어있는 라이브러리 파일을 참조하기 때문에 기본 prisma 설정으로는 나의 스키마에 대한 코드를 덮어씌울 수 없기 때문이다.</p>
<h3 id="접근">접근</h3>
<p><code>prisma</code> 폴더 내에서 다음과 같이 결과물 코드를 어디에 놓을 것인지 설정할 수 있다.</p>
<pre><code>generator client {
  provider      = &quot;prisma-client-js&quot;
  output = &quot;../../... 경로&quot;
}</code></pre><p>결국 핵심은 <code>prisma generate</code>를 통해 생성된 코드를 어떻게 사용할 것인가 이다.</p>
<h3 id="해결">해결</h3>
<ol>
<li><p>먼저 <code>@yarnpkg/pnpify</code> 를 통해 <code>prisma generate</code>를 실행할 수 있도록 한다.
그렇지 않으면 <code>package-lock.json</code> 파일과 <code>node_modules</code> 파일이 생성된다.</p>
<pre><code>yarn add @yarnpkg/pnpify</code></pre></li>
<li><p>그 후 다음과 같이 <code>prisma</code> 파일 코드를 변경한다.</p>
<pre><code class="language-prisma">generator client {
  provider = &quot;prisma-client-js&quot;
  output = &quot;./&quot;
}

datasource db {
  provider = &quot;mysql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}

... DB 내용</code></pre>
</li>
<li><p><code>@yarnpkg/pnpify</code> 라이브러리를 통해 <code>prisma generate</code>를 실행해준다.</p>
<pre><code>yarn pnpify prisma generate</code></pre></li>
<li><p>해당 파일을 사용한다.
나는 라이브러리를 잘 사용하기 위해 tsconfig에서 절대 경로를 설정해 사용했다.</p>
<pre><code class="language-json"> // tsconfig.json
 {
   &quot;compilerOptions&quot;: {
     &quot;baseUrl&quot;: &quot;./&quot;,
     &quot;paths&quot;: {
       &quot;@prisma&quot;: [&quot;./prisma/index&quot;],
       //...
     }
   }
}</code></pre>
<pre><code class="language-typescript">// 동작!
import { PrismaClient } from &quot;@prisma&quot;;
 const prisma = new PrismaClient();

// ...</code></pre>
</li>
<li><p>해당 파일을 라이브러리처럼 사용하기 때문에 <code>.gitignore</code>에 추가해주었다.</p>
<pre><code># prisma build files 
prisma/*
!prisma/schema.prisma</code></pre></li>
<li><p>명령어를 사용하기 쉽게 <code>package.json</code>에서 설정도 해서 완료!</p>
<pre><code class="language-json">// ...  
&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;nodemon --exec ts-node -r tsconfig-paths/register ./src/app.ts&quot;,
    &quot;prebuild&quot;: &quot;yarn prisma:generate&quot;,
    &quot;build&quot;: &quot;yarn prebuild &amp;&amp; yarn tsc&quot;,
    &quot;start&quot;: &quot;yarn build &amp;&amp; node ./build/app.js --node-args=&#39;-r ./tsconfig-paths-bootstrap.js&#39;&quot;,
    &quot;prisma:generate&quot;: &quot;yarn pnpify prisma generate&quot;
  },
// ...</code></pre>
</li>
</ol>
<h2 id="다른-방법">다른 방법</h2>
<p>검색중 <a href="https://nwlee.com/p/yarn-pnp-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-prisma-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95">nwlee - Yarn PnP 프로젝트에 Prisma ORM 붙이는 방법</a> 해당 블로그의 글도 있었다.</p>
<ol>
<li><p>yarn 프로젝트에 typescript 적용하기</p>
<pre><code class="language-bash"># 이 작업이 필요하다
yarn plugin import typescript
# vscode 에서 typescript를 인식하기 위한 명령어
yarn dlx @yarnpkg/sdks vscode</code></pre>
</li>
<li><p><code>.yarnrc.yml</code> 파일 수정하기</p>
<pre><code>nodeLinker: node-modules

plugins:
  - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
    spec: &#39;@yarnpkg/plugin-typescript&#39;

yarnPath: .yarn/releases/yarn-3.2.1.cjs</code></pre></li>
<li><p>PnP 방식으로 설치되지 않도록 설정</p>
<pre><code>yarn unplug prisma @prisma/client</code></pre></li>
</ol>
<p>여기서는 라이브러리들을 <code>node_modules</code>폴더를 사용하고 참조하게 함으로서 이를 가능하게 하는 방법을 확인하기는 했는데, 라이브러리 파일을 중복으로 가지고 있는 것 같아 패스했다.</p>
<h3 id="참고">참고</h3>
<p><a href="https://github.com/prisma/prisma/issues/8765">https://github.com/prisma/prisma/issues/8765</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS의 Rendering Data Fetching]]></title>
            <link>https://velog.io/@pyo-sh/NextJS-Rendering-Data-Fetching-m7nvqn5y</link>
            <guid>https://velog.io/@pyo-sh/NextJS-Rendering-Data-Fetching-m7nvqn5y</guid>
            <pubDate>Thu, 09 Feb 2023 13:52:59 GMT</pubDate>
            <description><![CDATA[<p>올해부터 마음을 다잡기 위해 개인 블로그를 만드는 프로젝트를 하고 있었다.
기획 단계를 건너뛰고 개발부터 들어가다보니 별 생각없이 React + vite 환경으로 진행했는데
로고 사진과 <code>manifest.json</code> 등 홈페이지에서 필요한 것들을 구성하다 보니 SEO 관련 걱정을 하지 않을 수 없었다.</p>
<p>2022년 후반부 쯤 누군가에게 &quot;Google의 검색 엔진에서 SPA도 SEO가 되는 것 같더라&quot; 라는 말을 들었다.
그 당시 아무렇지 않게 일 잘하는 구글을 칭찬하고 넘어갔는데 문득 궁금해서 해당 내용을 찾아보기로 했다.
<a href="https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering?hl=ko">Google 검색 센터</a>에서 해당 내용에 대한 힌트를 조금 얻을 수 있었는데 상위 빨간색 문구를 보고 Next.js로의 Migration을 결심하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/pyo-sh/post/92f00b92-d63e-4cb6-b509-80a84ac1820d/image.png" alt="Notification_Google"></p>
<p><a href="https://nextjs.org/docs/migrating/from-create-react-app">NextJS 공식 홈페이지</a>를 참고해서 Next.js 13버전으로 진행하려 했는데 예전에 Next를 사용했을 때와는 많이 다른 구조를 가지고 있는 것 같아서 기초부터 공부하고자 해당 글을 남기기로 했다.</p>
<h2 id="nextjs">Next.js</h2>
<p>Next.js는 클라이언트가 웹 페이지를 요청하면 서버에서 미리 웹 페이지를 Pre-Rendering 하고 이에 대한 결과물인 HTML을 클라이언트에게 전송한다.
이후 번들링 된 리액트코드를 클라이언트에게 전송하며 클라이언트는 이를 받아 렌더링하며 Hydrate하게 된다.</p>
<p>하지만 웹 페이지를 Pre-Rendering 할 때 필요한 정보가 클라이언트에게 있거나 데이터 서버에서 받아와야 할 때 등 페이지 정보마다 다르게 진행해야 할 경우가 있다.
이를 위해 Next.js에선 Data-Fetching 기능을 내부에서 제공하게 되는데 자세히 보도록 하자.</p>
<h3 id="getserversideprops"><a href="https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props">getServerSideProps</a></h3>
<ul>
<li>페이지 요청 시 다른 곳에서 데이터를 가져와 페이지를 렌더링해야 하는 경우에 사용한다.</li>
<li>서버 측에서만 실행되고 브라우저에서는 실행되지 않는다.</li>
<li>pages 폴더에서 각 페이지 파일에서 독립 실행형 기능으로 내보내야한다.<ul>
<li>_app, _document, _error에서 내보낼 수 없다</li>
</ul>
</li>
</ul>
<p>내가 이해한 해당 함수의 진행은 다음과 같다.</p>
<img alt="getServerSideProps" src="https://velog.velcdn.com/images/pyo-sh/post/1f4a7f72-b8f6-443c-a5b5-a0435dcdef2e/image.png" width="500px"/>

<p>요청 후 실행되어서 반환되는 데이터 값을 사용해 렌더링하므로 각 사용자에 대한 데이터를 렌더링해야 할 때 사용할 수 있다.
하지만 매 요청마다 실행되는걸 기다리기 때문에 성능 저하를 걱정해야 한다.
기본적으로 CDN에 캐싱되지 않으며 캐싱 기능을 사용하려면 따로 설정해주어야 하는데
캐싱 기능을 적용하는 상황이라면 아래의 함수들을 고려해 볼 필요가 있다.</p>
<pre><code class="language-javascript">페이지에서
function Page({ data }) {
  return &lt;&gt;~~&lt;/&gt;
}

// import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from &quot;next&quot;;

// Function Type = GetServerSideProps
export async function getServerSideProps(context) {
  // Prop (context) Type = GetServerSidePropsContext
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // Return Type = GetServerSidePropsResult
  return { props: { data } }
}

export default Page</code></pre>
<h3 id="getstaticprops"><a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-props">getStaticProps</a></h3>
<ul>
<li>빌드 시에 함수가 실행되고 이를 반환한 데이터를 사용한다.</li>
<li>한번 빌드 후 조작하지 않는다면 바뀌지 않는 데이터이다</li>
<li>서버 측에서만 실행되고 브라우저에서는 실행되지 않는다.</li>
<li>pages 폴더에서 각 페이지 파일에서 독립 실행형 기능으로 내보내야한다.<ul>
<li>_app, _document, _error에서 내보낼 수 없다</li>
</ul>
</li>
<li>개발 환경 (next dev)에서는 모든 요청에 대해 호출된다고 나와있다.</li>
</ul>
<p>내가 이해한 해당 함수의 진행은 다음과 같다.</p>
<img alt="getStaticProps" src="https://velog.velcdn.com/images/pyo-sh/post/69943979-88a9-4602-aee0-c16ffd3dfe50/image.png" width="500px">

<pre><code class="language-javascript">function Page({ posts }) {
  return (
    &lt;ul&gt;
      {posts.map((post) =&gt; (
        &lt;li&gt;{post.title}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )
}

// import type { GetStaticProps, GetStaticPropsContext, GetStaticPropsResult } from &quot;next&quot;;

// Function Type = GetStaticProps
export async function getStaticProps(context) {
  // Prop Type = GetStaticPropsContext
  const res = await fetch(&#39;https://.../posts&#39;)
  const posts = await res.json()

  // Return Type = GetStaticPropsResult
  return {
    props: {
      data,
    },
  }
}

export default Blog</code></pre>
<h3 id="getstaticpaths"><a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-paths">getStaticPaths</a></h3>
<p>이 기능은 Pre-Render를 위한 기능이 아니라, Static Generation 을 위한 기능이다</p>
<ul>
<li>getStaticProps와 비슷하게 빌드 시에 함수가 실행된다.</li>
<li>페이지에서 동적 경로를 사용할 때 예상 가능한 path들에 대해 페이지를 미리 빌드하는 기능이다.</li>
<li>경로를 지정해주기만 한다면 정적으로 사전 렌더링을 시작한다.</li>
<li>getServerSideProps와 함께 사용하지 못하며 getStaticProps와 사용해야 한다.</li>
</ul>
<p>내가 이해한 해당 함수의 진행은 다음과 같다.</p>
<img alt="getStaticPaths" src="https://velog.velcdn.com/images/pyo-sh/post/617f9b3f-99df-425e-b630-0d9840e7442b/image.png" width="500px">

<p>paths 값으로 빈 배열을 넘겨서 페이지 빌드를 연기할 수 있다고 한다.</p>
<pre><code class="language-javascript">// pages/posts/[id].js

// import { GetStaticPaths, GetStaticPathsContext, GetStaticPathsResult } from &quot;next&quot;;
// Function Type = GetStaticProps
export async function getStaticPaths(context) {
  // Prop Type = GetStaticPathsContext
  // Return Type = GetStaticPathsResult
  return {
    paths: [{ params: { id: &#39;1&#39; } }, { params: { id: &#39;2&#39; } }],
    fallback: false, // can also be true or &#39;blocking&#39;
  }
}

export async function getStaticProps(context) {
  return {
    props: { post: {} },
  }
}

export default function Post({ post }) {
  return &lt;ul&gt;
      {posts.map((post) =&gt; (
        &lt;li&gt;{post.title}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
}</code></pre>
<h2 id="모든-페이지에서-">모든 페이지에서 ?!</h2>
<p>공식 홈페이지에 따르면 아래와 같은 말이 있다.</p>
<blockquote>
<p>pages 폴더에서 각 페이지 파일에서 독립 실행형 기능으로 내보내야한다.
=&gt; _app, _document, _error에서 내보낼 수 없다</p>
</blockquote>
<p>Next.js 개발 중 모든 페이지에 대해서 getServerSideProps를 실행해야 할 경우 어떻게 해결해야 할까?</p>
<h3 id="getinitialprops"><a href="https://nextjs.org/docs/api-reference/data-fetching/get-initial-props">getInitialProps</a></h3>
<p>Next.js의 예전 버전에서 이 getInitialProps 함수를 이용해서 컴포넌트에 필요한 Props를 해결해왔었다.
_app, _document, _error와 같은 페이지의 컴포넌트가 동작할 때 여전히 getInitialProps 함수가 있다면 실행되기 때문에 해당 함수를 지정해주어 모든 페이지에 대해서 실행할 수 있다.</p>
<h3 id="automatic-static-optimization"><a href="https://nextjs.org/docs/advanced-features/automatic-static-optimization">Automatic Static Optimization</a></h3>
<p>Next 9.3 이후 버전에서 getServerSideProps, getStaticProps, getStaticPaths등의 기능이 나왔는데, Next.js에서 getInitialProps보다 위 함수들을 사용하길 권고하는 이유가 있다.</p>
<p>이 기능들은 Automatic-Static-Optimization 기능을 지원해준다고 한다</p>
<blockquote>
<p>If getServerSideProps or getInitialProps is present in a page, Next.js will switch to render the page on-demand, per-request (meaning Server-Side Rendering).
If the above is not the case, Next.js will statically optimize your page automatically by prerendering the page to static HTML.</p>
</blockquote>
<p>요약하자면 getServerSideProps 혹은 getInitialProps가 각 페이지에서 사용되지 않을 경우 Next.js가 이를 최적화하기 위해 Static Generation 페이지를 생성하고 제공하게 된다는 의미이다.
더 자세한 내용에 대해선 공식문서에서 확인할 수 있다.</p>
<p>만약 개발하는 프로젝트 중 Static Generation 을 통한 서비스 제공이 중요하다고 생각된다면 전역에서 getInitialProps를 선언해주는 것을 고려해주는게 좋다.</p>
<h2 id="참고">참고</h2>
<p><a href="https://nextjs.org/docs/basic-features/data-fetching/overview">Next.JS - Data Fetching</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MAC] Docker 설치 해보기]]></title>
            <link>https://velog.io/@pyo-sh/MAC-Docker-%EC%84%A4%EC%B9%98-%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@pyo-sh/MAC-Docker-%EC%84%A4%EC%B9%98-%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 17 Jul 2022 10:27:40 GMT</pubDate>
            <description><![CDATA[<h2 id="1설치">1.설치</h2>
<h3 id="homebrew-cask">homebrew-cask</h3>
<p>homebrew-cask는 맥의 (GUI를 제공하는) 응용프로그램을 커맨드로 설치해주는 편리한 기능이다.</p>
<p>cask 옵션을 통해 설치한 프로그램들은 기본적으로 Applications 폴더에 들어가게 된다.</p>
<p>이를 이용해 Docker 설치를 진행하면
Desktop on Mac을 설치하고 docker-compose, docker-machine 또한 같이 설치할 수 있다.</p>
<pre><code>&gt; brew install --cask docker</code></pre><h3 id="brew가-없다면">brew가 없다면?</h3>
<p>윈도우에 설치하는 것처럼 <a href="hub.docker.com/editions/community/docker-ce-desktop-mac">Docker 설치 사이트</a>로 가서 다운받으면 된다.
이후 Docker 프로그램을 Applications 폴더로 옮겨야 한다.</p>
<h3 id="첫-실행">첫 실행</h3>
<p>Docker의 첫 실행을 해보았을 때 아래와 같은 알림이 떴다.</p>
<img width="300" src="https://velog.velcdn.com/images/pyo-sh/post/3f65822e-dd4f-4ea5-9d16-2db312cac88c/image.png"/>

<p>해당 사항으로 프로그램을 실행할 수 없었는데 당황하지 않고</p>
<ol>
<li>Applications 폴더를 Finder로 열어서 </li>
<li>Control + 우클릭 후</li>
<li>열기 사항을 클릭하면 Docker를 실행할 수 있다.</li>
</ol>
<img width="600" src="https://velog.velcdn.com/images/pyo-sh/post/04b0cdde-e4bc-4eb3-b315-122eaf5e82a6/image.png"/>

<p>이후 해당 사항에 대한 약관 동의를 진행하면 Docker가 실행된다.</p>
<p>설치 이후 실행까지 완료해 버전 확인이 올바르게 되면 잘 된 것!</p>
<pre><code>&gt; docker --version</code></pre><h2 id="2-사용">2. 사용</h2>
<p><a href="www.docker.com/resources/what-container">Docker Container에 대한 설명</a>
<a href="https://docs.docker.com/get-started/">Docker, Getting started</a></p>
<h3 id="명령어">명령어</h3>
<table>
<thead>
<tr>
<th align="center">설명</th>
<th>명령어</th>
</tr>
</thead>
<tbody><tr>
<td align="center">버전 확인</td>
<td>docker -v / docker --version</td>
</tr>
<tr>
<td align="center">다운로드된 이미지들 확인</td>
<td>docker images</td>
</tr>
<tr>
<td align="center">이미지 다운로드</td>
<td>docker pull [이미지]</td>
</tr>
<tr>
<td align="center">컨테이너 생성</td>
<td>docker create [옵션] [이미지]</td>
</tr>
<tr>
<td align="center">컨테이너 실행</td>
<td>docker start [컨테이너]</td>
</tr>
<tr>
<td align="center">컨테이너 재실행</td>
<td>docker restart [컨테이너]</td>
</tr>
<tr>
<td align="center">컨테이너 접속</td>
<td>docker attach [컨테이너]</td>
</tr>
<tr>
<td align="center">컨테이너 정지</td>
<td>docker stop [컨테이너]</td>
</tr>
<tr>
<td align="center">실행중인 컨테이너들</td>
<td>docker ps</td>
</tr>
<tr>
<td align="center">모든 컨테이너들</td>
<td>docker ps -a</td>
</tr>
<tr>
<td align="center">컨테이너 이름 변경</td>
<td>docker rename [기존 컨테이너] [새로운 이름]</td>
</tr>
<tr>
<td align="center">컨테이너 삭제</td>
<td>docker rm [컨테이너]</td>
</tr>
</tbody></table>
<h4 id="docker-생성--실행-옵션">Docker 생성 &amp; 실행 옵션</h4>
<pre><code>&gt; docker run [옵션] [이미지] [커맨드] [다른 사항들...]</code></pre><table>
<thead>
<tr>
<th align="center">옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">-d</td>
<td>Detached Mode 백그라운드 모드</td>
</tr>
<tr>
<td align="center">-p</td>
<td>호스트와 컨테이너의 포트를 연결 (포워딩)</td>
</tr>
<tr>
<td align="center">-v</td>
<td>호스트와 컨테이너의 디렉토리를 연결 (마운트)</td>
</tr>
<tr>
<td align="center">-e</td>
<td>컨테이너 내에서 사용할 환경변수 설정</td>
</tr>
<tr>
<td align="center">–name</td>
<td>컨테이너 이름 설정</td>
</tr>
<tr>
<td align="center">–rm</td>
<td>프로세스 종료시 컨테이너 자동 제거</td>
</tr>
<tr>
<td align="center">-it</td>
<td>-i와 -t를 동시에 사용한 것으로 터미널 입력을 위한 옵션</td>
</tr>
<tr>
<td align="center">–link</td>
<td>컨테이너 연결 [컨테이너명:별칭]</td>
</tr>
</tbody></table>
<br/>

<h3 id="image">Image</h3>
<h4 id="ubuntu-image">Ubuntu Image</h4>
<p>MAC에서 Cloud 서비스 배포와 비슷하게 Ubuntu 기반의 서버를 돌려보기 위해서 이미지를 먼저 받았습니다.</p>
<p>설치할 버전에 대한 고민은 <a href="https://hub.docker.com/_/ubuntu">Docker hub - Ubuntu</a>가서 확인할 수 있습니다.</p>
<pre><code>&gt; docker pull ubuntu:20.04</code></pre><h4 id="mysql-image">MySQL Image</h4>
<p>MAC의 Local에서 MySQL을 설치해서 사용하기엔 많은 귀찮음과 더러움이 따라옵니다.</p>
<p>Docker를 통해 따로 MySQL을 돌려 테스팅 서버를 만들고자 합니다.</p>
<p>설치할 버전에 대한 고민은 <a href="https://hub.docker.com/_/mysql/?tab=tags">Docker hub - Mysql</a>가서 확인할 수 있습니다.</p>
<pre><code>&gt; docker pull mysql</code></pre><p>설치 이후 다음 명령어로 이미지들이 잘 설치되었는지 확인해봅니다.</p>
<pre><code>&gt; docker images</code></pre><img width="500" src="https://velog.velcdn.com/images/pyo-sh/post/4b8e243a-d4d2-40d6-9a71-e5b210215316/image.png"/>

<br/>

<h3 id="mysql-container">MySQL Container</h3>
<p>MySQL을 Docker에서 컨테이너를 생성해 서버를 실행해보겠습니다.</p>
<pre><code>&gt; docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=[password] -d -p 3306:3306 mysql</code></pre><p>아래 명령어를 통해 어떤 Docker Container가 있는지 확인할 수 있습니다</p>
<pre><code>&gt; docker exec -it mysql-container bash</code></pre><p><img src="https://velog.velcdn.com/images/pyo-sh/post/55177e52-8e68-4201-9fe8-61a45572498f/image.png" alt="docker containers"></p>
<h4 id="접속">접속?</h4>
<p>아래 명령어를 통해 Container를 터미널로 접속할 수 있으며 이후 MySQL을 사용하는 것과 같이 사용할 수 있습니다.</p>
<pre><code>&gt; docker exec -it mysql-container bash

bash# mysql -u root -p</code></pre><h3 id="참고">참고</h3>
<p><a href="https://dev-youngjun.tistory.com/3">https://dev-youngjun.tistory.com/3</a>
<a href="https://poiemaweb.com/docker-mysql">https://poiemaweb.com/docker-mysql</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MAC] MySQL 설치  해보기]]></title>
            <link>https://velog.io/@pyo-sh/MAC-MySQL-%EC%84%A4%EC%B9%98-%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@pyo-sh/MAC-MySQL-%EC%84%A4%EC%B9%98-%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 11 Jul 2022 01:57:02 GMT</pubDate>
            <description><![CDATA[<h4 id="들어가기에-앞서">들어가기에 앞서...</h4>
<p>MAC 초보자로서 MySQL을 <code>brew</code>로 설치하는 것을 처음해보았다.</p>
<p>윈도우는 응용프로그램 설치 &amp; 서버 ON/OFF 관리 등 신경쓸 것이 많았는데,
아직 <code>brew</code> 사용을 덜해서인지 아니면 MAC OS가 편해서인지는 모르겠지만 매우 간단함을 느끼고 있다.</p>
<h2 id="1-설치">1. 설치</h2>
<pre><code>&gt; brew install mysql</code></pre><p>설치 이후에 버전 확인이 올바르게 되면 잘 된 것!</p>
<pre><code>&gt; mysql -V</code></pre><p>이후에 MySQL의 server를 사용 여부를 아래의 명령어로 컨트롤 할 수 있다.</p>
<pre><code>&gt; mysql.server start
&gt; mysql.server stop</code></pre><h3 id="brew가-없다면">brew가 없다면?</h3>
<p>윈도우에서 설치하는 것처럼 <a href="https://dev.mysql.com/downloads/mysql/">MySQL 설치하는 사이트</a>로 가서 다운받으면 된다.</p>
<p>다른 기본 Application을 설치하는 것과 다른 것은 없었다.</p>
<h2 id="2-초기-설정">2. 초기 설정</h2>
<p>처음 설치를 하고 나면, 보안 관련 설정을 해주는게 좋지 않을까~?</p>
<p>터미널에서 아래의 명령어를 이용해 root 계정의 비밀번호를 설정해주었다.</p>
<pre><code>&gt; mysql_secure_installation</code></pre><h3 id="설정">설정</h3>
<ul>
<li><p>비밀번호의 강도를 높게할 것인지 낮게할 것인지?
→ 간단한 DB 개발이 목적이라 No</p>
</li>
<li><p>Annoymous 유저를 삭제할 것인지?
→ MySQL에서 유저를 생성하고 DB마다 유저 관리를 할 생각이라 Yes</p>
</li>
<li><p>root의 원격 접속을 허용할 것인지?
→ 다른 컴퓨터에서 접근할 일이 있을까 Yes를 했는데... (없다면 No를 하는게 좋아보인다.)</p>
</li>
<li><p>test 데이터 베이스를 삭제할 것인지?
→ Yes를 통해 사용할 일 없는 테스트 데이터를 삭제!</p>
</li>
<li><p>PRIVILEGES TABLE을 재시작 할 것인지?
→ Annoymous 유저, test 데이터 베이스 삭제를 진행했기 때문에 Yes!</p>
</li>
</ul>
<h2 id="3-접속">3. 접속</h2>
<p>위에서 MySQL 서버를 On 하는 명령어를 입력한 뒤,
터미널에서 아래와 같은 명령어로 MySQL 접속을 하면 관련 조작을 할 수 있다.</p>
<pre><code>&gt; mysql -u [유저ID] -p [?DB이름]</code></pre><p>처음엔 DB와 user가 없는 상태임으로 root 계정으로 접속할 수 있도록 하자.</p>
<h3 id="처음엔-무얼-해야하나">처음엔 무얼 해야하나?</h3>
<p>필자는 Express를 통해 간단한 테스트 DB 조작을 개발할 것이다.</p>
<p>토이 프로젝트마다 유저를 생성, 각자의 권한에 맞는 DB에만 접근할 수 있도록 할 예정이다.</p>
<p>그래서 유저와 데이터베이스를 생성하고 <strong>Express</strong>에서 잘 접근할 수 있는지 테스팅을 하였다.</p>
<h3 id="사용자">사용자</h3>
<p>해당 작업들은 mysql 데이터 베이스에 접근해서 User 값을 변경해야 한다.</p>
<pre><code>// mysql 데이터 베이스에 접근
mysql&gt; USE mysql;</code></pre><ol>
<li><p>사용자 확인</p>
<pre><code>// user 테이블의 값을 가져와 본다.
mysql&gt; SELECT host, user FROM user;</code></pre></li>
<li><p>사용자 추가
[유저ID]@[host]과 같은 경우는 MySQL 서버에 접근할 수 있는 host를 결정해 주는 부분이다.
ex) &#39;%&#39; 를 통해 host 설정을 해주면 모든 곳에서 유저가 MySQL 서버에 접근할 수 있도록 한다.
ex) &#39;localhost&#39; 를 통해 host 설정을 해주면 로컬에서만 MySQL 서버에 접근할 수 있다. </p>
<pre><code>// 유저를 생성
mysql&gt; CREATE USER &#39;[유저ID]&#39; IDENTIFIED BY &#39;[비밀번호]&#39;;
// 접근하는 곳을 제한시킨 유저 생성
mysql&gt; CREATE USER &#39;[유저ID]&#39;@&#39;[host]&#39; IDENTIFIED BY &#39;[비밀번호]&#39;;</code></pre></li>
<li><p>사용자 삭제</p>
<pre><code>mysql&gt; DROP USER &#39;[유저ID]&#39;@&#39;[host]&#39;</code></pre></li>
</ol>
<h3 id="데이터베이스">데이터베이스</h3>
<ol>
<li><p>DB 확인</p>
<pre><code>mysql&gt; SHOW databases;</code></pre></li>
<li><p>DB 생성</p>
<pre><code>mysql&gt; CREATE DATABASE [DB이름]
// 한글 사용할 수 있는 UTF8로 설정할 수 있음
mysql&gt; CREATE DATABASE [DB이름] default CHARACTER SET UTF8; </code></pre></li>
<li><p>DB 접근</p>
<pre><code>mysql&gt; USE [DB이름]</code></pre></li>
<li><p>DB에 사용자 권한 추가
DB 뒤의 * 은 모든 권한을 넘겨준다는 뜻입니다.</p>
<pre><code>mysql&gt; GRANT ALL PRIVILEGES ON [database].* TO &#39;[username]&#39;@&#39;[host]&#39;;
// 일종의 Reload
mysql&gt; FLUSH PRIVILEGES;</code></pre></li>
</ol>
<h4 id="flush-privileges">FLUSH PRIVILEGES?</h4>
<p>Privileges는 유저에 관한 권한이다.</p>
<p>유저의 권한을 변경해도 현재 MySQL 서버에서 제공하고 있는 DBMS에서는 적용되지 않는다.</p>
<p>DBMS에 적용되기 위해 권한에 대한 새로운 로딩을 제공하는 명령어가 <code>FLUSH PRIVILEGES</code>이다.</p>
<p><a href="https://www.interserver.net/tips/kb/mysql-flush-commands/">참고 사이트</a></p>
<blockquote>
<p>when we grant some privileges for a user, running the command flush privileges will reloads the grant tables in the mysql database enabling the changes to take effect without reloading or restarting mysql service.</p>
</blockquote>
<h3 id="테이블-생성">테이블 생성</h3>
<p>이제부턴 DB SQL 명령어 관련인데... 간단하게 테이블 생성까지 적어보았다.</p>
<pre><code>CREATE TABLE [테이블 이름] (
    id INT PRIMARY KEY AUTO_INCREMENT,
    [컬럼명] [데이터 형] [옵션들...],
    // ...
);</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[React] gh-pages 를 통해 Project 배포]]></title>
            <link>https://velog.io/@pyo-sh/React-gh-pages-%EB%A5%BC-%ED%86%B5%ED%95%B4-Project-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@pyo-sh/React-gh-pages-%EB%A5%BC-%ED%86%B5%ED%95%B4-Project-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Mon, 03 May 2021 23:21:17 GMT</pubDate>
            <description><![CDATA[<h1 id="gh-pages">gh-pages?</h1>
<p>github.io로 된 페이지들을 볼 수 있었는데, 이들은 모두 <a href="https://pages.github.com/">GitHub에서 제공하는 서비스</a>이다.</p>
<p>GitHub Pages는 유저의 Static HTML을 접속할 수 있게 제공한다.</p>
<p>HTML, CSS, JS 로 구성된 프로젝트를 gh-pages를 통해 작동하고 있도록 보여줄 수 있다.</p>
<h3 id="제한은-존재한다">제한은 존재한다</h3>
<p>GitHub Pages는 Static HTML을 제공한다고 했다.</p>
<p><strong>즉, Server Side Rendering을 하는 React는 이를 이용하기 어렵다</strong>
NextJS에서 Static HTML을 만들어주는 next export를 제공하고 있긴 하지만 SSR로 제공되는 기능들은 멈춘다고 봐도 무방하다</p>
<p>Single Page Application인 React는 라우팅 처리 로직이 React 앱 내에 존재해(내부에서 처리) index.html을 반환한다는 점이 있다.</p>
<p>JS를 통해 페이지를 동적으로 움직이므로 gh-pages 기능을 사용하기에는 문제가 없을 것이다.</p>
<h2 id="1-basic">1. Basic</h2>
<blockquote>
<p><code>https://유저아이디.github.io/</code></p>
</blockquote>
<p><strong>Base Page 만들기</strong></p>
<ol>
<li>페이지를 구성하고 사용하려면 <code>유저아이디.github.io</code> 의 이름을 가진 Repository를 작성한다</li>
<li>index.html (혹은 README.md)을 포함한 파일을 master branch에 Push한다.</li>
<li>해당 주소로 접속할 수 있는 것을 확인할 수 있다.</li>
</ol>
<h2 id="2-저장소-마다-다른-페이지">2. 저장소 마다 다른 페이지?</h2>
<p>만약 내가 프로젝트를 많이 했고, 이를 여러 곳에서 보여주고 싶다면 어떡해야할까?</p>
<p><del>매번 프로젝트를 만들고 Static HTML을 생성한 뒤 이에 대한 라우터를 연결시킬 수 없는 노릇이다...</del></p>
<p>다행스럽게도 GitHub 에서는 다른 저장소에 대한 접근을 허용시켜준다.</p>
<hr>
<blockquote>
<p><code>https://유저이름.github.io/레파지토리이름</code></p>
</blockquote>
<p>약간의 설정이 필요할 뿐 사실 기존 돌아가는 방법은 비슷하다.</p>
<h3 id="1-설정">1) 설정</h3>
<p>각 저장소에 들어가서 Settings의 GitHub Pages 목록을 보면 다음과 같이 나온다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/2ddf8c8b-eb72-49ec-a238-bb2a0046f580/image.png" alt=""></p>
<p>Source 설정에서 다음 Branch에 어떤 파일의 index.html(혹은 README.md)를 출력할 것인지 설정할 수 있다.</p>
<h2 id="3-in-react">3. In React!</h2>
<p>이제 우리는 GitHub Pages를 이용하는 방법을 우린 알고있다.</p>
<p>하지만 React Project를 정적으로 제공하고 싶을 때는 어떡해야할까..?</p>
<p><strong>npm의 gh-pages 라이브러리를 이용해 쉽게해보도록 하자!</strong></p>
<h3 id="1-installing-gh-pages">1) Installing gh-pages</h3>
<p><code>$ npm install gh-pages --save-dev</code></p>
<p>위의 명령어를 통해서 <a href="https://www.npmjs.com/package/gh-pages">gh-pages</a>를 받는다.</p>
<ul>
<li>해당 Repository에서 gh-pages를 어떻게 올릴 것인지에 대한 옵션들은 홈페이지 들어가서 확인해 보도록 하자...</li>
</ul>
<h3 id="2-setting-packagejson">2) Setting package.json</h3>
<p>package.json을 열었을 때, React 관련 명령어를 설정할 수 있는데 나의 설정은 아래와 같았다.</p>
<pre><code>&quot;scripts&quot;: {
    &quot;start&quot;: react-scripts start&quot;,
    &quot;build&quot;: react-scripts build&quot;,
    &quot;deploy&quot;: &quot;gh-pages -d build&quot;,
    &quot;predeploy&quot;: &quot;npm run build&quot;
},
&quot;homepage&quot;: &quot;https://유저이름.github.io/레파지토리이름&quot;</code></pre><ul>
<li><p>&quot;build&quot;: react-scripts build&quot;
정적 페이지를 index.html로 제공해야 하므로 React의 build를 사용해야 하므로 포함시켰다.</p>
</li>
<li><p>&quot;deploy&quot;: &quot;gh-pages -d build&quot;
<code>npm run deploy</code> 명령어를 사용했을 때, gh-pages -d build 라는 명령어를 실행하게 된다.</p>
</li>
<li><p>d 옵션은 디렉토리를 설정해주는 것인데 react-scripts build 하면 build 폴더에 정적 index.html을 제공하기 때문에 설정했다.</p>
</li>
<li><p>&quot;predeploy&quot;: &quot;npm run build&quot;
npm run deploy 전에 실행되는 명령어이다.
&amp;&amp; 로 해줄 수도 있겠지만, 넘어갔다</p>
</li>
</ul>
<h3 id="3-결과-확인">3) 결과 확인</h3>
<p><code>$ npm run deploy</code></p>
<p>위의 명령어를 입력했을 때, 저장소에 gh-pages branch가 생성된 것을 알 수 있다.</p>
<p>딜레이가 있을 수 있으나 새로고침을 하면 레파지토리에 해당 내용이 뜬다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/e65939b9-7e5d-42d9-b415-f06e044f7439/image.png" alt=""></p>
<p>이 메뉴를 들어가 View Deployment를 클릭하면 적용된 홈페이지를 볼 수 있다!</p>
<h2 id="4-routing-">4. Routing ?!</h2>
<p>React를 이용한 gh-pages 사용 시 Routing을 사용하고 있다면 정상 작동이 잘 안될 수도 있다.</p>
<p>Router는 Base URL(/가 시작되기 전)까지의 주소를 기준으로 주소를 변경하게 된다.</p>
<p>Base URL은 <strong>유저아이디.github.io</strong> 가 되므로 이를 따로 설정해줘야할 필요가 있다.</p>
<h3 id="1-basename-속성-사용">1) basename 속성 사용</h3>
<p>React의 Browser Router 사용하는 Component에 basename 속성을 Repository 이름으로 준다면 이는 해결된다.</p>
<pre><code>&lt;BrowserRouter basename=&quot;/레파지토리&quot;&gt;
    &lt;Link to=&quot;/login&quot; /&gt;
    &lt;Link to=&quot;/main&quot; /&gt;
&lt;/BrowserRouter&gt;</code></pre><h3 id="2-hashrouter">2) HashRouter</h3>
<p>HashRouter를 사용하게 되면 baseURL/#/~~ 로 사용하게 되므로 아마 문제는 없을 것이다.</p>
<p><del>대신 다른 유용한 기능을 사용할 수 없을지도...?</del></p>
<h2 id="참고">참고</h2>
<p><a href="https://velog.io/@ausg/gh-pages-react-router#%E2%99%80%EF%B8%8F-%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0">Github Page로 React SPA 배포하기 (ft. React Router)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS, Cross-Origin Resource Sharing]]></title>
            <link>https://velog.io/@pyo-sh/CORS-Cross-Origin-Resource-Sharing</link>
            <guid>https://velog.io/@pyo-sh/CORS-Cross-Origin-Resource-Sharing</guid>
            <pubDate>Sun, 21 Mar 2021 15:48:35 GMT</pubDate>
            <description><![CDATA[<h2 id="오류의-발생">오류의 발생</h2>
<p>개발자라면 프로젝트를 진행하고 간단한 통신이라도 다음과 같은 오류가 발생한 것을 한 번이라도 본 적이 있을 것이다.</p>
<blockquote>
<p>Access to fetch at ‘<a href="http://localhost:8000%E2%80%99">http://localhost:8000’</a> from origin ‘<a href="http://localhost:3000%E2%80%99">http://localhost:3000’</a> has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.</p>
</blockquote>
<p>처음 이 오류를 봤을 때, 너무나도 찾아보기 힘들었고 해결하기까지 왜 이러한 짓을 해야했는지 이해가 되지 않았었다.</p>
<p>서버에서 서버로 전송할 때는 매우 잘 됐었는데 브라우저에서 서버로 보낼 땐 안 됐기 때문이였다</p>
<h1 id="cors">CORS</h1>
<p>CORS는 Cross-Origin Resource Sharing의 줄임말로 W3C에서 내놓은 정책이라고 한다.</p>
<p>이는 특정 헤더를 통해서 Cross-Origin, 즉 교차 된 출처(Origin)에서 리소스를 공유되는 것이 허용된 것인지를 알 수 있는 방법이다.</p>
<h2 id="origin-출처">Origin, 출처</h2>
<p>URL 을 나타내는 출처는 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것이고 다음과 같이 표현될 수 있다고 할 수 있다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/b1b354cc-6584-490e-bb93-894724c20079/1.PNG" alt=""></p>
<p>출처 내의 포트 번호는 생략이 가능한데 이는 각 웹에서 사용하는 HTTP, HTTPS 프로토콜의 기본 포트 번호가 정해져있기 때문이다.</p>
<blockquote>
<p>HTTP가 정의된 <a href="https://tools.ietf.org/html/rfc2616#section-3.2.2">RFC 2616</a> 문서를 보면 다음과 같이 기본 포트 번호가 함께 정의되어있는 것을 볼 수 있다.</p>
</blockquote>
<p>그러나 출처에 포트 번호가 명시적으로 포함되어 있다면 이 포트 번호까지 모두 일치해야 같은 출처라고 인정된다.
하지만 이 케이스에 대한 명확한 정의가 표준으로 정해진 것은 아니다.</p>
<h2 id="origin-의-구분">Origin 의 구분</h2>
<p>사실 두 개의 출처가 서로 같다고 판단하는 방법은 두 URL의 구성 요소 중 Scheme, Host, Port, 이 3가지만 동일하면 된다.</p>
<p><del>여기서 Scheme은 https:// , http:// 등을 말하는 것 같다.</del></p>
<blockquote>
<p>CORS는 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않는다.</p>
</blockquote>
<p>출처를 비교를 하는 것은 각 브라우저에서 아래의 순서로 진행된다.</p>
<ol>
<li><p>CORS 정책을 위반하는 리소스 요청</p>
</li>
<li><p>해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답한다.</p>
</li>
<li><p>브라우저가 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않는다.</p>
</li>
</ol>
<h2 id="cors-의-동작">CORS 의 동작</h2>
<ol>
<li>기본적으로 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 된다.</li>
</ol>
<p><strong>이때 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아보낸다</strong></p>
<ol start="2">
<li><p>서버가 응답 헤더의 Access-Control-Allow-Origin 값에 “허용된 출처” 값을 설정해 응답한다.</p>
</li>
<li><p>브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.</p>
</li>
</ol>
<p>CORS가 동작하는 방식은 자세히 들여다보면 한 가지가 아니라 세 가지의 시나리오에 따라 변경된다고 한다.</p>
<h3 id="preflight-request">Preflight Request</h3>
<p>이 방식을 사용하는 브라우저는 요청을 한번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/c081412a-6764-4025-9168-cf71f885468f/cors-preflight.png" alt=""></p>
<ul>
<li>HTTP 메소드 중 OPTIONS 메소드가 사용된다.</li>
</ul>
<p>예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이다.</p>
<h3 id="simple-request">Simple Request</h3>
<blockquote>
<p>이 시나리오에 대한 정식 명칭은 없지만 <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#%EC%A0%91%EA%B7%BC_%EC%A0%9C%EC%96%B4_%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4_%EC%98%88%EC%A0%9C">MDN의 CORS 문서</a>에서는 이 시나리오를 Simple Request라고 부르고 있다.</p>
</blockquote>
<p>이 방식은 요청을 전송한 뒤 받은 응답에서 Access-Control-Allow-Origin과 같은 값을 보내주면 그때 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/cbf3e13b-d937-4778-9818-a93a855e5202/simple-request.png" alt=""></p>
<p>이 방식은 아래와 같은 조건이 성립되어야 사용할 수 있다고 한다!</p>
<ol>
<li><p>요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.</p>
</li>
<li><p>Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.</p>
</li>
<li><p>만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.</p>
</li>
</ol>
<h3 id="credentialed-request">Credentialed Request</h3>
<p>이 방법은 CORS의 기본적인 방식이라기 보다는 다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법이다.</p>
<p>기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다.</p>
<p>이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 credentials 옵션이다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/a86107a5-7cc0-4ad1-94be-10f1608c11b6/image.png" alt=""></p>
<p>요청에 인증 정보가 담겨있는 상태에서 다른 출처의 리소스를 요청하게 되면 브라우저는 CORS 정책 위반 여부를 검사하는 룰에 다음 두 가지를 추가하게 된다.</p>
<ol>
<li><p>Access-Control-Allow-Origin에는 *를 사용할 수 없으며, 명시적인 URL이어야한다.</p>
</li>
<li><p>응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 존재해야한다.</p>
</li>
</ol>
<h1 id="cors-정책-위반-해결">CORS 정책 위반 해결</h1>
<h2 id="access-control-allow-origin">Access-Control-Allow-Origin</h2>
<p>서버에서 Access-Control-Allow-Origin 헤더에 요청하는 Origin의 값을 적어서 허용하는 방법이 있다.</p>
<blockquote>
<p>Access-Control-Allow-Origin : *
값을 사용하게 된다면 모든 출처의 Resource를 허용하게 되는 것이다...</p>
</blockquote>
<p>보안을 위한다면 출처를 정확히 명시해주도록 하자.</p>
<h2 id="webpack-dev-server로-리버스-프록싱">Webpack Dev Server로 리버스 프록싱</h2>
<p>백엔드에서 Access-Control-Allow-Origin 헤더가 세팅되어도 <a href="http://localhost:3000">http://localhost:3000</a> 같은 범용적인 출처를 넣어주는 경우는 드물다.</p>
<p>프론트엔드 개발자는 대부분 웹팩과 webpack-dev-server를 사용하여 자신의 머신에 개발 환경을 구축하게 되는데, 이 라이브러리가 제공하는 프록시 기능을 사용하면 아주 편하게 CORS 정책을 우회할 수 있다.</p>
<pre><code class="language-javascript">module.exports = {
  devServer: {
    proxy: {
      &#39;/api&#39;: {
        target: &#39;http://localhost:8080&#39;,
        changeOrigin: true,
        pathRewrite: { &#39;^/api&#39;: &#39;&#39; },
      },
    }
  }
}</code></pre>
<p>웹팩이 요청을 프록싱해주기 때문에 마치 CORS 정책을 지킨 것처럼 브라우저를 속이면서도 우리는 원하는 서버와 자유롭게 통신을 할 수 있다.</p>
<h1 id="마무리">마무리</h1>
<p>아래의 참고 사이트에서 많은 것을 보고 배워서 이제 정말 이러한 문제에 마주치게 된다면 어떻게 해결해야 할지 잘 알고있을 것 같다!</p>
<h1 id="참고">참고</h1>
<p><a href="https://evan-moon.github.io/2020/05/21/about-cors/">CORS는 왜 이렇게 우리를 힘들게 하는걸까?</a></p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS">교차 출처 리소스 공유 (CORS) - MDN</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] NextJS 에서 이미지 import 하기]]></title>
            <link>https://velog.io/@pyo-sh/React-NextJS-%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-import-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pyo-sh/React-NextJS-%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-import-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 21 Mar 2021 14:29:16 GMT</pubDate>
            <description><![CDATA[<h1 id="nextjs-image">NextJS Image</h1>
<h2 id="image-tag">Image Tag</h2>
<p>먼저 next에서는 아래와 같이 이미지를 불러올 수 있는 기능을 제공한다.</p>
<blockquote>
<p><a href="https://nextjs.org/docs/api-reference/next/image">next/image - Next.js</a></p>
</blockquote>
<pre><code class="language-javascript">import Image from &quot;next/image&quot;;
export default function Header() {
  return (
    &lt;&gt;
      &lt;Image
        src=&#39;/&#39;
        width=&#39;&#39;
        height=&#39;&#39;
        /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>img 태그를 사용하지 않고 Image를 사용하는 이유를 요약하자면 아래와 같다.</p>
<blockquote>
<p><a href="https://nextjs.org/docs/basic-features/image-optimization">Image Optimization - Next.js</a></p>
</blockquote>
<ol>
<li><p>Next.js의 Image 태그는 최신 웹용으로 확장된 next/imageHTML img요소이다.</p>
</li>
<li><p>브라우저에서 지원하는 경우 JPEG보다 약 30 % 작은 WebP와 같은 최신 이미지 형식으로 이미지를 자동으로 제공한다.
(필요에 따라 이미지 최적화)</p>
</li>
<li><p>뷰포트를 스크롤하는 동안 특정 임계 값에 도달 한 경우에만 페이지 내부의 이미지를 지연로드한다.</p>
</li>
<li><p>동적으로 사용할 다양한 및 사용자 정의 해상도에 대해 다른 이미지 크기를 지정할 수 있다.</p>
</li>
<li><p>사진의 품질을 75 %로 설정된 낮은 임계 값으로 자동 변경한다
(각 호출에 대해 변경 가능)</p>
</li>
</ol>
<h2 id="image-usage">Image Usage?</h2>
<p>NextJS 프로젝트를 진행할 때 이미지를 사용하는 방법은</p>
<ol>
<li>public 에 넣고 사용하기도 하고</li>
<li>backend에서 받아서 사용
이 대부분일 것이다.</li>
</ol>
<p>하지만 그 이외의 위치에 있는 이미지를 import 하려는 매커니즘을 사용하려고 보니 오류가 났었다.</p>
<p><strong>코드</strong></p>
<pre><code class="language-javascript">import image from &#39;../images/image&#39;;

// or

const image = require(&#39;../images/image&#39;);

// ...</code></pre>
<blockquote>
<p>Unexpected character &#39;�&#39; (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.</p>
</blockquote>
<p>NEXT의 모듈을 import 할 때 이미지 파일은 할 수 없는 설정인 것 같았다.</p>
<p>이 문제는 아래와 같은 라이브러리를 통해 해결할 수 있었다.</p>
<h1 id="image-import-">Image Import !</h1>
<h2 id="install">Install</h2>
<blockquote>
<p><a href="https://www.npmjs.com/package/next-images">next-images</a></p>
</blockquote>
<pre><code>npm install --save next-images</code></pre><p>를 통해 다운로드 할 수 있는데 이미지 Load 같은 역할 이외에도 많은 것을 하는 라이브러리이다.</p>
<h2 id="webpack-config">Webpack Config</h2>
<p>NEXT에서 webpack 설정을 하는 next.config.js 파일을 프로젝트의 root 위치에 생성하고 다음과 같이 작성하여야 한다.</p>
<pre><code>const withImages = require(&#39;next-images&#39;)
module.exports = withImages()</code></pre><p>config 설정을 통해 module 을 import 할 때 image 도 받게 할 수 있는 설정이다.</p>
<p>다른 플러그인을 추가할 수도 있는데, 이는 공식문서에서 확인하고 사용하면 좋을 것 같다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Rails] Rails 에서 MySQL 설정 (Windows) - 실패]]></title>
            <link>https://velog.io/@pyo-sh/Rails-Rails-%EC%97%90%EC%84%9C-MySQL-%EC%84%A4%EC%A0%95-Windows-%EC%8B%A4%ED%8C%A8</link>
            <guid>https://velog.io/@pyo-sh/Rails-Rails-%EC%97%90%EC%84%9C-MySQL-%EC%84%A4%EC%A0%95-Windows-%EC%8B%A4%ED%8C%A8</guid>
            <pubDate>Thu, 11 Mar 2021 18:17:34 GMT</pubDate>
            <description><![CDATA[<h1 id="mysql2">MySQL2</h1>
<p>Windows 에서 Rails 를 사용하는 것은 정말 힘든 것 같다.</p>
<p>오류에 오류를 거듭해서 이게 맞는 방법인지 잘 모르겠지만 작성해보았다.</p>
<h2 id="databaseyml">database.yml</h2>
<p>mysql2 를 사용해서 database 를 접근할 것이라고 프로젝트에게 말해주어야 한다.</p>
<p>config/database.yml 에 들어가서 아래와 같이 적어줄 수 있다.</p>
<pre><code class="language-yml">default: &amp;default
  adapter: sqlite3
  pool: &lt;%= ENV.fetch(&quot;RAILS_MAX_THREADS&quot;) { 5 } %&gt;
  timeout: 5000
  username: root
  password: 12345
  socket: /tmp/mysql.sock

development:
  &lt;&lt;: *default
  database: db/development_database

test:
  &lt;&lt;: *default
  database: db/test.sqlite3

production:
  &lt;&lt;: *default
  database: db/production_database</code></pre>
<h2 id="gemfile">GEMFILE</h2>
<p>우선 gem 을 통해 mysql2 를 설치해야 한다.</p>
<pre><code>gem install mysql2 </code></pre><p>하지만 나는 여기서 오류가 났다.</p>
<p>mysql2 의 상위 버전이 rails 의 상위 버전과 연동 시에 버그가 있다고 한다.</p>
<p>GEMFILE 에서 sqlite3 를 지우고 mysql2 를 해당 버전으로 적고 bundle install 을 진행하였다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/2114bf1a-9e9d-460c-8a7c-33d06d8caaad/image.png" alt=""></p>
<p>감격스럽게도 설치가 되었다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/205c3996-94d0-4fa3-9e53-7893bded89cf/image.png" alt=""></p>
<p>하지만... 아래와 같은 오류가 발생했는데</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/dba05154-b828-4e56-b19f-5db76c6e65c1/image.png" alt=""></p>
<p>이의 해결 방법을 모르고 일단 sqlite3 를 사용하는 중이다...</p>
<p>역시 윈도우가 아니라 linux 에서 ruby를 사용해야 하는걸까? ㅠ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 설치 및 환경변수 설정]]></title>
            <link>https://velog.io/@pyo-sh/MySQL-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@pyo-sh/MySQL-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 11 Mar 2021 15:22:01 GMT</pubDate>
            <description><![CDATA[<h1 id="mysql">MySQL</h1>
<blockquote>
<p><a href="https://www.mysql.com/">공식 홈페이지</a></p>
</blockquote>
<p>관계형 데이터베이스 관리 시스템(RDBMS, Relational DBMS) 중 하나이다.</p>
<ul>
<li>오픈 소스이다
(MySQL은 오픈 소스 라이센스를 따르지만 상업적으로 사용할 때는 상업용 라이센스를 구입해야 한다)</li>
<li>다중 사용자와 다중 스레드를 지원한다</li>
<li>여러 프로그래밍 언어를 위한 다양한 API를 제공한다</li>
<li>표준 SQL 형식을 사용한다</li>
</ul>
<p>Ruby on Rails 에서 mySql 을 사용하기 위해서 설치를 진행해보았다.</p>
<h2 id="다운로드">다운로드</h2>
<blockquote>
<p><a href="https://dev.mysql.com/downloads/mysql/">설치 페이지</a></p>
</blockquote>
<p>홈페이지에 접속한 뒤 MySQL Community (GPL) Downloads 를 클릭한다
 -&gt; General Public License, 무료 이용 가능한 라이센스 이용</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/2c35226b-d0c3-4058-96f1-8e631c1778fa/image.png" alt=""></p>
<p>Community Server 를 통해서 다운로드 하자</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/f01f2fdc-7d85-418f-a620-25607a1b0a1b/image.png" alt=""></p>
<p>MSI Installer 를 받아서 실행해보자</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/50a5aa1a-459a-46d4-97e0-b794538deb07/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/pyo-sh/post/a87fbe1c-a647-4058-95fd-fa0b12a43278/image.png" alt=""></p>
<h2 id="설치">설치</h2>
<p>Installer 를 받아서 실행하면 다음과 같이 뜬다.</p>
<p>Developer Default 가 개발 목적으로 모든 것을 다운받을 수 있다
서버와 워크벤치 등 선택적 설치하고 싶다면 다르게 할 수 있다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/0de4c0ad-2a19-4d04-860a-21470fa3ad84/image.png" alt=""></p>
<p>아래와 같이 설치해야 하는 것들이 있는데 모두 설치하는 것이 좋을 것 같다
(나는 파이썬이 있는 상태였다)</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/210acab1-2233-44fb-872f-fd51649a65a0/image.png" alt=""></p>
<p>근데 아래와 같이 오류가 떴다.</p>
<pre><code>The requirement is still failing</code></pre><p>따로 설치를 해주도록 하자</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/643bf753-4ec9-48ad-b3c7-96697e6539ae/image.png" alt=""></p>
<blockquote>
<p><a href="https://www.microsoft.com/en-in/download/details.aspx?id=40784">Visual C++ Redistributable Packages for Visual Studio 2013</a></p>
</blockquote>
<p>Download 버튼을 누른다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/8273fe29-537d-4bf9-84d3-2f6de1a7fdc7/image.png" alt=""></p>
<p>모두 받아서 설치한다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/d22419c1-e25c-4606-aa41-e779579d2149/image.png" alt=""></p>
<p>그 후에 Execute 를 누르면 다음과 같이 모두 받을 수 있다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/7ee582dd-aa59-4257-b7de-96a7bad574b3/image.png" alt=""></p>
<p>설치가 완료된 후 계속 Next를 누르다 보면 비밀번호 설정이 나온다</p>
<p>이 컴퓨터에서는 그냥 사용할 예정이므로 12345 로 설정했다 ㅎㅎ,,, Weak</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/267e5502-1f05-4006-8a1e-c99e5e1b5f60/image.png" alt=""></p>
<p>난 이걸 사용할 때만 켤 것이므로 체크 해제했다</p>
<p>윈도우 시작할 때 MySQL 서버를 시작할 것인지에 대한 것이다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/430e6729-cc07-4244-93a5-3b6dc6406441/image.png" alt=""></p>
<p>그 후에 Execute로 계속 설치한다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/f3a99589-d879-49b2-920b-516b48e77259/image.png" alt=""></p>
<p>그 후에 Next 를 누르다 보면... Check 할 수 있는 항목이 나오는데 해본다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/10198bd6-e75e-4921-9253-592900593958/image.png" alt=""></p>
<p>또 Next 누르다 보면...</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/b15f6555-a9b9-44b1-a628-b690868d14e2/image.png" alt=""></p>
<p>드디어 끝났다</p>
<h2 id="실행">실행</h2>
<p>MySQL Workbench 를 통해 root 를 연결할 수 있다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/67a34d96-efe5-4f57-8fb9-e8832df2b283/image.png" alt=""></p>
<p>연결 완료됐다면 설치 완료 !</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/1da2e8fb-096a-407f-beed-a778ae2a3385/image.png" alt=""></p>
<h2 id="환경변수-설정">환경변수 설정</h2>
<p>Workbench 에서만 사용한다면 상관이 없지만 터미널에서 사용하기 위해 환경변수 설정까지 넣었다</p>
<p>일단 MySQL Server 의 bin 경로를 확인한다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/a0636dca-952e-42c3-83e6-f3d4662da3c6/image.png" alt=""></p>
<p>System 환경변수 설정에서 Path 에 위의 bin 경로를 넣는다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/30e66b79-1430-421c-894f-5eb6f716438a/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/pyo-sh/post/dbfd1f9a-cda7-40ac-bed4-4d7615d34965/image.png" alt=""></p>
<p>터미널에서 아래의 명령어가 실행이 된다면 완료!</p>
<pre><code>$ mysql -V</code></pre><p><img src="https://images.velog.io/images/pyo-sh/post/7928e8e6-5165-4459-bcf7-bc96cfb77c22/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git Warning] CRLF & LF ?]]></title>
            <link>https://velog.io/@pyo-sh/Git-Error-CRLF-LF</link>
            <guid>https://velog.io/@pyo-sh/Git-Error-CRLF-LF</guid>
            <pubDate>Wed, 10 Mar 2021 23:28:23 GMT</pubDate>
            <description><![CDATA[<h1 id="whitespace-error-">Whitespace Error ?</h1>
<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%83%88%EC%A4%84_%EB%AC%B8%EC%9E%90">새 줄 문자 - 위키백과</a></p>
</blockquote>
<p>MAC/Linux 시스템 에서는 줄의 끝이 LF(Line Feed) 로 이루어 진다.</p>
<p>반면 Windows 에서는 CR(Carriage Return)과 LF(Line Feed) 를 이용한 CRLF 로 나타낸다.</p>
<p><strong><em>따라서 MAC/Linux 개발자와 Windows 개발자가 협업했을 때 이와 관련한 오류가 발생할 수 있다.</em></strong></p>
<p>git 을 사용하다 보면 다음과 같은 경고를 본 적이 있을 것이다.</p>
<pre><code>warning: CRLF will be replaced by LF in ~
The file will have its original line endings in your working directory.</code></pre><p>CRLF / LF 중 어느 것을 사용해야 할 지 git이 결정하지 못했기 때문이다.</p>
<h2 id="해결-">해결 ?</h2>
<p>Git 에서는 core.autocrlf 설정을 통해 파일을 추가 할 때 CRLF 줄 끝을 LF로 자동 변환하고 파일 시스템에 코드를 체크 아웃 할 때 그 반대로 자동 변환하는 기능이 있다.</p>
<p>Windows 에서는 다음과 같은 명령어를 통해 core.autocrlf 를 켜보자.</p>
<pre><code>$ git config --global core.autocrlf true</code></pre><p>해당 프로젝트에서만 사용하고 싶다면 --global 옵션을 제거해주면 된다.</p>
<p><strong><em>그렇다고 MAC / Linux 에서는 CRLF 를 사용해야 하는가?</em></strong></p>
<p>그 반대상황을 위한 core.autocrlf 설정도 있다.</p>
<pre><code>git config --global core.autocrlf input</code></pre><p>Whitespace 에 대한 자세한 설정을 보고 싶다면 아래의 참조에서 공식 문서를 살펴보도록 하자.</p>
<h1 id="참조">참조</h1>
<p><a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#Formatting-and-Whitespace">Customizing Git - Git Configuration</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[윈도우에서 Ruby on Rails 시작하기]]></title>
            <link>https://velog.io/@pyo-sh/%EC%9C%88%EB%8F%84%EC%9A%B0%EC%97%90%EC%84%9C-Ruby-on-Rails-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pyo-sh/%EC%9C%88%EB%8F%84%EC%9A%B0%EC%97%90%EC%84%9C-Ruby-on-Rails-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 10 Mar 2021 23:02:21 GMT</pubDate>
            <description><![CDATA[<h1 id="1-ruby">1. Ruby</h1>
<p>Ruby on Rails 를 사용하기 위해선 Ruby 를 설치해야 한다.</p>
<p>아래의 홈페이지에서 알맞은 버전의 Ruby 를 설치하자</p>
<blockquote>
<p>WITH DEVKIT 버전을 받는 것을 추천한다</p>
</blockquote>
<p><a href="https://rubyinstaller.org/downloads/">RubyInstaller</a></p>
<blockquote>
<p>Td/Tk는 루비로 GUI 개발을 할 때 사용되는 것이다</p>
</blockquote>
<p>설치가 완료됐을 때 터미널의 아래 명령어가 먹힌다면 된 것이다.</p>
<pre><code>$ ruby -v</code></pre><h1 id="2-development-kit">2. Development Kit</h1>
<p>다음과 같은 명령어를 터미널에 작성하여 업데이트를 진행하였다</p>
<pre><code>$ gem update --system</code></pre><h2 id="2-1-오류">2-1. 오류</h2>
<blockquote>
<p>MSYS2 and MINGW develepment toolchain. Installation failed:
Installation failed: pacman failed</p>
</blockquote>
<p>pacman 오류라고 떠서 <a href="https://github.com/oneclick/rubyinstaller2/issues/101">인터넷</a>에 찾아보니,</p>
<pre><code>ridk exec pacman -Rdd catgets libcatgets --noconfirm
ridk install 2 3</code></pre><p>를 진행하라는 말이 있었지만 나에겐 먹히지 않았다.</p>
<p>그래서 MSYS2 설치를 이 <a href="https://www.msys2.org/">홈페이지</a>에서 따로 받아서 MSYS2를 실행하고 아래와 같은 설치와 업데이트를 실행하였다.</p>
<pre><code>$ pacman -Syu

$ pacman -Su</code></pre><p>그러고 난 후 cmd 에서 아래의 명령어로 받았더니 설치가 성공적으로 되었다.</p>
<pre><code>ridk install 3</code></pre><h1 id="3-ruby-on-rails">3. Ruby on Rails</h1>
<p>터미널에 아래의 명령어를 치면 Ruby on Rails를 설치할 수 있다.</p>
<pre><code>$ gem install rails</code></pre><p>Gem files will remain installed in ~ 이라는 단어가 나오면 설치가 완료된 것이다.</p>
<h2 id="3-1-project-시작">3-1. Project 시작</h2>
<p>다음과 같이 프로젝트 이름과 함께 Ruby on Rails 프로젝트를 시작할 수 있다.</p>
<pre><code>$ rails new &lt;Project Name&gt;</code></pre><blockquote>
<p>webpacker 설치에서 yarn 이 설치되어 있어야 한다.</p>
</blockquote>
<p>설치되어 있지 않다면 아래의 명령어를 미리 쳐서 yarn을 설치하자</p>
<pre><code>$ npm install --global yarn</code></pre><h2 id="3-2-project-확인">3-2. Project 확인</h2>
<p>아래의 명령어로 서버를 실행할 수 있다</p>
<pre><code>$ rails server</code></pre><p>아래와 같이 떴다면 서버가 실행된 것이다. localhost 의 port에 맞춰 접속해보자</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/05519181-c307-413d-ae96-4886c473c63d/image.png" alt=""></p>
<p>잘 실행된 것을 볼 수 있다!</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/a98d93e9-27b2-498f-b127-ca81fd8602a6/image.png" alt=""></p>
<p><del>이제 Ruby on Rails 를 신나게 해보자...</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[윈도우에서 React-Native 시작하기]]></title>
            <link>https://velog.io/@pyo-sh/%EC%9C%88%EB%8F%84%EC%9A%B0%EC%97%90%EC%84%9C-React-Native-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pyo-sh/%EC%9C%88%EB%8F%84%EC%9A%B0%EC%97%90%EC%84%9C-React-Native-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 02 Mar 2021 11:35:30 GMT</pubDate>
            <description><![CDATA[<h1 id="1-react-native">1. React-Native</h1>
<blockquote>
<p>원문 : <a href="https://reactnative.dev/">https://reactnative.dev/</a></p>
</blockquote>
<h1 id="2-install">2. Install</h1>
<h2 id="2-1-nodejs">2-1. Nodejs</h2>
<blockquote>
<p><a href="https://nodejs.org/ko/">https://nodejs.org/ko/</a></p>
</blockquote>
<p>React-Native 가 Javascript 의 런타임을 요구하므로 설치한다.</p>
<p>아래의 명령어들을 명령 프롬프트에 적어 제대로 작동하는지 확인할 수 있다.</p>
<pre><code>$ node -v

$ npm -v</code></pre><h3 id="react-native-cli">React-Native-CLI</h3>
<p>리액트 네이티브는 2가지 개발 방법이 있다.</p>
<ul>
<li>Expo CLI</li>
<li>React Native CLI</li>
</ul>
<blockquote>
<p>React Native CLI ?</p>
</blockquote>
<ul>
<li><p>직접 네이티브로 어플리케이션을 개발할 수 있다.</p>
</li>
<li><p>필요한 기능이 있는 경우, 모듈을 직접만들어 사용할 수 있다.</p>
</li>
<li><p>초기 구성이 오래걸린다.</p>
</li>
<li><p>배포하기 불편하다.</p>
<ul>
<li>Window 에서는 Android 밖에 Build 하지 못한다...</li>
</ul>
</li>
</ul>
<pre><code>npm install -g react-native-cli</code></pre><p>이를 위해 다음과 같은 명령어를 명령 프롬프트에 작성하자.</p>
<h2 id="2-2-python">2-2. Python</h2>
<blockquote>
<p><a href="http://www.python.org/downloads">http://www.python.org/downloads</a></p>
</blockquote>
<p>React-Native 의 빌드 시스템이 Python 을 사용한다고 한다.
Mac 에서는 기본적으로 파이썬이 설치되어 있지만 윈도우에서는 따로 설치해주어야 한다.</p>
<p><del>pip 설치를 해야하는 건지는 잘 모르겠다</del></p>
<h2 id="2-3-jdk-jre">2-3. JDK, JRE</h2>
<blockquote>
<p><a href="https://www.oracle.com/java/technologies/javase-downloads.html">https://www.oracle.com/java/technologies/javase-downloads.html</a></p>
</blockquote>
<p>안드로이드 앱 개발에는 JDK 가 필요하다!</p>
<p><del>환경변수 설정도 해주어야 할 듯...?</del></p>
<h2 id="2-4-android-studio">2-4. Android Studio</h2>
<blockquote>
<p><a href="https://developer.android.com/studio">https://developer.android.com/studio</a></p>
</blockquote>
<p>안드로이드 앱 빌드를 위한 Studio 설치이다.</p>
<p>기본 설치하면 jre 이런 것들이 이미 설정이 되어 있는듯 하다..?</p>
<h3 id="sdk-환경설정">SDK 환경설정</h3>
<p>변수 이름 : ANDROID_HOME
변수 값 : sdk 지정한 폴더 경로</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/273a62be-ee69-4a80-867d-ffc777cecbe5/image.png" alt=""></p>
<p>Path 경로에 SDK 밑의 platform-tools 도 등록한다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/0291514c-149b-4243-b69a-7e440c3bbe30/image.png" alt=""></p>
<p>cmd 에서 아래 명령어가 잘 실행된다면 완료!</p>
<pre><code>adb --version</code></pre><p><img src="https://images.velog.io/images/pyo-sh/post/4a0bf2a6-b576-4896-a062-f0d45886d08e/image.png" alt=""></p>
<h3 id="android-emulator-실행">Android Emulator 실행</h3>
<p>Android Studio 의 SDK 에 Android Emulator 이 설치되어 있어야 한다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/2825812f-d658-4d7d-aac4-9fb08b8fc08b/image.png" alt=""></p>
<p>위와 같이 AVD Manager 에 들어가보면 다음과 같은 Device 선택창이 나온다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/f508ace5-8244-45a7-b60a-8d8eb51b08e8/image.png" alt=""></p>
<p>Device 설정이 되어있는 상태에서 실행시키고 잘 돌아가는지 확인한다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/3e24c206-e7c6-41bc-83c8-b6f0bd3280a9/image.png" alt=""></p>
<h1 id="3-프로젝트-생성-및-실행">3. 프로젝트 생성 및 실행</h1>
<h2 id="3-1-생성">3-1. 생성</h2>
<p><code>npx react-native init AwesomeProject</code></p>
<p>다음과 같은 명령어를 실행하면 AwesomeProject 라는 이름을 가진 React-Native Project 가 생성된다.</p>
<h2 id="3-2-실행">3-2. 실행</h2>
<p><strong><em>프로젝트 실행을 위해선 Android Emulator가 실행 중이여야 한다.</em></strong></p>
<p>Emulator를 실행하고 Terminal 의 위치를 프로젝트 폴더로 변경한 뒤 다음과 같이 실행한다.</p>
<pre><code>npx react-native run-android</code></pre><p><img src="https://images.velog.io/images/pyo-sh/post/03d14634-d90c-4bdf-bbc0-ab9f3dd3f62c/image.png" alt=""></p>
<p>무사히 실행 된 것을 볼 수 있다.</p>
<h1 id="4-error">4. Error</h1>
<h2 id="4-1-gradle-과-java-version">4-1. Gradle 과 JAVA Version</h2>
<pre><code>Could not initialize class org.codehaus.groovy.runtime.InvokerHelper</code></pre><p>이라는 오류가 발생 했었다.</p>
<p>나의 JAVA version 은 최신 버전이였으므로 Gradle 버전의 문제라고 판단했다.</p>
<p>프로젝트의 gradle/wrapper/gradle-wrapper.properties 파일에서</p>
<pre><code>distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip</code></pre><p>적혀있었는데 6.3으로 변경하니까 오류가 해결되었다.</p>
<h2 id="4-2-license">4-2. License</h2>
<pre><code>Could not determine the dependencies of task &#39;:app:installDebug&#39;.
&gt; Failed to install the following Android SDK packages as some licences have not been accepted.</code></pre><p>다음 문제는 위와 같이 나왔었다.</p>
<p>우선 SDK Manager 에서 Google Play Licensing Libaray 을 설치해서 다시 실행해 보니 정상 실행되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Full Stack Development Study Intro]]></title>
            <link>https://velog.io/@pyo-sh/Full-Stack-Development-Study-Intro</link>
            <guid>https://velog.io/@pyo-sh/Full-Stack-Development-Study-Intro</guid>
            <pubDate>Tue, 02 Mar 2021 08:41:53 GMT</pubDate>
            <description><![CDATA[<h1 id="study">Study</h1>
<p>클라이언트와 서버 모두 어떻게 돌아가는지 알면 지식 만족을 느낄 것 같아서 React와 Spring을 진행하고 있었다.
혼자서 모든걸 진행하는 것은 너무나 어렵고 시간을 많이 뺏기는 것 같다.</p>
<p>나만의 풀스택 개발을 하면서 궁금한/어려운 점을 건우 형에게 물어보면서 Self Project 를 진행하던 도중 스터디를 추천받게 되었다.</p>
<p>그리하여 아래의 notion 을 보고 스터디를 신청하게 되었다.</p>
<blockquote>
<p><a href="https://www.notion.so/a1140a057f9d470d9738940f5d48f77d">https://www.notion.so/a1140a057f9d470d9738940f5d48f77d</a></p>
</blockquote>
<h2 id="진행">진행</h2>
<p>notion에 나온 것처럼 3월 수요일 저녁 7시에 모임을 가져 총 4회 진행될 것 같다.</p>
<p>스터디 진행이 Mac OS 위주로 진행 될 예정이라 Window10 에서 활동한 내용을 정리할까 싶어 벨로그 작성을 시작하게 되었다.</p>
<h2 id="모임">모임</h2>
<ul>
<li><p>2021년 3월 3일
React-Native Settings
React-Native Code Review</p>
</li>
<li><p>2021년 3월 10일
Ruby on Rails Settings
Ruby on Rails Code Review
QnA</p>
</li>
<li><p>2021년 3월 17일</p>
</li>
<li><p>2021년 3월 24일</p>
</li>
</ul>
<h1 id="환경">환경</h1>
<blockquote>
<p>React / React-Native / Rails</p>
</blockquote>
<ul>
<li>Visual Studio Code</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] React-Responsive / 반응형 웹 만들기]]></title>
            <link>https://velog.io/@pyo-sh/React-Responsive</link>
            <guid>https://velog.io/@pyo-sh/React-Responsive</guid>
            <pubDate>Fri, 19 Feb 2021 01:16:47 GMT</pubDate>
            <description><![CDATA[<h1 id="1-responsive-web">1. Responsive Web</h1>
<p>태플릿, PC, 모바일 등 다양한 해상도로 접근할 때 동일한 서비스를 제공하기 위한 웹</p>
<p>반응형 웹은 모바일 기기에서도 불편함이 없는 서비스를 제공하기 위해 각 해상도에 따라서 레이아웃과 스타일 변화를 준다.</p>
<h2 id="1-1-media-query">1-1. Media Query</h2>
<p>CSS 2.1 부터 미디어 타입으로 단말기 종류에 따라서 다른 스타일을 적용시키는 것이 가능했다.
하지만 기기의 특성을 정확히 판단하기가 어려워 많이 사용되지는 않았다.</p>
<p>CSS 3은 미디어 타입을 개선하여 구체적인 조건을 필요한 스타일을 적용할 수 있도록 확장하였는데 이를 미디어 쿼리라고 한다.</p>
<h3 id="syntax">Syntax</h3>
<p><img src="https://images.velog.io/images/pyo-sh/post/d2df7376-78d7-497e-8c7d-5422e5f04c47/jsseo-140329-CSS-02-1024x167.png" alt=""></p>
<ul>
<li>only|not<ul>
<li>only : 뒤의 조건에서 만</li>
<li>not : 뒤의 조건을 제외한</li>
</ul>
</li>
</ul>
<ul>
<li>미디어 타입<ul>
<li>all : 모든 미디어 타입</li>
<li>aural : 음성 합성 장치</li>
<li>braille : 점자 표시 장치</li>
<li>handheld : 손으로 들고 다니면서 볼 수 있는 작은 스크린에 대응</li>
<li>print : 인쇄 용도</li>
<li>projection : 프로젝터</li>
<li>screen : 컴퓨터 스크린</li>
<li>tty : 디스플레이 능력이 한정된 털렉스, 터미널, 수동 이동 장치 등 고정 된 글자를 사용하는 미디어</li>
<li>tv : 음성과 영상이 동시 출력되는 장치</li>
<li>embrossed : 페이지에 인쇄된 점자 표시 장치</li>
</ul>
</li>
</ul>
<ul>
<li>속성<ul>
<li>width : 웹 페이지의 가로 길이</li>
<li>height : 웹 페이지의 세로 길이</li>
<li>device-width : 단말기의 가로 길이</li>
<li>device-height : 단말기의 세로 길이</li>
<li>orientation : width 와 height 을 구해
width &gt; height 일 경우 landscape
height &gt; width 일 경우 portrait</li>
<li>aspect-ratio : width / height 비율</li>
<li>device-aspect-ratio : 단말기의 물리적인 화면 비율</li>
<li>color-index : 단말기에서 사용하는 최대 색상 수</li>
<li>monochrom : 흑백 컬러만을 사용하는 단말기에서 흰색과 검은색 사이의 단계</li>
<li>resolution : 지원하는 해상도를 판단</li>
<li>color : 단말기에서 사용하는 최대 색상 수의 비트 수
(2의 지수를 뜻한다) ex) 1 은 2, 2 는 4, 3 은 8</li>
</ul>
</li>
</ul>
<h2 id="1-2-fluid-grid">1-2. Fluid Grid</h2>
<p><img src="https://images.velog.io/images/pyo-sh/post/011a8a93-d1ee-4021-a95a-6e5d19021aff/jsseo-140329-CSS-05-1024x213.png" alt=""></p>
<p>그리드의 폭을 고정 값이 아닌 em 또는 %의 값으로 설정하는 것을 뜻한다.
가로 폭의 길이의 변화에 따라서 컬럼의 크기가 상대적으로 변하게 하는 방법이다.</p>
<p>레이아웃에는 변화가 없을 수 있으므로 폭이 많이 좁은 모바일에서 큰 효과를 볼 수 없을 수 있다.</p>
<h2 id="1-3-liquid-layouts">1-3. Liquid Layouts</h2>
<p><img src="https://images.velog.io/images/pyo-sh/post/b46705f8-6d95-446c-8525-d8fbced9c7ff/jsseo-140329-CSS-06-1024x495.png" alt=""></p>
<p>유동형 그리드와 같이 반응형 웹 기법 중 하나이다.
레이아웃 크기를 유동형 그리드와 같이 상대적 단위로 지정하여 웹의 크기에 따라 유동적으로 변화를 준다.
반응형 그리드와 같이 미디어 쿼리를 사용하여 일정 크기가 되면 레이아웃 구조를 바꾸어 준다.</p>
<h1 id="2-in-react-">2. in React ?</h1>
<p>위에서 반응형 웹 경우 HTML/CSS 만으로도 구현이 가능한 경우이다.</p>
<p>하지만 반응형 웹을 만들고 디자인하다 보면 구조나 기능 자체를 다르게 해야하는 경우가 있다.
ex) PC 에서는 길게 늘어져 있던 메뉴가 모바일에선 메뉴 버튼으로
ex) 디자인을 위해 순서를 바꿔 놓는 경우 ... etc</p>
<p>JS에서 이를 구현할 수 있는 많은 방법이 있다.</p>
<p>이 페이지에서는 react-responsive 의 훅을 이용해 구현하는 방법을 알아보고자 한다.</p>
<h2 id="2-1-react-responsive">2-1. react-responsive</h2>
<p><a href="https://www.npmjs.com/package/react-responsive">npm : react-responsive</a></p>
<p>react-responsive 모듈 설치
<code>npm install react-responsive</code></p>
<p>typescript 를 사용하는 경우 추가 설치
<code>npm install @types/react-responsive</code></p>
<h2 id="2-2-usemediaquery">2-2. useMediaQuery</h2>
<p>react-responsive 에서 제공하는 useMediaQuery 훅을 사용해보자.</p>
<pre><code class="language-jsx">import React from &#39;react&#39;
import { useMediaQuery } from &#39;react-responsive&#39;

const Example = () =&gt; {
  const isDesktopOrLaptop = useMediaQuery({ minDeviceWidth: 1224 })
  const isBigScreen = useMediaQuery({ minDeviceWidth: 1824 })
  const isTabletOrMobile = useMediaQuery({ maxWidth: 1224 })
  const isTabletOrMobileDevice = useMediaQuery({ maxDeviceWidth: 1224 })
  const isPortrait = useMediaQuery({ orientation: &#39;portrait&#39; })
  const isRetina = useMediaQuery({ minResolution: &#39;2dppx&#39; })

  return (
    &lt;div&gt;
      ...
    &lt;/div&gt;
  )
}</code></pre>
<p>공식 문서의 예제와 같이 위의 내용을 작성해서 변수를 사용할 수 있다.</p>
<p>해당 변수는 조건에 맞는지 아닌지에 대한 true / false 가 들어가게 된다.
특정 상황에서만 출력을 하도록 하고 싶다면 아래와 같은 예제를 만들 수 있다.</p>
<pre><code class="language-jsx">import { useMediaQuery } from &#39;react-responsive&#39;

const Example = () =&gt; {
  const isDesktopOrLaptop = useMediaQuery(
     { minDeviceWidth: 1224 },
     { deviceWidth: 1600 } // `device` prop
  )

  return (
    &lt;div&gt;
      {isDesktopOrLaptop &amp;&amp;
        &lt;p&gt;
          this will always get rendered even if device is shorter than 1224px,
          that&#39;s because we overrode device settings with &#39;deviceWidth: 1600&#39;.
        &lt;/p&gt;
      }
    &lt;/div&gt;
  )
}</code></pre>
<h3 id="easy-mode">Easy Mode</h3>
<p>공식 문서에 Context를 사용하는 방법 / onChange callback을 사용하는 방법이 있으므로 보는 것도 좋을 것 같다.</p>
<p>맨 아래에 있는 내용 중 제일 유용할 것 같은 예제를 가져와 봤다.</p>
<pre><code class="language-jsx">import { useMediaQuery } from &#39;react-responsive&#39;

const Desktop = ({ children }) =&gt; {
  const isDesktop = useMediaQuery({ minWidth: 992 })
  return isDesktop ? children : null
}
const Tablet = ({ children }) =&gt; {
  const isTablet = useMediaQuery({ minWidth: 768, maxWidth: 991 })
  return isTablet ? children : null
}
const Mobile = ({ children }) =&gt; {
  const isMobile = useMediaQuery({ maxWidth: 767 })
  return isMobile ? children : null
}
const Default = ({ children }) =&gt; {
  const isNotMobile = useMediaQuery({ minWidth: 768 })
  return isNotMobile ? children : null
}

const Example = () =&gt; (
  &lt;div&gt;
    &lt;Desktop&gt;Desktop or laptop&lt;/Desktop&gt;
    &lt;Tablet&gt;Tablet&lt;/Tablet&gt;
    &lt;Mobile&gt;Mobile&lt;/Mobile&gt;
    &lt;Default&gt;Not mobile (desktop or laptop or tablet)&lt;/Default&gt;
  &lt;/div&gt;
)

export default Example</code></pre>
<h1 id="마무리">마무리</h1>
<p>한 가지 방법에만 묶여있지 말고 여러가지 방법을 사용해 보도록 하자<del>!!</del>!~!</p>
<h1 id="참고">참고</h1>
<p><a href="https://www.nextree.co.kr/p8622/">CSS:반응형 웹(Responsive Web)</a></p>
<p><a href="https://www.npmjs.com/package/react-responsive">npm : react-responsive</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React(Typescript)] resize 이벤트 처리 & in NEXT.js]]></title>
            <link>https://velog.io/@pyo-sh/ReactTypescript-resize-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC-in-NEXT.js</link>
            <guid>https://velog.io/@pyo-sh/ReactTypescript-resize-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC-in-NEXT.js</guid>
            <pubDate>Wed, 17 Feb 2021 16:13:44 GMT</pubDate>
            <description><![CDATA[<h1 id="window-resize">Window Resize</h1>
<p>브라우저 화면 사이즈가 변경될 때 리액트 컴포넌트에서 ReRendering 하고 싶어 window 객체의 resize 이벤트를 찾고 State를 변경해 이를 활용하였다.</p>
<h2 id="resize-이벤트를-찾게된-이유">Resize 이벤트를 찾게된 이유</h2>
<p>학습 목적으로 인스타그램 홈페이지의 기능을 가지고 있는 웹 프로그래밍을 하던 도중 메인 화면에서 프로필을 나타내는 부분이 반응형으로 되어있다는 것을 확인하였고 이를 따라 구현하고 싶었다.</p>
<h2 id="responsive-web">Responsive Web</h2>
<p>태플릿, PC, 모바일 등 다양한 해상도로 접근을 하는 사람들이 많아지는 세상에 웹 페이지를 제작할 때 각기 다른 기기에서도 동일한 서비스를 제공하기 위해 반응형으로 만드는 것이 중요하다고 들었다.</p>
<p>Window Resize 이벤트를 추가해 주는 동시에 이를 스타일에 적용할 수 있다면 가능성이 많은 반응형 홈페이지를 만들 수 있을 것이다.</p>
<h1 id="1-react의-addeventlistener">1. React의 addEventListener</h1>
<p>JS에서 addEventListener 함수를 사용하는 것은 Event에 대한 하나의 반응을 추가하는 것이다.
현재 하고싶은 방향은 하나의 resize Event를 추가하고 싶은 것이므로 컴포넌트가 처음 마운트 될 때 addEventListener를 해주면 될 것이다.</p>
<blockquote>
<p>useEffect 훅의 두 번째 매개변수에 빈 배열을 넘겨준다면?
-&gt; 컴포넌트가 마운트 될 때만 첫 번째 매개변수로 입력된 함수가 호출되고 컴포넌트가 언마운트 될 때만 반환된 함수가 호출된다.</p>
</blockquote>
<pre><code class="language-jsx">import React, { useEffect } from &#39;react&#39;;

const RSComponent: React.FC = () =&gt; {
  const handleResize = () =&gt; {
    console.log(`브라우저 화면 사이즈 x: ${window.innerWidth}, y: ${window.innerHeight}`);
  }

  useEffect(() =&gt; {
    window.addEventListener(&#39;resize&#39;, handleResize);
    return () =&gt; {
      window.removeEventListener(&#39;resize&#39;, handleResize);
    }
  }, []);

  return (
    &lt;div&gt;
        브라우저 화면 너비 : {window.innerWidth}
    &lt;/div&gt;
  );
}</code></pre>
<h2 id="-이벤트-삭제하는-이유">! 이벤트 삭제하는 이유?</h2>
<blockquote>
<p>자원의 활용을 위해 라이프사이클 마지막에 이벤트를 삭제해 주는 것이다.</p>
</blockquote>
<p>위의 코드를 활용해보면 resize Event가 발생할 때마다 console에 해당 내용이 보이게 될 것이다.</p>
<h1 id="2-react-rerendering">2. React Rerendering</h1>
<p>설정한 resize Event 가 발생될 때 변경된 값을 Rerendering 할 예정이다.</p>
<p>React에서 Rerendering 되는 것은 State, Props 값이 변경될 때 이므로 State를 추가해 이를 제어해보도록 하자.</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from &#39;react&#39;;

const RSComponent: React.FC = () =&gt; {
  const [windowWidthSize, setWindowWidthSize] = useState&lt;number&gt;(
    typeof window !== &quot;undefined&quot; ? window.innerWidth : 0
  );

  const handleResize = () =&gt; {
    setWindowWidthSize(window.innerWidth);
  }

  useEffect(() =&gt; {
    window.addEventListener(&#39;resize&#39;, handleResize);
    return () =&gt; {
      window.removeEventListener(&#39;resize&#39;, handleResize);
    };
  }, []);

  return windowWidthSize &gt;= 1000
  ? (
    &lt;div&gt;
        브라우저 화면 너비 : {windowWidthSize}
    &lt;/div&gt;
  )
  : null;
}</code></pre>
<p>1000px 보다 작아지면 rendering 하지 않고, 나머지 상황에서는 margin을 지정하는 상황이라고 가정하고 코드를 작성하였다.</p>
<h2 id="-typeof-window--undefined-사용-이유">! typeof window !== &quot;undefined&quot; 사용 이유?</h2>
<blockquote>
<p>window 객체가 정해지지 않았을 때 Stater 값을 지정해 주는 과정이 있기 때문에 오류가 생겼었다.
-&gt; ReferenceError: window is not defined</p>
</blockquote>
<p><img src="https://images.velog.io/images/pyo-sh/post/eef58dd9-f5c5-4670-b93d-acf54f158130/image.png" alt=""></p>
<p>해당 오류를 고치기 위해 window에 값이 있을 때 이를 사용하도록 임시 방편을 마련했다.</p>
<h2 id="nextjs-ssr-에서의-오류">NEXT.JS (SSR) 에서의 오류</h2>
<p>위와 같이 코드를 작성하고 NEXT.js 에서 실행해보면 다음과 같은 오류가 발생했었다.</p>
<blockquote>
<p>Warning: Expected server HTML to contain a matching &lt;div&gt; in &lt;div&gt;</p>
</blockquote>
<p><img src="https://images.velog.io/images/pyo-sh/post/c3e6271d-cae4-4ec2-891b-2ccf5df2e390/image.png" alt=""></p>
<p>확실하지 않지만 나의 생각으로는 서버에서 브라우저 너비를 확인한 후 렌더링하도록 하는 구성이 이와 같은 오류를 발생시켰다고 짐작하고 있다.</p>
<p>즉 Server Side Rendering에서 브라우저 너비를 인식하지 못하기 때문이라고 생각한다.</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from &#39;react&#39;;

const RSComponent: React.FC = () =&gt; {
  const [windowWidthSize, setWindowWidthSize] = useState&lt;number&gt;(1000);

  const handleResize = () =&gt; {
    setWindowWidthSize(window.innerWidth);
  }

  useEffect(() =&gt; {
    setWindowWidthSize(window.innerWidth);
    window.addEventListener(&#39;resize&#39;, handleResize);
    return () =&gt; {
      window.removeEventListener(&#39;resize&#39;, handleResize);
    };
  }, []);

  return windowWidthSize &gt;= 1000
  ? (
    &lt;div&gt;
        브라우저 화면 너비 : {windowWidthSize}
    &lt;/div&gt;
  )
  : null;
}</code></pre>
<p>브라우저에서만 실행되어야하는 코드를 내부에서 실행되어야 한다고 생각해 addEventListner 를 추가할 때 setState 도 함께 진행하여 브라우저 너비를 인식하도록 변경하였다.</p>
<h1 id="3-performance-improve">3. Performance improve</h1>
<p>위와 같은 상황에서 resize Event는 px 단위로 매번 이벤트 핸들러가 호출된다.
잦은 리렌더는 부드럽게 움직이는 것으로 보이지만 성능을 생각해 React를 사용했는데 무용지물이 된 기분이 드는 것은 어쩔 수 없다...</p>
<p>debounce라는 기술을 이용해 리렌더의 횟수를 줄여 맨 마지막에 결정된 사이즈 값 하나를 사용하도록 해보자.</p>
<p>참고한 사이트에서 Debounce와 Throttle 개념을 <a href="https://webclub.tistory.com/607">여기</a>에서 확인하면 좋다고 한다.</p>
<p>개인적으로 <a href="https://velog.io/@edie_ko/React-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81-%EC%8B%9C%ED%82%A4%EA%B8%B0-feat.-Lodash-throttle-debounce">React | 컴포넌트 성능 향상 시키기 (feat. Lodash throttle &amp; debounce)</a>
이 게시글도 유용하다고 생각한다.</p>
<h2 id="디바운스debounce">디바운스(Debounce)</h2>
<blockquote>
<p>이벤트를 그룹화해 특정 시간이 지난 후 하나의 이벤트만 발생하도록 하는 기술이다</p>
</blockquote>
<p>lodash 패키지가 제공하는 debounce 함수를 사용하여 handleResize 함수가 지정한 시간에 맞추어 실행되도록 해보자.</p>
<p><a href="https://www.npmjs.com/package/lodash">lodash - npm</a></p>
<p>❗ React의 경우
<code>npm install lodash</code></p>
<p>❗ TypeScript React 경우
<code>npm install @types/lodash</code></p>
<pre><code class="language-jsx">import React, { useEffect, useState } from &#39;react&#39;;
import { debounce } from &#39;lodash&#39;;

const RSComponent: React.FC = () =&gt; {
  const [windowWidthSize, setWindowWidthSize] = useState&lt;number&gt;(1000);

  const handleResize = debounce(() =&gt; {
    setWindowWidthSize(window.innerWidth);
  }, 25);

  useEffect(() =&gt; {
    setWindowWidthSize(window.innerWidth);
    window.addEventListener(&#39;resize&#39;, handleResize);
    return () =&gt; {
      window.removeEventListener(&#39;resize&#39;, handleResize);
    };
  }, []);

  return windowWidthSize &gt;= 1000
  ? (
    &lt;div&gt;
        브라우저 화면 너비 : {windowWidthSize}
    &lt;/div&gt;
  )
  : null;
}</code></pre>
<p>resize 시에 부드럽게 적용하기 위해 25ms로 지정하였다.</p>
<h1 id="참고">참고</h1>
<p><a href="https://velog.io/@dblee/React-resize-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%EC%83%9D-%EC%8B%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A6%AC%EB%A0%8C%EB%8D%94">[React] 리액트에서 resize 이벤트 처리하기</a></p>
<p><a href="https://github.com/vercel/next.js/discussions/17443">vercel/next.js</a></p>
<p><a href="https://webclub.tistory.com/607">디바운스(Debounce)와 스로틀(Throttle ) 그리고 차이점</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] 파일(이미지) 업로드 구현하기]]></title>
            <link>https://velog.io/@pyo-sh/Spring-Boot-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@pyo-sh/Spring-Boot-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 Feb 2021 13:40:28 GMT</pubDate>
            <description><![CDATA[<h1 id="file-upload">File Upload</h1>
<h2 id="1-계획">1. 계획</h2>
<h3 id="frontend">FrontEnd</h3>
<p>유저가 파일을 업로드하고 이를 보낸다고 가정한다.</p>
<p>이 때 보내야 할 값들은 아래와 같다고 가정한다</p>
<ul>
<li>user : (String) 유저 이름</li>
<li>content : (String) 글 내용</li>
<li>files : (File Object 들의 배열) 업로드한 파일들</li>
</ul>
<h3 id="backend">BackEnd</h3>
<p>유저가 보낸 정보들을 한 번에 받아 게시글을 생성하려고 한다.</p>
<p>Post 값으로 받을 것이고 /board 에 전달 받을 것이다.</p>
<h2 id="2-frontend">2. FrontEnd</h2>
<h3 id="2-1-form-태그를-이용하여-전송">2-1. form 태그를 이용하여 전송</h3>
<p>보통 사용되는 방법이고 간단한 구현을 할 것이므로 html 만으로 작성한다.</p>
<pre><code class="language-HTML">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
    &lt;title&gt;파일 보내기 예제&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;form name=&quot;form&quot; method=&quot;post&quot; action=&quot;http://localhost:8080/board&quot; enctype=&quot;multipart/form-data&quot;&gt;
      &lt;input name=&quot;user&quot; value=&quot;Pyo&quot;/&gt;
      &lt;input name=&quot;content&quot; value=&quot;Content&quot;/&gt;
      &lt;input type=&quot;file&quot; name=&quot;files&quot; multiple=&quot;multiple&quot;/&gt;
      &lt;input type=&quot;submit&quot; id=&quot;submit&quot; value=&quot;전송&quot;/&gt;
    &lt;/form&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>아래와 같은 창에서 전송을 누르면 값이 Backend로 보내어진다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/6e61648b-e6db-4c62-b81d-877d8c7374a7/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/pyo-sh/post/71a7e889-5776-424f-917d-7306762ff0f0/image.png" alt=""></p>
<h3 id="2-2-axios-를-이용하여-전송">2-2. Axios 를 이용하여 전송</h3>
<p>자바스크립트에서 제어하는 방법도 알아보기 위해 Axios 예제도 만들어보았다.</p>
<pre><code class="language-HTML">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
    &lt;title&gt;파일 보내기 예제&lt;/title&gt;
    &lt;script src=&quot;https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js&quot;&gt;&lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;input type=&quot;file&quot; id=&quot;File&quot; name=&quot;files&quot; multiple=&quot;multiple&quot;/&gt;
    &lt;button id=&quot;Button&quot;&gt;전송&lt;/button&gt;

    &lt;script src=&quot;./index.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<blockquote>
<p>HTML 의 전송 기본은 form 태그로 감싸고, button의 이벤트를 onSubmit으로 설정하는 것이 기본이다!</p>
</blockquote>
<p>자바스크립트에서 전송 함수가 작동하는 것을 확인하려고 클릭 이벤트를 활용했다.
<del>오류에 찌들어서 미친것이다.</del></p>
<pre><code class="language-javascript">const ButtonElement = document.querySelector(&#39;#Button&#39;);
const FileElement = document.querySelector(&#39;#File&#39;);

const getData = async () =&gt; {
    try{
        const formData = new FormData();
        formData.append(&quot;user&quot;, &quot;Pyo&quot;);
        formData.append(&quot;content&quot;, &quot;this is content&quot;);
        for(let i = 0; i &lt; FileElement.files.length; i++){
            formData.append(&quot;files&quot;, FileElement.files[i]);
        }
        const url = &#39;http://localhost:8080/board&#39;;

        const response = await axios.post(url, formData);
        console.log(response)
    }
    catch (error){
        console.error(error);
    }
}

ButtonElement.addEventListener(&#39;click&#39;, getData);</code></pre>
<ol>
<li>async ~ await 를 사용해 비동기 처리를 해주었다.</li>
<li>try ~ catch 로 예상치 못 한 오류가 발생하면 Error를 출력하도록 하였다.</li>
<li>axios.post() 를 이용하였다
FormData 를 통해 값을 전달하였다.</li>
</ol>
<p><img src="https://images.velog.io/images/pyo-sh/post/125795e4-74ef-4a8a-9f1d-49d5b106ac9d/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/pyo-sh/post/e54da0c8-337c-49f4-a82f-75a4fc5248c3/image.png" alt=""></p>
<blockquote>
<p>FormData 객체의 append 를 이용해 배열을 전달하고 싶을 때, 배열 자체를 넣는 것이 아니라 같은 Key에 Element들을 하나하나 넣어주어야 한다.</p>
</blockquote>
<p>배열을 넣었을 때 Controller에서 이를 인식하지 못하는 오류를 겪었다.</p>
<p>아마도 FormData의 append 함수는 객체를 넣었을 때 무시되는 것 같다.
<del>확실한거 아님</del></p>
<h2 id="3-backend">3. BackEnd</h2>
<h3 id="3-1-사진을-저장하는-entity">3-1. 사진을 저장하는 Entity</h3>
<p>실제로 DB에 사진을 저장하는 것은 비효율, 병목현상 등을 고려해 하지 않는다고 한다.</p>
<p>실제 사진은 서버의 특정 위치에 저장하도록 하고 DB에는 사진에 대한 정보만을 저장한다.</p>
<p><strong>BoardPicture.class</strong></p>
<pre><code class="language-java">@Entity
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class BoardPicture {
    @Id
    @GeneratedValue
    private Integer ID;
    @NotNull
    private Integer boardIdx;

    // 원본 파일이름 과 서버에 저장된 파일 경로 를 분리한 이유?
    // 동일한 이름을 가진 파일이 업로드가 된다면 오류가 생긴다.
    // 이를 해결하기 위함
    @NotEmpty
    private String original_file_name;
    @NotEmpty
    private String stored_file_path;

    private long file_size;
}</code></pre>
<p>다시 사진을 읽을 때에는 저장된 정보를 이용해 불러올 것이다.</p>
<p><strong>BoardPictureRepository.java</strong></p>
<pre><code>public interface BoardPictureRepository extends CrudRepository&lt;BoardPicture, Integer&gt; {
    BoardPicture save(BoardPicture boardPicture);

    List&lt;BoardPicture&gt; findAllByBoardIdx(Integer boardIdx);
}</code></pre><blockquote>
<p><strong>CRUD Repository를 이용할 때 주의점</strong>
요소들을 Snake case 가 아닌 Camel case 를 이용할 것
스네이크 표기법인 컬럼으로 Repository에서 쿼리를 생성하는데 에러가 발생한다.</p>
</blockquote>
<p>언더스코어는 이미 Spring Data JPA의 탐색 경로를 설정하는 예약어이기 때문에 Property Expressions에 사용하면 안된다.
스네이크 표기법을 사용하기보단 자바의 네이밍 컨벤션인 카멜 표기법을 사용하는 것을 추천한다</p>
<h3 id="3-2-controller">3-2. Controller</h3>
<h4 id="a-cors-error">a) CORS Error</h4>
<p>Backend 에서 무작정 코딩을 한 뒤에 프론트에서 정보를 보내보면 아래와 같은 오류가 뜬다.</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/16cd5150-a25d-4211-9f08-ef001931fadc/image.png" alt=""></p>
<p>Same Origin과 Cross Origin Policy 때문에 내가 접속한 사이트에서 다른 Origin에 요청한 것들을 기본적으로 차단된다.
사전요청은 되지만 그 데이터를 읽는 건 보안상의 이유로 차단한다.</p>
<blockquote>
<p><strong>다른 Origin으로부터의 자원 공유가 필요한 경우에는?</strong>
CORS(Cross Origin Resource Sharing) : 다른 Origin의 데이터를 사용한다면 CORS 표준을 지켜 다른 도메인을 허용하도록 할 수 있게 한다</p>
</blockquote>
<p>컨트롤러에서 @CrossOrigin 어노테이션을 이용해 다른 포트에서의 요청을 허용할 수 있다.</p>
<p>Class 에서의 모든 Mapping 이 다른 Origin을 허용하는 방법은 아래와 같다.</p>
<pre><code class="language-java">@CrossOrigin
@RestController
public class BoardController {
    // ~~
}</code></pre>
<p>해당 함수만 다른 Origin을 허용하는 방법은 아래와 같다.</p>
<pre><code class="language-java">@RestController
public class BoardController {
    // ~~
    @CrossOrigin(origins = {&quot;http://localhost:5500&quot;})
    @PostMapping
    public ResponseEntity&lt;?&gt; create(){}
    // ~~</code></pre>
<h4 id="b-requestparam-이용하기">b) @RequestParam 이용하기</h4>
<p>Controller에서 PostMapping을 사용했을 때 @RequestBody를 사용했을 때 정보를 받으면 아래의 Error가 뜰 때가 있다.</p>
<pre><code>Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type &#39;multipart/form-data;boundary=----WebKitFormBoundary7bFMK5q5ziAvA0tq;charset=UTF-8&#39; not supported ]</code></pre><p>@RequestBody는 json형태의 바디로 들어오는 데이터를 파싱해주는데 Content-Type이 multipart/form-data 으로 전달될 때는 Exception 을 발생시키게 된다.</p>
<blockquote>
<p>@RequestParam() 을 이용해서 데이터를 받아보도록 하자</p>
</blockquote>
<p><strong>List&lt;MultipartFile&gt; 자료형을 이용해 여러 개의 이미지를 처리할 수 있다.</strong></p>
<pre><code class="language-java">@CrossOrigin
@RestController
public class BoardController {
    @Autowired
    private BoardService boardService;

    @PostMapping(&quot;/board&quot;)
    public ResponseEntity&lt;?&gt; createBoard(
            @Valid @RequestParam(&quot;user&quot;) String User,
            @Valid @RequestParam(&quot;content&quot;) String content,
            @Valid @RequestParam(&quot;files&quot;) List&lt;MultipartFile&gt; files
    ) throws Exception {
        Board board = boardService.addBoard(Board.builder()
                .user(user)
                .content(content)
                .build(), files);

        URI uriLocation = new URI(&quot;/board/&quot; + board.getID());
        return ResponseEntity.created(uriLocation).body(&quot;{}&quot;);
    }
}</code></pre>
<h4 id="c-multiparthttpservletrequest-">c) MultipartHttpServletRequest ?</h4>
<p>MultipartHttpServletRequest는 ServletRequest를 상속받아 구현된 인터페이스로 업로드된 파일을 처리하기 위한 여러 가지 메서드를 제공한다.</p>
<p>서버는 클라이언트의 HTTP 요청 메시지 자체를 컨트롤 하는 것이기 때문에 함수 내에서 요청 메시지 안에 포함된 파라미터들을 뜯어내야 하는 코드를 작성해야 한다.</p>
<pre><code class="language-java">@CrossOrigin
@RestController
public class BoardController {
    @Autowired
    private BoardService boardService;

    @PostMapping(&quot;/board&quot;)
    public ResponseEntity&lt;?&gt; createBoard(
            @Valid @RequestParam(&quot;user&quot;) String user,
            @Valid @RequestParam(&quot;content&quot;) String content,
            MultipartHttpServletRequest multipartHttpServletRequest
    ) throws Exception {
        Board board = boardService.addBoard(Board.builder()
                .user(user)
                .content(content)
                .build(), multipartHttpServletRequest);

        URI uriLocation = new URI(&quot;/board/&quot; + board.getID());
        return ResponseEntity.created(uriLocation).body(&quot;{}&quot;);
    }</code></pre>
<h3 id="3-3-service">3-3. Service</h3>
<h4 id="a-filehandler">a) FileHandler</h4>
<p>List&lt;MultipartFile&gt; 를 받아 사진을 저장하고 이에 대한 정보를 List&lt;boardPicture&gt; 로 변경하여 반환하는 클래스이다.</p>
<pre><code class="language-java">@Component
public class FileHandler {
    public List&lt;BoardPicture&gt; parseFileInfo(
            Integer boardID,
            List&lt;MultipartFile&gt; multipartFiles
    ) throws Exception{

        // 반환을 할 파일 리스트
        List&lt;BoardPicture&gt; fileList = new ArrayList&lt;&gt;();

        // 파일이 빈 것이 들어오면 빈 것을 반환
        if(multipartFiles.isEmpty()){
            return fileList;
        }

        // 파일 이름을 업로드 한 날짜로 바꾸어서 저장할 것이다
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(&quot;yyyyMMdd&quot;);
        String current_date = simpleDateFormat.format(new Date());
// 반환을 할 파일 리스트
        List&lt;BoardPicture&gt; fileList = new ArrayList&lt;&gt;();

        // 파일이 빈 것이 들어오면 빈 것을 반환
        if(multipartFiles.isEmpty()){
            return fileList;
        }

        // 파일 이름을 업로드 한 날짜로 바꾸어서 저장할 것이다
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(&quot;yyyyMMdd&quot;);
        String current_date = simpleDateFormat.format(new Date());

        // 프로젝트 폴더에 저장하기 위해 절대경로를 설정 (Window 의 Tomcat 은 Temp 파일을 이용한다)
        String absolutePath = new File(&quot;&quot;).getAbsolutePath() + &quot;\\&quot;;

        // 경로를 지정하고 그곳에다가 저장할 심산이다
        String path = &quot;images/&quot; + current_date;
        File file = new File(path);
        // 저장할 위치의 디렉토리가 존지하지 않을 경우
        if(!file.exists()){
            // mkdir() 함수와 다른 점은 상위 디렉토리가 존재하지 않을 때 그것까지 생성
            file.mkdirs();
        }

        // 파일들을 이제 만져볼 것이다
        for (MultipartFile multipartFile : multipartFiles){
            // 파일이 비어 있지 않을 때 작업을 시작해야 오류가 나지 않는다
            if(!multipartFile.isEmpty()){
                // jpeg, png, gif 파일들만 받아서 처리할 예정
                String contentType = multipartFile.getContentType();
                String originalFileExtension;
                    // 확장자 명이 없으면 이 파일은 잘 못 된 것이다
                if (ObjectUtils.isEmpty(contentType)){
                    break;
                }
                else{
                    if(contentType.contains(&quot;image/jpeg&quot;)){
                        originalFileExtension = &quot;.jpg&quot;;
                    }
                    else if(contentType.contains(&quot;image/png&quot;)){
                        originalFileExtension = &quot;.png&quot;;
                    }
                    else if(contentType.contains(&quot;image/gif&quot;)){
                        originalFileExtension = &quot;.gif&quot;;
                    }
                    // 다른 파일 명이면 아무 일 하지 않는다
                    else{
                        break;
                    }
                }
                // 각 이름은 겹치면 안되므로 나노 초까지 동원하여 지정
                String new_file_name = Long.toString(System.nanoTime()) + originalFileExtension;
                // 생성 후 리스트에 추가
                BoardPicture boardPicture = BoardPicture.builder()
                        .boardIdx(boardID)
                        .original_file_name(multipartFile.getOriginalFilename())
                        .stored_file_path(path + &quot;/&quot; + new_file_name)
                        .file_size(multipartFile.getSize())
                        .build();
                fileList.add(boardPicture);

                // 저장된 파일로 변경하여 이를 보여주기 위함
                file = new File(absolutePath + path + &quot;/&quot; + new_file_name);
                multipartFile.transferTo(file);
            }
        }

        return fileList;
    }</code></pre>
<blockquote>
<p>Spring Boot 설정에서 server.tomcat.basedir 를 특정 경로로 지정하지 않을 경우 기본으로 temp 경로가 설정이 된다.</p>
</blockquote>
<pre><code class="language-java">String absolutePath = new File(&quot;&quot;).getAbsolutePath() + &quot;\\&quot;;</code></pre>
<p>그래서 위와 같이 프로젝트 폴더를 구하고 절대경로를 따로 구하여 저장했다</p>
<p>절대 경로를 구하는 다른 방법은 아래와 같다.</p>
<pre><code class="language-java">String absolutePath = System.getProperty(&quot;user.dir&quot;);;</code></pre>
<p>이를 이용하지 않고 파일을 저장하려고 하면 아래와 같은 오류가 발생한다.</p>
<pre><code>java.io.IOException: java.io.FileNotFoundException: C:\Users\pyosh\AppData\Local\Temp\tomcat.10361523148407138883.8080\work\Tomcat\localhost\ROOT\images\20210209\298457256522500.jpg (지정된 경로를 찾을 수 없습니다)</code></pre><h4 id="b-각-repository-에-저장">b) 각 Repository 에 저장</h4>
<pre><code class="language-java">@Service
public class BoardService {
    private BoardRepository boardRepository;

    private BoardPictureRepository boardPictureRepository;

    private FileHandler fileHandler;

    @Autowired
    public BoardService(BoardRepository boardRepository, BoardPictureRepository boardPictureRepository) {
        this.boardRepository = boardRepository;
        this.boardPictureRepository = boardPictureRepository;
        this.fileHandler = new FileHandler();
    }

    public Board addBoard(
            Board board,
            List&lt;MultipartFile&gt; files
    ) throws Exception {
        // 파일을 저장하고 그 BoardPicture 에 대한 list 를 가지고 있는다
        List&lt;BoardPicture&gt; list = fileHandler.parseFileInfo(board.getID(), files);

        if(list.isEmpty()){
            // TODO : 파일이 없을 땐 어떻게 해야할까.. 고민을 해보아야 할 것
        }
        // 파일에 대해 DB에 저장하고 가지고 있을 것
        else{
            List&lt;BoardPicture&gt; pictureBeans = new ArrayList&lt;&gt;();
            for(BoardPicture boardPicture : list) {
                pictureBeans.add(boardPictureRepository.save(boardPicture));
            }
            board.setPictures(pictureBeans);
        }

        board.setReported_date(new Date().toString());

        return boardRepository.save(board);
    }
}</code></pre>
<h2 id="참조">참조</h2>
<p><a href="https://hongsii.github.io/2019/01/06/jpa-query-creation-with-underscore/">JPA Property Expressions 쿼리 생성시 참조타입 탐색 경로 지정하기</a></p>
<p><a href="http://wookje.dance/2020/07/16/spring-boot-file-upload-request/">Spring Boot 프로젝트에서 MultipartFile을 수신할 때 주의점</a></p>
<p><a href="https://caileb.tistory.com/152">MultipartFile 을 이용한 File 업로드</a></p>
<p><a href="http://blog.naver.com/PostView.nhn?blogId=goddes4&amp;logNo=220296020812&amp;parentCategoryNo=&amp;categoryNo=56&amp;viewDate=&amp;isShowPopularPosts=true&amp;from=search">MultipartFile.transferTo()로 파일 저장시 주의사항</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Project] 이상형 월드컵]]></title>
            <link>https://velog.io/@pyo-sh/React-Project-%EC%9D%B4%EC%83%81%ED%98%95-%EC%9B%94%EB%93%9C%EC%BB%B5</link>
            <guid>https://velog.io/@pyo-sh/React-Project-%EC%9D%B4%EC%83%81%ED%98%95-%EC%9B%94%EB%93%9C%EC%BB%B5</guid>
            <pubDate>Sun, 17 Jan 2021 10:31:54 GMT</pubDate>
            <description><![CDATA[<h2 id="기능-소개">기능 소개</h2>
<ul>
<li>Display<ul>
<li>각 음식에 대한 사진을 보여준다</li>
<li>마우스를 올리면 사진이 확대된다</li>
</ul>
</li>
<li>Select<ul>
<li>해당 음식의 사진을 클릭하면 그 음식은 승리하고 다음으로 넘어간다</li>
</ul>
</li>
<li>Make Winner<ul>
<li>단 하나의 음식이 남을 때 까지 반복된다</li>
</ul>
</li>
</ul>
<h2 id="참조-영상">참조 영상</h2>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=_7WF8TtHD8s&amp;ab_channel=DannyTWLC">React.js 를 사용해 이상형 월드컵 만들기
</a></p>
</blockquote>
<h2 id="코드">코드</h2>
<blockquote>
<p><a href="https://github.com/pyo-sh/Datapia_Frontend_Study_2020/tree/master/favorite-worldcup">GitHub : pyo-sh</a></p>
</blockquote>
<hr>
<h1 id="1-main-features">1. Main Features</h1>
<h2 id="1-1-구조">1-1. 구조</h2>
<p><img src="https://images.velog.io/images/pyo-sh/post/8122218a-8c68-4347-aa9d-39ea1026ef42/image.png" alt=""></p>
<ul>
<li>APP : GlobalStyle 을 적용</li>
<li>FlexBox : styled-component<ul>
<li>화면의 전체를 차지하게 되고 flex-1 의 위치를 정하게 된다</li>
</ul>
</li>
<li>Title : 프로그램이 무엇을 나타내는지 h1 태그로 보여준다<ul>
<li>position이 absolute로 독자적인 위치를 가진다 (FlexBox 하위 태그)</li>
</ul>
</li>
<li>flex-1 : 이상형 월드컵 게임 기능<ul>
<li>기능을 수행하는 이벤트가 있다</li>
<li>사용자에게 직관적인 인터페이스를 제공한다</li>
</ul>
</li>
</ul>
<h2 id="1-2-item-state">1-2. Item State</h2>
<h3 id="1-아이템-불러오기">1) 아이템 불러오기</h3>
<p>보통 정보를 가져올 때 API로 가져오거나 한다</p>
<p>영상에서는 큰 프로젝트가 아니므로 사진을 저장하고 이를 전역 변수로 저장하는 형태로 구현한 것 같다</p>
<p><strong>사진은 src/img 에 저장해서 구현했다</strong></p>
<pre><code class="language-javascript">const items = [
  {
    name: &quot;국밥&quot;,
    src: require(&#39;../../img/GukBob.jpg&#39;).default
  },
  {
    name: &quot;햄버거&quot;,
    src: require(&#39;../../img/Hamburger.jpg&#39;).default
  },
  {
    name: &quot;피자&quot;,
    src: require(&#39;../../img/Pizza.jpg&#39;).default
  },
  {
    name: &quot;초밥&quot;,
    src: require(&#39;../../img/Sushi.jpg&#39;).default
  },
];</code></pre>
<p><strong>React 에서 Image를 불러오는 방법</strong></p>
<ol>
<li>img src에 import 변수 from &#39;경로&#39;; 선언 후 변수를 통해 부여</li>
<li>img src에 require(&#39;경로&#39;).default 부여
(require(&#39;경로&#39;) 를 하면 모듈 Object로 들고 있더라...)</li>
<li>사진을 public에 저장하고 img src에 절대 경로로 지정</li>
</ol>
<h3 id="2-배열에서-랜덤-나열">2) 배열에서 랜덤 나열</h3>
<p>Math.random() 함수는 0 ~ 1 사이의 랜덤한 값을 가진다</p>
<p>Javascript 배열에서 sort 함수를 이용해 배열을 정렬할 때, Math.random() - 0.5 값을 통해 임의의 -, + 값이 오게 만들어 무작위로 정렬 되도록 한다</p>
<p>전역변수 items로 부터 받은 값을 컴포넌트의 첫 랜더링에만 저장하도록 하기 위해 useEffect(~~, []) 를 사용하였다</p>
<pre><code class="language-javascript">const [foods, setFoods] = useState([]);

useEffect(() =&gt; {
  items.sort(() =&gt; Math.random() - 0.5);
  setFoods(items);
  // ~~~
}, []);</code></pre>
<h2 id="1-3-display-state">1-3. Display State</h2>
<p>해당 코드에서는 Display 하는 item을 따로 State에 크기가 2인 배열로 저장한다</p>
<pre><code class="language-javascript">const [displays, setDisplays] = useState([]);

useEffect(() =&gt; {
  // ~~~
  setDisplays([items[0], items[1]]);
}, []);</code></pre>
<p>이 State만을 Rendering 해서 사용자에게 보여준다</p>
<pre><code class="language-jsx">return // ~~
  {displays.map(item =&gt; {
     return &lt;div
     className=&quot;flex-1&quot;
     key={item.name}
    onClick={clickHandler(item)}
      &gt;
       &lt;img className=&quot;food-img&quot; src={item.src} alt=&quot;food&quot;/&gt;
       &lt;div className=&quot;name&quot;&gt;{item.name}&lt;/div&gt;
    &lt;/div&gt;
  })}
// ~~</code></pre>
<h2 id="1-4-make-winner">1-4. Make Winner</h2>
<h3 id="1-사용자-인터페이스">1) 사용자 인터페이스</h3>
<p>이미지에 마우스를 올려놨을 때 아래의 효과들로 사용자가 이를 클릭할 수 있다는 암시를 준다</p>
<ul>
<li>사진이 흐려진다 (배경이 회색으로 되어있어 살짝 어두워 진다)</li>
<li>사진이 확대된다 (화면 밖으로 나가지는 않는다)</li>
<li>커서가 바뀐다</li>
<li>이름의 크기를 크게 하여 자신이 선택하는 사진이 무엇인지 알려준다</li>
</ul>
<p>이를 styled-component인 FlexBox 안에 하위 클래스로 아래처럼 css로 적용하였다</p>
<pre><code class="language-css">const FlexBox = styled.div`
    // ~~~
    .flex-1 {
        flex: 1;
        min-width: 500px;
        overflow: hidden;
        background-color: gray;
        position: relative;
    }
    .food-img {
        width: 100%;
        height: 100%;
        transition: 0.5s;
        cursor: pointer;
    }
    .food-img:hover{
        transform: scale(1.1);
        opacity: 0.8;
    }
    .name{
        position: absolute;
        z-index: 3;
        color: #ffffff;
        bottom: 10%;
        font-size: 90px;
        left: 50%;
        transform: translateX(-50%);
    }
`</code></pre>
<h3 id="2-클릭-이벤트">2) 클릭 이벤트</h3>
<p>사진을 클릭했을 때 사용자의 선택받은 아이템은 따로 저장되고 다음 아이템들이 Display 되어야 한다</p>
<p>이때 승리한 아이템을 넣는 변수를 따로 두어 저장하도록 한다</p>
<pre><code class="language-javascript">const [winners, setWinners] = useState([]);</code></pre>
<p>배열에서 아이템들을 빼내어 Display 하다보면 length가 2 이하일 때까지 반복하게 된다</p>
<pre><code class="language-javascript">const clickHandler = food =&gt; (event) =&gt; {
  // ~~
  else if(foods.length &gt; 2){
    setWinners([...winners, food]);
    setDisplays([foods[2], foods[3]]);
    setFoods(foods.slice(2));
  }
}</code></pre>
<p>length가 2 이하일 때는 사용자가 전체 아이템을 살펴보았다는 말이 된다</p>
<p>그 이후에는 사용자가 선택한 아이템 중에서 월드컵을 진행하고 선택된 아이템이 하나만 남을 때 까지 아이템을 선정한다</p>
<pre><code class="language-javascript">const clickHandler = food =&gt; (event) =&gt; {
  if(foods.length &lt;= 2){
    if(winners.length === 0){
      setDisplays([food]);
    } else{
      let updatedFood = [...winners, food];
      setFoods(updatedFood);
      setDisplays([updatedFood[0], updatedFood[1]]);
      setWinners([]);
    }
  //~~
}</code></pre>
<p>클릭 이벤트의 전체 코드는 위 두개의 코드를 합친 것과 같다</p>
<h1 id="2-sub-features">2. Sub Features</h1>
<h2 id="2-1-styled-component">2-1. styled-component</h2>
<h3 id="1-globalstyle">1) GlobalStyle</h3>
<p>App.js 에 다음과 같은 styled-component를 사용하여 전체 css를 적용하였다</p>
<pre><code class="language-javascript">import React from &quot;react&quot;;
import { createGlobalStyle } from &#39;styled-components&#39;;

import Game from &#39;./components/Game/Game&#39;;

const GlobalStyle = createGlobalStyle`
  * {
    margin: 0,
    padding: 0;
    box-sizing: border-box;
  }
`;

function App() {
  return (&lt;&gt;
    &lt;GlobalStyle&gt;&lt;/GlobalStyle&gt;
    &lt;Game/&gt;
  &lt;/&gt;);
}

export default App;</code></pre>
<h3 id="2-flexbox">2) FlexBox</h3>
<p>게임을 실행하는 div 태그를 하나의 styled-component로 두어 style.js 에 따로 저장한다</p>
<pre><code class="language-javascript">import styled from &#39;styled-components&#39;;

export const FlexBox = styled.div`
    // ...
`;</code></pre>
<p>이를 div 태그에서 사용하여 가독성을 높인 것(?) 같다</p>
<pre><code class="language-javascript">import React, { useState, useEffect } from &#39;react&#39;;
import { FlexBox } from &#39;./style&#39;;

// ~~
const Game = () =&gt; {
  // ~~
  return &lt;FlexBox&gt;
    // ~~
  &lt;/FlexBox&gt;;
}</code></pre>
<h2 id="2-2-고정-된-타이틀">2-2. 고정 된 타이틀</h2>
<p>Favorite WorldCup 임을 알리는 타이틀은 아래와 같은 태그를 가진다</p>
<pre><code class="language-html">&lt;h1 className=&quot;title&quot;&gt;Favorite WorldCup&lt;/h1&gt;</code></pre>
<p>이는 styled-component 안에 다음과 같이 css로 지정되어 상단 중앙에 위치하게 된다</p>
<pre><code class="language-css">  .title {
      position: absolute;
      z-index: 2;
      top: 0;
      left: 50%;
      transform: translateX(-50%);
      background-color: #ffffff;
      padding: 0px 30px;
      padding-bottom: 10px;
      text-transform: uppercase;
   }</code></pre>
<h1 id="후기">후기</h1>
<p>React에 대해 심화적으로 했다기 보다 styled-component 사용에 중점에 둔 프로젝트였던 것 같다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Project] ToDo List 제작]]></title>
            <link>https://velog.io/@pyo-sh/React-Project-To-Do-List-%EC%A0%9C%EC%9E%91</link>
            <guid>https://velog.io/@pyo-sh/React-Project-To-Do-List-%EC%A0%9C%EC%9E%91</guid>
            <pubDate>Sat, 16 Jan 2021 08:31:30 GMT</pubDate>
            <description><![CDATA[<h2 id="기능-소개">기능 소개</h2>
<ul>
<li>Create<ul>
<li>사용자는 자신이 할 일을 Input 창에 입력할 수 있다</li>
<li>ADD 버튼을 누르면 입력한 일이 아래에 저장된다</li>
</ul>
</li>
<li>Main<ul>
<li>자신이 기록한 할 일들이 아래에 리스트로 나타내어 진다</li>
<li>할 일들은 자신이 했는지 안 했는지 체크할 수 있다</li>
<li>자신에게 얼마만큼의 할 일이 남았는지 Display 한다</li>
</ul>
</li>
<li>Update<ul>
<li>글자를 클릭하면 할 일을 수정할 수 있다 (엔터로 확인)</li>
</ul>
</li>
<li>Delete<ul>
<li>x를 클릭하면 할 일을 삭제할 수 있다</li>
</ul>
</li>
</ul>
<h2 id="코드">코드</h2>
<blockquote>
<p><a href="https://github.com/pyo-sh/Datapia_Frontend_Study_2020/tree/master/react-todolist">GitHub : pyo-sh</a></p>
</blockquote>
<hr>
<h1 id="1-main-feature">1. Main Feature</h1>
<h2 id="1-1-구조">1-1. 구조</h2>
<p>React 사용 목적에 맞게 Component를 나누어서 작성</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/81b40ab5-b3bf-48cb-b911-29f4081ab0a7/image.png" alt=""></p>
<ul>
<li>APP + Header : 프로그램이 무엇을 나타내는 지<ul>
<li>일들의 list를 가진 state 보유</li>
<li>TODOLIST 타이틀</li>
<li>오늘의 날짜</li>
<li>남은 일의 갯수</li>
</ul>
</li>
<li>ToDoInput : 사용자가 일에 대해 입력 및 추가<ul>
<li>Input : 사용자에게 입력받기</li>
<li>Button : 사용자에게 추가기능 제공</li>
</ul>
</li>
<li>ToDoList : 할 일들을 Unordered List로 나열</li>
<li>ToDoItem : List 중 하나의 일<ul>
<li>할 일의 기능, 수정, 삭제 등을 제공</li>
</ul>
</li>
</ul>
<h2 id="1-2-할-일-추가하기">1-2. 할 일 추가하기</h2>
<h3 id="1-입력-받기">1) 입력 받기</h3>
<p>HTML input 태그의 onChange 이벤트를 이용해 값을 받는다</p>
<p>Component의 State 값으로 String 변수를 이용</p>
<pre><code class="language-javascript">const [inputTodoValue, setInputTodoValue] = useState(&quot;&quot;);</code></pre>
<p>onChange 이벤트가 발생할 때 마다 setState를 하여 값을 저장하고 있는다
State 값은 input의 값이 된다</p>
<pre><code class="language-javascript">const onChangeInputList = (event) =&gt; {
  const { value } = event.target;
  setInputTodoValue(value);
};</code></pre>
<pre><code class="language-jsx">// ...
return // ...
  &lt;input
    className=&quot;ToDo-Add-Input&quot;
    onChange={onChangeInputList}
    value={inputTodoValue}
    /&gt;
// ...</code></pre>
<h3 id="2-추가-하기">2) 추가 하기</h3>
<p>입력을 했다면 할 일을 추가할 수 있어야 한다
HTML Button 태그의 onClick 이벤트가 발생하면 일을 추가한다</p>
<p>Component의 State 값으로 id를 가지고 있어 일을 추가할 때 마다 고유한 id를 부여한다</p>
<pre><code class="language-javascript">const [id, setId] = useState(0);</code></pre>
<p>onClick 이벤트 발생 시 입력한 값(inputTodoValue)가 있을 때 할 일을 추가(상위 Component의 setState) 한다</p>
<p>list에 현재 id 값을 부여하고 id + 1을 setState 하여 각 할 일이 고유한 id를 가지고 있게 한다</p>
<pre><code class="language-javascript">const onClickAddList = () =&gt; {
    if (inputTodoValue) {
      setList((prev) =&gt; [
        ...prev,
        {
          id: id,
          content: inputTodoValue,
          isComplete: false,
          isUpdating: false
        }
      ]);
      setId((prev) =&gt; prev + 1);
      setInputTodoValue(&quot;&quot;);
    }
  };</code></pre>
<pre><code class="language-jsx">return // ...
    &lt;button
        className=&quot;ToDo-Add-Button&quot;
        onClick={onClickAddList}&gt;
          ADD
    &lt;/button&gt;
// ...</code></pre>
<h2 id="1-3-저장한-일들-나열하기">1-3. 저장한 일들 나열하기</h2>
<p>배열로 저장하고 있는 객체들을 map() 함수를 이용해 HTML li 태그로 출력한다</p>
<p>하위 컴포넌트에서 출력을 담당하도록 하게하고 나열만 하여 기능을 명확히 했다</p>
<pre><code class="language-jsx">// ...
function ToDoList({ list, setList }) {
  // ...
  return (&lt;ul className=&quot;ToDo-List&quot;&gt;
      {list.map((value) =&gt; (
        &lt;ToDoItem
          key={value.id}
          id={value.id}
          content={value.content}
          isComplete={value.isComplete}
          isUpdating={value.isUpdating}
          pressEnterKey={pressEnterKey}
          /&gt;
      ))}
    &lt;/ul&gt;);
}</code></pre>
<h2 id="1-4-클릭-기능">1-4. 클릭 기능</h2>
<blockquote>
<p>각각의 li 태그가 같은 onClick 이벤트를 실행할 예정이므로 이벤트 버블링을 활용한다
→ HTML ul 태그에 onClick 이벤트 하나를 두고 Event Target의 id를 통해 기능을 수행하도록 한다</p>
</blockquote>
<p>id를 ToDo-[기능]-[번호] 로 부여하여 어떤 기능을 수행해야 하는지 알아내고 어느 타겟에 적용해야 하는지 알아낸다</p>
<pre><code class="language-javascript">const onClickList = (event) =&gt; {
  if (event.target.id &amp;&amp; event.target.id.split(&#39;-&#39;).length &gt; 1) {
    const buttonFunction = event.target.id.split(&#39;-&#39;)[1]
    // ... 기능 수행
}</code></pre>
<h3 id="1-체크">1) 체크</h3>
<p>list 객체의 속성 중 isComplete가 true 면 완료, false면 할 일이 남은 것이다
default 값은 false로 두었다</p>
<p>isComplete의 값에 따라 Button의 Display를 달리하였다</p>
<pre><code class="language-jsx">return // ...
  &lt;button 
    className={isComplete ? &quot;ToDo-Item-Done&quot; : &quot;ToDo-Item-Left&quot;}
    id={&quot;ToDo-Complete-&quot; + String(id)}
    &gt;
      {isComplete ? &quot;✓&quot; : &quot;-&quot;}
  &lt;/button&gt;
// ...</code></pre>
<p>클릭 된 Button은 해당 객체의 isComplete 값만 바꾸게 된다</p>
<pre><code class="language-javascript">if(buttonFunction === &quot;Complete&quot;){
  setList(
    list.map((value) =&gt; {
      if (event.target.id === &quot;ToDo-Complete-&quot; + String(value.id)) {
        return { ...value, isComplete: !value.isComplete };
      }
      return value;
    })
  );
}</code></pre>
<h3 id="2-삭제">2) 삭제</h3>
<p>삭제 Button을 누르면 List 배열 중에서 해당 객체만 삭제하면 된다
많은 방법이 있지만 filter() 함수를 사용했다</p>
<pre><code class="language-javascript">if(buttonFunction === &quot;Delete&quot;){
  setList(
    list.filter((value) =&gt; event.target.id !== &quot;ToDo-Delete-&quot; + String(value.id))
  );
}</code></pre>
<h3 id="3-수정">3) 수정</h3>
<p>객체는 isUpdating 이라는 속성을 가지고 있어 수정하고 있는지에 대한 값을 저장하고 있다
default 값은 false이고 클릭하면 isUpdating의 값을 true로 바꿀 뿐이다</p>
<pre><code class="language-javascript">if(buttonFunction === &quot;Update&quot;){
  setList(
    list.map((value) =&gt; {
      if (event.target.id === &quot;ToDo-Update-&quot; + String(value.id)) {
        return { ...value, isUpdating: !value.isUpdating };
      }
      return value;
    })
  );
}</code></pre>
<p>isUpdating의 값에 따라 li 태그의 출력이 달라진다</p>
<pre><code class="language-jsx">return // ...
  {isUpdating
   ? &lt;input
       className=&quot;ToDo-Item-Update&quot;
       id={&quot;ToDo-Updating-&quot; + String(id)}
       onKeyPress={pressEnterKey}
       defaultValue={content}
       /&gt;
   : &lt;p
       className=&quot;ToDo-Item-Content&quot;
       id={&quot;ToDo-Update-&quot; + String(id)}
       &gt;
     {content}
   &lt;/p&gt;
  }
// ...</code></pre>
<p>수정이 끝났다면 사용자는 Enter를 눌러 저장할 수 있다
onKeyPress 이벤트에서 13번(Enter)가 발생하면 Input 값을 저장하고 isUpdating을 false로 만든다</p>
<pre><code class="language-javascript">const pressEnterKey = (event) =&gt; {
  if(event.charCode === 13){
    setList(
      list.map((value) =&gt; {
        if (event.target.id === &quot;ToDo-Updating-&quot; + String(value.id)) {
          return {
            ...value,
            content: event.target.value,
            isUpdating: false
          };
        }
        return value;
      })
    );
  }
}</code></pre>
<h1 id="2-sub-features">2. Sub Features</h1>
<h2 id="2-1-남은-할-일의-갯수-출력하기">2-1. 남은 할 일의 갯수 출력하기</h2>
<p>list의 배열 안 객체 중에 isComplete가 false인 객체가 몇 개 인지만 확인하면 된다</p>
<pre><code class="language-javascript">const renderComplete = () =&gt; {
  return list.reduce((accumulator, current) =&gt; {
    if (current.isComplete === false)
      return accumulator + 1;
    return accumulator;
  }, 0);
}</code></pre>
<p>이 함수에서 반환된 값을 출력하기만 하면 된다</p>
<pre><code class="language-jsx">return // ...
    &lt;div className=&quot;List-Count&quot;&gt;할 일 {renderComplete()}개&lt;/div&gt;
// ...
  );</code></pre>
<h2 id="2-2-오늘-날짜-출력하기">2-2. 오늘 날짜 출력하기</h2>
<p>Date 객체를 이용해서 오늘의 날짜를 출력하도록 했다.</p>
<p>요일은 미리 배열을 선언하고 index에 따라 출력 값을 정하도록 하였다.</p>
<pre><code class="language-javascript">const renderDate = () =&gt; {
  if(nowDate){
    const dayToKorean = [&#39;월&#39;, &#39;화&#39;, &#39;수&#39;, &#39;목&#39;, &#39;금&#39;, &#39;토&#39;, &#39;일&#39;];
    const year = nowDate.getFullYear();
    const month = String(Number(nowDate.getMonth()) + 1);
    const date = nowDate.getDate();
    const day = nowDate.getDay();

    return year + &#39;년 &#39; + month + &#39;월 &#39; + date + &#39;일 &#39; + dayToKorean[day] + &#39;요일&#39;;
  }
  return &#39;&#39;;
};</code></pre>
<p>이 또한 반환된 값을 출력하기만 하면 된다</p>
<pre><code class="language-jsx">return // ...
    &lt;div className=&quot;Header&quot;&gt;
      &lt;h1 className=&quot;Header-Title&quot;&gt;TODOLIST&lt;/h1&gt;
      &lt;div className=&quot;Header-Day&quot;&gt;{renderDate()}&lt;/div&gt;
  &lt;/div&gt;
// ...
  );</code></pre>
<h1 id="후기">후기</h1>
<p>작년에도 React로 Todo List 만들기를 두 번 정도 했었다
그때는 아무 것도 모르고 기능을 구현하는 것에만 초점에 두고 진행했었는데 역시 기초 공부를 한 뒤에 만드는 것이 얻어가는 것도 많은 것 같다
또한 똑같은 걸 만들더라도 여러번 하면 코딩 속도가 빨라지는 것 같다</p>
<p><del>디자인 실력은 어떻게 해도 늘지 않는 것 같다 ㅎ...</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 비동기 처리, 콜백 함수]]></title>
            <link>https://velog.io/@pyo-sh/JavaScript-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@pyo-sh/JavaScript-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98</guid>
            <pubDate>Thu, 14 Jan 2021 04:11:36 GMT</pubDate>
            <description><![CDATA[<h1 id="비동기-처리">비동기 처리</h1>
<blockquote>
<p>특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 자바스크립트의 특성을 의미한다</p>
</blockquote>
<h2 id="비동기-통신">비동기 통신</h2>
<p>비동기 처리의 가장 흔한 사례로 통신을 들 수 있다</p>
<pre><code class="language-javascript">const getData = () =&gt; {
    const url = &#39;~&#39;;
    let responseData;
     axios.get(url)
    .then((response) =&gt; {
        responseData = response;
    });
      console.log(responseData);
}

getData();</code></pre>
<p><a href="https://github.com/axios/axios">axios</a> 통신을 통해 데이터를 받는 예시로서 해당 url에 HTTP GET 요청을 날려 데이터를 요청하는 코드이다</p>
<p>위의 코드에 따르면 서버에서 받아온 데이터는 response 인자에 담겨서 responseData에 저장되고 출력되어야 할 것이다
하지만 결과는 아래와 같이 콘솔에 데이터는 보이지 않는다</p>
<pre><code>undefined</code></pre><p>axios.get() 함수가 데이터를 요청하고 받아올 때까지 기다려주지 않고  뒤의 console.log()를 실행하기 때문이다</p>
<h2 id="settimeout">setTimeout</h2>
<p>setTimeout()은 Web API의 한 종류이다
코드를  지정한 시간만큼 기다렸다가 로직을 실행하는 함수이다</p>
<pre><code class="language-javascript">console.log(&#39;first&#39;);

setTimeout(function() {
    console.log(&#39;second&#39;);
}, 3000);

console.log(&#39;third&#39;);</code></pre>
<p>비동기 처리에 대한 이해가 없는 상태에서 위 코드를 보면 아래와 같은 결과를 생각할 것이다</p>
<pre><code>first
second
third</code></pre><p>하지만 실제 결과값은 아래와 같다</p>
<pre><code>first
third
second</code></pre><h2 id="스크립트-모듈-로딩">스크립트, 모듈 로딩</h2>
<p>스크립트나 모듈을 로딩하는 것도 비동기 동작이다
아래는 스크립트를 읽어오는 함수 loadScript를 나타낸다</p>
<pre><code class="language-javascript">const loadScript = (src) =&gt; {
    let script = document.createElement(&#39;script&#39;);
    script.src = src;
    document.head.append(script);
}

loadScript(&#39;script.js&#39;);</code></pre>
<p>script.js는 아래와 같은 동작을 하는 함수가 있다고 가정한다</p>
<pre><code class="language-javascript">const displayMe = () =&gt; {
    console.log(&quot;script&quot;)
}</code></pre>
<p>하지만 이는 아래와 같이 함수를 찾을 수 없다는 오류가 발생한다
loadScript에서 스크립트 로딩이 완료하기도 전에 함수를 실행하려 하기 때문이다</p>
<p><img src="https://images.velog.io/images/pyo-sh/post/4a439b1d-af09-4c4b-8a34-534e0c3d828c/image.png" alt=""></p>
<blockquote>
<p>특정 로직의 실행이 끝날 때까지 기다려주지 않고 나머지 코드를 먼저 실행하는 것이 비동기 처리이다</p>
</blockquote>
<h1 id="콜백callback-함수">콜백(callback) 함수</h1>
<p>위에서 말한 것처럼 자바스크립트 비동기 처리 방식에 의해 생길 수 있는 문제들이 있다</p>
<p>이 문제들을 해결 할 때 바로 콜백 함수를 이용할 수 있다</p>
<p>아래 예시는 스크립트 로딩이 완료된 뒤 함수(콜백 함수)가 실행되어 무사히 함수를 실행한다</p>
<p><strong>예시 (스크립트 로딩)</strong></p>
<pre><code class="language-javascript">const loadScript = (src, callBack) =&gt; {
    let script = document.createElement(&#39;script&#39;);
    script.src = src;

    script.onload = () =&gt; callBack();

    document.head.append(script);
}

loadScript(&#39;script.js&#39;, () =&gt; {
    displayMe();
});</code></pre>
<p><strong>결과</strong></p>
<p><img src="https://images.velog.io/images/pyo-sh/post/4ef13a60-7846-4b83-ae61-74491d51d28d/image.png" alt=""></p>
<h2 id="error-handling">Error Handling</h2>
<p>콜백 함수는 에러를 핸들링 할 수 있어야 한다</p>
<h3 id="오류-우선-콜백-error-first-callback">오류 우선 콜백 (Error-First Callback)</h3>
<blockquote>
<p>오류 우선 콜백 스타일을 사용하면 단일 콜백 함수에서 에러 케이스와 성공 케이스 모두를 처리할 수 있다
콜백과 관련된 에러를 처리를 위한 일종의 코딩 약속과 같다</p>
</blockquote>
<p>규칙</p>
<ul>
<li>콜백의 첫 번째 매개변수에 에러 객체를 사용한다</li>
<li>에러가 null이나 undefined이면 정상이라고 판단한다</li>
</ul>
<p><strong>예시</strong></p>
<pre><code class="language-javascript">const loadScript = (src, callback) =&gt; {
  let script = document.createElement(&#39;script&#39;);
  script.src = src;

  script.onload = () =&gt; callback(null, script);
  script.onerror = () =&gt; callback(new Error(`Error`));

  document.head.append(script);
}

loadScript(&#39;script.js&#39;, (error, script) =&gt; {
  if (error) {
    // 에러 처리
  } else {
    // 스크립트 로딩이 성공적으로 끝남
  }
});</code></pre>
<h2 id="콜백-안의-콜백">콜백 안의 콜백</h2>
<p>콜백 함수로 일을 처리하다 보면 중첩 호출을 해야하는 상황이 올 수 있다</p>
<p>아래 예시는 script 로딩을 하고난 뒤 script2 로딩을 해야할 때 실행하는 콜백 속 콜백 함수 사용 예시이다</p>
<pre><code class="language-javascript">loadScript(&#39;script.js&#39;, (script) =&gt; {
  // ~~
  loadScript(&#39;script2.js&#39;, (script) =&gt; {
    // ~~
  });
});</code></pre>
<p>콜백 안에 콜백을 넣는 것은 수행하려는 동작이 단 몇 개뿐이라면 괜찮지만 동작이 많은 경우엔 좋지 않다</p>
<h3 id="멸망의-피라미드-콜백-지옥">멸망의 피라미드, 콜백 지옥</h3>
<blockquote>
<p>콜백 지옥은 비동기 처리 로직을 위해 콜백 함수를 연속해서 사용할 때 발생하는 문제이다</p>
</blockquote>
<p><img src="https://images.velog.io/images/pyo-sh/post/f49f1533-f3ca-4b08-b283-2a8da1a0b53d/image.png" alt=""></p>
<p>콜백 지옥 코드 구조는 가독성도 떨어지고 로직을 변경하기도 어렵다
이와 같은 코드 구조를 콜백 지옥이라 한다</p>
<p><strong>해결 방법</strong></p>
<ul>
<li>함수 분리</li>
<li>Promise</li>
<li>async await</li>
</ul>
<h2 id="참고-사이트">참고 사이트</h2>
<p><a href="https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/">자바스크립트 비동기 처리와 콜백 함수</a></p>
<p><a href="https://ko.javascript.info/callbacks">콜백</a></p>
<p><a href="https://samslow.github.io/development/2020/06/13/Javascript_Basic_Asyncronous/">비동기 통신</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] JPA]]></title>
            <link>https://velog.io/@pyo-sh/Spring-JPA</link>
            <guid>https://velog.io/@pyo-sh/Spring-JPA</guid>
            <pubDate>Tue, 12 Jan 2021 00:25:54 GMT</pubDate>
            <description><![CDATA[<h1 id="jpa-java-persistence-api">JPA (Java Persistence API)</h1>
<blockquote>
<p>JAVA에서 제공하는 DB 테이블과 자바 객체 사이의 매핑을 처리해주는 ORM 기술의 표준</p>
</blockquote>
<p>자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스라고 할 수 있다</p>
<ul>
<li>JPA는 특정 기능을 하는 라이브러리가 아닌 인터페이스이다.
→ ORM을 사용하기 위한 인터페이스
→ SQL이 아닌 자바 클래스와 DB 테이블을 매핑한다</li>
</ul>
<h3 id="persistence-영속화-작업">Persistence (영속화 작업)</h3>
<p>개발을 하다보면 데이터를 생성한 프로그램을 종료했을 때 데이터가 메모리에서만 존재했기 때문에 다시 데이터를 생성해야 하는 불편함을 겪은 적이 있었을 것이다</p>
<blockquote>
<p>Persistence는 한 마디로 별도의 기억 장치에 데이터를 보존하는 것이다</p>
</blockquote>
<p>Spring에서 데이터를 데이터베이스 저장하는 방법이 3가지가 있다</p>
<ol>
<li>JDBC (java)</li>
<li>Spring JDBC</li>
<li>Persistence Framework</li>
</ol>
<hr>
<h2 id="persistence-framework">Persistence Framework</h2>
<blockquote>
<p>데이터의 저장, 조회, 변경, 삭제를 다루는 클래스 및 설정 파일들의 집합이다</p>
</blockquote>
<p>Java를 통해 개발을 하다보면 정보들을 객체에 담아 보관하게 된다</p>
<p>객체들의 정보를 저장하기 위해 데이터베이스 연결할 때의 어려움 발생
참고 : <strong><em><a href="https://gmlwjd9405.github.io/2019/08/03/reason-why-use-jpa.html">JPA를 사용하는 이유</a></em></strong></p>
<p><strong><em>Persistence Framework</em></strong>는 JDBC 프로그래밍의 어려움과는 달리 간단하게 데이터베이스와 연동되는 시스템을 개발할 수 있다 </p>
<p>종류</p>
<ul>
<li>JPA (ORM)</li>
<li>Hibernate (ORM)</li>
<li>Mybatis (SQL mapper)</li>
</ul>
<h3 id="orm-object-relation-mapping">ORM (Object-Relation Mapping)</h3>
<blockquote>
<p>객체와 관계형 데이터베이스의 데이터를 자동으로 Mapping 해주는 것이다</p>
</blockquote>
<p>객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 객체 모델과 관계형 모델 간에 불일치를 해결한다</p>
<ul>
<li><p>객체 지향적인 코드로 인해 직관적이고 가독성이 높다
→ 재사용 및 유지보수에 용이</p>
</li>
<li><p>DBMS에 대한 종속성이 줄어든다
→ SQL 쿼리가 아닌 메서드로 데이터를 조작할 수 있다</p>
</li>
</ul>
<h3 id="sql-mapper">SQL Mapper</h3>
<blockquote>
<p>객체와 관계형 데이터베이스의 데이터를 개발자가 SQL문으로 직접 작성하여 Mapping 시킬 수 있는 것이다</p>
</blockquote>
<p>개발자가 작성한 SQL문으로 해당되는 ROW를 읽어 값을 객체화 시켜 사용 가능하게 만들어준다</p>
<ul>
<li><p>SQL의 세부적인 내용 변경 시 유리하다</p>
</li>
<li><p>DBMS에 대한 종속적이다</p>
</li>
</ul>
<h3 id="jdbc">JDBC</h3>
<p><img src="https://images.velog.io/images/pyo-sh/post/ea05debd-9eaa-44d8-99d6-13831ed75f40/Untitled%201.png" alt=""></p>
<p>JDBC는 DB에 접근할 수 있도록 자바에서 제공하는 API이다.</p>
<p>모든 JAVA Data Access 기술의 근간이다
⇒ 모든 Persistance Framework는 내부적으로 JDBC API를 이용한다.</p>
<hr>
<h2 id="spring-data-jpa">Spring Data JPA</h2>
<blockquote>
<p>JPA를 쉽게 사용하기 위해 스프링에서 제공하고 있는 Framework
<a href="https://spring.io/projects/spring-data-jpa#overview"><strong>Spring.io</strong></a></p>
</blockquote>
<p><img src="https://images.velog.io/images/pyo-sh/post/450507bc-5a21-4c25-ac2d-29c861ca4631/Untitled%202.png" alt=""></p>
<p>구현체는 Hibernate를 사용하며 JPA는 Entity Manager로 감싸 사용한다</p>
<ul>
<li><p>CRUD 처리를 위한 공통 인터페이스를 제공한다</p>
</li>
<li><p>인터페이스만 작성하면 동적으로 구현체를 생성해서 주입해준다
→ 인터페이스만 작성해도 개발을 완료할 수 있다</p>
</li>
</ul>
<h3 id="사용-예시">사용 예시</h3>
<blockquote>
<p><a href="https://www.fastcampus.co.kr/dev_online_jvweb/">패스트캠퍼스 자바 올인원 강의</a></p>
</blockquote>
<p>스프링 데이터 JPA 의존성 추가</p>
<pre><code class="language-java">implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;</code></pre>
<p>코드 및 설명</p>
<pre><code class="language-java">import com.fasterxml.jackson.annotation.JsonInclude;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Transient;

// Identifier로 구분이 되는 객체들 / 관계형 데이터베이스 에서도 각 객체를 구분
@Entity
public class MenuItem {
  // Entity 의 ID를 뜻한다.
  @Id
  // 자동으로 생성 / 관리
  @GeneratedValue
  private Long id;

  private Long restaurantId;

  private String name;

  // 엔티티 클래스 내의 특정 변수를 영속 필드에서 제외
  @Transient
  @JsonInclude(JsonInclude.Include.NON_DEFAULT)
  private boolean destroy;
}</code></pre>
<p>H2 Database 사용</p>
<pre><code class="language-java">implementation &#39;com.h2database:h2&#39;</code></pre>
<h2 id="참조">참조</h2>
<p><a href="https://spring.io/projects/spring-data-jpa#overview">Spring.io</a></p>
<p><a href="https://velog.io/@adam2/JPA%EB%8A%94-%EB%8F%84%EB%8D%B0%EC%B2%B4-%EB%AD%98%EA%B9%8C-orm-%EC%98%81%EC%86%8D%EC%84%B1-hibernate-spring-data-jpa#%EA%B7%B8%EB%9F%BC-spring-data-jpa-%EA%B0%99%EC%9D%80%EA%B1%B4-%EB%AD%90%EC%A3%A0">JPA는 대체 뭘까?</a></p>
<p><a href="https://gmlwjd9405.github.io/2019/08/03/reason-why-use-jpa.html">JPA를 사용하는 이유</a></p>
<p><a href="https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/">JPA, Hibernate, 그리고 Spring Data JPA의 차이점</a></p>
]]></description>
        </item>
    </channel>
</rss>