<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>chan_baek.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 개발자가 되는 그날까지</description>
        <lastBuildDate>Thu, 17 Jun 2021 14:28:12 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>chan_baek.log</title>
            <url>https://images.velog.io/images/chan_baek/profile/2583158b-c042-4c5d-8ee7-a519a0628a73/KakaoTalk_Photo_2021-06-10-14-59-41.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. chan_baek.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chan_baek" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[브랜디 인턴십 후기]]></title>
            <link>https://velog.io/@chan_baek/%EB%B8%8C%EB%9E%9C%EB%94%94-%EC%9D%B8%ED%84%B4%EC%8B%AD-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@chan_baek/%EB%B8%8C%EB%9E%9C%EB%94%94-%EC%9D%B8%ED%84%B4%EC%8B%AD-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 17 Jun 2021 14:28:12 GMT</pubDate>
            <description><![CDATA[<h2 id="브랜디를-소개합니다">브랜디를 소개합니다</h2>
<p>브랜디는 20대 여성을 위한 쇼핑앱이다. 여성쇼핑앱에 그치지 않고 남성쇼핑앱 &#39;하이버&#39;, 패션 창업인큐베이팅 서비스 &#39;헬피&#39;까지 서비스하며 사업 영역을 확장했다. 동대문 D2C(Direct to Consumer) 플랫폼 트랜디까지 동대문 패션 사업에 적극적으로 뛰어들고 있는 기업이다.
업계 최초로 100명 규모의 개발자 채용을 선언하는 등 빠른 성장을 이루고 있는 회사이다.</p>
<h2 id="프로젝트-정보">프로젝트 정보</h2>
<p>총 4명의 백엔드 개발자가 한 팀이 되어 1달간 프로젝트를 진행했다. Flask를 이용하여 &#39;Brandi&#39; 서비스&amp;어드민 페이지 구현했다. 프론트엔드 개발자는 참여하지 않았기 때문에 모든 프론트 구현은 팀장께서 혼자서 하셨다.</p>
<p>진행기간 : 2021.05.10 ~ 2021.06.03
팀원: 백승찬,김현영,서득영,이서진</p>
<h2 id="깃허브-주소">깃허브 주소</h2>
<p>서비스 페이지: <a href="https://github.com/poketsc/brandi-service">https://github.com/poketsc/brandi-service</a>
어드민 페이지: <a href="https://github.com/poketsc/brandi-admin">https://github.com/poketsc/brandi-admin</a></p>
<h2 id="기술스택">기술스택</h2>
<h3 id="framework">Framework</h3>
<ul>
<li>Flask(1.1.2)</li>
</ul>
<h3 id="language">Language</h3>
<ul>
<li>Python(3.8)</li>
</ul>
<h3 id="db">DB</h3>
<ul>
<li>Mysql</li>
<li>로컬 데이터베이스</li>
</ul>
<h3 id="erd">ERD</h3>
<ul>
<li>AqueryTool</li>
</ul>
<h3 id="communication">Communication</h3>
<ul>
<li>Github</li>
<li>Slack</li>
<li>Notion</li>
</ul>
<h3 id="통신">통신</h3>
<ul>
<li>Postman</li>
</ul>
<h2 id="우리팀이-구현한-기능">우리팀이 구현한 기능</h2>
<blockquote>
</blockquote>
<h3 id="서비스">서비스</h3>
<ul>
<li>로그인</li>
<li>메인페이지</li>
<li>상품상세</li>
<li>장바구니</li>
<li>배송지</li>
<li>주문</li>
<li>주문완료</li>
</ul>
<blockquote>
<h3 id="어드민">어드민</h3>
</blockquote>
<ul>
<li>로그인</li>
<li>셀러가입</li>
<li>회원 관리 &gt; 셀러 계정 관리 (마스터)</li>
<li>회원 관리 &gt; 셀러 정보 수정</li>
<li>상품 관리 &gt; 상품 등록</li>
<li>상품 관리 &gt; 상품 관리</li>
<li>상품 관리 &gt; 상품 관리 &gt; 판매, 진열 적용</li>
<li>주문 관리 &gt; 상품 준비 관리</li>
<li>주문 관리 &gt; 상품 준비 관리 &gt; 배송 처리</li>
<li>주문관리 &gt; 주문상세</li>
</ul>
<h2 id="내가-구현한-기능">내가 구현한 기능</h2>
<blockquote>
</blockquote>
<h3 id="서비스-1">서비스</h3>
<ul>
<li>상품상세</li>
<li>장바구니</li>
<li>배송지</li>
<li>주문</li>
<li>주문완료</li>
</ul>
<h2 id="진행방식">진행방식</h2>
<p>이번 인턴쉽 프로젝트에서도 다른 프로젝트때와 마찬가지로 Agile(애자일)의 대표 관리 Practice인 Scrum(스크럼) 방법으로 프로젝트를 진행하였다. Scrum은 특정 개발 언어나 방법론에 의존적이지 않으며, 제품 개발 뿐만 아니라 일반적인 프로젝트 관리에도 사용 가능한 프로세스 프레임워크이다. Scrum은 작은 주기(Sprint)로 개발 및 검토를 하며 효율적인 협업 방법을 제공한다. 정보처리기사 공부를 할때 Waterfall 방식과 애자일 방식을 배웠던 기억이 난다. 이번 인턴쉽 프로젝트가 내가 한 다른 프로젝트와 달랐던 점은 백엔드개발자만 있었다는 점이다. 프론트엔드 기능은 전부다 구현이 되어있었기 때문에 우리가 API를 모두 프론트와 통신할수 있었다. 우리 팀은 매일 아침 9시 30분에 만나서 15분~20분간 회의를 진행하였다. 프로젝트를 시작하고 매주 월요일에 중간점검을 가졌다. 우리가 처음 생각했던 기능들을 다 구현할수 있을지 확인하기 위해서였다.</p>
<h2 id="1주차">1주차</h2>
<p>우리는 모두 Django 프레임워크만 사용해본 개발자들이였다. 우리는 Flask에 대한 이해가 부족했고 Django와는 다르게 Flask는 아키텍처 패턴부터 전부다 구현해야 했다. 득영님이 Flask의 초기세팅을 담당했다. 득영님이 Flask 초기세팅을 혼자서 했기 때문에 고생을 많이 했다. 서진님이 Postman을 활용해서 프론트와 어떻게 통신해서 결과값을 어떻게 주고 받을지에 대해 문서작업을 했고 현영님과 나는 AqueryTool을 사용해서 데이터 모델링을 했다. 이번 데이터 모델링에서 다른 프로젝트와 달랐던 점은 이력관리를 고려했다는 점이다. 우리는 점이력과 선분이력 중에 어떤 방식으로 이력관리를 할지 고민했고 선분이력을 선택했다. 이번에 점 이력이 아닌 선분이력을 선택한 이유는 시작 시점과 종료 시점을 관리함으로써 과거 특정 시점의 데이터 조회를 손쉽게 할 수 있는 장점이 있기 때문이였다. 이번 데이터모델링은 실제 현업에서 사용하는 방법과 유사하게 구현했다. 이력관리로 인해 테이블의 개수가 2배는 늘어난것 같다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/c03f9412-cfd0-4891-bcb2-851ba3c62f94/brandi_20210614_34_23.png" alt=""></p>
<p>우리가 구현한 데이터모델링이다. 1주차에 완성한 모델링은 아니다. 기능을 구현하면서 많은 컬럼 추가와 관계 수정이 있었다. 우리는 처음으로 역정규화를 사용했는데 역정규화(denormalization)는 정규화된 데이터베이스에서 성능을 개선하기 위해 사용되는 전략이다. 일부 컬럼의 데이터를 중복 추가함으로써 데이터베이스의 읽기 성능을 개선하려는 의도였다. 팀장님께서 역정규화를 하는데 있어서 명확한 이유가 있어야 한다고 하셨다. </p>
<p><img src="https://images.velog.io/images/chan_baek/post/03729aa5-8e19-41f5-ae40-53f2a41bb5ad/image.png" alt=""></p>
<p>위에 테이블에서 address_histories 테이블에 있는 배송지주소, 배송지추가정보, 우편번호가 shipment_histories 테이블에도 있는것을 볼수있다. shipment_histories에도 똑같은 컬럼을 추가한 이유는 유저가 address 정보를 바꾸면 주문 했을 당시에 주소를 알수가 없어서 중복 추가 했다. 이 역시도 수정을 통해 완성된 모습이다. 한번에 완벽하게 데이터모델링을 하는것은 아직 불가능한것 같다. 많은 경험이 필요한것 같다.</p>
<p>AqueryTool 주소 : <a href="https://aquerytool.com:443/aquerymain/index/?rurl=4ee0ca00-2098-427d-b86d-cfad789e32c0">https://aquerytool.com:443/aquerymain/index/?rurl=4ee0ca00-2098-427d-b86d-cfad789e32c0</a>
password : 7vu8c5</p>
<h2 id="2주차">2주차</h2>
<p>Flask 초기세팅, API 문서작업, 데이터 모델링을 끝내고 우리는 API 기능 구현을 시작했다. 백앤드 api의 아키텍처에는 여러가지가 있을것이지만 우리는 가장 널리 사용되는 레이어드 아키텍처에 맞게 프로젝트를 3가지 section으로 나누었다.</p>
<blockquote>
<h3 id="view">view</h3>
<p>프로젝트에서 backend endpoint경로를 처리해주는 부분이다. 클라이언트에게서 온 request를 처리했다. db를 열고 닫는다.</p>
</blockquote>
<blockquote>
<h3 id="service">service</h3>
<p>api에서 로직을 담당하는 영역이다. 프론트로부터 받는 키값에 대한 에러처리를 제외한 대부분의 에러처리는 service영역에서 처리했다.</p>
</blockquote>
<blockquote>
<h3 id="model">model</h3>
<p>raw query를 사용해서 데이터베이스와 직접적으로 통신하는 영역이다.</p>
</blockquote>
<p>내가 처음으로 구현한 API 기능은 장바구니(POST) 였다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/27c3b69e-f1e3-4142-a8c5-783f2a480d0d/image.png" alt=""></p>
<p>view에서 먼저 프론트로부터 받은 데이터 형식이 올바른지 확인해주었다. 여러개의 상품이 한번에 담길수 있어서 for문으로 각각의 상품에 대한 형식을 체크했다. 그렇게 체크가 끝난후에 db를 열어주고 로직을 담당하는 post_cart 함수를 실행한다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/ec4c9888-9b6e-42b6-997b-e34402b54df2/image.png" alt="">
<img src="https://images.velog.io/images/chan_baek/post/d1bd2c8a-6957-4e5d-8f97-e9b6d6699782/image.png" alt=""></p>
<p>서비스 부분에서는 여러가지 경우의 수가 존재하기 때문에 if, else 문으로 경우의 맞게 처리하였다. 여기서 나는 하나의 dao 에서는 하나의 query문만을 쓰기 위해서 여러개의 dao를 만들어서 호출했다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/76fbfb32-6d0b-4845-8ef5-a72cd8f377b7/image.png" alt=""></p>
<p>이 코드는 dao에서 선분 이력을 관리하기 위한 코드이다. 장바구니에 같은 상품이 이미 존재하면 시간을 업데이트 하기 위해서 만든 쿼리문이다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/c9dd51c9-26b3-4285-82f2-7cec0bb91348/image.png" alt=""></p>
<p>이 코드는 시간을 업데이트 한 정보를 복사한 후 수정하기 위한 코드이다. 재사용성을 위해서 조건문을 사용했다. 어떻게 이력을 관리할까를 고민하다가 이렇게 두개의 dao를 통해서 이력관리를 하였다. 이 한개의 기능을 구현하는데 오랜 시간이 걸렸다. 처음 사용해본 레이어드 아키텍쳐였고 프로젝트에 raw query를 처음 사용해보았기 때문에 익숙하지 않았다.</p>
<h2 id="3주차">3주차</h2>
<p>3주차때는 2주차때와 다르게 속도가 많이 빨라졌다. 처음 접해본 Flask라서 이해하는데 시간이 좀 걸렸던것 같다. 프레임워크는 어려운것은 없다고 생각한다. 다만 익숙하지 않을뿐이다. 내가 구현한 기능들의 대부분은 3주차에 다 만든것 같다. 그리고 본격적으로 팀장님과 기능을 붙여보기 시작했다. 3주차에 제일 기억에 남는것은 Postman의 거짓말이였다! Postman에서 잘 작동되던 코드가 프론트와 통신할때는 제대로 작동되지 않았다.
<img src="https://images.velog.io/images/chan_baek/post/d8e559c0-9cfe-4e3c-ac35-ef6bb02e3b08/image.png" alt=""></p>
<p>이 코드는 2주차때 수정하기 전 처음 만든 코드이다. where절에 ch.end_time = %(now)s 로 설정했을때 Postman에서는 문제가 없이 잘 돌아갔지만 프론트와 통신하는 과정에서 여러번의 요청이 들어왔을때 같은 시간으로 입력되서 데이터가 생성되는것을 알았다. 이론상으로는 같은 시간이 찍힐수가 없다고 생각했지만 문제가 발생했다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/d286781e-27a1-4735-beb0-6b0ef573bc72/image.png" alt=""></p>
<p>문제를 해결하기 위해서 ch.id = %(cart_history_id)s를 넣는 방법을 택했다. primary Key를 이용하기 때문에 훨씬 안정적인 방법이였다. 이렇게 수정한 후에는 원하는대로 정상적으로 작동했다. 지금까지 Postman에서 잘 작동된 기능들은 프론트와 통신할때 문제를 발생시키지 않았다. 하지만 이번 경험을 통해서 100% 신뢰할 수 없다는 것을 깨달았다.</p>
<h2 id="4주차">4주차</h2>
<p>4주차에는 내가 구현한 모든 기능들을 프론트와 통신하고 주석을 다는데 시간을 썼다. 내가 그동안 했던 프로젝트에서는 주석을 사용하지 않았지만 이번 프로젝트에서는 각 코드가 어떤 의미를 갖는지, 어떤 데이터 값을 받고, 어떤 데이터를 결과값으로 리턴하는지 설명했다. 주석을 사용하면 코드는 깔끔해보이지는 않지만 어떤 의미로 기능을 구현했는지, 호출한 함수는 어떤 함수인지를 시간이 지난 후에도 좀더 쉽게 이해할수 있었다. 마지막날 내가 인턴쉽을 통해 배우고 구현한 기능들을 ppt로 만들어서 발표했다. ppt를 만드는데 시간을 좀더 투자했다면 더 멋진 발표가 되었을텐데 시간이 부족했다. 그 부분에서 많은 아쉬움이 남는다. 발표를 하면서 기억에 남는 질문은 왜 이력을 관리할때 END_DATE = &#39;9999-12-31 23:59:59&#39; 로 설정했는지 물어보셨다. 일반적으로 &#39;9999-12-31 23:59:59&#39;가 아닌 Null로 처리를 한다고 말씀해주셨다.</p>
<h2 id="느낀점">느낀점</h2>
<p>내가 했던 마켓컬리, 여기어때 프로젝트는 2주짜리 짧은 프로젝트였지만 이번 프로젝트는 가장 길었던 1달짜리 프로젝트였다. 이전 프로젝트 경험으로 이번 프로젝트에서는 완성도 높은 결과물을 기대했다. 하지만 raw query, flask, 선분이력으로 인해서 새롭게 공부해야되는 내용들이 너무 많았다. 데이터모델링 과 flask 초기세팅에 많은 시간이 들어가면서 실제로 기능을 구현하고 프론트와 통신하는 시간이 부족했다. 결국에는 프레젠테이션을 준비하는 시간도 부족했다. 어떤 새로운 문제가 발생했을때 고민하는데 시간을 너무 많이 투자하면 그만큼 완성도가 떨어질 수 있겠다라는 생각을 했다. 우리가 해야하는 프로젝트는 시간이 정해져있었다. 시간을 효율적으로 분배하고 너무 많은 고민보다는 빠른 판단력이 필요했던 것 같다. 기업협업을 통해서 많이 성장한 것 같다. 앞으로는 효율적인 sql 작성을 위해서 책을 읽으면서 공부할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[여기어때 프로젝트 후기]]></title>
            <link>https://velog.io/@chan_baek/%EC%97%AC%EA%B8%B0%EC%96%B4%EB%95%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@chan_baek/%EC%97%AC%EA%B8%B0%EC%96%B4%EB%95%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 17 Jun 2021 13:14:26 GMT</pubDate>
            <description><![CDATA[<h1 id="프로젝트-소개">프로젝트 소개</h1>
<p>종합숙박 O2O서비스 &#39;여기어때&#39;를 모티브로 한 프로젝트</p>
<p>개발 기간 : 2021.04.26 ~ 2021.05.07
팀 명 : 야, 여기어때(BetterChoice)
팀 인원  : 6명
프론트엔드 : 김효진, 박단비, 박성은
백엔드 : 백승찬, 신지원, 황수민</p>
<p>백엔드 깃허브 주소 : <a href="https://github.com/poketsc/19-2nd-betterChoice-backend">https://github.com/poketsc/19-2nd-betterChoice-backend</a></p>
<h2 id="기술스택">기술스택</h2>
<blockquote>
</blockquote>
<h3 id="framework">Framework</h3>
<ul>
<li>Django<h3 id="language">Language</h3>
</li>
<li>Python<h3 id="db">DB</h3>
</li>
<li>Mysql</li>
<li>RDS<h3 id="communication">Communication</h3>
</li>
<li>Github</li>
<li>Slack</li>
<li>Trello</li>
<li>Notion<h3 id="배포-및-통신">배포 및 통신</h3>
</li>
<li>EC2</li>
<li>Docker</li>
<li>Postman</li>
<li>httpie</li>
<li>JSON<h3 id="erd">ERD</h3>
</li>
<li>AqueryTool</li>
</ul>
<h2 id="trello-와-notion을-이용한-의사소통">Trello 와 Notion을 이용한 의사소통</h2>
<h2 id="trello">Trello</h2>
<p><img src="https://images.velog.io/images/chan_baek/post/9d83bce6-58f2-48d9-a50e-aa632832d8b4/image.png" alt=""></p>
<h2 id="notion">Notion</h2>
<p><img src="https://images.velog.io/images/chan_baek/post/caf16f6a-3a14-4094-96db-e2784d48a28a/image.png" alt=""></p>
<h2 id="erd-1">ERD</h2>
<p><img src="https://images.velog.io/images/chan_baek/post/1ac4dcc6-cd5c-4950-b8d0-ace86ae1c5f0/betterchoice_20210614_23_47.png" alt=""></p>
<p>URL : <a href="https://aquerytool.com:443/aquerymain/index/?rurl=9f3b4eae-ec1e-475c-b6aa-ccc73c62ed70">https://aquerytool.com:443/aquerymain/index/?rurl=9f3b4eae-ec1e-475c-b6aa-ccc73c62ed70</a>
password : i706x8</p>
<h2 id="우리팀이-구현한-endpoint">우리팀이 구현한 Endpoint</h2>
<p><img src="https://images.velog.io/images/chan_baek/post/65f307fa-a262-4f86-80f8-a16dc01df54f/image.png" alt=""></p>
<h2 id="내가-한-일">내가 한 일</h2>
<blockquote>
</blockquote>
<ul>
<li>회원가입 시 핸드폰 인증(네이버 클라우드) 기능 구현</li>
<li>회원가입 기능 구현 (bcrypt를 이용한 비밀번호 해싱 후 저장)</li>
<li>로그인 기능 구현 (jwt를 활용하여 인증 기능 구현)</li>
<li>AWS(S3)를 이용한 사진 업로드 기능 구현</li>
<li>모든 구현 기능 unittest 완료</li>
<li>Docker 와 AWS(EC2)를 활용하여 배포</li>
</ul>
<h2 id="이번-프로젝트-목표">이번 프로젝트 목표</h2>
<p>이번 프로젝트에서는 마켓컬리 프로젝트와는 다르게 많은 기능보다는 적은 기능이지만 소셜 기능과 unittest를 해보는것을 목표로 했다. 마켓컬리 프로젝트에서는 간단한 기능을 많이 해봤다면 이번 여기어때 프로젝트에서는 난이도가 조금 있는 기능들을 구현하려고 했다. 그래서 데이터모델링도 마켓컬리 프로젝트보다 훨씬 간단하게 구현했다. 마켓컬리 프로젝트와 똑같이 2주 프로젝트지만 unittest를 하기위해서 최대한 빠르게 데이터모델링을 끝내려고 했다.</p>
<h2 id="재밌었던-api-기능">재밌었던 api 기능</h2>
<p>마켓컬리 프로젝트에서는 email로 비밀번호 찾기를 구현했었는데 핸드폰인증 기능을 구현해보면 재밌을것같아 구현해본 기능이다. 핸드폰 인증으로 네이버 클라우드를 선택한 이유는 가격이 저렴했기 때문이였고 100000만 포인트를 지급받아서 프로젝트를 하는데 있어서 사실상 무료로 사용할수 있었기 때문이다. </p>
<p><img src="https://images.velog.io/images/chan_baek/post/4c0c2fe4-8ec0-4603-91a3-f49868e58969/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/chan_baek/post/feabb80c-893b-4148-a2da-bd7b59ec144e/image.png" alt=""></p>
<p>핸드폰 번호를 입력한후 핸드폰 인증버튼을 누르면 utils.py 에 직접만든 auth_number() 함수를 실행시켜 4자리의 난수를 받는다. update_or_create 메서드로 같은 핸드폰번호로 이미 데이터를 생성한적이 있으면
난수를 수정하고 데이터를 생성한적이 없으면 핸드폰번호와 생성된 난수로 인스턴스를 생성한다. 그리고 utils.py에 만들어진 send_sms() 함수를 호출하고 인자값으로 phone_number, 난수값을 입력한다. send_sms() 함수에서 URL,ACCESS_KEY,FROM_PHONE_NUMBER 등은 보안을 위해서 my_settings.py에 저장시킨후 불러왔다. make_signature() 와 send_sms() 함수는 네이버 클라우드에서 제공하는 api를 사용한것인데 그대로 사용하면 작동이 안된다. uri도 수정해야했고 몇번의 시행착오 끝에 실행 가능한 코드를 구현할수 있었다. 인증번호를 사용자에게 보낸후 사용자가 확인후에 인증번호를 입력하면 데이터베이스에 저장된 정보와 확인후에 일치하면 성공 메세지를 프론트에게 전달한다. 처음으로 구현해본 소셜 기능이였고 핸드폰으로 인증번호가 왔을때는 정말 신기하고 이게 진짜 되는구나라는 생각을 했다.</p>
<h2 id="또다른-재밌었던-api-기능">또다른 재밌었던 api 기능</h2>
<p>마켓컬리 프로젝트에서는 사진업로드가 되지않는 글만 입력할수 있게 review api를 만들었었다. 이번 프로젝트에서는 사진업로드가 가능한 api를 구현해 보고 싶어 AWS(S3)를 사용해 보았다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/aad7196b-ccf7-4784-aa20-1bd2f2c3ab2a/image.png" alt=""></p>
<p>S3_CLIENT 에는 aws_access_key_id, aws_secret_access_key 등의 정보가 들어가 있는데 보안을 위해서 my_setting.py에 저장한 후 불러왔다. AWS_S3_BUCKET_NAME도 보안을 위해서 my_setting.py에 저장한 후 불러왔다. 사진을 한장이 아닌 여러장을 올릴수 있게 하기 위해서 리스트로 받은 값을 for문으로 처리해 하나하나 S3에 업로드 시켰다. S3에 업로드 후에 데이터베이스에 업로드된 이미지 url을 저장하기 위해서 url이 만들어지는 패턴을 이용해서 f-string으로 url을 만든후에 데이터베이스에 저장한다. f&#39;https://{my_settings.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com/{file.name}&#39; 이때, 데이터베이스 hit을 최소화하고 속도를 향상 시키기 위해서 bulk_create를 사용했다.</p>
<h2 id="나를-힘들게-했던-unittest">나를 힘들게 했던 unittest!!!</h2>
<p><img src="https://images.velog.io/images/chan_baek/post/dbfe0de6-afe7-47d9-b7d7-e889524f00fb/image.png" alt=""></p>
<p>처음 해본 unittest지만 외부 api가 필요없는 기능에서는 unittest가 어렵지 않았다. 하지만 외부 api를 사용해야하는 기능에서는 mocking 기법을 사용해야했다. 그 이유는 운영 환경 대비 제약이 많은 테스트 환경에서는 실제 데이터베이스와 연동하거나 실제 외부 API를 호출하기가 불가능한 경우가 많기때문이다. 가령 가능하더라도, 이렇게 외부 서비스에 의존하는 테스트는 해당 서비스에 문제가 있을 경우 깨질 수 있으며 실행 속도도 느릴 수 밖에 없다. mocking 기법을 사용하지 않으면 test인데 실제로 파일이 s3가 올라가거나 핸드폰인증에서 실제로 메세지가 전송된다. 따라서 단위 테스트를 작성할 때 외부에 의존하는 부분을 임의의 가짜로 대체하는 기법이 자주 사용되는데 이를 mocking 이라고 한다. 다시 말해, 모킹(mocking)은 외부 서비스에 의존하지 않고 독립적으로 실행이 가능한 단위 테스트를 작성하기 위해서 사용되는 테스팅 기법이다. 특히 s3를 이용해야하는 기능에서는 mocking 기법도 사용해야하고 multipart form data로 데이터를 받아야해서 unittest 하는데 가장 어려웠던 부분이였다. Multipart form data로 값을 전달받는 과정에서 정말 많은 시행착오가 있었다. 위 코드는 이렇게 많은 시행착오 끝에 만들어진 unittest중에 여러개의 사진을 올렸을때 성공하는 테스트이다.</p>
<h2 id="느낀점">느낀점</h2>
<p>BetterChoice의 PM으로써 부족한점이 많았지만 팀원들의 밝은 에너지가 있어서 잘 마무리 할수 있었던것 같다. 팀원들을 많이 도와드리고 싶었지만 나도 처음해보는 기능들이여서 절대적인 시간투자가 필요했다. 특히 수민님, 질문에 대한 제대로된 답변을 드리지 못해 죄송해요!!! 2차 프로젝트라서 더 많이하고 잘할수 있었을것만 같았는데 역시 새로 해야되는 추가사항들이 있었다. unittest,docker,외부 api.... 어려웠지만 기능들이 마켓컬리 프로젝트에 비해 훨씬 재미었다. 특히 핸드폰인증 성공했을때 자랑하려고 사람들에게 핸드폰번호 물어봤었던 기억이 있다. 개발을 하면서 불가능한것은 없다고 느낀다. 다만 익숙하지 않을뿐이라고 생각한다. 이번 프로젝트를 통해서도 많이 성장한것 같아서 뿌듯하다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[마켓컬리 클론 프로젝트]]></title>
            <link>https://velog.io/@chan_baek/%EB%A7%88%EC%BC%93%EC%BB%AC%EB%A6%AC-%ED%81%B4%EB%A1%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@chan_baek/%EB%A7%88%EC%BC%93%EC%BB%AC%EB%A6%AC-%ED%81%B4%EB%A1%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Sun, 25 Apr 2021 12:52:59 GMT</pubDate>
            <description><![CDATA[<h1 id="마켓컬리-클론코딩-프로젝트">마켓컬리 클론코딩 프로젝트</h1>
<p><img src="https://images.velog.io/images/chan_baek/post/a3efe263-9bc2-4c72-a56e-a3a710c003c7/main.gif" alt=""></p>
<p><img src="https://images.velog.io/images/chan_baek/post/cc7aa081-6d57-40db-8394-b45fe7bbdd57/Chokurly%201.png" alt=""></p>
<p>프론트엔드 깃헙 주소
<a href="https://github.com/wecode-bootcamp-korea/19-1st-Market-ChoKurly-frontend">https://github.com/wecode-bootcamp-korea/19-1st-Market-ChoKurly-frontend</a></p>
<p>백엔드 깃헙 주소
<a href="https://github.com/wecode-bootcamp-korea/19-1st-Market-ChoKurly-backend">https://github.com/wecode-bootcamp-korea/19-1st-Market-ChoKurly-backend</a></p>
<p>진행기간 : 2021.04.12 ~ 2021.04.23
팀명 : MarketChokurly
팀원 : 프론트엔드 : 이예원,서동이,채준형 
      백엔드 : 백승찬,안정현,김영훈</p>
<h2 id="기술스택">기술스택</h2>
<blockquote>
</blockquote>
<h3 id="framework">Framework</h3>
<ul>
<li>Django<h3 id="language">Language</h3>
</li>
<li>Python<h3 id="db">DB</h3>
</li>
<li>Mysql</li>
<li>RDS</li>
<li>AqueryTool</li>
<li>CSV<h3 id="communication">Communication</h3>
</li>
<li>Github</li>
<li>Slack</li>
<li>Trello</li>
<li>Notion<h3 id="배포-및-통신">배포 및 통신</h3>
</li>
<li>EC2</li>
<li>Postman</li>
<li>httpie</li>
<li>JSON</li>
</ul>
<h2 id="백엔드-팀이-구현한-기능">백엔드 팀이 구현한 기능</h2>
<blockquote>
<ol>
<li>users</li>
</ol>
</blockquote>
<ul>
<li>회원가입</li>
<li>아이디 찾기</li>
<li>비밀번호 찾기</li>
<li>로그인</li>
<li>리뷰</li>
<li>좋아요</li>
</ul>
<ol start="2">
<li>products</li>
</ol>
<ul>
<li>카테고리</li>
<li>상품리스트</li>
<li>상품상세정보</li>
<li>상품검색</li>
</ul>
<ol start="3">
<li>orders</li>
</ol>
<ul>
<li>주문하기</li>
<li>주문상세정보</li>
<li>장바구니</li>
</ul>
<h2 id="내가-구현한-기능">내가 구현한 기능</h2>
<blockquote>
<ul>
<li>아이디 찾기</li>
</ul>
</blockquote>
<ul>
<li>비밀번호 찾기</li>
<li>리뷰</li>
<li>좋아요</li>
<li>카테고리</li>
<li>상품리스트</li>
<li>상품검색</li>
</ul>
<h2 id="진행방식">진행방식</h2>
<p>Agile(애자일)의 대표 관리 Practice인 Scrum(스크럼) 방법으로 프로젝트를 진행하였다. Scrum은 특정 개발 언어나 방법론에 의존적이지 않으며, 제품 개발 뿐만 아니라 일반적인 프로젝트 관리에도 사용 가능한 프로세스 프레임워크이다. Scrum은 작은 주기(Sprint)로 개발 및 검토를 하며 효율적인 협업 방법을 제공한다. 정보처리기사 공부를 할때 Waterfall 방식과 애자일 방식을 배웠던 기억이 난다. 이론적으로는 어떤 차이가 있는지는 알고 있었지만 이번에 프로젝트를 하면서 애자일의 방식인 Scrum 방식을 사용함으로써 개발자들이 어떤 환경에서 어떤 방식으로 일을 하는지 느낄 수 있었던 값진 시간이였다. 우리 팀은 매일 아침 11시 10분에 만나서 15분~20분간 회의를 진행하였다. 프로젝트를 시작하고 1주일뒤에 중간점검을 가졌다. 2주차에 더 나은 진행방식을 위해 1주일간 진행된 프로젝트 결과물을 보면서 지난 1주일을 되돌아보는 시간을 가졌다.</p>
<h2 id="중간점검을-통해-발견된-django의-이해부족">중간점검을 통해 발견된 Django의 이해부족</h2>
<p>내게는 개발자로서 첫번째 프로젝트였던 만큼 얼마나 빠른속도로 좋은 결과물을 만들어 내는가 보다는 팀원끼리의 코드리뷰가 더 중요하다고 생각했었다. 우리는 모델링을 통해 3개의 앱으로 기능을 나누었다. users, products, orders 앱들로 구성된 프로젝트를 3명의 백엔드 멤버들이 어떻게 나누어서 진행을 할까를 고민하다가 서로 코드를 리뷰 해줄수 있는 방식으로 진행하기 위해 하나의 앱에서 뷰를 나누어 작성하는 방식으로 진행하였다. 각각의 앱을 한개씩 맡는 방법도 생각했지만 서로 한번도 해보지않은 기능에 대해서 팀원이 질문하면 대답을 못해줄것 같았다. 그때는 View는 기능 단위로 짜야한다는 말을 제대로 이해하지 못했던 시간이였다. 1주일이 지나고 중간점검을 통해서 내가 View에 대한 이해가 부족했었다는것을 깨달았다. 프론트엔드 멤버들과 매일 소통하면서 나도 모르게 내 머릿속에 기능 단위가 아닌 페이지 단위로 기능을 구현하려고 했었다. 아이디찾기, 비밀번호 찾기 기능을 구현하는 순간에도 내 머릿속에는 아이디 찾기 페이지, 비밀번호 찾기 페이지를 떠올렸던것 같다.</p>
<h2 id="기능-단위를-이해한-후의-나의-모습">기능 단위를 이해한 후의 나의 모습</h2>
<p>난 기능을 하고 있는 View를 작성하고 있다고 생각 했지만 중간점검을 통해 멘토님에게 자신있게 보여주었던 View의 이름이 MainPageView였다. 그전에 내가 만들었던 View는 아이디 찾기와 비밀번호 찾기 였는데 이 두개의 View에서는 문제가 되지 않았는데 그럴수 있었던 이유가 이 두 개는 페이지가 달랐기 때문이였다. 만약 내가 기능이라는 말을 제대로 이해했다면 아이디 찾기와 비밀번호 찾기를 만든 후에 MainPageView라는 View를 만들지 않았을 것이다. 주말을 이용해서 열심히 만든 내 코드가 어떠한 기능을 위한것이 아닌 메인 페이지를 구현하기 위해서 만들어진 코드였다. 다른 웹사이트에서는 모르겠지만 마켓컬리 사이트의 경우 메인페이지를 위한 코드는 상품을 보여주는 View 하나면 가능했다. 이것을 깨닫고 난 후에 팀원들과 하나의 앱을 나누어서 같이 개발하자고 생각했던것이 효율성이 떨어진다는것을 깨달았다. 처음 만들어보는 app에서 우리가 처음부터 정확하게 기능을 나누는것도 어떻게 보면 어려운 일이였다. 예를 들어, 하나의 View를 만들면서 다른사람이 만들고 있는 View도 구현 할 수 있는 경우가 생겼다. 그래서 중간점검이 있고난 후에는 app별로 나누어서 개발하려고 했다.</p>
<h2 id="내가-발전할-수-있었던-코드">내가 발전할 수 있었던 코드</h2>
<pre><code class="language-python">class MainPageView(View):
    def get(self,request):
        products = Product.objects.order_by(&#39;?&#39;)[:21]
        this_products = []
        for product in products:
            this_products.append(
                {
                    &quot;id&quot;    : product.id,
                    &quot;name&quot;  : product.name,
                    &quot;price&quot; : product.price,
                    &quot;discount_rate&quot; : product.discount_rate.discount_rate,
                    &quot;discounted_price&quot; : product.price - (product.price * product.discount_rate.discount_rate),
                    &quot;thumbnail_image&quot;: product.thumbnail_image,
                    &quot;sticker&quot;:product.sticker.name
                }
            )
        # this_products

        high_discount_products = Product.objects.order_by(&#39;-discount_rate&#39;)[:7]
        affordable_products = []
        for high_discount_product in high_discount_products:
            affordable_products.append(
                {
                    &quot;id&quot; : high_discount_product.id,
                    &quot;name&quot; : high_discount_product.name,
                    &quot;price&quot; : high_discount_product.price,
                    &quot;discount_rate&quot; : high_discount_product.discount_rate.discount_rate,
                    &quot;discounted_price&quot; : high_discount_product.price - (high_discount_product.discount_rate.discount_rate * high_discount_product.price),
                    &quot;thumbnail_image&quot; : high_discount_product.thumbnail_image,
                    &quot;sticker&quot;: high_discount_product.sticker.name
                }
            )

        # affordable_products


        sub_category = SubCategory.objects.get(pk=2)
        product_sets = sub_category.product_set.all()
        product_sets = product_sets.order_by(&#39;?&#39;)[:7]
        md_recommends = []
        for product_set in product_sets:
            md_recommends.append(
                {
                    &quot;id&quot; : product_set.id,
                    &quot;name&quot; : product_set.name,
                    &quot;price&quot; : product_set.price,
                    &quot;discount_rate&quot; : product_set.discount_rate.discount_rate,
                    &quot;discounted_price&quot; : product_set.price - (product_set.price * product_set.discount_rate.discount_rate),
                    &quot;thumbnail_image&quot; : product_set.thumbnail_image,
                    &quot;sticker&quot;: product_set.sticker.name
                }
            )

        # md_recommends


        products = Product.objects.order_by(&#39;created_at&#39;)[:id]
        today_new_products = []
        for product in products:
            today_new_products.append(
                {
                    &quot;id&quot; : product.id,
                    &quot;name&quot; : product.name,
                    &quot;price&quot; : product.price,
                    &quot;discount_rate&quot; : product.discount_rate.discount_rate,
                    &quot;discounted_price&quot; : product.price - (product.price * product.discount_rate.discount_rate),
                    &quot;thumbnail_image&quot; : product.thumbnail_image,
                    &quot;sticker&quot;: product.sticker.name
                }
            )

        # today_new_products

        products = Product.objects.order_by(&#39;-stock&#39;)[:7]
        hot_products = []
        for product in products:
            hot_products.append(
                {
                    &quot;id&quot;   : product.id,
                    &quot;name&quot; : product.name,
                    &quot;price&quot; : product.price,
                    &quot;discount_rate&quot; : product.discount_rate.discount_rate,
                    &quot;discounted_price&quot; : product.price - (product.price * product.discount_rate.discount_rate),
                    &quot;thumbnail_image&quot; : product.thumbnail_image,
                    &quot;sticker&quot;: product.sticker.name
                }
            )

        # hot_products

        products = Product.objects.order_by(&#39;price&#39;)[:7]
        lowest_price_products = []
        for product in products:
            lowest_price_products.append(
                {
                    &quot;id&quot;   : product.id,
                    &quot;name&quot; : product.name,
                    &quot;price&quot; : product.price,
                    &quot;discount_rate&quot; : product.discount_rate.discount_rate,
                    &quot;discounted_price&quot; : product.price - (product.price * product.discount_rate.discount_rate),
                    &quot;thumbnail_image&quot; : product.thumbnail_image,
                    &quot;sticker&quot;: product.sticker.name
                }
            )

        # lowest_price_products

        return JsonResponse({&#39;this_products&#39;:this_products, &#39;affordable_products&#39;:affordable_products,
        &#39;md_recommends&#39;:md_recommends, &#39;today_new_products&#39;:today_new_products, &#39;hot_products&#39;:hot_products,
        &#39;lowest_price_products&#39;:lowest_price_products}, status=200)
</code></pre>
<p>내가 주말에 열심히 만들었던 MainPageView이다. 총 6개의 기능을 하는 View인데 신상품,최저가 등등을 메인페이지에서 보여주기 위해서 만든 코드이다. 이렇게 만들고 나서 메인페이지 뷰가 있을 필요가 없다는 것을 깨달았다. 사실 6개의 기능은 한개의 로직으로 구성된 뷰에서 전부 처리가 가능했다.</p>
<pre><code class="language-python">class ProductListView(View):
    def get(self,request):
        category_id     = request.GET.get(&#39;category_id&#39;, None)
        sub_category_id = request.GET.get(&#39;sub_category_id&#39;, None)
        order_by_type   = request.GET.get(&#39;order_by_type&#39;, None)
        page            = int(request.GET.get(&#39;page&#39;, 1))
        limit           = int(request.GET.get(&#39;limit&#39;, 8))
        start           = (page - 1) * limit
        end             = page * limit

        if not category_id and not sub_category_id:
            products = Product.objects.order_by(order_by_type)[start:end]
        if category_id:
            products = Product.objects.filter(category_id=category_id).order_by(order_by_type)[start:end]
        if sub_category_id:
            products = Product.objects.filter(sub_category_id=sub_category_id).order_by(order_by_type)[start:end]

        results = [{

            &quot;id&quot;: product.id,
            &quot;name&quot;: product.name,
            &quot;original_price&quot;: int(product.price),
            &quot;discount_rate&quot;: float(product.discount_rate.discount_rate) if product.discount_rate else None,
            &quot;discounted_price&quot;: int(product.price - (product.price * product.discount_rate.discount_rate)) if product.discount_rate else None,
            &quot;thumbnail_image&quot;: product.thumbnail_image,
            &quot;sticker&quot;: product.sticker.name if product.sticker else None,
            &quot;comment&quot;: product.productinformation.comment if category_id or sub_category_id else None

        } for product in products]

        return JsonResponse({&#39;RESULTS&#39;:results}, status=200)</code></pre>
<p>이 코드는 쿼리 파라미터를 이용해서 원하는 정렬 방식, 원하는 상품의 갯수, 원하는 페이지 상품 등을 프론트엔드로 부터 받아서 원하는 데이터를 보내주는 코드이다. ProductListView를 통해서 어느 페이지에 있는 상품 리스트든 전부 다 이 View 하나로 처리가 가능해졌다. MainPageView라는 이름으로 View를 만든것이 View는 기능단위라는 것을 제대로 인지하지 못했었다는것을 깨닫게 해주는 순간이였다. 그리고 쿼리,패스 파라미터가 얼마나 개발의 속도를 빠르게 만들어주는지도 깨닫는 순간이였다. 만약 쿼리,패스 파라미터가 없었다면 얼마나 많은 View를 만들어야하고 얼마나 많은 중복된 코드가 존재할지 생각만해도 끔찍하다.</p>
<pre><code class="language-python">class UserLikeView(View):
    @login_required
    def get(self,request):
        product_id = request.GET.get(&#39;product_id&#39;)

        if not product_id:
            return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_PRODUCT_ID&#39;}, status=400)

        user       = request.user
        user_check = UserLike.objects.filter(user=user, product=product_id).exists()
        like_count = UserLike.objects.filter(product=product_id).count()
        product    = Product.objects.get(id=product_id)

        if not user_check:
            user.product.add(product)
            user.save()
            like_count += 1
            return JsonResponse({&#39;RESULTS&#39;:like_count}, status=200)

        UserLike.objects.filter(user=user, product=product).delete()
        like_count -= 1
        return JsonResponse({&#39;RESULTS&#39;:like_count}, status=200)</code></pre>
<p>마지막에 내가 구현한 좋아요 기능이다. UserLike인데 Product와 User 테이블이 다대다 관계를 형성하면서 만들어진 중간 테이블인데 처음에 내가 구현할때는 좋아요를 누르고 취소하고를 반복할때 데이터를 추가하고 삭제하고를 반복하면 될거라고 생각했다. 하지만 데이터를 추가하고 삭제하는것을 반복하면 그만큼 DB관리에는 효율적이지 못하다는것을 알게 되었다. 또 한번 모델링 수정이 필요한 순간이였다.</p>
<pre><code class="language-python">class UserLikeView(View):
    @login_required
    def get(self,request):
        product_id = request.GET.get(&#39;product_id&#39;)

        if not product_id:
            return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_PRODUCT_ID&#39;}, status=400)

        user = request.user
        like, like_check = UserLike.objects.get_or_create(user=user,product=product_id)

        if not like_check:
            if like.is_like:
                like.is_like = False
            else:
                like.is_like = True

        like.save()
        total = UserLike.objects.filter(product=product_id, is_like=True).count()

        return JsonResponse({&#39;RESULTS&#39;: total}, status=200)</code></pre>
<p>데이터를 추가하고 반복하고를 하지않고 좋아요 기능을 구현한 코드이다. 우선 Product와 User의 중간 테이블인 UserLike 테이블에 is_like라는 BooleanField를 추가했다. 좋아요를 누르면 True 좋아요를 취소하면 False로 처리하기 위해서였다. 이렇게되면 데이터를 추가하고 삭제하고를 하지않고 좋아요 기능을 구현할 수 있다. get_or_create 메소드를 처음으로 사용해본 코드였다. 인스턴스가 생성된 적이 없으면 True를 반환하고, 생성된 적이 없으면 False를 반환한다.</p>
<h2 id="백엔드도-보여줄것이-있었던-코드">백엔드도 보여줄것이 있었던 코드</h2>
<pre><code class="language-python">class FindPasswordView(View):
    def random_choices(self):
        length      = 8
        string_pool = string.ascii_letters + string.digits
        auth_num    = &#39;&#39;

        for i in range(length):
            auth_num += random.choice(string_pool)

        return auth_num

    def post(self, request):
        data     = json.loads(request.body)
        auth_num = self.random_choices()

        try:
            name = data[&#39;name&#39;]
            identification = data[&#39;identification&#39;]
            email = data[&#39;email&#39;]

            if not User.objects.filter(Q(name=name) &amp; Q(identification=identification) &amp; Q(email=email)).exists():
                return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_USER&#39;}, status=400)

            salt = bcrypt.gensalt()
            hashed_password = bcrypt.hashpw(auth_num.encode(&#39;utf-8&#39;), salt)
            hashed_password = hashed_password.decode(&#39;utf-8&#39;)

            user = User.objects.get(email=email)
            user.password = hashed_password
            user.save()

            email = EmailMessage(&#39;[CHOKURLY] 이메일 인증번호&#39;, auth_num, to=[email])
            email.send()

            return JsonResponse({&#39;MESSAGE&#39;:&#39;SUCCESS&#39;}, status=200)

        except KeyError:
            return JsonResponse({&#39;MESSAGE&#39;:&#39;KEY_ERROR&#39;}, status=400)</code></pre>
<p>이 코드는 개인적으로 재밌었던 코드이다. 프론트엔드에 비해서 보여줄것이 없는 백엔드 입장에서는 흥미로웠던 코드였다. 사용자가 비밀번호를 찾을때 사용자 이메일로 직접 인증번호를 보내는 코드인데 진짜 이메일로 내가 원하는 제목과 내용으로 보내진다. 발표할때 많은 사람들이 신기해 했던 기능이다. 몇가지 셋팅이 필요한 코드이다. my.settings.py 에서 필요한 설정을 입력한 후에 gmail 계정의 보안등급을 낮춰야 사용이 가능했다. 2차 프로젝트때는 핸드폰 인증하는법을 구현해 봐야겠다.</p>
<h2 id="파이썬의-간결함">파이썬의 간결함</h2>
<p>나는 java,c의 문법을 공부한 적이 있다. 파이썬으로 알고리즘 문제를 풀면서 파이썬이 간결한 언어임은 깨닫고 있었지만 이번 프로젝트를 하면서 더 많이 느낀것 같다. 특히 이번 프로젝트에서 list comprehension을 많이 사용하면서 왜 list comprehension을 파이썬의 꽃이라고 하는지 알 것 같았다.</p>
<pre><code class="language-python">
results = [{

            &quot;id&quot;: product.id,
            &quot;name&quot;: product.name,
            &quot;original_price&quot;: int(product.price),
            &quot;discount_rate&quot;: float(product.discount_rate.discount_rate) if product.discount_rate else None,
            &quot;discounted_price&quot;: int(product.price - (product.price * product.discount_rate.discount_rate)) if product.discount_rate else None,
            &quot;thumbnail_image&quot;: product.thumbnail_image,
            &quot;sticker&quot;: product.sticker.name if product.sticker else None,
            &quot;comment&quot;: product.productinformation.comment if category_id or sub_category_id else None

        } for product in products]

        return JsonResponse({&#39;RESULTS&#39;:results}, status=200)
</code></pre>
<p>상품 리스트를 list comprehension으로 구현한 코드이다. results 리스트에 담아서 프론트에게 전달하는 코드인데 여기서 깨달은 점은 list compreshension으로 인해 중복코드를 줄일 수 있고 중첩 for문을 가독성 있게 표현 할 수 있다는 점이다. 중첩 for문을 쓰는것보다 list comprehension을 사용하면 어떤 데이터가 담기는지 한번에 알아보기 쉬워진다. 리스트 안에 딕셔너리를 담는 형태인데 만약 위의 코드처럼 딕셔너리 값에 조건문을 사용할 수 없다면 아마 중복 코드가 밑에 많이 생겨날 것이다. 파이썬의 간결함을 느꼈다.</p>
<h2 id="모델링의-중요성">모델링의 중요성</h2>
<p><img src="https://images.velog.io/images/chan_baek/post/f8aeee33-b0b7-4ec7-b622-ab56cefb7c03/fcfargo90@gmail.com_20210423_43_02.png" alt=""></p>
<p>프로젝트를 시작할때 모델링을 빠르게 끝내고 View 작성하는데 많은 시간을 투자 해야겠다고 생각했다. 하지만 내 생각과는 다르게 모델링은 빨리 끝나지 않았고 많은 수정을 해야만했다. 우리팀은 프로젝트가 끝나는 금요일 하루 전에도 모델링 수정을 해야만했다. 사실 이 과정들은 너무나도 당연했다. 경험이 쌓이면 모델링을 하면서 View 로직이 보인다고 말씀하셨던 멘토님이 떠오른다. 우리가 모델링을 할때 View에서 어떤 로직으로 구현할지에 대해 전혀 생각 하지 못했다. 그저 이것은 일대다, 다대다, 일대일 관계일 것이다 라는 관점만 생각했기 때문에 모델링은 완벽할 수 없었다. View를 만들면서 모델링이 잘못됬구나 라는것을 깨닫고 많은 수정을 하게 되었다. 아마 2차 프로젝트를 하면서는 모델링 수정이 1차 프로젝트보다는 많이 줄지 않을까 라는 생각을 해본다.</p>
<h2 id="프로젝트를-하면서-느낀점">프로젝트를 하면서 느낀점</h2>
<h3 id="과도한-목표-설정">과도한 목표 설정</h3>
<p>나는 이번 프로젝트에서 가장 중요하게 생각했던것이 많은 기능을 구현 하는 것이였다. 기능 구현에만 집중 하다보니 프론트와의 소통이 많이 이루어지지 않았다. 물론 매일같이 얘기를 나누고 노력한것은 사실이지만 기능을 구현했을때 빠르게 통신을 해보려고 하지 않았다. 그렇게 하다가 보니 프로젝트를 마무리 하는 과정, 즉 전체적인 기능을 확인해보는 상황에서 많은 시간이 지체되었고 급하게 마무리한 느낌이 들었다. 실제로 ppt로 발표한 내용보다 백엔드는 더 많은 기능이 구현되었지만 함께 프로젝트를 하면서 더 많은 기능을 구현하는것이 팀 프로젝트를 하면서 좋은 방향이였을까에 대한 의문이 남는다. 많은 기능 구현보다 프론트와 소통을 더 중요하게 생각했다면 조금 더 완성도 높은 결과물을 만들어냈을것 같다. 실제로 ppt 발표하는날에 배포를 하지 못해 local server로 발표를 진행하였다. 2차 프로젝트때는 기능 구현에 대한 욕심을 조금 줄이고 프론트와 더 많은 소통을 통해 완성도 높은 결과물을 만들어내려고 노력해야겠다.</p>
<h3 id="내가-맡은-일에만-집중한-것">내가 맡은 일에만 집중한 것</h3>
<p>팀 프로젝트 였지만 각자 맡은 부분이 달랐고 내가 맡은 일을 하기에도 어떻게보면 많이 버거웠었던것도 사실이지만 돌이켜서 생각해보면 나는 조금 더 시간이 남았던것 같다. 나와 맞춰봐야 하는 프론트엔드 예원님과 늦게까지 연락하면서 Visual Studio Code의 Live Share 를 통해서 통신한 결과 다른 팀원보다 일찍 구현이 완료 되었다. 그때 나는 추가 구현에 대한 욕심이 있었고 하나라도 더 해봐야지 라는 생각이 들었다. 그래서 더 많은 기능을 구현하기 위해서 혼자만 너무 앞서 나갔던것 같다. 그때 추가 구현을 하지 않고 팀원들의 부족한 점에 더 많은 관심을 기울이고 수정하는데 도움을 더 주었다면 마지막에 시간이 부족하지 않았을것 같다는 생각이 든다. 그렇게 했다면 발표 준비에도 더 많은 시간을 투자할 수 있었고 배포까지 해서 더 완성도 있게 프로젝트를 마무리 했을것 같다.</p>
<h3 id="같이-일하고-싶은-개발자">같이 일하고 싶은 개발자</h3>
<p>1차 프로젝트를 끝내고 지친 몸으로 세션을 들었을때 ppt 마지막 slide 내용이 같이 일하고 싶은 개발자였다. 많은 생각이 들게한 문장이였다. 사실 개발자는 팀으로 일을 하기 때문에 주니어 면접은 인성면접이라는 말이 나올 정도로 실력보다는 같이 일하고 싶은 사람이 채용에 큰 영향을 준다는 말을 들은적이 있다. 나는 그 말을 들었을때 나는 사람들하고 잘 지내기 때문에 아무런 문제 없을것 같다는 생각이 들었다. 하지만 프로젝트를 끝내고 &#39;같이 일하고 싶은 개발자&#39;라는 문장을 보았을때 나를 돌이켜보면서 팀원들이 나를 진짜 같이 일하고 싶은 개발자라는 말에 동의를 할까 라는 궁금증이 생겼다. 물론 싸운적이 없고 함께 즐겁게 프로젝트를 진행했다. 하지만 어떤 사람은 과정을 중시하고 어떤 사람은 결과를 중요하게 생각 할수도 있다. 멘토님이 우리팀을 어떤 목적으로 구성했을지는 모르겠지만 프로젝트를 진행하면서 내가 백엔드 팀을 이끌어야 하는 포지션이 되어버렸다. 내가 잘해서가 아니라 단순히 다른 팀원에 비해 조금 더 적극적이였고 질문이 많았으며 말을 많이 하는 성격탓에 그렇게 되어버린것 같다. 같이 일하고 싶은 개발자는 무엇일까? 재밌는 사람, 성격이 좋은사람, 다른사람의 의견을 받아들일수 있는 사람 등등 많은 이유가 있겠지만 내가 생각하는 같이 일하고 싶은 개발자는 같은 목표를 향해서 같은 열정으로 함께 만들어가는 것이라고 생각한다. 지난 2주를 돌아보면 정말 최선을 다했고 후회는 없다. 지금 당연히 조금 향상된 실력으로 2주 전으로 돌아간다면 더 잘할 수 있겠지만 그건 욕심인것같다. 같이 일하고 싶은 개발자가 되려면 다른사람들과 같은 보폭으로 나아가는것도 중요한것 같다. 그 전에는 단순하게 생각했던것 같다. 잘하는 사람하고 일하면 좋겠지 라는 생각을 했지만 내게 팀원 5명을 뽑아서 프로젝트를 진행하게 해준다면 난 단순히 잘하는 사람만을 뽑을것 같지는 않다. 앞으로 2차 프로젝트가 남았고 기업협업도 남았다. 진짜 함께 일하고 싶은 개발자는 어떤 사람일까에 대해서 더 많은 생각을 해보고 그런 사람이 되려고 더 많이 노력해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 8]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-8</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-8</guid>
            <pubDate>Thu, 08 Apr 2021 07:38:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/8b44ac13-7766-4f0d-a4dc-a4fa5612255d/image.png" alt=""></p>
<pre><code class="language-python">def top_k(nums, k):
  nums_number = list(set(nums))
  final_list = []
  count_dict = {}

  for i in nums_number:
    count_dict[i] = nums.count(i)

  dict_tuple = count_dict.items()
  sorted_dict_tuple = sorted(dict_tuple, reverse=True, key=lambda x:x[1])

  for i in range(k):
      final_list.append(sorted_dict_tuple[i][0])

  return final_list</code></pre>
<p>이 문제를 해석해보면 가장 많이 반복되는 숫자 순으로 k개가 리스트에 담겨져서 반환되어야 한다. 처음에는 리스트를 이용해서 구현하려고 했지만 잘 되지 않았다. 그래서 생각해 낸것이 딕셔너리이다. 일단, list(set(nums))으로 중복된 숫자를 제거하고 리스트에 담는다. 그리고 그 리스트에 담겨져 있는 숫자의 개수만큼 for문을 돌려서 딕셔너리 형태로 ex) {&#39;1&#39;:개수, &#39;2&#39;:개수} 만든다.
count_dict.items()를 사용하면 {(&#39;1&#39;:개수), (&#39;2&#39;:개수)} 이런식으로 만들어지는데 그렇게 되면 딕셔너리도 리스트처럼 index를 사용할 수 있게 된다. 그렇게 만들어진 딕셔너리를 sorted 메소드를 사용해서 value 값이 큰 순으로 정렬 시킨다. sorted(dict_tuple, reverse=True, key=lambda x:x[1]) -&gt; x:x[1] 의 의미가 value 값을 의미한다. x:x[0]으로 사용하면 key값을 의미하게 된다.</p>
<p>그렇게 정렬된 딕셔너리를 for문으로 k번 돌려서 key값을 차례로 뽑으면 된다.
이렇게 하기 위해서 value 값이 큰 순으로 정렬 시켰다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 7]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-7</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-7</guid>
            <pubDate>Thu, 08 Apr 2021 07:24:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/02f811c4-7e68-41dc-a572-58738acad342/image.png" alt=""></p>
<pre><code class="language-python">def is_valid(string):
    type_match = {&#39;(&#39;: &#39;)&#39;, &#39;[&#39;: &#39;]&#39;, &#39;{&#39;: &#39;}&#39;}
    type_check = [&#39;(&#39;, &#39;[&#39;, &#39;{&#39;]
    string_list = []
    string_final = []

    if len(string) &lt;= 1:
        return False

    if string[0] in type_check:
        for i in range(len(string)):
            if string[i] in type_check:
                string_list.append(string[i])
                string_final.append(string[i])
            else:
                if len(string_list) == 0:
                    return False
                else:
                    string_final.append(type_match[string_list[-1]])
                    string_list.pop()
    else:
        return False

    if len(string_list) != 0:
        for i in range(len(string_list)):
            string_final.append(string_list[i])


    return &#39;&#39;.join(string_final) == string</code></pre>
<p>이 문제를 읽었을때 많은 고민을 했다. s = &quot;(){}&quot; 와 같이 같은 타입이 반복되는 경우에는 쉽게 구현할 수 있지만 s = &quot;{[]}&quot; 와 같이 두개씩 비교를 할수 없는 경우로 인해 많이 힘들어진것 같다.</p>
<p>나는 이 문제를 풀기 위해서 어떤 값이 들어왔을때 올바른 값을 만들어서 올바르게 만들어진 값과 들어온 값을 비교해서 True or False를 리턴 시켰다.</p>
<p>예를들어, &quot;({{)&quot; 이 값이 들어온 경우, 올바른 값은 &quot;({{}})&quot; 이다 그래서 올바른 값을 나오게 구현하고 입력값과 비교해서 True or False를 반환하도록 구현한 코드이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 6]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-6</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-6</guid>
            <pubDate>Thu, 08 Apr 2021 04:59:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/a0e123d4-7f54-4bc1-8174-4a5b716bb3f6/image.png" alt=""></p>
<pre><code class="language-python">def more_than_half(nums):
  for i in range(len(nums)):
    if nums.count(nums[i]) &gt; (len(nums)/2):
      return nums[i]</code></pre>
<p>이 문제는 굉장히 간단했다. 리스트의 길이만큼 for문을 돌려주고 나오는 값들의 개수가 리스트 길이의 절반보다 큰지 묻고 크다면 바로 그 값을 리턴시키면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 5]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-5</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-5</guid>
            <pubDate>Thu, 08 Apr 2021 04:52:33 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/b64cdcaa-cb1e-47bd-b550-202e17ee446e/image.png" alt=""></p>
<pre><code class="language-python">def get_prefix(strs):
    answer = &#39;&#39;
    if strs == []:
        return answer

    check = min(strs)
    final = []

    if not check:
        return answer

    strs.sort(key=lambda x:len(x))

    for i in range(1, len(strs)):
        answer = &#39;&#39;
        for j in range(len(check)):
            if check[j] == strs[i][j]:
                answer += check[j]
            else:
                break
        final.append(answer)

    return min(final)</code></pre>
<p>문제를 봤을때 어렵지 않은 문제라고 생각했다. 글자를 한개씩 비교만 하면 된다고 생각 했는데 [] 일때와 [&#39;abc&#39;,&#39;abd&#39;,&#39;&#39;] 와 같은 경우도 같이 생각해야 했다. 앞부분에서 먼저 strs = [], strs = [&#39;abc&#39;,&#39;abcd&#39;,&#39;&#39;] 같은 경우를 먼저 판단해주고 그다음에 strs에 담긴 문자들을 짧은 길이 순으로 정렬 시킨다. 그리고 가장 길이가 짧은 문자길이 만큼 for 문을 for 문 안에 중첩으로 넣는다. 그렇게 하나씩 제일 짧은 길이의 문자랑 한개씩 비교하면서 같은 문자들을 final에 append 시킨다. 그렇게 len(strs) 만큼 반복시키고 난 후에 final 리스트에 담긴 문자들중 가장 짧은 길이의 문자를 리턴하면 된다.      </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 인스타그램 로그인 구현 (views.py)]]></title>
            <link>https://velog.io/@chan_baek/TIL-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-views.py</link>
            <guid>https://velog.io/@chan_baek/TIL-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-views.py</guid>
            <pubDate>Wed, 07 Apr 2021 12:42:39 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python">class LogInView(View):
    def post(self, request):
        data = json.loads(request.body)

        try:
            password = data[&#39;password&#39;]
            id       = data[&#39;id&#39;]
            user = &#39;&#39;

            if User.objects.filter(Q(email=id)|Q(phone_number=id)|Q(nickname=id)).exists():
                user = User.objects.get(Q(email=id)|Q(phone_number=id)|Q(nickname=id))
            if not user:
                return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_USER&#39;}, status=404)



     #두번째 구현 방식


        #     password = data[&#39;password&#39;]
        #     id = data[&#39;id&#39;]
        #     user = &#39;&#39;
        #
        #     if User.objects.filter(email=id).exists():
        #         user = User.objects.get(email=id)
        #     if User.objects.filter(nickname=id).exists():
        #         user = User.objects.get(nickname=id)
        #     if User.objects.filter(phone_number=id).exists():
        #         user = User.objects.get(phone_number=id)
        #     if not user:
        #         return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_USER&#39;}, status=404)

      # 첫번째 구현 방식


            # if &#39;email&#39; in data:
            #     email = data[&#39;email&#39;]
            #     if User.objects.filter(email=email).exists():
            #         user = User.objects.get(email=email)
            #     else:
            #         return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_USER&#39;}, status=401)
            #
            # if &#39;nickname&#39; in data:
            #     nickname=data[&#39;nickname&#39;]
            #     if User.objects.filter(nickname=nickname).exists():
            #         user = User.objects.get(nickname=nickname)
            #     else:
            #         return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_USER&#39;}, status=401)
            #
            # if &#39;phone_number&#39; in data:
            #     phone_number=data[&#39;phone_number&#39;]
            #     if User.objects.filter(phone_number=phone_number).exists():
            #         user = User.objects.get(phone_number=phone_number)
            #     else:
            #         return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_USER&#39;}, status=401)

            encode_password = user.password.encode(&#39;utf-8&#39;)
            checked_password = bcrypt.checkpw(password.encode(&#39;utf-8&#39;), encode_password)

            if not checked_password:
                return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_PASSWORD&#39;}, status=401)

            access_token = jwt.encode({&#39;id&#39;: user.id}, SECRET_KEY, algorithm=&#39;HS256&#39;)
            return JsonResponse({&#39;MESSAGE&#39;:&#39;SUCCESS&#39;}, status=200)

        except KeyError:
            return JsonResponse({&#39;MESSAGE&#39;:&#39;KEY_ERROR&#39;}, status=400)
</code></pre>
<p>인스타그램에서 로그인을 할때 email+password , nickname+password, phone_number+password 방식이 있다.</p>
<p>첫번째로 작성한 코드는 말 그대로 email+password, nickname+password, phone_number+password로 로그인 한다고 생각해서 email로 입력했을때, nickname을 입력했을때, phone_number를 입력했을때를 세가지 변수 각각 경우의 수를 따져서 코드를 구현했다. 작동은 되지만 비효율적인 코드라는것을 깨달았다.</p>
<p>그래서 생각한것이 두번째 방식인데, 생각해보면 사용자가 페이지에서 로그인을 할때 원하는값을 그냥 입력한다. nickname=&#39;sdafv&#39;, email=&#39;asdf@gmail.com&#39;, phone_number=&#39;010-1234-5689&#39; 이런식으로 나누어서 입력해서 로그인하지 않는다. id 입력칸에 원하는 타입의 id 값을 입력하고 password 칸에 password를 입력한다. 그래서 생각한것이 그냥 어떤 타입으로 입력을하든 id 라는 변수로 받는것이다. id 값으로 입력을 받고 그 id가 데이터베이스의 email or phone_number or nickname에 매칭되는것이 있는지 확인한 후 있으면 유저 정보를 가져오는 방식으로 구현했다. 이렇게 해도 코드는 첫번째 방식보다 훨씬 짧아졌다.</p>
<p>세번째 방식은 Q를 사용한것인데 SQL 문의 where 절과 의미가 비슷하다고 생각하면 된다. Q를 사용하면 여러 if 문으로 구현한 코드가 두번의 if 만으로도 구현이 가능해진다. 가독성이 좋고 간결한 코드로 구현할 수 있다.</p>
<p>그렇게 id가 데이터베이스에 매칭되는것이 있다면 password를 체크하게 되는데 우리가 회원가입을 할때 암호화 시킨 비밀번호를 decode해서 데이터베이스에 저장시켰다. 그래서 로그인 할때 비밀번호가 맞는지 확인하려면 데이터베이스에 decode된 비밀번호를 encode 시킨후에 입력된 비밀번호를 암호화시켜서 서로 똑같은지 비교한다. 비밀번호가 똑같다고 인정되면 &#39;SUCCESS&#39;를 반환한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 인스타그램 회원가입 구현(views.py)]]></title>
            <link>https://velog.io/@chan_baek/TIL-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84views.py</link>
            <guid>https://velog.io/@chan_baek/TIL-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84views.py</guid>
            <pubDate>Tue, 06 Apr 2021 12:30:39 GMT</pubDate>
            <description><![CDATA[<pre><code>import json
import re
import bcrypt
import jwt

from django.views       import View
from django.http        import JsonResponse

from user.models        import User
from westagram.settings import SECRET_KEY

class SignUpView(View):
    def post(self, request):
        data               = json.loads(request.body)
        email_check        = re.compile(&#39;[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$&#39;)
        phone_number_check = re.compile(&#39;^\d{3}-\d{3,4}-\d{4}$&#39;)
        password_check     = re.compile(&#39;^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&amp;])[A-Za-z\d$@$!%*#?&amp;]{8,}$&#39;)

        try:
            email        = data[&#39;email&#39;]
            phone_number = data[&#39;phone_number&#39;]
            password     = data[&#39;password&#39;]
            name         = data[&#39;name&#39;]
            nickname     = data[&#39;nickname&#39;]

            if User.objects.filter(email=email).exists():
                return JsonResponse({&#39;MESSAGE&#39;:&#39;EMAIL_ALREADY_EXIST&#39;}, status=400)

            if not email_check.match(email):
                return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_EMAIL&#39;}, status=400)

            if User.objects.filter(phone_number=phone_number).exists():
                return JsonResponse({&#39;MESSAGE&#39;:&#39;PHONE_NUMBER_ALREADY_EXIST&#39;}, status=400)

            if not phone_number_check.match(phone_number):
                return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_PHONE_NUMBER&#39;}, status=400)

            if User.objects.filter(nickname=nickname).exists():
                return JsonResponse({&#39;MESSAGE&#39;:&#39;NICKNAME_ALREADY_EXIST&#39;}, status=400)

            if not password_check.match(password):
                return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_PASSWORD&#39;}, status=400)

            salt = bcrypt.gensalt()
            hashed_password = bcrypt.hashpw(password.encode(&#39;utf-8&#39;), salt)
            hashed_password = hashed_password.decode(&#39;utf-8&#39;)

            User.objects.create(
                email        = data[&#39;email&#39;],
                phone_number = data[&#39;phone_number&#39;],
                password     = hashed_password,
                name         = name,
                nickname     = nickname
            )
            return JsonResponse({&#39;MESSAGE&#39;:&#39;SUCCESS&#39;}, status=200)

        except KeyError:
            return JsonResponse({&#39;MESSAGE&#39;:&#39;KEY_ERROR&#39;}, status=400)

</code></pre><p>http 요청을 받으면 views.py에서 요청을 처리하고 응답한다.</p>
<p>우선 회원가입을 위해서는 이메일 형식, 비밀번호 형식, 전화번호 형식이 웹사이트가 요구하는 형식이 맞는지 확인해야한다. 그래서 정규표현식을 사용해서 check 변수들을 만들었다. try 안에서 email,phone_number,password,name,nickname 변수들을 선언했으므로 하나라도 정보가 입력되지 않으면 KeyError가 발생한다.</p>
<p>모든 변수들을 다 입력했으면 그 다음부터 에러사항을 하나하나 체크해 나간다.</p>
<pre><code>if User.objects.filter(email=email).exists():
                return JsonResponse({&#39;MESSAGE&#39;:&#39;EMAIL_ALREADY_EXIST&#39;}, status=400)</code></pre><p>-&gt; 같은 이메일이 데이터 베이스에 이미 존재하면 이메일이 이미 존재한다는 에러 메세지를 보낸다. 여기서 exists()를 사용한 이유는 exists()는 데이터 베이스 안에 정보가 있는지 없는지만 체크하므로 데이터를 가져오지 않는다 (효율성이 좋다)</p>
<pre><code>if not email_check.match(email):
                return JsonResponse({&#39;MESSAGE&#39;:&#39;INVALID_EMAIL&#39;}, status=400)</code></pre><p>-&gt; 정규표현식을 사용해서 입력한 email이 올바른 형식으로 입력이 되었는지 확인하고 잘못된 형식으로 입력되었으면 에러메세지를 보낸다.</p>
<p>다른 if문들은 전부다 비슷한 의미를 나타내므로 따로 언급하지 않겠다.</p>
<p>이렇게 if 문으로 에러메세지를 보내는데 중복된 email,nickname,phone_number가 없고 원하는 타입 형식으로 제대로 입력이 되었으면 비밀번호 암호화를 시행한다.</p>
<p>bcrypt는 데이터베이스에 유저의 정보를 저장할 때, 비밀번호와 같이 암호화가 필요한 데이터를 쉽게 다룰 수 있도록 해주는 password hashing 라이브러리다.</p>
<p>먼저, 암호화를 위해서는 bcrypt를 설치해야하는데</p>
<p>pip install bcrypt로 설치한후 import bcrypt 한다.</p>
<pre><code>salt = bcrypt.gensalt()</code></pre><p>salt를 추가함으로써 암호를 더 복잡하게 만들어준다.
쉽게 말해, 해커들이 해킹하기 더 어렵게 만들어준다.</p>
<pre><code>hashed_password = bcrypt.hashpw(password.encode(&#39;utf-8&#39;), salt)
</code></pre><p>이렇게 hashpw를 사용해서 salt와 함께 encode 시키게 되면 단방향 암호화가 이루어지는데 단방향 암호화란 내가 입력한 비밀번호를 되찾을수 없다. 다시말해, 비밀번호를 까먹으면 그 비밀번호를 찾을수 없다는 뜻이다.</p>
<p>예를들어, 내가 비밀번호를 &#39;1234&#39;로 설정한다면 저 코드로 인해 비밀번호가 데이터베이스에 &#39;1234&#39;로 저장되지 않고</p>
<p>b&#39;$2b$12$4n5NY.agqiRfh.eNlcIJEuFif2gY.m0jIgK4HbwZpJO.FS5U8.vg.&#39; 이렇게 암호화된 비밀번호로 저장된다. 그래서 회원가입할때 유저가 비밀번호를 &#39;1234&#39;를 입력했다는것을 알 수 없다.</p>
<pre><code>hashed_password = hashed_password.decode(&#39;utf-8&#39;)</code></pre><p>encode로 인해서 비밀번호 타입이 &#39;bytes&#39;가 되었는데 데이터베이스에 저장할때는 decode를 한후 &#39;str&#39;타입으로 바꾸고 저장한다.</p>
<p>이렇게 암호화를 시킨후 </p>
<pre><code>User.objects.create(
                email        = data[&#39;email&#39;],
                phone_number = data[&#39;phone_number&#39;],
                password     = hashed_password,
                name         = name,
                nickname     = nickname
            )
            return JsonResponse({&#39;MESSAGE&#39;:&#39;SUCCESS&#39;}, status=200)</code></pre><p>create문으로 회원가입을 완료한다.</p>
<p>회원가입이 잘 되었다면 {&#39;MESSAGE&#39;:&#39;SUCCESS&#39;} 메세지를 리턴하도록 구현했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Django 인스타그램 회원가입 구현 (models.py)]]></title>
            <link>https://velog.io/@chan_baek/TIL-Django-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-models.py</link>
            <guid>https://velog.io/@chan_baek/TIL-Django-%EC%9D%B8%EC%8A%A4%ED%83%80%EA%B7%B8%EB%9E%A8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84-models.py</guid>
            <pubDate>Tue, 06 Apr 2021 10:45:53 GMT</pubDate>
            <description><![CDATA[<pre><code>from django.db import models

class User(models.Model):
    email          = models.CharField(max_length=50)
    phone_number   = models.CharField(max_length=50)
    password       = models.CharField(max_length=500)
    name           = models.CharField(max_length=50)
    nickname       = models.CharField(max_length=50)

    class Meta:
        db_table = &#39;users&#39;</code></pre><p>project 폴더에 user app을 만들어 models.py 에 User 클래스를 만들었다. email,phone_number,password,name,nickname 을 컬럼으로 만들었다. password는 비밀번호 암호화를 위해서 max_length를 500으로 잡았다. (처음에는 암호화를 고려하지 않아서 max_length=45로 했는데 그렇게 되면 암호화를 시키면 길이가 너무 길어서 에러가 발생했다.)</p>
<pre><code>   class Meta:
        db_table = &#39;users&#39;</code></pre><p>이 코드는 데이터베이스에 만들어지는 테이블 이름을 지정할 수 있게 만들어준다.
나는 &#39;users&#39;로 만들었다. 클래스 Meta를 사용하지 않으면 데이터베이스 테이블 이름을 내 마음대로 정할 수 없다. 일반적으로 class 명의 소문자 형태 + 복수형으로 만든다. 예) User -&gt; users</p>
<p>나는 http 요청으로 회원가입을 하려고한다. 그러기 위해서는 일단
brew install httpie 명령어로 httpie를 설치해야한다.</p>
<p>httpie를 설치하면 http POST localhost 형식으로 명령어를 보낼 수 있는데 이 명령어를 수행하려면 views.py를 작성해야한다. 다음 TIL 에서 자세하게 다룰것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 모의고사]]></title>
            <link>https://velog.io/@chan_baek/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC</link>
            <guid>https://velog.io/@chan_baek/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC</guid>
            <pubDate>Thu, 01 Apr 2021 15:23:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/2b126b4d-b2c8-4785-82c5-7c807b4a0767/image.png" alt="">
<img src="https://images.velog.io/images/chan_baek/post/1c92301c-1e29-4ecf-a993-7b4fed33d18d/image.png" alt=""></p>
<pre><code>def solution(answers):
    answers = [str(i) for i in answers]
    answer = &#39;&#39;.join(answers)
    a1 = [1,2,3,4,5]
    a2 = [2, 1, 2, 3, 2, 4, 2, 5]
    a3 = [3, 3, 1, 1, 2, 2, 4, 4, 5, 5]
    counta1 = 0
    counta2 = 0
    counta3 = 0
    totcount = []

    for i in range(len(answers)):
        if int(answer[i]) == a1[i%5]:
            counta1 += 1
        if int(answer[i]) == a2[i%8]:
            counta2 += 1
        if int(answer[i]) == a3[i%10]:
            counta3 += 1

    totcount.append(counta1)
    totcount.append(counta2)
    totcount.append(counta3)

    final = []

    for i in range(3):
        if totcount[i] == max(totcount):
            final.append(i+1)

    return final
</code></pre><p>이 문제는 효율성을 별로 중요하게 생각하지 않는거같다. 효율적으로 풀지 못한것 같았는데 정답으로 인정되었다. answers의 길이가 바뀔수 있으니 i%5, i%8, i%10 이렇게 길이만큼 나눈 나머지를 계산했다. 밑에 final를 리턴하는 for문은 좀더 효율적인 코드가 되도록 생각을 더 해봐야 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 4]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-4</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-4</guid>
            <pubDate>Thu, 01 Apr 2021 15:13:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/a4d1e95d-72da-4cf8-9be4-5e8ef6b62ef1/image.png" alt=""></p>
<pre><code>def same_reverse(num):

    answer = True
    for i in range(len(str(num))):
        if str(num)[i] != str(num)[-1-i]:
            answer = False

    return answer
</code></pre><p>숫자로 받은 num을 str 타입으로 바꾸고 길이 만큼 for문을 돌린다.
처음과 끝 글자가 같지 않으면 answer 에 False를 넣어준다. i 가 1씩 증가하면서 비교를 반복한다.
한번이라도 같지 않는 경우가 생기면 answer는 False 를 리턴한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 3]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-3</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-3</guid>
            <pubDate>Wed, 31 Mar 2021 11:44:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/9277f775-e8ff-407d-b2dd-a6bf95577a53/image.png" alt=""></p>
<pre><code>def get_len_of_str(s):
    word = []
    answer = []
    a = &#39;&#39;
    for i in range(len(s)):
        if i == 0:
            word.append(s[i])
            a += s[i]
        elif s[i] != word[-1]:
            word.append(s[i])
            a += s[i]
        elif s[i] == word[-1]:
            answer.append(a)
            word.append(s[i])
            a = &#39;&#39;
            a += s[i]
    answer.append(a)

    b = len(answer[0])
    for i in range(1,len(answer)):
        if len(answer[i]) &gt; b:
            b = len(answer[i])

    return b
</code></pre><p>이 코드는 내가 잘못푼 코드이다
나는 같은 문자가 연속해서 나오는것만 안되는줄 알았다.
하지만 한 문자열안에 같은 문자가 있으면 안되는 문제였다.
그렇게 되면 내가 처음에 생각했던 코드보다 비교적 쉽게 문제를 해결할 수 있다.</p>
<pre><code>def get_len_of_str(s):
    answer = []
    a = &#39;&#39;
    for i in s:
        if i not in a:
            a += i
        else:
            answer.append(a)
            a = &#39;&#39;
            a += i
    answer.append(a)

    b = len(answer[0])
    for i in range(len(answer)):
        if len(answer[i]) &gt; b:
            b = len(answer[i])

    return b</code></pre><p>이것은 정답으로 인정된 내가 푼 코드이다.
a = &#39;&#39; 할당하고 포문으로 문자열을 돌려서 a 에 문자가 없으면 a 에 문자를 더해주고 문자가 안에 존재하면 거기서 멈춘후 answer 리스트에 a를 넣어준다.
그리고 a를 빈 문자열로 다시 선언하고 다시 a 에 해당 문자가 있는지 확인 후 없으면 a에 문자를 더해준다. 그리고 마지막엔 무조건 a를 answer 리스트에 넣어준다.</p>
<p>그렇게 한 후 answer 리스트에서 길이가 제일 긴 문자열의 길이를 반환하면 끝이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 2]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-2</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-2</guid>
            <pubDate>Tue, 30 Mar 2021 10:20:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/0eea25df-9e8c-428d-9e78-6be7d030c73d/image.png" alt=""></p>
<pre><code>def reverse(number):
    answer = 0
    if number &lt; 0:
        answer = int(str(number)[1:][::-1]) * -1
    else:
        answer = int(str(number)[::-1])
    return answer</code></pre><p>[::-1] 은 리스트를 역순으로 정렬시켜준다.
[1:] 0번째 인덱스 값을 제외한 나머지 값들이 나온다.</p>
<p>이 두가지만 알고 있으면 이 문제는 쉽게 풀수 있는 문제였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 두 개 뽑아서 더하기]]></title>
            <link>https://velog.io/@chan_baek/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%91%90-%EA%B0%9C-%EB%BD%91%EC%95%84%EC%84%9C-%EB%8D%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chan_baek/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%91%90-%EA%B0%9C-%EB%BD%91%EC%95%84%EC%84%9C-%EB%8D%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 30 Mar 2021 09:02:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/993a2f12-2fd2-4f68-8cf9-2b9f5c46f857/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/chan_baek/post/e254d3b6-a09d-4fa7-8487-219bb38f5912/image.png" alt=""></p>
<pre><code>def solution(numbers):
    sum = 0
    total = []
    for i in range(len(numbers)):
        for j in range(i+1,len(numbers)):
            sum = numbers[i] + numbers[j]
            if sum not in total:
                total.append(sum)
    total.sort()
    return total</code></pre><p>처음에 내가 푼 방식은 2중 for문으로 두개의 숫자의 합을 한개씩 뽑아내면서 not in 을 사용하여 total 리스트에 그 값이 없을때 추가하도록 구현했다.
하지만 set 함수를 사용하면 not in을 사용하지 않고 더 간결한 코드로 구현할 수 있다는것을 깨달았다.</p>
<pre><code>def solution2(numbers):
    answer = []
    for i in range(len(numbers)):
        for j in range(i+1, len(numbers)):
            answer.append(numbers[i] + numbers[j])
    return sorted(list(set(answer)))</code></pre><p>2중 for문으로 돌리고 answer 배열에 합들을 모두 추가한 후 set을 이용하여 중복을 제거할 수 있다. 그리고 list로 바꾼뒤 정렬 시키면 된다. 훨씬 더 깔끔한 코드인것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Code Kata 1]]></title>
            <link>https://velog.io/@chan_baek/TIL-Code-Kata-1</link>
            <guid>https://velog.io/@chan_baek/TIL-Code-Kata-1</guid>
            <pubDate>Mon, 29 Mar 2021 12:12:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/chan_baek/post/acc588b2-a16a-43f4-825b-63fddf1ba403/image.png" alt=""></p>
<pre><code>import timeit

start = timeit.default_timer()

# solution 1

 def two_sum(nums, target):
     answer = []
     for i in range(len(nums)):
         for j in range(i+1,len(nums)):
             if nums[i] + nums[j] == target:
                 answer.append(i)
                 answer.append(j)
                 break

     return answer


# solution 2

def two_sum(num,target):
    for i in num:
        val1 = target - i
        if val1 in num:
            a = num.index(i)
            b = num.index(val1)
            return [a,b]

two_sum([1,2,3,4,5,6,7,8,9],17)

end = timeit.default_timer()

print(end-start)</code></pre><p>1번처럼 이중 포문으로 풀고나서 조금 더 효율적인 코드는 없을까를 생각해보다가 for문을 한번만 쓰고 풀수 있을거 같다는 생각이 들었다. 주어진 target을 이용해서 한번 풀어보려고 노력했다. 리스트 값들 중에 한개를 뽑아서 target 에서 뺀 뒤 in 을 사용해서 값을 찾는 방법이였다. 코드 반복이 줄여 효율적인 코드라고 생각했지만 리스트에서는 in 이 O(n)의 시간 복잡도를 가진다는것을 알게 되었다. 결국 in 으로 인해 하나하나 값들을 확인하면서 값을 찾아내므로 내가 원하는 효율적인 코드가 아니였다.
오히려 이중포문보다 더 늦게 결과가 도출되었다.</p>
<p>1번 1.30초 , 2번 4.37초 둘다 테스트는 통과한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Django 모델]]></title>
            <link>https://velog.io/@chan_baek/TIL-Django-%EB%AA%A8%EB%8D%B8</link>
            <guid>https://velog.io/@chan_baek/TIL-Django-%EB%AA%A8%EB%8D%B8</guid>
            <pubDate>Sun, 28 Mar 2021 09:16:25 GMT</pubDate>
            <description><![CDATA[<h1 id="django-modelspy">Django (models.py)</h1>
<p>Django에서는 데이터베이스를 설계하는데 있어서 app 디렉토리에 있는 models.py 파일에서 모든것이 이루어진다고 말할 수 있다.</p>
<pre><code>from django.db import models

# Create your models here.

class Menu(models.Model):
        name = models.CharField(max_length=20)
        class Meta:
            db_table = &quot;Menus&quot;


class Category(models.Model):
        name = models.CharField(max_length=20)
        menu = models.ForeignKey(&#39;Menu&#39;, on_delete=models.CASCADE)
        class Meta:
            db_table = &quot;Categories&quot;


class Drink(models.Model):
    korean_name = models.CharField(max_length=100)
    english_name = models.CharField(max_length=100)
    category = models.ForeignKey(&#39;Category&#39;, on_delete=models.CASCADE)
    description = models.TextField()

    class Meta:
        db_table = &quot;Drinks&quot;

class Allergy(models.Model):
    name = models.CharField(max_length=100)
        drink = models.ManyToManyField(&#39;Drink&#39;, through=&#39;Allergy_drink&#39;)

    class Meta:
        db_table = &quot;Allergies&quot;




class Allergy_drink(models.Model):
    allergy = models.ForeignKey(&#39;Allergy&#39;, on_delete=models.CASCADE)
    drink = models.ForeignKey(&#39;Drink&#39;, on_delete=models.CASCADE)

    class Meta:
        db_table = &quot;Allergy_drinks&quot;



class Nutrition(models.Model):
    one_serving_kca = models.DecimalField(max_digits = 5, decimal_places = 2)
    sodium_mg = models.DecimalField(max_digits = 5, decimal_places = 2)
    saturated_fat_g = models.DecimalField(max_digits = 5, decimal_places = 2)
    sugars_g = models.DecimalField(max_digits = 5, decimal_places = 2)
    protein_g = models.DecimalField(max_digits = 5, decimal_places = 2)
    caffeine_mg = models.DecimalField(max_digits = 5, decimal_places = 2)
    drink = models.ForeignKey(&#39;Drink&#39;, on_delete=models.CASCADE)
    size_mi = models.CharField(max_length=20)
    size_fluid_ounce = models.CharField(max_length=20)

    class Meta:
        db_table = &quot;Nutritions&quot;


class Image(models.Model):
    image_url = models.CharField(max_length=500)
    drink = models.ForeignKey(&#39;Drink&#39;, on_delete=models.CASCADE)

    class Meta:
        db_table = &quot;Images&quot;
</code></pre><p>이렇게 models.py 에서 만들어진 class 는 Database에서 테이블로 생성된다고 생각하면 된다. 여기서는 각각의 입력값들이 무엇을 의미하는지에 대해서는 언급하지 않을 것이다. Django를 공부하면서 느낀것이 sql 문법을 알지 못해도 파이썬 문법만으로 데이터베이스를 생성하고 관리할 수 있다는 것이다.</p>
<p>Django에서 데이터베이스를 관리하는데 있어서 중요한 역할을 하는것이 makemigrations, migrate 명령어인데 이 두개의 명령어는 ORM을 가능하게 해주는 명령어이다.</p>
<p>ORM은 Object Relational Mapping의 줄임말인데 한글로는 객체-관계 매핑 즉, 객체를 관계형 데이터베이스와 연결해주는 개념이라고 생각하면 된다.</p>
<p>객체지향에서는 클래스라는 개념이 있지만, 관계형 데이터베이스에는 테이블이라는 개념이 있다.</p>
<p>프로그래밍 쪽에서는 웬만해서는 객체지향 패러다임으로 만드는 경우가 많은데, 관계형 데이터베이스를 이용하면서도 객체에 대한 표현에 제약을 받지 않고 객체처럼 사용하기 위해서 사용되는 기술이라고 이해하면 좋을것 같다.</p>
<p>위에서 선언된 class 들을 데이터베이스에 연동해 테이블로 만들고 싶을때 manage.py가 있는 디렉토리에서 python manage.py makemigrations &#39;만든 app 이름&#39; 을 입력하면 makemigrations 을 실행시킴으로서, 내가 models.py에서 모델을 변경시킨 사실(class 생성이 될수도 있고, 수정이 될수도 있고 삭제가 될수도 있다.)을 migration으로 저장시키고 싶다는 것을 Django에게 알려준다. 이렇게 되면 </p>
<pre><code>class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name=&#39;Category&#39;,
            fields=[
                (&#39;id&#39;, models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&#39;ID&#39;)),
                (&#39;name&#39;, models.CharField(max_length=20)),
            ],
            options={
                &#39;db_table&#39;: &#39;Categories&#39;,
            },
        ),
        migrations.CreateModel(
            name=&#39;Menu&#39;,
            fields=[
                (&#39;id&#39;, models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&#39;ID&#39;)),
                (&#39;name&#39;, models.CharField(max_length=20)),
            ],
            options={
                &#39;db_table&#39;: &#39;Menus&#39;,
            },
        ),
        migrations.CreateModel(
            name=&#39;Drink&#39;,
            fields=[
                (&#39;id&#39;, models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&#39;ID&#39;)),
                (&#39;korean_name&#39;, models.CharField(max_length=100)),
                (&#39;english_name&#39;, models.CharField(max_length=100)),
                (&#39;description&#39;, models.TextField()),
                (&#39;category&#39;, models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=&#39;products.category&#39;)),
            ],
            options={
                &#39;db_table&#39;: &#39;Drinks&#39;,
            },
        ),
        migrations.AddField(
            model_name=&#39;category&#39;,
            name=&#39;menu&#39;,
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=&#39;products.menu&#39;),
        ),
    ]</code></pre><p>migrations 디렉토리가 생성이 된다. 위에서 보이는것처럼 코드가 저절로 만들어진다. makemigrations 명령어를 입력했을때는 아직 데이터베이스에 적용되기 전이다!</p>
<p>python manage.py migrate를 입력하면 위에 만들어진 코드를 바탕으로 sql구문으로 저절로 바꿔주면서 데이터베이스에 적용된다. 즉, 테이블들이 생성된다.</p>
<p>이렇게 우리는 makemigrations,migrate을 통해서 sql 문법을 알지 못해도 데이터베이스를 컨트롤 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Django (Settings)]]></title>
            <link>https://velog.io/@chan_baek/TIL-Django-Settings</link>
            <guid>https://velog.io/@chan_baek/TIL-Django-Settings</guid>
            <pubDate>Sun, 28 Mar 2021 08:07:13 GMT</pubDate>
            <description><![CDATA[<h1 id="django-설정하기">Django 설정하기</h1>
<pre><code>pip install django
# 이후에 MySQL server에 접속하기 위한 package
$ pip install mysqlclient
  (중요)mysql 부터 설치해주세요!!</code></pre><pre><code>$ django-admin startproject westarbucks
$ cd westarbucks

$ python manage.py startapp products</code></pre><p>startproject &#39;프로젝트명&#39; 으로 프로젝트를 생성하고
manage.py가 있는 디렉토리에서 startapp &#39;app 이름&#39; 으로 app을 생성한다.</p>
<pre><code>$ mysql.server start

$ mysql -u root -p

mysql&gt; create database &quot;NAME&quot; character set utf8mb4 collate utf8mb4_general_ci;</code></pre><p>데이터베이스를 설정하지 않으면 Django는 sqlite3으로 데이터베이스를 자동으로 설정하는데 mysql을 사용하는게 훨씬 좋아서 mysql을 설정해서 사용하기로 결정했다. mysql에 접속해서 create database &quot;NAME&quot; character set utf8mb4 collate utf8mb4_general_ci; 을 입력해서 데이터 베이스를 만든다.</p>
<pre><code>ALLOWED_HOSTS = [&#39;*&#39;]


# Application definition

INSTALLED_APPS = [
#    &#39;django.contrib.admin&#39;,
#    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.messages&#39;,
    &#39;django.contrib.staticfiles&#39;,
    &#39;products&#39;
]

MIDDLEWARE = [
    &#39;django.middleware.security.SecurityMiddleware&#39;,
    &#39;django.contrib.sessions.middleware.SessionMiddleware&#39;,
    &#39;django.middleware.common.CommonMiddleware&#39;,
#    &#39;django.middleware.csrf.CsrfViewMiddleware&#39;,
#    &#39;django.contrib.auth.middleware.AuthenticationMiddleware&#39;,
    &#39;django.contrib.messages.middleware.MessageMiddleware&#39;,
    &#39;django.middleware.clickjacking.XFrameOptionsMiddleware&#39;,
]

</code></pre><p>처음에 startproject로 프로젝트를 만들면 settings.py 파일이 자동적으로 설치되는데 여기로 들어가서 몇가지 설정을 바꿀 필요가 있다.</p>
<p>현재 나는 admin,auth 설정이 필요가 없어서 주석처리를 했고 HOSTS = [&#39;*&#39;] 값을 넣었다. admin,auth를 사용하지 않는데 주석처리를 하지 않으면 데이터베이스에 필요없는 테이블들이 생성되기 때문에 데이터베이스를 가볍게 만들기 위해서 주석처리했다.</p>
<p>INSTALLED_APPS를 보면 &#39;products&#39;가 입력된것을 볼 수 있는데 이것은 내가 만든 app의 이름이다. 이것을 반드시 입력시켜야 한다.
입력시키지 않으면 Django는 &#39;products&#39;라는 app을 만들었다는것을 알지 못한다.</p>
<pre><code>DATABASES = {
    &#39;default&#39; : {
        &#39;ENGINE&#39;: &#39;django.db.backends.mysql&#39;,
        &#39;NAME&#39;: &#39;DATABASE 명&#39;,
        &#39;USER&#39;: &#39;root&#39;,
        &#39;PASSWORD&#39;: &#39;mysql 비밀번호&#39;,
        &#39;HOST&#39;: &#39;localhost&#39;,
        &#39;PORT&#39;: &#39;3306&#39;,
    }
}
SECRET = {
        &#39;secret&#39;:&#39;=6xvu3t)6+8k8vi@b-(g4=2ah$@vi*^f@h#fraf)mc-_3@xw8t&#39;,
}
</code></pre><p>settings.py에 위에 값을 입력해도 되지만 개인정보 노출을 막기위해서 manage.py가 있는 디렉토리에 my_settings.py 파일을 만들어서 위에 값을 입력하면 된다.</p>
<pre><code>import my_settings
from pathlib import Path

DATABASES = my_settings.DATABASES</code></pre><p>입력후에 setting.py 파일로 돌아와서 import my_settings를 입력후에 DATABASES = my_settings.DATABASES 를 입력한다.</p>
<pre><code>from django.urls import path

urlpatterns = [
]</code></pre><p>admin, auth를 주석처리 했기 때문에 urls.py 파일에서 설정값을 위에 나와있는 것처럼 바꿔 놓으면 된다.</p>
<p>처음에는 설정값 입력하는것도 어려웠는데 2번정도 해보니 별로 어렵지 않았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Advanced Python]]></title>
            <link>https://velog.io/@chan_baek/TIL-Advanced-Python</link>
            <guid>https://velog.io/@chan_baek/TIL-Advanced-Python</guid>
            <pubDate>Tue, 23 Mar 2021 05:24:49 GMT</pubDate>
            <description><![CDATA[<h1 id="list-comprehensions">list comprehensions</h1>
<p>리스트 컴프리헨션을 사용하면 가독성이 좋아지는 장점도 있지만 일반적인 for문을 사용해서 리스트를 만들때보다 출력되는 속도가 빨라지는 장점이 있다.</p>
<h4 id="1다음과-같은-도시목록의-리스트가-주어졌을때-도시이름이-s로-시작하지-않는-도시만-리스트로-만들-때-리스트-컴프리헨션을-사용하여-함수를-작성해-보세요">1.다음과 같은 도시목록의 리스트가 주어졌을때, 도시이름이 S로 시작하지 않는 도시만 리스트로 만들 때 리스트 컴프리헨션을 사용하여 함수를 작성해 보세요.</h4>
<pre><code>cities = [&quot;Tokyo&quot;, &quot;Shanghai&quot;, &quot;Jakarta&quot;, &quot;Seoul&quot;, &quot;Guangzhou&quot;, &quot;Beijing&quot;, &quot;Karachi&quot;, &quot;Shenzhen&quot;, &quot;Delhi&quot; ]

cities_no_s = [ i for i in cities if i[0] != &#39;S&#39;]

print(cities_no_s)</code></pre><h4 id="2다음과-같은-도시-인구수가-튜플의-리스트로-주어졌을때-키가-도시-값이-인구수인-딕셔너리를-딕셔너리-컴프리헨션을-사용한-함수를-작성해-보세요">2.다음과 같은 도시, 인구수가 튜플의 리스트로 주어졌을때, 키가 도시, 값이 인구수인 딕셔너리를 딕셔너리 컴프리헨션을 사용한 함수를 작성해 보세요.</h4>
<pre><code>population_of_city = [(&#39;Tokyo&#39;, 36923000), (&#39;Shanghai&#39;, 34000000), (&#39;Jakarta&#39;, 30000000), (&#39;Seoul&#39;, 25514000), (&#39;Guangzhou&#39;, 25000000), (&#39;Beijing&#39;, 24900000), (&#39;Karachi&#39;, 24300000), ( &#39;Shenzhen&#39;, 23300000), (&#39;Delhi&#39;, 21753486) ]

dict_info_city = {population_of_city[i][0]:population_of_city[i][1] for i in range(len(population_of_city))}

print(dict_info_city)</code></pre><h4 id="1번-2번-출력값">1번, 2번 출력값</h4>
<p><img src="https://images.velog.io/images/chan_baek/post/012ef51a-f84c-4350-9820-2ba283568aa8/image.png" alt=""></p>
<h1 id="이터레이터">이터레이터</h1>
<h4 id="이터레이터는-값을-순차적으로-꺼내올-수-있는-객체-입니다">이터레이터는 값을 순차적으로 꺼내올 수 있는 객체 입니다.</h4>
<p>기본적으로 리스트에는 <strong>iter</strong> 함수가 존재한다. 
만약 l = [1,2,3] 이라는 리스트가 있다고 가정하고 dir(l)을 입력하면 아래와 같이 리스트에 어떠한 함수가 있는지 확인해 볼 수 있는데, iter가 목록에 있는것을 확인할 수 있다.</p>
<pre><code>[&#39;__add__&#39;, &#39;__class__&#39;, &#39;__class_getitem__&#39;, &#39;__contains__&#39;, &#39;__delattr__&#39;, &#39;__delitem__&#39;, &#39;__dir__&#39;, &#39;__doc__&#39;, &#39;__eq__&#39;, &#39;__format__&#39;, &#39;__ge__&#39;, &#39;__getattribute__&#39;, &#39;__getitem__&#39;, &#39;__gt__&#39;, &#39;__hash__&#39;, &#39;__iadd__&#39;, &#39;__imul__&#39;, &#39;__init__&#39;, &#39;__init_subclass__&#39;, &#39;__iter__&#39;, &#39;__le__&#39;, &#39;__len__&#39;, &#39;__lt__&#39;, &#39;__mul__&#39;, &#39;__ne__&#39;, &#39;__new__&#39;, &#39;__reduce__&#39;, &#39;__reduce_ex__&#39;, &#39;__repr__&#39;, &#39;__reversed__&#39;, &#39;__rmul__&#39;, &#39;__setattr__&#39;, &#39;__setitem__&#39;, &#39;__sizeof__&#39;, &#39;__str__&#39;, &#39;__subclasshook__&#39;, &#39;append&#39;, &#39;clear&#39;, &#39;copy&#39;, &#39;count&#39;, &#39;extend&#39;, &#39;index&#39;, &#39;insert&#39;, &#39;pop&#39;, &#39;remove&#39;, &#39;reverse&#39;, &#39;sort&#39;]</code></pre><p><strong><strong>iter</strong></strong>() 함수를 출력해보면
print(l.<strong><strong>iter</strong></strong>())</p>
<p>&lt;list_iterator object at 0x7fd0ed1c9f10&gt;</p>
<p>이터레이터 객체임을 확인할 수 있다.</p>
<p>그리고 L2 = l.<strong><strong>iter</strong></strong>() 이터레이터를 변수에 담고 dir를 확인해보면</p>
<p>print(dir(L2))</p>
<pre><code>[&#39;__add__&#39;, &#39;__class__&#39;, &#39;__class_getitem__&#39;, &#39;__contains__&#39;, &#39;__delattr__&#39;, &#39;__delitem__&#39;, &#39;__dir__&#39;, &#39;__doc__&#39;, &#39;__eq__&#39;, &#39;__format__&#39;, &#39;__ge__&#39;, &#39;__getattribute__&#39;, &#39;__getitem__&#39;, &#39;__gt__&#39;, &#39;__hash__&#39;, &#39;__iadd__&#39;, &#39;__imul__&#39;, &#39;__init__&#39;, &#39;__init_subclass__&#39;, &#39;__iter__&#39;, &#39;__le__&#39;, &#39;__len__&#39;, &#39;__lt__&#39;, &#39;__mul__&#39;, &#39;__ne__&#39;, &#39;__new__&#39;, &#39;__reduce__&#39;, &#39;__reduce_ex__&#39;, &#39;__repr__&#39;, &#39;__reversed__&#39;, &#39;__rmul__&#39;, &#39;__setattr__&#39;, &#39;__setitem__&#39;, &#39;__sizeof__&#39;, &#39;__str__&#39;, &#39;__subclasshook__&#39;, &#39;append&#39;, &#39;clear&#39;, &#39;copy&#39;, &#39;count&#39;, &#39;extend&#39;, &#39;index&#39;, &#39;insert&#39;, &#39;pop&#39;, &#39;remove&#39;, &#39;reverse&#39;, &#39;sort&#39;]
[&#39;__class__&#39;, &#39;__delattr__&#39;, &#39;__dir__&#39;, &#39;__doc__&#39;, &#39;__eq__&#39;, &#39;__format__&#39;, &#39;__ge__&#39;, &#39;__getattribute__&#39;, &#39;__gt__&#39;, &#39;__hash__&#39;, &#39;__init__&#39;, &#39;__init_subclass__&#39;, &#39;__iter__&#39;, &#39;__le__&#39;, &#39;__length_hint__&#39;, &#39;__lt__&#39;, &#39;__ne__&#39;, &#39;__new__&#39;, &#39;__next__&#39;, &#39;__reduce__&#39;, &#39;__reduce_ex__&#39;, &#39;__repr__&#39;, &#39;__setattr__&#39;, &#39;__setstate__&#39;, &#39;__sizeof__&#39;, &#39;__str__&#39;, &#39;__subclasshook__&#39;]
</code></pre><p>print문으로 확인해보면 <strong>next</strong> 함수가 들어있는 것을 확인할 수 있다. <strong><strong>next</strong></strong> 함수는 이름처럼 다음 요소를 하나씩 꺼내오는 함수이다.</p>
<p><strong><strong>next</strong></strong>함수를 호출하면서 동작을 확인해보면, 리스트안에 1,2,3 3개의 값이 들어있으므로 3번이상 호출한다.</p>
<pre><code class="language-python">print(iterator_L.__next__())
print(iterator_L.__next__())
print(iterator_L.__next__())
print(iterator_L.__next__())</code></pre>
<p>1,2,3 이 출력되고 StopIteration 이 발생하는 것을 확인할 수 있다. 즉, 리스트의 인덱스를 벗어나서 가져올 값이 없으면 StopIteration이 발생하는 것을 알 수 있다.</p>
<p>딕셔너리도 반복가능한 객체라서 앞서본 리스트와 같이 <strong>iter</strong>함수와 <strong>next</strong>함수를 사용할 수 있고 파이썬 기본함수인 iter, next 또한 사용할 수 있습니다. 다음의 간단한 키를 출력하는 딕셔너리에 대한 for 문을 while문으로 구현해 보세요.</p>
<pre><code class="language-python">D = {&#39;a&#39;:1, &#39;b&#39;:2, &#39;c&#39;:3}
for key in D.keys():
    print(key)</code></pre>
<p>코드:</p>
<pre><code>d = iter(D)
while True:
    try:
        i = next(d)
    except StopIteration:
        break
    print(i, end=&#39; &#39;)</code></pre><p>이렇게 while문으로 구현을 해보면 StopIteration이 발생 했을때 break 시켜주면 a b c 가 출력되는것을 확인할 수 있다.</p>
<h1 id="제너레이터">제너레이터</h1>
<p>파이썬에서 보통의 함수는 값을 반환하고 종료 하지만 제너레이터 함수는 값을 반환하기는 하지만 산출(yield)한다는 차이점이 있다. 그리고 제너레이터는 쉽게 얘기하면 이터레이터를 생성해주는 함수라고도 볼 수 있다.</p>
<p>다음 코드를 보면 함수안에서 yield를 사용하여 리스트의 제곱을 산출하는 함수가 있고, 이 함수를 print문으로 확인해보면 generator object 임을 확인할 수 있다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/2a159cb6-53e8-4d08-8862-31076010f61d/image.png" alt=""></p>
<p>아래의 코드를 분석해보겠습니다.</p>
<p><img src="https://images.velog.io/images/chan_baek/post/e494cca8-95e5-4bbb-b87e-88b1b4e657b8/image.png" alt=""></p>
<h4 id="리스트-컴프리헨션으로-출력했을때와-제너레이터로-출력했을때의-차이점은">리스트 컴프리헨션으로 출력했을때와 제너레이터로 출력했을때의 차이점은?</h4>
<p>L = [ 1,2,3] 가 있다고 가정해보면,</p>
<p>comprehension_list=
sleep 1s
sleep 1s
sleep 1s
1
2
3</p>
<p>generator_exp=
sleep 1s
1
sleep 1s
2
sleep 1s
3</p>
<p>두개의 출력값이 다르다는것을 확인할 수 있는데 컴프리헨션 리스트인 경우에는 lasy_return이 3번 출력되면서 return 되는 num을 리스트에 저장시킨 후 print_iter 함수를 통해서 한번에 출력된다.</p>
<p>반면에 제너레이터는 값을 저장하지 않고 한번씩 번갈아 가면서 출력되는것을 볼 수 있다. 쉽게 말하면, lazy_return 을 통해서 1이 출력이 되면 바로 print_iter에 1을 대입시켜 출력하고 다시 lazy_return 값을 출력시켜 2의 값을 바로 print_iter 함수에 대입해 한번씩 번갈아 가면서 값을 출력 시킨다. 이 반복을 리스트 길이 만큼 반복한다.</p>
<h4 id="제너레이터를-사용하는-이유는-무엇일까">제너레이터를 사용하는 이유는 무엇일까?</h4>
<p><img src="https://images.velog.io/images/chan_baek/post/cba11e32-7f19-46e2-8911-1ffb836cc74c/image.png" alt=""></p>
<p>위에서 처럼, 리스트 컴프리헨션을 사용했을때와 제너레이터를 사용했을때의 메모리 사용값을 확인해보면 제너레이터는 실제 데이터를 만드는것이 아니기 때문에 10개, 100개, 1000개의 값들을 넣어도 리스트 컴프리헨션과 달리 메모리 사용이 증가하지 않는것을 확인할 수 있다.</p>
<h1 id="lambda-expressions">lambda expressions</h1>
<p>람다는 인라인 함수를 정의할 때 사용하며 익명 함수(anonymous functions) 또는 람다 표현식(lambda expression)이라고 부른다.
람다 표현식을 잘 사용하면 코드가 깔끔해지는 장점이 있다.</p>
<h4 id="다음과-같이-비밀번호의-길이와-대문자가-포함된것을-확인하는-간단한-함수가-있다">다음과 같이 비밀번호의 길이와 대문자가 포함된것을 확인하는 간단한 함수가 있다.</h4>
<p><img src="https://images.velog.io/images/chan_baek/post/8f37a814-df08-45af-a79b-eab0f97a7797/image.png" alt=""></p>
<h4 id="이함수에-있는-if문-두개를-람다표현식을-이용하여-다음과-같은-형식으로-작성해-보세요">이함수에 있는 if문 두개를 람다표현식을 이용하여 다음과 같은 형식으로 작성해 보세요.</h4>
<h4 id="아래의-lambdas-리스트안에-두개의-람다표현식을-작성해야하며-주석으로-표시된-프린트가-출력결과로-나와야-합니다">아래의 lambdas 리스트안에 두개의 람다표현식을 작성해야하며 주석으로 표시된 프린트가 출력결과로 나와야 합니다.</h4>
<p><img src="https://images.velog.io/images/chan_baek/post/c45ea273-0f4e-4082-99aa-849722e11494/image.png" alt=""></p>
<p>람다의 기본 문법:</p>
<p><img src="https://images.velog.io/images/chan_baek/post/1b0e5c8c-fb30-4005-a6d9-c0d6546d8dd5/image.png" alt=""></p>
<pre><code>lambdas = [lambda x: &quot;SHORT_PASSWORD&quot; if len(x) &lt; 8 else None,
           lambda x: &quot;NO_CAPITAL_LETTER_PASSWORD&quot; if not any(c.isupper()
                 for c in x) else None]</code></pre><p>이렇게 람다 식으로 코드를 작성하면 print값이 잘 나온다.</p>
<p>SHORT_PASSWORD
NO_CAPITAL_LETTER_PASSWORD
True</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Modules & Packages]]></title>
            <link>https://velog.io/@chan_baek/TIL-Modules-Packages</link>
            <guid>https://velog.io/@chan_baek/TIL-Modules-Packages</guid>
            <pubDate>Sun, 21 Mar 2021 14:49:27 GMT</pubDate>
            <description><![CDATA[<h1 id="1-sysmodules-와-syspath의-차이점">1. <code>sys.modules</code> 와 <code>sys.path</code>의 차이점</h1>
<h4 id="sysmodules">sys.modules</h4>
<ol>
<li>파이썬이 모듈이나 package를 찾기위해 가장 먼저 확인하는 곳이다.</li>
<li>sys.modules은 단순한 dictionary 형태이다.</li>
<li>이미 import된 모듈과 package를 저장하고 있다.</li>
<li>import가 되어있는 모듈과 패키지의 경로가 저장되어 있어 다시 찾지 않아도 된다. (새로 import 하는 모듈은 sys.modules 에서 찾을 수 없음)</li>
</ol>
<h4 id="syspath">sys.path</h4>
<ol>
<li>파이썬이 마지막으로 보는 곳이다.</li>
<li>sys.path는 기본적으로 list형태이며 string 요소들을 가지고 있는 list 이다.</li>
<li>각 list의 경로를 하나하나 확인하면서 해당 경로에 import하고자 하는 package가 위치해 있는지 확인한다.</li>
</ol>
<h1 id="sys-도-import-해야하는-모듈입니다-파이썬은-sys-모듈의-위치를-어떻게-찾을-수-있을까"><code>sys</code> 도 <code>import</code> 해야하는 모듈입니다. 파이썬은 <code>sys</code> 모듈의 위치를 어떻게 찾을 수 있을까?</h1>
<p>sys 모듈은 파이썬에 포함되어 있는 내장 모듈이고 파이썬 설치시 기본적으로 내장 모듈에 대한 path정보가 default 값으로 지정되어있다. sys 모듈도 python에 내장된 built-in 모듈중 하나이므로 경로 검색순서에 따라 쉽게 찾을 수 있다.</p>
<h1 id="absolute-path와-relative-path의-차이점"><strong>Absolute path</strong>와 <strong>relative path</strong>의 차이점</h1>
<h4 id="absolute-path는-어느-파일-어느-위치에서-import-하던지-경로가-항상-동일하게-되므로-absolute-path-라고-한다">absolute path는 어느 파일, 어느 위치에서 import 하던지 경로가 항상 동일하게 되므로 absolute path 라고 한다.</h4>
<ol>
<li><p>Absolute path는 최상단 디렉토리를 기준으로 path 설정.</p>
</li>
<li><p>Absolute path는 최상위 루트부터 경로를 표시해야 하므로 경로가 길어질 수 있다.</p>
</li>
</ol>
<h4 id="relative-path-는-absolute-path와-다르게-프로젝트의-최상단-디렉토리를-기준으로-경로를-잡는게-아니라-import-하는-위치를-기준으로-경로를-정의한다">Relative path 는 absolute path와 다르게 프로젝트의 최상단 디렉토리를 기준으로 경로를 잡는게 아니라 import 하는 위치를 기준으로 경로를 정의한다.</h4>
<ol>
<li><p>Relative path는 실행파일이 위치한 디렉토리를 기준으로 path를 설정한다.</p>
</li>
<li><p>Relative path는 선언해야 하는 경로의 길이를 줄여준다는 장점은 있지만 헷갈리기 쉽고, 파일의 위치가 변경되면 경로 위치도 변경되어야 한다는 단점이 있다.</p>
</li>
</ol>
<p>absolute path를 사용하는것을 권장!</p>
<h1 id="calculator-패키지-만들어-보기"><code>calculator</code> 패키지 만들어 보기</h1>
<p><img src="https://images.velog.io/images/chan_baek/post/af1b55ab-7faf-43b4-981e-d5cb405715cc/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/chan_baek/post/8db4e70a-93c2-49d4-94db-aff3a3f7e9a3/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/chan_baek/post/fbd7dbec-09f2-47d4-a425-ecfa5dd57ed1/image.png" alt=""></p>
<h3 id="mainpy에서-상대경로로-add_and_mutiply-를-임포트-했을-때-발생하는-에러를-확인하고-다음의-파이썬-공식-문서를-참고해서-main-module-에서는-패키지의-모듈을-어떻게-임포트-해야하는가"><code>main.py</code>에서 상대경로로 <code>add_and_mutiply</code> 를 임포트 했을 때 발생하는 에러를 확인하고, 다음의 파이썬 공식 문서를 참고해서 main module 에서는 패키지의 모듈을 어떻게 임포트 해야하는가?</h3>
<p><a href="https://docs.python.org/3/tutorial/modules.html#intra-package-references">https://docs.python.org/3/tutorial/modules.html#intra-package-references</a></p>
<p><img src="https://images.velog.io/images/chan_baek/post/8ba87211-8067-4181-82e6-a3d1456e64d2/image.png" alt=""></p>
<p>파이참 환경에서 실행시켜보니 ImportError: attempted relative import with no known parent package 라는 오류 메세지가 나온걸 확인할 수 있다.</p>
<p>위에 나와있는 파이썬 공식 홈페이지에 들어가보면,</p>
<h4 id="상대경로로-import-시-현재-module의-이름을-기반으로-한다-main-모듈의-이름은-항상-main이기-때문에-파이썬-어플리케이션은-항상-절대경로를-사용해야-한다고-나와있다">상대경로로 import 시 현재 module의 이름을 기반으로 한다. main 모듈의 이름은 항상 &quot;<strong>main</strong>&quot;이기 때문에, 파이썬 어플리케이션은 항상 절대경로를 사용해야 한다고 나와있다.</h4>
<p>위 문구처럼 calculator 파일구조에서 main은 package에 속한 파일이 아니다. 그렇기 때문에 main.py가 속한 module은 &#39;<strong>main</strong>&#39;이 된다.
결국 절대경로로 calculator package를 최상단 디렉토리로 언급해야한다.</p>
<p>from calculator.add_and_multiply import add_and_multiply</p>
<p>이렇게 수정해야 한다.</p>
<h1 id="add_and_multiplypy에서-multiply함수를-절대경로와-상대경로도-각각-임포트-해보고-main-모듈과-차이점은-무엇인가">add_and_multiply.py에서 multiply함수를 절대경로와 상대경로도 각각 임포트 해보고 main 모듈과 차이점은 무엇인가?</h1>
<p>(상대경로)
from .multiplication import multiply </p>
<p>(절대경로)
from calculator.multiplication import multiply</p>
<p>둘다 실행된다.</p>
<p>기본 모듈은 다른 패키지나 모듈을 불러올때 절대경로로 불러와야 하며 기본 모듈이 아닌경우는 절대경로든 상대경로든 상관이 없다.</p>
<h1 id="initpy-파일의-역할"><strong>init</strong>.py 파일의 역할</h1>
<p>init.py란 디렉토리가 패키지로 인식되도록 하는 역할도 하고, 이름 그대로 패키지를 초기화하는 역할을 한다. 즉, import로 패키지를 가져오면 init.py 파일이 실행되므로 이 파일에서 from . import 모듈 형식으로 현재 패키지에서 모듈을 가져오게 만들어야 한다.</p>
<p>디렉터리에 __init.py 파일이 없다면 패키지로 인식되지 않을 수 있다.</p>
<p>하지만, 3.3부터는 <strong>init</strong> 이 없어도 인식 가능하다.</p>
]]></description>
        </item>
    </channel>
</rss>