<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yunsung_.log</title>
        <link>https://velog.io/</link>
        <description>웹과 앱을 사랑하는 남자</description>
        <lastBuildDate>Tue, 04 Aug 2026 05:49:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yunsung_.log</title>
            <url>https://images.velog.io/images/yunsung_/profile/0f6a656f-a5b4-438b-9036-57de4e375465/팽귄.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yunsung_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yunsung_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[나만의 포트폴리오 사이트 제작기]]></title>
            <link>https://velog.io/@yunsung_/%EB%82%98%EB%A7%8C%EC%9D%98-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/%EB%82%98%EB%A7%8C%EC%9D%98-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</guid>
            <pubDate>Tue, 04 Aug 2026 05:49:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>나만의 포트폴리오를 만들며 겪었던 상황들에 대한 정리입니다.</p>
</blockquote>
<p>지금까지 내 포트폴리오는 노션에 잘 정리되어 있었다. 아주 예쁘게. 나만 볼 수 있게.</p>
<p>문제는 여기서 시작된다.
회사에 지원하거나 누군가 &quot;그래서 지금까지 뭐 하셨어요?&quot;라고 물어보면... 보여줄 게 없다.
&quot;어 그... 노션에 있는데... 링크 드릴까요...?&quot; (상대방 표정: 🤨)</p>
<p>그리고 결정적으로, 나는 프론트엔드 개발자다.
근데 나를 소개하는 웹페이지가 없다.</p>
<p>프론트 개발자가 본인 웹사이트 하나 없는 건, 요리사가 집에서 배달 시켜 먹는 것과 비슷한 느낌이랄까. 틀린 말은 아닌데 뭔가 좀... 그렇잖아요.</p>
<p>트러블슈팅은 또 어떻고. 에러 만나면 그 순간엔 셜록 홈즈처럼 물고 늘어져서 해결한다. 그리고 3일 뒤 똑같은 에러를 만나면? &quot;어? 이거 어디서 봤는데...?&quot; 기억이 안 난다. 분명히 고쳤는데. 나는 대체 뭘 고친 걸까. 미제 사건만 늘어간다.</p>
<p>시도해보고 싶었던 기술들도 마찬가지다. &quot;이거 나중에 꼭 써봐야지&quot; 하고 북마크만 쌓여간다. 그 &#39;나중에&#39;는 대체 언제 오는 걸까? (스포일러: 안 옴) 그렇게 나의 성장 그래프는 완만한 우상향... 아니 그냥 평지를 유지하고 있었다.</p>
<p>그래서 반성했다. 그리고 결심했다. 포트폴리오를 직접 만들기로. 미뤄뒀던 기술들을 이참에 다 끌어와서, 이 프로젝트 하나에 그동안의 나를 몽땅 정리해보려 한다.</p>
<h1 id="기술-스택-선정">기술 스택 선정</h1>
<p>이번 포트폴리오에 쓴 스택은 이렇게 정리됐다.</p>
<table>
<thead>
<tr>
<th>분류</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td>프레임워크</td>
<td>Astro 6, TypeScript</td>
</tr>
<tr>
<td>스타일링</td>
<td>Tailwind CSS v4</td>
</tr>
<tr>
<td>콘텐츠</td>
<td>MDX, Astro Content Collections + Zod</td>
</tr>
<tr>
<td>배포</td>
<td>GitHub Actions → AWS S3 + CloudFront</td>
</tr>
</tbody></table>
<p><strong>Astro</strong> — 이유는 단순했다. 포트폴리오 사이트는 데이터가 실시간으로 바뀌지 않는다. 내 소개, 프로젝트 목록, 블로그 포스트 — 전부 빌드 타임에 확정되는 정적 콘텐츠다. 방문자가 뭘 누른다고 내 경력이 실시간으로 늘어나는 것도 아니고(늘어나면 좋겠다), 여기에 서버가 돌아갈 이유가 딱히 없었다. 이 사이트는 블로그까지 겸할 예정이었다. 마크다운으로 글 쓰고, 빌드하면 정적 페이지로 뽑히는 구조 — 딱 Astro가 제일 잘하는 일이다. 그래서 Astro가 낙점됐다. 기본이 SSG라, MDX를 빌드 타임에 HTML로 다 구워버리고 JavaScript는 정말 필요한 곳에만 콕 집어 끼워 넣는다. 그리고 사실 <a href="https://news.hada.io/topic?id=21910">GeekNews</a> 보다가 써보고 싶어져서 써봤다.</p>
<p><strong>Tailwind CSS v4</strong> — Styled Component 와 뭘 쓸지 고민을 좀 했다. 다만, 해당 프로젝트에서는 많은 컴포넌트가 나오지 않을 것이고 딱히 css 모듈화가 필요할 것 같지 않아, 한눈에 디자인과 화면 구성을 파악하기 쉽게 컴포넌트 파일 하나만 열면 마크업이랑 스타일이 다 보이는 Tailwind를 선택했다.</p>
<p><strong>MDX + Content Collections + Zod</strong> — 블로그 글은 MDX로 쓴다. 마크다운인데 필요하면 컴포넌트도 꽂을 수 있으니, 글 안에 인터랙티브한 요소를 넣고 싶을 때 편했다. 그리고 Astro의 Content Collections에 Zod로 스키마를 걸어놨다. 글마다 제목, 날짜, 태그 같은 frontmatter를 적는데, 오타로 <code>date</code>를 <code>data</code>라고 쓰거나 날짜 형식을 틀리면 빌드 단계에서 바로 걸린다. 런타임에 &quot;어? 왜 안 나오지?&quot; 하고 헤매는 대신, 빌드가 대신 잔소리를 해주는 셈이다. 타입 안전한 콘텐츠, 생각보다 마음이 편하다.</p>
<p><strong>GitHub Actions → S3 + CloudFront</strong> — CI/CD는 직접 구성했다. <code>main</code> 브랜치에 push 하면 자동으로 S3에 배포되는 파이프라인을, 이번엔 직접 만들어보고 싶었다. AWS 인증 붙이는 것부터 캐시 무효화(invalidation) 날리는 것까지 전부 손으로 짜보는 것 — 그게 하고 싶었던 일이다. 편한 버튼 누르고 넘어가면 또 &quot;됐으니까 됐지 뭐&quot; 하고 아무것도 안 남을 게 뻔했으니까.</p>
<h1 id="페이지-구조">페이지 구조</h1>
<p>스택을 정했으니, 이제 화면을 어떻게 쌓을지 차례다. 전체 구조를 먼저 트리로 펼쳐보면 이렇다.</p>
<pre><code>BaseLayout (Nav + Footer + 스크롤 등장 애니메이션)
├── / (index.astro)
│ ├── Hero — Matter.js 물리 시뮬레이션 + 헤드라인 사이클링
│ ├── About — 소개 + 경력/학력 타임라인
│ ├── Skills — 기술 스택 그룹 목록
│ ├── Projects — 사이드 프로젝트 카드 목록
│ └── Contact — 연락처
│
├── /blog (blog/index.astro)
│ └── 카테고리 필터 + 키워드 검색 + 페이지네이션
│
├── /blog/[slug] (BlogLayout)
│ └── MDX 블로그 포스트
│
└── /projects/[slug] (ProjectLayout)
└── MDX 프로젝트 세부 페이지</code></pre><p>크게 보면 <strong>메인 한 장(원 페이지)</strong> + <strong>블로그</strong> + <strong>프로젝트 상세</strong>, 이렇게 세 갈래다. 메인은 Hero부터 Contact까지 쭉 내려가며 보는 랜딩이고, 블로그랑 프로젝트는 MDX로 쓴 글이 <code>[slug]</code>별로 페이지가 되는 구조다.</p>
<p>껍데기를 매번 새로 짜기 싫어서, 레이아웃을 상속 구조로 잡았다. 위에서 아래로 물려받는다.</p>
<p><strong><code>BaseLayout</code> — 공통 껍데기</strong>
모든 페이지가 뒤집어쓰는 최상위 레이아웃이다. <code>&lt;head&gt;</code> 메타 태그, OG 태그, Pretendard 폰트, Nav, Footer가 전부 여기 들어있다. 그리고 <code>IntersectionObserver</code>로 <code>.animate-in</code> 요소들의 <strong>스크롤 등장 애니메이션</strong>도 여기서 한 번에 처리한다. 요소가 화면에 들어오는 순간 스르륵 나타나는 그거. 페이지마다 따로 붙일 필요 없이, 클래스만 달아두면 알아서 등장한다.</p>
<p><strong><code>BlogLayout</code> — 블로그 포스트용</strong>
BaseLayout을 상속받고, 여기에 <strong>글 읽기용 타이포그래피</strong>를 얹었다. 본문 폭, 줄 간격, 제목 위계 — 긴 글을 읽어도 눈이 안 피곤하게. 껍데기는 BaseLayout이 알아서 해주니, 얘는 글 읽는 경험에만 집중하면 됐다.</p>
<p><strong><code>ProjectLayout</code> — 프로젝트 상세용</strong>
마찬가지로 BaseLayout 상속. 대신 위쪽에 <strong>프로젝트 헤더(스택 / 기간 / 링크)</strong>를 붙이고, 아래엔 <strong>트러블슈팅 아코디언</strong>을 넣었다. 이 블로그를 시작한 이유 중 하나가 &quot;트러블슈팅 해결하고 그냥 넘어가서 기억 못 하는 것&quot;이었으니까, 프로젝트마다 삽질 기록을 접었다 폈다 할 수 있게 아예 레이아웃에 박아뒀다.</p>
<p>정리하면 <strong>BaseLayout이 공통 뼈대를 다 지고</strong>, 블로그와 프로젝트는 각자 필요한 옷만 덧입는 구조다. 덕분에 Nav 하나 고치려고 파일 세 개 뒤질 일은 없어졌다.</p>
<h1 id="구현-기능">구현 기능</h1>
<p>구조를 잡았으니, 이제 각 화면에 어떤 생각을 담았는지 차례로 풀어본다.</p>
<h2 id="🎯-hero">🎯 Hero</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/39436667-d3fb-4f3b-9b52-e18e21388b51/image.png" alt=""></p>
<p>화면에 딱 들어온 순간, 사용자의 시선을 잡아야 한다고 생각했다. 3초 안에 흥미를 못 끌면 그냥 뒤로가기니까.</p>
<p>그래서 화면을 좌우로 갈랐다. <strong>왼쪽엔 내가 무엇을 추구하는지</strong>를 큼직하게 박아두고, <strong>오른쪽엔 개발자로서 내가 믿는 가치와 지금까지 걸어온 과정</strong>을 담기로 했다.</p>
<p>문제는 오른쪽이었다. &quot;지금까지 해온 것들&quot;을 어떻게 보여줄까 고민하다가, <code>Matter.js</code>가 떠올랐다. <strong>키워드 필(pill)이 하늘에서 뚝뚝 떨어져 중력에 따라 아래로 쌓이는</strong> 그림. 키워드 하나하나가 떨어져 차곡차곡 쌓여가는 모습이, 내가 성장해온 과정을 은유적으로 보여줄 것 같았다. 경력이란 결국 그렇게 하나씩 쌓이는 거니까.</p>
<p>여기서 한 발 더 갔다. <strong>왼쪽과 오른쪽을 하나로 엮은 것.</strong></p>
<ul>
<li>오른쪽 필에 마우스를 <strong>호버</strong>하면, 왼쪽의 작은 설명 글씨가 그 키워드에 맞게 바뀐다. 필 하나하나가 왼쪽 문장과 연결되어 있는 셈이다.</li>
<li>오른쪽 필을 <strong>클릭</strong>하면, 그 키워드에 해당하는 섹션으로 스르륵 이동한다.</li>
</ul>
<p>그러니까 사용자는 <strong>첫 화면에서 손가락 몇 번으로 내 전부를 훑을 수 있다.</strong> 스크롤을 강요하지 않아도, 궁금한 필만 누르면 원하는 곳으로 데려다주는 구조. 화면 하나에 &quot;이게 접니다&quot;를 다 담고 싶었다.</p>
<p>거기에 헤드라인 3줄이 10초 간격으로 슬라이드 업 되며 바뀐다. 가만히 있어도 화면이 조금씩 살아 움직인다.</p>
<h2 id="👤-about--이게-접니다를-글로">👤 About — &quot;이게 접니다&quot;를 글로!</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/1a9c0c74-34b7-422b-9dd0-5cb0541afc58/image.png" alt=""></p>
<p>Hero가 시선을 끄는 자리라면, About은 <strong>내가 어떤 개발자인지 솔직하게 말하는 자리</strong>다. 화려한 수식어 대신, 내가 실제로 어떤 사람인지를 담고 싶었다.</p>
<p>나는 &quot;이 정도는 원래 참고 하는 거야&quot;라는 말을 잘 못 견딘다. 명단을 이틀씩 손으로 옮기는 것도, 납품마다 같은 화면을 처음부터 다시 만드는 것도, 인스타툰 새 화가 나왔는지 매번 확인하러 들어가는 것도 — 반복되는 불편을 마주하면 손이 근질거려 구조나 자동화로 바꿔버린다. 회사 일이든 내 개인적인 귀찮음이든 똑같다.</p>
<p>고등학생 때 첫 회사에서 <strong>실제 사용자에게 나가는 제품</strong>을 만들어왔다. 8만 명이 쓰는 플랫폼의 데이터 불일치를 잡고, 불공정하게 설계된 추첨 로직을 다시 짜고, 수작업으로 굴러가던 운영을 자동화하면서 배운 건 하나다 — <strong>잘 짠 코드보다, 실제로 문제를 해결하고 누군가 진짜로 쓰는 것이 먼저</strong>라는 것.</p>
<p>그 아래엔 <strong>경력과 학력을 타임라인</strong>으로 뒀다. 말로 풀어낸 가치가 실제로 어떤 시간을 거쳐 쌓였는지, 한 줄기로 내려가며 볼 수 있게. 결국 Hero에서 필이 쌓이던 그 은유를, About에선 시간순으로 펼쳐 보이는 셈이다.</p>
<h2 id="🛠-skills--쌓아온-걸-한눈에">🛠 Skills — 쌓아온 걸 한눈에</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/e6deb104-50fa-4ad6-a917-9c7f69f5e819/image.png" alt=""></p>
<p>Skills는 욕심을 버린 섹션이다. 숙련도를 별점이나 퍼센트 바로 표현하는 방식도 생각했지만, 관뒀다. &quot;React 90%&quot; 같은 숫자가 대체 무슨 의미인지 나도 설명 못 하겠더라. 보는 사람 입장에서도 신뢰가 안 가고.</p>
<p>그래서 <strong>그동안 다뤄온 기술을 그룹별로 묶어 한눈에 보이게</strong> 하는 데만 집중했다. 프레임워크는 프레임워크끼리, 스타일링은 스타일링끼리 — 카테고리로 정리하니 &quot;이 사람이 어느 영역을 다루는구나&quot;가 스크롤 한 번에 들어온다. 과장 없이, 있는 그대로.</p>
<h2 id="📂-projects--결과물보다-무엇을-풀었나">📂 Projects — 결과물보다 &quot;무엇을 풀었나&quot;</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/df4b20ae-914c-47c3-8682-3f27cdd33c08/image.png" alt=""></p>
<p>Projects에서 제일 신경 쓴 건, <strong>예쁜 결과 스크린샷이 아니라 &quot;무슨 문제를 어떻게 풀었나&quot;</strong>다.</p>
<p>About에서 말한 그대로다. 잘 짠 코드보다 실제로 해결한 문제가 먼저라면, 프로젝트도 그 기준으로 보여줘야 맞다. 그래서 카드에는 &quot;이거 만들었어요&quot;가 아니라 <strong>어떤 문제가 있었고, 어떻게 접근했는지</strong>가 드러나게 했다.</p>
<p>카드를 클릭하면 세부 페이지로 넘어가고, 거기엔 <strong>트러블슈팅 기록</strong>이 아코디언으로 접혀 있다. 이 블로그를 시작한 이유가 &quot;문제 해결하고 그냥 넘어가서 기억 못 하는 것&quot;이었으니까, 프로젝트마다 삽질의 과정을 아코디언으로 남겨뒀다. 결과만 자랑하는 포트폴리오는 많지만, 나는 <strong>과정을 보여주는 쪽</strong>이 더 나답다고 생각했다.</p>
<p>여기까지가 메인 페이지 한 장에 담긴 것들이다. 표로 정리하면 이렇다.</p>
<table>
<thead>
<tr>
<th>섹션</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>Hero</td>
<td>Matter.js 물리 엔진 — 키워드 필이 중력에 따라 화면 아래로 쌓임</td>
</tr>
<tr>
<td>Hero</td>
<td>필터 버튼으로 카테고리별 필만 남기고 나머지는 페이드 아웃</td>
</tr>
<tr>
<td>Hero</td>
<td>필 호버 → 좌측 설명 텍스트 전환 / 필 클릭 → 해당 섹션으로 이동</td>
</tr>
<tr>
<td>Hero</td>
<td>헤드라인 3줄이 10초 간격으로 슬라이드 업 전환 (Web Animations API)</td>
</tr>
<tr>
<td>About</td>
<td>자기소개 + 경력/학력 타임라인</td>
</tr>
<tr>
<td>Skills</td>
<td>기술 스택 그룹별 분류</td>
</tr>
<tr>
<td>Projects</td>
<td>MDX Content Collections 기반 프로젝트 카드, 클릭 시 세부 페이지로</td>
</tr>
<tr>
<td>Contact</td>
<td>연락처 링크</td>
</tr>
<tr>
<td>공통</td>
<td>IntersectionObserver 스크롤 등장 애니메이션 (<code>.animate-in</code>)</td>
</tr>
</tbody></table>
<h2 id="📚-블로그">📚 블로그</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/3d2ba0e2-5779-472f-97a2-e4107f058dae/image.png" alt=""></p>
<p>블로그는 &quot;글이 쌓였을 때 원하는 걸 빨리 찾는 것&quot;에 초점을 뒀다.</p>
<ul>
<li><strong>카테고리 탭</strong>으로 분류(전체 / Frontend / Backend / DevOps / AI / 생산성 / 기타)해서 원하는 주제만 골라 본다.</li>
<li><strong>키워드 검색</strong>은 제목 기준으로 실시간 필터링된다. 타이핑하는 대로 목록이 좁혀진다.</li>
<li>페이지당 5개씩, <strong>고정 높이 컨테이너</strong>로 담았다. 글이 4개든 5개든 페이지네이션 버튼 위치가 안 흔들리게.</li>
<li><code>draft</code> 포스트는 목록에서 자동으로 빠지고, <code>featured</code> 포스트는 위로 끌어올려 정렬한다.</li>
</ul>
<table>
<thead>
<tr>
<th>페이지</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>목록</td>
<td>카테고리 탭 필터 (전체 / Frontend / Backend / DevOps / AI / 생산성 / 기타)</td>
</tr>
<tr>
<td>목록</td>
<td>키워드 검색 (제목 기준 실시간 필터)</td>
</tr>
<tr>
<td>목록</td>
<td>페이지당 5개, 고정 높이 컨테이너로 페이지네이션 버튼 위치 고정</td>
</tr>
<tr>
<td>목록</td>
<td>draft 자동 제외, featured 우선 정렬</td>
</tr>
<tr>
<td>포스트 (<code>/blog/[slug]</code>)</td>
<td>MDX 파일명 → URL slug, BlogLayout 타이포그래피 적용</td>
</tr>
<tr>
<td>프로젝트 (<code>/projects/[slug]</code>)</td>
<td>프로젝트 헤더 (스택 / 기간 / 역할 / 링크)</td>
</tr>
<tr>
<td>프로젝트 (<code>/projects/[slug]</code>)</td>
<td>트러블슈팅 섹션 아코디언 (기본 접힘, 클릭 시 확장)</td>
</tr>
</tbody></table>
<h2 id="⚙️-cicd">⚙️ CI/CD</h2>
<p><code>main</code> 브랜치에 push 하면, GitHub Actions가 빌드부터 배포까지 알아서 끝낸다.</p>
<pre><code class="language-yaml">name: Deploy Website

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      - run: npm ci
      - run: npm run build

      - run: aws s3 sync --delete ./dist/ s3://${{ secrets.BUCKET_ID }}

      - run: aws cloudfront create-invalidation --distribution-id ${{ secrets.DISTRIBUTION_ID }} --paths &quot;/*&quot;</code></pre>
<p>흐름은 단순하다.</p>
<blockquote>
<p>main 브랜치에 push
        ↓
GitHub Actions 실행
        ↓
① 코드 체크아웃
② AWS 인증 (Secrets에서 키 로드)
③ npm ci        → 패키지 설치
④ npm run build → dist/ 정적 파일 생성
⑤ S3 sync       → dist/ 를 버킷에 동기화 (--delete 로 삭제된 파일도 반영)
⑥ CloudFront invalidation → CDN 캐시 갱신</p>
</blockquote>
<p>커밋 하나 밀어넣으면 잠시 후 사이트가 갱신돼 있다. 
배포는 push 딱 한 동작이면 끝이다.</p>
<h2 id="마치며">마치며</h2>
<p><a href="https://www.choiyunsung.dev/">https://www.choiyunsung.dev/</a> 실제 배포된 포트폴리오 사이트입니다.</p>
<p>이렇게 노션 속에만 잠들어 있던 포트폴리오가, 드디어 링크 하나로 보여줄 수 있는 웹사이트가 됐다. 이제 누가 &quot;그래서 지금까지 뭐 하셨어요?&quot;라고 물으면, 당당하게 주소를 던질 수 있다. (상대방 표정이 🤨에서 🙂로 바뀌길 바라며)</p>
<p>만들면서 미뤄두기만 했던 Astro도 써봤고, styled-components에서 Tailwind로 갈아탔고, CI/CD도 손으로 직접 짜봤다. 북마크만 쌓여가던 &quot;나중에 해봐야지&quot; 목록을, 이 프로젝트 하나에 몰아서 해치운 셈이다. 평지였던 성장 그래프가 아주 조금은 우상향으로 꺾였길.</p>
<p>그리고 무엇보다, 이제 트러블슈팅을 그냥 흘려보내지 않을 자리가 생겼다. 앞으로 만날 미제 사건들은 이 블로그에 차곡차곡 기록될 예정이다. 셜록 홈즈처럼 해결하고 3일 뒤에 까먹는 삶이여, 이제는 안녕.</p>
<p>물론 이 다짐이 얼마나 갈지는 나도 모른다. 하지만 적어도 지금은, 빈 페이지가 아니라 채워나갈 페이지가 생겼다는 것만으로 충분하다.</p>
<p>다음 글은... 이 블로그의 첫 트러블슈팅이 되지 않을까? (부디 그러길)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Claude Code 를 이용한 프로젝트 개발기]]></title>
            <link>https://velog.io/@yunsung_/Claude-Code-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/Claude-Code-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Mon, 03 Aug 2026 06:21:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기획부터 출시까지 혼자, 그리고 AI와 함께 만든 두 개의 서비스 이야기입니다.
왜 이 시기에 두 프로젝트를 몰아서 만들었고, AI를 어떻게 통제하며 협업했는지에 대한 기록입니다.</p>
</blockquote>
<hr>
<h2 id="들어가며">들어가며</h2>
<p>전역 이후 다시 개발자로 돌아오기 위해, 저는 전부터 구상해두었던 아이디어들을 직접 만들어보기로 했습니다. 기획도, 설계도, 개발도, 출시도 전부 혼자 해내는 완주에 도전한 것입니다.</p>
<p>군 복무를 하는 사이 세상은 빠르게 달라져 있었습니다. 제가 회사에 다닐 때까지만 해도 AI는 그저 코드를 파악하는 데 도움을 주는 정도의 도구였습니다.</p>
<p>스스로 코드를 온전히 작성하지도 못했고, 무언가를 맡기기에는 확실성이 부족한 보조 수단에 가까웠습니다. 그런데 전역하고 보니, AI는 어느새 프롬프트 하나만으로 앱 하나를 만들어낼 만큼 강력한 무기가 되어 있었습니다.</p>
<p>이런 급격한 변화를 마주하고 나니, 저는 두 가지 목표를 세우게 되었습니다. </p>
<p>하나는 <strong>AI를 개발에 제대로 활용하는 방법을 익히는 것</strong>, </p>
<p>다른 하나는 그 과정에서 <strong>서비스의 전반적인 구조를 스스로 파악하고 설계할 수 있는 개발자가 되는 것</strong>이었습니다. </p>
<p>단순히 코드를 짜는 사람을 넘어, 하나의 서비스를 처음부터 끝까지 조망할 수 있는 시야를 갖고 싶었습니다.</p>
<p>그 목표를 향해, 저는 성격이 서로 다른 두 개의 서비스를 동시에 만들기로 했습니다. 
하나는 모바일 앱, 하나는 웹 서비스.</p>
<p>의도적으로 다른 플랫폼과 다른 문제를 골랐습니다. </p>
<p>서로 다른 환경을 겪어봐야 &quot;서비스 전반을 보는 시야&quot;라는 목표에 더 가까워질 수 있다고 생각했기 때문입니다.</p>
<hr>
<h2 id="내가-ai를-사용한-방식--처음부터-잘한-건-아니었다">내가 AI를 사용한 방식 — 처음부터 잘한 건 아니었다</h2>
<h3 id="처음엔-저도-코드-생성기로-썼습니다">처음엔 저도 코드 생성기로 썼습니다.</h3>
<p>솔직히 처음에는 저도 AI를 코드 생성기로만 썼습니다. &quot;이 함수 짜줘&quot;, &quot;이 컴포넌트 만들어줘.&quot; 확실히 빨랐습니다.</p>
<p>그런데 며칠 지나자 이상한 낌새가 들었습니다. </p>
<p>로직이 화면 컴포넌트 안에 들어가 있기도 하고, 데이터 처리를 해야 할 곳에 UI 코드가 섞여 있기도 했습니다.</p>
<p>AI에게 매번 &quot;여기에 만들어줘&quot;라고 말하지 않으면, 코드는 아무 데나 생겨났습니다.</p>
<p>무엇보다 무서웠던 건, 제가 이해하지 못한 코드가 프로젝트 안에 조금씩 쌓이고 있다는 것이었습니다.</p>
<p>이대로 가면 프로젝트가 커질수록 무너지겠다는 생각이 들었습니다. 그래서 방식을 바꾸기로 했습니다.</p>
<h3 id="그래서-ai에게-지켜야-할-경계를-문서로-줬습니다---claudemd">그래서, AI에게 지켜야 할 경계를 문서로 줬습니다. - CLAUDE.md</h3>
<p>가장 먼저 한 일은 프로젝트 루트에 CLAUDE.md를 만드는 것이었습니다. Claude Code가 대화를 시작할 때마다 자동으로 읽는 파일입니다. 여기에 파일 구조부터 못 박았습니다.</p>
<p>Toonify에서는 이렇게 경계를 그었습니다.</p>
<pre><code>src/
├── screens/      ← 화면만
├── services/     ← 데이터 처리만
├── utils/        ← 앱·서버 공통 로직만
└── components/   ← UI 컴포넌트만</code></pre><p>이 한 장의 지도가 생기자, &quot;이 함수는 어디에 두어야 하나&quot;라는 질문이 사라졌습니다. AI도, 저도 같은 지도를 보고 움직이게 된 것입니다.</p>
<p>효과를 확인하고 나니 욕심이 생겼습니다.</p>
<p>그다음 만든 CrossStitch에서는 구조를 훨씬 더 촘촘하게 문서화했습니다. </p>
<p>폴더마다 역할을 주석으로 달아, AI가 어떤 파일이 무슨 일을 하는지 헷갈리지 않게 한 것입니다.</p>
<pre><code>CrossStitch/
├── .github/workflows/
│   ├── ci.yml            # lint → typecheck → test → deploy (main push 시)
│   └── daily-update.yml  # 매일 자정 UTC 크론 (커밋 집계 업데이트)
├── __tests__/            # 그리드·홈 상태·뒤로가기 단위 테스트
├── app/
│   ├── api/
│   │   ├── cron/         # 크론 엔드포인트 (커밋 집계 → Firestore)
│   │   ├── github/       # OAuth·커밋·통계·유저 조회
│   │   └── readme-card/[uid]/  # 동적 SVG 카드 생성 API
│   ├── home/_components/ # 격자 에디터·홈 상태 관리·모달
│   ├── lib/              # firebase.ts / firebase-admin.ts(서버 전용)
│   └── src/
│       ├── hooks/        # useAuth · useGridPersistence 등
│       ├── providers/    # AuthProvider · StitchProvider
│       └── utils/
│           ├── gridLogic.ts   # 그리드 셀 계산 로직
│           └── homeState.ts   # 홈 상태 파생 순수 함수 (단일 소스)
├── CLAUDE.md
├── SPEC.md
└── TROUBLESHOOTING.md</code></pre><p>그리고 CrossStitch에서는 파일 구조를 넘어 개발 절차 자체까지 문서에 적었습니다. &quot;브랜치를 만들고 → 커밋하고 → PR을 올리고 → CI를 확인한다&quot;는 흐름을요. 그랬더니 제가 &quot;이 버그 고쳐줘&quot;라고만 해도, AI가 알아서 새 브랜치를 만들고 커밋한 뒤 PR을 생성하기 시작했습니다.</p>
<p>문서로 경계를 그은 뒤에 얻은 건, 결국 하나였습니다. AI가 아무 데나 코드를 흩뿌리는 대신, 정해진 자리에 정해진 방식으로 일하기 시작했다는 것입니다.</p>
<h3 id="그러자-이번엔-반복-절차가-눈에-들어왔습니다">그러자 이번엔, 반복 절차가 눈에 들어왔습니다.</h3>
<p>절차를 문서로 정하고 나니, 정작 그 절차를 매번 사람이 챙기는 게 비효율적으로 느껴졌습니다. 브랜치 만들고, PR 올리고, CI 통과했는지 확인하고, 머지하고… 귀찮고 빠뜨리기 쉬운 일이었습니다.</p>
<p>그래서 CrossStitch에서는 이 절차들을 하나씩 자동화에 넘겼습니다. 원래 매번 손으로 챙기던 흐름은 이랬습니다.</p>
<blockquote>
<p>브랜치 생성 → 기능 구현 → Commit → Push → Pull Request 생성</p>
</blockquote>
<p><strong>먼저, PR 생성을 자동화했습니다.</strong> non-main 브랜치에 push하면 GitHub Actions가 알아서 PR을 열어줍니다. </p>
<p>위 흐름에서 앞의 네 단계는 AI가 CLAUDE.md 규칙대로 수행하고, 마지막 PR 생성은 push를 감지한 Actions가 자동으로 처리하는 식입니다.</p>
<p>AI가 브랜치를 만들고 push하면, 제가 GitHub 페이지를 열지 않아도 PR이 이미 생성돼 있는 것입니다.</p>
<p><strong>다음으로, 검증을 파이프라인으로 묶었습니다.</strong> 모든 PR은 lint → typecheck → test를 순서대로 거칩니다. </p>
<p>각 단계는 실패해도 멈추지 않고 끝까지 돌린 뒤, 마지막에 전체 결과를 한 번에 리포트하도록 했습니다.</p>
<p>그리고, 실패를 눈앞에 가져다 놓았습니다. </p>
<p>*<em>어느 단계에서 왜 실패했는지, 어떻게 고치면 되는지가 PR 코멘트로 자동으로 달립니다. *</em></p>
<p>GitHub Actions 탭을 파고들 필요 없이, PR 페이지에서 바로 원인을 확인할 수 있게 된 것입니다.</p>
<p>마지막으로, 배포를 CI에 종속시켰습니다. </p>
<p>CI 전체를 통과한 코드만 배포되도록 묶었습니다. 테스트가 깨진 코드는 애초에 프로덕션에 올라갈 수 없습니다.</p>
<p>이 자동화가 준 진짜 선물은 시간이 아니라 집중력이었습니다. </p>
<p>&quot;브랜치 맞나? PR 올렸나? CI 통과했나?&quot;에 쓰던 신경을, 이제 코드의 의도와 구조를 들여다보는 데 쓸 수 있게 됐으니까요.</p>
<h3 id="그런데-ai-코드를-그대로-믿었다가-데였습니다">그런데, AI 코드를 그대로 믿었다가 데였습니다.</h3>
<p>여유가 생기자 저는 한 가지 원칙을 세웠습니다. 내가 이해하지 못한 코드는 배포하지 않는다. 그래서 AI가 코드를 주면 &quot;이 로직이 어떻게 동작하는지 설명해줘&quot;를 먼저 물었습니다.</p>
<p>이 원칙이 왜 필요한지는 곧 실전에서 증명됐습니다. CrossStitch에서 셀을 무작위로 제거하는 함수를 AI가 만들었는데, 코드가 이랬습니다.</p>
<pre><code>javascript
return suffled.slice(0, removeCount);</code></pre><p>변수명 오타(suffled)는 눈에 띕니다. 진짜 문제는 slice(0, removeCount)였습니다. 이건 &quot;removeCount개만 남기고 나머지를 버린다&quot;는 뜻인데, 이 함수의 의도는 정반대였습니다. 제거해야 할 걸 남기고, 남겨야 할 걸 버리는 코드였던 겁니다. 겉보기엔 멀쩡히 돌아가는 코드였습니다.</p>
<p>만약 설명을 요구하지 않고 그대로 배포했다면 결코 못 잡았을 버그였습니다. 이 일을 겪고 나서 저는 확신했습니다. </p>
<blockquote>
<p>AI는 초안을 만들고, 판단은 사람이 한다.</p>
</blockquote>
<p>마지막으로, 같은 실수를 두 번 하고 나서야 배웠습니다.</p>
<h3 id="그런데-이번엔-버그를-아무리-고쳐도-끝나지-않았다">그런데 이번엔, 버그를 아무리 고쳐도 끝나지 않았다</h3>
<p>검증 원칙을 세운 뒤에도 실수는 이어졌습니다. 이번 무대는 <strong>Toonify</strong>였습니다.</p>
<p>버그는 하나씩 모습을 드러냈습니다.</p>
<p>첫 번째. OCR 결과에 &quot;완결&quot;이라는 글자만 있어도 매칭이 통과됐습니다. 시리즈와 아무 상관없는 이미지가 완결로 인식된 것입니다. 시리즈 키워드 확인을 건너뛰고 isCompleteEpisode(ocrText) 하나만으로 통과할 수 있는 구조가 원인이었습니다.</p>
<pre><code class="language-js">// 문제
const ocrMatched =
  allWords.some((w) =&gt; ocrText.includes(w)) || isCompleteEpisode(ocrText);
//                                           ↑ 시리즈 키워드 없어도 통과

// 수정
const ocrMatched = allWords.some((w) =&gt; ocrText.includes(w));
</code></pre>
<p>고쳤습니다. 그런데 다음 버그가 나왔습니다.</p>
<p>두 번째. 시리즈명이 짧아 키워드가 비면 minMatch가 0이 됐습니다. 0 &gt;= 0은 언제나 참이니, 결국 모든 캡션이 매칭됐습니다. keyWords.length &gt; 0 가드 한 줄이 없어서 벌어진 일이었습니다.</p>
<pre><code class="language-js">// 문제
const captionMatched =
  (captionIsComplete &amp;&amp; hasAnySeriesWord) || matchCount &gt;= minMatch;
// keyWords=[], minMatch=0 → 0 &gt;= 0 → 항상 true

// 수정
const captionMatched = keyWords.length &gt; 0 &amp;&amp; matchCount &gt;= minMatch;</code></pre>
<p>이것도 고쳤습니다. 그런데 또 나왔습니다.</p>
<p>세 번째. 완결을 감지할 때는 시리즈 키워드가 1개만 있어도 통과하는 분기가 있었습니다. 일반 매칭은 2개가 필요한데 완결만 유독 느슨했던 것입니다. 그 탓에 다른 시리즈의 완결 포스트가 내 툰의 완결로 잡혔습니다.</p>
<pre><code class="language-js">// 문제
const captionMatched =
  keyWords.length &gt; 0 &amp;&amp;
  ((captionIsComplete &amp;&amp; hasAnySeriesWord) || matchCount &gt;= minMatch);
//  ↑ 완결이면 키워드 1개만 있어도 통과

// 수정
const captionMatched = keyWords.length &gt; 0 &amp;&amp; matchCount &gt;= minMatch;
// isCompleteEpisode는 완결 여부 판단에만 사용, 매칭 기준에는 영향 없음</code></pre>
<p>네 번째. 캡션은 키워드 2개 이상을 요구했지만, OCR은 더 넓은 단어 목록에서 1개만 맞아도 통과였습니다. 그래서 캡션에서 막힌 포스트가 OCR을 우회 경로 삼아 슬그머니 들어왔습니다.</p>
<pre><code class="language-js">// 문제 — OCR은 allWords 중 1개만 있으면 통과
const ocrMatched = allWords.some((w) =&gt; ocrText.includes(w));

// 수정 — 캡션과 동일한 keyWords + minMatch 기준 적용
const ocrMatchedWords = keyWords.filter((w) =&gt; ocrText.includes(w));
const ocrMatched = ocrMatchedWords.length &gt;= minMatch;</code></pre>
<p>다섯 번째. 알림은 3화가 왔다는데, 앱 목록에는 5화까지 떠 있었습니다. 감지 로직(checkToon)은 엄격한 기준으로 고쳤는데, 정작 목록을 만드는 buildUnreadPosts는 옛 느슨한 조건을 그대로 쓰고 있었기 때문입니다.</p>
<pre><code class="language-js">// 문제 — buildUnreadPosts의 매칭 조건
const matched =
  captionIsComplete || allWords.some((w) =&gt; cap.includes(w));
//  ↑ 완결 단독 통과 + 1개 단어 substring 매칭

// 수정 — checkToon과 동일한 기준
const { ok } = captionMatches(keyWords, minMatch, cap);</code></pre>
<p>여기까지 오자 슬슬 이상했습니다. 분명 앱에서 다 고쳤는데, 왜 문제가 끝나지 않을까.</p>
<p>여섯 번째, 그리고 진짜 원인. 저는 이 버그들을 전부 앱에서만 고치고 있었습니다. 서버(check-toons.js)는 처음부터 끝까지 구버전 로직으로 돌아가고 있었던 것입니다. 같은 버그를 두 번 고치고, 한 곳만 고치고 다른 곳을 빠뜨리는 일이 반복된 진짜 이유가 여기 있었습니다.</p>
<p>버그 하나하나가 문제가 아니었습니다. 같은 로직을 두 벌 들고 있었다는 것이 문제의 뿌리였습니다.</p>
<p>결국 공통 로직을 matchingUtils.js 하나로 추출했습니다.</p>
<pre><code>src/utils/matchingUtils.js  ← SSOT(단일 진실 공급원)
    ↓ import              ↓ require
check-service.js        check-toons.js
(앱)                    (서버)</code></pre><pre><code class="language-js">// src/utils/matchingUtils.js
function buildSeriesKeys(seriesName) {
  // keyWords(3자 이상 단어 우선), minMatch 계산
}
function captionMatches(keyWords, minMatch, caption) {
  // 토큰 단위 매칭
}
function ocrMatches(keyWords, minMatch, text) {
  // 서브스트링 매칭 (조사 붙은 한국어 대응)
}

module.exports = { buildSeriesKeys, captionMatches, ocrMatches, /* ... */ };
js
// check-service.js (앱)
import { captionMatches, ocrMatches } from &#39;../utils/matchingUtils&#39;;

// check-toons.js (서버)
const { captionMatches, ocrMatches } = require(&#39;../src/utils/matchingUtils&#39;);</code></pre>
<p>이후로는 버그를 한 번만 고치면 두 곳이 동시에 고쳐집니다. 그리고 이 경험은 그대로 CLAUDE.md의 규칙 8번이 됐습니다.</p>
<p>앱과 서버가 함께 쓰는 로직은 반드시 공유 파일로 분리하고 양쪽이 import해서 사용한다. 같은 로직을 두 곳에 따로 작성하거나 복붙하는 것은 금지한다.</p>
<p>이렇게 겪은 교훈들은 TROUBLESHOOTING.md에 &quot;무엇을 고쳤나&quot;가 아니라 &quot;왜 틀렸나&quot; 중심으로 남겼습니다. 지금 이 기록이 16개까지 쌓였고, 새 기능을 건드릴 때마다 먼저 이 문서를 펼쳐봅니다. 같은 실수를 세 번은 하지 않기 위해서입니다.</p>
<p>그리고 이 문서를 참고하는 건 저만이 아닙니다. AI에게 작업을 맡길 때도 이 기록을 함께 참조하도록 했습니다. 과거에 어떤 실수를 왜 했는지를 AI가 알고 있으면, 같은 함정에 다시 빠지는 코드를 만들 확률이 눈에 띄게 줄었습니다. 결국 TROUBLESHOOTING.md는 저와 AI가 함께 보는 실패의 지도가 된 셈입니다.</p>
<hr>
<h2 id="프로젝트-①-toonify--인스타툰-새-화-알림-앱">프로젝트 ① Toonify — 인스타툰 새 화 알림 앱</h2>
<p><strong>인스타그램 인스타툰의 새 에피소드를 자동 감지해 푸시 알림으로 알려주는 React Native 앱</strong>입니다.</p>
<p>재미있는 인스타툰을 발견해도 다음 편이 올라왔는지 매번 직접 확인해야 하는 불편함에서 출발했습니다. 관심 작가를 등록해두면 새 화가 올라올 때 알림을 받을 수 있게 만들었습니다.</p>
<p>기술적으로 가장 고민한 지점은 <strong>비용</strong>이었습니다. 별도 서버 없이 GitHub Actions를 3시간 배치 서버로 활용했고, 캡션과 OCR을 결합한 2단계 감지 엔진을 설계했습니다.</p>
<p>초기에는 매 사이클마다 OCR을 최대 12회씩 호출하는 구조라 무료 한도가 금방 소진됐는데, &quot;마지막으로 처리한 지점을 기억하는&quot; 설계로 <strong>OCR 호출을 약 93% 절감</strong>했습니다.</p>
<p>온디바이스 OCR(ML Kit)을 도입했다가 인식률 한계로 되돌린 판단 과정도 함께 담았습니다.</p>
<blockquote>
<p>🔗 <strong><a href="https://www.choiyunsung.dev/projects/toon-notifier/">Toonify 프로젝트 자세히 보기</a></strong>
🔗 Live Demo: 
🍎 iOS : <a href="https://apps.apple.com/kr/app/toonify/id6794237802">https://apps.apple.com/kr/app/toonify/id6794237802</a>
🤖 Android : 심사중..
Stack: React Native, Expo, Supabase, GitHub Actions, OCR API, Expo Push API</p>
</blockquote>
<blockquote>
<p>회고록 작성중..</p>
</blockquote>
<hr>
<h2 id="프로젝트-②-crossstitch--github-커밋으로-그리는-픽셀-아트">프로젝트 ② CrossStitch — GitHub 커밋으로 그리는 픽셀 아트</h2>
<p><strong>GitHub 커밋 기록으로 20×20 픽셀 아트를 만들어 README에 삽입할 수 있는 웹 서비스</strong>입니다.</p>
<p>GitHub 프로필 README를 개성 있게 꾸미고 싶어도 마땅한 방법이 없다는 데서 출발했습니다. &quot;내 커밋으로 픽셀 아트를 그려 README에 박으면 어떨까?&quot;라는 아이디어였습니다. </p>
<p>특히 CHALLENGE 모드에서는 이번 달 실제 커밋 수만큼만 셀을 채울 수 있어, 커밋을 더 할수록 그림이 완성되는 구조로 잔디 심는 동기를 자극합니다.</p>
<p>기술적으로는 <strong>보안과 자동화</strong>에 집중했습니다. GitHub OAuth 토큰을 <code>httpOnly</code> 쿠키로만 관리해 XSS 탈취 경로를 차단했고, 매달 1일 크론 잡이 커밋 수를 비교해 초과된 셀을 자연스럽게 정리하도록 설계했습니다. </p>
<p>별도 이미지 서버 없이 SVG를 서버사이드에서 즉시 생성해 마크다운 문법만으로 README에 삽입되게 했고, <code>main</code> 브랜치 보호부터 ESLint → 타입 체크 → 테스트 → 자동 코드리뷰로 이어지는 CI/CD 파이프라인까지 갖췄습니다.</p>
<blockquote>
<p>🔗 <strong><a href="https://www.choiyunsung.dev/projects/cross-stitch/">CrossStitch 프로젝트 자세히 보기</a></strong>
🔗 Live Demo: <a href="https://www.gitcrossstitch.site">https://www.gitcrossstitch.site</a>
Stack: Next.js 15, React 19, TypeScript, Firebase, Vitest, GitHub Actions, Vercel</p>
</blockquote>
<blockquote>
<p>회고록 작성중..</p>
</blockquote>
<hr>
<h2 id="마치며">마치며</h2>
<p>두 프로젝트는 플랫폼도, 다룬 기술도 완전히 달랐습니다.</p>
<p>하나는 앱에서 비용 최적화와 감지 엔진을 고민했고, 다른 하나는 웹에서 보안과 CI/CD 자동화를 파고들었습니다. 하지만 그 밑바탕에는 똑같은 두 가지 목표가 흐르고 있었습니다 — <strong>AI를 제대로 활용하는 법을 익히는 것</strong>, 그리고 <strong>서비스 전체를 관리할 수 있는 개발자가 되는 것</strong>.</p>
<p>돌아보면 화려한 결과물은 아닙니다. 하지만 내 불편에서 출발한 아이디어를 실제로 동작하는 서비스로 만들어냈고, 서로 다른 두 환경에서 문제를 스스로 정의하고 풀어냈으며, AI를 통제 아래 두고 협업하는 저만의 방식을 세울 수 있었습니다.</p>
<p>무엇보다, 혼자서도 끝까지 해낼 수 있다는 사실을 스스로 증명한 시간이었습니다. 각 프로젝트의 구체적인 이야기가 궁금하시다면, 위의 회고록을 참고부탁드립니다.</p>
<p>감사합니다.</p>
<blockquote>
<p>🔗 GitHub: <a href="https://github.com/GSMYunsung">https://github.com/GSMYunsung</a>
🔗 포트폴리오: <a href="https://www.choiyunsung.dev">https://www.choiyunsung.dev</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[고등학생 개발자 부터 지금까지의 회고록]]></title>
            <link>https://velog.io/@yunsung_/%EA%B3%A0%EB%93%B1%ED%95%99%EC%83%9D-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B6%80%ED%84%B0-%EC%A7%80%EA%B8%88%EA%B9%8C%EC%A7%80%EC%9D%98-%EC%9D%B4%EC%95%BC%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/%EA%B3%A0%EB%93%B1%ED%95%99%EC%83%9D-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B6%80%ED%84%B0-%EC%A7%80%EA%B8%88%EA%B9%8C%EC%A7%80%EC%9D%98-%EC%9D%B4%EC%95%BC%EA%B8%B0</guid>
            <pubDate>Sat, 01 Aug 2026 09:08:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>들어가기전에</p>
</blockquote>
<p>지금부터 적는 저의 이야기는 마이스터고를 진학하기전부터 개발자를 겪고 군대를 다녀오고 난 후 까지의 과장없는 저의 실제 이야기입니다.</p>
<p>언젠가는 써야지하다가 지금이 가장 큰 인생의 변곡점에 서있는것같아 앞으로의 계획도 세울겸 고등학교부터 지금까지의 이야기들을 정리해보기위해 글을 작성합니다.</p>
<h1 id="0intro">0.Intro</h1>
<h3 id="0-1-모든것의-시작">0-1. 모든것의 시작</h3>
<p>저는 꿈도 방향도 없는 사람이었습니다. 어릴 적부터 소심한 성격 탓에 늘 다른 사람들에게 끌려다녔고, 제 목소리를 내지 못한 채 그저 그런 하루하루를 흘려보내며 살아왔습니다.</p>
<p>그런 저에게 변화의 실마리가 찾아온 건 중학교 2학년의 어느 날이었습니다. 친하게 지내던 친구가 문득 이런 질문을 던졌습니다.</p>
<blockquote>
<p>&quot;너, 마이스터고라고 알아?&quot;</p>
</blockquote>
<p>당시 저는 평범하게 일반계 고등학교를 나와 대학에 진학하는 것이 당연한 길이라 여기고 있었습니다. </p>
<p>그런 제게 친구는 마이스터고 팸플릿을 건네며, 함께 입학설명회에 가보지 않겠느냐고 물었습니다. </p>
<p>솔직히 그다지 내키지는 않았습니다. 하지만 친구의 권유를 뿌리치기도 어려웠고, &quot;한번 가보지 뭐&quot; 하는 가벼운 마음으로 그 길에 따라나섰습니다.</p>
<h3 id="0-2-마이스터고를-자세히-알게되다">0-2. 마이스터고를 자세히 알게되다</h3>
<p>제가 처음 방문한 마이스터고는 놀랍게도 소프트웨어마이스터고가 아닌 &#39;항만물류마이스터고&#39;였습니다. 그곳에서 여러 입학 설명을 듣고, 학교생활까지 간접적으로나마 체험해 볼 수 있었습니다. 하지만 항만물류라는 분야는 제게 좀처럼 와닿지 않았고, 결국 관심은 입학설명회를 끝으로 이어지지 못했습니다.</p>
<p>그럼에도 그날의 경험은 제게 적지 않은 흔적을 남겼습니다. </p>
<p>취업연계형 교육이라는, 그동안 상상해 본 적 없던 길이 존재한다는 사실 자체가 큰 충격이었기 때문입니다. </p>
<p>인문계 고등학교에 진학해 대학으로 가는 것만이 당연하다 여기던 제 생각은, 그렇게 조금씩 흔들리며 달라지기 시작했습니다.</p>
<h3 id="0-3-소프트웨어-분야에-흥미를-느끼다">0-3. 소프트웨어 분야에 흥미를 느끼다</h3>
<p>비록 항만물류마이스터고 자체에는 흥미를 느끼지 못했지만, 마이스터고라는 존재를 알게 된 그 순간부터 제 안에서는 무언가가 조용히 움직이기 시작했습니다. </p>
<p>저는 마치 무언가에 홀린 듯 전국에 어떤 마이스터고들이 있는지 하나하나 찾아보기 시작했습니다. </p>
<p>기계, 제빵, 조선, 에너지처럼 그동안 이름조차 몰랐던 다양한 분야의 학교들이 있었고, 그 목록을 들여다보는 것만으로도 묘한 설렘이 차올랐습니다.</p>
<p>아무런 꿈도 없던 제게, 남들보다 한발 먼저 분야를 정하고 경력을 쌓아 나갈 수 있다는 사실은 그 자체로 엄청난 동기가 되었습니다. </p>
<p>목표 없이 흘려보내던 시간에 처음으로 방향이라는 것이 생기는 기분이었습니다.그렇게 여러 학교를 살펴보던 어느 순간, 문득 어릴 적부터 제가 가장 좋아했던 것이 게임과 컴퓨터였다는 사실이 떠올랐습니다. </p>
<p>늘 곁에 있어 당연하게 여겼던 그 관심사가, 그제야 하나의 진로로 또렷하게 보이기 시작한 것입니다. </p>
<p>저는 곧장 소프트웨어마이스터고를 찾아보았고, 마침내 그 입학설명회의 문을 두드리게 되었습니다.</p>
<h3 id="0-4-진짜-내가-원하는게-뭔지-찾다">0-4. 진짜 내가 원하는게 뭔지 찾다</h3>
<p>이후 입학설명회에서 교육과정에 대한 설명을 듣고, 앞으로 지내게 될 기숙사와 학교생활 이야기까지 하나하나 접하다 보니, 개발자라는 직업이 얼마나 매력적이고 흥미로운 일인지 조금씩 실감이 나기 시작했습니다. </p>
<p>세상에 없던 것을 내 손으로 만들어 낼 수 있고, 그렇게 만든 것으로 누군가의 불편함을 덜어 줄 수도 있는 직업. </p>
<p>그 사실을 알게 된 순간, 막연했던 관심은 &quot;바로 이거다&quot;라는 확신으로 바뀌었습니다.</p>
<h3 id="0-5-학교에-입학하기위해-노력하다">0-5. 학교에 입학하기위해 노력하다</h3>
<p>이후 전 목적도 생겼겠다 마이스터고에 입학하기위해 학교에 계신 진로상담선생님께 도움을 받기위해 찾아갔습니다. </p>
<p>성적 및 앞으로의 진로등 제 고민을 털어놓은 후 상담을 통해 진로에 대한 확신을 얻은 뒤, 가장 먼저 마주한 현실은 낮은 성적이었습니다. </p>
<p>마이스터고에 진학하기 위해서는 성적 향상이 반드시 필요했기에, 저는 이를 목표로 삼고 최선을 다했습니다. </p>
<p>그 결과 상위 50% 정도였던 성적을 상위 30%까지 끌어올릴 수 있었습니다. 여기에 그치지 않고, 입학 후 코딩을 배울 때 빠르게 적응하기 위해 코딩 학원을 다니며 미리 기초 실력을 쌓았습니다.</p>
<h1 id="1-학교생활-중-여러-우여곡절을-겪다">1. 학교생활 중 여러 우여곡절을 겪다</h1>
<h3 id="1-1-준비-그리고-입학">1-1. 준비, 그리고 입학</h3>
<p>그렇게 나름의 준비를 마치고 입학한 소프트웨어마이스터고에서의 생활이 시작되었습니다. 저는 학교에서 평범하다면 평범한 학생이었지만, 개발을 좋아하는 마음만큼은 남들 못지않았습니다.</p>
<p>다만 학교생활 내내 저를 따라다닌 고민이 하나 있었습니다. 정작 제가 어떤 개발 분야를 하고 싶은지 감을 잡지 못했다는 것이었습니다. 그래도 언젠가는 취업을 해야 했기에, 마냥 고민만 하고 있을 수는 없었습니다.</p>
<h3 id="1-2-예상치-못한-시작-코로나">1-2. 예상치 못한 시작, 코로나</h3>
<p>하지만 학교생활은 제 예상과 다르게 흘러갔습니다. </p>
<p>코로나가 터지면서 한동안 집에서 화상통화로 수업을 들어야 했기 때문입니다. </p>
<p>솔직히 그때는 원격으로 진행되는 수업 내용을 제대로 집중해서 듣지 못했습니다. </p>
<p>그러다 이후 등교가 재개되어 학교에서 직접 수업을 듣게 되었을 때, 놓쳤던 부분들을 따라잡기 위해 남들보다 몇 배는 더 공부해야 했습니다. 돌이켜보면 그 시기의 부담이 결코 가볍지는 않았습니다.</p>
<h3 id="1-3-게임-개발로-내디딘-첫걸음">1-3. 게임 개발로 내디딘 첫걸음</h3>
<p>처음에는 개발이라는 것 자체가 좋았고, 그중에서도 게임 개발에 마음이 끌렸습니다. </p>
<p>그래서 게임 개발부터 시작하게 되었고, 그 노력의 결과로 운이좋게 교내 대회에서 상을 받기도 했습니다. </p>
<p>좋아하는 일로 성과까지 얻으니 개발에 대한 애정은 더욱 깊어졌습니다.</p>
<h3 id="1-4-방향성에-대한-고민">1-4. 방향성에 대한 고민</h3>
<p>하지만 게임 개발을 이어갈수록 무언가 저와는 방향이 맞지 않는다는 느낌이 들기 시작했습니다. </p>
<p>그러던 중 저는 제가 개발한 것이 화면에 직접 그려지고 눈에 보이는 순간을 특히 좋아한다는 것을 깨달았습니다. </p>
<p>결과물이 눈앞에 시각적으로 드러나는 과정에서 큰 흥미와 성취감을 느꼈던 것입니다.</p>
<h3 id="1-5-한-분야에-정착하지-못하다">1-5. 한 분야에 정착하지 못하다</h3>
<p>이러한 성향 때문인지, 다른 친구들은 1학년 때부터 하고 싶은 분야(백엔드, 프론트엔드, AI, 하드웨어 등)를 미리 정하고 2학년부터 그 분야를 깊게 파고들었던 반면, 저는 게임 개발로 시작해 안드로이드, 웹 프론트엔드까지 두루 거칠 만큼 한 분야에 쉽게 정착하지 못했습니다. 그러다 보니 특정 기술을 깊이 있게 다뤄보지 못했고, 이대로 가다가는 취업에 불리하겠다는 생각이 점점 커졌습니다.</p>
<h3 id="1-6-안드로이드-네이티브-개발자를-향해">1-6. 안드로이드 네이티브 개발자를 향해</h3>
<p>화면에 직접 그려지는 개발을 좋아했던 만큼, 저는 자연스럽게 안드로이드와 웹 프론트엔드 사이에서 진로를 고민하게 되었습니다. 그리고 그 두 갈래 중, 상대적으로 경쟁력이 있다고 판단한 안드로이드 네이티브 개발자에 도전해보고 싶다는 마음을 굳혔습니다.</p>
<p>그렇게 방향을 정한 뒤, 저는 안드로이드 분야를 집중적으로 공부하기 시작했습니다. 교내외 행사에 적극적으로 참여했고, 개인적으로도 여러 사이드 프로젝트를 만들어 나갔습니다. 이 과정에서 저는 사이드 프로젝트를 만드는 일 자체를 좋아했고, 또 즐겼습니다. (물론 오류를 마주하는 순간만큼은 그리 반갑지는 않았습니다)</p>
<h3 id="1-7-우여곡절-끝에-얻은-결실">1-7. 우여곡절 끝에 얻은 결실</h3>
<p>지금 돌아보면 대단히 멋진 결과물을 만들어내지는 못했습니다. 그럼에도 개발을 좋아하는 저의 진심을 좋게 봐주신 기업이 있었고, 덕분에 저는 3학년 때 취업에 성공할 수 있었습니다. 그렇게 저는 설렘과 두려움이 뒤섞인 채, 저의 진짜 개발자로서의 여정이 시작되었습니다.</p>
<h1 id="2-첫-회사부터-이직까지">2. 첫 회사부터 이직까지</h1>
<h3 id="2-1-첫-회사에서의-이야기">2-1. 첫 회사에서의 이야기</h3>
<p>첫 번째 회사에서 겪었던 이야기들 — 처음 맡았던 안드로이드 키오스크 프로젝트, 웹과 모바일을 동시에 공부하며 성장했던 시간, 10만 명이 넘는 사용자가 이용하는 서비스를 유지보수하며 번아웃과 성장을 함께 겪었던 순간들, 그리고 제가 추구하는 개발 가치와 회사가 바라는 업무 사이에서 고민하다 결국 이직을 결심하기까지의 과정 — 은 아래 글에 따로 정리해 두었습니다.</p>
<blockquote>
<p>👉 <a href="https://velog.io/@yunsung_/%EA%B3%A0%EC%A1%B8%EB%B6%80%ED%84%B0-%EC%B7%A8%EC%97%85-%EB%B0%8F-2%EB%85%84%EC%B0%A8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%9D%B4%EC%A7%81%EC%9D%84-%EC%83%9D%EA%B0%81%ED%95%98%EA%B8%B0%EA%B9%8C%EC%A7%80">고졸부터 취업 및 2년차 개발자가 이직을 생각하기까지..</a></p>
</blockquote>
<h3 id="2-2-산업기능요원이라는-제약-속에서">2-2. 산업기능요원이라는 제약 속에서</h3>
<p>첫 회사에서 근무할 당시, 저는 산업기능요원 자격으로 일하고 있었습니다. </p>
<p>이러한 신분 특성상 이직을 결정하는 일은 결코 쉽지 않았습니다. </p>
<p>그럼에도 제가 추구했던 코드리뷰 문화, 그리고 제대로 된 트래픽을 경험해보고 싶다는 마음까지 막을 수는 없었습니다.</p>
<p>물론 회사에서도 코드리뷰 문화를 정착시키려는 노력을 하지 않았던 것은 아닙니다. </p>
<p>다만 빠르게 PoC 과정을 거치고 여러 프로젝트를 동시에 맡아야 하는 환경에서는 분명한 한계가 있었습니다. </p>
<p>결국 저는 이러한 한계를 극복하기 위해 이직을 선택하게 되었습니다.</p>
<h3 id="2-3-준비-그리고-합격">2-3. 준비, 그리고 합격</h3>
<p>이직을 마음먹은 뒤로는 회사를 다니면서 틈틈이 면접을 준비했고, 여러 회사에 지원할 이력서를 다듬어 나갔습니다. </p>
<p>쉽지 않은 병행이었지만, 목표가 분명했기에 꾸준히 이어갈 수 있었습니다.</p>
<p>그 결과, 제가 생각하기에 최적의 조건을 갖춘 회사 </p>
<blockquote>
<p>코드리뷰 문화가 정착되어 있고, 규모 있는 트래픽을 경험할 수 있는 곳 </p>
</blockquote>
<p>에 합격할 수 있었습니다.</p>
<h1 id="3-두-번째-회사-그리고-마주한-벽">3. 두 번째 회사, 그리고 마주한 벽</h1>
<h3 id="3-1-낯선-방식-앞에서의-당황">3-1. 낯선 방식 앞에서의 당황</h3>
<p>두 번째 회사에 입사한 뒤, 저는 적잖이 당황했습니다. </p>
<p>지금까지는 거의 체계 없이 홀로 관리하던 프로젝트들을 다뤄왔는데, 이곳에서는 진짜 서비스를 여러 사람이 함께 만들어가는 협업 프로젝트를 마주하게 되었기 때문입니다. 일하는 방식 자체를 바꿔야 하는 상황이었습니다.</p>
<p>코드는 몇천 줄을 넘어갔고, 오래된 레거시 코드도 곳곳에 남아 있었습니다. </p>
<p>저는 그 레거시를 이해하고, 이를 마이그레이션하는 작업까지 거쳐야 했습니다.</p>
<h3 id="3-2-내가-맡았던-일들">3-2. 내가 맡았던 일들</h3>
<p>두 번째 회사에서 제가 맡았던 일은 다음과 같습니다.</p>
<p>Pug 템플릿 기반의 레거시 화면을 React 컴포넌트로 이식하는 작업을 담당했는데, 단순히 화면을 옮기는 데 그치지 않고 구조까지 신경 쓰며 개발해야 했습니다. </p>
<p>또한, 플랫폼에서 새로운 이벤트가 나올 때마다 그에 맞춰 UI와 내부 정책을 새롭게 변경하는 이벤트 대응 업무를 맡았고, 매일 QA에서 발견되는 오류들을 수정하는 일도 함께 진행했습니다.</p>
<p>3-3. 스스로 마주한 한계</p>
<p>하지만 이 과정에서 저는 제 부족함을 정면으로 마주하게 되었습니다. 무엇보다 제 코드 해석 능력, 그리고 주변에서 늘 강조하던 &quot;코드를 넓게 보는 시선&quot;을 저 스스로 제대로 구사하지 못한다는 사실을 깨달았습니다.</p>
<p>매일같이 올라오는 버그 해결 토큰을 처리할 때도 코드를 해석하는 능력이 부족했고, 업무적으로도 제가 예상했던 것보다 훨씬 복잡한 구현이나, 내부 정책을 깊이 이해해야 하는 기능들이 토큰으로 할당되어 적잖이 힘들었습니다. </p>
<p>그러다 보니 스스로 야근을 자처하는 날도 점점 늘어갔습니다.</p>
<h3 id="3-4-짧게-마무리된-도전">3-4. 짧게 마무리된 도전</h3>
<p>무엇보다 회사가 제게 거는 기대가 컸던 만큼, 그 기대를 온전히 채우지 못했다는 부담이 컸습니다. </p>
<p>제 능력 밖의 일도 몇 가지 있었고, 결국 저는 입사 당시 회사가 기대했던 퍼포먼스를 충분히 보여주지 못한채 6개월 만에 그만두게 되었습니다.</p>
<p>돌이켜보면 아쉬움이 남는 시간이었지만, 제가 무엇이 부족한지를 분명하게 마주할 수 있었던 시기이기도 했습니다.</p>
<h1 id="4-산업기능요원에서-군인이되다">4. 산업기능요원에서 군인이되다</h1>
<p>다만, 회사를 그만두게 되면서 저는 산업기능요원 자격을 잃고 군대에 가야 하는 상황에 놓였습니다.</p>
<p>처음에는 군 복무로 인해 생길 경력 단절이 두려웠습니다. </p>
<p>다른 친구들이 한창 스펙을 쌓아갈 시기에 군대에 가야 하는 제 자신이 싫기도 했습니다. </p>
<p>하지만 계속 고민하다 보니, 어차피 언젠가는 해결해야 할 일이라면 차라리 하나의 경험으로 받아들이자는 마음이 들었습니다. </p>
<p>산업기능요원을 시작할 때 다녀왔던 훈련소의 연장선이라 여기고 마음을 비우니 한결 나아졌습니다. </p>
<p>다만 훈련소부터 다시 시작해야 한다는 사실만큼은 여전히 막막하게 느껴졌습니다.</p>
<p>이후 저는 훈련소를 처음부터 끝까지 이수한 뒤 자대로 배치되었습니다. </p>
<p>그런데 이때 생각지도 못하게 군사경찰이라는 보직을 받게 되어, 말로만 듣던 군대를 색다른 자리에서 경험할 수 있었습니다. </p>
<p>다만 남들보다 빨리 전역한다는 사실 때문에 처음부터 상병으로 군 생활을 시작하게 되었는데, 이 점만큼은 분명 쉽지 않은 부분이었습니다.</p>
<p>하지만 이렇게 불리한 상황들을 겪어낸 덕분에 오히려 사회생활 능력만큼은 눈에 띄게 성장했습니다. </p>
<p>이제는 누구와도 좋은 관계를 유지할 수 있겠다는 자신감까지 생겼을 정도입니다. </p>
<p>그렇게 저는 짧지만 강렬했던 군 생활을 마치고 무사히 전역하게 되었습니다.</p>
<h1 id="5-다시-시작하는-개발자-취업기">5. 다시 시작하는 개발자 취업기</h1>
<h3 id="5-1-달라진-세상-그리고-닭갈비집-주방">5-1. 달라진 세상, 그리고 닭갈비집 주방</h3>
<p>군대를 전역한 이후, 저는 다시 취업을 준비하기로 마음먹었습니다. 하지만 제가 군 복무를 하던 그 짧은 사이, 세상은 빠르게 달라져 있었습니다. </p>
<p>얼마 전까지만 해도 코드의 맥락조차 제대로 파악하지 못하던 AI가, 이제는 프롬프트 하나만으로 앱 하나를 출시할 수 있을 만큼 강력한 무기가 되어 있었습니다. </p>
<p>이런 변화를 마주하고 나니, 저 역시 새로운 기술을 습득할 시간이 필요하다는 것을 절감했습니다.</p>
<p>문제는 그 시간을 버티려면 당장 돈을 벌어야 한다는 것이었습니다.</p>
<p>그래서 — 지금 생각하면 그때 무슨 마음이었는지 모르겠지만 — 저는 닭갈비집 주방 알바에 지원했습니다. </p>
<p>하루 12시간씩 생전 처음 해보는 주방 일을 하며, 돈을 번다는 것이 이렇게나 힘든 일인지 그때 처음 알았습니다.</p>
<p>원래는 퇴근 후 매일 공부하기로 다짐했었습니다. 하지만 하루 12시간을 꼬박 일하고 나면 &#39;오늘은 그냥 쉬자&#39;는 생각에 밀려, 결국 주방에서만 시간을 보내는 날들이 이어졌습니다.</p>
<h3 id="5-2-다시-개발-앞에-서다">5-2. 다시 개발 앞에 서다</h3>
<p>그러던 어느 날, 목적을 잃은 채 멍하니 설거지를 하고 있는 제 모습이 문득 눈에 들어왔습니다. </p>
<p>그 순간 &#39;아, 이제는 정말 개발을 하지 않으면 안 되겠다&#39;는 생각이 강하게 들었습니다. </p>
<p>그래서 전부터 구상해두었던 개발 아이디어 두 가지를 AI와 협업하며 다시 한번 개발에 도전해보기로 마음먹었습니다.</p>
<p>결심한 뒤로는 12시간씩 일하던 주방 일을 그만두고 근무 시간이 짧은 알바로 전향한 뒤, 개발에만 온전히 전념하게 되었습니다. </p>
<p>이후 두 아이디어를 각각 웹과 앱으로 개발하며, 최초 기획부터 디자인, 기능 구현, 검수, 출시에 이르기까지 개발의 모든 흐름을 직접 경험하게 되었습니다. </p>
<p>그 과정을 거치며 저는 한층 더 성장한 개발자가 될 수 있었습니다. (이에 대한 회고록은 이후에 따로 작성하겠습니다.)</p>
<p>군대에서 처음 계획했던 새로운 프로젝트와 역량 강화, 그리고 이력서 점검까지 마치고 &#39;본격적으로 다시 개발자로 취업하기&#39;라는 목표에 이르기까지, 전역 이후 장장 6개월이라는 시간이 걸렸습니다. </p>
<p>그리고 저는 이제야 그 출발선에 서게 되었습니다. </p>
<p>현재 저는 그 목표를 향해 구직 활동을 이어가고 있습니다.</p>
<hr>
<h1 id="마치며">마치며</h1>
<p>언젠가는 써야지, 써야지 하고 미뤄왔던 이야기를 이제야 작성했습니다.</p>
<p>여러 일들이 어느 정도 정리가 된 지금이, 지난 시간을 돌아보기에 알맞은 때라는 생각이 들었기 때문입니다.</p>
<p>돌이켜보면 고등학생 시절의 제가 감히 상상조차 하지 못했던 일들의 연속이었습니다. 정말 다양한 경험을 하고, 수많은 사람들을 만나며, 그 하나하나가 제게는 더없이 값진 시간이었습니다.</p>
<p>지금 저는 또 하나의 변곡점 위에 서 있습니다. 앞으로 어떤 이야기가 이어질지는 아직 알 수 없지만, 나중에 다시 돌아볼때 더 성장한 제가 되면 좋겠습니다.</p>
<p>긴글 읽어주셔서 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[고졸부터 취업 및 2년차 개발자가 이직을 생각하기까지..]]></title>
            <link>https://velog.io/@yunsung_/%EA%B3%A0%EC%A1%B8%EB%B6%80%ED%84%B0-%EC%B7%A8%EC%97%85-%EB%B0%8F-2%EB%85%84%EC%B0%A8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%9D%B4%EC%A7%81%EC%9D%84-%EC%83%9D%EA%B0%81%ED%95%98%EA%B8%B0%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@yunsung_/%EA%B3%A0%EC%A1%B8%EB%B6%80%ED%84%B0-%EC%B7%A8%EC%97%85-%EB%B0%8F-2%EB%85%84%EC%B0%A8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%9D%B4%EC%A7%81%EC%9D%84-%EC%83%9D%EA%B0%81%ED%95%98%EA%B8%B0%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 09 May 2024 05:22:43 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며..</h2>
<blockquote>
<p>이 글은 제가 마이스터고를 거치며 취직후 회사를 다니며 겪었던 경험을 공유합니다.</p>
</blockquote>
<blockquote>
<p>편하게 한 고졸 개발자가 풀어주는 썰이라고 생각하시면 좋을듯합니다. 🥸</p>
</blockquote>
<hr>
<h2 id="고등학생-개발자의-학교개발-생활-🚐">고등학생 개발자의 학교개발 생활 🚐</h2>
<h3 id="나는-결정장애">나는.. 결정장애?</h3>
<p>저의 경우 특목고인 소프트웨어마이스터고를 졸업하였습니다. 저는 학교에서 평범하다면 평범한? 학생이였고 고등학생 당시에도 역시 개발을 좋아하는 한 사람이였습니다.</p>
<p>다만, 여러 고민이 있었는데 내가 하고싶은 개발 분야를 감 잡지 못했다는것이였습니다.</p>
<h3 id="그래도-취직은-해야지">그래도 취직은 해야지..</h3>
<p>다른 친구들은 1학년부터 하고싶은 분야 (back-end , front-end, ai , hw 등등..) 를 미리 정해 다음 학년인 2학년부터 해당 분야를 파는것에 반해 저는 게임개발을 먼저하다 안드로이드 , 웹 프론트엔드를 거칠정도로 잘 한 분야에 정착하지 못하는 성격을 지녔고 이러다보니, 한 기술에 대해 깊게 정착하지 못하여 이대로는 취업에 불리할거같아 안드로이드 분야를 적극적으로 공부하였고 교내 및 교외 행사 , 개인적으로 여러 사이드 프로젝트도 만들었습니다.</p>
<p>이때에 저는 사이드 프로젝트를 만드는것을 좋아했고 또 즐겼습니다. ( 오류는 좋아하지 않았지만..)</p>
<p>사이드 프로젝트를 엄청나게 멋있게 만들지 못했지만 그래도 개발을 좋아하는 마음을 좋게 봐주신 기업이 있어 3학년때 취직을 할 수 있었던것 같습니다.</p>
<p>이에 대한 글은 따로 작성해보겠습니다 :)</p>
<h2 id="나의-처음-직장이자-새로운-사회-📕">나의 처음 직장이자 새로운 사회 📕</h2>
<blockquote>
<p>아직 미처 나에게 주어진 사회가 끝나기도 전에 새로운 사회를 맞이하는 도전</p>
</blockquote>
<h3 id="새로운-시작은-늘-설레지만은-않지">새로운 시작은.. 늘 설레지만은 않지</h3>
<p>이때의 저는 굉장히 마음이 두근거리고 또한 두려웠습니다. 그 이유는 고등학교도 졸업을 안한 내가 회사에 소속되며 회사 업무를 할 수 있었기 때문입니다.</p>
<p>또한, 고등학생인 저로써 무언가 실수하지않을까 혹은 회사의 업무를 해내지 못하면 어쩔지.. 고민을 많이 했었던거 같습니다.</p>
<h3 id="안드로이드-키오스크">안드로이드 키오스크..?</h3>
<p>회사에서 제가 가장 처음으로 받은 업무는 AI 동영상을 이용한 롤베이스 형태의 안드로이드 키오스크 였습니다. </p>
<p>키오스크에 말을 하면 STT 로 말을 인식하고 해당되는 질문에 대한 답을 해주는 간단한 롤베이스 였으나 기존에 제가 제작하던 방식과 많이 달라 파악하는데 시간이 걸렸던 기억이 있습니다.</p>
<p>추가적인 질문지 애니메이션이나 인식 후 자동 스크롤 및 여러 기능 최적화를 해보니 이제 진짜 내가 회사원이구나.. 하는 생각도 들고 상당히 흥분된 상태에서 개발 했었습니다.
또한 , 해당 앱은 키오스크에 들어가는앱이라 용량적인 문제도 있어 추가로 들어가야하는 애니메이션을 android compose 로 직접 구현했던 기억이 있습니다.</p>
<p>그래도 처음 맡았던 프로젝트이니 만큼 애정이 가는 프로젝트였기도 하고 사내에서 해당 키오스크를 이용해 CES 관련 사업도 진행한것을 공유 받아서 뿌듯하기도 했던 프로젝트였습니다 :)</p>
<h3 id="web--mobile-동시-공부">Web , Mobile 동시 공부</h3>
<blockquote>
<p>모바일 개발자에서 웹 개발자로?</p>
</blockquote>
<p>안드로이드 키오스크 프로젝트를 무사히 마친후 저에게 주어진 업무는 식단관리 관련 솔루션 개발이였습니다. 다만, 이 프로젝트에서 APP은 이미 만들어진 보일러플레이트 코드가 존재하였고 WEB 은 만들어지지 않은 상태였습니다.</p>
<p>그래서 APP 에서 사용되어진 라이브러리인 React Native 를 공부하여야하였고 추가적으로 웹도 만들어야 하는 상황이였기 때문에 React 를 먼저 공부하게 되었습니다.</p>
<p>React 는 이미 제가 학교에서도 몇번 경험해보았었고 JS 나 TS 도 경험해 보았어서 2주간의 업무 병행 학습으로 개발 방향성이 잡혔습니다.</p>
<p>다만, 이러한 학습에서 React Native 도 어느정도 진행해볼 수 있었지만 Web 은 프로젝트 특성상 백엔드 , 프론트엔드의 코드를 한번에 관리할 수 있는 SSR 프레임워크인 Remix 를 사용했습니다. 다만, 이때 Remix 를 도입한것은 프레임워크를 비교하며 결정한것이 아닌 최근 트렌딩에 Remix 프레임워크가 자주 보이고 또한 흥미가 생겨 도입하게 되었습니다. </p>
<p>( Next.js 를 도입할껄.. 추후 후회했습니다..)</p>
<p>이때 두가지 플렛폼을 동시에 맡으며 여러가지 라이브러리나 코드 스타일을 내 의지대로 도입도 해보고 시행 착오도 겪으며 많이 성장했던거 같습니다.</p>
<h3 id="1년차-주니어인-내가-여기선-10만-서비스-관리까지">1년차 주니어인 내가 여기선 10만+@ 서비스 관리까지?</h3>
<p>어느날, 저에게 회사에서 겪었던 업무중 가장 고난을 겪고 , 성장아닌 성장을 했던 프로젝트가 할당되었습니다.</p>
<p>해당 프로젝트는 회사에서 외주로 진행하는 외부 프로그램 유지보수였는데 해당 프로그램은 웹 / 관리자 웹 / 사용자 멤버십 웹으로 이루어져있었습니다.</p>
<p>다만, 프로그램에서 제가 경험해보지못한 JSP 및 각종 Spring 프레임워크를 사용하였었습니다. 저로써는 프로젝트를 받고 당황했습니다.</p>
<p>당장 지금부터 해당 프로젝트에 사용된 프레임 워크를 모두 공부하고 (특정 회사의 private 프레임 워크도 포함되어 있어 쉽지않았습니다..) 명목상 유지보수지만 신규 개발도 앞으로 한달 안에 해야하는 상황이였고 추가로 이미 10만+@ 사용자가 사용하고 있는 서비스라 부담도 많았습니다.</p>
<p>프로젝트를 하며 걱정했던대로 제가 모르는 부분에서 오류가 터지는것이 주를 이뤘고 프로젝트에서 사용하던 JAVA 버전이나 여러 프레임워크들이 노후화되어 유지보수하는데에도 난항을 겪었습니다.</p>
<p>그럼에도 불구하고 저와 같이 프로젝트를 진행하던 Back-end 개발자분과 함께 멤버십 회원 로그인 방식 변경 및 여러 이슈 트레킹 + 신규 기능 개발등 여러 시행착오를 겪으며 100+@ 의 pr 이 생겼을정도로 성장도 많이 되었지만 번아웃도 함께왔던 프로젝트였습니다.</p>
<h3 id="내가-원하는-성장-≠-회사가-바라는-업무-처리">내가 원하는 성장 ≠ 회사가 바라는 업무 처리</h3>
<p>제가 원래 원하는 성장 및 개발 방향은 팀원간의 소통이 주력이 되며 가독성이 좋은코드가 무엇인지 연구하며 클린코드 방향성이 지켜지며 개발하는것이였습니다.</p>
<p>다만, 현재 회사에서는 한가지를 깊게 유지보수하는것이 아닌 여러 프로젝트를 동시다발적으로 개발하여 업무 던지기 식으로 진행되었습니다. 이러한 환경에서 저는 업무를 하던 어느순간 제가 원하는 가치의 성장을 할 수 없다는것을 깨달았습니다. </p>
<p>( 나는 코드를 보고 수정 및 추가만 하는 사람인가..?)</p>
<p>또한, 개발 환경상으로 사수분이나 다른 코드를 봐주실 수 있는 개발자분이 존재하지 않다는 점 및 한번 프로젝트가 종료된 코드에 대해서 코드 클린을 안한다는점 + 저의 코드를 리뷰 받을 창구가 어디에도 없다는점<strong>(개인적인 공부에서도 주변에 도움을 받을 수 있는 환경이 없었다.)</strong> 에서 저의 성장은 매말라 있었다고 생각합니다.</p>
<p>그래서 이때부터 개인적으로 클린코드를 공부하여 새로운 프로젝트들은 처음 만들때 , 만들어 진후에 클린코드를 회사 코드에 적용시켰던거같습니다.</p>
<h3 id="클린코드-공부">클린코드 공부</h3>
<p>회사일을 하던 어느 순간.. 저는 문득 이런생각이 들었습니다.</p>
<blockquote>
<p><strong>내가 지금 일하며 쓰는 코드가 내가 추구하는 코드가 맞나?</strong></p>
</blockquote>
<p>회의감이 점점 더 커져가며 생각해보기론 바쁜 프로덕트에 맞추려는 제 자신만 보였고 제가 추구했던 &quot;개발자면 누구든지 볼 수 있는 코드&quot; 와는 거리가 멀었습니다. </p>
<p>실제로 제가 만들어놓은 컴포넌트들을 봐도 가독성 제로에 가까운 컴포넌트와 props drilling 현상으로 인해 의존성이 서로 연결되어버린 코드들.. 밖에 보이지 않았거든요.</p>
<p>그래서 차근차근 시작했습니다.</p>
<p>SOLID 기법을 이용한 react 에서 추구하는 이상적인 컴포넌트 생성 및 분리 방식 및 여러 디자인 패턴, 추가적인 web 내부 동작과정 / 테스트 코드 , 스타일 선언 방식 등등 </p>
<p>이전에 급하게 공부하느라 하지못했던 공부를 진행하였고 또한, 공부를 하며 이전에 풀리지 않았던 최적화 문제나 궁금했던 사항들도 여럿 알 수 있었습니다.</p>
<p>다만 , 제가 궁극적으로 추구했던 <strong>누구나 알 수 있는 코드</strong> 를 충족하기에는 다른 사람의 의견이 수용되지않은 제 주관적인 코드는 아직 부족했습니다. </p>
<p>🖥❤️ (코드로 토론을 해보고 싶은 내 마음) </p>
<p>개인적인 공부에서도 성장할 수 있는 한계를 느끼게 되었습니다..</p>
<hr>
<h2 id="결론">결론</h2>
<p>저는 위의 배경을 바탕으로 저는 코드리뷰가 존재하고 회사의 플랫폼 방향성 및 유저에게 직접 피드백을 받으며 일 할 수있는 환경에 대해 목말라지기 시작했습니다.</p>
<p>이 다음 글에서는 위의 배경을 바탕으로 시작했던 이직기에 대해 이야기해보겠습니다.</p>
<p>긴글 읽어주셔서 감사합니다!</p>
<p><strong>내가 원하는 성장을 얻기위해.. 또한, 내가 추구하는 가치를 위해..</strong></p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/3a89e4ad-77ae-45e7-8fc8-176f40f94680/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Bundler 간단하게 이해하기]]></title>
            <link>https://velog.io/@yunsung_/Bundler-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/Bundler-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 12 Apr 2024 06:00:54 GMT</pubDate>
            <description><![CDATA[<h2 id="번들러는-왜-세상에-나오게-되었을까">번들러는 왜 세상에 나오게 되었을까?</h2>
<p><strong>“초기의 웹사이트는 지금과 같이 크지 않았습니다.”</strong></p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/a813dacb-3bd4-4e14-a47a-d252fcf3e96a/image.png" alt=""></p>
<p>다만 지금은 컴퓨터의 사양도 더 좋아지고 네트워크 속도도 더 빨라지며 비약적인 기술적 성장이 이루워졌습니다.</p>
<p>위 배경에서 웹 어플리케이션의 규모는 더욱 커져갔습니다.</p>
<p>당연하겠지만 프로그램이 커짐에 따라 <strong>파일의 양과 크기가 다양</strong> 해지고 많아졌습니다.</p>
<p>이러한점은 기존에 사용하던 웹에 다양한 문제를 야기시켰습니다.</p>
<h3 id="1-1-중복된-이름으로-된-에러">1-1. 중복된 이름으로 된 에러</h3>
<p>대규모 웹페이지의 경우 <strong>수많은 JS 파일</strong>이 있고, <strong>여러사람이 협업하여 관리</strong>하다보니 함수나 변수명이 중복될 가능성이 있습니다.</p>
<p><strong>index.html</strong></p>
<pre><code class="language-html">&lt;head&gt;
    &lt;script src=&quot;./source/WordCode.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;./source/WordCode2.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Hello, Webpack&lt;/h1&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot;&gt;
        document.querySelector(&quot;#root&quot;).innerHTML = duplication;
    &lt;/script&gt;
&lt;/body&gt;</code></pre>
<p><strong>WordCode.js</strong></p>
<pre><code class="language-jsx">var duplication = &quot;Hello&quot;;</code></pre>
<p><strong>WordCode2.js</strong></p>
<pre><code class="language-jsx">var duplication = &quot;World&quot;;</code></pre>
<p>위 예제의 결과는 당연하게도 “word” 라는 변수가 중복되었기때문에 마지막에 import 되었던 </p>
<p>world.js 에 있는 word 변수의값인 <strong>“World”</strong> 가 렌더링 될것입니다.</p>
<p>이처럼 중복이 되는 함수나 변수명이 있다면 의도치않은 <strong>Side Effect</strong> 가 일어날 수 있습니다.</p>
<h3 id="1-2-파일로-인한-문제">1-2. 파일로 인한 문제</h3>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/cbd9a1dd-2ca1-4d91-931e-b8b5e127d6b2/image.png" alt=""></p>
<p>현재 프론트엔드를 공부하신분이면 아시겠지만 사용자가 웹 페이지를 들어가게 되면</p>
<p>DNS 서버에서 일련의 과정을 통해 해당 웹페이지가 올라가있는 IP 에서 웹 페이지 파일을 요청하게됩니다.</p>
<p>이과정에서 웹 어플리케이션을 구성하는 파일의 양이 많다면 사용자의 요청에 대해 응답하는 시간이 길어지게됩니다. 이때, 사용자 트레픽이 많아지면 응답을 제때 하지 못해 네트워크 병목현상이 일어날 수 있습니다.</p>
<p>해당 문제를 해결하기위해 하나의 JS 파일에 모든 로직이나 페이지를 몰아 작성한다면 위 문제를 어느정도 해결할 수있지만 개발자는 하나의 파일에서 수 천, 수 만줄의 코드를 유지보수 해야 하기 때문에 가독성 면에서 정말 정말 지옥이 펼쳐질것입니다.</p>
<p>즉 , <strong>파일이나 함수를 분리</strong>하면 개발자는 편하지만 <strong>실제 사용자에게 영향</strong>이가고</p>
<p>분리하지않고 <strong>한번에 관리</strong>하게되면 <strong>사용자에게 가는 영향은 줄어들겠지만 개발자 고통의 초행길</strong>일것입니다.</p>
<h2 id="번들러">번들러</h2>
<p><strong>번들러(Bundler)</strong> 는 <strong>이러한 과정</strong>을 <strong>효과적으로 해결</strong>하기위해 등장하였습니다.</p>
<p>번들러가 위 문제를 해결하기 위해 제시한 방향은 다음과 같습니다.</p>
<ul>
<li>여러개의 파일을 하나로 묶어주기</li>
</ul>
<p>음.. 여러개의 파일을 하나로 묶어준다?? 아직은 감이 재대로 오지 않습니다. </p>
<p>이해를 위해 여러 번들러중 가장 사용량이 높은 WebPack 을 중심으로 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/d6a9ece5-bf60-4dd0-bec4-5487508f75dd/image.png" alt=""></p>
<p>WebPack 은 애플리케이션에 필요한 모든 <strong>모듈</strong>을 알아서 매핑하며, 매핑 된 결과를 가지고 하나 이상의 번들(파일 묶음)을 생성합니다. </p>
<p>이를 이해하기 위해선 모듈의 개념을 알아야 합니다.</p>
<h2 id="모듈">모듈</h2>
<p>모듈의 사전적 정의는 다음과 같습니다.</p>
<blockquote>
<p>프로그램 내부를 기능별 단위로 분할한 부분</p>
</blockquote>
<p>우리가 사용하는 WebPack 이 동작하는 개념도 똑같습니다.</p>
<p>Javascript에서 말하는 모듈도 마찬가지입니다. 웹 애플리케이션을 구성하는 기능들을 분할한 단위라고 생각하면 됩니다. </p>
<p>즉, 코드를 기능에 따라 여러가지 파일로 <strong>분리 및 결합</strong>하는 것입니다.</p>
<h3 id="번들러의-동작-한눈에-보기">번들러의 동작 한눈에 보기</h3>
<p><strong>hello.js</strong></p>
<pre><code>var word = &quot;Hello&quot;;

export default word;</code></pre><p><strong>world.js</strong></p>
<pre><code>var word = &quot;World&quot;;

export default word;</code></pre><p><strong>index.html</strong></p>
<pre><code>&lt;body&gt;
    &lt;h1&gt;Hello, Webpack&lt;/h1&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot;&gt;
        import hello_word from &#39;./source/hello.js&#39;;
        import world_word from &#39;./source/world.js&#39;;
        document.querySelector(&quot;#root&quot;).innerHTML = hello_word + &quot; &quot; + world_word;

    &lt;/script&gt;
&lt;/body&gt;</code></pre><p>위의 코드를 보면 WebPack 이 적용된 코드는 <strong>“export default”</strong> 로 export 될 부분을 미리 export 하고 </p>
<p>실제로 사용할 부분에서 import 로 받아오고있습니다.</p>
<p>이렇게 모듈을 사용한다면 하기와 같은 장점을 얻을 수 있습니다.</p>
<ul>
<li>자주 사용되는 코드를 별도의 파일로 만들어서 <strong>필요할 때마다 재활용</strong>할 수 있다.</li>
<li>코드를 개선하면 이를 사용하고 있는 <strong>모든 애플리케이션의 퍼포먼스가 개선</strong>된다.</li>
<li>코드 수정 시에 필요한 로직을 빠르게 찾을 수 있다. ( <strong>가독성이 강화</strong>된다. )</li>
<li><strong>필요한 로직만을 로드</strong>해서 메모리의 낭비를 줄일 수 있다.</li>
<li>한번 다운로드된 모듈은 웹브라우저에 의해서 저장되기 때문에 <strong>동일한 로직을 로드 할 때 시간과 네트워크 트래픽을 절약</strong> 할 수 있다. ( 브라우저에서만 해당 )</li>
</ul>
<h3 id="번들러의-동작--기능을-분할하는-번들러-">번들러의 동작 ( 기능을 분할하는 번들러 )</h3>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/928e11c6-9cf9-42a4-bef9-ca75b26788d1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/9bc8281d-a5cd-474c-85a0-5cd2b6f4e03c/image.png" alt=""></p>
<p>위의 그림을 보면 기존에 사용하던 2개의 js 를 index 에서 사용하는 modle 로 묶어 하나의 js 파일로 통합하였습니다. 이때, 종속성을 알아서 확인하여 사용하지 않은 파일은 포함하지 않습니다.</p>
<p>이에따라 우리는 다음과 같은 이점을 얻을 수 있습니다.</p>
<ul>
<li><p><strong>파일의 크기를 줄여 페이지 로딩을 빠르게 하는 효과</strong>를 발휘합니다. 
→ 이를 통해 <strong>네트워크 병목현상을 최소화</strong> 할 수 있습니다.</p>
</li>
<li><p>함수나 변수 이름의 중복이 있더라도 각각 다르게 <strong>import 를 해주기때문에 SideEffect 를 방지</strong>합니다.
→ 모듈단위 코딩의 이점인 <strong>코드의 가독성이나 유지보수를 신경</strong>쓰며 코딩이 가능합니다.</p>
</li>
</ul>
<hr>
<h2 id="그외의-특징">그외의 특징</h2>
<h3 id="es6-버전-이상의-스크립트를-사용-가능">ES6 버전 이상의 스크립트를 사용 가능</h3>
<p>webpack은 로더(babel-loader)를 통해 Babel이라는 ES6 이상의 자바스크립트 문법을 ES5 버전의 자바스크립트 문법으로 변환시켜주는 트랜스파일러를 사용할 수 있습니다. 이를 통해 ES5만 지원하는 오래된 브라우저에서도 ES6 이상의 문법으로 이루어진 JS 파일을 작동할 수 있게 해줍니다.</p>
<h3 id="sass를-사용-가능">SASS를 사용 가능</h3>
<p>webpack은 style-loader와 css-loader, sass-loader라는 로더를 사용하여 SASS를 CSS로 컴파일하여 사용할 수 있게 만들어줍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트코드 입문 주니어의 개념 공부하기]]></title>
            <link>https://velog.io/@yunsung_/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C-%EC%9E%85%EB%AC%B8-%EC%A3%BC%EB%8B%88%EC%96%B4%EC%9D%98-%EA%B0%9C%EB%85%90-%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BD%94%EB%93%9C-%EC%9E%85%EB%AC%B8-%EC%A3%BC%EB%8B%88%EC%96%B4%EC%9D%98-%EA%B0%9C%EB%85%90-%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 04 Apr 2024 07:30:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yunsung_/post/0fa78870-33e3-42ce-8a73-e93f974a9a2d/image.png" alt=""></p>
<blockquote>
<p><del>테</del>스트 테<del>스트 테테테텥테텥 테스트</del></p>
</blockquote>
<h2 id="들어가며">들어가며..</h2>
<p>요즘 채용공고를 보면 항상 우대사항 혹은 필수역량에 언급되는 기술스택이 있습니다.
바로 <del><strong>시니어같은 주니어</strong></del> 가 아니라 <strong>테스트 코드 작성</strong> 경험 혹은 여부이죠.</p>
<p>사실, 테스트 코드는 채용공고말고도 우리가 심심치않게 항상 말하는 주제이긴 합니다.
우리에게 익숙한 React 라이브러리에서도 컴포넌트 분리의 중요성 측면에서 &quot;테스트 코드의 작성이 쉬워진다&quot; 라는 장점이 있고요.</p>
<p>우리는 이렇듯 평소 테스트코드의 존재는 알았지만 테스트코드에 입문하기 껄끄러웠을수도 있습니다. 그 이유는</p>
<blockquote>
<ul>
<li>실제 구현도 시간이 부족한데 테스트 코드까지 작성하려면 시간이 너무 오래걸려! 😈</li>
<li>어플리케이션 변경 사항을 테스트코드에 까지 업데이트해야해서 테스트코드도 유지보수해야해.. 😵💫</li>
</ul>
</blockquote>
<p>하지만, 테스트코드에는 단점만 있는것이 아닌 장점도 있습니다. <del>나름 무서운 친구는 아니랍니다</del>
그럼 이번글에서 테스트코드와 같이 친해지는 시간을 가져보아요~</p>
<h2 id="테스트코드-공부-계기">테스트코드 공부 계기</h2>
<p>본인은 사실 테스트 코드말고도 아직 모르는게 많은 주니어 개발자입니다. 따라서 일단 여러가지 기술스택을 공부하며 프로젝트 설계 및 기능구현도 벅찬 상황이였고 이 상황에서 </p>
<blockquote>
<p><strong>TDD 관점</strong> 으로 코딩을 하는것은 매우 어렵다. </p>
</blockquote>
<p>라는 이야기와 또한 회사에 아직 <strong>테스트코드</strong> 기술이 도입되지 않았다는 이유로 테스트 코드 설계에 대한 내용을 배우지 않는것을 <strong>정당화</strong> 하였습니다.</p>
<p>하지만, 시간이 지나며 나름 회사에서 진행한 프로젝트도 몇 있고 해당 프로젝트를 진행하며 또 진행 후에 유지보수 및 인수인계를 하며 내가 짠 코드가 <strong>어떤 아키텍처로 설계되어 있으며</strong> 추가로 <strong>컴포넌트 하나하나마다 어떤 생각으로 코드를 설계</strong>하였는지 기억이 나지않아 하나하나 다시 코드를 봐야하는 상황이 생겼습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/6634bbe8-1aeb-4478-9bcd-0ad729b8f208/image.png" alt=""></p>
<p><del>전 개발자가 어떤사람이였길래 ㄷㄷ... 난 누군지 정말 모르겠다</del></p>
<p>그래서 테스트코드를 사용해 TDD 기법을 이용하면 나의 주니어 시기에 성장에 큰 도움이 될거같아 공부하게 되었습니다.</p>
<h2 id="테스트의-정의-및-테스트코드-사용-이유">테스트의 정의 및 테스트코드 사용 이유</h2>
<blockquote>
<p>설이나 이론이 실제로 들어맞는지를 확인하기 위해 다양한 조건 아래에서 여러 가지 측정을 실시하는 일 ( 위키백과 )</p>
</blockquote>
<p>우리 위키백과 선생님 말씀으론 위와 같이 말하십니다.</p>
<p>사실 우리가 평소에 하는 테스트(테스트 코드를 사용하지않고)도 위 사전적 정의를 그대로 따라갑니다.</p>
<blockquote>
<ul>
<li>설이나 이론 : <strong>디자이너가 준 피그마 / 기획자의 기능정의서</strong></li>
<li>다양한 조건 아래서 여러가지를 측정 확인 : <strong>개발자가 개발하면서 진행하는 디버깅 및 QA</strong></li>
</ul>
</blockquote>
<p>즉, 우린 이미 &quot;테스트&quot; 라는 정의에 부합하는 일을 이미 하고있다는겁니다.</p>
<p><del>나 사실 일 잘하는 개발자일지도??</del></p>
<p>하지만, 이렇게 우리가 평소에 하는 테스트도 한계점이 있는데</p>
<p>만약에 기능이 매우 많거나 코드를 수정했는데 해당 기능과 관련된 다른 다양한 기능들이 있다고 가정해봅시다. (예를들면 회원가입 기능?)</p>
<p>이 상황에서 리팩토링 조금 한걸로 이 모든것에 대한 테스트 (비지니스 로직의 플로우 모두)를 계속 하게 된다면 매우 비효율적일테고 그렇다고 리팩토링을 안하자니 기술 부채는 증가할 것이고 악순환이 될겁니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/50ddd903-bca6-417b-9221-64ce0a523054/image.png" alt=""></p>
<p><del>ㅋㅋㅋㅋㅋㅋ 기술부채 막이래</del></p>
<p>이 상황에서 구세주 같은 코드가 바로 <strong>&quot;대 황 테스트 코드&quot;</strong> 되시겠습니다.</p>
<p><strong>대 황 코드</strong>를 작성하게 된다면</p>
<blockquote>
<ul>
<li>적극적인 리팩토링이 가능해지며 결함을 예방하고 요구사항을 충족하는지 검사할 수 있다. </li>
<li>테스트 코드가 명세서의 역할을 하여 코드를 파악하는데 도움을 주기 때문에 협업에서도 유용하다. </li>
<li>더 나아가 테스트 자동화로 반복적인 테스트에 대한 비용이 줄어든다.
(테스트 코드를 작성하는 비용이 있긴하지만..)</li>
</ul>
</blockquote>
<p>이러한 부분들에서 이점을 얻을 수 있습니다.</p>
<p>다만, 모든것이 완벽한 팔방미인도 내성발톱은 있는법..</p>
<blockquote>
<ul>
<li>테스트 코드를 작성하는 비용이 있습니다. 이때문에...
(모든 부분에서 하는것은 비효율적이기 때문에 프로젝트를 설계해야하는데 테스트 코드만 짤 수는 없습니다.)</li>
</ul>
</blockquote>
<p>따라서, 테스트 코드를 사용하기 위해선 테스트 코드가 테스트하는 기능의 중요도를 잘 체크해야합니다.</p>
<h2 id="프론트엔드-테스트-종류">프론트엔드 테스트 종류</h2>
<p>프론트엔드 테스트의 종류는 크게 4가지로 나눌 수 있습니다.</p>
<blockquote>
<ul>
<li>정적테스트</li>
<li>단위테스트</li>
<li>통합테스트</li>
<li>E2E (End to End)</li>
</ul>
</blockquote>
<h3 id="단위테스트">단위테스트</h3>
<blockquote>
<p>나는 나만 신경쓰고 나만의 길로만 간다 기호 2번 단위테스트~</p>
</blockquote>
<p>단위테스트는 말 그대로 어플리케이션을 가장 작은 단위로 분리하여 테스트하는것을 의미합니다.</p>
<p>프로젝트를 설계하면서 <strong>util 함수, 커스텀 훅, 컴포넌트 파일</strong> 등을 생성하게 된다. 이에 대한 <strong>가장 작은 단위</strong>의 테스트 코드를 설계하는것이 단위 테스트이다.</p>
<p>컴포넌트를 테스트할 경우 자식 컴포넌트까지 포함하여 렌더링하는 <strong>Sociable Test</strong>가 있고, 자식 컴포넌트를 Mocking해서 렌더링하는 <strong>Solitary Test</strong>가 있습니다.</p>
<p>여기서 <strong>Mocking</strong> 이란 모조품을 만들어 일단 코드가 돌아가게 하는것입니다. 컴포넌트의 자식 컴포넌트가 있을경우 자식 컴포넌트의 모조품을 만들어 가짜로 대체하는 것입니다.</p>
<p>테스트하고 싶은 기능이 다른 기능들과 엮여있을 경우 유용하게 사용할 수 있습니다.
(jest 의 jest.fn() 함수느낌..)</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/431560d9-7993-4106-b216-710eb9d99bfc/image.png" alt=""></p>
<p>단위 테스트를 함으로써 얻는 이점은 아래와 같습니다.</p>
<blockquote>
<ul>
<li>수행 목적에 따른 행위가 뚜렷한 함수와 컴포넌트등을 설계하기위해 고려하게된다.
이벤트도 발생시킬 수 있어 기존에 하던 콘솔 디버깅도 할 수 있다.</li>
</ul>
</blockquote>
<h3 id="통합intergration-테스트">통합(Intergration) 테스트</h3>
<blockquote>
<p>나는 모든걸 한번에 하기를 좋아해.. 한번 엮이면 일단 같이 움직여야하는 기호 3번 통합테스트</p>
</blockquote>
<p>어플리케이션의 여러 부분들이 통합되어 제대로 상호작용 되는지 테스트합니다. 단위테스트는 단일 모듈에서만 테스트를 진행하여 자신의 단위 내에서만 에러를검증할 수 있었다면, 통합테스트는 모듈 간의 연결에서 발생하는 에러를 검증할수 있고, 단위 테스트보다 넓은 범위에서 테스트하므로 리팩토링할 경우 사이드 이펙트가 적습니다.</p>
<p>의존성 있는 모든 모듈이 연결된 상태로 테스트하는 <strong>board test</strong>와 연결된 모듈을 모킹 또는 가상의 API로 대체하여 테스트하는 <strong>narrow test</strong>가 있습니다.</p>
<p>프론트엔드에서의 통합 테스트는 <strong>state 변화에 따른 UI변경, UI와 API간의 상호작용</strong>에 대한것이 있습니다.</p>
<p><strong>단위테스트와의 차이점</strong> 을 살펴보면 아래와 같습니다.</p>
<blockquote>
<ul>
<li>단위 테스트와 달리 개발자가 변경할 수 없는 부분까지 묶어 검증할 때 사용합니다. (Ex. 외부 라이브러리)</li>
<li>단위 테스트에서 발견하기 어려운 버그를 찾을 수 있습니다.</li>
<li>단위 테스트보다 더 많은 코드를 테스트하기 대문에 신뢰성이 떨어질 수 있고 어디에서 문제가 발생했는지 확인하기 어렵습니다.</li>
</ul>
</blockquote>
<p><img src="https://media2.giphy.com/media/i5RWkVZzVScmY/giphy.gif?cid=790b76115ce903e8786752696b73dcb3&rid=giphy.gif" alt=""></p>
<p><del>이게바로 무한동력 자동문??</del></p>
<h3 id="e2e-end-to-end">E2E (End to End)</h3>
<blockquote>
<p>나는 모두를 수용할 수 있는 능력을 지닌 기호 4번 E2E~</p>
</blockquote>
<p>유닛 테스트로 함수를 테스트하고, 통합 테스트로 서로 다른 시스템이 잘 상호작용하는지 테스트합니다. E2E 테스트는 종단(Endpoint)간 테스트로 유저의 입장에서 유저가 애플리케이션을 사용하는 상황을 가정하고 테스트를 수행합니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/c417d845-fb0b-4da7-8ec7-11becc0a88c8/image.png" alt="">
<del>개발자는 결코 유저를 만만하게 봐서는 안돼..(출처 : 청원이 유튜브)</del></p>
<h2 id="각-목적에-맞는-개발-라이브러리">각 목적에 맞는 개발 라이브러리</h2>
<h3 id="단위테스트-1">단위테스트</h3>
<h4 id="jest">Jest</h4>
<p>jest는 단위 테스트를 위한 프레임워크입니다. JavaScript로 작성된 모든 코드의 테스트를 지원하고 있으며 독립적으로 실행이 가능합니다.</p>
<p>TypeScript에서도 @types/jest와 jest에서 typescript를 실행할 수 있게 도와주는 ts-jest만 있으면 사용할 수 있습니다.</p>
<hr>
<h3 id="통합테스트">통합테스트</h3>
<h3 id="enzyme">enzyme</h3>
<p>Jest가 자바스크립트 테스트 프레임워크라면, enzyme은 React를 테스트하기 위해 airbnb에서 만든 라이브러리이다. enzyme는 독립적으로 사용할 수 없기 때문에 Jest와 함께 사용하여 React의 단위 테스트와 통합 테스트가 가능해진다.</p>
<h3 id="react-testing-libraryrtl">react-testing-library(RTL)</h3>
<p>enzyme와 다르게 컴포넌트 구현 세부 사항에 중점을 두지 않고, 실제 브라우저 DOM을 기준으로 테스트를 작성합니다. 그래서 RTL은 컴포넌트의 속성(state, props 등)에 대한 접근을 제공하지 않습니다.</p>
<hr>
<h3 id="e2eend-to-end">E2E(End to End)</h3>
<h3 id="cypress">cypress</h3>
<p>E2E 테스트(단위 테스트와 통합 테스트도 가능)가 가능한 도구이다. GUI 도구를 지원하고, 스펙관리 및 디버깅이 편리합니다. test runner가 jest가 아닌 mocha라는 특징이 있습니다.</p>
<hr>
<h3 id="마치며">마치며</h3>
<p>그동안 미뤄왔던 테스트 코드의 개념을 한번 되돌아보며 정리하는 시간을 가졌습니다.
다가가기 어려웠던 친구이니만큼 잘 활용해서 멋진 프론트엔드 개발자가 되어보겠습니다!</p>
<p>이제 테스트코드 넌 내 단짝이야~</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/a34d38d3-4353-4ffc-815d-e77f02435369/image.png" alt=""></p>
<p>추가로 다음은 <strong>TodoList + Jest + ReactTestingLibrary</strong> 를 이용한 실제 코드를 가져오겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Testing Setting ( Jest )]]></title>
            <link>https://velog.io/@yunsung_/React-Testing-Setting-Jest</link>
            <guid>https://velog.io/@yunsung_/React-Testing-Setting-Jest</guid>
            <pubDate>Thu, 28 Mar 2024 09:04:58 GMT</pubDate>
            <description><![CDATA[<h3 id="환경-세팅"><strong>환경 세팅</strong></h3>
<br>

<p><strong><em>CRA 명령어</em></strong></p>
<pre><code class="language-bash">npx create-react-app react-test --template typescript </code></pre>
<p><strong><em>Jest 설치 및 여러 부수 라이브러리 설치</em></strong></p>
<pre><code class="language-bash">npm i jest jest-dom jest-environment-jsdom ts-jest babel-jest --dev</code></pre>
<p><strong><em>testing-library 를 위한 모듈들 설치</em></strong></p>
<pre><code class="language-bash">npm i @testing-library/user-event @testing-library/react @testing-library/jest-dom @testing-library/dom --dev</code></pre>
<p><strong><em>package.json scripts 영역에 명령어 추가</em></strong></p>
<pre><code class="language-json">&quot;test&quot;: &quot;jest --watch --passWithNoTests&quot;,
&quot;test:ci&quot;: &quot;jest --ci --passWithNoTests&quot;</code></pre>
<p><strong><em>tsconfig / basic setting</em></strong></p>
<pre><code class="language-js">{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;./src&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./*&quot;],
      &quot;@pages/*&quot;: [&quot;pages/*&quot;],
      &quot;@components/*&quot;: [&quot;components/*&quot;],
      &quot;@utils/*&quot;: [&quot;utils/*&quot;],
      &quot;@hooks/*&quot;: [&quot;hooks/*&quot;],
      &quot;@types/*&quot;: [&quot;types/*&quot;]
    },
      //...
  },
  &quot;include&quot;: [&quot;next-env.d.ts&quot;, &quot;**/*.ts&quot;, &quot;**/*.tsx&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;]
}</code></pre>
<p><strong><em>tsconfig-setup</em></strong></p>
<pre><code class="language-js">{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es5&quot;,
    &quot;baseUrl&quot;: &quot;src&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./*&quot;],
      &quot;@pages/*&quot;: [&quot;pages/*&quot;],
      &quot;@components/*&quot;: [&quot;components/*&quot;],
      &quot;@utils/*&quot;: [&quot;utils/*&quot;],
      &quot;@hooks/*&quot;: [&quot;hooks/*&quot;],
      &quot;@types/*&quot;: [&quot;types/*&quot;]
    },
    &quot;lib&quot;: [
      &quot;dom&quot;,
      &quot;dom.iterable&quot;,
      &quot;esnext&quot;
    ],
    &quot;allowJs&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;allowSyntheticDefaultImports&quot;: true,
    &quot;strict&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;noFallthroughCasesInSwitch&quot;: true,
    &quot;module&quot;: &quot;esnext&quot;,
    &quot;moduleResolution&quot;: &quot;node&quot;,
    &quot;resolveJsonModule&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;noEmit&quot;: true,
    &quot;jsx&quot;: &quot;react-jsx&quot;
  },
  &quot;include&quot;: [&quot;next-env.d.ts&quot;, &quot;**/*.ts&quot;, &quot;**/*.tsx&quot;, &quot;jest.config.ts&quot;,&quot;src/jest-setup.ts&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;]
}
</code></pre>
<p><strong>package.json 명령어 추가</strong></p>
<pre><code class="language-json"> &quot;scripts&quot;: {
    &quot;start&quot;: &quot;react-scripts start&quot;,
    &quot;build&quot;: &quot;react-scripts build&quot;,
    &quot;test&quot;: &quot;jest --watch --passWithNoTests&quot;,
    &quot;test:ci&quot;: &quot;jest --ci --passWithNoTests&quot;,
    &quot;eject&quot;: &quot;react-scripts eject&quot;
  },</code></pre>
<hr>
<h3 id="환경-세팅중-이슈-해결"><strong>환경 세팅중 이슈 해결</strong></h3>
<blockquote>
<p><strong>(SyntaxError: Support for the experimental syntax ‘jsx’ isn’t currently enabled)</strong></p>
</blockquote>
<blockquote>
<p><strong>ReferenceError: React is not defined</strong></p>
</blockquote>
<p>.babelrc 및 웹팩 설정시 JSX 변환 오류가 있었다.</p>
<p>해당 error 는 웹팩에서 실행하는 babel-loader 에서 JSX의 &lt;&gt; 태그 문법이 변환이 되지않아 에러가 나는 상황이였다.</p>
<p>해당 에러는 다음과 같이 해결할 수 있었다.</p>
<p><strong>1).babelrc 생성</strong>  ( 해결 )</p>
<pre><code class="language-js">{
    &quot;presets&quot;: [
      &quot;@babel/preset-env&quot;,
      [&quot;@babel/preset-react&quot;, { &quot;runtime&quot;: &quot;automatic&quot; }],
      &quot;@babel/preset-typescript&quot;
    ]
}</code></pre>
<p>*<em>2)babel.config.js 생성 *</em></p>
<pre><code class="language-js">module.exports = {
  presets: [
    [&quot;@babel/preset-react&quot;, { runtime: &quot;automatic&quot; }],
};</code></pre>
<p>또한, 위와 같이 설정을 해주니 <strong><em>ReferenceError: React is not defined</em></strong> 이슈도 함께 해결되었다.</p>
<blockquote>
<p><strong>typeError: expect(...).toHaveAttribute is not a function</strong></p>
</blockquote>
<p>[Git hub 이슈] (<a href="https://github.com/testing-library/react-testing-library/issues/379)%EB%A5%BC">https://github.com/testing-library/react-testing-library/issues/379)를</a> 살펴보니 toHaveAttribute 같은 속성은 기본적으로 
<em><strong>import &quot;@testing-library/jest-dom/extend-expect&quot;;</strong></em> 를 추가해주어야한다.</p>
<p>test file 에 하기 import 를 추가했다.</p>
<pre><code class="language-js">import &quot;@testing-library/jest-dom/extend-expect&quot;;</code></pre>
<hr>
<p>역시 항상 초기세팅이 어려운것 같다..</p>
<p>다음에는 테스트 코드의 개념과 TDD 개발론을 정리해 오겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[StoryBook 넌 누구냐]]></title>
            <link>https://velog.io/@yunsung_/StoryBook-%EB%84%8C-%EB%88%84%EA%B5%AC%EB%83%90</link>
            <guid>https://velog.io/@yunsung_/StoryBook-%EB%84%8C-%EB%88%84%EA%B5%AC%EB%83%90</guid>
            <pubDate>Wed, 19 Jul 2023 07:44:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 StoryBook 에 대해 <strong>가볍게</strong> 정리한 글입니다.</p>
</blockquote>
<h1 id="배경">배경</h1>
<p>회사에서 컴포넌트를 여러 사람과 협업해 개발하게 되면 나 외의 다른 사람이 만든 컴포넌트의 의도나 쓰는 방법을 잘 알지 못해 생각보다 협업에 차질을 겪는 일이 발생했었습니다. </p>
<p>해당 이유로 코드에 주석이나 미리 인자를 정해놓고 개발을 진행 했어도 , 시간이 많이 소요된다는 단점이 있어 해결방법을 찾아야 했습니다. 그러다 문득 다른 기업은 어떤 방식으로 협업하는지 방법을 찾던중 <a href="https://www.codenary.co.kr">해당 사이트</a>에서 여러 기업이 자주 중복적으로 사용하는 <strong>Story Book</strong> 을 발견하게 되었습니다.</p>
<h1 id="storybook-에-대해">StoryBook 에 대해</h1>
<blockquote>
<p>Storybook is a frontend workshop for building UI components and pages in isolation. </p>
</blockquote>
<p>StoryBook 을 한마디로 요약하면 위와 같습니다.
그말인 즉슨 UI 개발 환경이며 , 동시에 UI 컴포넌트 PlayGround 라고 할 수있습니다.
심지어 UI 를 테스팅 함과 동시에 컴포넌트 문서화 까지 가능합니다.</p>
<p>이는 React 의 특징중 하나인 <strong>잘 분리시킨 컴포넌트를 이용해 재 사용성을 높이는 것</strong> 임에 미루어 봤을 때, Storybook은 단위 테스트 측면이나 컴포넌트 분리 측면에서 React와 꽤나 잘 어울리는 UI 테스팅 및 문서화툴 이라고 생각합니다.</p>
<h1 id="storybook-의-장점">StoryBook 의 장점</h1>
<p><span style="color: #d4feff"><em><strong>독립적으로 UI를 구축 할 수있습니다.</strong></em></span></p>
<p>Story Book 을 사용하면 UI의 모든 부분이 구성 요소가 됩니다. 구성 요소의 장점은 단순히 렌더링 방식을 보기 위해 전체 앱을 분리 시킬 필요가 없다는 것입니다. 요소의 데이터를 변경하거나 이벤트를 조작하여 특정한 UI를 렌더링할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/66c4e354-437b-494b-81d4-fbb8ceeb046b/image.png" alt=""></p>
<p><span style="color: #d4feff"><em><strong>특정한 스냅샷을 스토리로 만들고 테스트할 수 있습니다.</strong></em></span></p>
<p>재사용을 위해 만들어진 컴포넌트들을 Story에서 조합하여 기존 코드를 건드릴 필요없이 실시간으로 테스트하며 개발 할 수있습니다. 기존처럼 테스트를 위해 특정 페이지에 만들고 지울 필요 없이, 새로운 Story 객체 하나만 만들어 테스트할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/107dbc5a-f83b-4b66-9b98-6ff41bf7f8c3/image.png" alt=""></p>
<p><span style="color: #d4feff"><em><strong>협업을 위한 문서화를 할 수있습니다.</strong></em></span></p>
<p>다음과 같이 상태 자체를 Story 에 저장해 해당 컴포넌트의 인자 설명이나 특정 컴포넌트의 상태가 어떤 형태로 사용될 수 있는지 직관적으로 문서화 할 수있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/6d11563c-8ea4-4aa5-b54d-9c6402afafdd/image.png" alt=""></p>
<h1 id="storybook-설치-및-세팅">StoryBook 설치 및 세팅</h1>
<blockquote>
<p>제가 사용한 버전은 7.1 버전입니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/6fe90450-4df2-4ed4-87f2-47df383c65e6/image.png" alt=""></p>
<p>필자는 프로젝트 root 에 storyBook 을 설치했다. 설치하면 두개의 디렉토리가 생기게 되는데,</p>
<p><strong><em>.storybook : Storybook 설정 파일 포함 / main.js &amp; preview.js</em></strong></p>
<p><strong>_src/stories: Storybook 예제 컴포넌트들 / exComponent _</strong></p>
<p>해당 파일은 각각 storyBook 의 설정과 예시 컴포넌트들이 들어있는 파일이 들어있다.</p>
<h2 id="mainjs">main.js</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/026bcc04-3280-47ea-84d3-a6f8dc38cbd7/image.png" alt=""></p>
<p>main.js 에는 storybook을 위한 config 설정들이 담겨있습니다. <em><strong>@storybook/cli sb init</strong></em> 을 통해서 기본으로 설정되는 stories와 addons 세팅을 하였습니다. (stories에서 story 파일이 프로젝트 내 어디에 어떤 형식의 파일이 위치하는지를 명시해 주어야 storybook 실행 시 정상적으로 불러올 수 있습니다.)</p>
<h2 id="previewjs">preview.js</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/48a60677-2935-4aaf-ae39-b7a4edf277a0/image.png" alt=""></p>
<p>preview.js 에는 해당 프로젝트의 모든 Story에 global하게 적용될 포맷을 세팅하는 곳 입니다. 후에 나오겠지만, parameter와 decorater는 Story의 프로퍼티, 여기선 포맷에 해당합니다.</p>
<blockquote>
<p>⚠️ 주의
현재 프로젝트에선 <strong>tailwindcss</strong> 를 이용해 세팅되어 있으므로 ThemeProvider를 통해 감싸주고 GlobalStyle도 적용시켜 주었습니다. 해당 사례처럼 따로 외부 라이브러리를 적용시켜주지않으면 <strong>StoryBox</strong> 에도 적용되지 않습니다.</p>
</blockquote>
<p>parameters 에 적용된 속성은 <em><strong>@storybook/cli sb init</strong></em> 을 통해 기본적으로 세팅된 값 입니다. </p>
<h1 id="storybox-사용하기">StoryBox 사용하기</h1>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/af18bdd6-b3c3-4481-8f5b-ac170f54f04c/image.png" alt=""></p>
<p>우선, 해당 명령어로 StoryBox 서버를 가동해야합니다. 서버는 특별한일이 있지않은 이상은 포트번호 6006으로 열리지만 해당 포트를 이미 사용중이라면 자동으로 다른 포트로 변환해 열립니다.</p>
<p>열어보면 예시 컴포넌트들을 확인해 볼 수있는데, 해당 컴포넌트들은 <em><strong>root/stories</strong></em> 하위에 있습니다.
해당 폴더 하위에 새 컴포넌트를 작성해도 되고 외부에 컴포넌트를 구현해 story 폴더에 끌고오거나 아에 외부에서 story 컴포넌트를 만들 수도있습니다.</p>
<h2 id="새로운-story-생성">새로운 Story 생성</h2>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/f0f3c714-c072-4e0f-bfb5-2468c100e736/image.png" alt=""></p>
<p>새로운 Story 를 생성하기 위해선 이름에 stories 가 들어가야 하기때문에
<strong><em>{ component name } . stories . ts</em></strong> 가 되어야합니다.</p>
<p>새로운 파일을 생성하고 meta 와 story / story componet 를 작성해주도록 합시다.</p>
<h3 id="meta-에-대해">Meta 에 대해</h3>
<p>meta 의 경우 <span style='color:#fff5b1'><em><strong>title , component , tags , argTypes</strong></em></span> 로 나눌 수 있는데, 각 요소를 설명하자면</p>
<p><strong>Title</strong></p>
<p>스토리북 폴더 계층 구조 , Example/Button 을 예시로 사용하면 StoryBook 페이지에서 Example 폴더 안에 Button 파일이 생성되게 됩니다.</p>
<p><strong>Component</strong></p>
<p>옵셔널이지만 addon 을 위해 써주는것이 권장되는 Story 에 명시적으로 어떤 컴포넌트를 문서화 할지 적는 인자입니다. 해당 인자에는 새로운 Story로 만들 import 해온 컴포넌트를 넣습니다.</p>
<p><em><strong>tags</strong></em></p>
<p>스토리에 대한 자동 문서화를 활성화하거나 비활성화합니다. 곧 나올 컴포넌트 문서화를 위한 인자입니다.</p>
<p><em>*<em>argTypes *</em></em></p>
<p>story 에 대한 유동적인 메타데이터를 정의하는 인자입니다. 사용자가 storyBook 에서 컨트롤 할 수있는 인자를 정의합니다.</p>
<h3 id="args">Args</h3>
<p>모든 meta 인자를 정의해 주었다면 해당 Meta 인자를 Story 객체에 타입으로 넣어 args 에 담긴 인자들에 대한 컴포넌트를 캡처해주어야 합니다.</p>
<p>Story는 렌더링된 UI 컴포넌트의 state를 캡쳐하여 어떻게 컴포넌트가 렌더링 되는지 보여줄 수있습니다. 이때, Story는 React의 props와 비슷한 느낌으로 arguments를 args 라는 이름으로 가지고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/6db9de50-b7ab-4b9a-9253-74817f9e790b/image.png" alt=""></p>
<p>위와같이 args 를 이용해 정해진 인자값을 다르게 바꾸게 되면 story 객체는 여러개가 되고 StoryBook 에서 볼 때 여러 상태의 컴포넌트를 확인해 볼 수있습니다.</p>
<p>새로 생성한 파일에 제가 설명한 모든 인자가 있다면 포트를 열었던 사이트에서는 밑과 같이 여러 args 로 캡쳐된 컴포넌트를 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/yunsung_/post/19bb67b1-604a-46c5-8bad-44117c0a4778/image.png" alt=""></p>
<p>또한, 사용자가 StoryBook 에 들어가 실제 컴포넌트들의 args 을 controls 탭에서 args의 값을 자유롭게 수정할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/8758dbac-9ab5-4573-9a58-f1a78459b8c6/image.png" alt=""></p>
<h2 id="component-문서화">component 문서화</h2>
<p>컴포넌트를 문서화 할 수있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/3ee330fd-51e5-4a0b-9a56-c06e0001aeac/image.png" alt=""></p>
<p>다음과 같이 컴포넌트 인자값에 설명을 적게 되면 아까 열었던 storyBook Web 에서 밑과 같이 Description 에 설명이 자동으로 문서화됨을 볼 수있습니다. </p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/6f5525fe-1525-47d9-a14e-7568ed9953e0/image.png" alt=""></p>
<p>또한, Control 에서 직접 인자값을 바꿔보며 해당 인자값이 어떤 역할을 하는지 확인해볼 수도있습니다.</p>
<h1 id="마치며">마치며</h1>
<p>협업 할 경우 컴포넌트 관련 의사소통에 조금 더 신경을 써야함을 느끼고 StoryBook 을 사용해 보았는데 , 생각보다 UI 단위 테스트와 컴포넌트 문서화에 이점이 강해서 앞으로 회사에서 협업을 할경우 사수님에게 요청하여 자주 사용해보아야겠다고 생각했습니다. 다만 , 해당 사용법은 storyBook 기능의 기능중 기본이기 때문에 , 실질적 실무에서는 웹사이트를 더 참고 해야할것 같습니다.</p>
<p>긴글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[주니어의 Remix 도입기]]></title>
            <link>https://velog.io/@yunsung_/%EC%A3%BC%EB%8B%88%EC%96%B4%EC%9D%98-Remix-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/%EC%A3%BC%EB%8B%88%EC%96%B4%EC%9D%98-Remix-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Tue, 18 Jul 2023 05:41:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당글은 회사 프로젝트를 진행하며 도입한 Remix 라는 라이브러리 도입기와 
Remix 라이브러리에 대해 다룹니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/109dcede-a9b1-46c9-a3ec-0eeb8b221644/image.png" alt=""></p>
<p>어느날 회사에서 프로젝트 관련 요청 토큰이 할당되었습니다.</p>
<p>해당 요청은 새롭게 고객사 웹을 제작하라는 요청이였는데 전에 만들어 배포했던 식단관리 관련 앱의 유저 데이터를 한눈에 볼 수 있도록 관리자 페이지를 제작해야한다는 요청이였습니다.</p>
<p>평소 개발은 React 로 했으나 새롭게 만들어야할 고객사 웹은 관리자 페이지로 다른 웹들과 달리 데이터를 DB에서 가져올 경우가 많았는데, 해당 상황에서 CSR 를 사용한다면 서버 요청시간이 길어질수도 있던 상황이기도 했고 백엔드 엔지니어분들의 일정 부족으로 백엔드 코드까지 일부 건드려야할 상황이 생겼습니다.</p>
<p>해당 상황에서 저는 UI 와 API 를 분기하기보단 한 프로젝트에서 관리하는것이 편할것 같다고 생각했고 , API 요청과 UI 렌더링을 동시에 할 수 없는 CSR 보단 SSR 즉 , 서버 사이드 렌더링이 해당 프로젝트에 더욱 적합하다고 생각했습니다.</p>
<h2 id="ssr-csr">SSR? CSR?</h2>
<p>우선 왜 Remix 를 도입했는지 알기 위해선 위에서 잠깐 언급했던 두가지 렌더링 방법을 살펴보아야합니다.</p>
<p>간단하게 말하자면 CSR 은 <strong>웹페이지 렌더링이 클라우드(브라우저) 측에서 일어나는것을 의미</strong>합니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/1e812816-9a4d-415b-964e-46b118a76d8a/image.png" alt=""></p>
<p>클라이언트 사이드 렌더링은 UI 렌더링을 브라우저에서 모두 처리하는 것입니다. 즉, 자바스크립트를 불러온다음에 실행이 되어야 우리가 만든 화면이 사용자에게 보여집니다.</p>
<p>CRA로 만든 프로젝트의 개발 서버를 실행한 다음, 크롬 개발자 도구의 Network탭을 열고 <a href="http://localhost:3000/">http://localhost:3000/</a> 페이지의 요청에 대한 응답을 보시면 다음과 같이 root 엘리먼트가 비어 있는 것을 확인할 수 있습니다.</p>
<p>즉, 해당 페이지는 처음에는 빈 페이지입니다. 해당 페이지는 JS 가 실행되어야 비로소 UI 가 로드됩니다.</p>
<p>하지만 SSR 은 <strong>SSR은 서버에서 첫 페이지의 렌더링을 클라이언트 측이 아닌 서버 측에서 처리해주는 방식</strong>입니다.</p>
<p>서버에서 페이지의 렌더링을 클라우드가 아닌 서버측에서 관리해주기때문에 JS 가 실행되지않아도 UI 가 이미 로드되어있습니다.</p>
<p>해당 상황은 웹 검색 기능을 구현하기에도 유리한데, 서버 사이드 렌더링을 하여 사용자에게 페이지를 더욱 일찍 보여준다면, 페이지의 모든 데이터가 로딩될 때 까지 기다리지 않고 검색 할 가능성이 있기 때문에 의도한 데이터가 제대로 검색되지 않을 수 도 있습니다. </p>
<p>제가 구현해야하는 웹은 검색기능 및 렌더링 측면과 함께 렌더링 시점에서 다량의 DB 데이터를 가져와야하기때문에 SSR 방식이 적합했습니다.</p>
<h2 id="remix">Remix</h2>
<blockquote>
<p>그래서 Remix 가 뭔데?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/ed8b7b5f-abc7-45f7-9773-4f2f1b237c3e/image.jpeg" alt=""></p>
<p><a href="https://remix.run/">Remix 공식 문서</a></p>
<p>Remix는 React를 사용하는 풀스택 웹 프레임워크입니다. Prisma 및 디렉토리 기반 라우팅 구성으로   기존에 구현하기 까다로웠던 SSR 을 Remix 라이브러리로 간편하게 구현 할 수있습니다. </p>
<h2 id="remix-의-신세계">Remix 의 신세계</h2>
<p>Remix 를 디렉토리 기반 라우팅 , loader 및 action , from 개념 등 실질적 웹 구현에 있어서 
처음보는 Remix 만의 특징이 정말 신기했습니다.</p>
<ol>
<li><p>디렉토리 기반 라우팅</p>
<p>기존에 우리가 리액트 라우터를 사용할 때 라우트를 구성할 때에는, <strong>컴포넌트 선언 방식</strong>으로 설정을 했었던 반면, Remix의 경우에는 <strong>컴포넌트의 디렉터리에 기반하여 라우트</strong> 가 설정이 됩니다.</p>
<p><em>app/routes/index.jsx</em> 와 같이 말이죠.</p>
<p>따라서 밑의 사진과 같이 프로젝트 파일이 구성되고 해당 파일 주소에 따라 라우팅이 되는 방식이 신기했습니다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/07f99cc1-22a7-483d-a9d6-65d916a810f5/image.png" alt=""></p>
<ol start="2">
<li><p>loader 및 action</p>
<p> Remix 에서는 최초 로딩시 <strong>loader</strong> 라는 함수가 호출됩니다. 해당 함수는 최초에 로딩되어야하는 UI     에 API 를 호출 할 때 유리한데 저같은경우는 식단 정보 및 사용자 정보 , 관리자 정보 등등 관리자     페이지에 있어서 미리 호출되어야할 함수나 API 를 다음과 같이 불러 올 수 있었습니다.</p>
<p> 같은 양상으로 action 의 경우는 함수 이름대로 web 에서 어떤 동작이 주어지게 될 경우 실행되는 함수로 버튼이 눌리거나 input box 의 value 가 바뀌게 될 경우 호출되는 함수로, </p>
<pre><code> ![](https://velog.velcdn.com/images/yunsung_/post/dcfa97ae-23f7-40a2-a678-9f877ed173cb/image.png)</code></pre><p> <img src="https://velog.velcdn.com/images/yunsung_/post/d055d7cd-ef72-4353-89ee-4ec0949567e2/image.png" alt=""></p>
<p> 또한,추가적으로 Loader 및 Action 함수를 실제 컴포넌트 단위에서 사용하기위해선 밑과같은 useLoaderData 나 useActionData 를 사용해 UserData.(변수명) 해당 값을 가져올 수 있었습니다.</p>
<pre><code> ![](https://velog.velcdn.com/images/yunsung_/post/70363d91-e990-4964-ac07-08eb9a0b3cee/image.png)</code></pre></li>
<li><p>Form
 가장 핵심적인 부분입니다. 앞서 말했던 Action 에서 button 이나 다른 컴포넌트의 인자를 받고싶다면 form 으로 최상위 부모의 컴포넌트를 묶고 , 보이지 않는 Input 에 data 를 넣어 action 함수에 전달 해 줄 수 있습니다.</p>
<p><a href="https://remix.run/docs/en/1.18.1/guides/data-writes#plain-html-forms">Form 관련 문서</a></p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/f205c914-06d8-41bc-8e4b-6ce3d88a17a9/image.png" alt="">
 <img src="https://velog.velcdn.com/images/yunsung_/post/98d48437-a8a4-4810-af6b-62e9061b5d8c/image.png" alt=""></p>
</li>
</ol>
<h2 id="도입중-문제점의-시작">도입중 문제점의 시작..</h2>
<p><em><strong>TypeError: Cannot read properties of undefined (reading ‘root’)</strong></em></p>
<p>하지만 모두 잘 되진 않았습니다. <em><strong>TypeError: Cannot read properties of undefined (reading ‘root’)</strong></em> 열심히 로직을 작성하던중 해당 오류가 나오며 정상적으로 실행이 되지 않았습니다. 그래서 Remix 깃헙 이슈를 찾아보니, 저와 같은 문제로 고생중인 분들이 있었는데 원인은 브라우저 번들에 서버 환경에서만 실행 가능한 코드가 포함되었기 때문입니다.</p>
<p>해당 상황에서 해결법을 조금 더 찾아보니 서버 환경에서 실행되어야 하는 코드의 파일 이름을 something.server.ts 처럼 변경하는 방법이 가장 효과적임을 알 수있었고 이렇게 하면 해당 파일이 서버 환경에서만 필요하다는 것에 대한 힌트를 제공하는 방식이 된다는걸 알 수 있었습니다.</p>
<p><strong>해결 방법</strong></p>
<blockquote>
<p>서버 관련한 파일은 server 를 붙여주거나 앞에 api 주소를 붙여 서버에서 사용되는 코드임을 명시하였습니다.</p>
</blockquote>
<p><em><strong>CSS 관련 이슈</strong></em></p>
<p>웹 스타일 관련은 <a href="https://mui.com/">mui</a> 와 <a href="https://tailwindcss.com/">tailwindcss</a> 를 사용하였는데 , mui 커스텀을 할 일이 생겨 기존 react 에서 하던대로 css 파일을 임포트하면 적용이 되지않는 이슈가 발생했었습니다.</p>
<p><strong>해결 방법</strong></p>
<blockquote>
<p>이부분은 공식 문서에도 잘 나와있는데, remix 에서의 css 파일은 root 파일에 반드시 import 를 해주어야합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/acdb7997-19a5-47df-a072-594ebb3f4d9b/image.png" alt=""></p>
<p><em><strong>root 디렉토리 외의 api 호출</strong></em></p>
<p>유저 파일정보나 세부정보같은 메인에서 호출할경우 로직이 꼬이거나 , 불필요하게 메인에서 호출하지 않아도 되는 api 의 경우 root 디렉토리 외의 컴포넌트에서 호출해야 합니다. 하지만 remix 특성상 외부에서 prisma api 를 호출 하려하면, 다음과 같은 오류가 뜨며 호출이 되지않고 웹에 error 가 발생합니다.</p>
<blockquote>
<p>prisma 함수를 component 에서 직접 호출 할 수없습니다.</p>
</blockquote>
<p>당연히 해당 오류는 db를 직접 참조하는 prisma 함수에서 의도적으로 막아놓은것으로 만약 클라이언트에서 해당 함수를 실행하게된다면 웹이 정상적으로 작동하지않을 수 있습니다.</p>
<p><strong>해결 방법</strong></p>
<p>다음과 같이 fetch 함수를 통해 api 라는 서버에서 쓰는 페이지임을 명시하는 페이지 주소를 만들고 해당 페이지에 호출 할 수 있도록 만들었습니다. 그러나 이 방법이 최선인지는 아직 모릅니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/8c1f8e37-d0ad-4b18-bddb-07823e533b1e/image.png" alt="">
<img src="https://velog.velcdn.com/images/yunsung_/post/df77ad33-dc3b-4441-a64f-c93b6fd2b42c/image.png" alt=""></p>
<h2 id="회고--정리">회고 &amp; 정리</h2>
<p>7월 초반쯔음 프로젝트가 마무리 되었고, 앞으로도 유지보수가 걸려있어 유지보수를 하겠지만</p>
<p>어드민은 여러모로 동기부여를 쓸 수 있도록 도와준 프로젝트 였습니다. 특히 파일 구조로 web 주소를 설정하는 부분이나 loader / action 같은 remix 만의 고유 funcion 을 이용하여 신기하고 즐겁게 코드를 쓰는 기분이 너무 좋았습니다. 역시 개발자는 새로운 기술을 배우고 실전에 응용할 때 더욱 기쁜것 같습니다.</p>
<p>혹시 본인처럼 어드민을 만들 계획이 있거나 필요한 사람이 있다면 꼭 Remix로 하지 않아도 된다고 말하고 싶습니다. 내 생각에 어드민에서 중요한 것은 프레임워크보다 코드의 반복을 줄이고, 컴포넌트를 선언적으로 구성할 수 있게 만드는 것이 더 중요하다고 봅니다. <del>사실 난 그냥… Remix를 써보고 싶었을 뿐</del></p>
<p>하지만 아쉬운 부분도 여럿 있었는데, 거의 대부분의 api 가 새로운 api root 를 통해 호출 되었다는점과 여러 커스텀 컴포넌트의 인자 type 구성이 유지보수 대상이나 아직까지 건드리지 않았던 점이 조금 아쉬웠습니다.</p>
<p>물론 아쉬운점은 유지보수측면에서 점차 수정해나갈 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[구글 플레이 정책의 무서움을 깨달아버렸다..]]></title>
            <link>https://velog.io/@yunsung_/%EA%B5%AC%EA%B8%80-%ED%94%8C%EB%A0%88%EC%9D%B4-%EC%A0%95%EC%B1%85%EC%9D%98-%EB%AC%B4%EC%84%9C%EC%9B%80%EC%9D%84-%EA%B9%A8%EB%8B%AC%EC%95%84%EB%B2%84%EB%A0%B8%EB%8B%A4</link>
            <guid>https://velog.io/@yunsung_/%EA%B5%AC%EA%B8%80-%ED%94%8C%EB%A0%88%EC%9D%B4-%EC%A0%95%EC%B1%85%EC%9D%98-%EB%AC%B4%EC%84%9C%EC%9B%80%EC%9D%84-%EA%B9%A8%EB%8B%AC%EC%95%84%EB%B2%84%EB%A0%B8%EB%8B%A4</guid>
            <pubDate>Fri, 14 Jul 2023 07:04:17 GMT</pubDate>
            <description><![CDATA[<h1 id="app-reject--playstore-이슈--개인정보">App Reject / PlayStore 이슈 / 개인정보</h1>
<p>평화로운 어느날 회사에서 관리하던 앱이 갑자기 소리소문없이 내려갔습니다..!</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/033f6172-0699-4077-ab12-ac3b347c6320/image.png" alt=""></p>
<p>어제까지 잘 업데이트 되었던 앱이 하루아침에 사라지게 된계기는 다음과 같습니다.</p>
<blockquote>
<p>&quot;너! 앱에서 개인정보를 수집하는데 왜 정확히 설명안해!&quot;</p>
</blockquote>
<p>우선 저는 개인정보 처리방침 이야기가 있어 개인정보 처리방침부터 점검했습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/286345df-939b-4486-8cec-7f001d0851e1/image.png" alt=""></p>
<p>google 정책을 켜 개인정보 처리방침을 유심히보니 <strong>&quot;앱에서 엑세스 ,수집 , 사용 공유하는 개인 정보 및 민감한 사용자 정보 데이터를 공유하는 모든 주체 공개&quot;</strong> 라는 내용을 발견했습니다.</p>
<p>해당 글을 근거로 저는 개인정보 처리방침을 밑과 같이 교체하였습니다.</p>
<h3 id="개인정보-처리방침-개선-사항">개인정보 처리방침 개선 사항</h3>
<p>주체 공개 및 사용목적을 추가하여 더욱 투명한 개인정보 처리방침을 만들었습니다.</p>
<p><strong><em>추가된 개인정보 처리방침 약관</em></strong></p>
<blockquote>
<p>⛔ ex)
<strong>앱에서 액세스, 수집, 사용, 공유하는 &quot;개인 정보&quot; 및 &quot;민감한 사용자 데이터&quot; 유형</strong></p>
</blockquote>
<ul>
<li>본 앱에서 사용되는 개인정보는 구글 계정과 상관없는 기기의 ID, 비밀번호, 생년월일, 성별, 주소, 핸드폰번호, 이메일주소, 상품수령방법, 상품수령주소,핸드폰 ID, 위치정보, 앱 형태(유/무료), 사용언어, 국가명, 안드로이드 버전이 있습니다.</li>
<li>본 앱에서 사용하는 &quot;민감한 사용자 데이터&quot;는 사용자가 앱을 이용하여 생성하는 &quot;앱 자체 정보&quot;가 있습니다.</li>
<li>본 앱은 위 기술한 &quot;개인 정보&quot;, &quot;민감한 사용자 데이터&quot; 제외하고 이외 데이터를 수집, 사용, 공유하지 않습니다.</li>
<li>이 앱을 사용함으로써 자신의 실제 위치를 본사와 공유하는 데 동의하는 것으로 간주됩니다. 본사는 귀하의 위치 정보를 활용하여 귀하의 모바일 기기가 가진 GPS 기능을 활용하여 (회사명) 의 이벤트를 설계하지만, 이 정보는 귀하가 앱을 통해 데이터를 공개적으로 공유하지 않는 한 본사에서만 접속이 가능합니다.<br></li>
<li><em>개인 정보 및 민감한 사용자 데이터를 안전하게 처리하는 절차 및 사용 목적*</em></li>
<li>&quot;개인 정보&quot;의 ID, 비밀번호, 생년월일, 성별, 주소, 핸드폰번호, 이메일주소, 상품수령방법, 상품수령주소,핸드폰 ID, 위치정보 는 불법적인 중복 사용자를 걸러내는데 사용하거나 고객의 문의사항에 대응하기 위한 자료로 활용됩니다.</li>
<li>앱 형태(유/무료), 사용언어, 국가명, 안드로이드 버전은 앱 개선을 위한 정보로 본 회사 내부에서만 이를 관리합니다.</li>
<li>로그인 시 로그인 정보는 암호화 되어 본 회사의 서버와 통신합니다.</li>
<li>&quot;민감한 사용자 데이터&quot;는 앱을 사용하기 위한 기본적인 앱 데이터 값이며 사용자가 언제든 삭제할 수 있습니다 </li>
</ul>
<h3 id="app-에서-접근하기-쉬운-개인정보-처리방침-버튼-추가">APP 에서 접근하기 쉬운 개인정보 처리방침 버튼 추가</h3>
<p><strong><em>App 개선 사항</em></strong></p>
<blockquote>
<p>⛔ “APK에 중요한 공개가 있지만 공개가 적절하지 않습니다 
앱이 적절한 공개 없이 사용자의 설치된 응용 프로그램 정보를 업로드하고 있습니다.”</p>
</blockquote>
<p>해당 에러에서 지적한 점을 근거로 APP 의 첫 화면에 개인정보 처리방침의 접근이 쉬운 버튼을 배치하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/070e5aa0-e562-4432-b8bd-c5252e5c0680/image.png" alt=""></p>
<h3 id="추가적인-조치사항">추가적인 조치사항</h3>
<p>그 후에 추가적으로, <a href="https://play.google.com/console/u/0/developers/5580638715903018995/app/4974426047291815398/app-content/overview">google console</a> 의 앱콘텐츠중 개인정보 처리방침과 데이터 보안 탭을 살펴 보았습니다.</p>
<p>구글 플레이스토어 약관을 살펴보면 2022년 3월경부터 <strong>데이터 보안 설문지</strong>를 작성하지않거나 <strong>개인정보처리방침 url 에 누락된 정보나 url 자체에 문제</strong>가 발생할경우 앱이 PlayStore 에서 내려가도록 되어있습니다.</p>
<p>만약 앱이 이슈로 내려갔다면 기존에 작성되어있던 데이터보안 설문지를 확인하여 앱이 새로운 개인정보를 데이터 보안 설문지에 적혀있는 데이터보다 <strong>추가적인 데이터</strong>를 가져오게 되는지 혹은 <strong>개인정보 처리방침 Url 에 누락되어있는 정보나 url 자체가 접속이 되지 않는지 확인</strong>하고 조치해야합니다.</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/397acd54-43e8-4fd3-90f8-bf7390965d02/image.png" alt=""></p>
<h3 id="결과">결과</h3>
<p>성공!!</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/9ee599c3-b1b8-4317-84d4-14cbe06cc316/image.png" alt=""></p>
<p>구글 플레이 정책의 무서움을 깨달아버렸다..</p>
<p><img src="https://velog.velcdn.com/images/yunsung_/post/c6355da7-454c-4001-a353-da05dba6c91c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체지향과 절차지향의 오해]]></title>
            <link>https://velog.io/@yunsung_/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-%EC%A0%88%EC%B0%A8%EC%A7%80%ED%96%A5%EC%9D%98-%EC%98%A4%ED%95%B4</link>
            <guid>https://velog.io/@yunsung_/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-%EC%A0%88%EC%B0%A8%EC%A7%80%ED%96%A5%EC%9D%98-%EC%98%A4%ED%95%B4</guid>
            <pubDate>Mon, 07 Mar 2022 11:06:54 GMT</pubDate>
            <description><![CDATA[<h1 id="하나하나씩-뜯어보면서-알아보자">하나하나씩 뜯어보면서 알아보자</h1>
<p><img src="https://images.velog.io/images/yunsung_/post/190c8b30-9896-46e3-8c76-a4a4792081b5/image.png" alt=""></p>
<p>위의 사진을 보자. 먼저 절차지향을 보면 순서대로 프로그램을 실행하는 프로그램임이 틀림없습니다. </p>
<p>그 다음으로 눈길이가는 객체지향을 확인해봅시다. </p>
<p>개별적으로 자주 참조되거나 호출되는 변수들을 분리하는 </p>
<p>함수들을 &#39;객체&#39;라는 틀로 묶어서 사용했다는 것은 둘째 치고, </p>
<p><code>순서대로 차근차근 진행되는 실행방식</code>에 있어서는 차이점이 없는 것 같습니다.</p>
<h1 id="문제점을-짚어보자-🔎">문제점을 짚어보자 🔎</h1>
<blockquote>
<p>평소에 우리는 객체지향에 대해 알아보려면 어떻게 하나요?</p>
</blockquote>
<p>네 맞습니다. <code>절차지향 방식이랑 비교</code>를 합니다. (마치 꼬리표처럼 말이죠.)➰</p>
<p>하지만 이상하지 않나요? 왜 객체지향을 공부할때는 항상 절차지향과 비교를 하는건가요?</p>
<blockquote>
<p>객체지향은 코드가 절차에 따르지않고 동시다발적 혹은 순서가 없다는말이 될까요?</p>
</blockquote>
<p><img src="https://images.velog.io/images/yunsung_/post/b8e72ca4-be2c-440e-babf-6688fa8e67ce/image.png" alt=""></p>
<h1 id="정확히-다시-알아보자-👀">정확히 다시 알아보자 👀</h1>
<p>우선 두개의 방식 모두 순서대로 차근차근 진행되는 실행방식에 있어서는 차이점이 없는 것 같습니다. </p>
<blockquote>
<p>그렇다면 <strong>왜!</strong> 대체 짝꿍처럼 객체지향에 대해 알아보려면 굳이 &quot;절차&quot;지향과 비교하는것일까요?</p>
</blockquote>
<p>사실 객체지향의 개념과 특징을 설명함에 있어서 굳이 절차지향과 비교할 필요는 없습니다. 
<img src="https://images.velog.io/images/yunsung_/post/369ac8de-c8b9-4e94-af0c-fb57e1c9aeac/image.png" alt=""></p>
<p><strong>객체</strong> 란 기존의 방식인 변수 따로, 함수 따로의 분산적이고 통일성 없는 추상화 과정을 통합하여 표현 대상(문제 해결 대상)을 좀 더 <code>모듈화 하기 쉽게 도와주는 도구</code>에 불과하고</p>
<p><strong>객체지향</strong> 프로그래밍은 객체의 디자인을 한 뒤에 이들의 데이터 플로우를 짜고 진행 시나리오를 <code>설계해나가는 방식의 개발 방법론</code>일 뿐이기 때문입니다.</p>
<p><code>플로우차트를 먼저짜느냐 데이터 모델링을 먼저 하느냐의 차이</code> 지 그 이후로는 절차지향 프로그램이나 객체지향 프로그램이나 다들 정해진 알고리즘을 따라 순서대로 실행되는 건 마찬가지 입니다.</p>
<p><del>사실 절차지향이라는 말이 햇깔리게 만드는 주범이 아닐까요? 😅</del></p>
<p>굳이 &quot;절차지향&quot;이라는 이름을 써야 한다면</p>
<blockquote>
<p>&quot;절차지향 프로그래밍은 프로그램의 순서와 흐름을 먼저 세우고 필요한 자료구조와 함수들을 설계하는 방식이고, 객체지향 프로그래밍은 반대로 자료구조와 이를 중심으로 한 모듈들을 먼저 설계한 다음에 이들의 실행 순서와 흐름을 짜는 방식이다&quot;</p>
</blockquote>
<p>정도의 설명을 추가하는 것만으로도 그렇게까지 헷갈릴 일은 없을 것입니다. 그게 아니면, </p>
<p>절차지향이라는 이름 대신 차라리 <code>&quot;비 객체지향 방식&quot;</code>이라고 쓰는 게 나을지도 모릅니다.</p>
<blockquote>
<p>알기 쉽도록! 🙋</p>
</blockquote>
<p><img src="https://images.velog.io/images/yunsung_/post/9fef0f16-1d43-45bd-8cce-bbbbef216eaf/image.png" alt=""></p>
<p>당신이 다음과 같은 레고성을 만들고 싶을 때 만드는 유형을 2가지로 나눌 수 있는데,</p>
<pre><code>- 설명서를 보고 레고 블록을 그때그때 그 순서에 맞게 조립하는 방법

- 성을 조립할 레고블록을 성의 머리부분, 성의 다리부분, 성의 내부부분을

  각 파트별로 미리조립 후 필요할 때 조립하는 방법</code></pre><p>와 같이 두 방법이 있는것이죠. 😁</p>
<h1 id="착각-바로잡기-📿">착각 바로잡기 📿</h1>
<p><img src="https://images.velog.io/images/yunsung_/post/ccc4c900-e3c9-4d38-bfaf-ee90d459cb09/image.png" alt=""></p>
<p>앞서 살펴본 자판기 프로그램 예시에서도 볼 수 있듯이, 여러 책에서는 절차지향과 객체지향을 
비교할 때 대부분이 절차지향은 <code>&quot;순서도&quot;</code> 로, 객체지향은 <code>&quot;클래스 다이어그램&quot;</code> 으로 도표를 그려서 비교해놔서 혼란을 가중시킵니다.</p>
<p>애당초 순서도와 클래스 다이어그램은 서로 용도부터 다른 도구입니다. </p>
<p>알다시피 순서도는 <code>알고리즘</code>을 표현하기 위한 도구이고, </p>
<p>클래스 다이어그램은 자료구조 간의 <code>상호관계</code>를 기술하기 위한 도구입니다.</p>
<p>객체지향 방식으로 설계하더라도 그 알고리즘을 표현할 때 당연히 순서도를 활용할 수 있으며, 반대로 절차식 방식으로 설계하더라도 클래스 다이어그램을 이용하여 데이터들과 모듈 간의 관계를 설명할 수 있습니다.</p>
<p>물론 확실히 절차식 프로그래밍에서는 거의 <code>순서도 위주</code>로, </p>
<p>반대로 객체지향 프로그래밍에서는 <code>*UML 위주</code>로 설계가 이루어지는 것은 엄연한 사실입니다.</p>
<p>그러나 클래스 다이어그램의 개념과 용도에 익숙하지 않은 상태에서 저 그림만 본다면 객체지향 프로그램은 모듈들이 <code>실시간으로 정보를 주고받고, 순서에 상관없이 동시다발적으로 실행된다</code> 는 착각을 심어줄 수 도 있습니다.</p>
<blockquote>
<p><strong>UML 이 대체 뭔데?</strong></p>
<p>쉽게 말하자면 시스템의 정적인 상태인 &#39;논리적인 구조&#39;를 표현합니다.</p>
<p>클래스간의 관계를 한눈에 파악하는게 주 목적입니다.</p>
</blockquote>
<h1 id="결론-🔨">결론 🔨</h1>
<p>객체지향은 기존의 방식에 비해 프로그램의 수행절차를 간소화해주지만, 절차를 무시하는 것은 결코 아닙니다.</p>
<p>깔끔한 모듈화를 통해서, 단 몇 줄의 메서드 호출만으로 구성된 메인 함수를 작성할 수도 있도록 도와주지만 수행절차가 간소화되어 코드가 대폭 줄었다고 할지라도</p>
<p>코드 각 부분의 실행 순서는 엄연히 존재하며, 근본적으로 모듈화라는 것은 그 함수가 호출될 때 참조되어 실행되어야 할 다량의 본체 코드가 존재하고 단지 이를 별도의 장소로 분리해서 <code>정리해놓는 기술</code>이라는 사실을 잊어서는 안 됩니다.</p>
<p>절차를 줄여주고 읽기에 깔끔한 프로그램으로 보이는 것은 순전히 사용자(개발자) 입장에서나 추상화를 통해 그렇게 느끼는 것이며,</p>
<p>시스템 내부적인 관점에서는 실제로 실행되는 코드의 양은 절차식이나 객체지향이나 큰 차이가 없으며 둘 다 한 번에 한 줄씩 <code>&#39;순서대로&#39; 실행시키는 것</code> 에는 다름이 없다는 것을 명확히 해야합니다.</p>
<p>집에 물건들을 정리한다고해서 물건자체의 질량이 줄어드는건 아닌것처럼 말이죠. 🤗</p>
<h1 id="번외">번외</h1>
<blockquote>
<p>UML 의 특징</p>
</blockquote>
<pre><code>- OOP는 유연성(Flexibility)을 가진다.

- 유연성은 캡슐화와 추상화, 다형성을 이용해서 달성된다.

- 상속은 캡슐화를 활용하고, 다형성은 상속을 이용해서 만들어진다.

- 캡슐화를 통해서 가시성(Visibility) 개념이 만들어지고, 클래스 개념이 만들어진다.

- 클래스는 타입과 필드, 그리고 메서드를 가지고 있다.

- 클래스 개념은 인터페이스(Interface), 추상 클래스(Abstract Class), 구체 클래스-(Concrete Class) 개념을 파생시킨다.

- 객체(Object)는 구체 클래스(ConcreteClass)가 가진 개념을 포함한다.

- 객체는 Identity를 가진다.(구체 클래스를 통해 클래스가 가진 개념도 가지게 되므로 Method, Field, Type도 가지게 된다.)

- 메서드는 프로토타입을 가지며, 추상 메서드는 메서드의 일종이다.

- 필드는 Reference와 Primitive로 나뉜다.

- 클래스, 메서드, 필드는 가시성을 가진다.

- 클래스는 상속 가능(Inheritable)하다.

- 메서드는 재정의 가능(Overridable)하고, 오버 로딩 가능(Overloadable)하다.
</code></pre><blockquote>
<p>객체지향 vs 절차지향</p>
</blockquote>
<pre><code>1. 객체지향은 절차를 간소화하는 것이지, 결코 절차를 무시하는 게 아니다.

2. 절차지향은 데이터와 함수가 분리되고 통일성이 없지만, 객체지향은 좀 더 모듈화 되어 체계적이다.

3. 절차지향은 과도한 전역 변수의 사용, 스파게티 소스, 변경과 확장, 프로그램에 대한 이해

    가 어렵지만, 객체지향은 코드의 재사용성이 높다.

4. 절차지향은 프로그램의 순서와 흐름을 먼저 세우고 필요한 자료구조와 함수들을 설계하는 방

    식이고, 객체지향은 반대로 자료구조와 이를 중심으로 한 모듈들을 먼저 설계한 다음에 이들의 

    실행 순서와 흐름을 짜는 방식이다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Binding Adapter 에 대해 알아보자!]]></title>
            <link>https://velog.io/@yunsung_/Binding-Adapter-%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/@yunsung_/Binding-Adapter-%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 12 Sep 2021 09:08:43 GMT</pubDate>
            <description><![CDATA[<h2 id="before-start_">before start_</h2>
<p>안드로이드 xml을 짜다 문득 건들고 싶은 뷰의 속성이 xml 메서드에 없는것을 보았다.
그래서 엑티비티에서 내가 원하는 메서드를 만들어 사용하듯이 xml에서도 사용할 수 있는 속성 설정을 만들어 사용할 수있는 방법에 대해 찾아보다, binding Adapter 라는 속성을 발견하게 되었다. 그래서 오늘은 Binding Adapter에 대해 알아보도록 하겠다.</p>
<p><img src="https://jjalbang.today/jjv1HQ.gif" alt=""></p>
<h2 id="binding-adapter_">Binding Adapter_</h2>
<h3 id="1-사용-방법">1. 사용 방법</h3>
<pre><code>object TestBindingAdapter {
    @JvmStatic
    @BindingAdapter(&quot;this_is_binding_text&quot;)
    fun setText(view: TextView, text: String){
        view.text = text + &quot;\n&quot; + &quot;하지만 바인딩 어뎁터로 바뀌었습니다!&quot;
    }
}</code></pre><ul>
<li><p>object: Binding Adapter는 메모리상에 올려서 사용해야 하기 때문에 Object로 생성한다.</p>
</li>
<li><p>@JvmStatic: 전역 변수의 Getter Setter를 정적 함수로 설정하는 어노테이션이다.</p>
</li>
<li><p>@BindingAdapter: 괄호 안에 원하는 메서드 이름을 지어주면 된다.</p>
</li>
<li><p>setText: 이 메서드 이름도 원하는 걸로 정해주면 된다. 텍스트 뷰를 수정하는 기본 메서드 이름이 setText라고 해서 꼭 그걸 따라 사용하지 않아도 된다. (오버 라이딩 아님)</p>
</li>
</ul>
<p>다음은 바인딩 어뎁터의 필수 속성이다. 또한 setText 메서드 안에서 &#39;view.&#39; 까지만 입력해도 자신이 커스텀 할 수 있는 기능들이 다양하게 나타난다. 여기서는 사용자가 원하는 메서드를 이용해 원하는 기능을 넣어주면 된다.</p>
<blockquote>
<p>추가적으로 어노테이션에 대해 알고싶다면 다음 블로그를 참고하자!
<a href="https://jaejong.tistory.com/106">날고싶은개발자님 블로그</a></p>
</blockquote>
<h3 id="2-layout">2. Layout</h3>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;layout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools%22%3E

    &lt;data&gt;
        &lt;variable
            name=&quot;activity&quot;
            type=&quot;com.example.selfstudy_kotlin.MainActivity&quot; /&gt;
    &lt;/data&gt;

    &lt;androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        tools:context=&quot;.MainActivity&quot;&gt;

        &lt;TextView
            android:id=&quot;@+id/txt_number&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:textSize=&quot;24sp&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot;

            &lt;--인자 값으로 activity에 있는 introduce라는 변수를 넘겨준다.--&gt;

            this_is_binding_text=&quot;@{activity.introduce}&quot;/&gt; 
    &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
&lt;/layout&gt;</code></pre><p>레이아웃에서는 다음과 같이 사용할 수있다. 이때 앞에 &#39;android:&#39; 나 &#39;app:&#39;을 붙이지 않아도 된다. </p>
<h3 id="3-activity">3. Activity</h3>
<pre><code>class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    var introduce = &quot;안녕하세요. 저는 일반적인 텍스트 입니다.&quot;

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.activity = this
    }
}</code></pre><p>대충.. 바인딩을 배웠으면 알거라 믿지만 모르는 사람들을 위해 설명하자면 바인딩을<code>private lateinit var binding: ActivityMainBinding</code>로 받아와주고 다음과 같이 <code>binding = DataBindingUtil.setContentView(this, R.layout.activity_main)</code> 로 초기화를 시켜준다. 그리고 현재 엑티비티의 객체를 <code>binding.activity = this</code> 로 넘겨주는 코드이다.</p>
<h3 id="4-binding-adapter-왜필요한건데">4. Binding Adapter! 왜필요한건데?</h3>
<p><img src="https://images.velog.io/images/yunsung_/post/4dc91f15-9edd-4122-9b58-200cc1293de1/image.png" alt=""></p>
<p>단순히 뷰에 사용할 메서드를 직접 자신이 원하는대로 커스텀 할 수있다는 사실만으론 <code>binding Adapter</code> 를 써야하는 이유가 크게 와닫지않았다. 또한 방금의 코드들은 엑티비티에서 처리하게 하면 더욱 짧은 코드로 변환 할 수있었다.</p>
<p>그래도 굳이 쓰는 이유를 곰곰히 생각해 보았더니.. 역시 핵심은 <code>역할의 분리</code> 인것 같았다.</p>
<p>엑티비티에서 물론 처리해도 되지만 엑티비티에서 모든 로직을 처리하게 된다면 나중에는 엄청난 스파게티코드와 덤으로 엄청난 유지보수까지 ㅎㅎ.. 겪게 될것이다. 따라서 각자의 역할과 기능을 가지는 모듈로 잘게 쪼개서 보다 가시성도 늘리고 MVVM, ViewModel, Data Binding 등을 이용해 클린코드를 만들려고 사용하는 느낌인것 같다. </p>
<h2 id="result_">result_</h2>
<pre><code>안녕하세요. 저는 일반적인 텍스트 입니다.


               ⬇️          


안녕하세요. 저는 일반적인 텍스트 입니다.

하지만 바인딩 어뎁터로 바뀌었습니다!</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[HiltViewModel 이해하기]]></title>
            <link>https://velog.io/@yunsung_/HiltViewModel-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/HiltViewModel-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 11 Sep 2021 14:09:38 GMT</pubDate>
            <description><![CDATA[<h2 id="overview_">Overview_</h2>
<p>안드로이드 앱 개발시 뷰 모델을 구현할 때, 액티비티 또는 프래그먼트의 구성 변경에도
데이터가 유지되는 <strong>AAC-ViewModel🔎</strong> 클래스를 상속하는것을 권장합니다. <strong>AAC-ViewModel🔎</strong>을 사용할 때 생명주기에 맞춰 데이터를 유지, 보존하기위해 프로그래머는 <strong>ViewModelProvider.Factory🏭</strong> 를 구현하고 <strong>ViewModelProvider🤲</strong> 클래스가 뷰모델 인스턴스를 관리하도록 해야합니다.</p>
<p>하.지.만! </p>
<p>매번 ViewModelProvider.Factory 를 구현하려면 많은 중복 코드가 발생할 수 밖에없는데, 기존에는 이러한 중복코드를 막을 방법이 없었지만.. <code>Dagger2, Koin</code>과 같은 의존성 주입 라이브러리를 통해 비교적 편리하게 사용할 수 있게 되었습니다! 🎉</p>
<p>사실 Dagger라는 라이브러리도... 좋긴좋지만 복잡한 어노테이션, 높은 러닝커브 때문에 불편함이 많았던건 사실이라고 합니다...</p>
<p>그.래.서 </p>
<p><code>Dagger Hilt</code> 라는 라이브러리가 새로나오면서 적은 코드만으로 뷰모델 주입이 가능해졌습니다!(🎉🎉 이건 두번폭죽감이지ㅋㅋ)</p>
<p>이러한 배경을 보다가 최근 Dagger Hilt 공부를 시작하게되었는데 코드가 어떻게 작동하는지 문득 궁금해져서 공부겸 기록으로 남기려고 동작 원리에 대한 글을 쓰게 되었습니다.</p>
<h2 id="before-entering_">Before Entering_</h2>
<h3 id="multibindings">Multibindings</h3>
<p>우선 먼저 알아야하는 기능이 있습니다. 바로바로! ``Multibindings🎉`</p>
<blockquote>
<p><strong>멀티바인딩이란?</strong>
@IntoMap 어노테이션이 지정된 모든 주입 대상 객체를 컴포넌트 내부에 선언된 Map 인스턴스에 모아주는 기능입니다!</p>
</blockquote>
<p>우리는 이러한 바인딩을 기존 Dagger2 에서도 지원하는 기능인 멀티바인딩을 활용하여ViewModelProvider.Factory를 매번 구현하지 않고도 사용이 가능했습니다.</p>
<p><strong>ex)</strong>
<img src="https://images.velog.io/images/yunsung_/post/8278261a-3b0f-4601-92a4-33d716d2d6c3/image.png" alt="">이러한 클래스 묶음을</p>
<p><img src="https://images.velog.io/images/yunsung_/post/0871d2ba-3588-49fc-9d75-4c7cf395e600/image.png" alt=""></p>
<p>다음과같이 어노테이션이 해당되는클래스의 함수를 Map으로 묶어주는 느낌!</p>
<h2 id="기존-dagger2-에서의-뷰모델-생성-방식">기존 Dagger2 에서의 뷰모델 생성 방식</h2>
<p><img src="https://images.velog.io/images/yunsung_/post/f0868d6a-22fd-4fe9-834e-f64a2a127e0a/image.png" alt=""></p>
<ol>
<li>Dagger2 에서 멀티바인딩 기능을 사용하여 뷰모델을 식별해줄 Key 와 ViewModel 객체를 모듈에 선언합니다.</li>
<li>ViewModelProvder.Factory 를 상속받은 뷰모델팩토리(ViewModelFactory) 클래스를 만들어서 대거를 통해 주입받은 map(creators)에서 뷰모델 key 로 조회하여 뷰모델을 생성하도록 구현</li>
<li>액티비티에서는 viewModelFactory 를 주입 받아서 뷰모델을 생성!</li>
</ol>
<h3 id="개선된점">개선된점</h3>
<p>Dagger2 를 사용 했을 때는 뷰모델 클래스가 추가될 때마다 매번 viewModelFactory 를 만들어주어야 하는 불편함은 개선 되었지만, 멀티바인딩 기능에 대한 학습이 필요하고 의존성 주입을 하기 위해 필수적으로 구현해야 하는 모듈이 많았습니다.</p>
<h2 id="dagger-hilt-에서의-뷰모델-생성방식">Dagger Hilt 에서의 뷰모델 생성방식</h2>
<p><img src="https://images.velog.io/images/yunsung_/post/de41ca12-00ba-4822-872d-148c3efa3656/image.png" alt=""></p>
<p>1.액티비티에서는 @AndroidEntryPoint 어노테이션을 읽어서 Hilt_XXXActivity 를 생성합니다
2.Hilt_XXXActivity 내부에서는 getDefaultViewModelProviderFactory() 메서드를 오버라이드하여 앞서 Dagger2 를 사용할 때 직접 만들었던 ViewModelFactory 와 동일한 역할을 수행하기 위한 HiltViewModelFactory 를 반환하도록 합니다
3.activity-ktx 라이브러리에 선언된 확장함수를 이용하여 간단하게 AAC-ViewModel 을 생성하고 사용할 수 있게 되었습니다.</p>
<h2 id="conclusion_">Conclusion_</h2>
<p>Dagger Hilt 에서 개발자가 직접 구현했어야 했던 모듈, 컴포넌트를 모두 미리 생성해주어서 간단하게 DI 을 할 수 있게 되었습니다!!🎉</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC 패턴에 대해 알아보자]]></title>
            <link>https://velog.io/@yunsung_/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVC-%ED%8C%A8%ED%84%B4%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/@yunsung_/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVC-%ED%8C%A8%ED%84%B4%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 23 Aug 2021 08:02:08 GMT</pubDate>
            <description><![CDATA[<h2 id="시작에-앞서">시작에 앞서</h2>
<p>나는 개발을 하다가 종종 전에 코드를 짯던 프로젝트를 들어간다.</p>
<pre><code>내가 전에 짯던 코드를 보면 어떤방식으로 내가 어떻게 해결했나 하는 기억이 나기 때문이다.🔍</code></pre><p>   프로젝트를 들어가서 많은걸 느끼는데,  전에 내가 이런 문제를 해결했다니 하는 감탄도 나오지만 한편으로는 맛있는 스파게티(?) 코드가 보인다.
   <img src="https://media.giphy.com/media/iJJ6E58EttmFqgLo96/giphy.gif" alt=""></p>
<pre><code>                  나도 클린코드짜고싶어..</code></pre><p> 그래서 자연스럽게 <strong>클린코드(clean code)</strong> 에 눈을 들이게 되었고 그대로 공부를
 해보았으나 공부를 하다가 생각해보니 개념만 머리속에서 빙빙돌고 정확한 정리를 한 후
 써보는게 좋겠다 생각해서 디자인 패턴 개념 정리를 시리즈로 해보려 한다.</p>
<p><del>사실 면접때 이런 질문이 나올거같기도해서..</del></p>
<pre><code>앞으로 클린코드를 써, 유지 보수에 대해 민감하게 반응 할 수 있는 개발자가 되기로 하자!</code></pre><h2 id="그래서-디자인-패턴-🎨-이-뭔데">그래서 디자인 패턴 🎨 이 뭔데?</h2>
<blockquote>
<p>소프트웨어 개발 방법에서 사용되는 디자인 패턴은, 프로그램 개발에서 자주 나타나는 과제를 해결하기 위한 방법 중 하나로, 과거의 소프트웨어 개발 과정에서 발견된 설계의 노하우를 축적하여 이름을 붙여, 이후에 재이용하기 좋은 형태로 특정의 규약을 묶어서 정리한 것이다. 알고리즘과 같이 프로그램 코드로 바로 변환될 수 있는 형태는 아니지만, 특정한 상황에서 구조적인 문제를 해결하는 방식을 설명해 준다. 
<a href="https://ko.wikipedia.org/wiki/%EB%94%94%EC%9E%90%EC%9D%B8_%ED%8C%A8%ED%84%B4">위키피디아</a></p>
</blockquote>
<p>다음과 같이 디자인 패턴의 정의는 쉽게말해서 건축물을 지을 때 쓰는 건축 공법같은것이다.
<img src="https://images.velog.io/images/yunsung_/post/fcbf1fd8-3b17-4bae-8bfa-fa53157ef19e/image.png" alt="">
굳이 지키지 않아도 무방하지만 지키게되면 더 좋은 결과를 낼 수 있다.</p>
<p>안드로이드에서의 디자인 패턴을 코딩에 사용하게되면 의해 코드도 보기 좋게 작성이 됨과 동시에 
<strong>유닛 테스트</strong> 및 <strong>유지보수</strong> 또한 편해진다.</p>
<p>아무것도 모르는 초보시절에는 계산기, 일기장, 메모장과같은 디자인 패턴 없이 할 수 있는 간단한 앱을 만들때에는 별로 디자인패턴의 유용함을 느끼지 못하지만 실제 서비스 사용량이 많거나 더욱 복잡한 로직의 앱을 개발하다 보면 시간이 흐르면서 사용자가 원하는 요구사항이 더 생기고 처음보다 더 복잡한 기획이 생기고 유지 보수할 일이 생긴다. </p>
<pre><code>(실제로 나도 겪고 있는 현상이다.)</code></pre><blockquote>
<p>만약 이러한 디자인 패턴 없이 개발을 진행했다면?</p>
</blockquote>
<p>   변경이 될 때마다 화면을 구성하는 코드와 비즈니스 로직이 들어간 코드를 각각 동시에 매번 수정을 해야 한다..😅</p>
<p> 이러한 문제는 결국 서로 간의 의존성이 강해짐에 따라 유지보수를 하기가 힘들어 진다는것을 의미한다.</p>
<p>이러한 문제들을 해결하기 위해 디자인 패턴이 나오게 되었으며 <strong>Model-View 사이의 관계를 어떻게 해결해 나가냐</strong>에 따라 여러 패턴으로 갈려진다.</p>
<h2 id="그래서-내가-등장했지-mvc">그래서 내가 등장했지! MVC!</h2>
<p><img src="https://images.velog.io/images/yunsung_/post/5aa67e9c-478c-49f5-90c2-a8362dd517fc/image.png" alt=""></p>
<p> MVC는 안드로이드와 관계없이 프로그래밍 시 가장 널리 사용되는 구조 중 하나이며 
 간단하게 <strong>Model, View, Control</strong> 의 약자이다.</p>
<p> MVC 구조에서의 입력은 모두 Control에서 발생하게 되며 관리되게 되는 구조이다.
이벤트가 발생한 Control에 의해 각 모듈의 정의와 View의 사용 용도가 달라지게 된다.</p>
<h3 id="mvc-구조">MVC 구조</h3>
<ul>
<li><p><strong>모델(Model)</strong>
데이터 + 상태 + 비즈니스 로직 입니다. 말하자면 앱의 두뇌 역할! 
뷰나 컨트롤러에 묶이지 않으므로 많은 곳에서 재사용할 수 있다.</p>
</li>
<li><p><strong>뷰(View)</strong>
뷰는 모델의 표현 입니다. UI를 그리고 사용자가 앱과 상호작용할 때 컨트롤러와 통신하는 책임을 맡습니다. MVC 구조에서 뷰는 하위 모델에 대한 지식이나 상태에 대한 이해가 없고, 사용자가 버튼을 클릭하거나 값을 입력하는 등의 행동을 할 때 무엇을 해야 하는지 모른다는 점에서 상당히 “멍청합니다”. 그 이유는 뷰가 덜 알수록 모델에 종속되지 않으므로 보다 변화에 유연할 수 있기 때문이죠.</p>
</li>
<li><p><strong>컨트롤러(Controller)</strong>
컨트롤러는 앱을 묶어주는 접착제 입니다. 애플리케이션에서 발생하는 일을 담당하는 마스터 컨트롤러 역할이죠. 뷰가 컨트롤러에게 사용자가 버튼을 눌렀다고 알리면, 컨트롤러는 그에 따라 어떻게 모델과 상호작용할지 결정합니다. 모델에서 데이터가 변화되는 것에 따라 컨트롤러는 뷰의 상태를 적절하게 업데이트하도록 결정할 수 있습니다. 안드로이드 앱에서는 컨트롤러가 주로 <strong>액티비티나 프래그먼트</strong>로 표현됩니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/9516cb6d-ab54-4126-ab97-1abf226bd586/image.png" alt=""></p>
</li>
</ul>
<h2 id="mvc의-장점">MVC의 장점</h2>
<ul>
<li>Model과 View의 분리됨.</li>
<li>Model의 비종속성으로 재사용 가능함.</li>
<li>구현하기 가장 쉽고 단순함.</li>
<li>유닛테스트에서 View는 테스트 할 부분이 없기 때문에 쉽게 Model만 테스트 가능.</li>
<li>개발자라면 누구나 쉽게 파악 가능함.</li>
<li>개발기간이 짧아짐. (안드로이드에서의 장점)</li>
<li>그냥 다른거 생각할것 없이 안드로이드 액티비티에서 모든 걸 다 동작하게 처리만 잘 해주면 개발 기간이 짧아질수도 있다.<h2 id="mvc의-단점">MVC의 단점</h2>
</li>
<li>Model과 View사이에 의존성 발생함. (서로간의 의존성 완전히 없앨 수 없음)
즉, View의 UI 갱신을 위해 Model을 직/간접적으로 참조하므로 앱 자체가 커지고 로직이 복잡해질수록 유지보수가 힘들어집니다.</li>
<li>스파게티 코드가 될 가능성이 높음.</li>
<li>코드 복사/붙여넣기가 많아지게 되면서 코드 분리조차 되지않으면 코드가 아주 제대로 꼬여버립니다. 그렇기에 복잡도는 증가합니다. 다만, 이는 설계 단계에서 제대로해서 분리를 잘하면 어느정도 해소는 됩니다.</li>
<li>시간이 지날수록 컨트롤러에 많은 코드가 쌓여 코드가 비대화하여 문제 발생 가능
Controller가 안드로이드 API에 깊게 종속되므로 유닛 테스트가 어려움.</li>
</ul>
<p>이렇게 오늘은 디자인 패턴의 개념과 MVC를 알아보았다! 다음시간에는 MVP 패턴에 대해 알아보겠다.</p>
<hr>
<p>출처 : <a href="https://velog.io/@jojo_devstory/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%ED%8C%A8%ED%84%B4-MVC%EA%B0%80-%EB%AD%98%EA%B9%8C">안드로이드 MVC</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ViewModel 뽀개기❗️]]></title>
            <link>https://velog.io/@yunsung_/ViewModel-%EB%BD%80%EA%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/ViewModel-%EB%BD%80%EA%B0%9C%EA%B8%B0</guid>
            <pubDate>Mon, 12 Jul 2021 02:28:17 GMT</pubDate>
            <description><![CDATA[<h2 id="viewmodel-그게-뭔데-💁">ViewModel 그게 뭔데? 💁</h2>
<p>특정 액티비티 화면과 연동되며, 해당 화면에 보여줄 데이터를 형식화하는 로직을 두는곳.
화면에서 필요한 모든 데이터를 &quot;종합&quot; 하고 &quot;형식화&quot; 할 수 있는것이다.</p>
<blockquote>
<p>즉,생명주기에 영향을 받지않고 값을 저장 할 수 있는곳이다!</p>
</blockquote>
<h2 id="근데-왜-why-필요해❓">근데 왜 (Why?) 필요해❓</h2>
<p>화면이 로테이션 되어 엑티비티가 재실행 되거나 잠시 앱을 나가고 다시 실행할 경우해당 엑티비티의 리소스는 제거하고 다시 새로운 엑티비티의 리소스를 가져오게됩니다.</p>
<p>그래서..</p>
<p>우리는 생명주기가 변경되어도 저장되어야 하는 값은 저장시켜주어야합니다. 하지만 화면이 변경될 때 마다 저장을 일일이 시켜주기란.. 쉽지않습니다. 코드도 일일히 작성해야하고 무엇보다 리소스를 소모하게되어 좋지 않을 수 있습니다.</p>
<p>그.래.서!</p>
<p>우리는 <strong>ViewModel</strong> 을 사용하게 되었습니다! 왜냐하면 ViewModel 객체는 엑티비티의 Lifecycle 상태가 종료(FINISH)상태가 될 때 까지 소멸되지 않습니다. 즉, 화면이 위와같은 이유로 재실행되어도 ViewModel은 데이터를 앱이 Finished 가 되기전 까지 유지하고있습니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/46919603-8db4-402a-a386-2eeab5217160/image.png" alt=""></p>
<h2 id="좋아-그럼-사용하는방법은-🧾">좋아! 그럼 사용하는방법은? 🧾</h2>
<h3 id="우선-문제가-있는-앱을-참고해봅시다">우선 문제가 있는 앱을 참고해봅시다!</h3>
<p><img src="https://images.velog.io/images/yunsung_/post/0e4e4788-4e6b-400d-befd-d118aca9280b/image.png" alt=""></p>
<p>다음과 같은 심플한 타이머를 만들어 보았습니다. 이 앱의 문제점은?</p>
<blockquote>
<p>회전을 하면 타이머가 다시 0부터 시작해요!!</p>
</blockquote>
<p><img src="https://images.velog.io/images/yunsung_/post/d5e183a4-ef2e-4cd7-9b59-e0a28d7e0c48/image.png" alt="">
(Rotation되면서 Configuration Change가 발생하였고 액티비티가 재실행되어 다시 0부터 시작하는 모습)</p>
<p>그래서 우린 이 오류를 <strong>ViewModel</strong> 로 고쳐볼겁니다.</p>
<h3 id="viewmodel로-문제를-해결해봅시다🥸">ViewModel로 문제를 해결해봅시다!🥸</h3>
<h4 id="우선-viewmodel을-작성해주겠습니다">우선 ViewModel을 작성해주겠습니다!</h4>
<p><img src="https://images.velog.io/images/yunsung_/post/392d4579-cd95-4c64-b6f7-fd397208e1b6/image.png" alt=""></p>
<h4 id="그-다음-mainactivity의-코드를-작성해-주겠습니다">그 다음 MainActivity의 코드를 작성해 주겠습니다!</h4>
<p><img src="https://images.velog.io/images/yunsung_/post/11304dd3-6453-4b45-bbb7-71679091149f/image.png" alt=""></p>
<p>ViewModelProviders로 ViewModel의 객체를 생성해주고 그걸 이용해 countsave()로 ViewModel에 값을 저장하고 getcount로 값을 받아와 타이머에 넣어주어 타이머가 작동되게 해 주었습니다. 이제 만약 액티비티가 재실행된다고 해도 ViewModel은 소멸되지 않고 이전에 생성한 것을 사용하기 때문에 startTime은 처음에 설정된 값을 갖고 있습니다.</p>
<p>다음과 같이 하면??!?</p>
<p><img src="https://images.velog.io/images/yunsung_/post/e1860486-3927-4800-8476-ac56716898af/image.png" alt=""></p>
<p>쨘! 🎉 다음과 같이 화면을 돌려도 값이 변하지않는것을 알 수 있습니다.</p>
<h2 id="tmi추가-정보들--🦔">TMI(추가 정보들)  🦔</h2>
<h4 id="viewmodel에-대해-조금더-알아볼까요-🔍">ViewModel에 대해 조금더 알아볼까요? 🔍</h4>
<ol>
<li><p>ViewModelProVider (요청) =&gt; ViewModel (객체반환)</p>
</li>
<li><p>장치구성이 변경되어도 ViewModel을 다시 불러오는것이 아닌 최초로딩이된것을 가져오고 MainActivity의  상태에따라 그대로 변화한다.</p>
</li>
<li><p>단방향! MainActivity 는  ViewModel을 참조하지만 ViewModel은 MainActivity를 참조하지 않는다.</p>
</li>
<li><p>만약 ViewModel에서 Context나 Activity 객체를 사용하고 싶다면 ViewModel을 사용하지 말고 AndroidViewModel를 사용해야 합니다.</p>
</li>
</ol>
<p><strong>주의할점!</strong></p>
<ul>
<li><p>소멸되어야 하는 객체의 참조를 다른 객체가 참조하면 메모리 유실이 생길 수 있다! - 강한참조</p>
</li>
<li><p>현재 사용되지 않는 과거 액티비티의 참조를 가지게되어 ViewModel 인스턴스가 과거 액티비티의 뷰를 변경하려고 하면 오류발생!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Room DataBase 뽀개기! ]]></title>
            <link>https://velog.io/@yunsung_/Room-DataBase-%EB%BD%80%EA%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@yunsung_/Room-DataBase-%EB%BD%80%EA%B0%9C%EA%B8%B0</guid>
            <pubDate>Wed, 16 Jun 2021 07:28:41 GMT</pubDate>
            <description><![CDATA[<h2 id="room-너-정체가-뭐야-🧐">Room! 너 정체가 뭐야? 🧐</h2>
<h3 id="1sqlite-➡️-room">1.SQLite ➡️ Room</h3>
<p>Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리 입니다!
우리가 평소에 사용하는앱에도 Room이 적용되어 있는경우가 많은데 </p>
<ul>
<li>메모저장</li>
<li>나만의 노래 리스트</li>
<li>즐겨찾기 <em>etc...</em>(피드백 감사)</li>
</ul>
<p>다음과 같은 일을 하기위해 과거에는 <strong>SQLite</strong> 라는 데이터 베이스를 이용하였으나
<img src="https://images.velog.io/images/yunsung_/post/22ee762a-1d2b-4e3a-b51e-f66e23061cb2/image.png" alt=""></p>
<p>다음과 같이... 사용하기 어렵다는 빨간줄이 막 그어져있다.(구글도 인정한 어려움 ㅠㅠ 🤬) 
그.래.서 나온것이 <strong>Room</strong> 친구라는것이다! </p>
<ul>
<li>구글도 SQLite 대신 Room을 사용할 것을 권장하고있다. 💁</li>
</ul>
<p>하지만 그렇다고해서 Room이 완전히 달라진것은 아니고 
SQLite 이 친구를 활용하는 개념이다 라고만 보면된다. </p>
<h3 id="2room-너-어떻게-생겼어">2.Room 너 어떻게 생겼어?</h3>
<p><img src="https://images.velog.io/images/yunsung_/post/618e61ad-ff08-4e3f-aa30-56019fdcc52d/image.png" alt=""></p>
<p>위 사진에서 </p>
<ul>
<li>Room Database</li>
<li>Data Access Objects</li>
<li>Entities </li>
</ul>
<p>이 세가지 부분이 Room의 구성요소이고 Rest of The App은 앱의 나머지 부분을 뜻합니다. 
<code>즉, (Room Database - DAO - Entities) ↔️ Rest of The App</code>
다음과 같이 세가지 요소와 서로 <code>get,set!</code> 과 같이 서로 주고받는다! 
정도로 이해하고 넘어가시면 됩니다.</p>
<p>Room 의 요소 하나하나는 조금이따가 설명해드릴 예정입니다!😎</p>
<h2 id="그래서-사용은-어떻게해야하는건데-📝">그래서 사용은 어떻게해야하는건데? 📝</h2>
<h3 id="1gradle-추가">1.gradle 추가!</h3>
<p>👉 JAVA 기준</p>
<pre><code>    implementation &#39;androidx.room:room-runtime:2.2.6&#39;
    annotationProcessor &#39;androidx.room:room-compiler:2.2.6&#39;
</code></pre><p>👉 Kotiln 기준</p>
<pre><code>// gradle맨 위쪽에 추가

         plugins {
            id &#39;kotlin-kapt&#39;
        }
     implementation &#39;androidx.room:room-runtime:2.2.6&#39;
     kapt &#39;androidx.room:room-compiler:2.2.6&#39;</code></pre><h3 id="2entity">2.Entity</h3>
<p><img src="https://images.velog.io/images/yunsung_/post/0481c641-0f2d-47b1-907d-eb19ceb4da30/image.png" alt=""></p>
<blockquote>
<p>관련이 있는 속성들이 한곳에 모여 하나의 정보단위 를 이룬것을 말합니다!</p>
</blockquote>
<p>위의 사진과 같이 Primary Key(해당 테이블의 이름)를 기준으로 
관련있는 사용자의 정보가 한곳에 모여있는것을 보시면 이해하기 쉽습니다.</p>
<pre><code>// @Entity(tableName=&quot;userProfile&quot;) 와 같이 직접 해당 테이블의 이름을 지정한다.
// 놔두면 해당클래스 이름으로 테이블이름을 지정한다.

@Entity(tableName = &quot;userinfo&quot;)
data class UserEntity (
    //기본키설정
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = &quot;id&quot;) val id : Int = 0,
    //필드명 즉, 열의 이름을 다르게
    @ColumnInfo(name = &quot;name&quot;) val name : String,
    @ColumnInfo(name = &quot;email&quot;) val email : String
)</code></pre><p><strong>이제 개념을 알았으니 위의 코드를 보며 공부해보자! 💁</strong></p>
<p>우선 Enitiy를 생성해야 한다.(데이터베이스 테이블을 만든다고 생각하자!)</p>
<p>data class에 @Entity *어노테이션을 붙어주고 저장하고싶은 속성의 
변수 이름과 타입을 지정해주자!</p>
<p>또한 primaryKey 라는 어노테이션도 보이는데, 이 어노테이션은 대표값 즉, 
각각의 유저가 가진 id 와 같이 중복되지 않는 값을 넣을 때 사용되는데
위의 어노테이션이 적용되면 해당 변수는 기본키가 된다.</p>
<p><code>primaryKey는 유일한 키값이기 때문에 유일한 값이어야한다.본인이 직접 지정할 수 도있지만 autoGenerate = true으로 자동 생성 할 수도있다.</code></p>
<blockquote>
<p>*어노테이션(annotation) : &#39;@&#39;가 붙은 데이터를 설명하는 데이터</p>
</blockquote>
<br>

<h3 id="3-dao">3. DAO</h3>
<blockquote>
<p>데이터에 접근할 수 있는 메서드를 정의해놓은 인터페이스이다.</p>
</blockquote>
<pre><code>@Dao
interface UserDao {

    //Insert = 데이터베이스에서 값을 꺼내거나 값을 넣을때 인자값으로 끼워넣을 수 있다.

    @Insert
    fun insertUser(user : UserEntity?)

    @Delete
    fun deleteUser(user : UserEntity)

    @Update
    fun UpdateUser(user : UserEntity?)

}</code></pre><p>즉, Entity를 어떻게 해줄건지 <strong>결정하는 설명서</strong>🧾 라고 보면될거같다.</p>
<p>Dao는 위와같이 이렇게 생성하면된다. (interface 라는점을 유의하면서보자) </p>
<p>Entity와 같이 어노테이션을 지정하지만 이번에는 @Dao 어노테이션을 지정하고 그 안에 메소드를 지정해주는데</p>
<ul>
<li>@Insert를 붙이면 테이블에 데이터 삽입</li>
<li>@Update를 붙이면 테이블의 데이터 수정</li>
<li>@Delete를 붙이면 테이블의 데이터 삭제</li>
</ul>
<p>라고 이해하면 될거같다.</p>
<p>그렇다면 위와같이 삽입/수정/삭제 이외의 다른 기능을 하는 메서드를 만들고싶다면 어떻게 해야할까??</p>
<pre><code>    @Query(&quot;SELECT * FROM userinfo ORDER BY id DESC&quot;)
    fun getAllUserInfo() : List&lt;UserEntity&gt;
    ------------------------------------------------------
      @Query(&quot;SELECT * FROM User&quot;) 
      // 테이블의 모든 값을 가져와라
    fun getAll(): List&lt;User&gt;
    ------------------------------------------------------
    @Query(&quot;DELETE FROM User WHERE name = :name&quot;) 
    // &#39;name&#39;에 해당하는 유저를 삭제해라
    fun deleteUserByName(name: String)</code></pre><p>그렇다면 위와같이 @Query 어노테이션을 통해 선언하고 그 안에 어떤동작을 할건지 SQL 문법으로 작성을 해주어야한다.</p>
<h3 id="4-room-database">4. Room DataBase</h3>
<pre><code>//entities에는 앞서 만들었던 엔티티를 넣어준다.

@Database(entities = [UserEntity::class],version = 1)
abstract class RoomAppDB : RoomDatabase()  {</code></pre><blockquote>
<p>말 그대로 데이터베이스를 생성해준다.</p>
</blockquote>
<p>이번에는 데이터베이스를 생성하고 관리하는 데이터베이스 객체 만들기 위해서 위와 같은 추상 클래스를 만들어 줘야 한다. 우선 RoomDatabase 클래스를 상속받고, @Database 어노테이션으로 <strong>데이터베이스임</strong>을 표시한다.</p>
<p>또한 <strong>version</strong>은 앱을 업데이트하다가 entity의 구조를 변경해야 하는 일이 생겼을 때 이전 구조와 현재 구조를 구분해주는 역할을 한다.</p>
<pre><code>@Database(entities = arrayOf(User::class, Student::class), version = 1)
abstract class UserDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}</code></pre><p>만약 여러개의 Entitiy가 들어가는 경우라면 위와같이 arrayof 안에 ,로 구분해 넣어준다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/895f02fe-76e5-48b5-bc8c-b8fece869c52/image.png" alt=""></p>
<p><a href="https://developer.android.com/training/data-storage/room?hl=ko">공식문서</a>에서는 데이터베이스 객체를 인스턴스 할 때 싱글톤으로 구현하기를 권장하고 있습니다!</p>
<p><strong>Why?</strong>
일단 여러 인스턴스에 액세스를 꼭 해야 하는 일이 거의 없고, 객체 생성에 비용이 많이 들기 때문입니다.</p>
<blockquote>
<p>이 부분에 대한 이해가 잘 안 된다면 다음 목차로 넘어가도 당장 문제가 생기거나 하진 않습니다. 어디까지나 &quot;권장&quot; 하는 부분이고, 알아 두어야 할 부분이라는 걸 고려만 해두고 넘어가도 무관합니다.</p>
</blockquote>
<pre><code>@Database(entities = [User::class], version = 1)
abstract class UserDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        private var instance: UserDatabase? = null

        @Synchronized
        fun getInstance(context: Context): UserDatabase? {
            if (instance == null) {
                synchronized(UserDatabase::class){
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        UserDatabase::class.java,
                        &quot;user-database&quot;
                    ).build()
                }
            }
            return instance
        }
    }
}</code></pre><p>위와 같이 싱글톤을 사용하기 위해 companion object 로 객체를 선언해서 사용해주면 됩니다. 객체를 생성할 때 databaseBuilder라는 static 메서드를 사용하는데</p>
<ul>
<li>context</li>
<li>database 클래스 </li>
<li>데이터 베이스를 저장할 때 </li>
</ul>
<p>사용할 데이터베이스의 이름을 정해서 넘겨주면 됩니다.</p>
<p><code>❗️주의</code>
<code>다른 데이터베이스랑 이름이 겹치면 꼬여버리니 주의하자!!</code></p>
<h3 id="5-대망의-데이터베이스-사용👻">5. 대망의 데이터베이스 사용👻</h3>
<pre><code>var newUser = User(&quot;김똥깨&quot;, &quot;20&quot;, &quot;010-1111-5555&quot;)

val db = UserDatabase.getInstance(applicationContext)
        db!!.userDao().insert(newUser)</code></pre><blockquote>
<p>다음 형식은 예시일뿐이다.</p>
</blockquote>
<p>위와 같이 전에 만들어놓은 데이터베이스에 insert를 해주면 값이 들어가게된다.</p>
<p><code>❗️주의!</code>
<code>Cannot access database on the main thread since it may potentially lock the UI for a long period of time 와 같은 오류가 생길것이다. 위와같은 오류는 다음과 같이 해결할 수 있는데</code></p>
<pre><code>
1. 컴퓨터! 너 실행하라구 😤

-&gt; allowMainThreadQueries()를 사용해 강제로 실행시킨다

-&gt; 하.지.만 나중에 문제가 생길 수 있다. (공부하는 입장에서는 무관)



2. 컴퓨터님.. 어.. 실행 해주세요 뀨유~ 🙄

-&gt; 비동기 실행을 하면 된다.
</code></pre><hr>
<p>출처 : <a href="https://todaycode.tistory.com/39#recentComments">https://todaycode.tistory.com/39#recentComments</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Bottom AppBar]]></title>
            <link>https://velog.io/@yunsung_/Android-Bottom-AppBar</link>
            <guid>https://velog.io/@yunsung_/Android-Bottom-AppBar</guid>
            <pubDate>Sat, 22 May 2021 14:23:13 GMT</pubDate>
            <description><![CDATA[<h2 id="bottom-appbar란">Bottom AppBar란?</h2>
<p><img src="https://images.velog.io/images/yunsung_/post/64dd0bc1-eb09-4f4a-badc-48742fc986dd/image.png" alt=""></p>
<p>Material 2.0에서 소개된 바텀앱바는 특정한 한두가지의 주요한 동작(액션)이 있는 앱에 사용하기 적합한 UI입니다. 사용자에게 보여주어야할 메뉴가 많은경우에는 Drawer, 4~5가지 정도의 범주로 나누어진다면 Bottom Navigation View 혹은 TabLayout + Viewpager가 적합합니다.</p>
<h2 id="사용하기-유용한-상황">사용하기 유용한 상황!</h2>
<p>ex) </p>
<ul>
<li>TO-DO리스트 </li>
<li>노트처럼 할일만들기 </li>
<li>노트생성과 같은 기능</li>
</ul>
<p>즉, 기능이 명확할때! 사용하기 좋습니다.    </p>
<h2 id="bottom-appbar의-장점과-단점">Bottom AppBar의 장점과 단점!</h2>
<p><img src="https://images.velog.io/images/yunsung_/post/3fabb549-0e19-4233-94b3-fe64b9894d63/image.png" alt=""></p>
<p>Bottom AppBar는 다음과같이 사용자가 스크롤을 해야한다고 미리 암시해놓을 수 있습니다.
동그란 Floating App Bar와 Bottom App Bar 사이에 빈 공간을통해 컨텐츠를 일부분 노출함으로써 직관적으로 스크롤을 내려야 더 많은 컨텐츠를 볼 수 있다고 알려줍니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/496a7152-e9e9-4961-8e11-c62ca1069dfd/image.png" alt=""></p>
<p>또한 큰 장점중의 하나는 다음과 같이 스크롤 시 하단 부분을 가려 전체화면과 동일한 레이아웃효과를 제공합니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/3677991e-ae75-4ea2-9c6e-2b4e1ac53b2d/image.png" alt=""></p>
<p>하지만 다음과같이 Tost가 Appbar를 가릴 수 도있으니 주의해서 사용해야합니다.</p>
<h2 id="bottom-appbar-만들어보기">Bottom AppBar 만들어보기!</h2>
<p><strong>기본형식</strong></p>
<pre><code>&lt;androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;&gt;

  &lt;!-- FrameLayout 혹은 컨텐츠 영역 --&gt;

  &lt;com.google.android.material.bottomappbar.BottomAppBar
      android:id=&quot;@+id/bar&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;wrap_content&quot;
      android:layout_gravity=&quot;bottom&quot;
      app:navigationIcon=&quot;@drawable/ic_menu_24&quot;/&gt;

  &lt;com.google.android.material.floatingactionbutton.FloatingActionButton
      android:id=&quot;@+id/fab&quot;
      android:layout_width=&quot;wrap_content&quot;
      android:layout_height=&quot;wrap_content&quot;
      app:layout_anchor=&quot;@id/bar&quot;/&gt;

&lt;/androidx.coordinatorlayout.widget.CoordinatorLayout&gt;</code></pre><p>다음이 Bottom AppBar의 기본 형식입니다. BottomAppBar의 주의할점은 CoordinatorLayout과 함께 사용해야하며 안드로이드 스튜디오 3.2 이하버전에서는 레이아웃이 제대로 표현되지 않을 수 있습니다.</p>
<p><strong>AppBar의 속성</strong></p>
<p>FAB Alignment(app:fabAlignmentMode)
앱바의 정렬 위치를 설정할 수 있습니다.
<img src="https://images.velog.io/images/yunsung_/post/e46141ec-5740-46d2-a71b-2b89a2965f63/image.png" alt="">
fabAlignment: CENTER
<img src="https://images.velog.io/images/yunsung_/post/fb6d7f86-ebc2-499f-a811-05686def4175/image.png" alt="">
fabAlignment: End</p>
<br>
다음과 같은 코드로 프레그먼트가 전환될때 애니메이션을 줄 수도있습니다.

<pre><code>app_bar.fabAlignmentMode = BottomAppBar.FAB_ALIGNMENT_MODE_END</code></pre><p><img src="https://images.velog.io/images/yunsung_/post/18ae9822-c46d-4217-adb9-08468148686c/image.png" alt=""></p>
<p><strong>FAB Attached</strong></p>
<p>바텀앱바와 플로팅버튼이 떨어져있을지 붙어있을지 설정할 수 있습니다. false로 한다면 아래와같은 형태로 볼 수 있습니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/cf3fc0c4-7584-4b73-a29e-a04e4bfc31dc/image.png" alt=""></p>
<p><strong>fabCradleRoundedCornerRadius</strong>
플로팅버튼과 AppBar사이의 간격을 조절할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/0c0ccb22-23e8-4b18-89a3-61a71aa74e8d/image.png" alt=""></p>
<p><strong>navigation</strong></p>
<p><img src="https://images.velog.io/images/yunsung_/post/7b8c5219-252e-40b1-ac27-f2c7647eb06d/image.png" alt=""></p>
<p>왼쪽의 네비게이션 아이콘과 오른쪽의 메뉴아이콘입니다. 주로 왼쪽은 DrawrLayout처럼 여러 아이콘을 보여줄때 사용합니다.
주로 BottomSheetDialog를 통해 구현합니다.</p>
<pre><code>bottomAppBar.setNavigationOnClickListener</code></pre><p> 반면 오른쪽메뉴는 즐겨찾기, 검색과같이 사용자의 사용빈도가 조금 더 많은 메뉴들을 구현할때 사용합니다. 클릭될 경우 아래와 아래와 같은 리스너를 사용해 구현합니다.</p>
<pre><code> bottom_app_bar.replaceMenu(R.menu.menu_bottom_nav)
bottom_app_bar.setOnMenuItemClickListener({
    //구현부분
})</code></pre><hr>
<p>[출처] : <a href="https://medium.com/harrythegreat/android-bottom-appbar-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-9071ec428138">https://medium.com/harrythegreat/android-bottom-appbar-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-9071ec428138</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Companion Object]]></title>
            <link>https://velog.io/@yunsung_/Companion-Object</link>
            <guid>https://velog.io/@yunsung_/Companion-Object</guid>
            <pubDate>Fri, 14 May 2021 17:02:53 GMT</pubDate>
            <description><![CDATA[<h2 id="thiking🤨">Thiking🤨</h2>
<hr>
<p>안드로이드 Room DB를 공부하던도중 Companion Object라는 키워드를 발견했습니다. 여기서 궁금증이 생겼습니다. 과연 Companion Object가 무엇일까요? 단순히 자바(JAVA)의 static 키워드를 대체하기위해 만들어진 키워드일까요? 이 질문이 우리가 이번에 배우는데에 큰 도움을 줄것입니다.</p>
<h2 id="lets-go">Let&#39;s Go!</h2>
<p>자바의 static 키워드는 클래스 멤버(member)임을 지정하기 위해 사용합니다. 
<strong>static이 붙은 변수와 메소드를 각각 클래스 변수, 클래스 메소드</strong>라 부릅니다. 반면,
<strong>static이 붙지 않은 클래스 내의 변수와 메소드는 각각 인스턴스 변수, 인스턴스 메소드</strong>라 합니다. static이 붙은 멤버는 클래스가 메모리에 적재될 때 자동으로 함께 생성되므로 인스턴스 생성 없이도 클래스명 다음에 점(.)을 쓰면 바로 참조할 수 있습니다.</p>
<pre><code>public final class JAVAClass{
  static public final String TEST = &quot;This is JAVA Test&quot;;  //클래스 변수
  static public method(int i):int{     //클래스 메소드
    return i + 10
  }
}
System.out.println(MyClass.TEST);    //test
System.out.println(MyClass.method(1));   //11</code></pre><p>하지만 JAVA에 있는 static이 코틀린에는 없습니다. 대신 Kotiln은 Companion Object라는 키워드와 함께 블록({})안에 멤버를 구성합니다.</p>
<pre><code>
class KotilnClass{
    companion object{
        val TEST = &quot;This is Kotiln Test&quot;
        fun method(i:Int) = i + 10
    }
}
fun main(args: Array&lt;String&gt;){
    println(MyClass.TEST);    //test
    println(MyClass.method(1));   //11
}</code></pre><p>이 시점에서보면 static과 다를 바가 없습니다. 하지만!! companion object와 static의 차이점은 분명히 있고 static의 한계에 따라 companion object를 쓰는이유를 이제부터 저와 알아봅시다.</p>
<h2 id="kotiln-class-키워드-기초">Kotiln Class 키워드 기초</h2>
<p>우선 코틀린의 class 와 Object를 이해해 봅시다.</p>
<p><strong>class</strong></p>
<pre><code>class WhatThisFood(private val name:String){
    fun FavoriteFood() = &quot;내가 좋아하는 음식은 ${name}입니다.&quot;
}
fun main(args: Array&lt;String&gt;){
    val food1 = WhatThisFood(&quot;마라탕&quot;)
    val food2 = WhatThisFood(&quot;김치찌게&quot;)
    println(food1.WhatThisFood()) // 내가 좋아하는 음식은 마라탕입니다.
    println(food2.WhatThisFood()) // 내가 좋아하는 음식은 김치찌게입니다.
}</code></pre><p>위쪽은 Kotiln Class를 정의하는 방법입니다. 이경우 클래스 WhatThisFoodf를 정의하면서 val name:String값을 생성자의 인자로 받습니다. 자바를 주로 사용하셨던분이라면 이해가 안되실수도 있습니다. 이제 이 시점에서 자바코드로 바꾸어 보겠습니다.</p>
<pre><code>class WhatThisFood{
    private val name:String
    constructor(name:String){
        this.name = name
    }
    fun FavoriteFood() = &quot;내가 좋아하는 음식은 ${name}입니다.&quot;
}
</code></pre><p>위처럼 클래스 생성자는 constructor 키워드를 써서 정의합니다. 그리고 생성자에서 받은인자 name:String인 값을 속성인 this:name을 통해 private val name:String에 할당하고있습니다.</p>
<p>여기에서의 문제점은 <strong>중복되는 부분이 너무 많다</strong>는것입니다.
이러한 문제점을 Kotiln의 Class를 정의하면서 인자값을 받아오는부분으로 해결한것입니다!</p>
<p>즉, val m1 = WhatThisFood(“영수”) 꼴로 클래스에서 객체를 생성합니다.
코틀린에서는 new 키워드 없이 클래스 명 뒤에 괄호()를 붙인다는 점!! (마치 함수호출😁)</p>
<p><strong>Object</strong></p>
<p>코틀린에는 자바에는 없는 &quot;특별한&quot; 싱글턴(Singletion : 인수가 하나만 있는 클래스)가 있습니다. 다음과같이 class 대신 object라는 키워드를 사용합니다. 한번만 객체가 생성되게 하므로 메모리 낭비를 방지할 수 있고, 전역변수이므로 공유도 용이합니다.</p>
<pre><code>object MySingleton{
    val prop = &quot;나는 MySingleton의 속성이다.&quot;
    fun method() = &quot;나는 MySingleton의 메소드다.&quot;
}
fun main(args: Array&lt;String&gt;){
    println(MySingleton.prop);    //나는 MySingleton의 속성이다.
    println(MySingleton.method());   //나는 MySingleton의 메소드다.
}</code></pre><p>싱글톤이기 때문에 시스템 전체에서 쓸 기능(메소드로 정의)을 수행하는 데는 큰 도움이 될 수 있지만, 전역 상태를 유지하는 데 쓰면 스레드 경합 등으로 위험할 수 있으니 주의해서 사용해야 합니다.</p>
<p>이제 우리는 class 와 object에 대해 간결히 배워보았습니다. 이제부터는 이 지식들을 가지고 companion object를 다뤄봅시다!</p>
<h2 id="companion-object는-static이-아니다">Companion object는 static이 아니다!</h2>
<p>사실 Companiond Object는 static과 동일한것이 아니며, 동일하게 보이는것 뿐입니다.
이게 무슨말일까요? 밑의 예제를 살펴봅시다.</p>
<pre><code>class MyClass2{
    companion object{
        val prop = &quot;나는 Companion object의 속성이다.&quot;
        fun method() = &quot;나는 Companion object의 메소드다.&quot;
    }
}
fun main(args: Array&lt;String&gt;) {
    //사실은 MyClass2.맴버는 MyClass2.Companion.맴버의 축약표현이다.
    println(MyClass2.Companion.prop)
    //println(MyClass2.prop)와 같다.

    println(MyClass2.Companion.method())
}</code></pre><p>MyClass2 클래스에 companion object를 만들어 2개의 멤버를 정의했습니다. 이를 사용하는 main() 함수를 보면 이 멤버에 접근하기 위해 클래스명.Companion형태로 쓴 것을 확인할 수 있습니다. </p>
<p>즉, companion object{}는
<strong>MyClass2클래스가 메모리에 적재되면서 함께 생성되는 동반(companion)되는 객체</strong>입니다. 여기서 우리는 변수를 사용하기위해 같이 사용됨을 유추해 볼 수있습니다.</p>
<p>따라서 우리가 알 수 있는사실은 위의 MyClass2.prop 는 MyClass2.Companio.prop의 축약형이라는것을 알 수 있습니다. 우리가 지금까지 생각한 생각은 언어적으로 지원하는 축약 표현 때문에 companion object가 static으로 착각이 드는 것때문이라는것을 알 수 있습니다.</p>
<h2 id="companion-object는-객체입니다">Companion object는 객체입니다.</h2>
<pre><code>class MyClass2{
    companion object{
        val prop = &quot;나는 Companion object의 속성이다.&quot;
        fun method() = &quot;나는 Companion object의 메소드다.&quot;
    }
}
fun main(args: Array&lt;String&gt;) {
    println(MyClass2.Companion.prop)
    println(MyClass2.Companion.method())
    //이렇게 변수에 할당하는 것은 자바의 클래스에서 static 키워드로 정의된 멤버로는 불가능한 방법입니다.
    val comp1 = MyClass2.Companion  //--(1)
    println(comp1.prop)
    println(comp1.method())
    val comp2 = MyClass2  //--(2)
    println(comp2.prop)
    println(comp2.method())
}</code></pre><p>위와 같은 코드에서 우리는 Companion Object가 객체임을 알 수 있습니다
(1)번 주석과 같이 companion Object를 변수에 할당하는것을 볼 수있고,
(2)번 주석과 같이 .Companion을 빼고 클래스안에 들어가있는 companion Object 를 변수에 할당을 할 수있는것도 볼 수 있습니다.</p>
<p>즉, static 과는 다르게 companion object는 독립된 객체임을 알 수있습니다.</p>
<h2 id="companion-object에-이름을-지을-수-있습니다">Companion object에 이름을 지을 수 있습니다.</h2>
<p>companion object의 기본 이름은 Companion이지만 이름을 변경 할 수도 있습니다.</p>
<pre><code>class MyClass3{
    companion object MyCompanion{  // -- (1)
        val prop = &quot;나는 Companion object의 속성이다.&quot;
        fun method() = &quot;나는 Companion object의 메소드다.&quot;
    }
}
fun main(args: Array&lt;String&gt;) {
    println(MyClass3.MyCompanion.prop) // -- (2)
    println(MyClass3.MyCompanion.method())
    val comp1 = MyClass3.MyCompanion // -- (3)
    println(comp1.prop)
    println(comp1.method())
    val comp2 = MyClass3 // -- (4)
    println(comp2.prop)
    println(comp2.method())
    val comp3 = MyClass3.Companion // -- (5) 이름이 바뀌었으므로 에러발생!!!
    println(comp3.prop)
    println(comp3.method())
}</code></pre><p>위의 그림을 보면 주석(1)번에서 companion object에 이름을 주자, 주석(2),(3)번이 변경된 이름그대로 사용 할 수있음을 알 수 있습니다.
그러나 여전히 주석(4)번처럼 Companion키워드를 생략 할 수도있습니다.</p>
<h2 id="클래스내-companion-object는-딱-하나만-쓸-수-있습니다">클래스내 Companion object는 딱 하나만 쓸 수 있습니다.</h2>
<p>클래스내에서 Companion Object는 딱 하나만 쓸 수있습니다. 지금까지 공부한것을 생각해 보았을때 당연한 결과일지도 모릅니다. 왜냐하면 코틀린은 클래스명만으로도 참조가 가능하므로 한 클래스안에 두개의 Companion Object가 들어가있으면 무엇을 가져와야할지 결정을 못하기 때문입니다.</p>
<pre><code>class MyClass5{
    companion object{
        val prop1 = &quot;나는 Companion object의 속성이다.&quot;
        fun method1() = &quot;나는 Companion object의 메소드다.&quot;
    }
    companion object{ // -- 에러발생!! Only one companion object is allowed per class
        val prop2 = &quot;나는 Companion object의 속성이다.&quot;
        fun method2() = &quot;나는 Companion object의 메소드다.&quot;
    }
}</code></pre><p>위처럼 만들면 Only one companion object is allowed per class 에러가 발생합니다.
그렇다고 밑의 코드처럼 Companion object에 이름을 서로 다른이름으로 할당해주어도 오류가 발생합니다.</p>
<pre><code>class MyClass5{
    companion object MyCompanion1{
        val prop1 = &quot;나는 Companion object의 속성이다.&quot;
        fun method1() = &quot;나는 Companion object의 메소드다.&quot;
    }
    companion object MyCompanion2{ // --  에러발생!! Only one companion object is allowed per class
        val prop2 = &quot;나는 Companion object의 속성이다.&quot;
        fun method2() = &quot;나는 Companion object의 메소드다.&quot;
    }
}</code></pre><h2 id="상속-관계에서-companion-object-멤버는-같은-이름일-경우-가려집니다섀도잉-shadowing">상속 관계에서 Companion object 멤버는 같은 이름일 경우 가려집니다(섀도잉, shadowing).</h2>
<p>부모 클래스를 상속한 자식 클래스에 모두 companion object를 만들고 같은 이름의 멤버를 정의했다고 가정합니다. 이때, 자식 클래스에서 이 멤버를 참조하면 부모의 멤버는 가려지고 자식 자신의 멤버만 참조할 수 있습니다. 이해하기 쉽도록 코드로 보겠습니다.</p>
<pre><code>open class Parent{
    companion object{
        val parentProp = &quot;나는 부모값&quot;
    }
    fun method0() = parentProp
}
class Child:Parent(){
    companion object{
        val childProp = &quot;나는 자식값&quot;
    }
    fun method1() = childProp
    fun method2() = parentProp
}
fun main(args: Array&lt;String&gt;) {
    val child = Child()
    println(child.method0()) //나는 부모값
    println(child.method1()) //나는 자식값
    println(child.method2()) //나는 부모값
}</code></pre><p>위 코드처럼 부모/자식의 companion object의 멤버가 다른 이름이라면 자식이 부모의 companion object 멤버를 직접 참조할 수 있습니다. 하지만 같은 이름이면 어떻게 될까요? 다음 코드를 봅시다.</p>
<pre><code>open class Parent{
    companion object{
        val prop = &quot;나는 부모&quot;
    }
    fun method0() = prop //Companion.prop과 동일
}
class Child:Parent(){
    companion object{
        val prop = &quot;나는 자식&quot;
    }
    fun method1() = prop //Companion.prop 과 동일
}
fun main(args: Array&lt;String&gt;) {
    println(Parent().method0()) //나는 부모
    println(Child().method0()) //나는 부모
    println(Child().method1()) //나는 자식
    println(Parent.prop) //나는 부모
    println(Child.prop) //나는 자식
    println(Parent.Companion.prop) //나는 부모
    println(Child.Companion.prop) //나는 자식
}</code></pre><p>위에서  companion object가 같은 이름을 사용했을때의 경우를 보여줍니다.
같은이름을 사용하였을경우 자식클래스의 companion object 속성인 prop이 부모에서 정의 되어 있지만 가려져서 무시되는 것을 볼 수 있습니다.</p>
<p>즉, method0() 메소드는 Parent클래스 것이기때문에 그래서 부모의 companion object의 prop값인 “나는 부모”가 출력됩니다.</p>
<p>여기서 코드를 살짝 바꾸어보겠습니다. 자식클래스의 companion object에 이름을 부여합니다.</p>
<pre><code>open class Parent{
    companion object{
        val prop = &quot;나는 부모&quot;
    }
    fun method0() = prop
}
class Child:Parent(){
    companion object ChildCompanion{ // -- (1) ChildCompanion로 이름을 부여했어요.
        val prop = &quot;나는 자식&quot;
    }
    fun method1() = prop
    fun method2() = ChildCompanion.prop
    fun method3() = Companion.prop
}
fun main(args: Array&lt;String&gt;) {
    val child = Child()
    println(child.method0()) //나는 부모
    println(child.method1()) //나는 자식
    println(child.method2()) //나는 자식
    println(child.method3()) // -- (2)
}</code></pre><p>위 코드에서 주석 (1)에 자식 클래스의 companion object에 ChildCompanion로 이름을 부여했습니다. 그리고 자식 클래스에 3개의 메소드를 정의했습니다. </p>
<p>child.method0()은 <strong>부모의 method</strong>이므로 어렵지 않게 “나는 부모”가 출력된 것을 예상할 수 있습니다. 그리고 child.method1()과 child.method2()도 역시 자식의 companion object의 속성을 가리킨다는 것을 알 수 있습니다.</p>
<p>주석(2)는 어떤 결과가 나올까요? 답부터 말씀드리면 “나는 부모”입니다. 자식의 companion object를 뛰어넘어서 부모의 companion object에 직접 접근이 가능하게 되었습니다.</p>
<p>왜냐하면 자식은 ChildCompanion로 이름을 바꿨으니 부모의 Comaniond을가져와 여기서 Companion은 부모가 됩니다.</p>
<p>우리가 여기서 알 수 있는점은  부모/자식의 companion object에 정의된 멤버는 자식 입장에서 접근할 수 있지만, 이름이 겹친다면 <strong>섀도잉(shadowing)</strong> 이 되어 감춰진다는 점을 알 수 있었습니다.</p>
<hr>
<p>출처 :<a href="https://www.bsidesoft.com/8187">https://www.bsidesoft.com/8187</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ListView와 RecyclerView]]></title>
            <link>https://velog.io/@yunsung_/ListView%EC%99%80-RecyclerView</link>
            <guid>https://velog.io/@yunsung_/ListView%EC%99%80-RecyclerView</guid>
            <pubDate>Wed, 07 Apr 2021 11:45:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>ListView대신 RecyclerView가 나온 배경</p>
</blockquote>
<p><code>ListView</code>에서는 모든 데이터에대한 View를 만들고, View가 사라졌다가
나타날때마다 리소스를 불러와야한다. </p>
<p>즉 이말은 화면을 아래로 스크롤했다가 다시 위로 올릴때마다 리소스를 다시 불러오게 된다는소리다. </p>
<p>이러한 방법은 많은 메모리와 저장공간을 사용하기 때문에 대용량의 정보(데이터)를 불러오게된다면 앱이 느려지거나 충돌할 가능성이 있다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/58622294-cbef-4f61-ac82-f896bd8d5fdc/image.png" alt=""></p>
<p>이러한 이유때문에 <code>RecyclerView</code>가 ListView의 단점을 보안하기위해 나온것이다. </p>
<p><code>ViewHolder</code>를 필수적으로 사용해야하고, <code>LayoutManager</code>를 설정하는등 ListView보다 더 복잡한 구조긴하지만 앱에서 <code>메모리를 효율적으로 사용</code>할 수 있게 해주니 복잡한 구조말고는 장점이 두드러지는것이다.</p>
<blockquote>
<p>사용하기위해 준비해야할것</p>
</blockquote>
<pre><code>1.Gradle에서 Implement 추가
2.데이터 클래스 정의
3.레이아웃에 RecyclerView 추가
4.item 생성
5.어댑터 생성
6.어댑터 설정</code></pre><p>다음과 같은 리스트를 다뤄보도록 하겠다.
<img src="https://images.velog.io/images/yunsung_/post/7cb40672-697b-4aed-bef9-c0994e1b2121/image.png" alt=""></p>
<h3 id="1-gradle에서-implementation-추가">1. Gradle에서 Implementation 추가</h3>
<blockquote>
<p>RecyclerView는 기본 API에 제공되어 있지 않기 때문에, Support Library 추가를 해주어야 사용가능</p>
</blockquote>
<p><img src="https://images.velog.io/images/yunsung_/post/8abca15d-a86b-44be-80f4-1d4d2c86f99e/image.png" alt=""></p>
<p>Gradle Scrpits - build.gradle (Module: app)경로로 파일을 열어서 dependencies 안에 implementation을 추가해주어야한다.</p>
<pre><code>implementation &#39;com.android.support:recyclerview-v7:26.1.0&#39;</code></pre><p>혹은</p>
<pre><code>implementation &quot;androidx.recyclerview:recyclerview:1.1.0&quot;</code></pre><p>을 입력해준 다음 gradle 파일을 수정하고 나면 우측 상단의 Sync Now 를 눌러서 설정을 업데이트 한다.</p>
<h3 id="2-데이터-클래스-정의">2. 데이터 클래스 정의</h3>
<blockquote>
<p>데이터를 저장하는 공간!</p>
</blockquote>
<p>여기서는 개의 목록을 나타내는 RecyclerView를 만들기때문에 새로운 클래스인 DogClass를 만들었다. 데이터 변수는 breed,gender,age,photo로 구성하였다.</p>
<ul>
<li>여기서 photo는 <code>drawable</code>에 들어갈 이미지 파일의 이름</li>
</ul>
<pre><code>
class Dog (val breed: String, val gender: String, val age: String, val photo: String)</code></pre><p>그리고 변수를 초기화한후 개의 목록을 가지고있을 ArrayList를 추가로 써준다.</p>
<pre><code>/* MainActivity.kt* /

var dogList = arrayListOf&lt;Dog&gt;() // 우선 빈 dogList라는 리스트 생성</code></pre><h3 id="3레이아웃에-recyclerview-추가">3.레이아웃에 RecyclerView 추가</h3>
<p>전에 추가한 Gradle에 대해서 추가한 코드가 이번에는 빛을 발할시간이다! <code>&lt;android.support.v7.widget.RecyclerView&gt;</code> 경로를 통해 RecyclerView를 레이아웃에 추가하였다!</p>
<pre><code>// XML Layout코드
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;android.support.constraint.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&gt;

    &lt;android.support.v7.widget.RecyclerView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:id=&quot;@+id/mRecyclerView&quot;
        android:layout_marginStart=&quot;8dp&quot;
        android:layout_marginEnd=&quot;8dp&quot;
        android:layout_marginTop=&quot;8dp&quot;
        android:layout_marginBottom=&quot;8dp&quot;&gt;
    &lt;/android.support.v7.widget.RecyclerView&gt;

&lt;/android.support.constraint.ConstraintLayout&gt;</code></pre><h3 id="4-item-생성">4. Item 생성</h3>
<p>이번엔 RecyclerView의 항목 하나하나를 담당할 <code>item</code>뷰를 만든다. 여기서는 xml Layout파일을 왼쪽에 사진, 상단에 종류, 하단에 나이와 성별이 오도록 했다.</p>
<pre><code>&lt;!--main_rv_item.xml--&gt;
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;android.support.constraint.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;60dp&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:background=&quot;@drawable/item_border&quot;
    android:layout_marginTop=&quot;2dp&quot;
    android:layout_marginBottom=&quot;2dp&quot;
    android:layout_marginStart=&quot;4dp&quot;
    android:layout_marginEnd=&quot;4dp&quot;&gt;

    &lt;ImageView
        android:id=&quot;@+id/dogPhotoImg&quot;
        android:layout_width=&quot;54dp&quot;
        android:layout_height=&quot;54dp&quot;
        android:layout_marginBottom=&quot;4dp&quot;
        android:layout_marginStart=&quot;8dp&quot;
        android:layout_marginTop=&quot;4dp&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:srcCompat=&quot;@mipmap/ic_launcher_round&quot; /&gt;

    &lt;TextView
        android:id=&quot;@+id/dogBreedTv&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginStart=&quot;16dp&quot;
        android:textSize=&quot;20sp&quot;
        android:textStyle=&quot;bold&quot;
        app:layout_constraintStart_toEndOf=&quot;@+id/dogPhotoImg&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/dogPhotoImg&quot;
        tools:text=&quot;Breed&quot;/&gt;

    &lt;TextView
        android:id=&quot;@+id/dogAgeTv&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:textSize=&quot;16sp&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/dogPhotoImg&quot;
        app:layout_constraintStart_toStartOf=&quot;@+id/dogBreedTv&quot;
        tools:text=&quot;Age&quot; /&gt;

    &lt;TextView
        android:id=&quot;@+id/dogGenderTv&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginStart=&quot;16dp&quot;
        android:textSize=&quot;16sp&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/dogAgeTv&quot;
        app:layout_constraintStart_toEndOf=&quot;@+id/dogAgeTv&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/dogAgeTv&quot;
        tools:text=&quot;Gender&quot; /&gt;

&lt;/android.support.constraint.ConstraintLayout&gt;</code></pre><p>또한 다음 테두리를 가진 레이아웃을 만들기위해 drawable을 만들고,<code>android:background=&quot;@drawable/item_border&quot;</code>옵션을 주었다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/f83b0649-d6fe-434e-a66e-003c367e8feb/image.png" alt=""></p>
<h3 id="5-어댑터-생성">5. 어댑터 생성</h3>
<p>RecyclerView와 그곳에 들어갈 각각의 item, 연동할 데이터까지 설정을 마쳤다면 <code>Adapter</code>를 만들어야한다! 다음 사진과같이 <code>사진,종류,나이,성별등 어느 요소를 View에 넣을 것인지 연결해주는것</code>이 Adapter의 역할이다! (이제부터 신경쓸것이 많아지기 시작한다)</p>
<p><img src="https://images.velog.io/images/yunsung_/post/3e583bd5-28ed-4876-9a5c-78d02261ed5b/image.png" alt=""></p>
<p>우선, <code>Context</code> 와 <code>ArrayList&lt;Class&gt;</code>를 필요로 한다.</p>
<pre><code>class MainRvAdapter(val context: Context, val dogList, ArrayList&lt;Dog&gt;) :
RecyclerView.Adapter&lt;&gt;() {

    }</code></pre><p>RecyclerView.Adapter 에서는 <code>ViewHolder</code>라는것이 필요한데, 아직 만들어지지않았다. 따라서 <code>RecyclerView.Adapter&lt;&gt;</code> 부분에 오류가 날것이다. 당황하지말자!! 여기서 여기에 필요한 Holder클래스를 추가해주면 된다.</p>
<pre><code>class MainRvAdapter(val context: Context, val dogList, ArrayList&lt;Dog&gt;):
RecyclerView.Adapter&lt;&gt;() {

    inner class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView) {
        val dogPhoto = itemView?.findViewById&lt;ImageView&gt;(R.id.dogPhotoImg)
        val dogBreed = itemView?.findViewById&lt;TextView&gt;(R.id.dogBreedTv)
        val dogAge = itemView?.findViewById&lt;TextView&gt;(R.id.dogAgeTv)
        val dogGender = itemView?.findViewById&lt;TextView&gt;(R.id.dogGenderTv)

        fun bind (dog: Dog, context: Context) {
          /* dogPhoto의 setImageResource에 들어갈 이미지의 id를 파일명(String)으로 찾고,
          이미지를 불러오지 못했을경우 안드로이드 기본 아이콘을 표시한다.*/
            if (dog.photo != &quot;&quot;) {
                val resourceId = context.resources.getIdentifier(dog.photo, &quot;drawable&quot;, context.packageName)
                dogPhoto?.setImageResource(resourceId)
            } else {
                dogPhoto?.setImageResource(R.mipmap.ic_launcher)
            }
    /* 나머지 TextView와 String 데이터를 연결한다! 가져온 내용을 나타내주어야 하기때문 */
            dogBreed?.text = dog.breed
            dogAge?.text = dog.age
            dogGender?.text = dog.gender
        }
    }
}</code></pre><p>Holder의 상단에서는 각 View의 변수이름을 정해 변수를 가져오고, <code>findViewById</code>를 통해 ImageView인지 TextView인지 등등 종류를 구별하고 id를 통해 layout과 연결된다. <code>bind</code>함수는 ViewHolder와 클래스의 각 변수를 연결하는역할을 한다. 쉽게 말해 값을 itemLayout에서 가져오고 값을 대입해주는 역할을 해주는것이다.</p>
<p>그리고 상단 RecyclerView.Adapter&lt;&gt;의 괄호에 지금 만든 Holder를 넣는다. inner Class로 만들었기 때문에 MainRvAdapter.Holder를 입력한다.</p>
<pre><code>class MainRvAdapter(val context: Context, val dogList, ArrayList&lt;Dog&gt;):
RecyclerView.Adapter&lt;&gt;() //&lt;- 여기부분!</code></pre><p>또한 메인 클래스에 들어가보게되면 오류가 발생되어있는데, 필수로 사용해야 하는 함수를 Override하지 않았기 때문이다. <code>Alt + Enter</code>를 눌러 목록에 있는 세 개의 함수를 모두 Override 한다.</p>
<blockquote>
<p><strong>뷰홀더 설명</strong></p>
</blockquote>
<p>onCreateViewHolder : 화면을 최초 로딩하여 만들어진 View가 없는 경우, xml파일을 inflate하여 ViewHolder를 생성한다.</p>
<p>getItemCount : RecyclerView로 만들어지는 item의 총 개수를 반환한다.</p>
<p>onBindViewHolder : 위의 onCreateViewHolder에서 만든 view와 실제 입력되는 각각의 데이터를 연결한다.</p>
<pre><code>override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder {
    val view = LayoutInflater.from(context).inflate(R.layout.main_rv_item, parent, false)
    return Holder(view)
}

override fun getItemCount(): Int {
    return dogList.size
}

override fun onBindViewHolder(holder: Holder?, position: Int) {
    holder?.bind(dogList[position], context)
}</code></pre><blockquote>
<p>모든함수 + 뷰홀더 최종코드</p>
</blockquote>
<pre><code>class MainRvAdapter(val context: Context, val dogList: ArrayList&lt;Dog&gt;) :
RecyclerView.Adapter&lt;MainRvAdapter.Holder&gt;() {
    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder {
        val view = LayoutInflater.from(context).inflate(R.layout.main_rv_item, parent, false)
        return Holder(view)
    }

    override fun getItemCount(): Int {
        return dogList.size
    }

    override fun onBindViewHolder(holder: Holder?, position: Int) {
        holder?.bind(dogList[position], context)
    }

    inner class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView) {
        val dogPhoto = itemView?.findViewById&lt;ImageView&gt;(R.id.dogPhotoImg)
        val dogBreed = itemView?.findViewById&lt;TextView&gt;(R.id.dogBreedTv)
        val dogAge = itemView?.findViewById&lt;TextView&gt;(R.id.dogAgeTv)
        val dogGender = itemView?.findViewById&lt;TextView&gt;(R.id.dogGenderTv)

        fun bind (dog: Dog, context: Context) {
            if (dog.photo != &quot;&quot;) {
                val resourceId = context.resources.getIdentifier(dog.photo, &quot;drawable&quot;, context.packageName)
                dogPhoto?.setImageResource(resourceId)
            } else {
                dogPhoto?.setImageResource(R.mipmap.ic_launcher)
            }
            dogBreed?.text = dog.breed
            dogAge?.text = dog.age
            dogGender?.text = dog.gender
        }
    }
}</code></pre><h3 id="6어댑터-설정">6.어댑터 설정</h3>
<p><strong>*LayoutManager</strong> : RecyclerView의 각 item들을 배치하고, item이 더이상 보이지 않을 때 재사용할 것인지 결정하는 역할을 한다.</p>
<p>ListView Adapter와는 다르게, RecyclerView Adapter에서는 <code>*레이아웃 매니저 (LayoutManager)</code> 를 설정해주어야 한다.</p>
<blockquote>
<p>레이아웃 매니저를 써야하는 이유?</p>
</blockquote>
<p>LayoutManager 사용을 통해 불필요한 findViewById를 수행하지 않아도 되고, 앱 성능을 향상시킬 수 있다. 또한 RecyclerView를 불러올 액티비티에 LayoutManager를 추가한다.</p>
<p>마지막으로 recyclerView에 <code>setHasFixedSize</code> 옵션에 true 값을 준다.</p>
<blockquote>
<p>true 옵션을 주는이유!</p>
</blockquote>
<p> item이 추가되거나 삭제될 때 RecyclerView의 크기가 변경될 수도 있고, 그렇게 되면 계층 구조의 다른 View 크기가 변경될 가능성이 있기 때문</p>
<pre><code>       //[기본 3개의 Manager중 하나]
        //-LinearLayoutManager
        //-GridLayoutManager
        //-StaggeredGridLayoutManager

         val lm = LinearLayoutManager(this)
        mRecyclerView.layoutManager = lm
        mRecyclerView.setHasFixedSize(true)</code></pre><p> 하지만 여기서 마지막이 아니다! itemLayout에 넘겨줄 Arraylist가 비어있기 때문이다! 마지막으로 값이 잘들어가는지 Arraylist에 값을 넣어보자! 
 (Edit Text와 Button으로 값을 입력해주는것도 해줄 수 있지만 우리는 여기까지만하자)</p>
<pre><code> var dogList = arrayListOf&lt;Dog&gt;(
    Dog(&quot;Chow Chow&quot;, &quot;Male&quot;, &quot;4&quot;, &quot;dog00&quot;),
    Dog(&quot;Breed Pomeranian&quot;, &quot;Female&quot;, &quot;1&quot;, &quot;dog01&quot;),
    Dog(&quot;Golden Retriver&quot;, &quot;Female&quot;, &quot;3&quot;, &quot;dog02&quot;),
    Dog(&quot;Yorkshire Terrier&quot;, &quot;Male&quot;, &quot;5&quot;, &quot;dog03&quot;),
    Dog(&quot;Pug&quot;, &quot;Male&quot;, &quot;4&quot;, &quot;dog04&quot;),
    Dog(&quot;Alaskan Malamute&quot;, &quot;Male&quot;, &quot;7&quot;, &quot;dog05&quot;),
    Dog(&quot;Shih Tzu&quot;, &quot;Female&quot;, &quot;5&quot;, &quot;dog06&quot;)
)</code></pre><hr>
<h3 id="source">source.</h3>
<ul>
<li><a href="https://blog.yena.io/studynote/2017/12/06/Android-Kotlin-RecyclerView1.html">https://blog.yena.io/studynote/2017/12/06/Android-Kotlin-RecyclerView1.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin]ConstraintLayout]]></title>
            <link>https://velog.io/@yunsung_/KotlinConstraintLayout</link>
            <guid>https://velog.io/@yunsung_/KotlinConstraintLayout</guid>
            <pubDate>Sun, 04 Apr 2021 07:19:20 GMT</pubDate>
            <description><![CDATA[<h3 id="constraintlayout의-이해">ConstraintLayout의 이해</h3>
<p>우선, 설명하기전에 이 단어에 대해서 알아볼 필요가 있습니다!</p>
<pre><code>&lt;Button android:id=&quot;@+id/button1&quot; ...
         app:layout_constraintRight_toRightOf=&quot;parent&quot; /&gt;</code></pre><p><code>app:layout_constraintRight_toRightOf=”parent”</code> 이게 과연 무슨뜻일까요? 
정답은, button1 right의 constraint를 줄건데, 이건 parent의 right이다! 라는 뜻입니다.
즉, <code>parentlayout 속성의 오른쪽으로 배치한다!</code></p>
<p><img src="https://images.velog.io/images/yunsung_/post/c0b5082a-cc75-4346-b75a-84eaa969d877/image.png" alt=""></p>
<p>여기에서 좌우일렬로 서로 붙어있는 버튼 2개를 배치하고싶을 땐 어떻게 해야할까요?
하나의 버튼에는 바로 위처럼 right를 주고, 다른 하나의 버튼에는 left를 설정해주면 될까요? 한번 해보겠습니다.</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;android.support.constraint.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;
  xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;&gt;

  &lt;Button
    android:id=&quot;@+id/button1&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    app:layout_constraintRight_toRightOf=&quot;parent&quot;/&gt;

  &lt;Button
    android:id=&quot;@+id/button2&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    app:layout_constraintLeft_toLeftOf=&quot;parent&quot;/&gt;

&lt;/android.support.constraint.ConstraintLayout&gt;</code></pre><p><img src="https://images.velog.io/images/yunsung_/post/054fdf43-1ec8-4d04-92b0-56af25344417/image.png" alt=""></p>
<p>어라? 두개의 버튼이 서로 떨어져있네요? 우리의 목적은 버튼 두개를 붙여야하는데...
우리는 여기에서 한가지 개념을 더 공부할 필요가있습니다.
바로..</p>
<p>Chains입니다!</p>
<h3 id="chains">Chains</h3>
<blockquote>
<p>chain내의 뷰간에 공간을 공유하고 사용 가능한 공간을 분할하는 방식을 제어</p>
</blockquote>
<p>즉, 뷰 서로서로가 연결되어있다 정도로 이해하면 쉽습니다.</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;android.support.constraint.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;&gt;

  &lt;Button
    android:id=&quot;@+id/button1&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    app:layout_constraintRight_toRightOf=&quot;parent&quot;
    app:layout_constraintLeft_toRightOf=&quot;@+id/button2&quot; /&gt;

  &lt;Button
    android:id=&quot;@+id/button2&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
    app:layout_constraintRight_toLeftOf=&quot;@id/button1&quot; /&gt;

&lt;/android.support.constraint.ConstraintLayout&gt;</code></pre><p>이 코드는 위의 코드와 다르게 @id값을 참조하고 있습니다. button1의 왼쪽은 부모대신 button2의 오른쪽에 있고, button2의 오른쪽은 부모대신 button1의 왼쪽에 있게 하는 
설정값을 준것입니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/aef4f094-f372-45fb-92be-0b928a43a9f5/image.png" alt=""></p>
<p>실제로, 두 뷰를 서로 연결해주게 되면, 가운데에 다음과같은 체인모양이 형성됩니다!
하지만 두개의 상호관계를 정해주었긴해도 아직 두 뷰가 붙진 않았습니다..
여기서 chainStyle을 알고가셔야 합니다.</p>
<p><img src="https://images.velog.io/images/yunsung_/post/08f68e07-0296-443d-aae3-f4fd2a8c2fc5/image.png" alt=""></p>
<p>여기서 우리는 버튼을 붙여야 하니까 3번을 선택해줍니다!</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;android.support.constraint.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;&gt;

  &lt;Button
    android:id=&quot;@+id/button1&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:text=&quot;button1&quot;
    app:layout_constraintRight_toRightOf=&quot;parent&quot;
    app:layout_constraintLeft_toRightOf=&quot;@+id/button2&quot; /&gt;

  &lt;Button
    android:id=&quot;@+id/button2&quot;
    android:text=&quot;button2&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    app:layout_constraintHorizontal_chainStyle=&quot;packed&quot;
    app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
    app:layout_constraintRight_toLeftOf=&quot;@id/button1&quot; /&gt;

&lt;/android.support.constraint.ConstraintLayout&gt;</code></pre><p><img src="https://images.velog.io/images/yunsung_/post/7b7bc559-da5d-436f-885d-466f01e792de/image.png" alt=""></p>
<p>우와! 드디어 두개의 버튼이 붙었네요! 하지만 가운데로 정렬이 되어있습니다. 그이유는
각 버튼의 오른쪽과 왼쪽이 parent가 기준으로 되어있기 때문입니다.</p>
<p>이때 왼쪽으로 붙이고싶다면? </p>
<pre><code>app:layout_constraintHorizontal_bias=&quot;0&quot; </code></pre><p>이라는 코드만 추가하시면 됩니다. default값은 기본적으로 0.5로 설정되어있고
default값보다 작으면 왼쪽, 크면 오른쪽으로 배치됩니다.</p>
<h3 id="tip">Tip</h3>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;android.support.constraint.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot;&gt;

  &lt;TextView
    android:id=&quot;@+id/text1&quot;
    android:text=&quot;button2button2button2button2&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:maxLines=&quot;1&quot;
    app:layout_constrainedWidth=&quot;true&quot;
    app:layout_constraintHorizontal_weight=&quot;1&quot;
    app:layout_constraintHorizontal_bias=&quot;0&quot;
    app:layout_constraintHorizontal_chainStyle=&quot;packed&quot;
    app:layout_constraintStart_toStartOf=&quot;parent&quot;
    app:layout_constraintEnd_toStartOf=&quot;@id/button1&quot; /&gt;

  &lt;Button
    android:id=&quot;@+id/button1&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:text=&quot;button1&quot;
    app:layout_constraintEnd_toEndOf=&quot;parent&quot;
    app:layout_constraintStart_toEndOf=&quot;@id/text1&quot; /&gt;

&lt;/android.support.constraint.ConstraintLayout&gt;</code></pre><blockquote>
<p>dth를 0dp로 설정하고 weight를 1로 설정해도 버튼이 계속 사라지는 문제</p>
</blockquote>
<p>android:layout_width=”wrap_content”와 app:layout_constrainedWidth=”true” 으로 해결</p>
]]></description>
        </item>
    </channel>
</rss>