<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>gang.log</title>
        <link>https://velog.io/</link>
        <description>괜찮은 개발자가 되어 보자</description>
        <lastBuildDate>Mon, 04 Mar 2024 15:35:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>gang.log</title>
            <url>https://velog.velcdn.com/images/gangk_99/profile/391dd5b3-2a3f-4f5b-9bac-11784d6699cf/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. gang.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gangk_99" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[프로그래머스 웹 풀 사이클 데브코스 2월 회고]]></title>
            <link>https://velog.io/@gangk_99/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9B%B9-%ED%92%80-%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-2%EC%9B%94-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@gangk_99/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9B%B9-%ED%92%80-%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-2%EC%9B%94-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 04 Mar 2024 15:35:10 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">국비지원교육 현실, 국비지원 교육 추천, 코딩 부트캠프 현실, 코딩 부트캠프 추천, 코딩 부트캠프 비교</span></p>
<h2 id="✨-2월을-회고하며">✨ 2월을 회고하며</h2>
<hr>
<p>조금 늦어버린 2월 회고록 작성...
6개월의 데브코스 기간 중 벌써 절반 이상이 흘렀다. 대학생이었다면 지금쯤 개강을 해서 바쁘게 학교를 다니고 있었겠지만... 나는 취업 준비생이기 때문에 집에만 박혀 있는 중이다.
일단 수업을 듣고는 있지만, 이제 슬슬 상반기 공고가 올라오고 있기 때문에 자소서 쓰기와 이력서 작성을 준비해야할 듯 싶다. 뭔가 내 자신이 크게 달라졌다는 생각은 못하고 있는데, 이렇게 벌써 상반기 취업을 준비해야하다니... 시간은 참 빠른 것 같다.</p>
<h3 id="💫-한-달-동안-한-것들">💫 한 달 동안 한 것들</h3>
<hr>
<ol>
<li><p>수업 듣기
: 다만 프론트엔드를 시작한 후부터는 조금 진도를 늦게 따라가고 있다. 최근에 따로 하는 일도 있고, 난 백엔드쪽으로 직무를 확실하게 정했기 때문에 일단 프론트엔드 쪽보다는 백엔드에 집중하고 있다. 물론 그렇다고 해서 소홀하게 프로젝트를 만드는 건 아니다. 그냥 힘을 좀 뺐다고 하는 게 옳은 표현일 것 같다!</p>
</li>
<li><p>새로운 팀 시작
: 기존 첫 번째 팀원들과 헤어지고 새로운 팀이 꾸려졌다. 이번에는 나까지 포함하여 총 여섯 명이 한 팀이 되었다. 이번에도 다양한 분들과 함께 팀이 되었다.</p>
</li>
<li><p>프로젝트 공부
: 추가하고 싶은 기능에 대해서 공부하고 있다. 다만 요즘 NestJS로 기존 프로젝트를 다시 만드는 걸 고민 중인데... 마침 지금 리액트도 타입스크립트로 짜고 있기도 하고 타입스크립트 공부를 하면서 NestJS를 시도해보는 건 어떨까... 고민중이다.</p>
</li>
</ol>
<h3 id="❤️-잘한-점">❤️ 잘한 점</h3>
<hr>
<p>솔직히 이번 달의 잘한 점을 잘 모르겠다... 집에 일이 있어 이전과는 확실히 많이 해이해지고 무기력해졌다. 지금은 어느정도 해결이 되기도 했고, 일단 최대한 무기력한 기분을 해소하기 위해 산책을 하거나 음악을 듣거나 이런 방법들로 해소를 하고 있다.</p>
<h3 id="💧-아쉬운-점">💧 아쉬운 점</h3>
<hr>
<p>아쉬운 점... 사실 너무 많다. 수업 진도가 좀 늦어진 점, 백엔드 프로젝트를 완성한다고 하고 막상 그 이후로는 거의 손대지 못한 점, 기타 등등... 가장 아쉬운 건 무기력해진 내 자신이다. 멍하니 책상 앞에 앉아 있는 시간이 많이 길어졌다. 딱히 취업에 대한 불안감은 아닌 것 같은데... 번아웃이 온 것 같기도 하다.</p>
<h3 id="🍀-앞으로-해야-할-것">🍀 앞으로 해야 할 것</h3>
<hr>
<ol>
<li><p>번아웃 극복하기
: 사실 말이 쉽지 가장 어려운 게 아닐까 싶다. 휴대폰을 보는 시간을 줄이고 멍때리는 시간도 최대한 줄이려고 한다. 이유 모를 무력감에 시달리면 노래를 들으며 산책을 하거나, 밖에서 음료라도 한 잔 마시고 오거나 하면서 기분 전환을 할 예정이다.</p>
</li>
<li><p>취업 준비
: 말했던대로 상반기 공채 시즌이다. 딱히 높은 데만 노리는 건 아니지만, 사실 취업에 대한 간절함도 아직 없어서...(이게 가장 문제인듯) 그래도 자소서나 이력서 꾸준히 넣어 보고 코딩 테스트도 좀 봐보려고 한다.</p>
</li>
<li><p>코딩 테스트 준비
: 요즘 알고리즘 공부에 너무 손을 놓고 있었다. 하루에 그래도 적어도 세 문제 이상은 풀고 싶다. 백엔드로 정했기 때문에 기존에 썼던 파이썬으로 코테 언어를 정했다. 근데 문제는 이미 또 자바스크립트로 한동안 준비를 해서... 파이썬도 금방 까먹었다😂 금붕어 기억력이냐고...</p>
</li>
<li><p>NestJS...?
: 이건 조금 생각을 해봐야할 것 같다. 위의 일에 현재 진행하는 프로젝트만 하더라도 시간이 부족할 것 같은데... 근데 취업을 생각하면 또 공부를 하는 게 맞는 것 같고... 어쩌면 타입스크립트를 하는 지금이 기회가 아닐까 싶다.</p>
</li>
</ol>
<p><del>그리고 이건 조금 뻘소리인데... 사실 요즘 게임 개발이 좀 궁금해졌다. 근데 게임 개발이라고 한다면 보통 유니티이고 이건 C#을 사용하니... 웹개발과는 완전 다른 쪽이다. 머리로는 웹개발을 선택해야한다는 건 아는데 자꾸 가슴은 게임 개발쪽에 흥미가 간다...😂</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[VS Code] Prettier 설정하기]]></title>
            <link>https://velog.io/@gangk_99/VS-Code-Prettier-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@gangk_99/VS-Code-Prettier-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 25 Feb 2024 17:37:03 GMT</pubDate>
            <description><![CDATA[<h2 id="⭐-prettier란">⭐ Prettier란?</h2>
<hr>
<p><code>Prettier</code>란 내가 작성한 코드를 보기 좋게, 또 일관성 있게 포맷팅을 해주는 도구다. 줄바꿈이나 들여쓰기 간격, 세미콜론을 사용할 것인지, 큰따옴표를 쓸 것인지 혹은 작은 따옴표를 쓸 것인지를 설정해서 내가 파일을 저장할 때마다 형식에 맞게 보기 좋게 정렬을 해준다.</p>
<p>예시를 보여주자면</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/206611b5-272c-4636-b712-110922725703/image.png" alt=""></p>
<p>이렇게 들여쓰기도 엉망으로 되어 있고 세미콜론도 없는(자바스크립트에서는 필수가 아니긴 하지만) 이 파일을 저장하는 순간!</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/4af4f1dd-76cc-4221-b22f-2b62c8917e36/image.png" alt=""></p>
<p>이런 식으로 알아서 내가 원하는 포맷팅 형식에 맞게 정리한 후 저장을 해주는 거다. 편리하고, 가독성도 좋아지기 때문에 많은 사람들이 이 도구를 사용한다.</p>
<h3 id="❤️-prettier-설정하기">❤️ Prettier 설정하기</h3>
<hr>
<p>이제 차근히 Prettier 옵션을 내가 원하는 대로 설정해보자.</p>
<p><strong>1. VS code의 Extension에서 Prettier 설치</strong></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/d0eb2fe0-26e3-4657-9564-c57b990d04f6/image.png" alt=""></p>
<p>VS Code의 Extension에서 먼저 Prettier 확장 프로그램을 설치해준다. 여러 개가 나올 텐데 가장 위에 있는, 즉 사진과 같은 익스텐션을 설치해주자.</p>
<p><strong>2. Setting 설정</strong></p>
<p>먼저 <code>Ctrl</code> + <code>,</code>를 동시에 눌러 vscode의 설정에 들어가준다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/0e30b121-6151-4dda-a958-1d1aeb4dd078/image.png" alt=""></p>
<p><code>Editor: Default Formatter</code>을 검색란에 입력 후 해당 설정을 <code>Prettier</code>로 설정해준다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f965834c-2af0-478b-b159-c392e0496543/image.png" alt=""></p>
<p>그 다음 <code>Editor: Format on save mode</code>를 검색 후 체크해준다. (파일을 저장할 때마다 자동으로 prettier 실행하도록)</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/ca806a62-0238-448f-866b-dcf71fd6bda9/image.png" alt=""></p>
<p>마지막으로 <code>Prettier</code>를 검색 후 원하는대로 설정해주면 된다. 각 옵션이 어떤 걸 의미하는지는 뒷부분에서 설명할 예정이다.</p>
<p>만약 모든 프로젝트에 동일한 Prettier를 설정할 예정이라면 위와 같이 설정하고 끝내도 무관하다. 하지만 각기 다른 프로젝트에 다른 포맷팅을 적용하고 싶을 때가 있다. 또한 협업을 진행하게 된다면 팀 컨벤션에 맞추어 Prettier 설정이 달라지기도 한다. 보통 이럴 때는 <code>.prettierrc</code>라는 설정 파일을 생성하여 해당 파일에 원하는 포맷팅 형식을 저장한다. 이후에 다룰 내용이 바로 이 파일을 생성하고 원하는 옵션을 적용해보는 것이다.</p>
<p><strong>3. <code>.prettierrc</code> 파일로 개별 옵션 설정하기</strong></p>
<p><strong>3-1. prettier 설치</strong></p>
<p>먼저 npm을 통해서 <code>prettier</code>를 설치해준다. <code>prettier</code>는 배포에 무관한 개발 관련 패키지 이기 때문에 --save-dev 옵션을 추가해서 설치해주면 된다.</p>
<pre><code>npm i --save-dev prettier</code></pre><p><img src="https://velog.velcdn.com/images/gangk_99/post/2ef8483e-174c-480a-afb6-974370884b2d/image.png" alt=""></p>
<p>이렇게 <code>devDependencies</code>에 정상적으로 <code>prettier</code>가 설치된 것을 볼 수 있다.</p>
<p>3-2. <code>.prettierrc</code> 파일 생성</p>
<p>가장 루트 폴더에 <code>.prettierrc</code> 파일을 하나 생성해준다. 그리고 원하는 Prettier 옵션을 추가해서 설정해준다.</p>
<p>물론 우리가 vscode 내 세팅란에서 prettier 설정을 완료했지만, vscode 세팅에서 설정한 값보다 <code>.prettierrc</code> 파일의 적용 순위가 더 높기 때문에 기존 설정값이 무시되고 <code>.prettierrc</code>에서의 설정값이 적용된다. 따라서 보통 협업할 때에도 해당 파일을 사용해서 팀 컨벤션을 적용하곤 한다.</p>
<p>옵션에 대해서는 따로 추가를 해두었지만, 가장 자주 쓰는 몇 가지만 예시로 보여주고자 한다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/de490426-b7f7-4820-a272-8153e264da91/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/64bf7be5-22e9-4125-8863-478f3564f8cc/image.png" alt=""></p>
<p>먼저 기본으로 설정한 <code>.prettierrc</code>를 적용했을 때의 파일이다. 여기서 이제 <code>tabWidth</code> 설정을 바꿔보자. 참고로 <code>tabWidth</code>는 들여쓰기 시 공백을 얼만큼 줄 것인지 설정하는 옵션이다. 기본적으로는 4로 설정이 되어 있는데 난 이게 너무 넓다고 생각해서 보통은 2~3으로 줄여서 사용하는 편이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f2c2850a-bdd3-43e7-aa9c-94baf7f14017/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5b373a15-65c7-49e5-8804-e7efaafc3967/image.png" alt=""></p>
<p>이렇게 파일을 수정하면 위 예시처럼 들여쓰기 간격이 확연히 줄어든 걸 확인할 수 있다.</p>
<p>그 다음으로는 세미콜론을 추가할지의 여부도 선택할 수 있는데, 자바스크립트는 세미콜론을 쓰지 않아도 무방하다. 다만 내게 있어서는 습관이기도 하고 과거에는 대부분 세미콜론을 붙이는 경향이 대다수였기 때문에 나는 보통 true로 설정해둔다. 근데 보면 최근에는 코드를 가독성 좋고 깔끔하게 작성하는 것을 선호하기 때문에 세미콜론을 일부러 붙이지 않는 경우도 많다고 한다. 이건 취향 차이일 것 같으니 개인 성향에 맞게 요령껏 조절해주면 될 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/b62797d9-d9d6-43f3-a9f4-7ef4b02d481a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/bd452b20-7aca-423c-97d0-636b495c9477/image.png" alt=""></p>
<p>해당 파일을 저장 후 확인해보면 기존의 세미콜론들이 전부 사라진 걸 확인할 수 있다.</p>
<p><strong>4. <code>.prettierignore</code> 파일 생성하기</strong></p>
<p>Git을 자주 사용하는 개발자라면 <code>.gitignore</code> 파일에 대해 다들 익숙할 것이다. <code>.prettierignore</code> 파일 또한 <code>.gitignore</code> 파일과 비슷한 역할을 한다. 말 그대로 prettier가 적용되지 않을 파일을 설정해두는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/310ac8e7-a554-4a96-a192-6272b554c7e9/image.png" alt=""></p>
<p>나는 예시로 작성한 파일 하나와 패키지 파일들에는 prettier가 적용되지 않도록 설정했다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/549b1f94-6249-43d5-8e4e-6016ac1af109/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/1faafc88-c808-408f-bf82-e2df453e814a/image.png" alt=""></p>
<p>확인해보면 해당 파일들에는 내가 설정해둔 Prettier 옵션이 적용되지 않는 것을 확인할 수 있다!</p>
<h3 id="✨-prettier-옵션-뜯어보기">✨ Prettier 옵션 뜯어보기</h3>
<hr>
<pre><code class="language-javascript">{
  &quot;arrowParens&quot;: &quot;always&quot;, // 화살표 함수의 매개변수가 하나일 때 괄호를 사용할지 여부
  &quot;bracketSpacing&quot;: true, // 객체 리터럴에서 중괄호 내부에 공백 삽입할지 여부 
  &quot;endOfLine&quot;: &quot;auto&quot;, // EoF 방식, OS별로 처리 방식이 다름 
  &quot;htmlWhitespaceSensitivity&quot;: &quot;css&quot;, // HTML 공백 감도 설정
  &quot;jsxBracketSameLine&quot;: false, // JSX의 마지막 `&gt;`를 다음 줄로 내릴지 여부 
  &quot;jsxSingleQuote&quot;: false, // JSX에 singe 쿼테이션 사용 여부
  &quot;printWidth&quot;: 80, //  한 줄에 출력되는 코드의 최대 길이
  &quot;proseWrap&quot;: &quot;preserve&quot;, // markdown 텍스트의 줄바꿈 방식 (v1.8.2)
  &quot;quoteProps&quot;: &quot;as-needed&quot; // 객체 속성에 쿼테이션 적용 방식
  &quot;semi&quot;: true, // 세미콜론 사용 여부
  &quot;singleQuote&quot;: true, // single 쿼테이션 사용 여부
  &quot;tabWidth&quot;: 2, // 탭 간격
  &quot;trailingComma&quot;: &quot;all&quot;, // 여러 줄을 사용할 때, 후행 콤마 사용 방식
  &quot;useTabs&quot;: false, // 탭 사용 여부
  &quot;vueIndentScriptAndStyle&quot;: true, // Vue 파일의 script와 style 태그의 들여쓰기 여부 (v1.19.0)
  &quot;parser&quot;: &#39;&#39;, // 사용할 parser를 지정, 자동으로 지정됨
  &quot;filepath&quot;: &#39;&#39;, // parser를 유추할 수 있는 파일을 지정
  &quot;rangeStart&quot;: 0, // 포맷팅을 부분 적용할 파일의 시작 라인 지정
  &quot;rangeEnd&quot;: Infinity, // 포맷팅 부분 적용할 파일의 끝 라인 지정,
  &quot;requirePragma&quot;: false, // 파일 상단에 미리 정의된 주석을 작성하고 Pragma로 포맷팅 사용 여부 지정
  &quot;insertPragma&quot;: false, // 미리 정의된 @format marker의 사용 여부 (v1.8.0)
  &quot;overrides&quot;: [ 
    {
      &quot;files&quot;: &quot;*.json&quot;,
      &quot;options&quot;: {
        &quot;printWidth&quot;: 200
      }
    }
  ], // 특정 파일별로 옵션을 다르게 지정함, ESLint 방식 사용
}</code></pre>
<p>위 옵션들은 아래 정리된 블로그에서 발췌했다. Prettier 외에도 ESLint에 대해서 잘 정리해주셔서 도움이 많이 되었으니 다들 한 번 쯤 읽어보는 걸 추천한다!</p>
<p><a href="https://velog.io/@kyusung/eslint-prettier-config">ESLint 와 Prettier 적용</a></p>
<h3 id="🌸-prettier-옵션을-확인해보자">🌸 Prettier 옵션을 확인해보자</h3>
<hr>
<p>사실 위에 정리해두긴 했지만 글로만 정리되어 있다 보니 정확히 어떤 옵션이 무엇을 뜻하는지 알기 어려울 때도 있다. 내가 원하는 옵션을 눈으로 확인하면서 직접 <code>.prettierrc</code>를 작성하고자 할 때 아래 사이트가 큰 도움이 될 것이다.</p>
<p><a href="https://prettier.io/">Prettier</a></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f067393a-8ac1-4d8e-a9bd-617e9694f645/image.png" alt=""></p>
<p>위 사이트에 접속하면 우상단에 Playground라는 메뉴가 보인다. 해당 메뉴를 클릭해서 들어가자.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/3574abe7-9763-45e5-9724-04f80f6a482e/image.png" alt=""></p>
<p>그러면 위 사진처럼 옵션을 하나씩 수정해가면서 결과물을 눈으로 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/9776f8b9-6092-4215-917c-4354b3f1b665/image.png" alt=""></p>
<p>원하는 옵션을 전부 선택하면 하단에 위 사진과 같은 메뉴가 뜰 텐데, 여기서 <code>Copy config JSON</code>을 클릭하자. 그러면 우리가 방금 눈으로 보며 설정한 prettier 옵션을 손쉽게 우리의 프로젝트 내에서 사용할 수 있다.</p>
<pre><code class="language-javascript">{
  &quot;arrowParens&quot;: &quot;always&quot;,
  &quot;bracketSameLine&quot;: true,
  &quot;bracketSpacing&quot;: true,
  &quot;semi&quot;: false,
  &quot;experimentalTernaries&quot;: false,
  &quot;singleQuote&quot;: true,
  &quot;jsxSingleQuote&quot;: true,
  &quot;quoteProps&quot;: &quot;as-needed&quot;,
  &quot;trailingComma&quot;: &quot;es5&quot;,
  &quot;singleAttributePerLine&quot;: false,
  &quot;htmlWhitespaceSensitivity&quot;: &quot;css&quot;,
  &quot;vueIndentScriptAndStyle&quot;: false,
  &quot;proseWrap&quot;: &quot;preserve&quot;,
  &quot;insertPragma&quot;: false,
  &quot;printWidth&quot;: 80,
  &quot;requirePragma&quot;: false,
  &quot;tabWidth&quot;: 4,
  &quot;useTabs&quot;: false,
  &quot;embeddedLanguageFormatting&quot;: &quot;auto&quot;
}</code></pre>
<p>복사한 내용을 <code>.prettierrc</code> 파일에 붙여넣기 해주면 우리가 원하는 옵션으로 prettier 설정이 손쉽게 완료되었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 그래서 Redux를 왜 쓰는데?]]></title>
            <link>https://velog.io/@gangk_99/React-%EA%B7%B8%EB%9E%98%EC%84%9C-Redux%EB%A5%BC-%EC%99%9C-%EC%93%B0%EB%8A%94%EB%8D%B0</link>
            <guid>https://velog.io/@gangk_99/React-%EA%B7%B8%EB%9E%98%EC%84%9C-Redux%EB%A5%BC-%EC%99%9C-%EC%93%B0%EB%8A%94%EB%8D%B0</guid>
            <pubDate>Thu, 15 Feb 2024 03:33:59 GMT</pubDate>
            <description><![CDATA[<h2 id="❓-redux란">❓ Redux란?</h2>
<hr>
<h3 id="💧-prop-drilling">💧 Prop drilling...</h3>
<hr>
<p>React에서 상태 관리를 할 때 기본적으로는 <code>props</code>를 이용하여 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달한다. 컴포넌트의 depth가 깊지 않다면 <code>props</code>를 이용해도 큰 불편함이나 어려움은 느끼지 않을 수 있다.
하지만 개발 스케일이 커진다면 필히 컴포넌트의 depth도 깊어지게 된다. 아래 사진을 한 번 보자.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/b24790fb-1779-4564-8666-d163077c6788/image.png" alt=""></p>
<p><a href="https://1-blue.github.io/posts/Redux/">출처</a></p>
<p>최상위 컴포넌트에서 최하위 컴포넌트로 데이터를 전달하려면 2 -&gt; 5 -&gt; 8 -&gt; 11 순서로 접근을 해야한다. 만약 여기서 depth가 더 깊어진다면? 아주 복잡하고 또한 유지 보수가 어려워지게 된다. 이렇게 데이터를 전달하기 위해서 중간 컴포넌트를 다수 거쳐야 하는 상황을 <code>props drilling</code>이라고 하는데, 우린 이걸 방지하기 위해 상태 관리 라이브러리를 사용하게 된다.</p>
<h3 id="⭐-그래서-redux를-사용한다">⭐ 그래서 Redux를 사용한다</h3>
<hr>
<p>이 불편함 속에서 등장한 것이 <code>Redux</code>이다. <code>Redux</code>는 상태 관리 라이브러리로 이 <code>props</code> 없이 상태값을 보다 편리하게 관리할 수 있게 된다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/8fb0d694-cc8b-4102-a9ed-23a996278b56/image.png" alt=""></p>
<p><a href="https://www.scalablepath.com/react/context-api-vs-redux">출처</a></p>
<p>위 사진을 보면 대충 예상이 갈 것이다. 간단하게 표현을 하자면, 저 사진에 보이는 <code>store</code>라는 매체에 상태값을 저장해두고 상태를 업데이트하고 업데이트 된 상태를 전달 받을 수 있는 거다. 즉, 상태값이 컴포넌트에 종속이 되는 것이 아니라 상태 관리를 바깥에서 바깥에서 할 수 있게 된다.</p>
<h3 id="✨-redux의-기본-개념">✨ Redux의 기본 개념</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/7d241eb3-b47e-4def-9bda-0c4b45eae731/image.gif" alt=""></p>
<p><a href="https://redux.js.org/tutorials/essentials/part-1-overview-concepts">출처</a></p>
<p>먼저 Redux의 기본적인 흐름은 위와 같다.</p>
<ol>
<li>먼저 Store에 저장된 state에 접근을 한다.</li>
<li>위 예시처럼 deposit이나, withdraw처럼 state가 변경되어야 하는 이벤트가 발생하면, Action 객체가 생성된다.</li>
<li>이제 Store에서 이전 state와 Action을 입력 받아 Reducer을 실행하고 리턴값을 새로운 state로 저장한다.</li>
<li>state가 변하면 React는 리렌더링을 한다.</li>
</ol>
<p>여기서 언급된 키워드에 대해서 설명을 하자면,</p>
<ol>
<li><code>Action</code>
: 상태를 변경하기 위한 요청이나 명령을 나타낸다. 액션은 일반적으로 자바스크립트 객체이며 type 속성은 필수적으로 가지고 있어야 한다. 그 외에 필요한 값들은 상황에 따라 추가될 수 있다.</li>
</ol>
<pre><code class="language-javascript">const increaseCounter = {
  type: &#39;INCREASE_COUNTER&#39;,
};

const setUser = {
  type: &#39;SET_USER&#39;,
  data: { id: 1, name: &#39;John Kim&#39; },
};
</code></pre>
<ol start="2">
<li><code>Reducer</code>
: 변화를 일으키는 함수다. 현재 state와 Action을 파라미터로 입력받아 변화된 state 값을 반환한다.
React의 useReducer에서는 일반적으로 default에 에러를 발생시키도록 처리하는게 일반적이지만 Redux의 Reducer에서는 기존 state를 그대로 반환하도록 작성해야합니다.</li>
</ol>
<pre><code class="language-javascript">const counterReducer = (state = 0, action) =&gt; {
  switch (action.type) {
    case &#39;INCREASE_COUNTER&#39;:
      return state + 1;
    default:
      return state;
  }
};</code></pre>
<ol start="3">
<li><p><code>Store</code>
: 컴포넌트 외부에 있는 상태 저장소이다. 현 state들과 Reducer, 그리고 몇 가지 내장 함수들이 포함되어 있다.</p>
</li>
<li><p><code>Dispatch</code>
: Action을 발생시키는 함수이다. Dispatch로 호출을 하면 Action 객체가 생성이 되고, 그 후 Reducer가 호출이 된다. 그리고 Reducer에서 반환된 값으로 액션을 처리해서 새로운 상태가 만들어지는 것이다.</p>
</li>
<li><p><code>Subscribe</code>
: Subscribe라는 개념도 있는데, 단어에서 보면 알 수 있듯이 말 그대로 Store를 구독을 하고 있다가 Dispatch가 이루어질 때마다 전달한 함수를 호출해준다.</p>
</li>
</ol>
<h3 id="🚨-redux의-세-가지-원칙">🚨 Redux의 세 가지 원칙</h3>
<hr>
<p>지금까지 설명을 보면 알 수 있듯 과정이 꽤나 복잡하다. 그렇기에 Redux를 사용할 때 통용되는 규칙이 세 가지가 있다.</p>
<ol>
<li><p>Store는 하나만 사용하기
: 하나의 어플리케이션에서는 하나의 Store만 사용하는 것을 권장한다. 상태가 예측 가능하며 디버깅이 용이해지고 데이터 흐름이 단순화된다는 장점이 있다.</p>
</li>
<li><p>상태는 읽기 전용
: Redux에서 상태는 불변해야 한다. 상태를 직접 수정하는 것이 아니라 기존 상태를 변경하지 않고 새로운 상태를 반환하는 방식으로 상태를 업데이트해야 한다. 이는 상태 변화를 추적하고 예측 가능한 방식으로 관리하는 데 도움이 된다.</p>
</li>
<li><p>Reducer는 순수 함수여야 한다
: Reducer는 현재 상태와 액션을 파라미터로 받아 새로운 상태를 반환하는 함수다. 이 함수는 동일한 파라미터를 입력할 시 항상 동일한 결과를 반환하는 순수 함수여야 한다. 이는 상태 변화를 예측 가능하게 만들어주고 디버깅이 용이하게 만들어준다.</p>
</li>
</ol>
<h3 id="🍀-redux의-장단점">🍀 Redux의 장단점</h3>
<hr>
<p>이제 마지막으로 Redux의 장점과 단점에 대해 알아보자.</p>
<ul>
<li><p>장점</p>
<ul>
<li>Store를 통해 상태를 한 곳에서 관리하기 때문에 전역 상태를 관리할 때 아주 효과적이다.</li>
<li>상태가 읽기 전용이기 때문에 불변성을 유지하게 된다. 이는 상태 변화가 예측이 가능하기 때문에 디버깅이 쉬워진다. 또한 이전 상태로 돌아가기 위해서는 그저 이전 상태를 현재 상태에 덮어쓰기만 하면 되기 때문에 특정 시점으로 돌아가기도 쉽다.
하지만 여기서 주의할 점이 한 가지 있다. Redux는 상태를 읽기 전용으로 취급할 뿐, 실제 읽기 전용으로 만들어주는 것은 아니다. 따라서 상태를 실수로 직접 변경하지 않도록 항상 주의해야 한다.</li>
<li>단방향 모델링이다. action을 dispatch 할때마다 기록이 남기 때문에 에러를 찾기 쉽다.</li>
</ul>
</li>
<li><p>단점</p>
<ul>
<li>기본적으로 러닝 커브가 높다. 상태나 스토어, 리듀서 등등 생소한 개념을 익혀야 하기 때문에 초보자가 공부하기에는 어려운 편이다.</li>
<li>코드의 양이 증가한다. 몇 가지의 파일을 필수로 만들어야하기 때문에 만약 작은 애플리케이션을 개발할 때는 적합하지 않을 수 있다. 중대형 프로젝트를 진행하는 데 적합하다.</li>
<li>보일러플레이트 코드가 많이 발생할 수 있다. 보일러플레이트 코드란 애플리케이션에서 반복적으로 발생하는, 그리고 주로 기능과는 직접적인 연관이 없는 일반적인 코드를 나타낸다. 주로 액션이나 리듀서 코드를 작성할 때 발생한다.</li>
</ul>
</li>
</ul>
<p>다만 위의 단점들을 해결하기 위해 <code>Redux Toolkit</code>이라는 것이 나왔다. 상세하게 들어간다면 너무 길어지기 때문에 몇 가지만 가볍게 짚고 넘어가자면,</p>
<ol>
<li>Action type과 Action creator를 따로 생성해주지 않아도 된다.</li>
<li>immer가 내장되어 있어 mutable 객체를 사용할 수 있다.</li>
<li>redux thunk가 내장되어 있어 비동기를 지원한다.</li>
<li>타입스크립트 지원이 잘 된다.</li>
</ol>
<p>즉, <code>Redux</code>의 복잡성을 낮추고 사용성을 높이기 위해 만들어진 것이 바로 <code>Redux Toolkit</code>이다.</p>
<h4 id="참고-자료">참고 자료</h4>
<hr>
<p><a href="https://www.youtube.com/watch?v=QZcYz2NrDIs&amp;t=51">React 입문자들이 알아야할 Redux 쉽게설명 (8분컷)</a></p>
<p>이미 알 사람들이라면 전부 알겠지만 코딩 유튜버이신 코딩애플 님이시다. <code>Redux</code>에 대해서 정말 쉽게 설명을 해주시는데 영상 길이가 길지도 않으니 꼭 한 번 쯤 봐보는 걸 추천한다.</p>
<p><a href="https://velog.io/@youthfulhps/What-is-Redux-and-why-use-it#-%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0-%EC%95%9E%EC%84%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%84%EB%9E%B5%ED%95%9C-%EC%84%A4%EB%AA%85">리덕스는 무엇이고, 왜 사용하는가?</a></p>
<p>벨로그에 올라온 <code>Redux</code> 정리글이다. 위 영상이 <code>Redux</code>에 대해서 간단한 개념을 잡아준다면 이건 좀 더 상세한 내용을 짚어준다. 설명이 굉장히 잘 되어 있으니 위 벨로그 글을 읽어보는 것도 추천한다! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] 클래스와 접근 제어자]]></title>
            <link>https://velog.io/@gangk_99/Typescript-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%A0%91%EA%B7%BC-%EC%A0%9C%EC%96%B4%EC%9E%90</link>
            <guid>https://velog.io/@gangk_99/Typescript-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%A0%91%EA%B7%BC-%EC%A0%9C%EC%96%B4%EC%9E%90</guid>
            <pubDate>Thu, 08 Feb 2024 05:17:02 GMT</pubDate>
            <description><![CDATA[<h3 id="✨-클래스란">✨ 클래스란?</h3>
<hr>
<p>비슷한 특징을 가진 객체를 생성하기 위한 템플릿이라고 할 수 있다. 자바스크립트에서도 존재하지만 완전히 똑같은 코드로 클래스를 생성하는 것은 아니다.</p>
<pre><code class="language-javascript">class Student {
  constructor(name) {
    this.name = name;
  }
}</code></pre>
<p>자바스크립트는 클래스 내에 인스턴스 변수를 굳이 선언해줄 필요가 없다. 클래스 내부의 생성자에서 this를 통해 인스턴스에 동적으로 추가가 된다.</p>
<pre><code class="language-typescript">class Student {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}</code></pre>
<p>하지만 타입스크립트 정적 언어기 때문에 클래스 내에 인스턴스 변수를 선언해주어야 한다. 클래스를 생성하는 방법은 위와 같다.</p>
<pre><code class="language-typescript">const student = new Student(&#39;John&#39;);</code></pre>
<p>클래스를 기반으로 객체를 생성하기 위해서는 <code>new</code> 키워드를 통해 객체를 생성해야 한다. 그 후 클래스 내부에 실행하고 싶은 메서드가 있다면 생성된 객체에 접근하여 클래스 내부의 메서드를 실행시킨다.</p>
<pre><code class="language-typescript">class Employee {
    empName: string;
    age: number;
    empJob: string;

    constructor(name: string, age: number, job: string) {
        this.empName = name;
        this.age = age;
        this.empJob = job;
    }

    printEmp(): void {
        console.log(&#39;이름 : &#39; + this.empName);
        console.log(&#39;나이 : &#39; + this.age);
        console.log(&#39;직업 : &#39; + this.empJob);
    }
}

const emp = new Employee(&#39;John&#39;, 30, &#39;Developer&#39;);
emp.printEmp();</code></pre>
<p>이건 클래스를 기반으로 객체를 생성하고 클래스 내부의 메서드에 접근하여 출력하는 코드의 예시다. 실행을 해보면 아래와 같이 잘 실행이 되는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/667b48e4-4049-440f-8d11-94186816e6e8/image.png" alt=""></p>
<h3 id="✨-접근-제어자">✨ 접근 제어자?</h3>
<hr>
<p>자바를 해 본 사람이라면 아주 익숙한 접근 제어자가 타입스크립트에도 존재한다! 자바랑은 다르게 패키지라는 개념이 없어 default 키워드는 제외가 되었고, 그래서 기본 제어자가 public으로 설정된다고 한다. 즉, 우리가 접근 제어자 없이 코드를 작성하면 기본적으로 모두 public으로 설정된다고 보면 된다.</p>
<p>접근 제어자에는 <code>public</code>, <code>private</code>, <code>protected</code>가 존재한다. 뭔가 단어에서부터 느껴지겠지만 public은 어디서든 접근할 수 있는 제어자, private은 내 클래스 내에서만 접근이 가능하고, protected는 내 클래스를 상속한 자식 클래스 내까지만 접근이 가능하다.</p>
<ol>
<li><code>public</code> 접근 제어자</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5810ac9d-d9d4-45e0-9220-0a1d4083ade5/image.png" alt=""></p>
<p><code>public</code> 접근 제어자는 언제 어디서든 접근할 수 있다. 클래스 외부에서도 언제든지 접근이 가능하다.</p>
<ol start="2">
<li><code>private</code> 접근 제어자</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/6e913727-ce7a-4b85-b543-a68b21ae4e18/image.png" alt=""></p>
<p>private 접근 제어자는 클래스 내부에서만 접근이 가능하다. name 변수는 private으로 선언했고, 이름을 출력하는 printName은 public으로 선언을 했다. name 변수는 같은 클래스 내부에서 접근이 가능하기 떄문에 클래스 내의 printName에서 사용했을 때는 에러가 발생하지 않지만, 클래스 외부에서 접근하려고 하면 에러가 발생하는 것을 볼 수 있다.</p>
<ol start="3">
<li><code>protected</code> 접근 제어자</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/86a39a9d-3f9d-47ba-91d2-4932564c9799/image.png" alt=""></p>
<p>protected 접근자를 먼저 Person3 클래스 내부에 선언을 한다. 위에서도 이야기했듯 protected 접근 제어자는 해당 클래스를 상속한 자식 클래스 내에까지만 접근이 가능하다. 따라서 Person3을 상속한 Student 클래스 내에서는 protected 제어자로 선언한 name 변수에 접근할 수 있다. 하지만 클래스  외부에서 선언하려고 하면 에러가 발생하는 것을 볼 수 있다. 또한 printName도 접근할 수 없다는 에러가 발생하는 것을 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] Array와 Tuple (Tuple에서 왜 push, pop 등이 가능한가?)]]></title>
            <link>https://velog.io/@gangk_99/Typescript-Array%EC%99%80-Tuple-Tuple%EC%97%90%EC%84%9C-%EC%99%9C-push-pop-%EB%93%B1%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%9C%EA%B0%80</link>
            <guid>https://velog.io/@gangk_99/Typescript-Array%EC%99%80-Tuple-Tuple%EC%97%90%EC%84%9C-%EC%99%9C-push-pop-%EB%93%B1%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%9C%EA%B0%80</guid>
            <pubDate>Thu, 08 Feb 2024 03:05:40 GMT</pubDate>
            <description><![CDATA[<h3 id="✨-array와-tuple">✨ Array와 Tuple</h3>
<hr>
<p>Array는 다른 언어에서도 많이 나오는 개념이고 당연히 자바스크립트에도 존재하기 때문에 이해하기 어렵지 않을 것이다. 타입스크립트에는 이런 배열 형태를 조금 더 특수하게 사용할 수 있는 tuple이라는 타입이 있다. 자바스크립트에서는 지원되지 않는 데이터 타입이다.
파이썬에도 튜플이 있기는 하다. 요솟값을 생성, 수정, 삭제를 할 수 있는 리스트와는 다르게 튜플은 요솟값을 바꿀 수 없다. 이 파이썬의 튜플과 타입스크립트의 튜플이 같다고 할 수는 없지만 비슷한 점은 존재한다. 타입스크렙트의 튜플에서는 타입에 맞게 요솟값을 수정을 할 수는 있지만 마찬가지로 생성이나 삭제는 불가능하다(라고 생각했는데...). 이후에 설명하겠지만 튜플은 타입의 순서도 정해져 있고 고정된 크기를 가지고 있기 때문이다.</p>
<h4 id="💫-array">💫 Array</h4>
<hr>
<p>타입스크립트에서 배열을 생성하는 방법은 간단하다.</p>
<pre><code class="language-javascript">let numbers: number[] = [1, 2, 3, 4, 5];
let fruits: string[] = [&#39;apple&#39;, &#39;banana&#39;, &#39;strawberry&#39;];</code></pre>
<p>기존 타입을 지정했던 방식과 동일하게 타입을 선언하되, 그 뒤에 배열을 생성하는 대괄호를 추가해주면 된다. 일단은 기본적으로 하나의 타입으로만 배열을 생성하였지만, 지난번에 배웠던 유니온 타입을 이용해서 여러 타입을 선언할 수도 있다.</p>
<pre><code class="language-javascript">let mixedArray: (string | number)[] = [&#39;apple&#39;, 3, 4, &#39;banana&#39;];</code></pre>
<p>만약 배열을 순회하며 타입에 따라 다른 동작을 수행하고 싶게 하고 싶다면 타입가드를 사용하자.</p>
<p>Array는 동적인 크기를 가지고 있기 때문에 삽입, 수정, 삭제 등이 자유롭다. 아래 예시 사진을 한 번 보자.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/a4202347-8010-4f55-826e-39137e7b99b8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/45284ce9-6df3-4def-bf6e-92476728aea3/image.png" alt=""></p>
<p>위처럼 배열의 정렬이나 삽입, 수정 등의 메서드를 자유롭게 사용할 수 있다.</p>
<h4 id="💫-tuple">💫 Tuple</h4>
<hr>
<p>튜플은 동적인 크기를 가지는 배열과는 다르게 고정된 크기를 가진다. 또한 서로 다른 타입을 가질 수 있다. 사용 방법은 아래와 같다.</p>
<pre><code class="language-javascript">let tuple: [string, number] = [&#39;apple&#39;, 3];
let tuple2: [number, string, boolean] = [&#39;apple&#39;, 3, true]; // Error</code></pre>
<p>고정된 크기를 가지고 있고, 타입의 순서가 정해져 있기 때문에 해당하는 순서에 맞지 않는 값이 오거나 다른 크기의 요소가 할당될 수 없다.
그래서 당연히 요솟값을 직접적으로 삽입, 삭제하는  push나 pop 등이 오지 못할 거라고 생각을 했는데...</p>
<h4 id="🚨-tuple에서-push-pop-등이-사용되는-이유">🚨 Tuple에서 push, pop 등이 사용되는 이유</h4>
<hr>
<p>당연히 되지 않을 거라고 생각하고 코드를 작성한 후 실행을 했다. 근데 내 예상과는 전혀 다른 결과가 나왔다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/b7c5f00c-d94e-495a-aad9-c706c012516a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/2563abdf-6621-4d2f-a697-f37268319f14/image.png" alt=""></p>
<p>...? 왜 되지? 튜플은 고정된 크기를 가지고 있고 마찬가지로 타입의 순서 또한 정해져 있는데 그렇다면 push나 pop 등 요솟값에 직접적인 영향을 주는 메서드를 사용할 수 없어야 하는 것 아닌가?</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/7c4d76f4-6a64-4ffa-843e-f1daa20aa426/image.png" alt=""></p>
<p>실제 바나나를 기존 튜플에 추가해보면 위와 같은 에러가 뜬다.</p>
<p>왜 이러는 거지...? 예상과 다른 결과에 나는 당황해서 열심히 구글링을 하기 시작했다. 그리고 아니나 다를까, 나와 비슷한 생각을 한 사람들이 이미 스택오버플로에 질문글을 많이 올려두셨다.</p>
<p><a href="https://stackoverflow.com/questions/64069552/typescript-array-push-method-cant-catch-a-tuple-type-of-the-array">[스택오버플로] Typescript array push method can&#39;t catch a tuple type of the array</a></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5ab7e6e7-d88c-4a05-b7e6-2672d8d29221/image.png" alt=""></p>
<p>나와 아주 똑같은 생각을 하신 분이다. 1번은 작동을 안 하는데 왜 2번은 작동이 되는가? 이런 비슷한 질문이 상당히 많이 올라와 있었다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/9942b5b6-c584-459b-84db-68e7ffbca96b/image.png" alt="">
<img src="https://velog.velcdn.com/images/gangk_99/post/f290535c-a8f1-4593-b5aa-3bfec006b3f9/image.png" alt=""></p>
<p>이게 답변인데... 그냥 요약하자면 이미 논의가 된 문제이지만 마이크로소프트에서 거부함 이거다. 기존 코드에 변화가 너무 크게 생길 것 같아서라고 한다.</p>
<p><a href="https://github.com/microsoft/TypeScript/issues/6325">[Github Issue] Remove destructive methods in tuple type</a></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5c894fbc-713c-406b-94ca-f58fe7bd4557/image.png" alt=""></p>
<p>실제로 깃허브 이슈에 이런 제안을 한 사람이 있다. push, pop, shift 같은 건 제한해야 하는 것 아닌가요? 답변을 봐도 그냥 다른 방법을 제안할 뿐 튜플은 그냥 배열이다 우리 설계는 그렇기 때문에 바꾸지 않을 거다 뭐 이런 내용이다.</p>
<p>이에 따른 제안이 깃허브 이슈에 오픈되어 있는데</p>
<p><a href="https://github.com/microsoft/TypeScript/issues/48465">[Github Issue] New type for tuples which have mutable properties but can&#39;t be mutated with array methods</a></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/8d268243-9660-4448-ba65-a8e1e3ee2a29/image.png" alt=""></p>
<pre><code class="language-javascript">type StrictTuple&lt;T extends any[]&gt; =
    Omit&lt;T, keyof any[]&gt; extends infer O ? { [K in keyof O]: O[K] } : never;</code></pre>
<p>보다 엄격한 관리를 위해 특정 조건을 만족하는 튜플 타입을 제안하고 있다. 실제 코드를 작성해서 확인해보았다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5a73ef18-d60a-416a-a00f-a9bd3d4ee8de/image.png" alt=""></p>
<p>드디어 원하는 결과가 반환되고 있긴 하다. 그렇긴 한데... 보니까 실제 배열로 작동하는 것이 아니라 key-value 쌍의 객체 타입을 만드니까 push를 사용하지 못하는 것 같다. 그래서 스택오버플로에 답변을 다신 분도 근데 오히려 이렇게 하는 게 더 번거로울 수도 있으니 그냥 튜플 변형하지 말고 사용하세요~ 라고 하고 계신다. 보니까 이건 최근까지도 논의되고 있는 부분인 것 같아 뭐랄까 속시원히 결론이 나지 않았다.</p>
<h3 id="🍀-결론">🍀 결론</h3>
<hr>
<p>우선 Array와 Tuple의 차이점에 대해 마지막으로 정리해보자.</p>
<ol>
<li>Array
: 배열은 동적인 크기를 가진다. 모든 요소가 동일한 타입이어야 한다. (유니온 타입 같은 경우는 다른 타입이 올 수도 있음) 요소의 추가외 제거 등이 자유롭다.</li>
<li>Tuple
: 튜플은 고정된 크기를 가진다. 각 요소가 다른 타입일 수도 있으며 타입의 순서가 정해져있다. 고정된 크기를 가지고 있으니 요소의 추가, 제거가 자유롭지 않지만... 딱히 제한을 하고 있지는 않다. (계속 논의 중인 사항인 듯)</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] 유니온 타입]]></title>
            <link>https://velog.io/@gangk_99/Typescript-%EC%9C%A0%EB%8B%88%EC%98%A8-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@gangk_99/Typescript-%EC%9C%A0%EB%8B%88%EC%98%A8-%ED%83%80%EC%9E%85</guid>
            <pubDate>Wed, 07 Feb 2024 17:03:13 GMT</pubDate>
            <description><![CDATA[<h3 id="✨-유니온-타입-union-type">✨ 유니온 타입 (Union Type)</h3>
<hr>
<p>우리는 타입스크립트를 사용할 때 변수 등에 타입을 지정한다. 지금까지는 기본적으로 하나의 타입만 지정해서 사용했지만, 만약 여러 타입이 와야하는 경우라면 어떨까? 예를 들어 우리가 숫자를 입력 받을 때 문자열로 받아올 때가 있다.
이럴 때 우리는 여러 타입 중 하나가 되는 값을 나타내기 위해 <code>유니온 타입</code>을 사용하게 된다. 사용 방법은 아주 간단하다. 기존 리터럴 타입에서 사용했던 것처럼 or 연산자 <code>|</code>로 지정할 타입을 입력하면 된다. 아래 예시를 한 번 보자.</p>
<pre><code class="language-javascript">let numStr: number | string;
numStr = 10;
numStr = &#39;10&#39;;
numStr = true; // Error</code></pre>
<p>number 변수에는 <code>number</code> 타입과 <code>string</code> 타입이 모두 올 수 있다. 대신 두 타입이 아닌 다른 타입이 오게 된다면 에러가 발생할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/e812da9f-7829-4cf0-83f2-f4a072568b2b/image.png" alt=""></p>
<p>실제 vscode 상에서도 위처럼 위 타입에 할당될 수 없다는 에러를 나타내고 있다. 하지만 여기서 문제점... 만약 number과 string 타입을 모두 받을 수 있는 num1과 num2 변수가 각각 존재할 때, 이 두 변수를 더하면 과연 어떻게 될까?
타입을 지정하지 않는 자바스크립트라면 알아서 덧셈을 진행한다. (결과는 원하는대로 나오지 않을 수 있음) 그렇다면 타입이 지정된 상태라면? 그것도 두 타입을 모두 받아올 수 있는 유니온 타입이라면?</p>
<p>결론은 에러가 발생한다.</p>
<h3 id="✨-타입-가드-type-guard">✨ 타입 가드 (Type Guard)</h3>
<hr>
<p>이럴 때 사용하는 것이 바로 타입 가드이다. 가드라는 단어는 모두가 알겠지만 무언가를 지키고, 방어하는 느낌의 단어이다. 눈치가 빠른 사람이라면 이미 눈치를 채셨을 수도 있다. 맞다. 유니온 타입의 값을 처리할 때 어떤 타입인지 검사를 하고 해당 조건에 맞는 경우에만 그 타입을 선택하는 것이다.</p>
<p>이렇게 글로 적으니 뭔 말인지 도통 이해가 안 갈 수도 있다. 하지만 예시를 보면 쉽게 이해가 될 것이다.</p>
<pre><code class="language-javascript">function addNum(num1: number | string, num2: number | string): number | string {
   return a + b;
}</code></pre>
<p>심플한 코드다. 두 변수가 함수의 매개변수로 존재하는데 number 또는 string을 타입으로 받아올 수 있다. 그리고 받아온 두 매개변수를 합치는 코드이다.
위에서도 설명했듯, 이 코드는 에러가 발생한다. 내가 숫자형을 입력할지, 문자열을 입력할지 알 수 없기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/d50f9d2a-e39d-4fdd-81e2-eb343ad59315/image.png" alt=""></p>
<p>실제로 코드를 작성해도 이렇게 에러가 나타나는 걸 볼 수 있다.
자, 이제 if와 typeof를 사용하여 <code>타입 가드</code>를 한 번 적용해보자.</p>
<pre><code class="language-javascript">function addNum(num1: number | string, num2: number | string): number | string {
    if (typeof num1 === &#39;number&#39; &amp;&amp; typeof num2 === &#39;number&#39;) {
        return num1 + num2;
    } else {
        return `${num1} ${num2}`;
    }
}</code></pre>
<p>조건문을 활용하여 변수들이 각각 숫자형으로 들어왔을 때에는 더하기 연산을 사용해주고 문자열이 들어왔을 경우에는 단순히 두 값을 이어 붙여서 나타내도록 하였다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/240d27d6-867d-4d26-b922-50fc0473eca3/image.png" alt=""></p>
<p>사진을 보면 알 수 있듯 에러가 발생하지 않는다. 과연 실행을 했을 때 우리가 예상한 값이 나타날까?</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/ac6beb2d-6e17-49cc-bd93-94474ce7e418/image.png" alt=""></p>
<p>각각 숫자로 입력한 경우는 정상적으로 두 숫자가 더해진 값이 반환되었고 문자열 + 문자열, 문자열 + 숫자 같은 경우는 단순히 값을 이어 붙여진 채 반환되는 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] 리터럴 타입]]></title>
            <link>https://velog.io/@gangk_99/Typescript-%EB%A6%AC%ED%84%B0%EB%9F%B4-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@gangk_99/Typescript-%EB%A6%AC%ED%84%B0%EB%9F%B4-%ED%83%80%EC%9E%85</guid>
            <pubDate>Wed, 07 Feb 2024 14:36:00 GMT</pubDate>
            <description><![CDATA[<h3 id="✨-리터럴-타입-literal-type">✨ 리터럴 타입 (Literal Type)</h3>
<hr>
<p><code>literal</code>의 뜻은 변하지 않는 데이터를 의미하는데 즉, 단어의 뜻을 보면 알 수 있듯 리터럴 타입은 특정한 값을 나타내는 타입이다. 해당하는 값이 특정한 리터럴 타입을 가져야하고, 값이 다르다면 에러가 발생한다. 특정 값의 사용을 강제하고자 할 때 사용된다고 한다. 또한 허용되는 타입이 정해져 있기 때문에 코드의 가독성도 높아진다.
리터럴 타입은 문자열, 숫자, 불리언 값 등에 사용이 된다. 이제 예시와 함께 알아보자.</p>
<h4 id="💫-문자열-리터럴-타입">💫 문자열 리터럴 타입</h4>
<hr>
<pre><code class="language-javascript">let gender: &quot;male&quot; | &quot;female&quot;;

gender = &quot;male&quot;;
gender = &quot;female&quot;;</code></pre>
<p>위 코드를 보면 gender라는 변수는 &quot;male&quot;, &quot;female&quot; 두 가지 문자열만 허용을 하는 리터럴 타입을 가진다. 만약 여기서 <code>gender = &quot;other&quot;;</code> 이라는 코드를 추가하면 어떻게 될까?</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/854a68dc-b81b-4b7a-81ba-ad51ee7bd96c/image.png" alt=""></p>
<p>위 캡처를 보면 알 수 있듯 우리는 정해진 리터럴 타입과 맞지 않는 값을 할당한다면 에러가 발생한다.</p>
<h4 id="💫-숫자형-리터럴-타입">💫 숫자형 리터럴 타입</h4>
<hr>
<p>숫자형도 문자열과 동일한 흐름이다. 단지 타입이 숫자로 바뀌었을 뿐이다. 몇 가지 숫자를 할당하고 해당 값과 다른 값이 할당된다면 에러를 반환한다.</p>
<pre><code class="language-javascript">let speed: 50 | 100 | 150 | 200;

speed = 50;
speed = 100;
speed = 250; // Error</code></pre>
<h4 id="💫-불리언-리터럴-타입">💫 불리언 리터럴 타입</h4>
<hr>
<p>불리언 타입 또한 타입이 불리언으로 바뀐 게 전부다. 만약 &#39;true&#39;를 할당한다면 반드시 해당하는 리터럴 타입을 가지는 변수에 &#39;true&#39;를 할당해야만 한다.</p>
<pre><code class="language-javascript">let isTrue: true;

isTrue = true;
isTrue = false; // Error</code></pre>
<h4 id="💫-객체-리터럴-타입">💫 객체 리터럴 타입</h4>
<hr>
<p>타입스크립트는 객체에도 리터럴 타입을 사용할 수 있다.</p>
<pre><code class="language-javascript">let person: { name: &quot;John&quot;, age: 25 };

person = { name: &quot;John&quot;, age: 25 };
person = { name: &quot;Alice&quot;, age: 30 }; // Error: 다른 값 할당
person = { name: &quot;John&quot; }; // Error: age 속성이 없음</code></pre>
<p>객체에 다른 값이 할당되거나 모든 속성이 없다면 에러가 발생한다.</p>
<h4 id="💫-타입-별칭">💫 타입 별칭</h4>
<hr>
<p>타입스크립트에서 타입에 별칭을 지정할 수 있는 기능이다. </p>
<pre><code class="language-javascript">type CardinalDirection = &#39;North&#39; | &#39;East&#39; | &#39;South&#39; | &#39;West&#39;;
let direction: CardinalDirection;

direction = &#39;North&#39;;
direction = &#39;Northeast&#39;; // Error</code></pre>
<p>위 코드를 보면 동서남북 중에 하나의 타입을 가지도록 지정을 하고 원하는 변수에 리터럴 타입으로 설정을 해주면 된다. 그리고 리터럴 타입으로 설정한 변수에 해당되지 않는 값이 할당된다면 에러가 발생한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 웹 풀 사이클 데브코스 1월 회고]]></title>
            <link>https://velog.io/@gangk_99/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9B%B9-%ED%92%80-%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-1%EC%9B%94-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@gangk_99/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9B%B9-%ED%92%80-%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-1%EC%9B%94-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 04 Feb 2024 09:42:44 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">국비지원교육 현실, 국비지원 교육 추천, 코딩 부트캠프 현실, 코딩 부트캠프 추천, 코딩 부트캠프 비교</span></p>
<h2 id="✨-1월을-회고하며">✨ 1월을 회고하며</h2>
<hr>
<p>2024년이 되고 첫번째 달이 지나갔다. 한 달 동안 백엔드 프로젝트를 마치고 이제 프론트엔드 공부를 시작했다. 벌써 데브코스를 수강한지도 세 달 째라니... 사실 아직 실감이 잘 안 난다.
1월은 이래저래 복잡한 달이었다. 프로젝트를 열심히 만들기는 했지만 프로젝트에만 집중한 나머지 다른 부분을 잘 공부하지는 못했다. 내가 비염이 심한 편인데 1월 중순부터 지독한 비염에 시달리고 있어서 컨디션 관리가 잘 되지 않아서 더 아쉬운 면이 많다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/a435dff5-ac5d-4fc2-a8da-08f16212d517/image.jpg" alt=""></p>
<p>최근에 집안일로 스트레스가 많아서 그런지 잠도 잘 못 자고 있어서 더 컨디션이 안 좋은 상태다. 공부에도 집중을 잘 못 하고 있고... 하루에 여섯시간도 못 자고 있는데 와중에 계속 깨는 중😂</p>
<p>아무튼 개인사는 이쯤 얘기하고... 한 달 동안 공부한 것에 대해 이야기를 해보자.</p>
<h3 id="💫-한-달-동안-한-것들">💫 한 달 동안 한 것들</h3>
<hr>
<ol>
<li><p>수업 듣기
: 역시나 수업을 열심히 들었다. 근데 열심히 들었다고 하긴 좀 애매한게 일단 수업은 가볍게 보고 프로젝트는 내 마음대로 했다. 프로젝트 기간 동안 챗지피티와 스택오버플로우랑 참 많이 친해진 듯...😅</p>
</li>
<li><p>멘토링
: 우리 조는 주에 한 번 멘토링을 진행 중이다. 이번에는 프로젝트를 계속해서 진행해서 멘토님께서 코드 리뷰를 해주셨다. 정말 많이 도움이 됐다. 괜찮은 패키지도 많이 추천을 받았고 코드를 깔끔하게 만드는 방법에 대해서도 많이 배웠다. 이번 달의 가장 큰 수확이라고 해도 과언이 아니다!</p>
</li>
<li><p>프로젝트 마무리
: 한 달 조금 넘게 진행이 된 백엔드 프로젝트가 마무리되었다. 수업이 마무리 된 거지 개인적인 욕심으로는 좀 더 프로젝트를 고도화시키고 싶어서 이거저거 추가해보고 있다. 예를 들어 <code>nodemailer</code>를 활용하여 실제로 메일을 보낼 수 있게 하는 기능이라든가 그런 것들! 트랜잭션을 적용할 생각도 있고... 일단 지금은 유저 기능을 조금 더 추가해보고 있다. 유저 수정이나 유저 탈퇴 기능 같은 것들을 먼저 추가해볼까 한다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/71f91b06-6d4d-43b5-a38d-4dca8673e816/image.png" alt=""></p>
<p>조금 허접한 나의 잔디밭... ㅎㅎ... 웬만해서는 1일 1커밋을 하고 싶었는데 마음처럼 쉽지는 않더라. 지금도 프론트엔드 공부 시작하고부터 또 시들해지는 바람에 다시 정신차리고 커밋하려고 노력 중이다.</p>
<ol start="4">
<li>코딩테스트 스터디
: 여전히 코딩테스트 스터디는 계속해서 진행중이다! 자바스크립트로 계속 진행중인데 사실 조금 고민중이다. 백엔드로 진로를 잡은 만큼 파이썬으로 코테를 보는 게 맞을 것 같다. 애초에 원래 이쪽으로 준비했어서 이게 더 편하기도 하고... 일단 스터디는 스터디대로 하고 파이썬으로 코테를 따로 공부할 예정이다.</li>
</ol>
<h3 id="❤️-잘한-점">❤️ 잘한 점</h3>
<hr>
<ol>
<li>프로젝트를 완성했다.
: 과제니까 당연한 말일 수도 있는데 개인적으로 내가 기능을 추가하고 따로 공부하는 과정에서 많은 걸 배웠다. 욕심이 나서 주어진 기능이 아니라 다른 것들도 추가해보고 있는데 이건 좋은 방향이라고 생각한다. 단순히 과제 제출에만 만족하는 것이 아니라 불특정 다수에게 보일만한 하나의 프로젝트를 완성하고 싶다.</li>
</ol>
<h3 id="💧-아쉬운-점">💧 아쉬운 점</h3>
<hr>
<ol>
<li><p>컨디션 관리 실패
: 스트레스는 만병의 근원이라는 말이 있듯 스트레스를 한 번 받기 시작하니 집중력도 떨어지고 의욕도 떨어졌다. 당연히 이런저런 감기나 이런 잔병치레도 하고 있고... 스트레스를 안 받는 게 사실 힘든 일이긴 한데 이게 일상 생활까지 영향을 주니까 방법을 조금 생각해보긴 해야할 것 같다.</p>
</li>
<li><p>해이해진 마음가짐
: 첫 시작과는 다르게 해이해졌다. 강의도 설렁설렁 듣고 있고 오전 기상도 늦어지고 있다... 초심을 유지하는 것이 가장 어렵다는 말이 있기는 하지만 나는 이 수업에서 많은 걸 배워야 하는 입장인데 이러면 곤란하다!!!! 다시 정신 다잡고 열심히 노력해보자.</p>
</li>
</ol>
<h3 id="🍀-앞으로-공부할-것">🍀 앞으로 공부할 것</h3>
<hr>
<ol>
<li><p>신청했던 도서 끝내기
: 또!! 또 도서를 완독하지 못했다!!! 2월에는 반드시 완독할 거다...</p>
</li>
<li><p>프로젝트 고도화하기
: 기존에 했던 도서 정보 백엔드 프로젝트를 고도화하고 readme.md도 정리를 해두려고 한다. 스웨거도 기왕이면 해두고... 배포까지 할 수 있다면 좋을텐데 그건 비용도 비용이고 일단 2월 안에 거기까지 진도가 나갈지는 모르겠다. 일단 지금은 프론트엔드 쪽을 공부하고 있으니까...</p>
</li>
<li><p>타입스크립트 공부하기
: 이건 저번달에도 다짐했던 건데 이번 달에는 정말 해야 한다. 왜냐면 지금 리액트를 타입스크립트로 진행하고 있다...</p>
</li>
<li><p>블로그 관리하기
: 이제 TIL을 쓰는 것보단 내가 공부했던 것을 올리는 거에 집중을 해볼까 한다. CS지식이나 사용하는 패키지나 에러의 해결법을 포스트로 적어보려고 한다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C] 컴파일 과정에 대해 알아 보자]]></title>
            <link>https://velog.io/@gangk_99/C-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84-%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@gangk_99/C-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84-%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 31 Jan 2024 16:42:56 GMT</pubDate>
            <description><![CDATA[<h2 id="💡-컴파일">💡 컴파일?</h2>
<hr>
<p>지금 내가 듣고 있는 자바스크립트와 타입스크립트이지만 타입스크립트 강의를 시작하면서 강사님이 C언어에 대해 한 주동안 설명해주셨다. 기왕 C언어를 공부하는 김에 C언어의 컴파일 과정에 대해서 공부해보면 좋을 것 같아 정리를 해보았다. 사실 컴파일을 하는 과정에서 상당히 많은 에러가 발생하고 이 컴파일 과정을 알고 있다면 에러를 이해하는 데에도 도움이 된다.
뭐 물론 자바스크립트만 판다면 사실 필요하지 않을 수도 있지만... 그래도 사람 일은 모르기 때문에... 지식을 쌓아둬서 나쁠 것도 없고 말이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/e6b4ba71-031d-4c3f-bb56-13713eb0b043/image.png" alt=""></p>
<p>일단 컴파일이란 간단하게 말하자면 컴퓨터, 즉 기계가 읽기 쉽게 기계어로 변환해주는 과정이라고 보면 된다. 아래에서도 예시 사진을 보여주겠지만 기계어는 사람이 읽을 수가 없고 그렇기에 사람들은 고수준 언어, 사람에 가까운 언어로 먼저 코드를 작성한 후 기계어로 변환하게 된다.
그 중에서도 C언어는 저수준 언어에 속하기 때문에 개발자가 메모리에도 직접 접근하는 등 오류 발생도 빈번하기도 하고 코드 작성도 비교적 복잡하다. 대신 메모리를 직접 접근하는 만큼 효율적으로 사용할 수도 있고, 일단 실행 속도가 빠르다. 보통 이런 언어는 임베디드 시스템 쪽에서 자주 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/1cb8a93c-6f0c-4f20-8e9d-c305aa09657b/image.png" alt=""></p>
<p>우선 우리가 작성한 c 파일의 컴파일 과정을 간단하게 나타내면 위 사진과 같다. 전처리 과정, 컴파일 과정, 어셈블 과정, 링킹 과정으로 나누어진다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/00d425d4-954d-4c5f-9c91-1e5df8ae7eb7/image.png" alt=""></p>
<p>부연 설명으로 적혀있듯이, 우선 전처리 과정에서는 <code>헤더 파일 삽입</code>, <code>매크로 치환</code> 과정이 이루어진다. 그 후 컴파일 과정에서는 코드를 어셈블리어로 변환해준다. 그리고 어셈블 과정에서 비로소 기계어로 변환이 된다. 이제 소스 파일과 라이브러리를 결합해서 최종적으로 실행 파일을 생성하게 된다.
이제 각 과정을 예시와 함께 공부해보자.</p>
<h3 id="💫-전처리-과정">💫 전처리 과정</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/dd1278ee-204b-43ce-b7d7-e8633d7c6853/image.png" alt=""></p>
<p>우선 컴파일 과정을 확인하기 위해 위와 같은 간단한 덧셈 코드를 만들었다. 매크로가 치환되는 전처리 과정을 보이기 위해 A와 B를 매크로로 정의했다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/75b4835a-5023-475d-97b9-5addbc464d15/image.png" alt=""></p>
<p><code>gcc</code>를 통해서 어떻게 컴파일이 되는지 알아보자. 중간 과정을 모두 건너 뛰고 컴파일을 하기 위해서는 아래와 같이 작성하면 된다.</p>
<pre><code class="language-c">gcc calculator.c -o calculator</code></pre>
<p><code>program.exe</code> 파일이 생성될 텐데 <code>./program</code> 명령어를 이용하여 실행하면 된다.</p>
<p>다만 나는 컴파일 과정을 이해하기 위해 각 단계까지만 수행하였다. 먼저 전처리 과정만 수행하기 위해서 아래와 같이 입력해주자.</p>
<pre><code class="language-c">gcc -E calculator.c -o calculator.i</code></pre>
<p>생성된 <code>calculator.i</code> 파일을 한 번 열어 보자.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/56143877-5fd6-40ca-a371-57fb80332f18/image.png" alt=""></p>
<p>뭐가 엄청나게 많다. <code>#include</code>로 포함한 헤더 파일을 받아왔다고 보면 된다. 중간에 보면 우리가 포함한 헤더 파일인 <code>stdio.h</code>가 보일 거다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/9c001889-aab1-443a-b63b-225ed25732bc/image.png" alt=""></p>
<p>밑으로 쭉 내려보면 우리가 입력했던 <code>#define</code> 매크로는 사라지고 매크로로 정의한 숫자들이 a와 b에 잘 들어가 있는 걸 확인할 수 있다!</p>
<p>즉! 전처리 과정에서는 <code>#include</code>와 <code>#define</code>과 같은 전처리 지시자를 해석한다. 주로 <code>stdio.h</code>나 <code>stdlib.h</code> 같은 헤더 파일을 받아오고 매크로를 치환하는 작업이 이루어진다고 보면 될 것 같다.</p>
<h3 id="💫-컴파일-과정">💫 컴파일 과정</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/20ce70a9-113d-4a09-80a4-9b687a10f246/image.png" alt=""></p>
<p>컴파일 과정까지만 수행하기 위해서는 아래와 같이 입력해주면 된다.</p>
<pre><code class="language-c">gcc -S calculator.i -o calculator.s</code></pre>
<p>ls 명령어로 확인해보면 <code>calculator.s</code> 파일이 잘 생성된 걸 볼 수 있다. 이제 이 파일을 한 번 열어보자.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5e99bdea-f551-403f-a35a-89bd0d88f112/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/67fa3fad-6ca5-4eee-8bd0-a9af1ba72223/image.png" alt=""></p>
<p>짠.
좀 더 이상해졌다. 굳이 하나하나 뜯어볼 필요는 없고 그냥 어셈블리어로 변환이 됐다... 이 정도만 알면 될 것 같다. 이 컴파일 과정에서 전단부, 중단부, 후단부로 나뉘는데 이거까지 설명한다면 부가적으로 설명할 게 너무 많아서 이건 차후에 따로 다뤄보는 걸로... (사실 아직 나도 불특정다수에게 설명할 만큼 잘 알고 있는 게 아님)</p>
<h3 id="💫-어셈블-과정">💫 어셈블 과정</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/ee05b4d9-41c8-4bfb-ac59-a0ca93ec33d5/image.png" alt=""></p>
<p>이제 최종적으로 컴퓨터가 읽을 수 있게 기계어로 변환하는 과정이다.</p>
<pre><code class="language-c">gcc -c calculator.s -o calculator.o</code></pre>
<p>어셈블 과정만 진행하기 위해서는 위처럼 입력해주면 된다. 정상적으로 object 파일인 <code>calculator.o</code> 파일이 생성된 걸 볼 수 있다. 이제 저 파일을 열어보면...</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/55477795-e917-4ec3-beea-fb4c8f47c694/image.png" alt=""></p>
<p>그냥 닫자.</p>
<p>아무튼 우리가 읽을 수 없는 그런 컴퓨터만 읽을 수 있는 기계어로 최종적으로 변환이 된 걸 확인할 수 있다.</p>
<h3 id="💫-링킹-과정">💫 링킹 과정</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/58095f21-5986-4c94-9b0b-0d11d4120387/image.png" alt=""></p>
<p>자 이제 최종 링킹 과정이다. 생성된 목적 파일 (*.o) 파일과 표준 C 라이브러리, 사용자 라이브러리를 링크해주고 비로소 우리가 실행할 수 있는 실행 가능한 파일이 만들어지게 된다.</p>
<pre><code class="language-c">gcc calculator.o -o calculator.exe</code></pre>
<p>이 링킹 과정을 진행하기 위해서 위와 같이 입력해주자. .exe는 붙여도 되고 안 붙여도 된다. 정상적으로 실행 파일까지 생성이 되었다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/abeea383-b40a-4bd8-b533-2eeb0f0a40b7/image.png" alt=""></p>
<p><code>./calculator.exe</code>로 해당 파일을 실행을 해주면 아주 정상적으로 컴파일 후 실행이 되는 걸 확인할 수 있다.</p>
<p>보통은 <code>gcc 소스 파일 -o 실행 파일명</code> 을 입력해서 실행 파일을 바로 만들지만 실제로는 위와 같은 과정들이 이루어졌다는 걸 알 수 있다.</p>
<p>이렇게 C언어에서 컴파일 과정이 어떻게 일어나는지 알아보았다. 이전에 교수님께서 컴파일 과정을 알고 있는 개발자와 모르는 개발자의 차이는 크다고 강조하셨다. 솔직하게 말해서 큰 체감을 느끼지는 못했는데 그래도 프로그램을 이해하는 데에는 도움이 되지 않았나... 싶다. 마침 컴파일러 수업을 듣고 그 다음 들었던 수업이 임베디드 수업이어서 좀 더 시너지가 났던 것 같기도 하고🤔</p>
<p>참고 자료 출처
<a href="https://sublivan.tistory.com/6">링크</a>
<a href="https://gracefulprograming.tistory.com/16">링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 9주차 DAY 5]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-5</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-5</guid>
            <pubDate>Mon, 22 Jan 2024 18:05:11 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-9주차-day-5">🚩 프로그래머스 데브코스 웹 풀사이클 과정 9주차 DAY 5</h2>
<hr>
<p>주문하기 관련 기능을 구현했다. 관련 데이터베이스 테이블도 세 개나 연관 관계인데다 post로 보내는 방식도 이래저래 꼬여 있어서 SQL문을 짜는 걸 고민을 많이 했다. 오류도 얼마나 많이 났던지... 그래도 SQL문을 어떻게 만들어야 하는지 고민하는 과정에서 실력이 좀 괜찮아진 것 같다. 마침 과거의 내가 SQL 시험을 신청하는 바람에 공부를 해야했는데...ㅋ</p>
<h3 id="💡express로-주문하기-구현하기">💡Express로 주문하기 구현하기</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/00047655-4d65-4a7a-9387-86a161e64fbc/image.png" alt=""></p>
<p>먼저 주문하기 기능과 직접적으로 관련이 있는 테이블은 <code>delivery</code>, <code>orderedBook</code>, <code>orders</code>다. 테이블명에서부터 알 수 있지만 <code>orderedBook</code>은 주문한 책을 저장하는 테이블이고 <code>delivery</code>는 배송지 관련 데이터를 저장하는 테이블, <code>orders</code>는 기타 주문 관련 내용을 저장하는 테이블이다.
즉 주문을 하면 세 테이블에 모두 저장이 되도록 해야한다.</p>
<pre><code class="language-javascript">const order = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { items, delivery, totalQuantity, totalPrice, userId, firstBookTitle } = camelcaseKeys(req.body);
    const sqlInsertDelivery = `insert into delivery (address, receiver, contact) values (?, ?, ?)`;
    const valuesDelivery = [delivery.address, delivery.receiver, delivery.contact];
    const sqlInsertOrder = `insert into orders (book_title, total_quantity, total_price, user_id, delivery_id) values (?, ?, ?, ?, ?)`;
    const sqlInsertOrderedBook = `insert into orderedBook (order_id, book_id, quantity) values (?, ?, ?)`;

    try {
        const [rowsDelivery] = await connection.query(sqlInsertDelivery, valuesDelivery);
        const deliveryId = rowsDelivery.insertId;

        const valuesOrder = [firstBookTitle, totalQuantity, totalPrice, userId, deliveryId];
        const [rowsOrder] = await connection.query(sqlInsertOrder, valuesOrder);
        const orderId = rowsOrder.insertId;

        for (const item of items) {
            const valuesOrderItem = [orderId, item.book_id, item.quantity];
            await connection.query(sqlInsertOrderedBook, valuesOrderItem);
        }

        return res.status(StatusCodes.OK).json({
            message: &#39;주문이 성공적으로 처리되었습니다.&#39;,
        });
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;주문 중 에러가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/67c7dbff-77cf-4964-8c7e-acc0283114fe/image.png" alt=""></p>
<p>POST 요청 시 body 값은 위 사진과 같다.</p>
<ol>
<li>먼저 <code>delivery</code> 테이블에 배송지 관련 내용을 저장한다.</li>
<li>배송지 저장 후 <code>order</code> 테이블에 주문 관련 데이터를 저장한다.</li>
<li>이후 item 객체 내 데이터들을 for문으로 돌려가면서 하나씩 저장해준다.</li>
</ol>
<p>일단은 이렇게 구현을 하긴 했는데... 위 코드에는 문제가 좀 있다.
우선 delivery, order, orderedBook 테이블 각각 insert를 하기 때문에 예를 들어 order 시 문제가 생기면 delivery 테이블에만 데이터가 들어가고 이후 테이블에는 데이터가 입력이 안 된다. 이와 비슷한 문제로 현재 items 내의 주문 도서들이 for문으로 들어가고 있는데 중간에 에러가 발생한다면 도서가 몇 개만 저장이 되고 그대로 종료되게 된다. 이 경우에는 만약 에러가 발생했을 때 아예 처음으로 돌아가 데이터가 입력이 되지 않도록 하는 게 좋을 것 같다고 멘토님이 조언해주셨다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/cc5d09b7-997d-4b5a-a455-5cd8a6a4b96e/image.png" alt=""></p>
<h3 id="💡-express로-주문-내역-전체-조회-구현하기">💡 Express로 주문 내역 전체 조회 구현하기</h3>
<hr>
<pre><code class="language-javascript">const getOrders = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { userId } = camelcaseKeys(req.body);
    const sql = `select * from orders where user_id=?`;
    const values = [userId];

    try {
        const [rows] = await connection.query(sql, values);

        if (rows.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;주문 내역이 없습니다.&#39;,
            });
        }

        return res.status(StatusCodes.OK).json(rows);
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;주문 내역 조회 중 에러가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>유저의 주문 내역을 조회하는 코드다. 그냥 데이터베이스에서 select만 제대로 해주면 된다. 근데... 이거 쓰면서 생각한 건데 전체 조회 시 주문 내역이 없는게 에러가 될 수 없는데 난 왜 에러로 만든 거냐?😕 그냥 빈 배열만 반환하면 되잖아...</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/c13292df-062f-420e-bad3-c4a8ba06abab/image.png" alt=""></p>
<h3 id="💡-express로-주문-내역-상세-조회-구현하기">💡 Express로 주문 내역 상세 조회 구현하기</h3>
<hr>
<pre><code class="language-javascript">const getOrderDetail = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { userId } = camelcaseKeys(req.body);
    const orderId = req.params.id;
    const sql = `select * from orders where id=? and user_id=?`;
    const values = [orderId, userId];

    try {
        const [rows] = await connection.query(sql, values);

        if (rows.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;주문 내역이 없습니다.&#39;,
            });
        }

        return res.status(StatusCodes.OK).json(rows);
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;주문 내역 조회 중 에러가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>이것도 위 코드와 비슷하다. 그냥 하나의 주문 내역만 자세히 보여주면 끝이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/fa9c0fbc-f8de-410d-8a2c-3e057ecabf61/image.png" alt=""></p>
<h3 id="💡express로-주문-내역-삭제-구현하기">💡Express로 주문 내역 삭제 구현하기</h3>
<hr>
<pre><code class="language-javascript">const deleteOrder = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const orderId = req.params.id;
    const sqlSelectOrder = `select * from orders where id=?`;
    const sqlDeleteOrderedBook = `delete from orderedBook where order_id=?`;
    const sqlDeleteOrder = `delete from orders where id=?`;
    const sqlDeleteDelivery = `delete from delivery where id=?`;

    try {
        const [rowsOrder] = await connection.query(sqlSelectOrder, orderId);
        if (rowsOrder.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;주문 내역이 존재하지 않습니다.&#39;,
            });
        }

        await connection.query(sqlDeleteOrderedBook, orderId);

        const deliveryId = rowsOrder[0].delivery_id;

        await connection.query(sqlDeleteOrder, orderId);
        await connection.query(sqlDeleteDelivery, deliveryId);

        return res.status(StatusCodes.OK).json({
            message: &#39;주문이 성공적으로 삭제되었습니다.&#39;,
        });
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;주문 삭제 중 에러가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>먼저 주문 내역이 존재하는지 확인하고 차례대로 관련 데이터베이스를 모두 삭제해준다. 근데 이것도 중간에 에러가 발생하면 하나 혹은 두 개 테이블에서만 데이터가 삭제될 수 있기 때문에 좀 더 방법을 고민해봐야 할 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/4fbb0e02-8cbb-46a7-aebf-50686bd49bfd/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 9주차 DAY 4]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-4</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-4</guid>
            <pubDate>Mon, 22 Jan 2024 14:58:56 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-9주차-day-4">🚩 프로그래머스 데브코스 웹 풀사이클 과정 9주차 DAY 4</h2>
<hr>
<p>쉬엄쉬엄 쉬어갔던 날... 패키지를 조금 수정하고 swagger 문서를 작성한 게 전부다. 머쓱... swagger 문서를 작성할 때는 Chat GPT의 도움을 조금 받았다. 기존 작성한 코드를 보여주면서 틀을 짜달라고 했더니 짠 하고 짜주더라. 물론 내가 자잘한 부분은 수정을 했다.</p>
<h3 id="💡-swagger-문서-작성하기">💡 Swagger 문서 작성하기</h3>
<hr>
<ol>
<li><code>전체 도서 조회</code></li>
</ol>
<pre><code>/**
 * @swagger
 * /books:
 *   get:
 *     summary: 전체 도서 조회
 *     description: 전체 도서를 조회합니다.
 *     tags: [Books]
 *     parameters:
 *       - in: query
 *         name: category_id
 *         description: 카테고리 아이디로 도서를 필터링합니다.
 *         schema:
 *           type: integer
 *       - in: query
 *         name: news
 *         description: 신간 도서만 조회합니다.
 *         schema:
 *           type: boolean
 *       - in: query
 *         name: limit
 *         description: 한 페이지에 보여줄 도서의 개수를 지정합니다.
 *         schema:
 *           type: integer
 *       - in: query
 *         name: current_page
 *         description: 현재 페이지를 지정합니다.
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: 전체 도서 조회 성공
 *         content:
 *           application/json:
 *             example:
 *               - id: 1
 *                 title: Book 1
 *                 img: 1
 *                 category_id: 1
 *                 form: ebook
 *                 isbn: 123456789
 *                 summary: Book 1 Summary
 *                 detail: Book 1 Detail
 *                 author: Author 1
 *                 pages: 100
 *                 contents: Book 1 Contents
 *                 price: 10000
 *                 pub_date: 2021-01-01
 *                 likes: 10
 *               - id: 2
 *                 title: Book 2
 *                 img: 2
 *                 category_id: 2
 *                 form: ebook
 *                 isbn: 987654321
 *                 summary: Book 2 Summary
 *                 detail: Book 2 Detail
 *                 author: Author 2
 *                 pages: 120
 *                 contents: Book 2 Contents
 *                 price: 20000
 *                 pub_date: 2023-12-12
 *                 likes: 16
 *       404:
 *         description: 해당하는 도서가 존재하지 않을 때 반환합니다.
 *         content:
 *           application/json:
 *             example:
 *               message: 해당하는 도서가 존재하지 않습니다.
 *       500:
 *         description: 서버 에러
 *         content:
 *           application/json:
 *             example:
 *               message: 도서 조회 중 에러가 발생하였습니다.
 */</code></pre><p><img src="https://velog.velcdn.com/images/gangk_99/post/3e7cc222-4ac3-43b4-9644-b0af84b2cfcd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/1273e349-ba78-42f1-9fca-081908a2b9d8/image.png" alt=""></p>
<ol start="2">
<li>개별 도서 조회</li>
</ol>
<pre><code>/**
 * @swagger
 * /books/{id}:
 *   get:
 *     summary: 개별 도서 조회
 *     description: 개별 도서를 조회합니다.
 *     tags: [Books]
 *     parameters:
 *       - in: path
 *         name: id
 *         description: 도서 아이디로 조회합니다.
 *         required: true
 *         schema:
 *           type: integer
 *       - in: body
 *         name: user_id
 *         description: 좋아요 여부를 확인할 사용자 아이디입니다.
 *         required: true
 *         schema:
 *           type: object
 *           properties:
 *             user_id:
 *               type: integer
 *     responses:
 *       200:
 *         description: 개별 도서 조회 성공
 *         content:
 *           application/json:
 *             example:
 *               id: 1
 *               title: Book 1
 *               img: 1
 *               category_id: 1
 *               form: ebook
 *               isbn: 123456789
 *               summary: Book 1 Summary
 *               detail: Book 1 Detail
 *               author: Author 1
 *               pages: 100
 *               contents: Book 1 Contents
 *               price: 10000
 *               pub_date: 2021-01-01
 *               category_name: IT
 *               likes: 10
 *               liked: true
 *       404:
 *         description: 해당하는 도서가 존재하지 않을 때 반환합니다.
 *         content:
 *           application/json:
 *             example:
 *               message: 해당하는 도서가 존재하지 않습니다.
 *       500:
 *         description: 서버 에러
 *         content:
 *           application/json:
 *             example:
 *               message: 도서 조회 중 에러가 발생하였습니다.
 */</code></pre><p><img src="https://velog.velcdn.com/images/gangk_99/post/25498087-54d2-47cd-9b90-4519ea190e2b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/efeb5330-c6d7-452e-8eb4-de7222118f1a/image.png" alt=""></p>
<ol start="3">
<li>카테고리 조회</li>
</ol>
<pre><code>/**
 * @swagger
 * /category:
 *   get:
 *     summary: 카테고리 목록 조회
 *     description: 카테고리 목록을 조회합니다.
 *     tags: [Categories]
 *     responses:
 *       200:
 *         description: 카테고리 목록 조회에 성공했을 때 응답합니다.
 *         content:
 *           application/json:
 *             example:
 *               - category_id: 1
 *                 category_name: Fiction
 *               - category_id: 2
 *                 category_name: Mystery
 *       404:
 *         description: 카테고리가 존재하지 않을 때 응답합니다.
 *         content:
 *           application/json:
 *             example:
 *               message: 카테고리가 존재하지 않습니다.
 *       500:
 *         description: 서버 에러
 *         content:
 *           application/json:
 *             example:
 *               error: 카테고리 조회 중 에러가 발생하였습니다.
 */</code></pre><p><img src="https://velog.velcdn.com/images/gangk_99/post/b3f394b5-4006-4224-b5f6-612c0076bf02/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/e7308f61-fc07-4162-8ea6-00776553049e/image.png" alt=""></p>
<ol start="4">
<li>좋아요 추가 및 취소</li>
</ol>
<pre><code>/**
 * @swagger
 * /books/{id}:
 *   post:
 *     summary: 좋아요 추가
 *     description: 도서에 좋아요를 추가합니다.
 *     tags: [Likes]
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         description: 도서 ID
 *         schema:
 *           type: integer
 *       - in: body
 *         name: requestBody
 *         required: true
 *         description: 유저 ID
 *         schema:
 *           type: object
 *           properties:
 *             user_id:
 *               type: integer
 *     responses:
 *       200:
 *         description: 좋아요 추가에 성공했을 때
 *         content:
 *           application/json:
 *             example:
 *               message: 좋아요 성공
 *       400:
 *         description: 이미 좋아요한 도서에 좋아요를 추가하려고 할 때
 *         content:
 *           application/json:
 *             example:
 *               message: 이미 좋아요한 책입니다.
 *       404:
 *         description: 도서나 유저가 존재하지 않을 때
 *         content:
 *           application/json:
 *             example:
 *               message: 존재하지 않는 유저입니다.
 *       500:
 *         description: 서버 에러
 *         content:
 *           application/json:
 *             example:
 *               message: 서버 에러
 *   delete:
 *     summary: 좋아요 취소
 *     description: 도서에 좋아요를 취소합니다.
 *     tags: [Likes]
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         description: 도서 ID
 *         schema:
 *           type: integer
 *       - in: body
 *         name: requestBody
 *         required: true
 *         description: 유저 ID
 *         schema:
 *           type: object
 *           properties:
 *             user_id:
 *               type: integer
 *     responses:
 *       200:
 *         description: 좋아요 취소에 성공했을 때
 *         content:
 *           application/json:
 *             example:
 *               message: 좋아요 취소 성공
 *       400:
 *         description: 좋아요하지 않은 도서에 좋아요를 취소하려고 할 때
 *         content:
 *           application/json:
 *             example:
 *               message: 좋아요하지 않은 책입니다.
 *       404:
 *         description: 도서나 유저가 존재하지 않을 때
 *         content:
 *           application/json:
 *             example:
 *               message: 존재하지 않는 유저입니다.
 *       500:
 *         description: 서버 에러
 *         content:
 *           application/json:
 *             example:
 *               message: 서버 에러
 */</code></pre><p><img src="https://velog.velcdn.com/images/gangk_99/post/962cfbe8-1cbc-4cdd-be23-3958949a7709/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/2cfc1bae-53e7-4501-bd9e-9f1990b667e6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5d786fa5-6918-4af0-93a8-b63ea9fbdb13/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/98495b7c-62b8-47c4-aec0-bc5e6f81fdd4/image.png" alt=""></p>
<ol start="5">
<li>장바구니 담기 및 조회</li>
</ol>
<pre><code>/**
 * @swagger
 * /carts:
 *   post:
 *     summary: 장바구니에 도서 추가
 *     description: 장바구니에 도서를 추가합니다.
 *     tags: [Carts]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               book_id:
 *                 type: integer
 *               quantity:
 *                 type: integer
 *               user_id:
 *                 type: integer
 *     responses:
 *       201:
 *         description: 장바구니에 도서 추가 성공했을 때
 *         content:
 *           application/json:
 *             example:
 *               message: 장바구니에 도서가 추가되었습니다.
 *       200:
 *         description: 장바구니에 이미 도서가 담겨있을 때
 *         content:
 *           application/json:
 *             example:
 *               message: 이미 장바구니에 담긴 도서입니다. 원하시는 수량만큼 장바구니에 담긴 도서의 수량이 증가하였습니다.
 *       400:
 *         description: 존재하지 않는 유저일 때
 *         content:
 *           application/json:
 *             example:
 *               message: 존재하지 않는 유저입니다.
 *       404:
 *         description: 도서가 존재하지 않을 때
 *         content:
 *           application/json:
 *             example:
 *               message: 존재하지 않는 도서입니다.
 *       500:
 *         description: 서버 오류
 *         content:
 *           application/json:
 *             example:
 *               message: 장바구니 추가 중 문제가 발생하였습니다.
 *
 *   get:
 *     summary: 장바구니 조회
 *     description: 장바구니를 조회합니다.
 *     tags: [Carts]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               user_id:
 *                 type: integer
 *               selected:
 *                 type: array
 *                 items:
 *                   type: integer
 *     responses:
 *       200:
 *         description: 장바구니 조회 성공했을 때
 *         content:
 *           application/json:
 *             example:
 *               - id: 1
 *                 book_id: 123
 *                 title: &#39;Book Title&#39;
 *                 summary: &#39;Book Summary&#39;
 *                 quantity: 2
 *                 price: 20000
 *               - id: 2
 *                 book_id: 456
 *                 title: &#39;Another Book Title&#39;
 *                 summary: &#39;Another Book Summary&#39;
 *                 quantity: 1
 *                 price: 20000
 *       404:
 *         description: 장바구니에 담긴 도서가 없을 때
 *         content:
 *           application/json:
 *             example:
 *               message: &#39;장바구니에 담긴 도서가 없습니다.&#39;
 *       500:
 *         description: 서버 오류
 *         content:
 *           application/json:
 *             example:
 *               message: &#39;장바구니 조회 중 문제가 발생하였습니다.&#39;
 */</code></pre><p><img src="https://velog.velcdn.com/images/gangk_99/post/2d1497c0-fe13-4809-bb46-e3ea263352af/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/2110c73c-e498-4567-bdbf-3526fabaacf8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/e373262a-30b8-4eed-a3d4-2a76a6f910c4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f2b2d76b-0f95-4704-a6b0-6ce0e2b077b7/image.png" alt=""></p>
<ol start="6">
<li>장바구니 삭제</li>
</ol>
<pre><code>/**
 * @swagger
 * /carts/{id}:
 *   delete:
 *     summary: 장바구니 도서 삭제
 *     description: 장바구니 도서를 삭제합니다.
 *     tags: [Carts]
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         description: 장바구니 도서 id
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: 장바구니 도서 삭제 성공했을 때
 *         content:
 *           application/json:
 *             example:
 *               message: &#39;장바구니에서 도서가 삭제되었습니다.&#39;
 *       404:
 *         description: 존재하지 않는 장바구니 도서일 때
 *         content:
 *           application/json:
 *             example:
 *               message: &#39;존재하지 않는 장바구니 도서입니다.&#39;
 *       500:
 *         description: 서버 오류
 *         content:
 *           application/json:
 *             example:
 *               message: &#39;장바구니 도서 삭제 중 문제가 발생하였습니다.&#39;
 */</code></pre><p><img src="https://velog.velcdn.com/images/gangk_99/post/dfbc84be-8441-4df5-834c-df17f5dc4896/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/e1046877-1dc8-46e6-a613-4acc8a0901cd/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 9주차 DAY 3]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-3</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-3</guid>
            <pubDate>Tue, 16 Jan 2024 11:08:37 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-9주차-day-3">🚩 프로그래머스 데브코스 웹 풀사이클 과정 9주차 DAY 3</h2>
<hr>
<p>장바구니 기능을 구현했다. 마찬가지로 큰 어려움은 없긴 했는데 강의에서는 구현되지 않은 예외 처리를 생각하느라 시간이 조금 걸렸다.</p>
<h3 id="💡-express로-장바구니-담기-구현하기">💡 Express로 장바구니 담기 구현하기</h3>
<hr>
<ol>
<li><code>CartController.js</code></li>
</ol>
<pre><code class="language-javascript">const checkExist = `select (select count(*) from users where id = ?) as user_exists, (select count(*) from books where id = ?) as book_exists`;

const checkExistValues = async (connection, values) =&gt; {
    const [rows] = await connection.query(checkExist, values);
    return {
        user_exists: rows[0].user_exists === 1,
        book_exists: rows[0].book_exists === 1,
    };
};

const addToCart = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { bookId, quantity, userId } = camelcaseKeys(req.body);

    const sqlInsertCart = &#39;insert into cartItems (book_id, quantity, user_id) values (?, ?, ?)&#39;;
    const sqlSelectCart = &#39;select * from cartItems where user_id = ? and book_id = ?&#39;;
    const sqlUpdateCart = &#39;update cartItems set quantity = quantity + ? where user_id = ? and book_id = ?&#39;;
    const values = [bookId, quantity, userId];
    const existValues = [userId, bookId];

    try {
        const { user_exists, book_exists } = await checkExistValues(connection, existValues);

        if (!user_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 유저입니다.&#39;,
            });
        }

        if (!book_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 도서입니다.&#39;,
            });
        }

        const [rowsSelect] = await connection.query(sqlSelectCart, existValues);

        if (rowsSelect.length &gt; 0) {
            const [rowsUpdate] = await connection.query(sqlUpdateCart, [quantity, userId, bookId]);

            if (rowsUpdate.affectedRows &gt; 0) {
                return res.status(StatusCodes.OK).json({
                    message:
                        &#39;이미 장바구니에 담긴 도서입니다. 원하시는 수량만큼 장바구니에 담긴 도서의 수량이 증가하였습니다.&#39;,
                });
            } else {
                return res.status(StatusCodes.BAD_REQUEST).json({
                    message: &#39;장바구니 추가에 실패하였습니다.&#39;,
                });
            }
        }

        const [rowsInsert] = await connection.query(sqlInsertCart, values);

        if (rowsInsert.affectedRows &gt; 0) {
            return res.status(StatusCodes.CREATED).json({
                message: &#39;장바구니에 도서가 추가되었습니다.&#39;,
            });
        } else {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;장바구니 추가에 실패하였습니다.&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;장바구니 추가 중 문제가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<ol>
<li>먼저 존재하는 유저인지, 존재하는 도서인지 확인한다.</li>
<li>장바구니에 이미 담겨있는 도서일 때 원하는 수량만큼 장바구니에 담긴 도서의 수량이 증가한다.</li>
<li>위의 예외 처리가 모두 끝난 후 장바구니에 도서를 추가한다.</li>
</ol>
<p>처음에는 장바구니에 도서를 다시 못 담게 막아뒀는데 생각해보니까 똑같은 도서를 담지 못하는 건 말도 안되지 않은가. 그래서 같은 도서를 다시 담으려고 할 때 해당 수량만큼 기존 도서 수량에 증가하는 방식을 택했다.</p>
<ol start="2">
<li><code>CartMiddleware.js</code></li>
</ol>
<pre><code class="language-javascript">const { param, body, validationResult } = require(&#39;express-validator&#39;);

const validate = (req, res, next) =&gt; {
    const err = validationResult(req);

    if (!err.isEmpty()) {
        return res.status(400).json(err.array());
    } else {
        return next();
    }
};

const validateUserId = body(&#39;user_id&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;유저 아이디를 입력해주세요.&#39;)
    .isInt()
    .withMessage(&#39;숫자로 입력해주세요.&#39;);

const validateBookId = body(&#39;book_id&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;도서 아이디를 입력해주세요.&#39;)
    .isInt()
    .withMessage(&#39;숫자로 입력해주세요.&#39;);

const validateQuantity = body(&#39;quantity&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;수량을 입력해주세요.&#39;)
    .isInt()
    .withMessage(&#39;숫자로 입력해주세요.&#39;)
    .custom((value) =&gt; value &gt; 0)
    .withMessage(&#39;수량은 1 이상이어야 합니다.&#39;);

const validatesAddToCart = [validateUserId, validateBookId, validateQuantity, validate];
const validatesGetCartItems = [validateUserId, validate];

module.exports = { validatesAddToCart, validatesGetCartItems };
</code></pre>
<p>다른 유효성 검사와 기본적으로 비슷하기는 한데 수량 유효성 검사는 기본적으로 1 이상만 올 수 있게 막아뒀다.</p>
<ol start="3">
<li><code>Carts.js</code></li>
</ol>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const router = express.Router();
const { addToCart, getCartItems, deleteCartItem } = require(&#39;../controller/CartController&#39;);
const { validatesAddToCart, validatesGetCartItems } = require(&#39;../middleware/CartMiddleware&#39;);

router.use(express.json());
router
    .route(&#39;/&#39;)
    // 장바구니 담기
    .post(validatesAddToCart, addToCart)
    // 장바구니 조회, 선택된 장바구니 조회
    .get(validatesGetCartItems, getCartItems);
// 장바구니 도서 삭제
router.delete(&#39;/:id&#39;, deleteCartItem);

module.exports = router;</code></pre>
<p>다른 라우터 파일과 비슷하다.</p>
<h3 id="💡-express로-장바구니-조회-구현하기">💡 Express로 장바구니 조회 구현하기</h3>
<hr>
<pre><code class="language-javascript">const getCartItems = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { userId, selected } = camelcaseKeys(req.body);

    const sqlSelectUser = &#39;select * from users where id = ?&#39;;
    const sqlSelectAllCart = `select cartItems.id, book_id, title, summary, quantity, price
    from cartItems left join books on cartItems.book_id = books.id where user_id = ?`;
    const sqlSelectSelectedCart = `select cartItems.id, book_id, title, summary, quantity, price
    from cartItems left join books on cartItems.book_id = books.id where user_id = ? and cartItems.id in (?)`;

    let sqlSelectCart;
    let values;

    if (selected) {
        sqlSelectCart = sqlSelectSelectedCart;
        values = [userId, selected];
    } else {
        sqlSelectCart = sqlSelectAllCart;
        values = [userId];
    }

    try {
        const [rowsUser] = await connection.query(sqlSelectUser, userId);

        if (rowsUser.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 유저입니다.&#39;,
            });
        }

        const [rowsSelect] = await connection.query(sqlSelectCart, values);

        if (rowsSelect.length &gt; 0) {
            return res.status(StatusCodes.OK).json(rowsSelect);
        } else {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;장바구니에 담긴 도서가 없습니다.&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;장바구니 조회 중 문제가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>기본적으로 selected가 들어오지 않고 userId만 들어온다면 해당 유저가 장바구니에 담은 모든 도서를 반환하고, selected가 배열로 함께 들어오면 선택한 장바구니만 반환한다.
강의에서는 selected가 들어오지 않을 때의 예외 처리를 하지 않으셔서 이 부분은 내가 따로 추가했다.</p>
<h3 id="💡-express로-장바구니-삭제-구현하기">💡 Express로 장바구니 삭제 구현하기</h3>
<hr>
<pre><code class="language-javascript">const deleteCartItem = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { id } = req.params;

    const sqlDeleteCart = &#39;delete from cartItems where id = ?&#39;;

    try {
        const [rowsDelete] = await connection.query(sqlDeleteCart, id);

        if (rowsDelete.affectedRows &gt; 0) {
            return res.status(StatusCodes.OK).json({
                message: &#39;장바구니에서 도서가 삭제되었습니다.&#39;,
            });
        } else {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 장바구니 도서입니다.&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;장바구니 도서 삭제 중 문제가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>딱히 특별한 건 없고 다른 delete 요청과 마찬가지로 그냥 삭제하는 로직이다.</p>
<p>마찬가지로 에러 처리 부분에서 생각이 조금 많아진다. 좀 더 나은 방법이 있을 것 같긴 한데 일단 지금 기능 구현하는 것에 바빠서 리팩토링을 잘 못하고 있다... 당장 수목금 여행을 가서 최대한 빨리 끝내놔야 하는데!!!!
남은 부분 열심히 해서 마무리 지어보자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 9주차 DAY 2]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-2</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-2</guid>
            <pubDate>Mon, 15 Jan 2024 16:45:49 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-9주차-day-2">🚩 프로그래머스 데브코스 웹 풀사이클 과정 9주차 DAY 2</h2>
<hr>
<p>밀린 TIL 쓰기...(밀린 시점부터 TIL이 아닌가?) 저번주부터 이런저런 일이 있어서 사실 개발도 그렇게 많이 하지 못했다...</p>
<p>일단 해당 날짜에는 좋아요를 구현했다. 좋아요 추가와 취소를 구현했는데 로직 자체는 어렵지 않았고 유저와 도서가 존재하는지 체크하는 부분(특히 SQL문)을 많이 고민했다.</p>
<h3 id="💡express로-좋아요-추가-구현하기">💡Express로 좋아요 추가 구현하기</h3>
<hr>
<ol>
<li><code>LikeController.js</code></li>
</ol>
<pre><code class="language-javascript">const sqlSelect = `select * from likes where user_id = ? and liked_book_id = ?`;
const checkExist = `select (select count(*) from users where id = ?) as user_exists, (select count(*) from books where id = ?) as book_exists`;

const checkExistValues = async (connection, values) =&gt; {
    const [rows] = await connection.query(checkExist, values);

    return {
        user_exists: rows[0].user_exists === 1,
        book_exists: rows[0].book_exists === 1,
    };
};

const addLike = async (req, res) =&gt; {
    const { id } = req.params;
    const { user_id } = req.body;
    const sqlInsert = `insert into likes (user_id, liked_book_id) values (?, ?)`;
    const values = [user_id, id];

    const connection = await conn.getConnection();

    try {
        const { user_exists, book_exists } = await checkExistValues(connection, values);

        if (!user_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 유저입니다.&#39;,
            });
        }

        if (!book_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 도서입니다.&#39;,
            });
        }

        const [result] = await connection.query(sqlSelect, values);

        if (result.length &gt; 0) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;이미 좋아요한 책입니다.&#39;,
            });
        }

        const [insertResult] = await connection.query(sqlInsert, values);

        if (insertResult.affectedRows &gt; 0) {
            return res.status(StatusCodes.OK).json({
                message: &#39;좋아요 성공&#39;,
            });
        } else {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;좋아요 실패&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;서버 에러&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>먼저 유저와 도서가 존재하는지 확인하는 부분은 좋아요 취소 로직에도 반복되기 때문에 하나의 함수로 뺐다. 근데 이렇게 빼더라도 status return은 따로 해줘야해서 그렇게 크게 차이가 나려나 싶다. status code와 반복되는 메시지까지 반환하는 방법이 없는지 찾아봤는데 아직은 찾지 못했다... 아니면 아예 에러 핸들러 파일을 만들어야 하려나🙄</p>
<p>이미 좋아요 한 책이라면 반복해서 좋아요를 할 수 없게끔 하는 예외 처리도 만들었다. 근데 사실 이 좋아요도 그렇고 존재하지 않는 유저, 존재하지 않는 도서에 좋아요를 할 수 없게끔 하는 건 프론트에서 애초에 막아둘 테긴 한데... 요청이 잘못 들어올 수도 있으니까 일단 할 수 있는 예외 처리는 전부 해두었다.</p>
<ol start="2">
<li><code>LikeMiddelware.js</code></li>
</ol>
<pre><code class="language-javascript">const { param, body, validationResult } = require(&#39;express-validator&#39;);

const validate = (req, res, next) =&gt; {
    const err = validationResult(req);

    if (!err.isEmpty()) {
        return res.status(400).json(err.array());
    } else {
        return next();
    }
};

const validateLikedBookId = param(&#39;id&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;도서 아이디를 입력해주세요.&#39;)
    .isInt()
    .withMessage(&#39;숫자로 입력해주세요.&#39;);

const validateUserId = body(&#39;user_id&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;유저 아이디를 입력해주세요.&#39;)
    .isInt()
    .withMessage(&#39;숫자로 입력해주세요.&#39;);

const validatesLike = [validateLikedBookId, validateUserId, validate];

module.exports = { validatesLike };</code></pre>
<p>이건 좋아요 유효성 검사 미들웨어인데 사실 특별한 건 없다. 그냥 도서 아이디와 유저 아이디를 숫자로 입력받았는지만 확인하는 미들웨어다.</p>
<ol start="3">
<li><code>likes.js</code></li>
</ol>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const router = express.Router();
const { addLike, deleteLike } = require(&#39;../controller/LikeController&#39;);
const { validatesLike } = require(&#39;../middleware/LikeMiddleware&#39;);

router.use(express.json());

router
    .route(&#39;/:id&#39;)
    // 좋아요 추가
    .post(validatesLike, addLike)
    // 좋아요 취소
    .delete(validatesLike, deleteLike);

module.exports = router;
</code></pre>
<p>라우터 파일이다. 마찬가지로 다른 라우터 파일과 형식이 동일하다.</p>
<p>포스트맨으로 API 테스트를 하면 아래와 같이 잘 나오는 걸 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f503fe41-5aec-4bad-b6a3-a49ab8ec9529/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/2c5c0b63-9f17-4e6e-a6a0-e5e4b75bfb3b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/7955b45d-f5ad-47a3-b4ce-9b33b0bd221b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/b3cd7e62-5c39-4235-9221-7523e8bc81c4/image.png" alt=""></p>
<h3 id="💡-express로-좋아요-취소-구현하기">💡 Express로 좋아요 취소 구현하기</h3>
<hr>
<pre><code class="language-javascript">const deleteLike = async (req, res) =&gt; {
    const { id } = req.params;
    const { user_id } = req.body;
    const sqlDelete = `delete from likes where user_id = ? and liked_book_id = ?`;
    const values = [user_id, id];

    const connection = await conn.getConnection();

    try {
        const { user_exists, book_exists } = await checkExistValues(connection, values);

        if (!user_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 유저입니다.&#39;,
            });
        }

        if (!book_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 도서입니다.&#39;,
            });
        }

        const [result] = await connection.query(sqlSelect, values);

        if (result.length === 0) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;좋아요하지 않은 책입니다.&#39;,
            });
        }

        const [deleteResult] = await connection.query(sqlDelete, values);

        if (deleteResult.affectedRows &gt; 0) {
            return res.status(StatusCodes.OK).json({
                message: &#39;좋아요 취소 성공&#39;,
            });
        } else {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;좋아요 취소 실패&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;서버 에러&#39;,
        });
    } finally {
        connection.release();
    }
};

module.exports = {
    addLike,
    deleteLike,
};
</code></pre>
<p>좋아요 취소도 좋아요 추가와 로직은 얼추 비슷하다. 그냥 SQL문만 조금 달라지고 이미 좋아요 했던 책이 아니라 좋아요를 하지 않은 책일 때 Bad Request를 날려주는 것 정도?</p>
<p>마찬가지로 포스트맨으로 API 테스트를 한 결과는 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/027186b6-9185-43ba-b996-39ca0db760d6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/ea976fb4-1e08-4b82-b90a-7d2843f432d5/image.png" alt=""></p>
<h3 id="💫-개선할-사항">💫 개선할 사항</h3>
<hr>
<p>같은 팀원 분께서 해당 코드를 보시고 코드 리뷰를 해주셨다. 유저와 도서가 존재하는지 확인하는 부분에서 SQL문을 실행하지 않고 DB에서 던지는 에러를 확인해서 해당 로직을 대체하는 것이 어떻겠냐고 제안해주셨다.
이 코드 리뷰를 보고 든 생각... 이런 방법도 있구나!!! 그러고 보니 DB에서 보내는 에러에 errno라고 에러 번호가 함께 날아오는데 이걸 이용한다면 select SQL문을 실행하지 않고도 유저와 도서의 유무를 확인할 수 있을 것 같다.
근데 이렇게 한다면 다른 에러들도 아예 핸들러로 따로 빼서 정리하는 게 나을 것 같다는 생각도 한다. 일단 지금 있는 기능을 완성시키고 리팩토링을 하면서 고민해보는 걸로!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 9주차 DAY 1]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-1</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-9%EC%A3%BC%EC%B0%A8-DAY-1</guid>
            <pubDate>Tue, 09 Jan 2024 14:18:50 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-9주차-day-1">🚩 프로그래머스 데브코스 웹 풀사이클 과정 9주차 DAY 1</h2>
<hr>
<p>TIL이 늦어진 이유... 주말 + 어제까지 코드를 전체적으로 수정하느라... 계속 다짐만 하던 문제의 비동기!!! 모든 코드를 드디어 비동기 처리로 바꿨다. 사실 코드 수정 자체는 막 그렇게 오래 걸리지는 않았는데 수정 중간중간 발생하는 에러라든가 비동기 공부를 하느라 그게 더 오래 걸렸다.</p>
<h3 id="❓-왜-비동기로-작성해야-함">❓ 왜 비동기로 작성해야 함?</h3>
<hr>
<p>사유 : 콜백지옥</p>
<p>좋아요를 구현하는데 말만 들었던 그 콜백 지옥을 경험하고 말았다... </p>
<pre><code class="language-javascript">const addLike = (req, res) =&gt; {
    const { id } = req.params;
    const { user_id } = req.body;
    const sqlInsert = `insert into likes (user_id, liked_book_id) values (?, ?)`;
    const values = [user_id, id];
    conn.query(checkExist, values, (err, result) =&gt; {
        if (err) {
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                message: &#39;서버 에러&#39;,
             });
          }
       if (result[0].user_exists === 0) {
               return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 유저입니다.&#39;,
             });
          }
       if (result[0].book_exists === 0) {
               return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 도서입니다.&#39;,
                });
             }
       conn.query(sqlSelect, values, (err, result) =&gt; {
               if (err) {
                console.log(err);
                return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                    message: &#39;서버 에러&#39;,
                });
             }
               if (result.length &gt; 0) {
                return res.status(StatusCodes.BAD_REQUEST).json({
                    message: &#39;이미 좋아요한 책입니다.&#39;,
                });
            }
            conn.query(sqlInsert, values, (err, result) =&gt; {
                if (err) {
                    console.log(err);
                    return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                        message: &#39;서버 에러&#39;,
                    });
                }
                return res.status(StatusCodes.OK).json({
                    message: &#39;좋아요 성공&#39;,
                });
            });
};
</code></pre>
<p>위 코드를 보면 계속해서 중첩해서 호출을 하고 있다. 보기에 정말 아주 불편한 코드다. 이렇게 들여쓰기가 반복되는 코드를 일명 <code>콜백지옥(callback hell)</code>, <code>멸망의 피라미드(pyramid of doom)</code>라고 하는데 이를 해결하기 위해서는 async/await 구조를 활용하게 된다.</p>
<h3 id="💧-대대적인-코드-수정">💧 대대적인 코드 수정</h3>
<hr>
<p>벨로그... 왜 접어 보기가 안되는 거냐고!!!! 코드가 깁니다... 스압주의</p>
<ol start="0">
<li><code>mariadb.js</code><pre><code class="language-javascript">const mysql = require(&#39;mysql2/promise&#39;);
</code></pre>
</li>
</ol>
<p>const pool = mysql.createPool({
    host: process.env.HOST,
    user: process.env.USER,
    database: process.env.DATABASE,
    password: process.env.PASSWORD,
});</p>
<p>const getConnection = async () =&gt; {
    try {
        const connection = await pool.getConnection(async (conn) =&gt; conn);
        return connection;
    } catch (err) {
        return err;
    }
};</p>
<p>module.exports = {
    getConnection,
};</p>
<pre><code>
데이터베이스도 connection pool을 이용하게 수정하였다.

1. `UserController`

```javascript
const getUserByEmail = async (connection, email) =&gt; {
    const selectEmail = &#39;select * from users where email = ?&#39;;

    const [rows] = await connection.query(selectEmail, email);
    return rows.length &gt; 0 ? rows[0] : null;
};

const hashPassword = (password, salt) =&gt; {
    try {
        return crypto.pbkdf2Sync(password, salt, 10000, 10, &#39;sha512&#39;).toString(&#39;base64&#39;);
    } catch (err) {
        console.log(err);
        throw new Error(&#39;비밀번호 암호화 중 문제가 발생하였습니다.&#39;);
    }
};

const signup = async (req, res) =&gt; {
    const connection = await conn.getConnection();

    const { email, name, password } = req.body;

    const salt = crypto.randomBytes(10).toString(&#39;base64&#39;);
    const hashedPwd = hashPassword(password, salt);

    const sqlInsert = &#39;insert into users (email, name, password, salt) values (?, ?, ?, ?)&#39;;

    const values = [email, name, hashedPwd, salt];

    try {
        const existedEmail = await getUserByEmail(connection, email);
        if (existedEmail) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;이미 존재하는 이메일입니다.&#39;,
            });
        }

        const [rows] = await connection.query(sqlInsert, values);

        if (rows.affectedRows &gt; 0) {
            res.status(StatusCodes.CREATED).json({
                message: &#39;회원가입 성공&#39;,
            });
        } else {
            res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;회원가입 실패&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;회원가입 중 문제가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};

const signin = async (req, res) =&gt; {
    const { email, password } = req.body;
    const connection = await conn.getConnection();

    try {
        const existedEmail = await getUserByEmail(connection, email);

        if (!existedEmail) {
            return res.status(StatusCodes.UNAUTHORIZED).json({
                message: &#39;해당하는 이메일이 존재하지 않습니다.&#39;,
            });
        }

        const hashedPwd = hashPassword(password, existedEmail.salt);

        if (hashedPwd === existedEmail.password) {
            const token = jwt.sign(
                {
                    email: existedEmail.email,
                },
                process.env.TOKEN_PRIVATE_KEY,
                {
                    expiresIn: &#39;1h&#39;,
                    issuer: process.env.TOKEN_ISSUER,
                }
            );
            res.cookie(&#39;token&#39;, token, {
                httpOnly: true,
                secure: true,
                sameSite: &#39;none&#39;,
            });
            return res.status(StatusCodes.OK).json({
                message: &#39;로그인 성공&#39;,
                token: token,
            });
        } else {
            return res.status(StatusCodes.UNAUTHORIZED).json({
                message: &#39;비밀번호가 일치하지 않습니다.&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;로그인 중 문제가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};

const pwdResetRequest = async (req, res) =&gt; {
    const { email } = req.body;
    const connection = await conn.getConnection();

    try {
        const existedEmail = await getUserByEmail(connection, email);

        if (!existedEmail) {
            return res.status(StatusCodes.UNAUTHORIZED).json({
                message: &#39;해당하는 이메일이 존재하지 않습니다.&#39;,
            });
        } else {
            return res.status(StatusCodes.OK).json({
                message: &#39;이메일 발송 성공&#39;,
                email: email,
            });
        }
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;이메일 발송 중 문제가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};

const pwdReset = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { email, password } = req.body;

    const salt = crypto.randomBytes(10).toString(&#39;base64&#39;);
    const hashedPwd = hashPassword(password, salt);

    const sqlUpdate = &#39;update users set password = ?, salt = ? where email = ?&#39;;

    const values = [hashedPwd, salt, email];

    try {
        const existedEmail = await getUserByEmail(connection, email);

        if (!existedEmail) {
            return res.status(StatusCodes.UNAUTHORIZED).json({
                message: &#39;해당하는 이메일이 존재하지 않습니다.&#39;,
            });
        }

        const hashedNewPassword = hashPassword(password, existedEmail.salt);

        if (hashedNewPassword === existedEmail.password) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;새 비밀번호는 기존 비밀번호와 달라야 합니다.&#39;,
            });
        }

        const [rows] = await connection.query(sqlUpdate, values);

        if (rows.affectedRows &gt; 0) {
            res.status(StatusCodes.OK).json({
                message: &#39;비밀번호 초기화 성공&#39;,
            });
        } else {
            res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;비밀번호 초기화 실패&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;비밀번호 초기화 중 문제가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre><p>기존에 콜백마다 정의했던 서버 에러 처리도 try/catch 구문으로 좀 더 보기 좋게? 정리해보았다.</p>
<ol start="2">
<li><code>BookController</code></li>
</ol>
<pre><code class="language-javascript">const allBooks = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { category_id, news, limit, current_page } = req.query;
    // limit : 페이지 당 도서 수
    // currentPage : 현재 페이지
    // offset : 페이지 당 도서 수 * (현재 페이지 - 1)

    const parsedLimit = parseInt(limit);
    const parsedCurrentPage = parseInt(current_page);
    const offset = parsedLimit * (parsedCurrentPage - 1);
    const values = [];

    let sql = &#39;select *, (select count(*) from likes where books.id=liked_book_id) as likes from books&#39;;

    if (category_id &amp;&amp; news) {
        sql +=
            &#39; left join category on books.category_id = category.category_id where books.category_id = ? and pub_date between date_sub(now(), interval 1 month) and now()&#39;;
        values.push(category_id);
    } else if (category_id) {
        sql += &#39; left join category on books.category_id = category.category_id where books.category_id = ?&#39;;
        values.push(category_id);
    } else if (news) {
        sql += &#39; where pub_date between date_sub(now(), interval 1 month) and now()&#39;;
    }

    sql += &#39; limit ?, ?&#39;;
    values.push(offset, parsedLimit);

    try {
        const [rows] = await connection.query(sql, values);

        if (rows.length === 0) {
            const message = category_id ? &#39;해당하는 도서가 없습니다.&#39; : &#39;도서가 없습니다.&#39;;
            return res.status(StatusCodes.NOT_FOUND).json({
                message: message,
            });
        }

        return res.status(StatusCodes.OK).json(rows);
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;도서 조회 중 에러가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};

const bookDetail = async (req, res) =&gt; {
    const connection = await conn.getConnection();
    const { user_id } = req.body;
    const book_id = req.params.id;
    const sql = `select *, (select count(*) from likes where books.id=liked_book_id) as likes,
        (select exists(select * from likes where liked_book_id=? and user_id=?)) as liked from books
        left join category on books.category_id = category.category_id where books.id=?`;
    const values = [book_id, user_id, book_id];
    try {
        const [rows] = await connection.query(sql, values);

        if (rows.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;해당하는 도서가 없습니다.&#39;,
            });
        }

        return res.status(StatusCodes.OK).json(rows[0]);
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;도서 조회 중 에러가 발생하였습니다.&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>오늘 수정한 부분도 포함되어 있어서 SQL문이 이전과 살짝 다르다. async/await을 이용하니 훨씬 보기 좋고 깔끔해졌다.</p>
<ol start="3">
<li><p><code>CategoryController</code></p>
<pre><code class="language-javascript">const allCategory = async (req, res) =&gt; {
 const connection = await conn.getConnection();
 const sql = &#39;select * from category&#39;;

 try {
     const [rows] = await connection.query(sql);

     if (rows.length === 0) {
         return res.status(StatusCodes.NOT_FOUND).json({
             message: &#39;카테고리가 없습니다.&#39;,
         });
     }

     return res.status(StatusCodes.OK).json(rows);
 } catch (err) {
     console.log(err);
     return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
         error: &#39;카테고리 조회 중 에러가 발생하였습니다.&#39;,
     });
 } finally {
     connection.release();
 }
};</code></pre>
</li>
<li><p><code>LikeController</code></p>
</li>
</ol>
<pre><code class="language-javascript">const checkExistValues = async (connection, values) =&gt; {
    const [rows] = await connection.query(checkExist, values);

    return {
        user_exists: rows[0].user_exists === 1,
        book_exists: rows[0].book_exists === 1,
    };
};

const addLike = async (req, res) =&gt; {
    const { id } = req.params;
    const { user_id } = req.body;
    const sqlInsert = `insert into likes (user_id, liked_book_id) values (?, ?)`;
    const values = [user_id, id];

    const connection = await conn.getConnection();

    try {
        const { user_exists, book_exists } = await checkExistValues(connection, values);

        if (!user_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 유저입니다.&#39;,
            });
        }

        if (!book_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 도서입니다.&#39;,
            });
        }

        const [result] = await connection.query(sqlSelect, values);

        if (result.length &gt; 0) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;이미 좋아요한 책입니다.&#39;,
            });
        }

        const [insertResult] = await connection.query(sqlInsert, values);

        if (insertResult.affectedRows &gt; 0) {
            return res.status(StatusCodes.OK).json({
                message: &#39;좋아요 성공&#39;,
            });
        } else {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;좋아요 실패&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;서버 에러&#39;,
        });
    } finally {
        connection.release();
    }
};

const deleteLike = async (req, res) =&gt; {
    const { id } = req.params;
    const { user_id } = req.body;
    const sqlDelete = `delete from likes where user_id = ? and liked_book_id = ?`;
    const values = [user_id, id];

    const connection = await conn.getConnection();

    try {
        const { user_exists, book_exists } = await checkExistValues(connection, values);

        if (!user_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 유저입니다.&#39;,
            });
        }

        if (!book_exists) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;존재하지 않는 도서입니다.&#39;,
            });
        }

        const [result] = await connection.query(sqlSelect, values);

        if (result.length === 0) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;좋아요하지 않은 책입니다.&#39;,
            });
        }

        const [deleteResult] = await connection.query(sqlDelete, values);

        if (deleteResult.affectedRows &gt; 0) {
            return res.status(StatusCodes.OK).json({
                message: &#39;좋아요 취소 성공&#39;,
            });
        } else {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;좋아요 취소 실패&#39;,
            });
        }
    } catch (err) {
        console.log(err);
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
            message: &#39;서버 에러&#39;,
        });
    } finally {
        connection.release();
    }
};</code></pre>
<p>문제의 LikeController 부분도 async/await으로 수정을 하니 훨씬 깔끔해진 걸 볼 수 있다.
근데 아직 좀 아쉬운 게 먼저 유저/책이 존재하는지 확인하는 부분이 중복되고 있어서 이걸 따로 빼고 싶어서 이런저런 시도를 해봤는데... 딱히 마음에 들게 수정되지 않아서 일단 저대로 뒀다.
그리고 서버 에러 부분도 계속 중복되고 있으니 저것도 handler로 뺄 수 있을까 고민중이다. 근데 내가 원하는 방식으로는 자꾸 안 되고 있어서 조금 더 찾아봐야 할 것 같다.
일단 비동기에 대해서 잘 모르는 상태이기도 해서 코드가 좀 이상할 수도 있다...😂 이 부분은 계속 공부하면서 아마 수정해나가지 않을까 싶다!!! 일단 당장 내일부터 비동기에 대한 수업이라서 그 부분을 들으면서 수정 방향을 정해볼까 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 8주차 DAY 5]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-5</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-5</guid>
            <pubDate>Sun, 07 Jan 2024 16:17:59 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-8주차-day-5">🚩 프로그래머스 데브코스 웹 풀사이클 과정 8주차 DAY 5</h2>
<hr>
<p>테이블을 조인하여 도서를 조회할 때 카테고리 명을 반환할 수 있게끔 했다. 신간 도서를 조회할 수 있도록 했고, 또한 페이징을 구현하였다.</p>
<h3 id="💡-express로-카테고리명-반환-구현">💡 Express로 카테고리명 반환 구현</h3>
<hr>
<ol>
<li><code>CategoryController.js</code></li>
</ol>
<pre><code class="language-javascript">const conn = require(&#39;../mariadb&#39;);
const { StatusCodes } = require(&#39;http-status-codes&#39;);

const allCategory = (req, res) =&gt; {
    const sql = &#39;select * from category&#39;;

    conn.query(sql, (err, results) =&gt; {
        if (err) {
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                error: &#39;서버 에러&#39;,
            });
        }

        if (results.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;카테고리가 없습니다.&#39;,
            });
        }

        return res.status(StatusCodes.OK).json(results);
    });
};

module.exports = { allCategory };
</code></pre>
<ol start="2">
<li><code>category.js</code></li>
</ol>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const router = express.Router();
const { allCategory } = require(&#39;../controller/CategoryController&#39;);
router.use(express.json());

// 전체 카테고리 조회
router.get(&#39;/&#39;, allCategory);

module.exports = router;
</code></pre>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/b18d2370-8402-4f07-86d6-bb49410beee1/image.png" alt=""></p>
<p>카테고리는 말 그대로 전체 카테고리만 받아오면 되기 때문에 그냥 데이터베이스에 저장 된 모든 카테고리를 반환할 수 있게 구현하였다.</p>
<h3 id="💡-express로-조인한-테이블-데이터-반환">💡 Express로 조인한 테이블 데이터 반환</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/08980320-31aa-45bb-a2ce-ad9c7cb509ae/image.png" alt=""></p>
<p>먼저 books 테이블과 category 테이블의 관계는 위와 같다. 도서를 반환할 때 카테고리명도 함께 반환하게 하기 위해서 테이블을 조인한다. 기존 코드에서 크게 달라지지 않고 SQL문만 조금 수정하면 된다.</p>
<pre><code class="language-javascript">values = [];

const sql = &#39;select * from books left join category on books.category_id = category.id where category_id = ?&#39;;

values.push(category_id);

conn.query(sql, values, (err, results) =&gt; {
    // SQL 쿼리 실행
}</code></pre>
<p>조인 조건은 books 테이블의 category_id가 category 테이블의 id와 같을 때이다. 이후 쿼리로 받아온 category_id를 필터링해서 결과값을 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/4ddd14ce-566b-450b-8210-1343acf0211a/image.png" alt=""></p>
<p>도서 조회를 해보면 위처럼 name으로 카테고리명이 함께 반환되는 것을 볼 수 있다.</p>
<h3 id="💡express로-신간-도서-조회-구현">💡Express로 신간 도서 조회 구현</h3>
<hr>
<p>현재 날짜를 기준으로 최근 한 달 이내 책이 발간되었을 때 해당 책만 필터링해서 보여준다. 요청 쿼리로 <code>news</code>가 <code>true</code> 일 때만 신간 도서가 필터링되어 보인다.</p>
<pre><code class="language-javascript">const allBooks = (req, res) =&gt; {
  const { category_id, news } = req.query;

  let sql = &#39;select * from books&#39;;

  if (category_id &amp;&amp; news) {
       sql +=
           &#39; left join category on books.category_id = category.id where category_id = ? and pub_date between date_sub(now(), interval 1 month) and now()&#39;;
       values.push(category_id);
   } else if (category_id) {
       sql += &#39; left join category on books.category_id = category.id where category_id = ?&#39;;
       values.push(category_id);
   } else if (news) {
       sql += &#39; where pub_date between date_sub(now(), interval 1 month) and now()&#39;;
   }

  conn.query(sql, values, (err, results) =&gt; {
       if (err) {
           console.log(err);
           return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                  error: &#39;서버 에러&#39;,
          });
          }

       if (results.length === 0) {
           const message = category_id ? &#39;해당하는 도서가 없습니다.&#39; : &#39;도서가 없습니다.&#39;;
           return res.status(StatusCodes.NOT_FOUND).json({
               message: message,
           });
       }

          return res.status(StatusCodes.OK).json(results);
      });
}
</code></pre>
<p><strong>SQL문</strong></p>
<pre><code class="language-javascript">&#39;select * from books where pub_date between date_sub(now(), interval 1 month) and now()&#39;</code></pre>
<p><code>DATE_SUB</code>를 이용하여 한달 이전 날짜와 현재 날짜 사이에 출간한 도서만 조회하는 SQL문이다.</p>
<ol>
<li><code>select * from books</code>가 반복되고 있기 때문에 해당 SQL을 밖으로 빼고 필요한 SQL문은 조건에 따라 추가하였다.</li>
<li>요청 쿼리에 <code>카테고리명</code>, <code>신간 도서 여부</code>를 받아오기 때문에 카테고리명만 받아올 때, 혹은 신간 여부만 받아올 때 또는 둘 다 받아올 때를 나누어서 SQL문을 더해준다.</li>
<li>해당하는 도서가 없을 때의 에러 메시지는 전체 도서를 받아올 때 도서가 하나도 없을 때 (이런 경우는 없겠지만), 카테고리명을 입력했을 때 해당하는 카테고리의 도서가 없을 때, 혹은 신간이 존재하지 않을 때를 나누어서 다른 메시지를 받아오게 했다.
조건부 삼항 연산자를 활용해 <code>category_id</code>가 있다면 해당하는 도서가 없다는 메시지를, <code>category_id</code>가 없다면 도서가 없다는 메시지를 출력한다.</li>
<li>모든 연산이 끝난 후 결과를 반환한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/219a6f9c-c951-4f1a-a0be-892e7b220004/image.png" alt=""></p>
<p>예시 사진은 내가 페이징까지 전부 구현한 후라서 <code>limit</code>와 <code>current_page</code>를 요청 쿼리로 함께 받아 온다. <code>news</code>가 true일 때, 즉 현재 날짜에서 한 달 이내에 출간한 책만 잘 반환되는 걸 볼 수 있다.</p>
<h3 id="💡-express로-페이징-구현">💡 Express로 페이징 구현</h3>
<hr>
<pre><code class="language-javascript">const allBooks = (req, res) =&gt; {
    const { category_id, news, limit, current_page } = req.query;
    // limit : 페이지 당 도서 수
    // currentPage : 현재 페이지
    // offset : 페이지 당 도서 수 * (현재 페이지 - 1)

    const parsedLimit = parseInt(limit);
    const parsedCurrentPage = parseInt(current_page);

    if (!parsedLimit || !parsedCurrentPage || parsedLimit &lt; 1 || parsedCurrentPage &lt; 1) {
        return res.status(StatusCodes.BAD_REQUEST).json({
            error: &#39;limit 혹은 currentPage가 잘못되었습니다.&#39;,
        });
    }

    const offset = parsedLimit * (parsedCurrentPage - 1);
    const values = [];

    let sql = &#39;select * from books&#39;;

    if (category_id &amp;&amp; news) {
        sql +=
            &#39; left join category on books.category_id = category.id where category_id = ? and pub_date between date_sub(now(), interval 1 month) and now()&#39;;
        values.push(category_id);
    } else if (category_id) {
        sql += &#39; left join category on books.category_id = category.id where category_id = ?&#39;;
        values.push(category_id);
    } else if (news) {
        sql += &#39; where pub_date between date_sub(now(), interval 1 month) and now()&#39;;
    }

    sql += &#39; limit ?, ?&#39;;
    values.push(offset, parsedLimit);

    conn.query(sql, values, (err, results) =&gt; {
        if (err) {
            console.log(err);
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                error: &#39;서버 에러&#39;,
            });
        }

        if (results.length === 0) {
            const message = category_id ? &#39;해당하는 도서가 없습니다.&#39; : &#39;도서가 없습니다.&#39;;
            return res.status(StatusCodes.NOT_FOUND).json({
                message: message,
            });
        }

        return res.status(StatusCodes.OK).json(results);
    });
};</code></pre>
<p>드디어 대망의 페이징이다! 페이징을 요청할 때는 페이지 당 몇 개의 데이터를 보여줄 것인지, 그리고 현재 페이지가 어디인지, 또한 데이터베이스에서 가져올 도서 목록의 시작 위치를 나타낼 변수 또한 필요하다.</p>
<p>먼저 해당 코드에서 <code>limit</code> 요청 쿼리는 페이지 당 도서 수를 몇 개로 할지 보낸다. <code>current_page</code>는 현재 페이지를 보낸다.
그리고 <code>offset</code>은, 가져온 도서 목록에서 시작 위치를 나타낸다. 예를 들어서 페이지 당 도서 수를 5로 잡고, 현재 페이지가 3이라면 <code>offset</code>은 15가 된다. 즉, 데이터베이스에서 15번째 도서부터 가져와야 한다는 것을 뜻한다.
따라서 <code>offset = 페이지 당 도서 수 * (현재 페이지 -1)</code>으로 계산할 수 있다.</p>
<p><strong>SQL문</strong></p>
<pre><code class="language-javascript">select * from books limit ?, ?;
// offset, limit 순으로 들어간다.
select * from books limit ? offset ?;
// 위의 쿼리와 같다. 다만 이 쿼리는 limit, offset 순으로 들어간다.</code></pre>
<p>limit은 반환하는 데이터의 수를 제한해준다. 건너뛸 데이터의 수, 그리고 반환할 최대 데이터 수를 차례대로 넣어준다.</p>
<h4 id="🚨-서버-에러-발생">🚨 서버 에러 발생</h4>
<hr>
<p><strong>1. 첫번째 에러</strong></p>
<p>서버를 실행하고 postman으로 API 테스트를 하려고 했는데 아뿔싸! 갑자기 서버 에러가 발생하고 말았다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/466805ec-c796-4695-a506-a4ce63beca0e/image.png" alt=""></p>
<p>그리고 콘솔로 에러 로그를 찍어보니 위와 같은 메시지가 나왔다. 이유는 정말 간단했다... 처음에는 내가 첫 초기화를 한 SQL문에 <code>select * from books limit ?, ?</code> 식으로 변수 저장을 해두었다. 문제는 이쪽이었다.
찾아보니 LIMIT 문은 SQL 문의 가장 끝에 들어가야 한다고 한다. 그래서 모든 조건문이 실행된 이후 새롭게 LIMIT가 추가될 수 있도록 수정하였다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/cfcf40a9-7a22-4a62-834d-ecf5ffe49635/image.png" alt=""></p>
<p>다시 서버를 실행해보면 에러 없이 데이터를 잘 반환하는 것을 볼 수 있다.</p>
<ol start="2">
<li>두 번째 에러</li>
</ol>
<p>두 번째 에러는 정말 단순했다. 그냥 <code>limit</code>와 <code>current_page</code> 요청 쿼리를 받아오지 않으면 서버 에러가 발생하는 거였는데, 사실 도서를 조회할 때 필수적으로 페이징이 들어가긴 하겠지만... 그래도 정말 혹시나 하는 상황이 있을 수 있으니 예외 처리를 추가해줬다. <code>limit</code>와 <code>current_page</code>는 무조건 1 이상이어야 하니 1보다 작은 수가 오거나 아예 <code>limit</code>나 <code>current_page</code>가 오지 않을 때 404 Bad Request 에러를 반환했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 8주차 DAY 4]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-4</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-4</guid>
            <pubDate>Fri, 05 Jan 2024 13:43:36 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-8주차-day-4">🚩 프로그래머스 데브코스 웹 풀사이클 과정 8주차 DAY 4</h2>
<hr>
<p>도서 조회를 할 수 있는 BookController.js 부분을 완성시켰다. 그리고 조원 분의 조언에 따라 유효성 검사 하는 부분을 미들웨어로 분리시켰다!</p>
<h3 id="💡-express로-도서-전체-조회-카테고리별-도서-조회-구현하기">💡 Express로 도서 전체 조회, 카테고리별 도서 조회 구현하기</h3>
<hr>
<pre><code class="language-javascript">const allBooks = (req, res) =&gt; {
    const { category_id } = req.query;

    if (category_id) {
        const sql = &#39;select * from books where category_id = ?&#39;;

        conn.query(sql, category_id, (err, results) =&gt; {
            if (err) {
                return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                    error: err.message,
                });
            }

            if (results.length === 0) {
                return res.status(StatusCodes.NOT_FOUND).json({
                    message: &#39;해당하는 카테고리의 도서가 없습니다.&#39;,
                });
            }

            return res.status(StatusCodes.OK).json(results);
        });
    } else {
        const sql = &#39;select * from books&#39;;

        conn.query(sql, (err, results) =&gt; {
            if (err) {
                return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                    error: err.message,
                });
            }

            if (results.length === 0) {
                return res.status(StatusCodes.NOT_FOUND).json({
                    message: &#39;도서가 없습니다.&#39;,
                });
            }

            return res.status(StatusCodes.OK).json(results);
        });
    }
};</code></pre>
<p><code>/books</code> GET
: 데이터베이스 내에 존재하는 모든 도서를 응답으로 반환해준다. 보낸 응답에서 필요한 정보를 프론트에서 선별해서 사용하게끔 상세 정보까지 전부 반환한다.</p>
<p><code>/books?category_id=()</code> GET
: 쿼리로 카테고리 아이디를 받아 해당하는 카테고리의 도서를 모두 반환해준다. 여기서 path parameter로 받는 게 아니라 쿼리로 받은 이유는 보통 필터링을 할 때는 쿼리로 받아 온다고 한다.</p>
<p>보면 위의 코드가 상당히 지저분해보인다. 이 글을 쓰고 있는 현재는 리팩토링을 한 상태이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/13cd926e-f1e0-4b8c-b55f-0797aacdaea3/image.png" alt=""></p>
<p>데이터베이스 내의 모든 도서 목록을 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/3b8af830-d9de-44be-b732-3ccd92f396b2/image.png" alt=""></p>
<p>쿼리로 카테고리 아이디를 받아오면 해당 카테고리에 맞는 도서를 필터링해서 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/72fa0f42-f751-472f-8527-0d1cb50ceeb9/image.png" alt=""></p>
<p>해당하는 카테고리 아이디가 없다면 404 Not Found를 반환한다.</p>
<h3 id="💡-express로-도서-상세-조회-구현하기">💡 Express로 도서 상세 조회 구현하기</h3>
<hr>
<pre><code class="language-javascript">const bookDetail = (req, res) =&gt; {
    const { id } = req.params;
    const sql = &#39;select * from books where id = ?&#39;;

    conn.query(sql, id, (err, results) =&gt; {
        if (err) {
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                error: err.message,
            });
        }

        if (results.length === 0) {
            return res.status(StatusCodes.NOT_FOUND).json({
                message: &#39;해당하는 도서가 없습니다.&#39;,
            });
        }

        return res.status(StatusCodes.OK).json(results[0]);
    });
};</code></pre>
<p><code>/books/:id</code> GET
: 파라미터로 아이디를 받아 해당하는 아이디의 도서를 반환한다. 아이디가 존재하지 않으면 404 Not Found를 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/a534b795-ae48-42f6-9da0-df52fbaca08a/image.png" alt=""></p>
<p>파라미터로 도서 아이디 3을 받아오자 해당하는 아이디의 도서의 모든 정보가 반환되는 것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/2746ad8f-d97d-4c53-87f6-65e35d1a3c71/image.png" alt=""></p>
<p>데이터베이스 내에 해당 아이디의 도서가 존재하지 않는다면 404 Not Found 에러를 반환한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 8주차 DAY 3]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-3</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-3</guid>
            <pubDate>Wed, 03 Jan 2024 13:29:11 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-8주차-day-3">🚩 프로그래머스 데브코스 웹 풀사이클 과정 8주차 DAY 3</h2>
<hr>
<p>어제 하다가 끝내지 못한 UserController.js를 완성시켰다. 로그인 기능과 비밀번호 초기화 요청, 비밀번호 초기화가 있다. 생각해보니 로그아웃 기능이 없는데 이건 내가 따로 만들어 봐야겠다. <del>사실 지금도 강의랑은 좀 다르게 짜고 있지만</del></p>
<h3 id="💡-express로-로그인-구현">💡 Express로 로그인 구현</h3>
<hr>
<pre><code class="language-javascript">const signin = (req, res) =&gt; {
    const { email, password } = req.body;
    const sql = &#39;select * from users where email = ?&#39;;

    conn.query(sql, email, (err, results) =&gt; {
        if (err) {
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                message: &#39;서버 에러&#39;,
            });
        }

        const signinUser = results[0];

        if (!signinUser) {
            return res.status(StatusCodes.UNAUTHORIZED).json({
                message: &#39;해당하는 이메일이 존재하지 않습니다.&#39;,
            });
        }

        try {
            const hashedPassword = crypto.pbkdf2Sync(password, signinUser.salt, 10000, 10, &#39;sha512&#39;).toString(&#39;base64&#39;);

            if (hashedPassword === signinUser.password) {
                const token = jwt.sign(
                    {
                        email: signinUser.email,
                    },
                    process.env.PRIVATE_KEY,
                    {
                        expiresIn: &#39;1h&#39;,
                        issuer: &#39;minkyung&#39;,
                    }
                );
                res.cookie(&#39;token&#39;, token, {
                    httpOnly: true,
                    secure: true,
                    sameSite: &#39;none&#39;,
                });
                return res.status(StatusCodes.OK).json({
                    message: &#39;로그인 성공&#39;,
                    token: token,
                });
            } else {
                return res.status(StatusCodes.UNAUTHORIZED).json({
                    message: &#39;비밀번호가 일치하지 않습니다.&#39;,
                });
            }
        } catch (err) {
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                message: &#39;비밀번호 해싱 중 문제가 발생하였습니다.&#39;,
            });
        }
    });
};</code></pre>
<p>비동기로 코드를 짠다면 좀 더 보기 좋아질 것 같은데 내가 아직 비동기 부분에 대해서 좀 미숙한지라... 조금 더 공부한 후 전체적으로 리팩토링을 할 예정이다.</p>
<ol>
<li>해당하는 이메일이 존재하는지 확인, 없으면 <code>Unauthorized</code> 반환</li>
<li>이메일이 존재한다면 비밀번호 일치하는지 확인, 일치하지 않으면 <code>Unauthorized</code> 반환</li>
<li>데이터베이스에 저장한 솔트값을 꺼내서 해시 후 비교</li>
<li>jwt 생성, 만료 시간은 1시간</li>
<li>로그인 성공 시 토큰 반환</li>
<li>비밀번호 해싱 시 문제가 생길 것을 고려하여 try/catch 사용</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f13c0d48-5208-4df7-a34e-39d1f142652e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/9d10db25-b486-4080-9be3-c17766d97502/image.png" alt=""></p>
<p>에러 정상 반환</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/1c75e26b-d844-46c0-b383-8809e81d71a7/image.png" alt=""></p>
<p>로그인 성공 시 토큰 정상 반환</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f9d08977-7ffd-4858-b4a5-1d8fae305321/image.png" alt=""></p>
<p>위 경우는 솔트값이 포함되지 않은 계정으로 로그인 시 발생하는 에러</p>
<h3 id="💡-express로-비밀번호-재설정-구현">💡 Express로 비밀번호 재설정 구현</h3>
<hr>
<ol>
<li>비밀번호 초기화 요청</li>
</ol>
<pre><code class="language-javascript">const pwdResetRequest = (req, res) =&gt; {
    const { email } = req.body;
    const sql = &#39;select * from users where email = ?&#39;;

    conn.query(sql, email, (err, results) =&gt; {
        if (err) {
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                message: &#39;서버 에러&#39;,
            });
        }

        const user = results[0];

        if (!user) {
            return res.status(StatusCodes.UNAUTHORIZED).json({
                message: &#39;해당하는 이메일이 존재하지 않습니다.&#39;,
            });
        } else {
            return res.status(StatusCodes.OK).json({
                message: &#39;이메일 발송 성공&#39;,
                email: email,
            });
        }
    });
};</code></pre>
<ol start="2">
<li>비밀번호 초기화</li>
</ol>
<pre><code class="language-javascript">const pwdReset = (req, res) =&gt; {
    const { email, password } = req.body;

    const salt = crypto.randomBytes(10).toString(&#39;base64&#39;);
    const hashPwd = crypto.pbkdf2Sync(password, salt, 10000, 10, &#39;sha512&#39;).toString(&#39;base64&#39;);

    const sqlUpdate = &#39;update users set password = ?, salt = ? where email = ?&#39;;
    const sqlSelect = &#39;select * from users where email = ?&#39;;
    const values = [hashPwd, salt, email];

    conn.query(sqlSelect, email, (err, results) =&gt; {
        if (err) {
            return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                message: &#39;서버 에러&#39;,
            });
        }

        const user = results[0];

        if (!user) {
            return res.status(StatusCodes.UNAUTHORIZED).json({
                message: &#39;해당하는 이메일이 존재하지 않습니다.&#39;,
            });
        }

        const hashedNewPassword = crypto.pbkdf2Sync(password, user.salt, 10000, 10, &#39;sha512&#39;).toString(&#39;base64&#39;);

        if (hashedNewPassword === user.password) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message: &#39;새 비밀번호는 기존 비밀번호와 달라야 합니다.&#39;,
            });
        }

        conn.query(sqlUpdate, values, (err, results) =&gt; {
            if (err) {
                return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
                    message: &#39;서버 에러&#39;,
                });
            }

            if (results.affectedRows &gt; 0) {
                res.status(StatusCodes.OK).json({
                    message: &#39;비밀번호 초기화 성공&#39;,
                });
            } else {
                res.status(StatusCodes.BAD_REQUEST).json({
                    message: &#39;비밀번호 초기화 실패&#39;,
                });
            }
        });
    });
};</code></pre>
<p>마찬가지로 비동기를 사용하지 않아 코드가 조금 지저분...하다. 빨리 공부해서 리팩토링해야지😭</p>
<ol>
<li>비밀번호 재설정 요청 시에는 확인 메일을 전송하기 때문에 가입 시 입력했던 메일이 필요하다. 이 부분은 <code>nodemailer</code>를 이용해서 차후 추가할 예정, 지금은 우선 메일이 맞는지만 확인</li>
<li>메일이 존재한다면 해당하는 메일 주소를 반환</li>
<li>비밀번호 재설정 부분에는 혹시 몰라 해당하는 이메일이 존재하지 않을 때의 예외 처리를 해두긴 했는데... 생각해보니 이메일 반환 후 바로 페이지가 넘어갈텐데 굳이 필요하려나? 이 부분은 좀 더 생각해보자</li>
<li>입력한 새 비밀번호가 기존 비밀번호가 같다면 에러 반환</li>
<li>비밀번호 초기화 성공 시 200 OK</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/21880597-372c-46ed-8cbf-9b4c358e9902/image.png" alt=""></p>
<p>이메일 존재 시 해당 이메일 주소 반환</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/9e030aa3-5099-4432-86df-8ca376f8c3dd/image.png" alt=""></p>
<p>이메일 존재하지 않으면 <code>Unathorized</code> 반환</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/a0055522-f7b4-4960-a5b5-26c0c4005678/image.png" alt=""></p>
<p>새로운 비밀번호 초기화에 성공한다면</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/74d0eba2-a2ad-4ff3-8786-de2456b7f96a/image.png" alt=""></p>
<p>이게 기존 비밀번호 (해시화되어 저장)</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/890fab12-97fa-48a8-836d-b5dbb168c9d2/image.png" alt=""></p>
<p>변경 후 다시 조회하면 비밀번호와 솔트값 모두 바뀌어있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/e2ae1d06-6511-4b31-b177-10c4f403908e/image.png" alt=""></p>
<p>변경 이후 변경된 비밀번호로 로그인 시 정상적으로 로그인이 되는 걸 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/73c322c9-5f11-4922-98a7-66a9fe6f69c5/image.png" alt=""></p>
<p>기존 비밀번호와 똑같은 비밀번호를 입력 후 수정하려고 하면 <code>Bad request</code>를 반환한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 8주차 DAY 2]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-2</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-8%EC%A3%BC%EC%B0%A8-DAY-2</guid>
            <pubDate>Tue, 02 Jan 2024 11:21:04 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-8주차-day-2">🚩 프로그래머스 데브코스 웹 풀사이클 과정 8주차 DAY 2</h2>
<hr>
<p>저번주부터 도서 쇼핑몰 프로젝트에 들어갔다. 저번주는 내내 API 설계와 데이터베이스 설계를 했고 이번주부터 본격적으로 API 개발에 들어갔다. 오늘 수업에서는 회원가입, 로그인, 비밀번호 초기화까지 진행을 했는데 난 우선 회원가입 부분만 커밋해서 깃허브에 푸시했다.</p>
<h3 id="💡-express로-회원가입">💡 Express로 회원가입</h3>
<hr>
<ol>
<li>컨트롤러 파일 : <code>UserController.js</code><pre><code class="language-javascript">const conn = require(&#39;../mariadb&#39;);
const { StatusCodes } = require(&#39;http-status-codes&#39;);
const { body, param, validationResult } = require(&#39;express-validator&#39;);
const crypto = require(&#39;crypto&#39;);
</code></pre>
</li>
</ol>
<p>// 최소 영문자 하나, 숫자 하나, 특수문자 하나 이상의 8자 16자 사이의 비밀번호
const regex = /^(?=.<em>[a-zA-Z])(?=.</em>[!@#$%^<em>+=-])(?=.</em>[0-9]).{8,16}$/;</p>
<p>const validate = (req, res, next) =&gt; {
    const err = validationResult(req);</p>
<pre><code>if (!err.isEmpty()) {
    return res.status(StatusCodes.BAD_REQUEST).json(err.array());
} else {
    return next();
}</code></pre><p>};</p>
<p>const validateEmail = body(&#39;email&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;이메일을 입력하지 않았습니다.&#39;)
    .isEmail()
    .withMessage(&#39;이메일 형식이 아닙니다.&#39;);</p>
<p>const validatePwd = body(&#39;password&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;비밀번호를 입력하지 않았습니다.&#39;)
    .matches(regex)
    .withMessage(&#39;비밀번호는 영문자, 숫자, 특수문자가 하나 이상 포함되며 8자에서 16자 사이여야 합니다.&#39;);</p>
<p>const validateName = body(&#39;name&#39;)
    .trim()
    .notEmpty()
    .withMessage(&#39;이름을 입력하지 않았습니다.&#39;)
    .isString()
    .withMessage(&#39;문자열로 입력해주세요.&#39;);</p>
<p>const validatesSignup = [validateEmail, validatePwd, validateName, validate];</p>
<p>const signup = (req, res) =&gt; {
    const { email, name, password } = req.body;</p>
<pre><code>const salt = crypto.randomBytes(10).toString(&#39;base64&#39;);
const hashPwd = crypto.pbkdf2Sync(password, salt, 10000, 10, &#39;sha512&#39;).toString(&#39;base64&#39;);

const sqlInsert = &#39;insert into users (email, name, password, salt) values (?, ?, ?, ?)&#39;;
const sqlSelect = `select * from users where email = ?`;

const values = [email, name, hashPwd, salt];

conn.query(sqlSelect, email, function (err, results) {
    if (results.length &gt; 0) {
        res.status(StatusCodes.BAD_REQUEST).json({
            message: &#39;이미 존재하는 이메일입니다.&#39;,
        });
    } else {
        conn.query(sqlInsert, values, function (err, results) {
            if (results.affectedRows &gt; 0) {
                res.status(StatusCodes.CREATED).json({
                    message: &#39;회원가입 성공&#39;,
                });
            } else {
                res.status(StatusCodes.BAD_REQUEST).json({
                    message: &#39;회원가입 실패&#39;,
                });
            }
        });
    }
});</code></pre><p>};</p>
<pre><code>
2. 라우터 파일 : `users.js`

```javascript
const express = require(&#39;express&#39;);
const router = express.Router();
const { signup, signin, pwdResetRequest, pwdReset, validatesSignup } = require(&#39;../controller/UserController&#39;);

router.use(express.json());

// 회원가입
router.post(&#39;/signup&#39;, validatesSignup, signup);

module.exports = router;</code></pre><ol start="3">
<li>데이터베이스 연결 : <code>mariadb.js</code></li>
</ol>
<pre><code class="language-javascript">// get the client
const mysql = require(&#39;mysql2&#39;);

// create the connection to database
const connection = mysql.createConnection({
    host: &#39;localhost&#39;,
    user: &#39;root&#39;,
    password: &#39;root&#39;,
    database: &#39;Bookstore&#39;,
    dateStrings: true,
});

module.exports = connection;
</code></pre>
<ol start="4">
<li><code>app.js</code></li>
</ol>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const app = express();
const dotenv = require(&#39;dotenv&#39;);

dotenv.config();

app.listen(process.env.PORT);

const userRouter = require(&#39;./routes/users&#39;);

app.use(&#39;/users&#39;, userRouter);
</code></pre>
<p>사용한 패키지는 상태코드를 위한 <code>http-status-codes</code>, 비밀번호 암호화를 위한 <code>crypto</code>, 유효성 검사를 위한 <code>express-validator</code>가 있다.</p>
<p>회원가입을 구현할 때 신경쓴 부분은
먼저 이미 이메일이 존재할 경우에는 BAD REQUEST를 반환하고, 이메일이 존재하지 않을 경우에만 회원가입이 성공하게끔 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/b798a561-a34d-4edb-b1c6-6415be823913/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/c311ea44-a8b5-4beb-a3af-0dd9d21f2d2a/image.png" alt=""></p>
<p>그리고 비밀번호는 8자 이상 16자 이하로 입력하되 영문자, 숫자, 특수문자를 각각 하나 이상 포함되게 유효성 검사를 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/7c096f9b-7884-4aff-91e0-c8375d6bf619/image.png" alt=""></p>
<p>비밀번호는 <code>crypto</code>를 이용하여 암호화되어 저장되고 솔트 값도 함께 데이터베이스에 저장한다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5e815a86-7ea8-42d5-8a71-87d0cea92be1/image.png" alt=""></p>
<p>그리고 컨트롤러와 라우터를 구분해서 파일을 생성했고, .env 파일을 통해 유출되면 안 되는 정보를 따로 저장해두었다.</p>
<p>더 자세한 코드를 보고 싶다면 ↓
<a href="https://github.com/alsrud991026/bookstore-service">깃허브</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 웹 풀 사이클 데브코스 12월 회고]]></title>
            <link>https://velog.io/@gangk_99/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9B%B9-%ED%92%80-%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-12%EC%9B%94-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@gangk_99/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%9B%B9-%ED%92%80-%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-12%EC%9B%94-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 29 Dec 2023 09:51:20 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">국비지원교육 현실, 국비지원 교육 추천, 코딩 부트캠프 현실, 코딩 부트캠프 추천, 코딩 부트캠프 비교</span></p>
<h2 id="✨-12월을-회고하며">✨ 12월을 회고하며</h2>
<hr>
<p>어느덧 데브코스를 수강한지도 벌써 한 달이 훌쩍 지났다. 지금 7주차 수업을 수강하고 있으니 거진 두 달이 다 되었다고 볼 수도 있을 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/49713438-0dbd-4d82-8af8-167794b5817c/image.jpg" alt=""></p>
<p>벌써 2023년이 끝나가다니 정말 믿기지가 않는다. 이렇게 한 살을 더 먹어가는구나...</p>
<h3 id="💫-한-달-동안-한-것들">💫 한 달 동안 한 것들</h3>
<hr>
<ol>
<li>수업 듣기
: 당연한 거지만... 수업은 열심히 잘 들었다. 아팠을 때 말고는 웬만하면 자정 지나고 + 오전 중에 다 들었던 것 같다. 본격적으로 백엔드로 API 만드는 걸 배웠는데 배우면서 백엔드 쪽이 나와 더 맞는 것 같다는 생각이 들었다. 분명 예전엔 흥미가 없었던 것 같은데 지금 다시 배우니까 생각보다 재밌어서... 이쪽으로 제대로 준비해볼까 한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/63a2320d-a0f9-4bd9-b6d5-f5dfe2bf8c16/image.png" alt=""></p>
<p>큰 프로젝트는 아니지만 DB도 연결해보고 유효성 검사도 해봤다. 재밌어서 혼자 이거저거 찾아보면서 공부했던 것 같다.</p>
<ol start="2">
<li>코딩테스트 스터디
: 팀원들과 함께 코딩테스트 스터디를 진행했다. 자바스크립트로 하는 코딩테스트인데 처음엔 잘 익숙하지 않아서 헤맸던 것 같다. 아직 1단계이긴 하지만 기본부터 천천히 하니까 좀 더 적응하기 쉬운 것 같다!
보통을 일주일에 5문제씩 풀고 발표는 사다리 타기를 돌려서 한 문제씩 맡아서 한다. 팀원들 모두 잘 풀어왔고 한 명도 빠지지 않고 발표도 했다!</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/5d5a681b-ef01-45e0-8842-46ddb2279e7f/image.png" alt=""></p>
<p>나름 체크 되어 있는 걸 보면 뿌듯하다.</p>
<ol start="3">
<li>복습 발표
: 데브코스에서는 매주 한 주간 배운 내용에 대해서 발표를 하는 복습 발표 시스템(?)이 있다. 우리 팀은 각자 하루씩 맡아서 발표를 하는데 다들 정말 준비를 잘 해온다. 나도 덩달아서 열심히 하게 되는 것 같다.
근데 후반부로 갈수록 프로젝트만 해서 어떤 걸 발표해야할지 조금 고심하긴 했다. 그래서 보통 수업을 들으면서 내가 따로 찾아본 것들을 공유했다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/393a48c9-9e7a-4c99-beba-d0c531c82229/image.png" alt=""></p>
<p>한 주도 빼먹지 않고 했다! 팀원들 모두 훌륭하다👍</p>
<ol start="4">
<li><p>멘토링
: 매주 한 시간 정도 멘토님과 함께 커피챗 시간을 가졌다. 현직에 계시는 분과 직접 소통을 하는 시간이어서 평소 알기 어려운 현직 관련 내용을 많이 알아갈 수 있었다.</p>
</li>
<li><p>Nodejs 교과서 공부
: 프로그래머스에서 지원해준 책 공부도 물론 했다. 근데 아직 완독은 못했고...😂 틈날 때마다 열심히 보고 있다. 찾아보니 책 저자인 제로초 님이 찍으신 유튜브도 있어서 책도 같이 읽으면서 함께 영상도 보고 있다.
영상 링크는 ↓
<a href="https://www.youtube.com/watch?v=JS4El_tz79M&amp;list=PLcqDmjxt30RsGIPBBKX7xl05VuqJeCTFn">Nodejs 교과서</a></p>
</li>
</ol>
<h3 id="❓-데브코스를-들으며-느낀-점">❓ 데브코스를 들으며 느낀 점</h3>
<hr>
<p>거의 두 달 가까이 데브코스를 들으며 느낀점을 짧게 적어볼까 한다.</p>
<ol>
<li><p>온라인 강의
: 사실 이건 장점이 될 수도 있고 단점이 될 수도 있지만... 난 개인적으로 만족하면서 듣고 있다. 물론 오프라인이 집중도 더 잘 되고 분위기 형성에 좋기는 하지만 사실 온라인으로 들으면 세이브할 수 있는 금액이 상당하다. 당장 식비도 절약되고 교통비도 절약할 수 있다. 그리고 내게 익숙한 환경에서 세팅된 로컬에서 작업할 수 있다는 것도 큰 장점이라고 생각한다.</p>
</li>
<li><p>활발한 팀 활동
: 위에도 말했듯 우리 팀은 주에 한 번 복습 발표, 멘토링 활동, 코딩테스트 스터디를 한다. 활발하게 서로가 하는 공부를 공유하고 스터디도 진행하면서 실력도 많이 쌓이는 것 같다. 나도 복습 발표 준비를 하면서 공부가 되기도 하고 말이다.</p>
</li>
<li><p>신기한 게더타운
: 이전 학교에서 졸업 프로젝트를 게더타운에 전시하기는 했는데 사실 그 때는 사용법도 잘 모르고 흥미도 없어서 대충 둘러보기만 하다가 나왔었다. 그런데 이번 데브코스에서 이제 진행을 게더타운으로 하게 됐는데... 굉장히 신기하다. 일단 멀어지면 소리도 함께 멀어지는 시스템도 그렇고 내가 캐릭터를 어느정도 커스터마이징 해서 돌아다닐 수 있는 것도 재밌다. 비록 캐릭터이긴 하지만 한 공간에 모여서 팀 활동을 하고 공지도 들으니까 뭔가 진짜 같이 수업을 듣는 느낌? 개인적으로 아주 만족하고 있다.</p>
</li>
<li><p>하지만 아쉬운 출석 시스템...
: 풀스택 데브코스는 실시간 수업이 있는 날은 QR로, 그 외의 날은 프로그래머스 사이트 내에서 LMS로 출석을 진행한다. 근데 이유를 모르겠는데 출석을 하면 자꾸 중복으로 창을 띄웠다는 경고창이 수시로 뜬다. 로그아웃을 하고 끄는 것도 아니고 애초에 이 컴퓨터로만 수업을 듣기 때문에 왜 뜨는지 알 수가 없다. 당연히 중복으로 창을 띄우지도 않았다. 그런 창이 뜨면 일단 나가서 몇 분 정도 뒤에 다시 들어오면 창이 사라지긴 하던데... 왜 뜨는지 여전히 이유는 잘 모르겠다.</p>
</li>
<li><p>그리고 역시 아쉬운 과제 테스트 시스템
: 프로그래머스에는 조금 특이하게 API 설계를 해서 테스트를 할 수 있는 시스템이 있다. vscode 환경도 구성되어 있어서 처음 접해보고 굉장히 신세계다!!! 라고 생각을 했지만...🤔
이번 과제 테스트를 진행하면서 알 수 없는 오류를 많이 겪었다. 분명 API 테스트를 했을 때는 정상적으로 돌아가는데 시험 채점만 하면 에러가 발생했다. 근데 그 에러가 또 상세하게 나오는 게 아니라 뭉뚱그려서 나오는데다가 앞의 문제가 틀렸다고 뒷부분까지 전부 틀렸다고 해서... 답안이라도 있으면 비교해서 뭘 틀렸는지 알 텐데 답도 따로 알려주지 않는다. 개인적으로 이 과제 테스트 뿐만 아니라 다른 테스트를 진행하더라도 답은 알려주면 좋을 것 같다. 어떤 걸 틀렸는지 알고 싶다.
다만 위 문제와 관련해서 내가 팀 슬랙에 푸념(?) 식으로 적은 게 있었는데 이걸 매니저님이 보시고!!! 이른 아침부터 연락을 주셨다. 매니저님께서 문제를 내신 건 아니어서 내가 겪은 에러의 원인은 알 수 없었지만... 그래도 아 정말 수시로 모니터링을 하시는구나 싶어서 감동을 받았다. 흑흑😭</p>
</li>
</ol>
<h3 id="❤️-잘한-점">❤️ 잘한 점</h3>
<hr>
<ol>
<li><p>강의를 밀리지 않고 들었다.
: 당연한 얘기지만 내가 취침 시간이 늦어서 보통 새벽에 듣고 오전~오후에 </p>
</li>
<li><p>어느정도 방향성을 정했다.
: 여기서 방향성이란 내가 프론트를 선택할지 백엔드를 선택할지에 대한 내용이다. 졸업까지 해놓고 부끄러운 이야기지만 난 아직까지 프론트엔드와 백엔드 중에서 확실하게 정하지 못했다. 그래서 풀스택을 공부하는 이 데브코스를 수강하게 된 것이고... 아직 프론트는 제대로 배우지 못했지만, 백엔드를 공부하다보니 이쪽이 나와 더 맞는 것 같은 느낌적인 느낌이 들었다. 물론 프론트는 내가 한 내용이 한눈에 보이기에 좀 더 가시적으로 결과물을 볼 수 있다는 장점이 있다. 개인적으로 매력적인 분야라고 생각하기도 한다. 하지만 뭔가... 뭔가 공부를 하면서 어, 이게 나랑 맞는 건가 하는 생각을 지우지 못했는데 백엔드를 처음부터 공부하다보니 이렇게 API를 설계하고 코드를 짜는 게 더 나와 맞을 것 같다고 생각하게 됐다. 본격적으로 이제 백엔드 쪽을 공부를 해볼까 한다!</p>
</li>
</ol>
<h3 id="💧-아쉬운-점">💧 아쉬운 점</h3>
<hr>
<ol>
<li><p>이번달의 TIL 포기...
: 정말 정말 가장 아쉬운 점이라고 할 수 있겠다. 중간에 바쁜 일이 생겨서 TIL을 며칠 놓친 적이 있는데 그 이후부터 결국 쭉 못 쓰게 됐다. 복습 발표를 준비하면서 TIL을 적은 것 빼고는 다른 날은 적지 못했다. 물론 따로 필기를 하기는 했지만 뭔가 잘 써야 한다는 강박 관념에 사로잡힌 건지 미루고 미루다가 결국 월말까지 오고 말았다...😭
아주 아쉽고 반성해야하는 부분이라고 생각한다. 다음달에는 정말 간단하게라도 쓰면서 밀리지 말고 써보자.</p>
</li>
<li><p>연휴로 조금 해이해졌다.
: 크리스마스 연휴가 껴서 그 전후로 많이 해이해지고 말았다. 다들 노는 분위기여서 나도 덩달아 휩쓸린 것 같다. 나는 온라인 수업을 하기 때문에 특히 휩쓸리지 않는 게 중요하다. 1월에는 복습 방학이 있는데 또 해이해지지 않게 정신 똑디 차리자!</p>
</li>
<li><p>여전히 바른 생활을 못 하고 있다.
: 내가 평균적으로 잠드는 시간은 새벽 세시 반 쯤이고 오전 아홉시 쯤 기상한다. 수면 시간이 짧은 편인데다가 내가 귀도 아주 밝아서 중간중간 자주 깨는데 상당히 피곤하다. 피곤함을 해결하려고 커피를 들이 붓다보니 또 밤에는 잠이 안 오고... 이러다보니 새벽에 수업을 들어서 다음날 조금 여유롭게 들을 수 있다는 장점이 있긴 한데 다음날 오전의 내 상태를 보면 딱히 장점도 아닌 것 같다. 그냥 일찍 자고 일찍 일어나서 수업 들으면 되는 건데!!! 그래서 최대한 두시 이전에 자려고 노력 중이다. 그래도 어제는 한시 반 쯤 잔 것 같다.</p>
</li>
</ol>
<h3 id="🍀-앞으로-공부할-것">🍀 앞으로 공부할 것</h3>
<hr>
<ol>
<li><p>백엔드 공부하기
: 본격적으로 백엔드에 집중해서 더 깊게 파보려고 한다. 지금 배우는 Nodejs 뿐만 아니라 이전에 잠깐 접했던 Nestjs를 공부해보려고 한다. 멘토님께서도 추천해주시기도 했고 Nestjs가 스프링과 상당히 유사한 패턴이어서 차후 스프링을 공부하게 될 때 더 편하게 공부할 수 있지 않을까 싶은 마음도 있다. 최근에 사용하는 스타트업도 점점 많아지고 있기도 하고.</p>
</li>
<li><p>타입스크립트 공부하기
: Nestjs가 타입스크립트 기반이기 때문에 타입스크립트 공부도 병행해야한다. 타입스크립트가 입문할 때는 조금 어색하지만 쓰다 보면 굉장히 편하기 때문에 그렇지 않아도 공부해야지 하고 생각하기는 했었다. 기왕 백엔드로 자리잡은 김에 열심히 공부해보자.</p>
</li>
<li><p>Nodejs 교과서 마저 공부하기
: 병행해서 하느라 사실 진도를 많이 나가지는 못했다. 내 목표는 1월 안에 해당 책을 전부 완독하는 것이다!</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 풀사이클 데브코스 TIL 6주차 DAY 5]]></title>
            <link>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-6%EC%A3%BC%EC%B0%A8-DAY-5</link>
            <guid>https://velog.io/@gangk_99/%EC%9B%B9-%ED%92%80%EC%82%AC%EC%9D%B4%ED%81%B4-%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-TIL-6%EC%A3%BC%EC%B0%A8-DAY-5</guid>
            <pubDate>Tue, 26 Dec 2023 06:34:49 GMT</pubDate>
            <description><![CDATA[<p><span style="color: gray">프로그래머스 데브코스, 국비지원교육, 코딩부트캠프</span></p>
<h2 id="🚩-프로그래머스-데브코스-웹-풀사이클-과정-6주차-day-5">🚩 프로그래머스 데브코스 웹 풀사이클 과정 6주차 DAY 5</h2>
<hr>
<p>그냥... 하루종일 프로젝트를 만들었다. 그래서 솔직하게 말하자면 정말 쓸 게 없었다😂 만든 프로젝트를 올리기에는 사실 딱히 하나하나 설명을 하기에도 조금 애매해서...</p>
<p>그래서 내 선택은!
express의 유효성 검증 모듈인 express-validator에 대해서 설명을 해볼까 한다.</p>
<h3 id="✨express-validator">✨express-validator</h3>
<hr>
<p>먼저 express-validator란 무엇인가!</p>
<blockquote>
<p>서버로 들어오는 요청에 대한 유효성을 쉽게 체크할 수 있도록 도와주는 유효성 검증 모듈</p>
</blockquote>
<p>이 유효성 검증 모듈을 사용하기 이전에는 우리는 if문으로 데이터 유효성 검사를 진행했다. 단순히 데이터가 비었는지 확인할 때야 if문 하나로 사용해도 괜찮겠지만, 프로젝트가 점점 커지고 체크할 유효성이 많아진다면 if문이 중첩되고 또 늘어나게 될 것이다. 이렇게 되면 코드를 짤 때도, 코드를 읽을 때도 불편해진다.</p>
<p>바로 이런 우리의 불편을 해소해주는 모듈이다.</p>
<h4 id="💡설치하기">💡설치하기</h4>
<hr>
<p><a href="https://www.npmjs.com/package/express-validator">npmjs</a></p>
<pre><code>npm install express-validator</code></pre><p>설치는 아주 간단하다. 늘 그렇듯 npm으로 설치해주면 끝이다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/af519c02-6edc-4577-9ab3-681492b6d199/image.png" alt=""></p>
<p>설치를 해주면 위처럼 <code>package.json</code>의 dependecies에 정상적으로 추가된 걸 확인할 수 있다. 우린 이제 express-validator를 사용할 수 있다!</p>
<h4 id="💡유효성-검증을-해보자">💡유효성 검증을 해보자!</h4>
<hr>
<pre><code class="language-javascript">const { body, param, validationResult } = require(&#39;express-validator&#39;);</code></pre>
<p>먼저 require을 통해 <code>express-validator</code> 모듈을 가져와준다.</p>
<p>그리고 이제 유효성 검증을 해볼 시간이다!
express-validator에는 유효성 검증을 할 수 있는 다양한 내장 함수를 지원한다. 모든 걸 전부 다루기에는 이 포스트가 영원히 끝나지 않을 것 같기 때문에... 가장 자주 쓰이는 몇 가지만 골라서 설명해보고자 한다.</p>
<ol start="0">
<li><code>withMessage()</code>
: 우리는 <code>withMessage()</code>를 사용하여 에러 메시지를 커스텀 할 수 있다. 유효성 검증에서 통과하지 못했을 때 해당 함수를 사용해서 출력될 에러 메시지를 커스텀하자.</li>
</ol>
<p><a href="https://express-validator.github.io/docs/6.13.0/custom-error-messages/">Custom Error Message</a></p>
<ol>
<li><code>notEmpty()</code>
: 값이 비었는지 확인</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/a106f5b9-55b2-4529-8851-2afa3bb3a333/image.png" alt=""></p>
<p>정말 자주 사용하는 유효성 검증이 아닐까 싶다. 말 그대로 입력한 값이 없는지 확인해준다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/eb4b893d-b893-4f21-a4a9-560275fcf05f/image.png" alt=""></p>
<p>이메일 부분의 값을 비우고 값을 보내자 위 사진처럼 이메일 값이 비었다는 에러 메시지가 함께 출력된다.</p>
<ol start="2">
<li><code>isEmail()</code>
: 회원가입을 할 때나 이메일로 로그인을 할 때 주로 사용한다. 말 그대로 입력값이 이메일 형식인지 확인해주는 함수다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/ceb004e5-2ba0-4eef-8aad-6efd6d516614/image.png" alt=""></p>
<p>바디로 email을 넘겼지만 유효성 검사에서 통과하지 못해 에러가 발생한 걸 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/89a88112-b471-4bf9-846b-8be4a99a1b51/image.png" alt=""></p>
<p>위처럼 이메일 형식으로 제대로 보냈을 때는 200 OK가 뜨는 걸 확인할 수 있다.</p>
<ol start="3">
<li><code>isLength()</code>
: 회원가입을 할 때 보통 최소 몇 자 이상, 최대 몇 자 이하로 설정하는 경우가 있다. 이런 경우에 특히 자주 사용한다.</li>
</ol>
<pre><code class="language-javascript">body(&#39;password&#39;).isLength({
    min: 8,
    max: 16
}).withMessage(`password parameter length is invalid`)</code></pre>
<p>위와 같이 최소값과 최댓값을 설정해주면 들어오는 입력값을 알아서 검증해준다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/f5b264ee-b253-412e-afae-e761866b95bd/image.png" alt=""></p>
<p>최소값인 8자 이하로 비밀번호를 입력하자 유효성 검사에서 통과하지 못했다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/4b4dd409-c7dc-4834-b19c-0224b8438f00/image.png" alt=""></p>
<p>8자 이상 16자 이하로 비밀번호를 입력하자 이번에는 유효성 검사에서 통과 후 200 OK가 정상적으로 반환됐다!</p>
<ol start="4">
<li><code>matches()</code>
: 정규표현식을 활용하여 문자열 혹은 숫자가 입력값에 존재하는지 확인할 때 주로 사용</li>
</ol>
<pre><code class="language-javascript">// 영어 소문자 반드시 하나 이상 포함
body(&#39;password&#39;).matches(/^.*[a-z].*$/).withMessage(`password parameter is invalid`)</code></pre>
<p>솔직하게 말하자면 나는 정규식을 잘 외우지 못해서...😂 항상 필요할 때마다 찾아보면서 한다. 특히 비밀번호 정규식 같은 경우는 우리의 빛 스택오버플로에 정말 많기 때문에 참고해서 사용하면 아주 좋을 것 같다👍</p>
<p><a href="https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a">비밀번호 정규식</a></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/461625c0-698b-4a71-bd4b-e3ba7bbf371a/image.png" alt=""></p>
<p>비밀번호 최소, 최대 길이는 충족했지만 영어 소문자가 포함되지 않았기 때문에 에러 메시지가 발생한 걸 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/230de379-e3e3-401b-9ef3-72453f30cbeb/image.png" alt=""></p>
<p>영어 소문자를 넣어서 다시 POST를 하면 정상적으로 200 OK가 리턴되는 걸 볼 수 있다.</p>
<ol start="5">
<li><code>trim</code>
: 문자열 양쪽의 공백을 제거해준다. <strong>중요!!!</strong> 문자열 사이의 공백을 제거하는 게 아니다. 문자열 양쪽의 공백을 제거해주는 거다. 아마 이메일이나 패스워드를 입력할 때 종종 앞에 공백이 입력될 때를 방지하기 위해서 있는 게 아닐까 싶다.</li>
</ol>
<pre><code class="language-javascript">body(&#39;name&#39;)
            .trim()
            .notEmpty()
            .withMessage(`space must not be included`)
            .isLength({
                min: 3,
                max: 5,
            })
            .withMessage(`name parameter is invalid`)</code></pre>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/36024c32-5a62-4c32-9928-cb18ac1d66e7/image.png" alt=""></p>
<p>이런 식으로 앞부분에 입력된 공백을 제거해준다. 나는 최소 길이를 3으로 잡았기 때문에 앞부분의 공백을 모두 제거한 후 두 글자로만 나머지 유효성 검사를 진행해준다.
실제 value를 확인해보면 앞부분 공백이 제거된 값으로 전달된 걸 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/aa12a676-14e1-4593-9460-b5db6b7a4b81/image.png" alt=""></p>
<p>공백까지 포함되면 최대 5글자를 넘는데도 유효성 검사에 통과한 걸 볼 수 있다. 공백이 제거돼서 그렇다.</p>
<ol start="6">
<li><code>isDate()</code>
: 입력한 문자열이 날짜 형식인지 확인해준다.</li>
</ol>
<pre><code>check if the string is a valid date. e.g. [2002-07-15, new Date()].

options is an object which can contain the keys format, strictMode and/or delimiters.

format is a string and defaults to YYYY/MM/DD.

strictMode is a boolean and defaults to false. If strictMode is set to true, the validator will reject strings different from format.

delimiters is an array of allowed date delimiters and defaults to [&#39;/&#39;, &#39;-&#39;].</code></pre><p>공식 문서? 깃허브에는 위와 같이 설명이 되어있다.
간단하게 설명하자면 날짜는 YYYY/MM/DD 혹은 YYYY-MM-DD 식으로 입력할 수 있다. </p>
<pre><code class="language-javascript">body(&#39;date&#39;)
            .notEmpty()
            .withMessage(`date parameter is empty`)
            .isDate()
            .withMessage(`date parameter is invalid`)</code></pre>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/801e7a54-a1fd-402a-b973-c2823bc14498/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/0198f0b2-eb4c-41b8-b6b9-422971cd9ff0/image.png" alt=""></p>
<p>위처럼 날짜 구분 기호를 /나 -를 사용하면 정상적으로 데이터가 전송되는 걸 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/95f0b57c-e35f-445a-aa9b-22af58472b04/image.png" alt=""></p>
<p>하지만 허용된 데이터 입력 방식과 다른 방식으로 입력이 되면 에러가 발생하는 걸 볼 수 있다.</p>
<ol start="7">
<li><code>isURL()</code>
: 입력받은 데이터가 URL 형식인지 확인한다. 이 함수의 경우 커스텀 할 수 있는 옵션이 상당히 많다.</li>
</ol>
<pre><code>require_protocol - if set to true isURL will return false if protocol is not present in the URL.
require_valid_protocol - isURL will check if the URL&#39;s protocol is present in the protocols option.
protocols - valid protocols can be modified with this option.
require_host - if set to false isURL will not check if host is present in the URL.
require_port - if set to true isURL will check if port is present in the URL.
allow_protocol_relative_urls - if set to true protocol relative URLs will be allowed.
allow_fragments - if set to false isURL will return false if fragments are present.
allow_query_components - if set to false isURL will return false if query components are present.
validate_length - if set to false isURL will skip string length validation (2083 characters is IE max URL length).</code></pre><p>깃허브 문서에는 위와 같이 나와 있다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/7974d58f-7324-4e33-bc03-d87db4ecd6b7/image.png" alt=""></p>
<p>정상적이지 않은 URL 경로를 입력하면 위와 같이 에러를 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/gangk_99/post/758de34b-6e34-456d-b42e-32cd9fdc7934/image.png" alt=""></p>
<p>이 외에도 정말 다양한 함수가 있는데, 궁금하다면 github의 마크다운 문서와 공식 깃허브 블로그를 참고해보자.  번역은 없는 것 같지만 해석하기에 어렵지는 않다. <del>(그리고 우리에게는 파파고가 있으니..)</del></p>
<p><a href="https://github.com/validatorjs/validator.js">github</a>
<a href="https://express-validator.github.io/docs">express-validator</a></p>
]]></description>
        </item>
    </channel>
</rss>