<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>DoStudyPlz.log</title>
        <link>https://velog.io/</link>
        <description>중요한 건 꺾여도 다시 일어서는 마음</description>
        <lastBuildDate>Sun, 26 Oct 2025 06:38:44 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>DoStudyPlz.log</title>
            <url>https://images.velog.io/images/jeon-yj/profile/b090257d-cced-448e-8ea4-ed73347ac958/KakaoTalk_20211105_161144335.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. DoStudyPlz.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jeon-yj" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[트러블슈팅] 한글 입력 시 Enter 이벤트가 두 번 호출되는 문제]]></title>
            <link>https://velog.io/@jeon-yj/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Input-%ED%95%9C%EA%B8%80-%EB%91%90</link>
            <guid>https://velog.io/@jeon-yj/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Input-%ED%95%9C%EA%B8%80-%EB%91%90</guid>
            <pubDate>Sun, 26 Oct 2025 06:38:44 GMT</pubDate>
            <description><![CDATA[<p>React에서 텍스트 입력 핸들러를 구현하다가
한글 입력 중 Enter를 누르면 이벤트가 두 번 호출되는 현상이 있었다.
영문 입력 시에는 문제가 없었지만, 한글 입력에서만 중복 호출이 발생했다.</p>
<br />

<h2 id="현상">현상</h2>
<p>다음은 기존 코드이다.</p>
<pre><code class="language-tsx">const handleKeyDown = async (e: React.KeyboardEvent&lt;HTMLTextAreaElement&gt;) =&gt; {
  if (e.isPropagationStopped()) return;
  if (e.key === &#39;Enter&#39; &amp;&amp; !e.shiftKey &amp;&amp; text.trim().length &gt; 0) {
    e.preventDefault();
    e.stopPropagation();
    console.log(&#39;enter&#39;);
    console.log(text);
    onSubmit?.(text);
    setText(&#39;&#39;);
  }
};</code></pre>
<p>영문 입력에서는 정상 동작했다.
하지만 한글 입력 중 Enter를 누르면 <code>console.log</code>가 두 번 찍히거나,
마지막 글자가 한 번 더 붙는 현상이 발생했다.</p>
<br />

<h2 id="원인">원인</h2>
<p>한글 입력은 단일 키 입력이 아니다.
‘ㅎ’, ‘ㅏ’, ‘ㄴ’을 순서대로 입력하면 조합 과정을 거쳐 ‘한’이라는 글자가 만들어진다.
이 과정을 <strong>조합 입력(Composition)</strong> 이라고 한다.</p>
<br />

<p>브라우저는 이 조합 과정을 다음 이벤트로 관리한다.</p>
<ul>
<li><code>compositionstart</code></li>
<li><code>compositionupdate</code></li>
<li><code>compositionend</code></li>
</ul>
<br />

<p>문제는 이 조합 과정 중에도 <code>keydown</code> 이벤트가 발생한다는 점이다.
한글 조합이 완료되기 전, 즉 <code>compositionend</code> 이전에 Enter를 누르면
이벤트가 두 번 호출된다.
조합 중 한 번, 조합 완료 후 한 번이다.</p>
<br />

<h2 id="해결">해결</h2>
<p><code>KeyboardEvent</code> 객체에는 조합 중인지 여부를 알 수 있는 속성이 있다.
<code>isComposing</code> 이다.</p>
<p>React에서는 <code>e.nativeEvent.isComposing</code> 으로 접근할 수 있다.
이 값이 <code>true</code>일 때는 아직 조합이 끝나지 않은 상태이다.
따라서 이 시점의 Enter 이벤트는 무시해야 한다.</p>
<br />

<h2 id="수정한-코드">수정한 코드</h2>
<pre><code class="language-tsx">const handleKeyDown = async (e: React.KeyboardEvent&lt;HTMLTextAreaElement&gt;) =&gt; {
  // 조합 중에는 Enter 이벤트 무시
  if (e.nativeEvent.isComposing) return;

  if (e.key === &#39;Enter&#39; &amp;&amp; !e.shiftKey &amp;&amp; text.trim().length &gt; 0) {
    e.preventDefault();
    e.stopPropagation();

    console.log(&#39;enter&#39;);
    console.log(text);

    const currentText = text;
    setText(&#39;&#39;);
    onSubmit?.(currentText);
  }
};</code></pre>
<p><code>if (e.nativeEvent.isComposing) return;</code> 한 줄로 해결했다.
조합 중 Enter 이벤트를 무시했기 때문에 이제 이벤트는 한 번만 호출된다.</p>
<br />

<p>또한 <code>setText(&#39;&#39;)</code> 이후 <code>onSubmit</code>을 호출하면 상태가 비동기로 초기화될 수 있기 때문에 현재 값을 별도의 변수에 담은 후 호출하는 순서로 변경했다.</p>
<br />

<h2 id="정리">정리</h2>
<ul>
<li>한글 입력은 조합 입력 방식이기 때문에 Enter가 두 번 발생할 수 있다.</li>
<li><code>e.nativeEvent.isComposing</code> 값으로 조합 중인지 확인할 수 있다.</li>
<li>조합 중일 때는 Enter 이벤트를 무시하면 된다.</li>
<li>이 방식은 일본어, 중국어 등 IME 기반 입력에서도 동일하게 적용된다.</li>
</ul>
<br />

<h2 id="참고">참고</h2>
<ul>
<li><a href="https://kimyeongseo.tistory.com/60">https://kimyeongseo.tistory.com/60</a></li>
<li><a href="https://inner-stella.tistory.com/entry/%ED%95%9C%EA%B8%80-%EC%9E%85%EB%A0%A5-%EC%8B%9C-Enter-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%A4%91%EB%B3%B5-%ED%98%B8%EC%B6%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0">https://inner-stella.tistory.com/entry/한글-입력-시-Enter-이벤트-중복-호출-문제-해결</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing">MDN KeyboardEvent.isComposing</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅] HTTP 캐싱]]></title>
            <link>https://velog.io/@jeon-yj/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-HTTP-%EC%BA%90%EC%8B%B1</link>
            <guid>https://velog.io/@jeon-yj/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-HTTP-%EC%BA%90%EC%8B%B1</guid>
            <pubDate>Sun, 18 May 2025 09:35:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>바야흐로 목요일, 평소와 같이 배포를 했는데 갑자기 사용자들에게 앱이 흰 화면으로 보인다는 이슈가 인입이 되었다😱
S3 + CloudFront + Next.js 웹뷰 환경에서 캐싱 문제 트러블슈팅한 이야기!! 두두등장</p>
</blockquote>
<p>최근 프론트엔드 배포 시스템을 AWS S3와 CloudFront 조합으로 이전한 뒤, 캐싱 문제로 사용자들에게 흰 화면이 발생하는 심각한 이슈를 겪었다.
웹뷰 환경에서는 작은 캐시 이슈도 치명적으로 작용한다는 것을 이번 경험을 통해 절실히 느꼈다.</p>
<p>이 글에서는 문제를 어떻게 분석하고 해결해갔는지 과정을 정리해보았다.</p>
<h2 id="문제-상황-평소처럼-배포했는데-흰-화면">문제 상황: 평소처럼 배포했는데 흰 화면?</h2>
<p>어느 날, 앱 고객센터로부터 &quot;앱을 열었더니 흰 화면만 보여요&quot;라는 제보가 들어왔다.</p>
<p>우리는 이전에도 배포를 여러 번 해왔고, 별다른 문제가 없었기 때문에 처음엔 뭔가 일시적인 문제겠거니 했다.</p>
<p>하지만 직접 웹에서 열어보니 콘솔에 아래와 같은 에러가 있었다.</p>
<pre><code>_app.js?ts=1747359952985:498 Uncaught SyntaxError: Invalid or unexpected token</code></pre><p>이후 새로고침을 하면 정상적으로 화면이 로딩되었다.</p>
<p>이 증상은 <strong>일부 사용자에게만 발생</strong>했고, <strong>새로고침을 하지 않은 상태에서 내부 팝업 등을 통해 라우팅</strong>할 때 주로 발생했다.</p>
<hr>
<h2 id="원인-분석-캐시된-오래된-js-파일과-최신-html의-충돌">원인 분석: 캐시된 오래된 JS 파일과 최신 HTML의 충돌</h2>
<p>콘솔 에러를 본 뒤, 네트워크 탭을 확인해봤다.</p>
<p>에러가 나는 시점에는 <code>main-7b9a</code>라는 오래된 JS 파일이 로딩되고 있었고, 정상일 때는 <code>main-f1ef</code>이라는 최신 JS 파일이 로딩되었다.</p>
<p>즉,</p>
<ul>
<li>사용자의 브라우저에는 이전 배포 시점의 JS 파일이 <strong>캐시</strong>되어 있었고</li>
<li>HTML은 새로운 버전으로 불러오면서 <strong>버전이 불일치</strong>하게 되었다.</li>
<li>이 때문에 구버전 JS로 최신 HTML을 해석하려다 <strong>에러가 발생</strong>한 것이다.</li>
</ul>
<hr>
<h2 id="환경-변화-왜-갑자기-이런-문제가-생겼을까">환경 변화: 왜 갑자기 이런 문제가 생겼을까?</h2>
<p>기존에는 프론트엔드를 다른 방식으로 배포하고 있었지만, 최근 S3 + CloudFront로 전환한 이후부터 이런 문제가 생기기 시작했다.</p>
<p>이 환경에서는 브라우저 캐싱이 더욱 강력하게 동작하기 때문에, 의도하지 않으면 캐시가 쉽게 무효화되지 않는다.</p>
<hr>
<h2 id="처음-시도한-해결-방법-캐싱-정책-변경">처음 시도한 해결 방법: 캐싱 정책 변경</h2>
<p>우리는 HTTP 캐싱을 문제의 원인으로 보고, 배포 워크플로우의 <code>aws s3 sync</code> 명령에 <strong>캐시 무효화 정책</strong>을 추가했다.</p>
<h3 id="적용한-캐시-정책은-다음과-같다">적용한 캐시 정책은 다음과 같다:</h3>
<pre><code class="language-bash">
# _next/static/* → 캐시 X
aws s3 sync out/_next/static s3://.../_next/static \
  --cache-control &quot;no-cache, no-store, must-revalidate&quot;

# *.html → 캐시 X
aws s3 sync out/ s3://... \
  --include &quot;*.html&quot; \
  --cache-control &quot;no-cache, no-store, must-revalidate&quot;

# 나머지 정적 자산 → immutable 캐시 (1년)
aws s3 sync out/ s3://... \
  --exclude &quot;_next/static/*&quot; \
  --exclude &quot;*.html&quot; \
  --cache-control &quot;max-age=31536000, public, immutable</code></pre>
<p><strong>CloudFront invalidation</strong>도 함께 설정하여, 모든 경로에 대해 캐시를 무효화하도록 했다.</p>
<pre><code class="language-bash">aws cloudfront create-invalidation --paths &quot;/*&quot;</code></pre>
<hr>
<h2 id="그런데-문제는-해결되지-않았다">그런데 문제는 해결되지 않았다</h2>
<p>우리는 완벽하게 캐시 정책을 적용했다고 생각했지만, 여전히 사용자에게 흰 화면이 간헐적으로 나타났다.</p>
<p>원인은 생각보다 단순한 곳에 있었다.</p>
<hr>
<h2 id="원인-재발견--include---exclude-옵션의-맹점">원인 재발견: <code>-include</code> / <code>-exclude</code> 옵션의 맹점</h2>
<p>S3에 파일을 업로드할 때 <code>aws s3 sync</code>는 한 번의 명령에 대해 <strong>하나의 <code>--cache-control</code>만 적용</strong>할 수 있다.</p>
<p>그런데 우리는 아래처럼 <code>--include &quot;*.html&quot;</code> 같은 옵션을 쓰고 있었다.</p>
<pre><code class="language-bash">aws s3 sync out/ s3://... \
  --include &quot;*.html&quot; \
  --cache-control &quot;no-cache, no-store, must-revalidate&quot;</code></pre>
<p>문제는, <code>--include</code> 옵션이 있다고 해서 해당 파일만 <strong>cache-control이 설정되는 게 아니라는 점</strong>이다.</p>
<p>정확히는:</p>
<ul>
<li><code>-include</code>는 <strong>대상 파일을 필터링</strong>하는 역할만 하고</li>
<li>그 외 모든 파일도 함께 업로드되지만, <strong>동일한 cache-control이 적용된다</strong></li>
</ul>
<p>그래서 의도치 않게 <code>.js</code>나 이미지 파일에도 <code>no-cache</code>가 적용되었거나, 반대로 <code>.html</code>에 <code>immutable</code>이 적용되어 있었을 수도 있다.</p>
<p>📌 참고 레퍼런스:</p>
<p><a href="https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html">aws s3 sync 공식 문서</a></p>
<hr>
<h2 id="두-번째-접근-서비스-워커를-직접-구현">두 번째 접근: 서비스 워커를 직접 구현</h2>
<p>캐시 문제를 사용자 단에서 최대한 자동으로 해결하기 위해, 서비스워커를 직접 구현해보기로 했다. 기존에는 서비스워커가 존재하지 않았기 때문에, 이슈가 발생했을 때 사용자가 강제 새로고침을 하지 않으면 캐싱된 오래된 리소스를 계속 불러오는 문제가 있었다.</p>
<h3 id="서비스워커의-역할">서비스워커의 역할</h3>
<p>이번 이슈를 해결하기 위한 서비스워커는 다음과 같은 목적을 갖는다:</p>
<ul>
<li><strong>정적 리소스의 최신화 감지 및 캐시 삭제</strong></li>
<li><strong>오프라인 환경 대응</strong></li>
<li><strong>에러 발생 시 자동으로 캐시 정리 시도</strong></li>
<li><strong>명시적인 캐시 삭제 API 제공</strong></li>
</ul>
<h3 id="주요-구현-내용">주요 구현 내용</h3>
<ol>
<li><p><strong>설치 시 사전 캐싱 (<code>install</code>)</strong></p>
<pre><code class="language-js"> self.addEventListener(&#39;install&#39;, (event) =&gt; {
   event.waitUntil(
     caches.open(CACHE_NAME).then((cache) =&gt; cache.addAll(PRECACHE_ASSETS))
   );
   self.skipWaiting();
 });
</code></pre>
<p> 배포 시 정의된 정적 파일들을 미리 캐싱하여, 앱 로딩 시 일부 파일들이 빠르게 불러올 수 있도록 하고 <code>skipWaiting()</code>을 통해 새 버전이 즉시 활성화될 수 있도록 했다.</p>
</li>
<li><p><strong>이전 버전 캐시 삭제 (<code>activate</code>)</strong></p>
<pre><code class="language-js"> self.addEventListener(&#39;activate&#39;, (event) =&gt; {
   event.waitUntil(
     caches.keys().then((cacheNames) =&gt;
       Promise.all(
         cacheNames.map((cacheName) =&gt; {
           if (cacheName !== CACHE_NAME) return caches.delete(cacheName);
         })
       )
     )
   );
   self.clients.claim();
 });
</code></pre>
<p> 새로운 캐시 이름이 등록되면, 이전 버전의 캐시를 자동으로 제거함으로써 충돌을 방지했다.</p>
</li>
<li><p><strong>네트워크-우선 or 캐시-우선 전략 (<code>fetch</code>)</strong></p>
<ul>
<li><p><code>_next/static</code>, <code>.js</code> 등은 <strong>네트워크 우선</strong></p>
</li>
<li><p>나머지 리소스는 <strong>캐시 우선</strong></p>
<p>이는 최신 JavaScript 코드가 반드시 반영되도록 하기 위해서다.</p>
</li>
</ul>
</li>
<li><p><strong>스크립트 로딩 실패 시 자동 캐시 삭제</strong></p>
<p> HTML 내에 <code>&lt;script&gt;</code>로 다음과 같은 처리를 추가했다:</p>
<pre><code class="language-js"> window.addEventListener(&#39;error&#39;, function(event) {
   if (event.target?.src?.includes(&#39;_next/static&#39;)) {
     window.clearCaches();
   }
 }, true);
</code></pre>
<p> Next.js의 정적 JS 파일 로딩이 실패했을 경우 자동으로 캐시를 삭제하고 새로고침을 유도한다.</p>
</li>
<li><p><strong>수동 캐시 정리용 메시지 이벤트 (<code>message</code>)</strong></p>
<pre><code class="language-js"> self.addEventListener(&#39;message&#39;, (event) =&gt; {
   if (event.data?.type === &#39;CLEAR_CACHES&#39;) {
     caches.keys().then((keys) =&gt; Promise.all(keys.map(caches.delete)));
   }
 });
</code></pre>
<p> 앱 내에서 <code>serviceWorker.controller.postMessage({ type: &#39;CLEAR_CACHES&#39; })</code>로 호출하여 명시적으로 캐시 정리를 요청할 수 있다.</p>
</li>
<li><p><strong>URL 파라미터 or 로컬 스토리지를 이용한 캐시 무효화</strong></p>
<p> 앱이 열릴 때 다음과 같이 처리했다:</p>
<pre><code class="language-js"> const forceRefresh = urlParams.get(&#39;force_refresh&#39;) || localStorage.getItem(&#39;refreshCache&#39;);
</code></pre>
<p> URL에 <code>?force_refresh=true</code>가 붙어있거나, 로컬 스토리지에 특정 플래그가 있을 경우 자동으로 캐시와 쿠키를 삭제하고 새로고침을 수행한다.</p>
</li>
</ol>
<h3 id="한계">한계</h3>
<ul>
<li>Service Worker는 최신 브라우저에서만 동작한다.</li>
<li>웹뷰 환경마다 서비스워커 지원 범위가 다르기 때문에 일부 디바이스에서는 완벽하게 동작하지 않는다.</li>
<li>사용자가 앱을 완전히 종료하지 않는 이상, 캐시 정리 후 적용이 지연될 수 있다.</li>
</ul>
<p>그래서 팀원과 논의한 결과, 서비스워커만으로는 완전한 해결이 어렵다고 판단하여 앱 단에서도 웹뷰 초기 로딩 시 캐시 삭제 처리를 추가로 구현했다.</p>
<hr>
<h2 id="병행-접근-앱-웹뷰-단에서-캐시-삭제-처리">병행 접근: 앱 웹뷰 단에서 캐시 삭제 처리</h2>
<p>팀원은 앱단에서 웹뷰를 초기화할 때 캐시를 삭제하는 방식으로 대응했다.</p>
<p>이는 대부분의 이슈를 해결할 수 있었고, 서비스 워커 방식과 병행하면서 흰 화면 문제는 거의 사라졌다. 하지만 캐싱에 대한 이점을 상쇄하는 해결방법이기 때문에 지양해야하는 해결법이다.</p>
<hr>
<h2 id="결론-캐시는-의도하지-않으면-무서운-존재다">결론: 캐시는 의도하지 않으면 무서운 존재다</h2>
<p>이번 경험을 통해,</p>
<ul>
<li><strong>S3 + CloudFront의 캐시 정책은 매우 정확하게 컨트롤해야 하며</strong></li>
<li><strong>Next.js의 아키텍처에 맞춰 HTML과 JS의 버전 동기화를 신경 써야 하고</strong></li>
<li><strong>웹뷰 환경에서는 브라우저 캐시 문제가 더 치명적</strong>이라는 걸 배웠다.</li>
</ul>
<p>서비스 워커나 앱단 캐시 삭제는 보완책일 뿐, 근본적으로는 <strong>HTTP 캐시 설정과 빌드 방식</strong>을 꼼꼼히 점검하는 게 정답이었다.</p>
<hr>
<h2 id="향후-고려할-수-있는-개선-방안">향후 고려할 수 있는 개선 방안</h2>
<ul>
<li>CloudFront의 캐시 정책을 Lambda@Edge나 Response Header Policy로 명확히 설정</li>
<li>Next.js에서 <code>appDir</code> 또는 <code>serverActions</code>를 사용해 더 동적인 빌드 구조 도입</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 경험을 통해 배포 환경과 캐시 정책의 디테일이 얼마나 중요한지 깨달았다.</p>
<p>단순히 &quot;잘 되겠지&quot;라는 안일한 배포보다는, 작은 설정 하나에도 <strong>정확한 이해와 실험</strong>이 필요하다는 걸 다시금 확인했다.</p>
<p>누군가 비슷한 문제를 겪는다면 이 글이 도움이 되길 바란다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SSAFY] 자율프로젝트 별이삼샵 회고록]]></title>
            <link>https://velog.io/@jeon-yj/SSAFY-%EC%9E%90%EC%9C%A8%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B3%84%EC%9D%B4%EC%82%BC%EC%83%B5-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@jeon-yj/SSAFY-%EC%9E%90%EC%9C%A8%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B3%84%EC%9D%B4%EC%82%BC%EC%83%B5-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Fri, 06 Dec 2024 14:41:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>쪽지를 장소에 숨기고 직접 찾아서 읽는 Mobile App 메신저 서버스</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/1dc2c115-4a5d-457a-b112-2ccf2bf790d9/image.png" alt=""></p>
<h3 id="들어가기-전에">들어가기 전에,,</h3>
<p>9-6를 제일 잘 지킨 프로젝트이면서도 초반 팀장의 싸탈로 인해 팀장직을 넘겨받으면서 제일 신경을 많이 쓴 프로젝트기도 하다.
성격 상 의견을 잘 내고 주도하는 스타일이어서 팀장이던 아니던 구성원으로서 비슷한 일을 할 것이라고 생각했는데 아니었다..!🙃
팀원들의 진행 사항 체크부터 산출물 제출 등 신경써야할 부분이 많았지만, 계속 회피했던 발표도 해보고 정말 의미있는 경험이었다.
프로젝트가 끝난 시점에서 팀원들에게 좋은 팀장이었는지 궁금하다!</p>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<p>앱 프로젝트 / 6인 프로젝트 / 024.10.14 ~ 2024.11.19</p>
<p><strong>위치 기반 쪽지로</strong> 사용자들이 자신이 선택한 장소에 직접 가서 사진을 찍고, 사진의 픽셀화된 이미지를 힌트로 삼아 쪽지를 숨깁니다. </p>
<p>쪽지를 받은 사용자는 힌트를 단서 삼아 장소를 찾아가 쪽지를 열람할 수 있습니다.</p>
<p>1대1 소통뿐만 아니라 단체 쪽지, 불특정 다수 등 다양한 소통 방식을 제공합니다.</p>
<h2 id="기획-과정">기획 과정</h2>
<p>기업 연계 프로젝트가 하고 싶어서 공통 때 같이 한 팀원 몇명과 팀을 꾸려서 지원했지만 떨어져서 어쩔 수 없이 기획을 다시 해야했다.
앞선 두번의 프로젝트(공통, 특화)에서 경험한 바로 기획이 제일 힘들었기에 이번 프로젝트는 주제를 빨리 정하고 싶었다(실제로 기간도 짧았기도 하고)</p>
<p>일반 메세지 기능은 기존의 메시징 서비스에 비해 경쟁력이 없다고 판단하여  조금 더 직접적으로, 물리적인 방식으로 아날로그 감성을 더할 수 있는 ✨위치 기반 쪽지 서비스를 기획했다.</p>
<p>전반적으로 싸피를 통해 새로운 기술을 배우고 사용해보고 싶었다.
이번에는 익스텐션이나 vanilla javascript만으로 프로젝트를 만들어보고 싶기도 했지만 백엔드 볼륨이 작아 아쉽게도 다음을 기약했다.</p>
<p>메신저 서비스가 전체적인 볼륨이 작은 대신 1차 배포를 빨리해서 사용자 피드백을 받고 사용자 분석을 해보기로 정하고 계속 해보고 싶었던 네이티브 앱 개발을 Flutter로 해보기로 했다!! 예<del>~</del>😁</p>
<h2 id="도전-과제-및-해결책">도전 과제 및 해결책</h2>
<h3 id="positionedfill의-위치">Positioned.fill의 위치</h3>
<p>다트와 플러터를 이틀만에 공부한 바람에 위젯의 올바른 사용에 대해 완벽하게 알지 못했다.</p>
<p>디버깅 모드로 개발을 했을 때는 화면상에 문제가 없어, 배포하려고 빌드하고 apk를 실행해본 순간...!!
하얀색 오버레이가 위에 씌어진 듯한 화면에 터치도 먹지 않았다ㅜㅜㅜ</p>
<p>내가 만든 layout.dart가 문제라고 생각했고 잘못된 부분은 아래와 같다.</p>
<pre><code class="language-dart">return Scaffold(
      backgroundColor: themeProvider.backgroundColor,
      body: Stack(
        children: [
          Positioned.fill(
            child: child,
          ),
          IgnorePointer(
            ignoring: true,
            child: Positioned.fill( // 여기가 잘못됨!
            ),
          ),</code></pre>
<p>positioned.fill는 <strong>Stack 바로 밑</strong>에 있어야하는데 그렇지 않을 경우 디버깅 모드에선 괜찮은데 릴리즈모드에선 화면이 깨질 수 있다!
<img src="https://velog.velcdn.com/images/jeon-yj/post/6eed6a83-9f81-457c-bffb-c8514cbf5b27/image.png" width="40%"></p>
<p><a href="https://github.com/flutter/flutter/issues/151891">관련 이슈</a></p>
<p>Positioned.fill과 비슷하게 Expanded 위젯도 위치를 잘못 배치하면  디버그 모드에서는 오류 메시지가 나타나지만, 릴리스 모드에서는 UI가 응답하지 않거나 깨지는 현상이 발생할 수 있다.</p>
<h3 id="formdata-null">FormData null</h3>
<p>메세지 전송 api를 테스트하는데 계속 10초 타임아웃 에러가 났다.
처음에는 이미지가 커서 전송하는데 시간이 걸리나 싶어 압축을 하고 보내봐도 nginx는 여전히 499 에러,,</p>
<p>이미지가 문제가 아니라고 생각하여, 잠시 nginx가 아니라 백서버로 요청을 보내 정확한 에러 로그를 확인해보니...422에러!!!
계속 Int가 String으로 넘어간다는 거시었다</p>
<p>이미지가 있어  &#39;multipart/form-data&#39;로 넘겨주기로 했는데 아무리 출력을 해봐도 정확히 출력이 되는데 뭐가 문젠가 울고 싶었다 /(ㄒoㄒ)/~~</p>
<p>문제는 바로..바로.. null 값이 &quot;&quot;로 넘어가기 때문이다!!
그래서 출력을 해도 알 바가 없었고, 백엔드 팀원이 더 정확한 에러 메세지를 알려줘서 해결할 수 있었다. (하얗게 불태웠다..)</p>
<p>피티햄 왈:</p>
<p><em>Dio는 내부적으로 null 값을 처리할 때, 필드를 무조건적으로 빈 문자열로 변환하지는 않습니다.
하지만 FormData의 일부 필드가 null로 설정된 경우, 이를 직렬화(serialization)하는 과정에서 빈 문자열로 변환될 수 있습니다.
이는 HTML 폼 제출의 기본 동작을 모방한 것으로, null 값을 빈 문자열로 처리하는 것을 기본값으로 삼기 때문입니다.</em></p>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/407acb59-f565-4e98-8c29-11690a69b3b0/image.png" alt=""></p>
<p>그래서 값이 비어있는 경우 formData에서 빼는 식으로 코드를 바꿔주었다.</p>
<pre><code class="language-dart">//이전 코드
final rawData = data.toJson();
final processedData = rawData.map((key, value) {
  if (value is String &amp;&amp; value.isEmpty) {
    return MapEntry(key, null); 
  }
  return MapEntry(key, value);
});
final formData = FormData.fromMap(processedData);


//수정된 코드
 final Map&lt;String, dynamic&gt; rawData = data.toJson();
      final Map&lt;String, dynamic&gt; processedData = {};
      rawData.forEach((key, value) {
        if (value != null &amp;&amp;
            !((value is String &amp;&amp; value.isEmpty) ||
                (value is List &amp;&amp; value.isEmpty) ||
                (value is File))) {
          processedData[key] = value;
        }
      });
</code></pre>
<p>6시에 시작한 작업이
<img src="https://velog.velcdn.com/images/jeon-yj/post/4e4b2463-1de5-4208-a9b4-7e145f3fd965/image.png" alt=""></p>
<p>11시 반이 되어야 끝나는 매직 🤮🤩</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/jeon-yj/post/e63b02ea-8914-466c-b3a4-8e3704ef4982/image.png" align="center" width="30%">  <img src="https://velog.velcdn.com/images/jeon-yj/post/5d2afcd9-d3ea-4210-9621-f95c8477b8a6/image.png" align="center" width="30%">  <figcaption align="center">착한 우리 팀원들 😭</figcaption>
</p>

<p>사이사이 백엔드에서 respnse type이 잘못 주기도 했고, fastApi에 보내야하는 request body 형식과 ec2서버에 보내야하는 body 형식이 다른 문제도 있어서 에러를 하나 해결하면 새로운 에러가 나오고 마치 에러 순회 공연을 하는 것 같았다ㅋㅋㅋ</p>
<h3 id="datetime">DateTime</h3>
<p>여러 서버를 쓰면서 서버의 timeZone이 다른 문제가 있었다.
알 길이 없는 프론트는 똑같이 보냈는데, 알림함에서는 시간이 맞고 메세지함에서는 9시간이 더해진 두개의 시간대를 살게되는 신기한 모험을 하게됐다.</p>
<p>백엔드에게 물어보니, createdTime을 각각 처리하는데 서버 간의 timeZone이 달라서 같은 Date객체를 파싱하는데 문제가 있었다.</p>
<p>덕분에 TimeZone에 대해 깊게 알아볼 수 있었다.</p>
<p>아래는 우리 팀원의 한마디이다. 
<img src="https://velog.velcdn.com/images/jeon-yj/post/e98d46da-d822-454d-82ad-6d1ebe0acc74/image.png" alt=""></p>
<p><a href="https://yozm.wishket.com/magazine/detail/1695/">요즘IT Timezone 설명 글</a>
이 글만 읽어도 Timezone에 대한 개념은 잘 잡힐거 같다.</p>
<h3 id="다양한-exception-처리">다양한 Exception 처리</h3>
<p>앱이다 보니 웹프로젝트보다 더 꼼꼼하게 에러핸들링을 해줬다.</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/jeon-yj/post/3820f535-03b6-4b20-8bd2-9c3520c762a6/image.png" align="center" width="40%">  <img src="https://velog.velcdn.com/images/jeon-yj/post/7a994513-bf1e-4ce1-94d4-218a03348146/image.png" align="center" width="40%">  <figcaption align="center">🚨엄청난 에러코드..</figcaption>
</p>
서버에서 주는 에러 뿐만 아니라 사용자 네트워크 연결 여부에 따른 화면, 타임 아웃 에러 등 다양한 예외를 처리해주었다.
<img src="https://velog.velcdn.com/images/jeon-yj/post/29119ebe-28b6-42a5-ad67-ccc15893bedd/image.png" align="center" width="40%"> 


<p>ResponseCode 클래스를 만들어서 api response를 포괄적으로 처리하고, ErrorHandler 클래스를 만들어 에러를 캡슐화하고 사용자에게 에러에 따른 일관된 메세지를 제공하도록 설계했다.</p>
<h2 id="향후-계획">향후 계획</h2>
<p>다른 팀원이 구현한 Map을 대거 리팩토링하고 싶다..
중간에 한번 시도했는데 마감이 얼마남지 않아 포기했었기 때문에 더 미련이 남는다.</p>
<h2 id="마무리-및-후기">마무리 및 후기</h2>
<h3 id="배운-점">배운 점</h3>
<ul>
<li>FCM</li>
<li>Google Analytics
처음으로 GA4를 사용해보면서 사용자 분석을 할 수 있었다.
분석이랄 것도 없지만, 확실히 로그인 이전에 메인화면에서 이탈이 많은 것을 확인할 수 있었다.  </li>
</ul>
<p>다음에 또 싸피를 한다면(?) 로그인이 필요없는 서비스, 앱이 아니라 웹 서비스(+ pwa)로 만들어서 접근성을 높여 더 많은 사용자 데이터를 받아보고 싶다.</p>
<h3 id="아쉬운-점">아쉬운 점</h3>
<p>실제 스토어에 올려보지 못한건 아쉽다!
올려도 프로젝트가 끝나면 서버가 내려가기 때문에 안되겠지만,, </p>
<p>또한 프론트엔드와 백엔드에서의 데이터 가공에 대해 팀원들과 좀더 대화를 나눠보고 싶었다. 이번에 백에서 과도하게 데이터를 가공해서 주지 않았나 싶기 때문에, 어떤 데이터는 포맷해주고 어떤 데이터는 프론트가 하는게 좋을지 얘기해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SSAFY] 특화프로젝트 Vincent Run Gogh 회고록]]></title>
            <link>https://velog.io/@jeon-yj/SSAFY-%ED%8A%B9%ED%99%94%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Vincent-Run-Gogh-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@jeon-yj/SSAFY-%ED%8A%B9%ED%99%94%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Vincent-Run-Gogh-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Mon, 25 Nov 2024 08:46:28 GMT</pubDate>
            <description><![CDATA[<img src="https://velog.velcdn.com/images/jeon-yj/post/ee1dd08c-a366-49d0-8cb6-6b49dbe3abec/image.jpg" width="900">

<blockquote>
<p>러닝 경로를 추천해주고, GPS를 활용해 러닝 경로로 그림을 그리는 Mobile App</p>
</blockquote>
<h3 id="들어가기-전에">들어가기 전에,,</h3>
<p>어쩌다 공통 프로젝트 때 알게된 친구가 팀 제안을 줘서 들어가게 됐다.
나와 다른 프엔 팀원 빼고 이미 공통프로젝트를 같이 했던 팀원들이라 처음에는 소외감을 느낀다거나 어색하지 않을까 싶었지만 모두들 반갑게 맞아주고 정말 정말 성격이 잘 맞아서 😁 하루만에 친해지게되었다ㅋㅋ <del>(애초에 그런거 잘 안 느끼면서 걱정만 함 ㅋ)</del></p>
<p>편한 분위기에서 프로젝트 주제가 정말 많이 나왔지만 모두의 맘에 딱 드는 주제가 없어 반려하고 하나씩 제외하다 보니 결국 마지막에 남은 주제를 선택하게되었다ㅜ 이 부분에는 아쉬움이 있다.
그럼에도 불구하고 모두 최선을 다했고 할 수 있는 범위 내에서 최대한 고도화시킨거 같다.</p>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<p>앱 프로젝트 / 6인 프로젝트 / 2024.08.26~24.10.11</p>
<p>사용자가 지도에 직접 그린 그림을 기반으로 도로 경로를 설정하고 러닝을 통해 맵에 드로잉을 할 수 있는 서비스.
커뮤니티를 통해 다른 사용자가 만든 경로나 뛴 경로를 자신의 경로로 추가할 수 있습니다.</p>
<h2 id="기획-과정">기획 과정</h2>
<p>전 프로젝트와 다르게 회의록을 혼자 작성하지도 않고 시키지 않아도 다들 자기 할 일을 알아서 하니까 너무 편하고 좋았다! 이거시 팀프로젝트?!
빅데이터 분산이 주제인 만큼 데이터 확보에 어려움이 있을 것이라 생각했고, 실제로도 그러했다.
그래서 데이터부터 찾고 주제를 정하자는 의견과 주제를 정하고 데이터를 찾자는 의견으로 나누어졌다. 이런 고민이 무색하게도 데이터를 찾으면 주제가 별로였고, 주제를 정하면 데이터가 마땅치 않았다.
무수한 아이디어 회의들,, 
<img src="https://velog.velcdn.com/images/jeon-yj/post/d515723b-530a-466a-9b56-c1ebd0842314/image.png" alt="">
여기서 내가 하고 싶었던 주제는 스마트팩토리 MES었다. 그래서 적극적으로 데이터도 찾고 구상도 하면서 디지털 트윈까지 해보면 어떨까 찾아보았지만 다들 관심 분야도 아니었을 뿐더러, 코치님께서 기업 연계 프로젝트를 스마트팩토리와 관련된 주제로 진행했었는데 고충이 꽤나 많으셨는지 재고를 권하셔서 이 친구도 버려지게 되었다,,😥</p>
<p>지금와서 생각해보면 조금 더 어필해볼걸 후회가 든다. 자율 프로젝트에 파이프라인 디지털 트윈을 주제로 프로젝트를 완성도 있게 한 팀을 보고 1. 나도 해보고 싶다 2. 아 빅데이터가 아니었다면 모니터링하는 부분을 축소해서 어느정도는 구현할 수 있었을텐데 하는 아쉬움이 들었다.</p>
<p>그래서 회고를 하면서도 다시금 느끼는 것은 나한테 ** 프로젝트 주제는 동기부여와 성취감에 정말 중요하다!** 
회사에 가면 원하는 프로젝트를 선택할 순 없을테니 원하는 산업의 회사로 취업하자!
최근 서류가 잘 안되서 은행권이나 아무 산업군의 IT라도 가자 라는 생각이었는데, 과연 가서 만족할 수 있을까? 라는 고민이 들게 된다,,</p>
<h2 id="도전-과제-및-해결책">도전 과제 및 해결책</h2>
<h3 id="svelte-도전기">Svelte 도전기</h3>
<p>전부터 관심 있던 스벨트!
프엔 팀원을 꼬셔 스벨트를 써보기로 했다. 너무 고맙게도 흔쾌히 오케이하고 오히려 먼저 공부해와서 팀원에 대한 신뢰와 공부에 대한 자극을 받을 수 있었다.</p>
<p><strong>🤔왜 스벨트를 쓰고 싶었는가?</strong></p>
<ol>
<li>리액트를 쓰면서 useState와 다른 컴포넌트 구조 코드(boilerplate) 등 들어가는 반복되는 코드도 너무 싫었고</li>
<li>스벨트는 컴파일러로 virtual DOM을 쓰지 않고 실제 DOM에 반영하여 reactive하게 만든다는 것이 기존에 사용해왔던 react와 vue와 비교했을 때 매력적으로 느껴졌다.</li>
</ol>
<p>하지만(1),,
리액트를 사용했을 땐 구선생에게 물어보면 한글로 번역된 공식 문서부터 티스토리, 스택오버플로우 등 다양한 자료들이 있어서 비교적 쉽게 공부할 수 있었고 다양한 라이브러리가 있어 이거 없어? 이런 일이 별로 없었다..</p>
<p>그러나 스벨트는 간간히 정리한 블로그는 있었지만 공식 문서의 내용을 재정리한 느낌이지 기능적인 구현을 한 글은 리액트에 비해 많지 않았다.</p>
<p>그렇기 때문에 이번 프로젝트에서는 공식문서를 많이 참고하고 날것? 의 코드를 많이 참고한거 같다. 또한 이전까지는 에러를 해결하지 못할 때만 잠깐 쓰던 gpt햄을 결제할 수 밖에 없었다. 그래도 낮은 러닝 커브의 프레임워크였기 때문에 빠르게 익숙해질 수 있었다!</p>
<p>하지만(2),,
프로젝트를 위해 빠르게 익혔기 때문에 생명주기라던가 반응성, store에 허점이 있었고 화면 랜더링 관리에 어려움이 있었다. 그리고 리액트만 하다가 해서 그런지 직접 DOM에 접근해 이벤트를 설정한다던가 하는 방식이 어색했다.</p>
<p>또한 외부 라이브러리(Chart.js)와 함께 반응성 변수를 사용하는 경우 컴포넌트 리렌더링이 원하는 시점에 이뤄지지 않는 문제도 있었다.</p>
<p><del>이렇게 적고 나니 어려움이 많았네 ㅎ</del></p>
<h3 id="javascript를---typescript">JavaScript를 -&gt; TypeScript</h3>
<p>프로젝트 초기에는 JavaScript를 사용했지만 일주일 정도 진행을 한 상태에서 TypeScript로 변경하자고 팀원에게 의견을 물었다.
변경하고 싶은 이유와 변경 소요 시간, 관련 자료들로 설득을 하니 주말동안 고민을 해보겠다고 하더니 이번에도 팀원은 타입스크립트에 대해 잔뜩 찾아보고 자신의 코드를 어느정도 변경한 채로 다음 회의에 왔다... 진짜 머싯는 사람,,😮</p>
<p><strong>TypeScript를 도입한 이유로는</strong></p>
<ol>
<li><p><strong>정적 타입 체크</strong> 
백에서 받고 보내는 데이터의 타입을 확실히 하고, 위치 데이터를 가공하는데 일관성을 주고 싶었다.</p>
</li>
<li><p><strong>자체적인 문서화, 유지보수성 향상</strong> 
팀 프로젝트에서 가장 어려운 점은 다른 팀원의 코드를 이해하고 사용하는 것이라 생각한다. js를 썼을 때는 내가 사용하고 싶은 코드를 이해하려면 전반적으로 다 이해해서 이 변수에 어떤 값이 들어가는지 다 확인해야했지만, ts에서는 타입 정의 코드만 보면 되니 코드 가독성이 높아질 수 밖에 없다. 실제로도 사용해보고 느낀 점이다!</p>
</li>
<li><p>그냥 사용해보고 싶었음!
제일 큰데,, ㅋㅋ 채용 공고나 job script에 ts는 자주 나오니 사용해보고 싶었다ㅎ</p>
</li>
</ol>
<p>tsconfig.json 파일 등을 만들고 설정을 바꾸면서 js 프로젝트를 ts로 변경하는 작업을 진행했는데
타입을 지정하면  <text style="color:red">typescript preprocessor</text> 에러가 발생해서 결국 프로젝트를 ts로 다시 생성했다ㅜ</p>
<h3 id="gps-보정">gps 보정</h3>
<p>다른 러닝 앱처럼 일정 시간 동안 위치를 받아오며 연결해서 선을 그려줘야했는데 PWA여서 그런지 gps가 가끔<del>(자주)</del> 튀었다.
기기마다 또 내/외부 마다 gps의 정확도가 천차만별이라 라이브 시연이 걱정이었고, 실제로 시연때도 gps가 말썽이었다.</p>
<p>이부분은 프로젝트 발표 전날 새벽까지도 계속 리팩토링하며 최적화를 하려고 노력했지만 PWA의 한계라 생각하여 많이 아쉬웠다.
다시는 gps 프로젝트를 안하리 다짐을 하며,,,😥</p>
<p>최적화는 아래와 같은 로직으로 진행했다.</p>
<ol>
<li>이전 위치와 현재 받아온 위치의 거리를 계산하고, 속도 추정</li>
<li>이전까지의 속도와 설정한 최대 속도와 비교 </li>
<li>계산된 속도가 최대 속도를 초과하면, 위치 값 무시</li>
</ol>
<p>이런 간단한 로직을 적용했더니 여러 예외 사항이 발생했다ㅋㅋ
대표적으로는 잘못된 위치를 지속적으로 받아올 경우, 이동이 감지 되지 않았다!!! 망해따 싶었다ㅎㅎ</p>
<p>그래서 2차 최적화에서는 <strong>초과 속도 감지 시 최대 이동 가능 거리를 고려하여 좌표를 조정</strong>했다.</p>
<p>위의 로직에서 &#39;무시&#39;가 아니라 이동하고 있는 방향을 계산하여 그 방향의 좌표값으로 값을 바꾸었다.</p>
<ol>
<li>튄 방향으로의 방향 벡터 계산</li>
<li>이전 속도 값으로 Math.sin(radians)와 Math.cos(radians)을 사용하여 위도와 경도를 조정</li>
<li>조정된 값 반환</li>
</ol>
<h3 id="비동기-작업">비동기 작업</h3>
<p>이 부분도 마지막까지 최적화를 한 부분인데 실제 코드에 반영되진 않았다. 조금 더 리팩토링이 필요해보인다.</p>
<p>서비스에서 Leaflet 맵을 html2canvas를 사용해 지도 캡처하여 이미지를 뽑아내는 과정이 있는데
여기서 맵이 다 로딩 되지 않았을 때 사용자가 캡쳐 버튼을 누르면 맵이 누락되는 버그🐛가 있었다. </p>
<p>그래서 Leaflet 맵의 로드 완료 여부를 체크하고, 완전히 로드되기 전에 캡처를 시도할 경우 사용자에게 알림을 주고 재시도를 유도하는 로직을 구현했다.</p>
<ol>
<li><p>로드 상태 확인 및 대기
tileLayer.on(&#39;load&#39;)과  map.on(&#39;movestart&#39;)을 통해 맵의 로딩 상태를 지속적으로 체크하여 화면 캡처를 실행하기 전에 isMapLoaded 플래그를 확인하며 맵이 로드될 때까지 비동기 대기(Promise) 로직을 사용했다.</p>
</li>
<li><p>타임아웃 로직 추가
만약 로드 상태 확인이 일정 시간(20초) 내에 완료되지 않을 경우, 타임아웃 에러를 발생시키고 사용자에게 재시도를 요청하는 피드백을 제공했다. 실제 사용해보니 어떤 기기에서는 바로 되고 여러번 반복해도 캡쳐가 안되는 기기가 있었다. <del>(대체 왜..?)</del></p>
</li>
<li><p>로딩 중 사용자 경험 개선
캡처 작업이 진행되는 동안 사용자에게 로딩 알림(loadingAlert)을 표시하여 작업 진행 상황을 명확히 전달하고, 오류 발생 시 명확한 메시지를 통해 재시도를 유도하였다.</p>
</li>
</ol>
<p>이 작업을 통해 비동기 프로세스와 관련된 문제를 사전에 예측하고 효과적으로 처리하는 방법을 설계할 수 있었다.</p>
<h2 id="향후-계획">향후 계획</h2>
<p>웨어러블 기기용 앱을 만들어서 연동시키는 건 어떤가 싶다.</p>
<h2 id="마무리-및-후기">마무리 및 후기</h2>
<h3 id="배운-점">배운 점</h3>
<p><strong>화면 설계서 작성</strong>
프로젝트 설계 때 화면 설계서를 자세히 작성하여 프로젝트 구조와 사용자 인터페이스를 명확히 정의했다. 확실히 설계를 꼼꼼히 할 수록 개발이 편하다
<img src="https://velog.velcdn.com/images/jeon-yj/post/9c1bdc1e-0ab0-4292-8906-7e93ab2c3599/image.png" alt=""></p>
<h3 id="아쉬운-점">아쉬운 점</h3>
<p>공통 때와 마찬가지로 네이티브 앱으로 만들었으면 더 높은 완성도가 나왔을 것 같다.
하지만 위치가 잘 받아와졌다고 가정하면 PWA로 배포한 선택은 사용자 접근성도 좋아서 싸피 프로젝트로는 맞는 선택이었다고 생각한다.</p>
<p>또한 중간에 ts로 변경하여 타입 정의와 정리를 못해 ts를 100% 사용했다는 느낌이 안들었다. 중간중간 any도 많이 쓴거 같다 ㅎ
다음에 할때는 설계 때 타입 명세서를 작성하여 좀 더 typescript틱 한 코드를 짜고 싶다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준]1학년 JS]]></title>
            <link>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%801%ED%95%99%EB%85%84</link>
            <guid>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%801%ED%95%99%EB%85%84</guid>
            <pubDate>Wed, 16 Oct 2024 05:49:30 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/5557">문제 링크
</a>
정말 오랜만에 풀이하는 알고리즘,, 코테 준비를 위해 중간 중간 풀었는데 한동안 뜸했더니 실력이 처참해졌다..😱
왜 올리는데는 한 세월이고 떨어지는데는 한 순간인가.. </p>
<p>사족은 넘기고 문제로 가면 평이한 Dp 문제이다.
아직까지는 나 Dp에요~ 하는 문제는 금방 점화식 세우고 풀 수 있는거 같다.</p>
<h3 id="문제-풀이-빌드업">문제 풀이 빌드업</h3>
<ol>
<li><p>dp배열의 index와 value를 뭐로 할까
등수의 수가 계속 update되고 구해야하는 해이므로 value로 할까.
처음에는 +와 -로 두개를 나눠야하나 싶었는데 이전 분기에서 +인지 -인지는 중요하지않고 수식을 통해 나온 수가 중요했기 때문에 index 또한 몇번째 수를 포함한 수식인지로 정했다.</p>
</li>
<li><p>top-down 일까 bottom-up일까
잘 설명은 안되지만 dp[0]이랄까 초기값이 있기도 하고 bottom-up으로 해야할 것 같은 느낌이 든다.</p>
</li>
<li><p>그래서 구해야하는 값은 무엇인가
dp[등호 이전까지의 idx][원하는 값 == 등호 이후의 수 == 마지막 수] = 가능한 수식
으로 글로 나타낼 수 있겠다.</p>
</li>
</ol>
<h3 id="시행착오">시행착오</h3>
<ol>
<li>문제에서 <em><strong>올바른 등식의 개수는 263-1 이하이다.</strong></em> 라고 나와있다ㅎㅎ
보긴했었는데 javascipt 니까 괜찮겠지 라는 안일한 생각과 number형의 크기가 int보단 크다고 대충 알고 있어 정답까지 시간이 더 걸리게됐다.</li>
</ol>
<p>이 기회에 공부한 자스 <strong>Number</strong>~
_Number 는 원시 래퍼 객체입니다.
Number 생성자는 숫자를 다루기 위해 상수와 메소드를 가지고 있습니다. 다른 타입의 값은 Number() 함수를 사용하여 숫자로 바꿀 수 있습니다.
일반적인 숫자는 &#39;배정밀도 부동소수점 숫자(double precision floating point number)&#39;로 알려진 64비트 형식의 IEEE-754에 저장됩니다.</p>
<p>임의의 길이를 가진 정수는 BigInt 숫자로 나타낼 수 있습니다. 일반적인 숫자는 253이상이거나 -253이하일 수 없다는 제약 때문에 BigInt라는 새로운 자료형이 만들어졌습니다._</p>
<p><strong>BigInt</strong>
<em>BigInt는 정수 리터럴의 뒤에 n을 붙이거나(10n) 함수 BigInt()를 호출해 생성할 수 있습니다.
BigInt와 Number는 어떤 면에서 비슷하지만 중요한 차이점이 있습니다. 예컨대 BigInt는 내장 Math 객체의 메서드와 함께 사용할 수 없고, 연산에서 Number와 혼합해 사용할 수 없습니다. 따라서 먼저 같은 자료형으로 변환해야 합니다. 그러나, BigInt가 Number로 바뀌면 정확성을 잃을 수 있으니 주의해야 합니다.</em></p>
<p>라고 한다.</p>
<ol start="2">
<li><p>두번째 실수는 BigInt는 그냥 콘솔 찍으면 뒤에 n이 붙는다는 것이다.
따라서 정답을 print 할때 값을 toString해야한다.</p>
<h3 id="정답-풀이">정답 풀이</h3>
<pre><code class="language-javascript">const fs = require(&quot;fs&quot;);
let [n, ...arr] =fs.readFileSync(&quot;/dev/stdin&quot;)
.toString()
.trim()
.split(/\s/)
.map(Number)
let MAX_VALUE =20
let dp=Array.from({length:n-1},e=&gt;Array.from({length:MAX_VALUE+1},e=&gt;BigInt(0)));
dp[0][arr[0]]=BigInt(1)
for(let i=1;i&lt;n-1;i++){
for(let j=0;j&lt;=20;j++){
 if(dp[i-1][j]===0)continue;

 if(j+arr[i]&lt;=20){//+
   dp[i][j+arr[i]]+=dp[i-1][j]
 }
 if(j-arr[i]&gt;=0){ // -
   dp[i][j-arr[i]]+=dp[i-1][j]
 }
}
}
console.log(dp[n-2][arr[n-1]].toString())
</code></pre>
</li>
</ol>
<pre><code>

![](https://velog.velcdn.com/images/jeon-yj/post/75a45db0-3b11-43aa-8a37-e335c335c1d3/image.png)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[SSAFY] 공통프로젝트 SaveME & RescU 회고록]]></title>
            <link>https://velog.io/@jeon-yj/SSAFY-%EA%B3%B5%ED%86%B5-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-SaveME-RescU-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@jeon-yj/SSAFY-%EA%B3%B5%ED%86%B5-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-SaveME-RescU-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Tue, 15 Oct 2024 14:35:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>화상 응급 신고와 태깅을 통한 타인 신고 서비스를 제공하는 Web/ Mobile App</p>
</blockquote>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<ul>
<li>웹 &amp; 앱 프로젝트 / 5인 프로젝트 / 2024.07.08~24.08.15 </li>
</ul>
<p>WebRTC와 WebSocket을 사용하여 신고자와 소방 상황실 간의 실시간 화상통화, 채팅 기능을 제공하고
앱을 통해 사용자의 의료 정보를 사전에 등록해 1초라도 빠르게 신고 절차가 이뤄질 수 있도록 하는 서비스.</p>
<p>nfc 태깅을 통해 타 사용자를 대신해서 신고하고, 소방 상황실에서는 태깅 id를 통해 의료 정보를 제공받을 수 있습니다.</p>
<h2 id="기획-과정">기획 과정</h2>
<p>1학기 때 친했던 백엔드 친구와 프론트엔드 친구 세명이서 팀을 꾸리고 비전공자 세분을 모셔왔다. 팀원 중 119 신고에 대해 관심이 있는 분이 계셨고 다들 사회 공헌적인 주제를 희망해서 주제 자체는 첫날에 나왔지만</p>
<ol>
<li>관공서인 119 상황실에서 사용해야하고</li>
<li>이미 119 전화 신고 만연하기 때문에</li>
</ol>
<p>어떤 점에서 우리 서비스가 차별점이 있을까 고민을 많이 하여 E-gen을 통한 응급실 현황 연결, nfc 태깅을 통한 타인 대리 신고 기능을 생각할 수 있었다.
어찌저찌 mvp를 정하니 프엔 전공 친구가 싸탈을 해버려 프로젝트 볼륨이 애매하게 되어 디자인적인 부분은 간소화하고 mvp에 집중하는 것으로 기획을 끝낼 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/9a77131b-a5a7-41b0-bae3-5874a45f2b08/image.png" alt=""></p>
<p>기획 회의만 이주 꽉 채워하면서 사용자리서치, 소방 대원과 인터뷰, 실제 소방서에서 쓰는 포맷을 참고하여 서비스화를 한다는 가정하에 프로젝트의 완성도를 높이고자 했다.</p>
<h2 id="도전-과제-및-해결책">도전 과제 및 해결책</h2>
<h3 id="nfc-태깅">nfc 태깅</h3>
<p>신고 서비스를 구상하고 모바일 서비스가 확정되면서 기획 초기에는 PWA로 배포하기로 기획했다.
하지만 테스트 결과 PWA에서는 보안적인 문제로 기기의 화면이 켜져 있어야 nfc 태깅이 가능하다는 것을 알게되고, 사실상 억지스러운 유저 플로우가 되버려 기능을 넣을지 말지 고민을 하게됐다..ㅜ
하지만 기존의 서비스와 차별점을 줄 수 있는 프로젝트의 핵심 기능 중 하나였기 때문에 nfc 태깅이 자유로운 안드로이드 개발을 차선책으로 생각할 수 밖에 없었다.
다행히 팀원이 웹 뷰를 제안하여 안드로이드 웹뷰로 웹 서비스를 띄우기로 결정했다!</p>
<h3 id="openvidu-적용">openvidu 적용</h3>
<p>예상치 못한 백엔드 TURN 서버 문제로 구현한 프론트엔드 socket 코드를 openvidu로 바꿔야했다.
아까운 내 소켓 코드.. 백엔드가 약간 미웠지만 이것도 배움에 일부라고 생각하고 openvidu.js 코드로 바꾸게 되었다.
주변에서 openvidu를 나중에 올리면 충돌나서 설정해줄게 많다고 겁을 줘서 걱정을 많이 했는데 인프라 팀원이 너무 잘해줘서 큰 변경사항 없이 배포를 성공할 수 있었다. </p>
<p>*<em>초기 WebRTC 구조
*</em>Node.js로 구현한 시그널링 서버를 통해 P2P방식으로 연결했다.
노마드 코더의 Zoom 클론코딩을 참고해서 기초 코드를 작성하고 우리 프로젝트에 맞는 이벤트를 작성했다.
javascript 코드라 백엔드에서 하기 어렵다는 의견이 나와 혼자 개발을 진행했고,
시그널링 서버 구현, 프론트엔드 연결을 마치고 구글에서 제공하는 무료 STUN 서버를 사용해 동작함을 확인했다.</p>
<p>| 신고 webRTC 로직 | 
| --- | --- |
| <img src="https://velog.velcdn.com/images/jeon-yj/post/b939f9cd-eb69-42b8-8360-f154b6f1fda0/image.png" alt=""> | <img src="https://velog.velcdn.com/images/jeon-yj/post/f67ee290-c5d5-408b-ab4f-a5d260b54242/image.png" alt=""> |</p>
<p><strong>openvidu 세션관리</strong>
오픈비두를 사용하여 세션을 접속하고 연결을 끊는 과정에서 간헐적으로 딜레이가 생겨 세션이 중복되거나 모바일 사용자끼리 세션이 열리는 버그가 있었다.
시그널링 서버로 구현했을 때는 세션 큐를 구현해서 타 신고자가 들어가지 않게 하려했는데 오픈비두에서는 자체적으로 세션을 할당해 주기 때문에 특정 세션 id를 부여해서 버그를 방지할 수 박에 없었다.
그렇기 때문에 추가적으로 백 서버에 요청을 보내서 현재 입장 가능한 세션 id를 get하고 그 id로 openvidu 세션에 연결을 시도하는 로직이 추가되었고 상황실 로직에서도 백 서버와 불필요한 연결이 들어갈 수 밖에 없었다.</p>
<h3 id="axios-retry">Axios retry</h3>
<p>세션 연결이 실패하면 다시 119대원과 연결하는 요청을 해야했다.
구글링 결과 axios-retry 라이브러리를 통해 반복적인 rest Api 호출이 가능하다는 것을 알게됐고 요청이 실패했을 경우 1초 간격으로 10번 정도 다시 요청하고 그래도 실패했을 경우 전화 신고를 유도하는 모달을 띄우는 것으로 기능을 구현했다.</p>
<p>이전에는 Axios Interceptors를 통해 response의 Http status가 200이 아닐 때, 더 들어가 401인 경우 access Tocken이 만료된 것으로 생각하여 재발급 api를 요청하고 거기서 실패하면 refresh Tocken이 만료된 것으로 처리해 로그인을 다시 하도록 리다이렉트시켰다.</p>
<p>더 구글링 해보니 <a href="https://github.com/axios/axios/issues/934#issuecomment-322003342">Axios 팀에서도 Retry를 작성할 때는 interceptor를 이용하라고</a> 한다. 또한 재시도 전략이라는 것도 있는데 실패했을 때 언제 다시 재시도 요청을 할 것인지에 대해 즉 재시도 시기 결정에 대해 구분하여 <a href="https://anu95.medium.com/implement-retry-logic-using-javascript-e502693e0b5c">어떨 때 사용자 경험이 향상되는 지 분석해놓은 글</a>도 있었다.</p>
<p>학습을 하고 나니 무작정 재요청을 보내는게 맞지 않다고 생각했지만, 응급 신고인 만큼 빠르게 요청을 성공시켜야한다고 생각하여 retry하는게 아니라 애초에 세션 대기 큐에 상황실 대원이 있는게 아니라 신고자가 대기 큐에 있어야하는게 아닌가 고민이 들었다.</p>
<h3 id="useform-hook">useForm Hook</h3>
<p>사용자의 정보를 입력받는 폼에서 웹에서는 보통 하나의 페이지에서 입력을 받아 폼을 나눈다는 생각을 못했는데 모바일의 UI UX에 대해 찾아보면서 한 화면에 많은 input을 두지 않는 것을 알게 되었다.</p>
<p>페이지를 나누니 이전에 입력했던 input을 저장할 필요가 있었고, form의 validation, useState, onChange, onSubmit 코드가 계속 중복되어 불편했다.
그래서 form을 컴포넌트 외부에서 관리하는 useForm 훅을 만들었고 훅에서 value에 맞는 유효성 검사와 에러에 따른 helper text 설정, onSubmit, onChange handler 함수들의 상태를 관리하도록 리팩토링했다.</p>
<p>상태관리로 Zustand를 사용했기 때문에 zustand에 input store를 만들어서 훅을 통해 value와 validation 상태를 저장했다.</p>
<h3 id="disable된-버튼의-active-interaction">disable된 버튼의 active interaction</h3>
<p>그냥 생각해서는 button이 disabled 되어있으면 active interaction은 당연히되지 않을거라고 생각했는데 작용한다.
스택오버플로우에서 <em><strong>:active doesn&#39;t exclude :disabled elements.</strong></em> 라는 답을 주었다.
styled-component를 사용중이었기 때문에 간단하게 disalbe 상태라면 active props를 주지 않는 식으로 해결했다.</p>
<h2 id="향후-계획">향후 계획</h2>
<p>고도화 한다면 웨어러블 기기나 스마트폰 자체의 센서를 통해 낙상을 감지하여 자동 신고를 한다던가,
현재는 nfc 스티커를 통해 태깅 신고를 하려면 타 앱을 설치하여 스티커에 url을 저장해야하는 복잡한 절차가 있기 때문에 자체 nfc writer 앱을 만들어 보는 것도 고도화할 부분이라고 생각한다.</p>
<h2 id="마무리-및-후기">마무리 및 후기</h2>
<h3 id="수상">수상</h3>
<p>사회 공헌적인 주제여서 인지 발표 덕분인지 삼성 청년 SW아카데미 공통 프로젝트 우수상을 수상하였다. 수상 욕심을 가지지 않고 시작한거라 얼떨떨하고 개인적으로 부족하다고 생각하여 더욱 좋은 모습을 못보여준게 아쉽기도 했다.</p>
<h3 id="배운-점">배운 점</h3>
<h3 id="아쉬운-점">아쉬운 점</h3>
<p>또한 백엔드 문제로 사용하지 못한 node.js 소켓 코드를 써보기 위해 직접 TURN 서버를 구현해 배포해보고 싶다.</p>
<p>안드로이드 개발</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준]구간 합 구하기4 JAVA]]></title>
            <link>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%EA%B5%AC%EA%B0%84-%ED%95%A9-%EA%B5%AC%ED%95%98%EA%B8%B04-JAVA</link>
            <guid>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%EA%B5%AC%EA%B0%84-%ED%95%A9-%EA%B5%AC%ED%95%98%EA%B8%B04-JAVA</guid>
            <pubDate>Wed, 31 Jan 2024 13:00:22 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-링크"><a href="https://www.acmicpc.net/problem/11659">문제 링크</a></h3>
<p>구간 합 구하기 문제는 실버부터 플레까지 난이도 폭이 크다. n값과 연산 처리 여부에 따라 난이도가 달라지는데, 4번은 실버 3으로 쉬운 편이다.</p>
<h3 id="구간-합-문제-유형">구간 합 문제 유형</h3>
<p>이 유형으로는 아래와 같은데,</p>
<ul>
<li>구간 합(a부터 b까지)</li>
<li>누적 합(0~c까지)
풀이 알고리즘이 다양하다. 그래서 재밌당 ㅎㅎ
구간 합 문제는</li>
</ul>
<ol>
<li>dp</li>
<li>세그먼트 트리</li>
<li>팬윅 트리
와 같은 알고리즘을 통해 해결할 수 있다.</li>
</ol>
<p>각각의 대한 설명은 다른 좋은 글이 있으니 링크만 남겨두겠다. 
<a href="https://yabmoons.tistory.com/431">세그먼트 트리 설명 블로그</a>
<a href="https://yabmoons.tistory.com/438">펜윅트리 설명 블로그</a></p>
<h3 id="문제-풀이-빌드업">문제 풀이 빌드업</h3>
<p>나는 펜윅트리와 dp를 사용해서 각각 풀어보았다.
펜윅트리는 누적합에 효율적이라 사실 세그먼트트리가 더 좋은 해결법이었겠지만, 난 펜윅이 좋다(구현하기 편하자너😛😌)</p>
<h4 id="dp-코드">Dp 코드</h4>
<pre><code class="language-java">public class 구간합구하기4Dp {

    public static void main(String[] args) throws IOException {
        BufferedReader  br=new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st=new StringTokenizer(br.readLine());
        int n=Integer.parseInt(st.nextToken());
        int m=Integer.parseInt(st.nextToken());

        //누적 합을 저장할 배열
        int sum[]=new int[n+1];

        st=new StringTokenizer(br.readLine());
        for (int i = 1; i &lt;= n; i++) {
            int num=Integer.parseInt(st.nextToken());
            //입력받으면서 누적합 계산
            sum[i]=sum[i-1]+num;
        }

        for (int i = 0; i &lt; m; i++) {
            st=new StringTokenizer(br.readLine());
            int start=Integer.parseInt(st.nextToken());
            int end=Integer.parseInt(st.nextToken());
            //sum[end]-sum[start]를 하면 start 원소가 빼지게 된다.
            bw.write(Integer.toString(sum[end]-sum[start-1])+&quot;\n&quot;);
        }
        bw.flush();
    }
}</code></pre>
<h4 id="펜윅트리-코드">펜윅트리 코드</h4>
<pre><code class="language-java">public class 구간합구하기4FenwickTree {
    static int n;
    static long[]fenwik;
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        n=sc.nextInt();
        int m=sc.nextInt();
        fenwik=new long[n+1];
        //팬윅은 1부터 시작
        for (int i = 1; i &lt;=n; i++) {
            int temp=sc.nextInt();
            updateTree(i, temp);
        }
        for (int i = 0; i &lt; m; i++) {
            int a=sc.nextInt();
            int b=sc.nextInt();
            long answer=sum(b)-sum(a-1);
            System.out.println(answer);
        }
    }
    //트리 초기화/갱신 메소드.
    static void updateTree(int index,int val) {
        while(index&lt;=n) {
            fenwik[index]+=val;
            index+=index&amp;-index;
        }
    }
    //누적합 계산 메소드
    static long sum(int index) {
        long result=0;
        while(index&gt;0) {
            result+=fenwik[index];
            index-=index&amp;-index;
        }
        return result;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/b517bb3d-ee63-4abb-a115-13355c04d73a/image.png" alt="">
두 방법으로 풀고 메모리와 시간을 보니 차이가 많이 났다.
위에가 dp고 오래 걸린 아래가 펜윅트리이다.
짧은 알고리즘 실력으로는 펜윅트리는 누적합 계산을 위한 update를 할때 트리를 순회하게 되서 그런게 아닌가 싶다.
이유를 아시는 분은 댓글 부탁드립니다용</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SWEA] 최적경로 JAVA]]></title>
            <link>https://velog.io/@jeon-yj/SWEA-%EC%B5%9C%EC%A0%81%EA%B2%BD%EB%A1%9C-D5</link>
            <guid>https://velog.io/@jeon-yj/SWEA-%EC%B5%9C%EC%A0%81%EA%B2%BD%EB%A1%9C-D5</guid>
            <pubDate>Wed, 31 Jan 2024 07:58:44 GMT</pubDate>
            <description><![CDATA[<p>오늘 재밌는 문제?를 풀어서 리뷰해본다.
알고리즘을 풀때 사소한 코드 하나 하나 질문할 멘토가 있다는 건 정말 좋은 점인거 같다.
싸피 최고 싸피 짱 <del>삼전 짱</del></p>
<h3 id="문제-링크"><a href="https://swexpertacademy.com/main/code/problem/problemDetail.do?contestProbId=AV15OZ4qAPICFAYD">문제 링크</a></h3>
<h3 id="문제-빌드업">문제 빌드업</h3>
<ol>
<li><p>처음 문제를 읽을 때는 두점 사이의 거리와 한점에서 모든 지점을 가는 줄 알고 다익스트라, bfs/dfs를 생각했다</p>
</li>
<li><p>가장 짧은 경로의 이동거리만 알면 된다. 경로 자체를 찾는것이 아님</p>
</li>
<li><p>N&lt;=10이므로 N!도 가능 =&gt; 완탐</p>
</li>
<li><p>순열을 구한 후 거리 구하기로 풀어야겠다</p>
<h3 id="시행착오">시행착오</h3>
</li>
<li><p>문제를 대충 읽고 회사 - 고객 - 회사 - 집 인줄 알았다 ㅎ</p>
<h3 id="첫-코드">첫 코드</h3>
<pre><code class="language-java">import java.util.*;
class Solution
{
 static Pos[] cus;
 static Pos company;
 static Pos home;
 static boolean[]visited;
 static int []order;
 static int[][]distance;//company는 n번째
 static int n;
 static int answer;
 public static void main(String args[]) throws Exception
 {
     Scanner sc=new Scanner(System.in);
     int T=sc.nextInt();
     for (int t = 1; t &lt;= T; t++) {
         n=sc.nextInt();
         cus=new Pos[n];
         visited=new boolean[n];
         order=new int[n];
         distance=new int[n+2][n+2];
         answer=Integer.MAX_VALUE;
         int a=sc.nextInt();
         int b=sc.nextInt();
         company=new Pos(a,b);
         a=sc.nextInt();
         b=sc.nextInt();
         home=new Pos(a,b);
         for (int i = 0; i &lt; n; i++) {
             int c=sc.nextInt();
             int d=sc.nextInt();
             cus[i]=new Pos(c,d);
         }
         permute(0);
         System.out.println(&quot;#&quot;+t+&quot; &quot;+answer);
     }

 }
 static void permute(int count) {
     if(count==n) {
         //다 구함
         answer=Math.min(answer,getAnswer());
         return;
     }
     for (int i = 0; i &lt; cus.length; i++) {
         if(visited[i])continue;
         //가거나
         visited[i]=true;
         order[count]=i;
         permute(count+1);
         visited[i]=false;
         //안가거나
     }
 }
 static int getAnswer() {
     //order 순회하면서 길이 합 구하기
     //회사- 첫 고객, 마지막 고객-집도 더해야함
     int sum=0;
     sum+=getDistance(company,cus[order[0]],n,order[0]);
     for (int i = 0; i &lt; n-1; i++) {
         if(distance[order[i]][order[i+1]]!=0) {
             sum+=distance[order[i]][order[i+1]];
         }
         else sum+=getDistance(cus[order[i]],cus[order[i+1]],order[i],order[i+1]);
     }
     sum+=getDistance(home,cus[order[n-1]],n+1,order[n-1]);
     return sum;
 }
 // 두 점의 거리를 구하는 메소드
 //distance배열을 만들어 거리를 저장해줬다.
 static int getDistance(Pos a,Pos b,int i1,int i2) {
     int d=Math.abs(a.x-b.x)+Math.abs(a.y-b.y);
     distance[i1][i2]=d;
     distance[i2][i1]=d;
     return d;
 }
 static class Pos{
     int x;
     int y;
     public Pos(int x, int y) {
         super();
         this.x = x;
         this.y = y;
     }
 }
}</code></pre>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/64ccdf13-51b0-497c-a0c6-ce5998e11dcd/image.png" alt=""></p>
<p>처음에는 순열을 다 구하고 마지막에 거리를 계산해줬다.
불필요한 배열도 많고 변수도 많아서 리팩토링이 필요해 보였다.</p>
<h3 id="두번째-코드">두번째 코드</h3>
<p>두번째에서는 getAnswer()의 for문에 if(distance[order[i]][order[i+1]]!=0)을 넣어 합을 구하면서 sum이 answer보다 크면 굳이 뒤를 다 더하지 않게 조건문을 넣어주었다.</p>
<p>나는 이 코드를 넣으면 시간이 더 줄 수 있다고 생각했다..!
하지만,,
<img src="https://velog.velcdn.com/images/jeon-yj/post/8e305c43-2e01-4136-a8d9-b10974b5fb39/image.png" alt=""></p>
<p><strong>늘었다!!!</strong>
조금이라 사실 문제 정답 여부와는 상관이 없겠지만, 궁금증이 생겼다.</p>
<p>이후 멘토님께 여쭤보니 for문으로 합을 구하는 건 O(1*n)이지만 조건문에서 조건을 확인하는건 O(1000)은 잡아야한다고 하셨다.
나는 무조건 for문을 빠져나오는게 좋을 거라 생각했는데, n값이 작고 가지치기 할 경우가 없다면 굳이 반복문을 빠져나오게 항상 시간적 이득을 얻는건 아니었다.</p>
<h3 id="세번째-풀이">세번째 풀이</h3>
<p>그렇다면 어떨 때 조건문을 써서 빠져나올 것인가?
위에 적은대로 가지치기를 해서 뒤에 경우를 크게 줄일 경우이다.
그래서 sum을 순열을 구한 후 계산하지 않고, 순열을 구하면서 더해주었다.</p>
<pre><code class="language-java">import java.util.*;

public class Solution {
    static Pos[] location;
    static boolean[]visited;
    static int prev;//cus index
    static int[][]distance;//company는 n번째, home n+1
    static int n;
    static int answer;
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int T=sc.nextInt();
        for (int t = 1; t &lt;= T; t++) {
            n=sc.nextInt();
            location=new Pos[n+2];
            visited=new boolean[n];
            distance=new int[n+2][n+2];
            answer=Integer.MAX_VALUE;
            int a=sc.nextInt();
            int b=sc.nextInt();
            location[n]=new Pos(a,b);
            prev=n;
            a=sc.nextInt();
            b=sc.nextInt();
            location[n+1]=new Pos(a,b);
            for (int i = 0; i &lt; n; i++) {
                a=sc.nextInt();
                b=sc.nextInt();
                location[i]=new Pos(a,b);
            }

            permute(0,0);

            System.out.println(&quot;#&quot;+t+&quot; &quot;+answer);
        }

    }
    static void permute(int count,int sum) {
        if(count==n) {
            //다 구함
            //System.out.println(Arrays.toString(order));
            //마지막 고객- 집
            sum+=getDistance(n+1, prev);
            answer=Math.min(answer,sum);
            return;
        }
        if(sum&gt;=answer)return;
        for (int i = 0; i &lt; n; i++) {
            if(visited[i])continue;
            //가거나
            visited[i]=true;
            int d=0;
            int temp=prev;
            if(distance[prev][i]!=0) {
                d=distance[prev][i];
            }else d=getDistance(prev,i);
            prev=i;
            permute(count+1,sum+d);
            prev=temp;
            visited[i]=false;
            //안가거나
        }
    }
    static int getDistance(int i1,int i2) {
        Pos a=location[i1];
        Pos b=location[i2];
        int d=Math.abs(a.x-b.x)+Math.abs(a.y-b.y);
        distance[i1][i2]=d;
        distance[i2][i1]=d;
        return d;
    }
    static class Pos{
        int x;
        int y;
        public Pos(int x, int y) {
            super();
            this.x = x;
            this.y = y;
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/3e98ecc7-64b8-43b7-b341-7c4430ba45dc/image.png" alt="">
엄청 줄었다!!!! 대~박</p>
<p>맞은 문제지만 코드 한줄 한줄 고민하고 리팩토링해보니 너무 재밌었다.
물론 마지막 코드에서도 리팩토링할 부분은 많다.
입출력 방식이라던지, 변수 전역화 부분 이라던지... 
그래도 유의미한 고민을 한거 같아서 만족하고 더욱 고민하고 코드를 음미할 수 있는 개발자가 되기 위해 공부해야겠다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SWEA] 수영대회 결승전]]></title>
            <link>https://velog.io/@jeon-yj/Problem-Solving-%EC%88%98%EC%98%81%EB%8C%80%ED%9A%8C-%EA%B2%B0%EC%8A%B9%EC%A0%84</link>
            <guid>https://velog.io/@jeon-yj/Problem-Solving-%EC%88%98%EC%98%81%EB%8C%80%ED%9A%8C-%EA%B2%B0%EC%8A%B9%EC%A0%84</guid>
            <pubDate>Mon, 15 Jan 2024 02:47:54 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-링크"><a href="https://swexpertacademy.com/main/talk/solvingClub/problemView.do?solveclubId=AYyJy6Q6DHADFASu&amp;contestProbId=AWKaG6_6AGQDFARV&amp;probBoxId=AYyJy6Q6DHEDFASu&amp;type=USER&amp;problemBoxTitle=%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98+Track+%28%EB%82%9C%EC%9D%B4%EB%8F%84+%EC%83%81%29&amp;problemBoxCnt=6#none">문제 링크</a></h3>
<h3 id="내-문제-풀이-빌드업">내 문제 풀이 빌드업</h3>
<ol>
<li><p>n&lt;=15, 테케 15개 2초이므로 시간에 크게 구애받지 않음.</p>
</li>
<li><p>최단 시간, 경로이므로 bfs, dfs를 생각할 수 있음.</p>
</li>
<li><p>한 지점에서 특정 한지점이므로 bfs를 선택.</p>
<h3 id="시행착오">시행착오</h3>
</li>
</ol>
<ul>
<li>bfs는 많이 풀어봐서 큰 틀 코딩까지는 얼마 안걸렸지만 테케 하나에서 시간초과가 계속 났다.</li>
<li>bfs에서 시간초과라 하면 방문처리밖에 없다고 생각해서 다른 bfs와 다른 점인 소용돌이를 기다리는 경우에 대해 생각해봄.</li>
<li>모든 시점에서 기다리는 경우를 큐에 넣어줘서 큐가 비지 않았음.</li>
<li>방문 처리에 횟수 제한을 둬서 3번 이상은 큐에 넣어주지 않음<h3 id="정답-풀이">정답 풀이</h3>
<pre><code class="language-cpp">#include&lt;iostream&gt;
#include&lt;vector&gt;
#include&lt;utility&gt;
#include&lt;queue&gt;
#include&lt;algorithm&gt;
#define MAX 987654321
</code></pre>
</li>
</ul>
<p>using namespace std;</p>
<p>struct Pos {
    int x;
    int y;
    Pos(int X,int Y) : x(X),y(Y){};
};</p>
<p>int dx[4] = {1,0,-1,0};
int dy[4] = {0,1,0,-1};
int n,answer;
Pos start(0,0), arrive(0,0);
vector &lt;vector<int>&gt; arr;
vector &lt;vector<int>&gt; visited;
queue &lt;pair&lt;int, Pos&gt;&gt; bfs;
void getAnswer();
bool canGo(vector &lt;vector<int>&gt;, int, int,int);
int main(int argc, char** argv)
{
    int test_case;
    int T;
    cin &gt;&gt; T;
    for (test_case = 1; test_case &lt;= T; ++test_case)
    {
        cin &gt;&gt; n;
        arr.resize(15, vector <int>(15));
        visited.resize(15, vector <int>(15,0));
        bfs = queue &lt;pair&lt;int, Pos&gt;&gt;();
        answer = MAX;
        //배열 입력
        for (int i = 0; i &lt; n; i++) {
            for (int j = 0; j &lt; n; j++) {
                cin &gt;&gt; arr[i][j];
                visited[i][j] = 0;
            }
        }
        int x, y;
        cin &gt;&gt; x &gt;&gt; y;
        start.x = x;
        start.y = y;
        cin &gt;&gt; x &gt;&gt; y;
        arrive.x = x;
        arrive.y = y;
        bfs.push(make_pair(0, start));
        // bfs 돌기
        getAnswer();
        if(answer==MAX)cout &lt;&lt; &quot;#&quot; &lt;&lt; test_case &lt;&lt; &quot; &quot; &lt;&lt; -1 &lt;&lt; endl;
        else {
            cout &lt;&lt; &quot;#&quot; &lt;&lt; test_case &lt;&lt; &quot; &quot; &lt;&lt; answer &lt;&lt; endl;
        }
    }
    return 0;//정상종료시 반드시 0을 리턴해야합니다.
}
void getAnswer() {
    visited[start.x][start.y]=1;
    while (!bfs.empty()) {
        pair &lt;int,Pos&gt; p = bfs.front();
        bfs.pop();</p>
<pre><code>    int currTime = p.first;
    Pos currPos = p.second;

    if (currPos.x == arrive.x &amp;&amp; currPos.y == arrive.y) { 
        answer = currTime;
        break;
    }
    //1. 기다리기
    if(visited[currPos.x][currPos.y]&lt;3){
            bfs.push(make_pair(currTime + 1,currPos));
        visited[currPos.x][currPos.y]++;
    }
    //2. 움직이기
    for (int i = 0; i &lt; 4; i++)
    {
        int nextX = currPos.x + dx[i];
        int nextY = currPos.y + dy[i];
        if (canGo(arr, nextX, nextY,currTime)) {
            visited[nextX][nextY] = 1;
            bfs.push(make_pair(currTime + 1, Pos(nextX, nextY)));       
        }
    }
}</code></pre><p>}
bool canGo(vector &lt;vector<int>&gt; v, int x, int y,int currTime) {
//배열 범위 안일때
    if (x &gt;= 0 &amp;&amp; x &lt; n &amp;&amp; y &gt;= 0 &amp;&amp; y &lt; n) {
    //장애물이 아니고 아직 안간 곳일 때
        if (arr[x][y]!=1&amp;&amp;visited[x][y]==0) {
            if (arr[x][y] != 2) return true;
            else {
            //소용돌이인데 시간 상 건너갈 수 있을 때
                if (currTime % 3 == 2) return true;
            }
        }
    }
    return false;
}</p>
<pre><code>![](https://velog.velcdn.com/images/jeon-yj/post/41fd05b9-9667-4830-b6e0-e5c525edbf0f/image.png)

### 두번째 풀이
다른 사람들의 실행 시간을 보니 10ms 대 였다...
당황,,😱 완벽한 풀이가 아니라고 생각해서 다시 풀어보았다.
1. canGo에 if문을 switch문으로 바꿨다.
=&gt;실행시간이 50ms에서 39ms로 단축</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[SSAFY 11기 최종 합격 후기]]></title>
            <link>https://velog.io/@jeon-yj/SSAFY-11%EA%B8%B0-%EC%B5%9C%EC%A2%85-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@jeon-yj/SSAFY-11%EA%B8%B0-%EC%B5%9C%EC%A2%85-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 21 Dec 2023 12:48:17 GMT</pubDate>
            <description><![CDATA[<p>우수수 떨어진 지원들 속 나를 구원해준 싸피느님...
입과를 고민중이긴 하지만 합격해서 일단은 좋다.</p>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/e2bf8a86-18b5-43a8-87c3-cd04de34bdbe/image.png" alt=""></p>
<h2 id="📅11기-일정">📅11기 일정</h2>
<p>지원서 접수 : 10.23(월) ~ 11.6(월)
에세이 제출 : 11.7(화) ~ 11.18(토)
SW 적성 진단 : 11.19(일)[SW전공]
SW 적성 진단 결과 발표 : 11.28(화)
인터뷰 : 12.06(월) ~ 12.12(화)
인터뷰 결과 발표 : 12.21(목)</p>
<h2 id="🧐지원-동기">🧐지원 동기</h2>
<p>제일 큰 이유는 1년 넘게 취준을 하면서 아무것도 없는 공백기가 무서웠다.
사실 아무것도 안하지 않고 프로젝트도 여러개 했지만 연이은 탈락에 싸피라도 하자 싶어 지원했다. </p>
<h2 id="준비-과정">준비 과정</h2>
<p>싸피의 준비 과정은  에세이 작성, SW 적성 진단, 인터뷰가 있다.
이 세개 다 최선을 다해 준비하진 않았다ㅎ
그렇지만 다른 기업 면접이나 코테 준비하면서 자연스럽게 준비가 된거 같다.</p>
<h3 id="🧾에세이-작성">🧾에세이 작성</h3>
<p>기업 자소서를 많이 써봐서 작성 자체는 어렵지 않았지만 교육 기관에 지원한다는 점에서 어떤 것을 어필해야할 지 조금 고민했다.
지인에게 첨삭을 받고 무엇을 할 줄 아는 지를 나열하기 보다</p>
<ul>
<li>어떤 개발자가 되고 싶은 지</li>
<li>되기 위해 무슨 노력을 했는지</li>
<li>그 경험 속에서 무엇이 부족하다 느꼈는지</li>
<li>싸피에서 부족한 부분을 극복할 수 있는 지=왜 싸피에 지원했는 지</li>
<li>성장에 대한 열정이 있는 지</li>
</ul>
<p>이 다섯 가지를 담아내고자 노력했고 또 삼성 컨퍼런스에 가서 느낀 점을 어필하며 에세이를 마무리했다.</p>
<p>미래의 지원자들도 첨삭 많이 받고 수정도 여러번 해서 많이 작성해보고 제출하는 것을 추천한다!!
그리고 *<em>에세이를 통해 차후의 인터뷰의 질문을 이끌어 낼 수 있기 때문에 질문 유도용 단어나 프로젝트를 넣어놓고 미리 준비하는 것이 좋다. *</em></p>
<p>올 한해 제출한 자소서만 몇 갠지..😥</p>
<h3 id="👩💻-sw-적성-진단코딩-테스트">👩‍💻 SW 적성 진단(코딩 테스트)</h3>
<p>이미 기업 코테를 위해 코테 경험도 많고 준비도 어느 정도 되어있어서 따로 준비를 하진 않았다.
전공자의 경우 오전, 오후 나눠서 보는 식이었는데 비슷한 난이도였던거 같다. 
체감 난이도는 백준 실버 ~ 골드 2-4?  정도로 프로그래머스 기준 레벨 2를 시간 내에 풀 수 있으면 크게 문제가 없었을 거 같다.
싸피 코테가 <a href="https://swexpertacademy.com/main/main.do">SWEA</a>의 제출 환경과 같아서 미리 경험해 보는 걸 추천한다.</p>
<p>어려운 문제를 풀어보기 보다 정석적인 알고리즘을 완벽하게 이해한 후 다양한 문제를 접하고 어떤 알고리즘을 적용할지 고민해보는 것을 추천한다. </p>
<p><del>번외로 몇 솔인지는 합격에 크게 영향이 없는거 같았다.</del>
<del>오픈카톡방의 정보로는 0솔도 붙는다고 하고 올 솔도 떨어진다고 하는 걸 보니,,(믿는 건 자유)</del></p>
<p>아래는 추천하는 글이다. 
이런 엄청난 분이 있기에 취준하면서 도움도 많이 얻고 도전하고자 하는 희망이 생긴다.
<a href="https://covenant.tistory.com/224">코테 알고리즘 별 백준 문제 정리 - 티스토리</a>
<a href="https://github.com/tony9402/baekjoon?tab=readme-ov-file">코테 알고리즘 별 백준 문제 정리 - 깃헙</a></p>
<h3 id="🙋♀️-인터뷰">🙋‍♀️ 인터뷰</h3>
<p>다들 오픈카톡방에 들어가서 면접 스터디를 만들고 한다고 하는데 귀찮+굳이 라는 생각에 그냥 지인과 대면 스터디 1회 온라인 스터디 1회로 총 2번 연습 후 인터뷰를 했다ㅋ
심지어 인터뷰가 아침 7시 45분이었는데 만화보다 새벽 3시에 자버림ㅋ
이 글을 읽는 다른 지원자분들은 이러지 말고 후회 없이 준비하시길 바란다..
나는 기업 면접 경험이 있었기 때문에 면접 답변의 준비와 시선 처리 등 전체적인 면접 준비가 되어 있어 굳이 스터디를 진행하지 않았지만 경험이 없다면 스터디를 적극 추천한다.</p>
<p>면접 내용은 대외비이기 때문에 언급할 수 없어 내가 준비한 내용으로 공유하겠다.</p>
<h4 id="pt-면접">PT 면접</h4>
<p>어떤 식으로 진행하는지 전혀 정보가 없었기 때문에 꺄루 it 키워드에 대해 정리하면서 개념/활용방안/예상어려움/해결방안 위주로 큼지막하게 외웠다.
또한 어떤 주제가 나오든 한계로 보안문제와 환경문제를 말하고자 했다.
  ex)</p>
<ul>
<li>보안문제로 양자컴퓨터를 활용한 양자암호 -&gt; 비싸면 해시값으로 대체</li>
<li>환경문제로 디지털탄소 생김 -&gt; 최근이상기후 -&gt; 개인, 정부가 줄이는 방법 물색</li>
<li>새로운 기술 -&gt; 장.노년층은 잘 알지 못함 -&gt; 정부가 정보 소외계층을 줄이는 방법 물색</li>
</ul>
<p>정리한(할) 주제로는 클라우드, 핀테크, AI, 블록체인, 메타버스, VR, nft, IOT.. 등 많지만
이걸 다 외우면 이미 취업한 사람이라고 생각하면서 몇가지만 중점적으로 준비했다.
<strong>AI, 메타버스, 핀테크, 블록체인</strong>
이 4가지만 준비해갔고 활용방안이나 예상 어려움, 해결 방안을 최근 뉴스나 대두되는 문제점과 연결지어 정리했다.
ex)</p>
<ul>
<li>AI : ai 판사, 자율 주행 버스 심야A21</li>
<li>메타버스 : 이세계 아이돌 공연</li>
</ul>
<p>아래는 참고한 사이트 입니다. 프론트엔드를 직무를 준비하다 보니 웹 위주의 글을 평소에도 자주 보긴했습니다.</p>
<p><a href="https://blog.naver.com/PostList.naver?blogId=with_msip&amp;categoryNo=56&amp;skinType=&amp;skinId=&amp;from=menu&amp;userSelectMenu=true">과학기술정보통신부 블로그</a>
<a href="https://www.lgcns.com/blog/it-trend/">LG CNS 블로그</a>
<a href="https://opensource.samsung.com/blog">삼성 블로그</a>
<a href="https://fe-developers.kakaoent.com/">카카오 프론트엔드 블로그</a></p>
<h4 id="인성기술-질문">인성/기술 질문</h4>
<p>기업 면접 준비하면서 대략적으로 답변이 준비되어 있어 자기소개와 마지막 할 말 정도만 다시 만들어 정리했다. 
하지만 기술 질문에서 빈틈이 많아서 에세이에 적은 언어나 tool 위주의 질문들은 추가로 공부했다. </p>
<p>어차피 면접 답변 준비해가도 그거 대로 안나오고 나와도 까먹는다ㅋㅋ
그렇기 때문에 나에 대해 정리하고 프로젝트나 경험들을 정리하면서 에세이에서부터 일관성있는 &#39;나&#39;를 만드는 것이 중요하다. 
다시 말하지만 면접관들은 에세이를 통해 나에 대한 힌트(질문 거리)를 얻는다.
면접 스터디를 통해 에세이에 대한 질문을 받고 꼬리 질문이나 비슷한 질문을 준비하면 좋은 결과가 있을 것이다. </p>
<p>아래는 준비한 예상 질문이다.
<img src="https://velog.velcdn.com/images/jeon-yj/post/71e1cf95-0f79-4f35-906d-de870aecfa67/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/aabca58f-bac4-447e-a73d-c159ac5c20d1/image.png" alt=""></p>
<p>아래는 추천하는 글이다. 
이런 엄청난 분이 있기에 취준하면서 도움도 많이 얻고 도전하고자 하는 희망이 생긴다.
<a href="https://github.com/JaeYeopHan/Interview_Question_for_Beginner/tree/main">Tech-Interview - 깃헙</a>
<a href="https://github.com/Sub2n/FrontEnd-Study">Frontend-Interview- 깃헙</a></p>
<h3 id="후기">후기</h3>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/78ba79e1-83d2-47f4-a43e-6b517bc6c7a3/image.jpg" alt="">
면까몰이라지만 이번엔 기대도 안했었다.
그래서 발표도 바로 확인 안했는데 이게 뭐람🤣</p>
<p>1지망 서울은 떨어졌지만 2지망 대전에 합격하여 1시간 고민하는 척했다가 입과 확정을 했다ㅋㅋ
서울이었음 1초도 고민 안하지만 솔직히 타지 생활에 막막함이 있긴하다. 아오 또 장거리 연애해야해😭</p>
<p>기업 지원도 그렇고 싸피 지원 과정 동안 자아 성찰도 하고 감사함에 대해 배우게 되는 좋은 경험이었다.
앞으로 어떻게 살 것인지, 어떤 개발자가 될 것인지에 대해 깊게 고민하고 성장할 수 있었다.
진짜루..</p>
<p>이 경험 놓치지 않고 붙잡아서 내년에는 더 발전하는 내가 되기 위해 노력할 것이다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LG CNS 최면탈 후기]]></title>
            <link>https://velog.io/@jeon-yj/LG-CNS-%EC%B5%9C%EB%A9%B4%ED%83%88-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@jeon-yj/LG-CNS-%EC%B5%9C%EB%A9%B4%ED%83%88-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 07 Dec 2023 02:25:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jeon-yj/post/385a289c-4cb3-4b6f-b4be-0a6e301567f5/image.jpg" alt=""></p>
<p>이번 하반기에 유일한 희망이었던 lg cns 최종 발표가 어제 나왔다.
면접에서 실수한 것도 없고, 간절했기에 기대하는 마음이 좀 컸나보다.
왜 떨어졌는지 묻고 싶고, 재고해달라고 떼 쓰고 싶은 마음 뿐이다ㅋㅋ</p>
<p>1차 면접에 합격하고 2차 면접을 준비하고 마지막 결과를 기다릴땐 진짜 시간도 안가고 애가 탔는데, 허무하다.
같이 준비한 스터디원분은 간절하지 않았는데 합격하셨다. 그래서 더 비참하다.</p>
<p>남은 공고 지원과 싸피면접 준비를 해야하니 우울감에 빠져있을 시간이 없다.
이 글 출간을 하면 털어버리고 다시 일어나야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 징검다리 건너기 C++]]></title>
            <link>https://velog.io/@jeon-yj/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A7%95%EA%B2%80%EB%8B%A4%EB%A6%AC-%EA%B1%B4%EB%84%88%EA%B8%B0</link>
            <guid>https://velog.io/@jeon-yj/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A7%95%EA%B2%80%EB%8B%A4%EB%A6%AC-%EA%B1%B4%EB%84%88%EA%B8%B0</guid>
            <pubDate>Fri, 13 Oct 2023 08:13:27 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-링크"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/64062#">문제 링크</a></h3>
<h3 id="내-문제-풀이-빌드업">내 문제 풀이 빌드업</h3>
<ol>
<li><p>n&lt;=2십만이므로 O(N),O(logN)인 알고리즘으로 풀어야함</p>
</li>
<li><p>단순 순회는 디딤돌 최대값이 2억이라 시간초과</p>
</li>
<li><p>logN의 대표적인 알고리즘인 이분탐색으로 풀었지만, 윈도우 슬라이딩이나 정렬 등 여러 방법도 가능할 것 같았음</p>
<h3 id="시행착오">시행착오</h3>
</li>
</ol>
<ul>
<li><p>금주에 보는 코테 언어 중 js가 없어서 다시금 c++을 공부했더니 까먹은 stl이 많았다 😥</p>
</li>
<li><p>이분 탐색의 기준을 건널 수 있는 사람 수가 아닌 돌의 값이라는 생각을 못함. 같은 말이지만 머릿 속에서 정리되지 않았다ㅜ</p>
</li>
<li><p>돌의 값이 k보다 작을때 스킵해야하는데 작거나 같을 때로 조건화 해서 계속 실패가 떴다. </p>
</li>
</ul>
<h3 id="정답-풀이">정답 풀이</h3>
<pre><code class="language-cpp">int solution(vector&lt;int&gt; stones, int k) {
    int answer = 0;
    //디딤돌 값의 최대 최소 구하기
    int start=*min_element(stones.begin(),stones.end());
    int end=*max_element(stones.begin(),stones.end());
    int mid;
    //이분 탐색
    while(start&lt;=end){
        mid=(start+end)/2;
        //skip해야하는 돌의 수와 그 중 최대
        int skip=0,maxSkip=0;
        for(int i=0;i&lt;stones.size();i++){
        //돌이 mid보다 작으면 스킵해야함
        //같을때는 건널 수 있음. 건너고 나서 0이 됨
            if(stones[i]-mid&lt;0) skip++;
            else skip=0;
            maxSkip=max(skip,maxSkip);
            //if문을 for문 밖에 넣어도 되지만 
            //stones의 길이가 길 경우 굳이 다 돌지 않기 위해
            if(maxSkip&gt;=k){
                end=mid-1;
                break;
            }
        }
        //스킵 횟수가 k보다 작으면 start값을 크게
        if(maxSkip&lt;k){
            answer=max(answer,mid);
            start=mid+1;
        }
    }
    return answer;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/7607d8d4-d041-4f3c-ab53-bc29d142454c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준]컨베이어 벨트 위의 로봇 20055 JS]]></title>
            <link>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%EC%BB%A8%EB%B2%A0%EC%9D%B4%EC%96%B4-%EB%B2%A8%ED%8A%B8-%EC%9C%84%EC%9D%98-%EB%A1%9C%EB%B4%87-20055-JS</link>
            <guid>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%EC%BB%A8%EB%B2%A0%EC%9D%B4%EC%96%B4-%EB%B2%A8%ED%8A%B8-%EC%9C%84%EC%9D%98-%EB%A1%9C%EB%B4%87-20055-JS</guid>
            <pubDate>Wed, 06 Sep 2023 16:32:31 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-링크"><a href="https://www.acmicpc.net/problem/20055">문제 링크</a></h3>
<h3 id="내-문제-풀이-빌드업">내 문제 풀이 빌드업</h3>
<ol>
<li><p>n&lt;=100이므로 시간 제한 1초 안에는 n^3까지 가능
 =&gt; 단순 구현 문제일 가능성 높음</p>
</li>
<li><p>자료구조 : 원형 리스트가 필요한가? 
 =&gt; 어떻게 구현하는지도 잘 모르고 문제에서 로봇이 추가되는 지점과 로봇이 나가는 지점이 항상 일정하게 변함.
 즉 인덱스의 변화만으로 원형리스트처럼 배열로 풀이 가능</p>
<ol start="3">
<li>문제 해석이 어려워서 질문게시판을 여러번 정독함 ㅎ
문제에서 설명하는 &#39;<strong>과정</strong>&#39;을 반복하면서 0의 개수가 K가 되면 종료</li>
</ol>
<h3 id="시행착오">시행착오</h3>
</li>
</ol>
<ul>
<li><p>문제의 &#39;언제든지 로봇이 내리는 위치에 도달하면 그 즉시 내린다&#39;를 제대로 생각하지 않고, 과정의 첫번째인 벨트가 회전할 때만 내리게 처리하는 실수를 했다.</p>
<p>하지만 2번의 로봇이 칸을 이동할 때에도 내리는 위치로 이동하는 경우로 처리해줘야한다.</p>
</li>
<li><p>처음엔 start와 end의 인덱스를 변화시키며 저장하였는데, 로봇이 내리는 위치인 mid가 필요하게 되서 다시 계산하는 불필요한 계산이 있었다. 사실 지금 풀이도 맞는 풀이인진 모르겠다</p>
</li>
</ul>
<h3 id="정답-풀이">정답 풀이</h3>
<pre><code class="language-javascript">const fs = require(&quot;fs&quot;);
let [n,k, ...arr] = fs.readFileSync(&quot;/dev/stdin&quot;)
  .toString()
  .trim()
  .split(/\s/)
  .map(Number)
let start = 0;
let mid = n - 1;
let count = 0;
// 각 단계 실행하기
let ans = 0
let box = Array.from({ length: 2 * n - 1 }, e =&gt; false)
while (count &lt; k) {

  if (start == 0) start = 2 * n - 1
  else start--
  if (mid == 0) mid = 2 * n - 1
  else mid--
  //내리기
  box[mid] = false
  //  로봇 이동하기. curr에 갈 수 있는지 확인
  let curr = mid
  for (let i = 0; i &lt; n; i++) {
    let before = curr === 0 ? 2 * n - 1 : curr - 1
    if (arr[curr] &gt; 0 &amp;&amp; !box[curr] &amp;&amp; box[before]) {
      box[curr] = true
      arr[curr]--
      box[before] = false
      if (curr === mid) box[curr] = false
    }
    curr = before
  }
  //로봇 올리기
  if (!box[start] &amp;&amp; arr[start] &gt; 0) {
    box[start] = true
    arr[start]--
  }
  count = 0
  for (let i = 0; i &lt; 2 * n; i++) {
    if (arr[i] === 0) count++
  }
  ans++
}
console.log(ans)</code></pre>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/001a796b-08e5-4f4e-bf03-f37dd88bc791/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 내가 필요해서 쓰는 redux 정리]]></title>
            <link>https://velog.io/@jeon-yj/react-%EB%82%B4%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%B4%EC%84%9C-%EC%93%B0%EB%8A%94-redux-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jeon-yj/react-%EB%82%B4%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%B4%EC%84%9C-%EC%93%B0%EB%8A%94-redux-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 24 Aug 2023 15:10:52 GMT</pubDate>
            <description><![CDATA[<p>새 프로젝트를 만들때마다 넣어주는 라이브러리들.. 
매번 필요해서 넣지만 왜 얘를 사용하는지 정확히 하는 일이 뭔지 매번 찾아보고 사용한다. 나는 참 1회용 뇌를 갖고 있다.</p>
<p><a href="https://react.vlpt.us/redux/">벨로퍼트</a>님의 블로그를 여러번 읽었지만 아직도 헷갈린다.  <a href="https://kyounghwan01.github.io/blog/React/redux/redux-basic/#%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%82%E1%85%B3%E1%86%AB-%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%B2">기억보다 기록을 님 글</a>도 여러번 읽었다. ㅎ
이건 내 잘못이 아니라 라이브러리가 너무 많은 탓이다.(암튼 그럼)</p>
<p>그래서 이번 글에선 내 식대로 내가 기억하기 좋게 적어놓을 것이다. </p>
<pre><code class="language-jsx">//reducer 불변성 관리하게 해주는 모듈
&quot;immer&quot;: &quot;^10.0.2&quot;,

//redux를 react와 연동해서 사용하기 편리하도록 만든 라이브러리
  //액션단계에서 connect라는 함수가 사용된다는 차이점 존재
&quot;react-redux&quot;: &quot;^8.1.2&quot;,
&quot;redux&quot;: &quot;^4.2.1&quot;,
&quot;redux-actions&quot;: &quot;^3.0.0&quot;,

//redux를 통해 바뀔 이전 state, dispatch 실행으로 인해 바뀐 state가 콘솔에 찍혀 디버깅 쉽게 해줌.
&quot;redux-logger&quot;: &quot;^3.0.6&quot;,

//미들웨어
&quot;redux-thunk&quot;: &quot;^2.4.2&quot;,</code></pre>
<h2 id="redux">Redux</h2>
<p><strong>컴포넌트들의 상태 관련 로직들을 다른 파일들로 분리시켜서 관리, 글로벌 상태 관리</strong></p>
<ul>
<li>redux는 컴포넌트에 종속되지 않고,  컴포넌트 바깥에서 상태관리.</li>
<li>프로젝트 루트레벨에서 store라는 곳에 state를 저장하고, 모든 컴포넌트는 store에 구독을 하면서 state와 그 state를 바꾸는 함수를 전달 받음. </li>
<li>함수를 바꿈으로 state가 바뀌면 해당 state를 바라보고 있는 컴포넌트는 모두 리렌더링 됨.</li>
<li><em>Action객체 생성 -&gt; Dispatch함수의 인자로 -&gt; Reducer함수로 전달-&gt; 저장소 Store의 상태 변경-&gt;React 화면 렌더링. 단방향으로만 흐름*</em></li>
</ul>
<h2 id="redux-thunk">Redux-thunk</h2>
<p>redux-thunk는 리덕스에서 <strong>비동기 작업을 처리</strong> 할 때 가장 많이 사용하는 미들웨어입니다. 이 미들웨어를 사용하면 액션 객체가 아닌 함수를 디스패치 할 수 있다.</p>
<p><strong>리덕스 미들웨어란?</strong>
액션이 디스패치 된 다음, 리듀서에서 해당 액션을 받아와서 업데이트하기 전에 추가적인 작업을 할수 있게 하는 함수. 연달아 두번 리턴하는 함수.
미들웨어 라이브러리는 redux-thunk, redux-saga, redux-observable, redux-promise-middleware 등이 있다.
EX)</p>
<ul>
<li>특정 조건에 따라 액션이 무시되게 만들 수 있다.</li>
<li>액션을 콘솔에 출력하거나, 서버쪽에 로깅을 할 수 있다.</li>
<li>액션이 디스패치 됐을 때 이를 수정해서 리듀서에게 전달되도록 할 수 있다.</li>
<li>특정 액션이 발생했을 때 이에 기반하여 다른 액션이 발생되도록 할 수 있다.</li>
<li>특정 액션이 발생했을 때 특정 자바스크립트 함수를 실행시킬 수 있다.</li>
<li>비동기 처리. 리액트 앱에서 우리가 만약 백엔 드 API 를 연동해야 된다면, 리덕스 미들웨어를 사용하여 처리</li>
</ul>
<h2 id="redux-actions">Redux-actions</h2>
<p>액션 생성 함수를 더 짧은 코드로 작성할 수 있게 해준다.
리듀서를 작성할 때 switch문이 아닌 handleActions라는 함수를 사용할 수 있게 해준다.
<a href="https://bitkunst.tistory.com/entry/Redux-redux-actions">참고 링크</a></p>
<pre><code class="language-js">import { createActions, handleActions, combineActions } from &#39;redux-actions&#39;;

const defaultState = { counter: 10 };

const { increment, decrement } = createActions({
  INCREMENT: (amount = 1) =&gt; ({ amount }),
  DECREMENT: (amount = 1) =&gt; ({ amount: -amount })
});

const reducer = handleActions(
  {
    [combineActions(increment, decrement)]: (
      state,
      { payload: { amount } }
    ) =&gt; {
      return { ...state, counter: state.counter + amount };
    }
  },
  defaultState
);

export default reducer;</code></pre>
<h2 id="immer">Immer</h2>
<p>불변성을 유지하는 코드를 쉽게 작성할 수 있도록 도와주는 라이브러리.
얘가 필요한 이유는 Redux의 중요한 3가지 규칙때문인데,</p>
<ol>
<li>하나의 애플리케이션 안에는 하나의 스토어.</li>
<li>상태는 읽기전용.</li>
<li>변화를 일으키는 함수, 리듀서는 순수한 함수여야 한다.
여기서 2번 때문이다.</li>
</ol>
<p>데이터의 변경을 감지하기 위해서는 내부 데이터까지 전부 찾아봐야 하는데 이 경우 시간이 너무 오래 걸린다.
따라서 기존의 상태를 수정하지 않고 새로운 상태를 생성하여 업데이트한다. 이를 통해 불변성을 유지할 수 있다. 
따라서 기존 상태의 객체를 새로운 객체로 변경하면 객체의 주소가 다르므로 얕은 비교를 통해 변경을 쉽게 확인.</p>
<p>...(spread 연산자)를 사용해 기존의 상태값을 복사해서 새로운 객체에 넣어주는 방식으로 상태값을 바꿔주는데 복잡한 구조에서는 까다롭기 때문에 immer 사용하여 불변성 유지.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준]평범한 배낭 여러 알고리즘으로 풀기]]></title>
            <link>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%ED%8F%89%EB%B2%94%ED%95%9C-%EB%B0%B0%EB%82%AD-%EC%97%AC%EB%9F%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9C%BC%EB%A1%9C-%ED%92%80%EA%B8%B0</link>
            <guid>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%ED%8F%89%EB%B2%94%ED%95%9C-%EB%B0%B0%EB%82%AD-%EC%97%AC%EB%9F%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9C%BC%EB%A1%9C-%ED%92%80%EA%B8%B0</guid>
            <pubDate>Wed, 23 Aug 2023 05:34:57 GMT</pubDate>
            <description><![CDATA[<h2 id="0-1-knapsack-problem">0-1 KnapSack Problem</h2>
<p>배낭 문제는 가중치를 자를 수 있는 문제인 Fractional KnapSack Problem와 없는 문제로 나뉜다.
이번 글에서는 보석(가중치)를 자를 수 없는 문제만 다룬다.
Fractional일 경우 그리디로 풀 수 있지만 0-1에서는 불가하고 아래 알고리즘으로 풀 수 있다.</p>
<ol>
<li>부루트포스</li>
<li>Dynamic Programming</li>
<li>Back Tracking</li>
<li>Branch N Bound</li>
</ol>
<h3 id="dp">DP</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
using namespace std;

struct Tool {
    int weight;
    int value;
    void initTool(int w, int v) {
        weight = w;
        value = v;
    }
};
vector &lt;Tool&gt; tools;
int knapsackDP(vector&lt;Tool&gt;&amp; tools, int capacity) {
    int n = tools.size();
    //dp[i][w]는 i번째 항목까지 고려한 배낭의 용량이 w일 때 얻을 수 있는 최대 가치
    vector&lt;vector&lt;int&gt;&gt; dp(n + 1, vector&lt;int&gt;(capacity + 1, 0));

    for (int i = 1; i &lt;= n; ++i) {
        for (int w = 0; w &lt;= capacity; ++w) {
            if (tools[i - 1].weight &lt;= w) {
                dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - tools[i - 1].weight] + tools[i - 1].value);
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }

    return dp[n][capacity];
}

int main() {
    int n,k;
    cin &gt;&gt; n &gt;&gt; k;
    tools.resize(n+1);
    for (int i = 0; i &lt; n; i++)
    {
        int w, v;
        cin &gt;&gt; w &gt;&gt; v;
        Tool t;
        t.initTool(w, v);
        tools[i] = t;
    }
    cout &lt;&lt;knapsackDP(tools, k);
    return 0;
}</code></pre>
<h3 id="backtracking">BackTracking</h3>
<p>백트래킹만해서 풀면 시간복잡도가 최악 2^n 이므로 n이 커질수록 효율적이지 않다. 실제로 백준 문제에서 시간 초과가 떠서 시간복잡도를 줄이기 위해 메모제이션을 사용하여 해결하였다.</p>
<pre><code class="language-cpp">#include&lt;iostream&gt;
#include&lt;vector&gt;
# include&lt;algorithm&gt;
using namespace std;
int n, k;

struct Tool {
    int weight;
    int value;
    int profit;
    void initTool(int w, int v) {
        weight = w;
        value = v;
        profit= v / w;
    }
};
vector &lt;Tool&gt; tools;
int maxValue = 0;
//memo[Index][Weight]는Index 번째 항목까지 고려하고 현재 무게가 cWeight일 때 얻을 수 있는 최대 가치
vector&lt;vector&lt;int&gt;&gt; memo;

int knapsackBacktrack(vector &lt;Tool&gt; items, int n, int capacity, int currentIndex, int currentWeight) {
    if (currentIndex &gt;= n || currentWeight &gt; capacity) {
        return 0;
    }

    if (memo[currentIndex][currentWeight] != -1) {
        return memo[currentIndex][currentWeight];
    }

    int value1 = 0;
    if (currentWeight + items[currentIndex].weight &lt;= capacity) {
        value1 = items[currentIndex].value + knapsackBacktrack(items, n, capacity,
            currentIndex + 1,
            currentWeight + items[currentIndex].weight);
    }

    int value2 = knapsackBacktrack(items, n, capacity, currentIndex + 1, currentWeight);

    memo[currentIndex][currentWeight] = max(value1, value2);
    return memo[currentIndex][currentWeight];
}
int main() {
    cin &gt;&gt; n &gt;&gt; k;
    tools.resize(n+1);
    for (int i = 0; i &lt; n; i++)
    {
        int w, v;
        cin &gt;&gt; w &gt;&gt; v;
        Tool t;
        t.initTool(w, v);
        tools[i] = t;
    }
    memo.assign(n, vector&lt;int&gt;(k+ 1, -1));

    cout &lt;&lt; knapsackBacktrack(tools, n, k, 0,0);
    return 0;
}</code></pre>
<h3 id="branch-and-bound">Branch and Bound</h3>
<p>분기한정 알고리즘은 어떤 마디에서 확장 여부를 결정할 때 
지금까지 구한 답 중에서 가장 좋은 값보다 한계값이 더 좋은 지 검사한다.
BFS 이용-&gt; 더 많은 경우 탐색. 비효율
<strong>코드는 이후 첨부 예정</strong></p>
<blockquote>
<p><a href="https://hi-guten-tag.tistory.com/160">참고링크</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] 뱀서 클론 프로젝트 회고록]]></title>
            <link>https://velog.io/@jeon-yj/Unity-%EB%B1%80%EC%84%9C-%ED%81%B4%EB%A1%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@jeon-yj/Unity-%EB%B1%80%EC%84%9C-%ED%81%B4%EB%A1%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Sun, 20 Aug 2023 13:18:39 GMT</pubDate>
            <description><![CDATA[<p><em>회고록이라 적지만 주저리 주저리 일기장이니 참고 바랍니다.</em>
<img src="https://velog.velcdn.com/images/jeon-yj/post/32432657-dd1b-45ad-a92f-b73a00d01b2c/image.gif" alt=""></p>
<h3 id="프로젝트-비하인드">프로젝트 비하인드</h3>
<p>올해 초 부터 8월 초가 되어야 끝난 기나긴 프로젝트가 막을 내렸다.
전공자이지만 유니티 초면인 6명과 비전공자 1명이 만나 우여곡절이 많았다ㅋㅋ
나를 포함한 개발자 4명과 기획자 3명으로 7명이 참여했고 처음엔  비대면 회의/개발을 하고 막판에는 매주 만나서 같이 코딩했다.(7월 코딩량이 제일 많은건 비밀...)
프로젝트를 통해 배운 것과 직무 내외로 성장한 부분이 많다. 
재미없는 취준생의 삶을 살고 있던 나에게 프로젝트 제안 해준 친구에게 너무 고맙다. </p>
<h3 id="프로젝트-진행-방식">프로젝트 진행 방식</h3>
<ul>
<li>매주 금요일 전체 회의를 통해 한 주 구현한 기능 설명, to do 하나 가져가기.</li>
<li>깃헙 wiki에 기능 문서화 하기.</li>
<li>기획팀이 작성한 기능 명세서에서 질문이 생기면 discussions 올리기</li>
</ul>
<p>일단 기본 틀은 이 세가지였다.. ㅋㅋ 허접했고 빈틈이 많았다. 개발팀 모두 협업을 많이 해보지 않았고, 나 또한 게임 프로젝트는 처음이라 &#39;히히 부딪히면서 배우는거지 뭐~&#39;라고 생각했다.
얼마가지 않아 이는 잘못된 생각임을 깨달았다!(진짜 얼마 안감 1주일 후 바로 깨달음)</p>
<h4 id="잘못된-점">잘못된 점</h4>
<ol>
<li>매주 구현 작업을 배분할 때 선구현되어야 하는 기능을 생각 못함. </li>
<li>기획팀의 기능 명세서 작성 순서가 중구난방이고 상세하지 못함.</li>
<li>팀 간 코드 중복 발생</li>
<li>클래스 기능 중복 발생</li>
</ol>
<p>크게 이 4가지였다. 사실 너무 중요하지만 팀프로젝트를 처음 하면 대부분 실수하는 부분이라 생각한다.
첫 회의 때 클래스 다이어그램을 만들었는데 각자 작성하고, 취합하는 과정도 없었고 이를 토대로 개발을 진행하지도 않았다ㅋㅋ
프로젝트를 끝내고 초반 설계 작업의 중요성을 깨달았고, 기획팀의 중요성도 느끼게 되었다.</p>
<h3 id="성장한-부분">성장한 부분</h3>
<ol>
<li><p><strong>클래스 구조 설계 능력 향상</strong>
기존에 react Component를 다루다보니 게임 Class를 MVC 패턴과 비슷하게 짜게 되었다. 이는 장점도 있고 단점도 있는 부분인거 같다.
클래스 구조를 설계하면서 얼마나 작은 기능을 처리할지, 단일 책임 원칙을 따르면서 불필요한 클래스 생성을 안하기 위해 리팩토링할 때 다들 고생 좀 했다. 
또 인터페이스에 대해 잘 모르는 팀원이 있어서 인터페이스를 만들어서 줬음에도 사용하지 않아 속으로 ㅡㅡ;; 염불을 외웠다.</p>
</li>
<li><p><strong>팀 프로젝트에서의 Git &amp; GitHub 사용 방법 체득</strong>
이제까지 소수의 팀프로젝트만 해보고 React 백-프론트 끼리라던지, 프론트끼리도 페이지를 나눠 개발하기 때문에 충돌날 일이 별로 없었다. 하지만.. 스크립트 파일 충돌은 선녀고 씬파일에서 충돌이 나면 어떻게 수정할 수가 없었다. 씬에서 같은 오브젝트를  수정한 경우 충돌 병합을 위해 파일 수정을 할 수가 없고 커밋 되돌리고 재작업 or 충돌 씬 삭제 후 main꺼 받고 재작업밖에 안되는 상황이 발생하였다. 더 좋은 방법이 있을 수도 있지만 검색도 해보고 현업에 있는 유니티 개발자 선배에게도 자문한 결과 애초에 같은 씬을 안건들여야 한다는 사실을 늦게 깨달았다ㅎㅎ..😥   </p>
<p>이 때문에 풀 리퀘를 올리고 머지하고 병합하고 하는데 기본 1시간이 걸리고 메인 브랜치가 더러워지고 깃 트리가 이리저리 꼬이고~ 이후 main 브랜치 보호 전략이 필요하다는 것을 깨달았다.</p>
</li>
<li><p><strong>페어코딩 및 오프라인 개발의 중요성</strong>
성장한 부분이랄까 필요성을 깨달았다.
실제로 페어를 진행하니 오타, 변수명 등 잔실수가 줄고 막혔을 때 새로운 인사이트를 공유할 수 있었다. 정말 놀랐다. 한 컴퓨터에서 둘이 작업하면 당연히 1의 작업량밖에 못 할거라 생각했는데 2+알파의 작업량을 만들어내는 효과적인 프로그래밍이었다. </p>
</li>
<li><p><strong>개발 문서화</strong>
제일 귀찮지만 제일 중요한 개발 문서화.. 대부분 How-To 가이드와 기술 레퍼런스 형식으로 작성하였다.
위 잘못 된점의 3번이 문서화의 빈약함 때문이었다. 한 팀원은 몰아서 문서화를 하였고 이 때문에 구현한 클래스 사용방법을 모르니 재사용을 못하고 연락하고 답변받고,, 중복코드 생기고 의도와 맞지 않게 사용되고,, 
그 팀원이 반문교사가 되어줬다^^ 곰아어<del>🤪 그래도 막판엔 제일 열심히 작성했고 코드 해설을 잘해주어 도움이 되었다. 다들 10</del>20개 정도 작성했다. 굳굳👍</p>
</li>
</ol>
<h3 id="회고록을-마치며">회고록을 마치며</h3>
<p>뭘 더 적어야할지 모르겠어서 주저리 하고 싶은 말을 적었는데 남에게도 이후 나에게도 도움이 될진 모르겠다ㅋㅋ
그래도 고생한 팀원들과 나에게 수고했다는 말을 하고싶어서 적어본다.</p>
<blockquote>
<p><a href="https://github.com/SeoulTechTCPGame/HumanSurvival">프로젝트 깃허브 링크</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스]양과늑대 C++]]></title>
            <link>https://velog.io/@jeon-yj/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%96%91%EA%B3%BC%EB%8A%91%EB%8C%80-C</link>
            <guid>https://velog.io/@jeon-yj/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%96%91%EA%B3%BC%EB%8A%91%EB%8C%80-C</guid>
            <pubDate>Sun, 20 Aug 2023 11:52:59 GMT</pubDate>
            <description><![CDATA[<h3 id="문제링크"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/92343">문제링크</a></h3>
<h3 id="내-문제-풀이-빌드업">내 문제 풀이 빌드업</h3>
<ol>
<li>노드개수&lt;=17. 완탐 가능한가?</li>
<li>BFS, DFS 로 백트래킹하자.</li>
<li>노드를 재방문할 때는 양/늑대 수 변화 없음</li>
</ol>
<p>-&gt; 그럼 왜 재방문? 그 노드와 연결된 자식 노드에 가려고
-&gt; 그럼 방문 가능한 노드를 따로 저장해서 순회하자.</p>
<h3 id="첫-풀이틀림">첫 풀이(틀림)</h3>
<details>
<summary>코드 보기</summary> 

<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include&lt;algorithm&gt;
#include&lt;list&gt;
using namespace std;

vector &lt;vector&lt;int&gt;&gt; adj;
vector &lt;int&gt; infos;
int maxSheep=0;
// 현재 노드, 양의 수 , 늑대의 수,남아있는 양 수, 방문여부 불린 벡터,현재 접근 가능한 노드 리스트
void dfs(int curr,int s,int w,int remainSheep,vector &lt;bool&gt; visited, list &lt;int&gt; canGo) {
    maxSheep = max(s, maxSheep);
    if (remainSheep == 0||s&lt;=w) return;

    for (auto i : adj[curr]) {
        canGo.push_back(i);
    }
    for (list&lt;int&gt;::iterator next = canGo.begin(); next != canGo.end();++next) {

        if (!visited[*next]){
            if (infos[*next] == 0) { 
                visited[*next] = true;
                canGo.erase(next);
                dfs(*next, s + 1, w, remainSheep - 1,visited,canGo); 
            }
            else if (infos[*next] == 1 &amp;&amp; s &gt; w + 1) {
                visited[*next] = true;
                canGo.erase(next);
                dfs(*next, s, w + 1, remainSheep,visited, canGo);
            }
        }
    }
    return;
}
int solution(vector&lt;int&gt; info, vector&lt;vector&lt;int&gt;&gt; edges) {
    int answer = 0;
    int sheeps = 0;
    adj.resize(info.size());
    vector &lt;bool&gt; visited;
    list &lt;int&gt; cango;
    visited.resize(info.size(),false);

    infos = info;
    for (auto i:info)
    {
        if (i == 0)sheeps++;
    }
    for (auto i : edges) {
        adj[i[0]].push_back(i[1]);
    }
    for (auto i : adj[0]) {
        cango.push_back(i);
    }
    dfs(0,1,0,sheeps-1,visited,cango);
    return maxSheep;
}</code></pre>
</details>

<h3 id="시행착오">시행착오</h3>
<ol>
<li>방문가능한 노드 리스트인 CanGo에서 방문한 현재 노드를 빼줄때 틀림. 
List는 BidirectionalIterator이므로 순차적으로 접근한다.
erase()로 해당 원소가 삭제되면 다음 위치를 잃어버린다. 
따라서 Iterator=List.erase(Iterator)로 리턴값을 사용해야한다.</li>
<li>완탐문제에서 배열이나 리스트의 자료형태의 값을 독립적으로 바꾸는 작업을 수행 할 때는, 무조건 복제를 하여 독립적 리스트로써 사용하도록 주의해야한다.</li>
<li>visit 배열은 필요 없다. 어차피 자식노드만 살피고 cango에 추가하여 순회하기 때문에.<h3 id="정답-풀이">정답 풀이</h3>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include&lt;algorithm&gt;
#include&lt;list&gt;
using namespace std;
</code></pre>
</li>
</ol>
<p>vector &lt;vector<int>&gt; adj;
vector <int> infos;
int maxSheep=0;
// 현재 노드, 양의 수 , 늑대의 수,남아있는 양 수, 현재 접근 가능한 노드 리스트
void dfs(int curr,int s,int w,int remainSheep, list <int> canGo) {
    if (infos[curr] == 1) {
        w++;
        if (s &lt;= w)return;
    }
    else {
        s++;
        remainSheep--;
        maxSheep = max(s, maxSheep);
        // 트리에 양이 남아있지 않으면 그 이후 노드를 볼 필요가 없다.
        if(remainSheep==0)return;
    }
    list <int> newCanGo(canGo);</p>
<pre><code>for (auto i : adj[curr]) 
    newCanGo.emplace_back(i);
 newCanGo.remove(curr);
 for (auto next : newCanGo)
     dfs(next, s, w, remainSheep, newCanGo);
return;</code></pre><p>}
int solution(vector<int> info, vector&lt;vector<int>&gt; edges) {
    int answer = 0;
    int sheeps = 0;
    adj.resize(info.size());
    list <int> cango;</p>
<pre><code>infos = info;
for (auto i:info)
{
    if (i == 0)sheeps++;
}
for (auto i : edges) {
    adj[i[0]].push_back(i[1]);
}
dfs(0,0,0,sheeps,cango);
return maxSheep;</code></pre><p>}</p>
<p>  ```
  <img src="https://velog.velcdn.com/images/jeon-yj/post/81331fd0-156e-40c6-8f11-034b38790b13/image.png" alt=""></p>
<h3 id="다른-사람들의-해결-방법">다른 사람들의 해결 방법</h3>
<ul>
<li><p>양방향 그래프 </p>
</li>
<li><blockquote>
<p>불필요한 무한 재귀호출 가능성 有</p>
</blockquote>
</li>
<li><p>삼차원 visited 배열 [현재 노드,양의수, 늑대의 수].
visit[A][B][C] = true : A번 노드에 양을 B마리 늑대를 C마리를 데리고 방문한 적이 있다는 뜻. <a href="https://yabmoons.tistory.com/727">참고링크</a></p>
</li>
<li><blockquote>
<p>직접 해보진않았지만 우연히 겹치는 같은 양의수,늑대의 수 경우가 있으면 우짜지?</p>
</blockquote>
<ul>
<li>비슷한 풀이로 비트마스킹을 이용하여 방문 상태 저장
01789 정점 방문 시 1110000011(2)=899로 나타내어 visited[899]=true. 즉 2^n개 의 상태로 관리.</li>
<li><blockquote>
<p>PS할때 공간 복잡도 계산을 잘 안해서 이 경우 되는지 가늠이 잘 안간다ㅜ.</p>
</blockquote>
</li>
</ul>
</li>
<li><p>갈 수 있는 정점을 따로 담아서 재귀호출
  -&gt;내가 푼 방법</p>
</li>
<li><p>dp로 n마리 모은 경로를 바탕으로 n+1 마리를 모을 수 있는 경우 구하기 반복 <a href="https://github.com/Juniork725/coding_test/blob/main/%EB%82%9C%EC%9D%B4%EB%8F%843/%EC%96%91%EA%B3%BC%20%EB%8A%91%EB%8C%80.md">참고링크</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준]내려가기2096 C++]]></title>
            <link>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%EB%82%B4%EB%A0%A4%EA%B0%80%EA%B8%B02096-C</link>
            <guid>https://velog.io/@jeon-yj/%EB%B0%B1%EC%A4%80%EB%82%B4%EB%A0%A4%EA%B0%80%EA%B8%B02096-C</guid>
            <pubDate>Tue, 08 Aug 2023 15:56:26 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-링크"><a href="https://www.acmicpc.net/problem/2096">문제 링크</a></h3>
<h3 id="생각해야할-부분">생각해야할 부분</h3>
<ol>
<li>메모리 제한이 4MB이다.
보통 문제 풀때 시간 제한만 보고 알고리즘을 구상하다보니 첫 제출에서 메모리 초과가 났다. ㅜ.ㅜ
dp 문제라 메모라이즈하려는 생각에 간과한 메모리초과라고 할 수 있다.
첫 풀이에서는 vector을 써서 n만큼 입력 벡터와 dp 벡터를 할당해 주었다. 사실 이도 십만 x 3 x 2 x 4바이트(int)라 얼추 가능할거라 생각했다.</li>
</ol>
<p><strong>But</strong> c++ 자체에서 1-2MB 사용하고 iostream 사용시 기본 2MB 사용한다고 한다.. 그러면 절대 안되지..
알고나면 간단한 문제이고 더 좋은 코드를 쓰기 위해 중요한 부분인거 같다. </p>
<h3 id="정답-풀이">정답 풀이</h3>
<pre><code class="language-cpp">#include&lt;iostream&gt;
#include&lt;algorithm&gt;
using namespace std;

pair&lt;int, int&gt; dp[3][3];
int n;
void getValue(int []);
int main() {
    cin &gt;&gt; n;
    int arr[3];
    for (int i = 0; i &lt; n; i++)
    {
        for (int j = 0; j &lt; 3; j++)
        {
            int temp;
            cin &gt;&gt; temp;
            arr[j] = temp;
        }
        if (i == 0) {
            dp[0][0]=make_pair(arr[0], arr[0]);
            dp[0][1]=make_pair(arr[1], arr[1]);
            dp[0][2]=make_pair(arr[2], arr[2]);
        }else getValue(arr);
    }
    cout &lt;&lt; max({ dp[0][0].second, dp[0][1].second, dp[0][2].second }) &lt;&lt; &quot; &quot; &lt;&lt; min({ dp[0][0].first, dp[0][1].first, dp[0][2].first });
    return 0;
}
void getValue(int a[]) {
    //order번째 인덱스 체크중
    int minV;
    int maxV;

    //case 0: //0 or 1갈 수 있다.
    minV= min(dp[0][0].first, dp[0][1].first)+a[0];
    maxV= max(dp[0][0].second, dp[0][1].second)+a[0];
    dp[1][0]=make_pair(minV, maxV);
    //case 1: //0 or 1 or 2갈 수 있다.
    minV = min({ dp[0][0].first, dp[0][1].first ,dp[0][2].first})+a[1];
    maxV= max({ dp[0][0].second, dp[0][1].second ,dp[0][2].second })+a[1];
    dp[1][1]=make_pair(minV, maxV);
    //case 2: //1 or 2갈 수 있다.
    minV= min(dp[0][1].first, dp[0][2].first)+a[2];
    maxV= max(dp[0][1].second, dp[0][2].second)+a[2];
    dp[1][2]=make_pair(minV, maxV);
    swap(dp[0], dp[1]);
}
/*
n= 십만
dp[n번째 줄][i번째 칸]=[최소,최대]=dp[n-1][가능한 i중] &lt;min,max&gt;
메모리 제한이 4MB로 그냥 n으로 만들어서 구현하면 초과가 난다.

*/</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 표 병합 JS]]></title>
            <link>https://velog.io/@jeon-yj/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%91%9C-%EB%B3%91%ED%95%A9-JS</link>
            <guid>https://velog.io/@jeon-yj/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%91%9C-%EB%B3%91%ED%95%A9-JS</guid>
            <pubDate>Tue, 08 Aug 2023 13:24:48 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-링크"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/150366">문제 링크</a></h3>
<h3 id="첫-풀이틀림">첫 풀이(틀림)</h3>
<pre><code class="language-js">function solution(commands) {
    var answer = [];
    let parent = Array.from({ length: 51}, (e, i) =&gt; Array.from({ length: 51 }, (e2, i2) =&gt; [i, i2]))
    //50*50 배열
    let arr = Array.from({ length: 51 }, e =&gt; Array(51).fill(null))

    const findParent = (x, y) =&gt; {
        if (parent[x][y][0] == x &amp;&amp; parent[x][y][1] == y) return [x, y]
        return parent[x][y] = findParent(parent[x][y][0], parent[x][y][1])
    }
    const union = (x1, y1, x2, y2) =&gt; {
        if (x1 === x2 &amp;&amp; y1 === y2) return
        let parentA = findParent(x1, y1)
        let parentB = findParent(x2, y2)
        if (parentA[0]=== parentB[0]&amp;&amp;parentA[1]=== parentB[1]) return
        const value = arr[parentA[0]][parentA[1]] !== null ? arr[parentA[0]][parentA[1]] : arr[parentB[0]][parentB[1]];
        for (let i = 1; i &lt; 51; i++) {
            for (let j = 1; j &lt; 51; j++) {
                let temp = findParent(i, j)
                if (parentB[0] === temp[0] &amp;&amp; parentB[1] === temp[1]) {
                    parent[i][j] = parentA
                    arr[i][j] = value
                }
            }
        }
    }
    commands.forEach(c =&gt; {
        let command = c.split(&quot; &quot;)
        //5가지 명령어 구분하기
        if (command[0] === &quot;UPDATE&quot; &amp;&amp; command.length === 4) {
            //그룹 확인=&gt;부모의 값만 갱신
            let p = findParent(Number(command[1]), Number(command[2]))
            arr[p[0]][p[1]] = command[3]
        } else if (command[0] === &quot;UPDATE&quot; &amp;&amp; command.length === 3) {
            for (let i = 1; i &lt; 51; i++) {
                for (let j = 1; j &lt; 51; j++) {
                    if (arr[i][j] === Number(command[1])) arr[i][j] = Number(command[2])
                }
            }
        } else if (command[0] === &quot;MERGE&quot; &amp;&amp; command.length === 5) {
            //그룹의 부모들도 갱신해야힘(unMerge때문에)
            union(Number(command[1]), Number(command[2]), Number(command[3]), Number(command[4]))
        } else if (command[0] === &quot;UNMERGE&quot; &amp;&amp; command.length === 3) {
            //해당 위치의 부모값을 갖고 있는 노드들 구하기
            let p = findParent(Number(command[1]), Number(command[2]))
            let value = arr[p[0]][p[1]]
            for (let i = 1; i &lt; 51; i++) {
                for (let j = 1; j &lt; 51; j++) {
                    let temp = findParent(i, j)
                    if (p[0] === temp[0] &amp;&amp; p[1] === temp[1]) {//부모가 같으니끊어내기
                        parent[i][j] = [i, j]
                        arr[i][j] = null
                    }
                }
            }
            arr[Number(command[1])][Number(command[2])] = value
        } else if (command[0] === &quot;PRINT&quot; &amp;&amp; command.length === 3) {
            let p = findParent(Number(command[1]), Number(command[2]))
            arr[p[0]][p[1]] == null ? answer.push(&quot;EMPTY&quot;) : answer.push(arr[p[0]][p[1]])
        }
    })
    return answer;
}</code></pre>
<h3 id="실행-결과">실행 결과</h3>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/e0b3cff2-84cb-4d64-a5bd-857a903b73fb/image.png" alt=""></p>
<h3 id="시행착오">시행착오</h3>
<ol>
<li><p>union할때 값이 없는 애가 처음 인자일 수 있다.
 역시 제일 중요한건 주어진 문제를 온전히 이해하기!!</p>
</li>
<li><p>unmerge는 for문으로 순서대로 하기 때문에 union에서 자식의 부모값도 최상위 부모로 바꿔주지 않으면 타고 들어갈 수 없다.</p>
</li>
<li><p>B가 C의 부모인 상태에서 A와 C를 병합하려고 하면 B와 C의 부모를 모두 업데이트해야한다. C의 부모만 A로 수정하면 문제 발생.</p>
</li>
<li><p>셀을 class화 해서 solution의 함수들을 빼줘야한다.
 클래스와 재활용 할 수 있는 코드 함수화를 습관화 하자!</p>
</li>
</ol>
<h3 id="정답-풀이">정답 풀이</h3>
<p>class Node는 배열의 값이므로 몇번째 인덱스인지 식별하지 않아도 된다.
값과 부모/자식 배열만 있으면 된다.
merge는 앞에 있는 값이 n2 이다. </p>
<pre><code class="language-js">
const Cell = class {
    constructor() {
        this.parent = null
        this.child = []
        this.value = null;
    }
    findParent() {
        if (this.parent) return this.parent
        return this
    }
    getValue() {
        return this.findParent().value
    }

    updateValue(newValue) {
        this.findParent().value = newValue
    }
    merge(n2) {
        let parent2 = n2.findParent()
        let myParent = this.findParent()
        if (parent2 === myParent) return
        myParent.child.forEach(c =&gt; {
            c.parent = parent2
            parent2.child.push(c)
        })
        // parent2에 소속됐으니 자식 초기화
        myParent.child = []
        myParent.parent = parent2
        parent2.child.push(myParent)
        if (parent2.value === null) parent2.value = myParent.value
        myParent.value = null
    }
    unmerge() {
        let p = this.findParent()
        let v = this.getValue()
        p.child.forEach(c =&gt; {
            c.parent = null
            c.value = null
        })
        p.value = null
        p.child = []
        p.parent = null
        this.value = v
    }
}
function solution(commands) {
    let answer = []
    let arr = Array.from({ length: 51 }, e =&gt; Array.from({ length: 51 }, e =&gt; new Cell()))
    commands.forEach(c =&gt; {
        let command = c.split(&quot; &quot;)
        //5가지 명령어 구분하기
        if (command[0] === &quot;UPDATE&quot; &amp;&amp; command.length === 4) {
            arr[Number(command[1])][Number(command[2])].updateValue(command[3])
        } else if (command[0] === &quot;UPDATE&quot; &amp;&amp; command.length === 3) {
            for (let i = 1; i &lt; 51; i++) {
                for (let j = 1; j &lt; 51; j++) {
                    if (arr[i][j].value === command[1]) arr[i][j].updateValue(command[2])
                }
            }
        } else if (command[0] === &quot;MERGE&quot;) {
            if (Number(command[1]) !== Number(command[3]) || Number(command[2]) !== Number(command[4])) {
                const n2 = arr[Number(command[1])][Number(command[2])]
                arr[Number(command[3])][Number(command[4])].merge(n2)
            }
        } else if (command[0] === &quot;UNMERGE&quot;) {
            arr[Number(command[1])][Number(command[2])].unmerge()
        } else if (command[0] === &quot;PRINT&quot;) {
            arr[Number(command[1])][Number(command[2])].getValue() == null ? answer.push(&quot;EMPTY&quot;) : answer.push(arr[Number(command[1])][Number(command[2])].getValue())
        }
    })
    return answer
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jeon-yj/post/8bbbb7b0-65d0-411b-9e93-b97be4602a07/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] Modal 구현]]></title>
            <link>https://velog.io/@jeon-yj/react-%EB%AA%A8%EB%8B%AC</link>
            <guid>https://velog.io/@jeon-yj/react-%EB%AA%A8%EB%8B%AC</guid>
            <pubDate>Mon, 22 May 2023 11:51:42 GMT</pubDate>
            <description><![CDATA[<p>어떤 방식으로 react 모달을 구현할것인가 고민중..</p>
<ul>
<li><p>단순 state </p>
</li>
<li><p>react potal 
<a href="https://velog.io/@velopert/react-portals">리액트 - Portals 를 통한 부모 컴포넌트의 외부 DOM 에 컴포넌트 렌더링하기</a></p>
</li>
</ul>
<p>이런 방법도 있다
<a href="https://siosio3103.medium.com/react%EB%82%B4%EC%97%90%EC%84%9C-modal%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%A0%EB%95%8C-%EC%A2%80-%EB%8D%94-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%A0-%EB%B0%A9%EB%B2%95-useimperativehandle-101bc1dde8e1">useImperativeHandle로 효율적인 modal구현하기 </a></p>
<p>.. 라고 임시 글을 쓰고 몇달 후
컴포넌트마다 Modal open,close state 로직을 반복시키는게 너무 귀찮아서 상태 공유를 할 수 있는 방법을 찾아봤다. </p>
<p>좋은 글을 발견하여 이 방법으로 프로젝트 내의 modal을 적용해보려 한다. 
<a href="https://velog.io/@arara90/react-Modal-Modal-Modal">arara90님 의 글</a></p>
<p>나와 같은 고민을 하셨고 redux로 시도할까 고민한 부분도 같아서 도움이 많이 되었다. </p>
<h3 id="방법은-custom-modal-hook--context-api">방법은 Custom Modal Hook &amp; Context API</h3>
<p><a href="https://dev.to/alexandprivate/your-next-react-modal-with-your-own-usemodal-hook-context-api-3jg7">Your next React Modal with your own &quot;useModal&quot; Hook &amp; Context API- 영어 참고 글</a>
Context API로 Modal 컴포넌트를 저장하는 방법이다. 하지만 컴포넌트를 state로 저장하는 것이 권장되지 않고 이 방법은 빠르게 해결하는 방법일뿐이라고한다~</p>
<h3 id="최종-참고">최종 참고</h3>
<p><a href="https://nakta.dev/how-to-manage-modals-1">https://nakta.dev/how-to-manage-modals-1</a></p>
]]></description>
        </item>
    </channel>
</rss>