<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>young_pallete.log</title>
        <link>https://velog.io/</link>
        <description>People are scared of falling to the bottom but born from there. What they've lost is nth. 😉</description>
        <lastBuildDate>Tue, 07 May 2024 00:49:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>young_pallete.log</title>
            <url>https://images.velog.io/images/young_pallete/profile/17df04be-cd4b-4166-a8fb-f18fb0e3c4e1/348735caea8cf079ea4be012c31da6e4 (1).jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. young_pallete.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/young_pallete" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[기술블로그 개발기] 2. 블로그 개발 목표 및 기획]]></title>
            <link>https://velog.io/@young_pallete/%EA%B8%B0%EC%88%A0%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0-2.-MVP-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EA%B8%B0%ED%9A%8D</link>
            <guid>https://velog.io/@young_pallete/%EA%B8%B0%EC%88%A0%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0-2.-MVP-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EA%B8%B0%ED%9A%8D</guid>
            <pubDate>Tue, 07 May 2024 00:49:27 GMT</pubDate>
            <description><![CDATA[<h1 id="기술블로그-목표-및-기획">기술블로그 목표 및 기획</h1>
<h2 id="서론">서론</h2>
<p>본격적으로 일단 기술블로그를 어떠한 방식으로 운영하면 좋은지에 대해 고민해보고자 한다.
(velog에서 새로운 기술블로그를 개발한다는 이야기를 하니, 꽤 머쓱하기도 하다 🤣)</p>
<p>요새 항상 다른 사람들의 글들을 많이 참고하는 편인데, 가장 와닿았던 말은 다음과 같다.</p>
<blockquote>
<p><strong>현재 우리가 좋다고 생각하는 서비스는 이전의 서비스들의 단점들을 합산한 것이다.</strong></p>
</blockquote>
<p>따라서 이번 기획은, 내가 블로그를 쓰면서, 느꼈던 장점과 단점들을 어떻게 develop할 건지에 초점을 맞추고자 한다.</p>
<h2 id="본론">본론</h2>
<h3 id="velog에서-좋다고-생각했던-점들">velog에서 좋다고 생각했던 점들</h3>
<h4 id="장점-1-시리즈">장점 1. 시리즈</h4>
<p>나는 여러 콘텐츠들을 볼 때, 시리즈 물들을 좋아한다.
어려운 고민은 여러 번 생각을 되뇌어야 실마리를 풀어낼 수 있듯, 시리즈 형식의 콘텐츠는 누군가의 무거운 고민을 양과 질이 높은 방식으로 해결했다고 생각하기 때문이다.</p>
<p>velog는 이러한 사람들의 니즈를 충족하는 시리즈 기능이 있다.
이는 그대로 살릴 것 같다.</p>
<h4 id="장점-2-목차">장점 2. 목차</h4>
<p>처음에 velog를 볼 때 가장 마음에 들었던 부분이다.
목차를 통해 간략하게 이 콘텐츠가 이야기를 풀어나가는 부분들을 알 수 있고, 빠르게 필요한 부분을 살펴나갈 수 있다.</p>
<p>아마 개발을 할 때에도 이러한 빠르게 훑어볼 수 있는 기능은 중점으로 풀어내지 않을까.</p>
<h3 id="단점">단점</h3>
<h4 id="단점-1-seo">단점 1. SEO</h4>
<p>나의 최근 가장 많은 관심사는 <code>SEO</code>였다.
기본적으로 개발자는 비즈니스를 생각해야 한다고 생각하는 편이다. 
우리가 개발하는 이유는, 궁극적으로 개발을 통해 비즈니스 가치를 높이기 위함이라 생각하기 때문이다.</p>
<p>그런 관점에서, <code>SEO</code>를 많이 관심 가지기도 했고, 최근에는 회사에서 <code>SEO</code> 프로젝트를 총괄하고 있기도 해선지 요새 이쪽에 눈을 많이 들이고 있다.</p>
<p>그런데 아쉽게도 내가 애정하며 사용하는 velog에서는 이러한 부분이 좀 아쉽지 않았나 싶다.</p>
<p>예컨대 다음과 같다.</p>
<h5 id="1-1-h1에-대한-제어-미비">1-1. <code>h1</code>에 대한 제어 미비</h5>
<p>논리적으로 한 문서 당 <code>h1</code>은 하나만 존재해야 한다. 
(* 실제로 이를 지키지 않아도 보이는 것에서 별다른 문제는 없지만, 논리적으로는 그렇다)</p>
<blockquote>
<p>나 역시 SEO를 잘 몰랐던 당시, <code>h1</code>을 남발하면서 포스트를 쓰는 바람에(...), 
조만간 이전에 썼던 글들의 내용을 수정할 겸, 모두 다 업데이트할 예정이다.</p>
</blockquote>
<p>더군다나 <code>h</code> 태그의 크기가 과연 온전히 콘텐츠들에 대한 계층적 배치를 다 담을 수 있을까 싶다.
현재 velog의 <code>h</code> 태그는 헤딩의 계층 크기에 따라 폰트 크기도 커지는 구조인데, <code>h5</code>부터는 기본 텍스트보다 작아진다. 좀 더 세부적인 계층 부분 역시 잘 컨트롤 할 수 있는 방법을 생각해야겠다.</p>
<h5 id="1-2-일부-시멘틱-태그에-대한-기능이-약하다">1-2. 일부 시멘틱 태그에 대한 기능이 약하다.</h5>
<p>간단히 말하자면, 검색엔진마다 좋아하는 콘텐츠 양식과 태그들이 분명 존재한다. 이에 반해 velog는 특정 키워드 검색 우선순위를 높이기 위한 시멘틱 태그 (<code>table</code>) 등에 대한 지원이 약하다. 이에 관해서는 나중에 별도로 콘텐츠를 만들까 생각 중이다.</p>
<h4 id="단점-2-패키지-버전">단점 2. 패키지 버전</h4>
<p>나는 글을 볼 때, 패키지 버전을 중요시한다. 이슈트래킹을 할 때 패키지가 어떤 버전에서 있는지에 따라서 이 글을 보아야 하나 말아야 하나를 결정하는 편이기 때문이다.</p>
<p>여태까지 수많은 글들을 보았지만, 패키지 버전의 경우 잘 써준 글들을 보면 정말 고마웠지만, 실제로 많은 글들은 쓰여져 있지 않다. 이럴 때에는 대충 몇 년도에 써진 글인지 보고 &#39;아, 이때 쓰여진 글이겠구나&#39; 추측한다.</p>
<p>이왕이면, 버전까지 기술하는 것이 좋은 콘텐츠이지 않을까 고민하면서, 이 역시 추가해보려 한다. 방식은 아직 고민 중이다.</p>
<h4 id="단점-3-콘텐츠-수정-시점을-알-수-없음">단점 3. 콘텐츠 수정 시점을 알 수 없음</h4>
<p>우리가 접하는 많은 블로그들은 웬만한 글들 모두 어떤 버전에서 썼는지, 어떻게 수정이 일어났는지, 왜 수정을 했는지에 대한 이유를 알 수 없다.</p>
<p>즉, 사용자에게는 편하지만, 독자에게는 불편한 UX로 전개되어 있다고 생각한다.
하지만, 과연 사용자에게도 편할지는 의문이다. 내 변경에 관한 history가 전혀 제어되지 않기 때문이다.</p>
<p>약간 <code>confluence</code>와 같이, 왜 업데이트를 진행했는지 등을 정리할 수 있도록 하고 싶다.</p>
<h4 id="단점-4-세부-커스터마이징에-대한-고민">단점 4. 세부 커스터마이징에 대한 고민</h4>
<p>아직 이는 내가 많이 찾지 못해서일 수도 있지만, 세부적인 설정들을 내가 직접 하지 못하고 시스템에 의존해야 한다는 점 역시 다소 아쉽다. </p>
<p>물론 너무나 간편하고, 좋은 블로그 시스템이기는 하지만, 나는 좀 더 욕심이 드는 게 사실이다.
콘텐츠를 발행했으면 RSS를 통해 구독자들에게 뿌린다던지, 메타 태그에 대한 세부적인 설정이라던지.
좀 더 시스템에 의존하지 않고, 내 의도에 맞게 콘텐츠를 만들고 싶다.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/aa76162d-620f-42ed-9f61-05661e3b9889/image.png" alt="wrong meta description example"></p>
<p>예컨대 velog의 경우 초기 콘텐츠 몇 자 이상까지는 절대 마크다운 형식을 쓰지 않아야 하는 시스템이다. 해당 콘텐츠가 날 것 그대로 description에 반영되기 때문이다.</p>
<p>결국 이런 것들에 대한 하나하나의 러닝커브를 따지다 보면, 만드는 것과 비슷할 것 같아서 제작해보려 한다.</p>
<h2 id="주요기능">주요기능</h2>
<p>따라서 블로그를 개발할 시 반드시 있어야 할, 최소 기능은 다음과 같이 설정하려 한다.</p>
<ol>
<li>(당연하게도) 블로그 목록 / 상세 조회 기능</li>
<li>블로그 포스트 시리즈 조회 기능</li>
<li>목차를 통한 탐색 기능</li>
<li>작성된 기술 포스트의 패키지 버전 열거 기능</li>
<li>업데이트 이력 및 변경 내역 조회 기능</li>
<li>표준에 맞는, 시멘틱한 레이아웃 구성 및 RSS 피드 자동 업데이트 등 강력한 SEO 지원</li>
</ol>
<blockquote>
<p>일단 두루뭉실하게 기능을 작성해보기는 했는데, 이는 함께 할 팀원 분들과 좀 더 이야기해보아야겠다. 사이드 프로젝트와 더불어 병행할 거라, 5월은 여러모로 바쁜 달이 될 것 같다. 🙇🏻‍♂️</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기술블로그 개발기] 1. 개발 배경과 계획]]></title>
            <link>https://velog.io/@young_pallete/tech-blog</link>
            <guid>https://velog.io/@young_pallete/tech-blog</guid>
            <pubDate>Sun, 21 Apr 2024 13:14:59 GMT</pubDate>
            <description><![CDATA[<h1 id="기술블로그-개발-배경과-계획">기술블로그 개발 배경과 계획</h1>
<h2 id="배경">배경</h2>
<p>시작은 어느 날, 팀원분들과 소소히 이야기하던 중 나온 가벼운 한 마디였다.</p>
<blockquote>
<p><strong>&quot;우리도 기술블로그를 만들어보면 어떨까요?&quot;</strong></p>
</blockquote>
<p>나는 갈증이 있었다.</p>
<ul>
<li>성장을 하고 싶었고</li>
<li>장기적으로 내가 얼마나 성장했는지 돌아보고자 글로 저장하고 싶었으며</li>
<li>내가 올바르게 고민하고 해결했는지, 다른 개발자들의 이야기를 듣고 싶었다.</li>
</ul>
<p>다만 이런 일들이 생각보다 쉽지는 않다.</p>
<p>현재 회사에서 진행 중인 프로젝트를 동시 다발적으로 이끌기도 쉽지 않았고, 
별도로 진행 중인 사이드 프로젝트 역시 TODO가 쌓이다 보니 점차 피로도가 증가했다.
결과적으로 어느정도 번아웃이 온 것 같다.</p>
<p>그렇기에 작년에 들었던 저 말에 대하여 결정을 내리기까지 반 년의 시간이 걸렸고, 결국 보람차게 개발하기 위해 하기로 결심했다.</p>
<h3 id="가장-결정적이었던-계기-번아웃">가장 결정적이었던 계기, 번아웃</h3>
<blockquote>
<p>먼저 나 자신에게 묻는다.
일단 휴식을 하지 못하는가?</p>
</blockquote>
<p>그렇지는 않다. 번아웃을 느낀 최근에는 심지어 매일 1~2시간씩 운동을 하던, 누워서 쉬던 나 자신의 기분을 먼저 보살피기 위해 노력하고 있다.</p>
<p>그럼에도 불구하고, 원인 모를 결핍이 느껴지고, 이에 대한 압박감만 커지는 느낌이다.
내면에서 끊임없이 압박을 주니, 언제 이 스트레스가 팽창할지 몰라 두려운 느낌도 있었다.</p>
<p>그리고 외부의 환경은 이러한 생각을 떨칠 수 없었기에, 결국 삭히다가 점차 해결 불가능한 상태에 이르렀던 것 같다.</p>
<h3 id="몰입하는-삶을-갖자">몰입하는 삶을 갖자</h3>
<p>그렇게 원인 모를 피로감에 지쳐있던 중, 여느 때처럼 무던하게 지하철에서 유튜브를 보다 뇌과학 영상을 보게 됐다.
영상에서는 이런 말이 나왔다.</p>
<blockquote>
<p>톨스토이가 말하기를 시시각각으로 죽음을 의식하는 것과 하지 않는 삶은 완전히 다르다. 
전자는 신의 상태에 가깝고 후자는 동물의 상태에 가깝다.
자극을 쫓는 몰입을 하지 말고, 스스로에게 자극을 주고 주어진 일에 몰입하는 삶을 가져라.</p>
</blockquote>
<p>회사를 가는 길 내내 저 영상을 보며 멍했던 것 같다.
반복된 개발 사이클을 이어가며, 스스로에게 지쳐있던 나는 <code>죽은 개발자로서의 삶</code>을 사는 것이 아닐까. 하고 생각했던 것 같고, 어느정도 수긍했다. 요새는 개발에도 몰입하지 못하는 것 같아서다.</p>
<p>그나마 다행이었던 것은, 수동적인 죽음보다는 발버둥을 치고 싶은 삶의 의지가 있다는 것이었다.
살기 위해 어떻게 몰입할지를 생각해야만 했고, 그러던 중 글쓰기를 진짜 해야겠다고 결심했다.</p>
<blockquote>
<p>개발 일을 하면서 글을 쓰면서 정리하며 개발했던 때가 가장 행복했던 것 같아서. 
그리고 가장 미친듯이 문제에 몰입했던 것 같아서다.</p>
</blockquote>
<p>다만 시작에 있어, 내가 왜 글을 쓰지 못했는가에 대해 곰곰이 일주일 간 생각했다. 
그리고 이에 대한 여러 가지 이유를 어느정도 정리했고, 따라서 기술 블로그를 만들어 이전하지 않을까, 생각하고 있다.</p>
<blockquote>
<p>그렇다고 벨로그를 정리하지는 않을 것 같다.
간단한 서술은 벨로그에서, 자세한 서술은 이전할 블로그에서 서술하지 않을까.</p>
</blockquote>
<h2 id="계획">계획</h2>
<h3 id="일단-틈틈이-초안을-작성할-계획이다">일단 틈틈이 초안을 작성할 계획이다.</h3>
<p>사실 제목이 그렇듯, 나는 기술블로그를 개발할 것이다. 이것은 결정된 사안이며, 당장이라도 개발할 의지가 있다.</p>
<p>다만, 아직 기술블로그 개발을 어떤 식으로 구성할지를 팀원들과 논의해야 한다.
개발이 물론 진행되지도 않았고 말이다.</p>
<p>특히 SEO적인 측면에서 보았을 때, 과연 어떻게 글을 기고하는 것이 좋을지 정말 많은 생각을 하고 있다.</p>
<blockquote>
<p>헤맨만큼 자기 땅이라던가. 
내게 가장 힘들었던 SEO 최적화가 이렇게 나한테 개발적으로 많은 영감을 줄지 몰랐다.</p>
</blockquote>
<p>따라서 일단 벨로그에 틈틈이 기고할 것 같다.</p>
<ul>
<li>그냥 크고 작은지를 논하기보다는</li>
<li>문제의 질에 초점을 맞추기보다는</li>
<li>내가 어떻게 이 문제를 생각했고</li>
<li>어떤 과정으로 해답을 찾아나갔는지. </li>
</ul>
<p>그런 예전의 초심으로 돌아가기 위한 그런 소소한 글을 쓰지 않을까.
예전에는 의식하기 바빴던 글이라면, 이제는 개발에 몰두하며 보람차게 살기 위해 글을 쓰지 않을까 싶다.</p>
<h3 id="팀원들과-함께-개발할-계획이다">팀원들과 함께 개발할 계획이다.</h3>
<p>요새 이러한 블로그 이야기를 회사에서 자주 한다.
다행히도 여전히 팀원들은 함께 기술 블로그를 만들고 싶어했다.
이미 2주 간 서로의 상황에 대한 공유를 진행했고, 차후 어떻게 진행할지 간단한 방향성만 공유한 상태이다.</p>
<p>이러한 구체적인 논의가 끝나고 나면, 어떻게 해결했는지를 추상화해서 보여주기 위한 블로그 개발을 진행할 예정이다.</p>
<blockquote>
<p>아마 다음 글도, 어떻게 기획할 것인가.
이러한 부분들을 논하지 않을까 싶은데, 어느정도 생각하고 글로 정리해야겠다.</p>
</blockquote>
<h2 id="마치며">마치며</h2>
<p>이만큼 글을 쓰는데 약 30분. 
이 짧은 시간 동안 글을 쓰면서, 꽤 해방감을 얻었다.
그래도 뭔가 할 일을 해낸 것 같고, 마음 속으로 번잡했던 것들이 많이 씻겨져 나간 것 같아 행복하다.</p>
<blockquote>
<p>일단 이기적이지만, 나를 위한 글을 쓰기.
내가 먼저 몰입하는 글을 쓰기에 집중해야겠다고 다짐했다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2023 회고] 다시 시작, 그리고 새로운 시작]]></title>
            <link>https://velog.io/@young_pallete/2023%EB%85%84-%ED%9A%8C%EA%B3%A0-%EC%9D%B4%EC%A7%81-%EB%8B%A4%EC%A7%90-2024%EB%85%84-%EB%AA%A9%ED%91%9C-%EB%B2%84%ED%82%B7%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@young_pallete/2023%EB%85%84-%ED%9A%8C%EA%B3%A0-%EC%9D%B4%EC%A7%81-%EB%8B%A4%EC%A7%90-2024%EB%85%84-%EB%AA%A9%ED%91%9C-%EB%B2%84%ED%82%B7%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Mon, 12 Feb 2024 12:43:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<ul>
<li>생성일자: <code>2024.02.12</code></li>
<li>수정 내역: 없어요!</li>
</ul>
</blockquote>
<h1 id="시작하며">시작하며</h1>
<p>블로그를 쓰기에는 꽤 많이 일에 몰두했고, 너무나도 바쁜 2023년이었다.
그리고 지금은 감사하게도(?) 더 바쁜 한 해를 맞이하고 있다.</p>
<p>이번 설을 맞이하여, 그간 느꼈던 좋은 영감들을 기록으로 남겨두고자 블로그를 조심스레 들어와 1년만에 작성하고자 한다.</p>
<h1 id="본론">본론</h1>
<h2 id="6개월-간-다시-독학-그리고-시작한-이직">6개월 간 다시 독학, 그리고 시작한 이직</h2>
<p>저는 2022년, 6개월 차가 된 시점에 8월에 권고사직으로 퇴사했다.
이후에는 6개월 간 공부만 했다.
신입 시절, 홀로 프론트엔드 웹/앱 전체를 담당하다 보니 정말 부족하단 걸 느꼈기 때문이었다.</p>
<p>물론 가족들도 반대했고, 주위 사람들도 반대했다.</p>
<blockquote>
<p>왜 경력 놔두고 빈 공백기를 쌓냐고.
가뜩이나 시장도 힘든데, 왜 굳이 더 어려운 길을 가려 하냐고.</p>
</blockquote>
<p>제일 힘든 때는, 회사가 파산했을 때 임금 체불로 인해 돈이 들어오지 않은 때였다.
그때 나는 약 1달 간 커피도 살 돈이 없어서 집에서 공부했다.</p>
<p>최근에 회사 사람들에게 누구나 &#39;인생의 보릿고개가 있다!&#39;라는 말을 우스갯소리로 했었다.
그리고 내게 인생의 보릿고개는 이때였던 것 같다. 정말 추락했다는 느낌을 받았기 때문이다.</p>
<p>하지만 아이러니하게도 사람은 바닥에서부터 태어났기 때문일까?
결국 올라설 곳만 남았다는 생각으로, 차분히 공든 탑을 쌓겠다는 신념으로 독학했다.</p>
<p>그리고 6개월간 두문불출하며 보내고 약속의 2월. 
변변찮은 스펙임에도, 시장에서 꽤 준수한 성적을 받았다.</p>
<p>계산해보니 약 30개의 이력서를 지원했고,
힘든 시장에서도 7개의 서류 통과에 2곳의 합격을 받았다.</p>
<p>고민이 됐던 건 한 곳은 돈과 복지가 좋았고, 다른 한 곳은 사람이 좋았다.
그 중 나는 사람을 선택했다. (지금도 두 조건이 놓인다면, 같은 선택을 하지 않을까)
그리고 좋은 개발자 분들과, 부족한 환경에서 새로운 문화를 만들며 설레는 한 해를 보냈다.</p>
<blockquote>
<p>가끔 생각한다.
내가 빠르게 생산활동을 했다면, 지금처럼 행복하게 일했을까?
그렇지 않았을 것 같다. 
끊임없이 타인과 비교하며 열등감을 갖고 개발하고 있지 않았을까.</p>
</blockquote>
<p>그렇기에 시장을 거슬러 힘든 내 선택과 도전이, 결국 지금의 더 멋진 나를 만들었다고 자부한다.</p>
<h3 id="철저한-준비가-스스로의-운을-만든다">철저한 준비가 스스로의 운을 만든다</h3>
<p>최근에 존경하는 분께 책을 받았다. 
그리고 책에는 다음과 같은 글귀가 있었다.</p>
<img src="https://velog.velcdn.com/images/young_pallete/post/1dc25f1c-9c07-4bb4-804c-a44ee9313b4e/image.png" style="margin-bottom: 4px;"/>
  <p style="font-size: 12px; text-align: center">식당에서 받자마자 감동받고 찍어서, 조금은 잘렸지만 감동받았다 🥹 </p>

<p>어쩌면 작년 한 해가 저 말로 다 마무리가 되지 않았을까.</p>
<blockquote>
<p>결국 나는 도전했고, 스스로 더 나은 운을 만들었다.
그리고 앞으로도 더 나은 기회들을 얻기 위해 스스로 헤엄치며 나아갈 뿐이다.</p>
</blockquote>
<h2 id="현재-내가-만들어가는-운은">현재, 내가 만들어가는 운은</h2>
<h3 id="회사-안에서---새로운-역할을-맡으며">회사 안에서 - 새로운 역할을 맡으며</h3>
<p>현재 회사에서 프론트엔드 리드님의 역할을 보조하는 부팀장의 역할을 하게 됐다.
사실 이제 2년 차라는 상황에 비해 굉장히 부담스럽지만, 상당히 재미있는 것들을 하고 있다.</p>
<p>기존처럼 </p>
<ul>
<li>사람들의 이슈의 난이도를 체크하고 사전에 문제점을 검토해주고</li>
<li>일감의 기한은 어느정도가 적당한지도 사전에 조율하는 역할을 하며</li>
<li>틈틈이 다른 분들의 이슈들의 현황들도 체크해주고 보조하는 역할을 하는</li>
</ul>
<p>PL의 역할들도 배우고 있다.</p>
<h3 id="회사-안에서---어떻게-하면-아이디어가-결과로-이어질-수-있을지를-고민하며">회사 안에서 - 어떻게 하면 아이디어가 결과로 이어질 수 있을지를 고민하며</h3>
<p>요새 가장 흥미롭게 몰두하고 연구하는 게 있다.
이슈가 끝나면, 스프린트 리뷰를 하는 문화가 어떻게 정착될 수 있을지 고민한다.</p>
<p>항상 아쉬웠던 게 있다.</p>
<blockquote>
<ul>
<li>왜 이슈는 단발적으로 끝날까?</li>
<li>왜 문제점들이 보이는데 더 백로그가 쌓이지 않을까?</li>
</ul>
</blockquote>
<p>아무래도 사내에 PM이 없다보니, 이러한 차후적인 해결 목록들이 다음 스프린트로 이어지지 않는 것 같다.</p>
<p>다행히도 우리 개발팀은 이러한 불편함을 계속해서 인지하고 있다.
따라서 서로 의논한 결과, 우리가 어느 정도 이러한 프로젝트 리딩을 겸해서 하자고 의견을 모았다.</p>
<p>결과적으로 최근에 회원가입 로직에 대한 이슈를 처리했고, 이후 스프린트 리뷰를 진행했다.
결과적으로 회원가입 로직의 고도화가 필요하다고 의견을 모았고, 기타 의견들도 전달되어 기획 측에서 새롭게 기획 중이다.</p>
<blockquote>
<p>결과는 어떻게 될지 모른다.
하지만 시도도 하지 않고 포기하면 아무것도 나오지 않는다.
그래서 나는 오늘도 뭔가 새롭게 계속해서 만들어가고 있다 😁</p>
</blockquote>
<h3 id="회사-안에서---새로운-개발에-도전하며">회사 안에서 - 새로운 개발에 도전하며</h3>
<p>현재 새로운 업무가 주어졌다.</p>
<h4 id="대규모-마이그레이션-프로젝트-리드">대규모 마이그레이션 프로젝트 리드</h4>
<p>사내에서는 Next.js 10버전을 사용하고 있는데, 이번에 13버전까지 마이그레이션하는 것을 총괄하고 있다.</p>
<p>다행히도, 이미 내가 마이그레이션에 대한 프로토타입을 구축하고, 문서화를 해놓은 게 존재하다 보니(이래서 문서화가 중요하다) 나름 순조롭게 진행하고 있다. 한 50%...?
(잘 되면, 조만간 소규모 엔터프라이즈에서 마이그레이션을 점진적으로 했던 경험에 대해서도 글을 쓰지 않을까 싶다)</p>
<blockquote>
<p>여튼 항상 마이그레이션이 이루어질 수 있도록 힘 써주시는 우리 프론트엔드 분들.
리드 보성님, 민혜님, 선주님, 도현님, 상우님, 의민님께 적잖은 감사를 드린다!</p>
</blockquote>
<h4 id="seo-최적화-프로젝트-리드">SEO 최적화 프로젝트 리드</h4>
<p>또한, 이것과 별도로 아무래도 내가 광고홍보 쪽 전공 출신이다 보니 마케팅 부서와 협력하며 SEO 최적화를 리딩하고 있다.</p>
<p>SEO의 경우, 사실 대학교에서도 다루진 않기에 그렇게 많은 지식을 갖고 있지는 않은 분야였다.
하지만 결국 회사에서 원하는 것을 해내야 하는 것이 내 역할이기에, 그저 계속해서 탐구하고, 새로운 문제점들을 발견하고 개선하고 있다.</p>
<p>SSR과 더불어 사이트맵, RSS와 같은 온 페이지 SEO뿐만 아니라, 계속해서 오프 페이지 SEO 등도 공부하면서 새로운 지식들을 쌓아가고 있다.</p>
<h4 id="번외---그러면서-생긴-갈증">번외 - 그러면서 생긴 갈증</h4>
<p>아무래도 마이그레이션이 꽤나 팀 내에서는 우선순위가 높다. 
그리고 SEO 역시 회사에서 꽤 중요한 업무다 보니, 다른 업무는 자잘한 것들만 맡는다.</p>
<p>물론 다른 작업들도 팀원들을 보조하거나, 따로 처리하지만, 기존에 개발된 것들을 개선하는 작업을 주로 1달 반째 하고 있다.</p>
<blockquote>
<p>그래서인지 요새는 미친듯이 새로운 사업/기획들에 관한 이슈를 개발하고 싶기도 하고... 그렇다. 크흡.</p>
</blockquote>
<h3 id="회사-밖에서---새로운-도전들을-이어가며">회사 밖에서 - 새로운 도전들을 이어가며</h3>
<p>작년 하반기 초부터 새로운 프로젝트를 진행하고 있다.
이전부터 사업을 하자고 하는 친구가 있었는데, 여러 차례 설득 후에 나 역시 하기로 했다.</p>
<p>일단 내가 맡은 것은 웹/앱 개발, 그리고 기획과 디자인 총괄이다.
사실상 너무나 많은 일들을 하여... 요새 블로그 쓸 몸이 남아돌지 않았다 🥲</p>
<p>일단 난 모든 툴에 대해 리소스를 기준으로 생각하는데, 지금은 돈을 아낄 때다.
따라서 기획은 <code>노션</code>으로 진행하다가, 좀 더 무료 요금제에 너그러운 <code>컨플루언스 + 지라</code> 스택으로 갈아탔다.
(깃헙으로 유도하려 했지만, 비개발자 동료분들의 난이도를 고려하여... 🥲)</p>
<p>디자인은 아이콘 등의 작업은 <code>피그마</code>로 하고, 페이지 단의 레이아웃은 <code>프레이머</code>로 작업하고 있다.</p>
<blockquote>
<p>그러다 보니 지금은 정말 제너럴리스트<del>(a.k.a 잡부)</del>의 느낌이 나지만, 
결국 헤매는 만큼 자기 땅이고, 나는 더 많은 방면으로 성장하고 있다. </p>
</blockquote>
<p>일단 저번 주에 론칭했는데, 회원가입 수가 벌써 100명을 넘어갔다는 것에 놀랐다.
다음 달에 아마 투자 결과에 따라서 본격적으로 실시할 것 같다.</p>
<p>다만 그 상황이 오기 전까지, 지금의 나의 역할에 맞게 계속해서 아이디어를 디벨롭하고, 프로토타입을 만들며, 개발로 이어가고 있다.</p>
<blockquote>
<p>이 살인적인 스케줄이 다행히도 사내의 갈증과 맞물려... 요새는 꽤 재밌다는 생각도 드는 생각<del>(착각)</del> 중이다 😊</p>
</blockquote>
<h1 id="마치며">마치며</h1>
<p>사실 회사에서 개발한 것들을 자랑할까도 싶었지만, 그런 건 나중에 이력서 쓸 때나 적기로 했다.</p>
<p>난 글을 쓸 때도 단일 책임의 원칙이 있다고 생각하는데, 
이러한 기술적인 성과는 지금 플로우에 객체지향적이지 않은 것 같기 때문이다.
그래도 사내에서 기록을 하다 보니, 감은 떨어지지 않아서 다행이다.</p>
<p>사실, 블로그에 글을 쓰지 못했던 것은 한 편으로 적잖은 죄책감이 있어 왔다.</p>
<ul>
<li>절필했을 때의 길티프레져와 </li>
<li>블로그를 썼을 때의 부담감을 생각했을 때, </li>
</ul>
<p>펜의 무게가 꽤나 내게 무거워서, 다시 찾아오기가 어려웠다.</p>
<p>하지만 결국 버킷리스트에 썼던 이 포스트를 쓰고 나니, <code>왜 내가 이렇게 무서워했을까</code>라는 생각이 들었다. 그냥 내 생각 쓰면 되는 건데 말이다.</p>
<blockquote>
<p>여튼, 새해에 부담 털고
저번 년도가 다시 날아오른 해라면
이번 년도는 내가 열정으로 타오르는 해로 만들 것이다. 😎</p>
</blockquote>
<p>조만간 다시 또 놀러와야겠다 👐🏻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 9. 메타볼 융합 로직 최적화하기 (End)]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-8.-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%9C%B5%ED%95%A9-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0-End</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-8.-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%9C%B5%ED%95%A9-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0-End</guid>
            <pubDate>Tue, 28 Mar 2023 08:37:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue | 이슈내용</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">PR | 해당 코드에 대한 commit(92b15f9)</a></li>
</ul>
<p><strong>🗒️ 이 글의 수정 내역</strong> (마지막 수정 일자: 없음)</p>
</blockquote>
<h1 id="🚀-시작하며">🚀 시작하며</h1>
<p>드디어 이것으로 메타볼 애니메이션 구현 시리즈를 마칠 수 있게 됐군요!
사실 처음에는 힘들었는데요. 제가 이뤘던 것들을 간만에 다시 살펴보니, 꽤나 저 역시 이를 이해하느라 고생 많이 했다는 것을 다시금 느꼈습니다. 🥰</p>
<p>여튼, 기존에 했던 융합은 <strong>캔버스의 전체 픽셀에 대해 완전탐색</strong>을 하고 있었죠?
그리고 이로 인한 가장 큰 한계는 <strong>성능이 굉장히 느리다</strong>는 것이었습니다.</p>
<p>그렇다면 저는 어떻게 이를 해결해나갔는지, 한 번 살펴봅시다!</p>
<h1 id="🚦-본론">🚦 본론</h1>
<h2 id="어떻게-설계할-것인가">어떻게 설계할 것인가</h2>
<p>일단 기존에는 픽셀을 하나의 공간이라 치면, 그 공간에 대해 각 메타볼 도형간의 영향력을 고려하여, 이 영향력이 역치를 넘어서면 융합하는 방식으로 구현을 했습니다.</p>
<p>그런데 생각해봅시다.</p>
<blockquote>
<p><strong>애초에 메타볼끼리만 비교하면 안될까요? 🥹</strong></p>
</blockquote>
<p>물론 메타볼의 공식 그대로 구현하는 것이 가장 정확하지만, 저는 메타볼이 융합하는 느낌만 가져가도 충분했습니다.</p>
<p>따라서 <a href="http://paperjs.org/">paperJS</a>라는 캔버스 라이브러리를 참고하며 다음과 같이 하나씩 정복해나갔어요.</p>
<ol>
<li>두 원의 거리, 각도 등을 비교한다.</li>
<li>두 원이 가진 외접선을 비교한다.</li>
<li>두 원이 포개져 있다면 내접점을 비교한다.</li>
<li>1 ~ 3의 정보를 활용하여 fuse할 수 있는지를 계산한다.</li>
<li>이후 융합하는 듯한 애니메이션의 모양을 결정할 핸들의 위치와 값을 계산한다.</li>
<li>핸들과 기존에 구했던 각 점들을 <code>path</code>로 그려내며 결과값을 반환한다.</li>
</ol>
<h2 id="기존의-전략-객체를-대체할-객체-생성">기존의 전략 객체를 대체할 객체 생성</h2>
<p>이전 포스트에서 <code>FuseStrategy</code>를 만들었던 것 기억나시나요?
이제 이를 대체할 새로운 <code>FuseStrategy</code>의 뼈대를 생성해볼게요.</p>
<h3 id="optimizedfusestrategy">OptimizedFuseStrategy</h3>
<pre><code class="language-ts">  before?: (...args: unknown[]) =&gt; void;

  after?: (...args: unknown[]) =&gt; void;

  constructor(public fuseWeight: number = 1.2) {}

  setBefore(callback: (...args: unknown[]) =&gt; void) {
    this.before = callback.bind(this);
  }

  setAfter(callback: (...args: unknown[]) =&gt; void) {
    this.after = callback.bind(this);
  }

  setFuseWeight(weight: number) {
    this.fuseWeight = weight;
  }

  exec(ctx: Canvas[&#39;ctx&#39;], balls: (DynamicMetaball | StaticMetaball)[]) {
  }</code></pre>
<p>이후에는 이 객체를 기존의 <code>fuseStrategy</code>에 대해 대체해봅시다.</p>
<h3 id="appts">App.ts</h3>
<pre><code class="language-ts">function main() {
  const moveStrategy = new MoveStrategy();
  const drawStrategy = new DrawStrategy();
  // const fuseStrategy = new FuseStrategy();
  const fuseStrategy = new OptimizedFuseStrategy();

  app.setDynamicMetaballMove({
    moveStrategy,
    key: EMetaballObserverKeys.dynamic,
  });

  app.setDynamicMetaballDraw({
    drawStrategy,
    key: EMetaballObserverKeys.dynamic,
  });

  moveStrategy.setBefore(() =&gt; {
    fuseStrategy.exec(app.canvasCtx, app.allMetaballs);
  });

  app.mount($target);
}

main();</code></pre>
<p>어떤가요?
원래였다면 이를 대체하기 위해 새로운 메타볼 클래스를 만들어서 상속해야 했는데, 그냥 전략만 대체함으로써 해결해냈어요.</p>
<p>물론 어느 상황에서나 좋은 건 아니지만, 그래도 객체 지향적으로 문제를 개방-폐쇄 원칙에 맞게 잘 해결하게 되었군요!</p>
<p>그럼, 이제 본격적으로 로직을 분석하러 가볼까요?</p>
<h2 id="원들끼리-비교하기">원들끼리 비교하기</h2>
<p>일단 <code>exec</code>이 실행되면, 원들 간의 융합을 할지말지를 결정해야 합니다.
이를 위해서는 필수적으로 각 원들간의 비교가 선행돼요.</p>
<p>이를 한 번 <code>exec</code>에서 구현해봅시다.</p>
<pre><code class="language-ts">export class OptimizedFuseStrategy implements Strategy {
  // 기존 코드 생략 ...

  exec(ctx: Canvas[&#39;ctx&#39;], balls: (DynamicMetaball | StaticMetaball)[]) {
    for (let i = 0; i &lt; balls.length; i += 1) {
      const nowBall = balls[i];

      for (let j = i; j &lt; balls.length; j += 1) {
        const cmpBall = balls[j];

        this.fuse(ctx, nowBall, cmpBall);
      }
    }
  }

  fuse( 
    ctx: Canvas[&#39;ctx&#39;],
    ball1: DynamicMetaball | StaticMetaball,
    ball2: DynamicMetaball | StaticMetaball,
  ) {}
}</code></pre>
<p>모든 메타볼들에 대해 비교를 해줌으로써 융합에 대해 <code>fuse</code> 메서드에서 판단하고 그려줄 거에요.
그렇다면 이제, <code>fuse</code>를 구현하러 가봅시다.</p>
<h3 id="최대-거리-구하기">최대 거리 구하기</h3>
<p>메타볼이 융합이 될지 여부는, 서로간에 가까운지에 대한 여부겠죠?
따라서 먼저 두 원을 불러준 다음, 현재의 거리와 최대 거리를 구해줍시다.</p>
<pre><code class="language-ts">    const {x: x1, y: y1, r: r1} = ball1;
    const {x: x2, y: y2, r: r2} = ball2;

    const totalRadiusSum = r1 + r2;

    const getDist = (x1: number, y1: number, x2: number, y2: number): number =&gt;
      Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);

    const dist = getDist(x1, y1, x2, y2);
    const maxDist = totalRadiusSum * this.fuseWeight;

    if (dist &gt;= maxDist) {
      return;
    }</code></pre>
<p>이제 결과적으로 최대 거리를 넘어가면 계산을 하지 않도록 <code>early return</code>하게 되었어요! 👏🏻</p>
<h2 id="융합에-대한-최소-각도-구하기">융합에 대한 최소 각도 구하기</h2>
<p>그렇다면 이제 이전의 코드를 넘어가면 모두 융합해야 하는 메타볼이겠죠?!
우리는 이후 두 원의 내접점에 대한 각도를 구해야 해요. 이를 통해 최소 확산 거리를 알 수 있게 됩니다. </p>
<p>왜냐하면, 두 내 접점 미만의 각도로는 이미 합쳐져 있으니 융합을 계산할 필요가 없기 때문이죠.</p>
<pre><code class="language-ts">    // 기존 코드 생략...
    const isOverlapping = dist &lt; totalRadiusSum;

    const squaredR1 = r1 ** 2;
    const squaredR2 = r2 ** 2;
    const squaredDist = dist ** 2;

    /**
     * @description
     * 내접하는 각 메타볼의 가운데와 접점을 이어 삼각형을 만들었을 때, 해당 각도를 구하는 공식.
     * 세 변을 알 수 있다면 각도를 구할 수 있다.
     * @see: https://en.wikipedia.org/wiki/Law_of_cosines
     */
    const u1 = isOverlapping
      ? Math.acos((squaredDist + squaredR1 - squaredR2) / (2 * r1 * dist))
      : 0;

    const u2 = isOverlapping
      ? Math.acos((squaredDist + squaredR2 - squaredR1) / (2 * r2 * dist))
      : 0;
</code></pre>
<h2 id="외접점을-통해-최대-확산-각도-구하기">외접점을 통해 최대 확산 각도 구하기</h2>
<p>반대로 왜 외접점을 구해야 하는지는 눈치가 빠르시다면 이해하셨을 것 같아요! 🙇🏻‍♂️
가령 메타볼이 존재한다면, 마치 융합하는 듯한 사이의 애니메이션은 두 원의 외접점들을 넘어갈 수 없어요. 따라서 이 외접점을 미리 구함으로써 우리는 유효한 거리에 있는 두 원 간에 대해 최대로 늘릴 수 있는 limit를 정해줄 수 있습니다.</p>
<p>이를 구하기 위해 여러 자료를 찾아본 결과, <a href="http://www.ezformula.net/esne/aboard/m_Fcontents_viewer.php?fcode=1113176&amp;bgrcode=1021&amp;mgrcode=1158&amp;fupman=metalheart">다음 공식</a>이 유효했어요.
이를 통해 현재 메타볼의 외접점과 중심, 그리고 비교할 메타볼의 중심을 이었을 때의 각도를 알 수 있습니다.</p>
<pre><code class="language-ts">const maxSpread = Math.acos((r1 - r2) / dist);</code></pre>
<p>이후 이 확산할 수 있는 각도를 찾았다면, 외접점을 구해주면 됩니다.
이때, 우리는 융합에 대한 좌표를 찾아야 하는데요. 미리 이를 구할 함수 <code>getVector</code>을 해당 클래스의 메서드로 구현해놓을게요.</p>
<pre><code class="language-ts">  getVector(
    x: number,
    y: number,
    angle: number,
    radius: number,
  ): [number, number] {
    return [x + radius * Math.cos(angle), y + radius * Math.sin(angle)];
  }</code></pre>
<pre><code class="language-ts">    const v = 0.5;

    /**
     * @description
     * 결국 두 원의 중심 간에 애초에 존재하던 각도를 베이스로 잡기 위해 정의한다.
     */
    const baseAngle = getAngle(x2, y2, x1, y1);

    const spreadV1 = u1 + (maxSpread - u1) * v;
    const spreadV2 = Math.PI - u2 - (Math.PI - u2 - maxSpread) * v;

    const angle1a = baseAngle + spreadV1;
    const angle1b = baseAngle - spreadV1;

    const angle2a = baseAngle + spreadV2;
    const angle2b = baseAngle - spreadV2;

    const p1a = this.getVector(x1, y1, angle1a, r1);
    const p1b = this.getVector(x1, y1, angle1b, r1);
    const p2a = this.getVector(x2, y2, angle2a, r2);
    const p2b = this.getVector(x2, y2, angle2b, r2);</code></pre>
<p>여기서 특이한 점은 <code>v</code>입니다. <code>v</code>는 무엇이길래 0.5라는 수치를 <code>spreadV1</code>, <code>spreadV2</code>에 곱해주는 걸까요?</p>
<p>저 역시 이 값이 이해가 되지 않았으나 추측하기로는 평균을 내기 위함인 것 같아요.
결국 외접점에 대한 최대 각도와, 내접점에 대한 최소 각도의 평균으로 가중치를 줌으로써, 메타볼의 융합하는 효과를 적절하게 핸들링하는 것으로 파악할 수 있습니다!</p>
<p>이후에는 해당 각도를 구하여(<code>angle</code>), 융합하는 곳을 그릴 각 4지점 좌표를 구합니다.</p>
<h2 id="핸들지점-구하기">핸들지점 구하기</h2>
<p>휴! 이제 거의 다 구현했어요.
이제 핸들할 곳을 구할 건데요. 이 용도는, 추후 베지어 곡선을 그려, 자연스러운 융합 효과를 구현하는 데 사용할 거에요.</p>
<pre><code class="language-ts">    const handleLength = 2.4;
    const baseHandleDist =
      Math.min(v * handleLength, getDist(...p1a, ...p2a) / totalRadiusSum) *
      Math.min(1, (dist * 2) / totalRadiusSum);

    const handleRadius1 = r1 * baseHandleDist;
    const handleRadius2 = r2 * baseHandleDist;

    const h1a = this.getVector(...p1a, angle1a - PIH, handleRadius1);
    const h1b = this.getVector(...p1b, angle1b + PIH, handleRadius1);
    const h2a = this.getVector(...p2a, angle2a + PIH, handleRadius2);
    const h2b = this.getVector(...p2b, angle2b - PIH, handleRadius2);</code></pre>
<p>휴! 결과적으로 우리는 필요한 정보들을 모두 구했네요.</p>
<h2 id="그리기">그리기</h2>
<p>최종적으로 그릴 시간입니다.
여기서는 <code>bazierCurve</code>라는 것을 사용할 거에요. 이를 통해 핸들 축을 2개 지정하여 베지에 곡선을 그림으로써 자연스러운 곡선을 연출할 거에요.</p>
<pre><code class="language-ts">    ctx.beginPath();

    ctx.moveTo(...p1a);

    ctx.bezierCurveTo(...h1a, ...h2a, ...p2a);
    ctx.lineTo(...p2b);
    ctx.bezierCurveTo(...h2b, ...h1b, ...p1b);

    ctx.closePath();

    ctx.fill();</code></pre>
<p>자, 이제 모든 게 끝났어요.
한 번 결과를 확인해볼까요?</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/1e3962f3-b8ea-4949-a776-6e9068f0d588/image.gif" alt=""></p>
<p>잘 나오는군요!</p>
<h2 id="optimizedfusestrategy-전체-코드">OptimizedFuseStrategy 전체 코드</h2>
<pre><code class="language-ts">export class VectorFuseStrategy implements Strategy {
  before?: (...args: unknown[]) =&gt; void;

  after?: (...args: unknown[]) =&gt; void;

  constructor(public fuseWeight: number = 1.2) {}

  setBefore(callback: (...args: unknown[]) =&gt; void) {
    this.before = callback.bind(this);
  }

  setAfter(callback: (...args: unknown[]) =&gt; void) {
    this.after = callback.bind(this);
  }

  setFuseWeight(weight: number) {
    this.fuseWeight = weight;
  }

  exec(ctx: Canvas[&#39;ctx&#39;], balls: (DynamicMetaball | StaticMetaball)[]) {
    for (let i = 0; i &lt; balls.length; i += 1) {
      const nowBall = balls[i];

      for (let j = 0; j &lt; balls.length; j += 1) {
        const cmpBall = balls[j];

        this.fuse(ctx, nowBall, cmpBall);
      }
    }
  }

  getVector(
    x: number,
    y: number,
    angle: number,
    radius: number,
  ): [number, number] {
    return [x + radius * Math.cos(angle), y + radius * Math.sin(angle)];
  }

  fuse(
    ctx: Canvas[&#39;ctx&#39;],
    ball1: DynamicMetaball | StaticMetaball,
    ball2: DynamicMetaball | StaticMetaball,
  ) {
    const {x: x1, y: y1, r: r1} = ball1;
    const {x: x2, y: y2, r: r2} = ball2;

    /**
     * @see: https://github.com/paperjs/paper.js/blob/develop/examples/Paperjs.org/MetaBalls.html
     */
    const v = 0.5;
    const handleLength = 2.4;

    const totalRadiusSum = r1 + r2;

    const dist = getDist(x1, y1, x2, y2);
    const maxDist = totalRadiusSum * this.fuseWeight;

    if (dist &gt;= maxDist) {
      return;
    }

    const maxSpread = Math.acos((r1 - r2) / dist);

    const isOverlapping = dist &lt; totalRadiusSum;

    const squaredR1 = r1 ** 2;
    const squaredR2 = r2 ** 2;
    const squaredDist = dist ** 2;

    /**
     * @description
     * 내접하는 각 메타볼의 가운데와 접점을 이어 삼각형을 만들었을 때, 해당 각도를 구하는 공식.
     * 세 변을 알 수 있다면 각도를 구할 수 있다.
     * @see: https://en.wikipedia.org/wiki/Law_of_cosines
     */
    const u1 = isOverlapping
      ? Math.acos((squaredDist + squaredR1 - squaredR2) / (2 * r1 * dist))
      : 0;

    const u2 = isOverlapping
      ? Math.acos((squaredDist + squaredR2 - squaredR1) / (2 * r2 * dist))
      : 0;

    /**
     * @description
     * 결국 두 원의 중심 간에 애초에 존재하던 각도를 베이스로 잡기 위해 정의한다.
     */
    const baseAngle = getAngle(x2, y2, x1, y1);

    const spreadV1 = u1 + (maxSpread - u1) * v;
    const spreadV2 = Math.PI - u2 - (Math.PI - u2 - maxSpread) * v;

    const angle1a = baseAngle + spreadV1;
    const angle1b = baseAngle - spreadV1;

    const angle2a = baseAngle + spreadV2;
    const angle2b = baseAngle - spreadV2;

    const p1a = this.getVector(x1, y1, angle1a, r1);
    const p1b = this.getVector(x1, y1, angle1b, r1);
    const p2a = this.getVector(x2, y2, angle2a, r2);
    const p2b = this.getVector(x2, y2, angle2b, r2);

    const baseHandleDist =
      Math.min(v * handleLength, getDist(...p1a, ...p2a) / totalRadiusSum) *
      Math.min(1, (dist * 2) / totalRadiusSum);

    const handleRadius1 = r1 * baseHandleDist;
    const handleRadius2 = r2 * baseHandleDist;

    const h1a = this.getVector(...p1a, angle1a - PIH, handleRadius1);
    const h1b = this.getVector(...p1b, angle1b + PIH, handleRadius1);
    const h2a = this.getVector(...p2a, angle2a + PIH, handleRadius2);
    const h2b = this.getVector(...p2b, angle2b - PIH, handleRadius2);

    ctx.beginPath();

    ctx.moveTo(...p1a);

    ctx.bezierCurveTo(...h1a, ...h2a, ...p2a);
    ctx.lineTo(...p2b);
    ctx.bezierCurveTo(...h2b, ...h1b, ...p1b);

    ctx.closePath();

    ctx.fill();
  }
}</code></pre>
<h2 id="퍼포먼스-비교">퍼포먼스 비교</h2>
<p>드디어! 퍼포먼스를 비교할 수 있게 되었어요. 한 번 성능을 살펴볼까요?
똑같은 조건에서 동일하게 10초간 아무런 동작 없이 렌더링한 결과입니다.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/4d1d2a3e-d561-4b21-b7d5-4b5cf5dba9be/image.png" alt=""></p>
<p>헉! 스크립트 성능이 약 50~60배를 왔다갔다 하는군요 🫣
유휴 상태 역시 적지는 않지만 넉넉하기에, 이는 필요한 애니메이션이라면 실제로 쓸 수 있겠군요!</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/e82ed0eb-679d-4529-ab5f-3d9bf13bb82c/image.png" alt=""></p>
<p>함수 호출 시간 역시 상당히 짧아졌음을 확인했습니다.</p>
<h2 id="한계">한계</h2>
<p>그러나 분명 단점 역시 존재해요.</p>
<ul>
<li>그리는 로직이 원과 별도로 존재하게 되어 조금씩 <code>sync</code>가 맞지 않는 이슈가 생깁니다. 이는 어쩔 수 없다고 생각하는 게, <code>canvas</code>는 래스터 기반으로 비트맵 단위로 조작할 수 있게되는데요. 소수점 단위까지는 커버할 수 없기에 발생한 이슈라 생각해요.</li>
<li>메타볼 특성상 융합된 결과로 메타볼 원자가 커져야 합니다. 그러나 이를 핸들링할 수 없다는 건 마음이 아프네요 🥲</li>
</ul>
<h1 id="🎉-마치며">🎉 마치며</h1>
<p>제가 가장 애정을 쏟았던 작업이니만큼, 꽤나 오랜 시간이 걸렸네요.
힘도 빠질 법 하지만, 오히려 기분이 좋았어요.</p>
<ul>
<li>이 글을 쓰면서 디자인 패턴에 대해 복습하게 되어 진심으로 기분이 좋았고, </li>
<li>이전의 제가 열심히 살았구나~하는 느낌을 받았습니다.</li>
</ul>
<p>실제로 수정할 때마다 <code>delete</code>되는 코드들이 거의 없고, 확장에만 코드 작성이 집중되었음을 확인하며 설계가 나쁘지 않았다는 것을 느꼈어요. </p>
<blockquote>
<p>아무래도 이제 저의 경우, 포트폴리오 사이트를 바꿀 계획인데요. 바꾸게 되면 이 코드는 사용하지 않게 될지도 모르겠네요. 그러나 누군가에게 어떤 방식으로든 도움이 되었으면 좋겠어서 글로 남기게 되었네요.</p>
</blockquote>
<p>그렇다면, 긴 글 읽으시느라 고생하셨습니다 🥰</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 8. 메타볼 융합 구현하기 (최적화 이전 - 최종)]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-8.-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%9C%B5%ED%95%A9-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%9D%B4%EC%A0%84-%EC%B5%9C%EC%A2%85</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-8.-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%9C%B5%ED%95%A9-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%9D%B4%EC%A0%84-%EC%B5%9C%EC%A2%85</guid>
            <pubDate>Mon, 27 Mar 2023 15:38:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue | 이슈내용</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">PR | 해당 코드에 대한 commit(0204ad4)</a></li>
</ul>
<p><strong>🗒️ 이 글의 수정 내역</strong> (마지막 수정 일자: 없음)</p>
</blockquote>
<h1 id="🚀-시작하며">🚀 시작하며</h1>
<p>드디어 대망의 최적화 이전의 로직을 모두 구현하게 되어 기분이 좋아요! (오열)
마지막으로 메타볼을 융합하는 로직을 만들 건데요. 이는 생각보다 쉽지 않아요.</p>
<p>차근차근 설명할 예정이니, 따라오시죠! 🙆🏻‍♀️</p>
<h2 id="알고리즘-설계">알고리즘 설계</h2>
<p>일단 융합에 있어서 가장 필요한 것은, 어떻게 설계하는지에 관한 것이에요.
최적화 이전, 당시에는 다음과 같이 판단했습니다.</p>
<ol>
<li>1픽셀씩 전체 화면의 <code>width</code>, <code>height</code>를 나누어주자.</li>
<li>각 픽셀마다, 해당 픽셀에서 갖고 있는 <strong>어떤 특정한 역치를 초과하면 융합</strong>되듯이 <code>fillColor</code>한다.</li>
<li>렌더링을 진행한다.</li>
</ol>
<p>여기서 중요한 것은 2번입니다.
어떻게 특정한 역치를 산정할 수 있을까요? 사실 메타볼을 만들 때 가장 고민했던 부분이 이 부분입니다.</p>
<h3 id="원의-방정식을-활용하라">원의 방정식을 활용하라</h3>
<p><a href="https://greentec.github.io/shadertoy-metaball/">이 글</a>에서 많은 영감과 도움을 받았습니다.
메타볼을 만드는 데 있어, 결과적으로 원의 방정식을 활용하는 것이죠.</p>
<p>우리의 원을 픽셀이라는 개념으로 말하면 어떻게 말할 수 있을까요?
<code>x, y</code>라는 포지션에서 일정 거리의 반지름 <code>r</code> 안에 있는 모든 픽셀을 원이라 할 수 있죠.
그렇다면, 이는 다음과 같은 공식을 만족합니다.</p>
<blockquote>
<p>픽셀의 위치를 <code>a, b</code>라 할 때 이 픽셀이 원 안에 들어와 있다면
<code>(a - x) ^ 2 * (b - y) ^ 2 &lt;= r ^ 2</code></p>
</blockquote>
<p>즉, 만약 <code>r</code>이 어떤 핵으로부터 핵이 가지는 영향력이라고 본다면, 핵의 영향력은 거리의 크기에 반비례함을 알 수 있죠.</p>
<p>이는 실제로 <a href="https://en.wikipedia.org/wiki/Metaballs">Wikipedia - metaball</a>에도 명시되어 있듯, 역제곱 법칙이 성립한다고 할 수 있습니다.</p>
<p>즉, 임계값을 1이라고 칠 때, 이 값은 그 주변에 존재하는 메타볼들의 <code>r ^ 2 / ((a - x) ^ 2 * (b - y) ^ 2)</code>값들보다 크면 되는 것이죠! </p>
<p>이것이 의미하는 것은, <code>r</code>이라는 게 핵의 영향력이라면 </p>
<ol>
<li>주변의 핵이 가진 영향력 역시 반영해야 하기에 이를 각 메타볼마다 구해야 합니다.</li>
<li>이들을 모두 합한 결과는 그 위치에서 메타볼이 영향력을 가지는 세기이죠.</li>
<li>그리고 그 세기가 1보다 크다는 것은 특정 원의 내부가 가지는 세기의 최소 요건(임계값 이상)을 만족하는 것이죠.</li>
<li>따라서 융합을 할 수 있다고 판단하여 색을 칠하게 되는 것이죠!</li>
</ol>
<blockquote>
<p>저 역시 이쪽 전문은 아니다 보니(...) 아무래도 부족한 설명이라 이해가 됐을런지 모르겠네요. 
혹시나 더 궁금하시다면, 위에서 첨부한 위키피디아를 참고하시는 게 더욱 객관적이라 보입니다 😉</p>
</blockquote>
<p>그렇다면, 이제 전략을 구현하러 가볼까요?</p>
<h3 id="fusestrategy">FuseStrategy</h3>
<pre><code class="language-ts">
function shouldFuse(
  balls: (DynamicMetaball | StaticMetaball)[],
  cx: number,
  cy: number,
) {
  const total = balls.reduce((forceSum, ball) =&gt; {
    const {x, y, r} = ball;

    const acc = forceSum + r ** 2 / ((cx - x) ** 2 + (cy - y) ** 2);
    return acc;
  }, 0);

  return total &gt;= 1;
}


export class FuseStrategy implements Strategy {
  before?: (...args: unknown[]) =&gt; void;

  after?: (...args: unknown[]) =&gt; void;

  constructor() {}

  setBefore(callback: (...args: unknown[]) =&gt; void) {
    this.before = callback.bind(this);
  }

  setAfter(callback: (...args: unknown[]) =&gt; void) {
    this.after = callback.bind(this);
  }

  exec(ctx: Canvas[&#39;ctx&#39;], balls: (DynamicMetaball | StaticMetaball)[]) {
    this.before?.();

    const {innerWidth, innerHeight} = window;

    for (let cx = 0; cx &lt; innerWidth; cx += 1) {
      for (let cy = 0; cy &lt; innerHeight; cy += 1) {
        if (shouldFuse(balls, cx, cy)) {
          ctx.save();

          ctx.fillStyle = &#39;#ffaa00&#39;;
          ctx.fillRect(cx, cy, 1, 1);

          ctx.restore();
        }
      }
    }

    this.after?.();
  }
}</code></pre>
<p>구현이 단순하다 보니, 생각보다 큰 시간이 소요되지 않았어요. 🙆🏻🙆🏻‍♀️
다만, 이제 주의할 게 있어요.</p>
<h3 id="엇-기존-메타볼들끼리는-어떻게-비교해야-하나요">엇, 기존 메타볼들끼리는 어떻게 비교해야 하나요?</h3>
<p>설계에 있어 이게 가장 고민이 되었어요. 😖
이유는, <code>Metaball</code>을 각각 비교하는 게 아니라, <code>Canvas</code> 자체에서 각 픽셀들의 영향력을, <code>Observers</code>가 갖고 있는 각각의 메타볼들의 거리를 기준으로 합산하여 계산하기 때문입니다.</p>
<p>즉, 대상이 픽셀이 되어버리기 때문에 기존처럼 <code>Observer</code>에 알고리즘을 넣어줄 수 없는 거에요.</p>
<p>이럴 때에는 차분하게, <strong>무엇이 필요한지를 생각해보면 돼요.</strong>
우리가 필요한 것은 </p>
<ul>
<li>메타볼들에 대한 모든 데이터</li>
<li>거리에 따라 결과를 그려내는 것</li>
</ul>
<p>이죠?!</p>
<p>따라서 이 2개를 그려내기 위해 <code>App</code>에서 <code>ctx</code>와 <code>allMetaballs</code>를 꺼내줍시다.</p>
<h3 id="animationsubjectts">AnimationSubject.ts</h3>
<pre><code class="language-ts">export class AnimationSubject implements MetaballsSubject {
  // 기존 코드 생략 ...

  get allMetaballs() {
    const arr: (StaticMetaball | DynamicMetaball)[] = [];

    const allBalls = [...this.observers].map(observer =&gt; observer.metaballs);

    allBalls.forEach(balls =&gt; {
      arr.push(...balls.balls);
    });

    return arr;
  }
}</code></pre>
<h3 id="canvasts">Canvas.ts</h3>
<pre><code class="language-ts">export class MetaballCanvas implements GradientCanvas {
  // 기존 코드 생략...

  get allMetaballs() {
    return this.metaballAnimationSubject.allMetaballs;
  }</code></pre>
<h3 id="appts">App.ts</h3>
<pre><code class="language-ts">export class MetaballAnimation implements CanvasAnimation {
  // 기존 코드 생략...

  get canvasCtx() {
    return this.canvas.ctx;
  }

  get allMetaballs() {
    return this.canvas.allMetaballs;
  }
}</code></pre>
<p>이제 이를 외부에서 세팅하면 끝나겠죠?!
이때, 모든 것을 다 그린 후에, 융합되는 로직을 그려내면 됩니다.</p>
<blockquote>
<p>💡 <strong>잠깐! 그런데 우리는 어디에서 이 전략을 넣어야 하죠?! 클래스 내부에서는 넣어주는 곳이 없잖아요!</strong></p>
</blockquote>
<p>맞아요. 이럴 때 유연하게 쓰기 위해, 저는 전략에 <code>before</code>와 <code>after</code>이라는 메서드를 넣어주었죠. 😄</p>
<p>항상 앞으로 어떤 것이 예상될지를 생각하고, 몇 줄 간단한 정도의 코드만 더 작성해주면 이렇게 당황스러운 일에도 잘 대처할 수 있게 돼요. 저 역시 혹시나 해서 설계를 기존처럼 미리 했었는데, 확장성을 고려한 설계에 대한 보람을 간만에 느꼈어요!</p>
<p>이후 생성에 관한 메인 &amp; 전역 로직들을 모두 적어둘게요.</p>
<h3 id="appts-1">App.ts</h3>
<pre><code class="language-ts">
const $target = document.body;

const {innerWidth, innerHeight} = window;
const app = new MetaballAnimation({
  canvas: new MetaballCanvas({
    gradients: [&#39;#123141&#39;, &#39;#235234&#39;],
    width: innerWidth,
    height: innerHeight,
    type: ECanvasGradientType.linear,
    options: {
      autoplay: true,
    },
  }),
  dataset: {
    dynamic: Array.from({length: 4}, (_, idx) =&gt; {
      const rate = 0.1 * (idx + 1);
      return {
        x: innerWidth * rate,
        y: innerHeight * rate,
        r: 300 * rate,
        v: {x: 10 * rate, y: 1 / rate},
        vWeight: 1 * rate,
      };
    }),
  },
});

function main() {
  const moveStrategy = new MoveStrategy();
  const drawStrategy = new DrawStrategy();
  const fuseStrategy = new FuseStrategy();

  app.setDynamicMetaballMove({
    moveStrategy,
    key: EMetaballObserverKeys.dynamic,
  });

  app.setDynamicMetaballDraw({
    drawStrategy,
    key: EMetaballObserverKeys.dynamic,
  });

  drawStrategy.setAfter(() =&gt; {
    fuseStrategy.exec(app.canvasCtx, app.allMetaballs);
  });

  app.mount($target);
}

main();</code></pre>
<h2 id="결과">결과</h2>
<p>잘 작동하는군요!</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/a16426b4-9a60-4780-b0c5-791b593a5c47/image.gif" alt=""></p>
<h2 id="최적화-이전의-장단점-분석">최적화 이전의 장단점 분석</h2>
<p>일단 장점 먼저 말씀드리자면, <strong>사실상 완전 탐색이니 가장 정확합니다.</strong>
현재 최적화 이전의 로직은 가장 메타볼의 구현 로직을 충실히 따르기 때문에, 융합했을 때 도형이 커지는 현상, 융합하는 현상이 가장 자연스러워요.</p>
<p>다만 단점이 있다면, 지금 보이는 애니메이션처럼 성능이 매우 느립니다.
이를 Chrome에서 10초간 재생했을 때 발생하는 퍼포먼스 비용을 한 번 살펴볼게요.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/464ec657-9dba-4cb7-be9d-d68d7f6319bb/image.png" alt=""></p>
<p>렌더링, 페인팅에 관해서는 빠르게 되어 있는데요! 스크립트 처리 속도가 거의 97%를 차지하는 군요.
(또한, 실제로는 스크립트가 유휴상태를 다 차지해버렸으니, 이 역시 아직 빠르다고 판단하기는 이릅니다.)
네. 이 알고리즘의 단점은 굉장히 느립니다. </p>
<p>실제로 <code>drawStrategy</code>의 <code>exec</code> 메서드는 <code>FuseStrategy</code>까지 합하여 꽤나 많은 렌더링을 차지하고 있어요. 약 2418ms를 차지하고 있네요.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/be0b74b8-6b8a-4a2c-a6bd-0c41007bb4e9/image.png" alt=""></p>
<p>컨텍스트 정보를 <code>restore</code>한 것에 대한 비용이 생각보다 많아 이를 한 번 주석처리하고 렌더링을 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/7da4bc3e-c64d-4103-9c04-27f5004c965d/image.png" alt=""></p>
<p>유휴 상태는 아주 조금 생기기 시작했지만, 실제로 연산은 매우 과하며</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/e43a7cbd-ba1a-4f24-901a-f017a242e822/image.png" alt=""></p>
<p>확실히 여유공간이 생기니, 페인팅에 대한 비용이 증가한 것을 확인할 수 있죠!
이는 <strong>픽셀 전체를 일일이 하나씩 <code>fill</code>하기 때문에 발생한 비용임을 짐작할 수 있습니다.</strong></p>
<blockquote>
<p>따라서 결론은 다음과 같아요. 
<strong>실제로 페인팅과 각 픽셀 전체를 완전탐색하는  알고리즘의 시간 복잡도가 높기 때문에 실제로 사용하기 어렵다.</strong></p>
</blockquote>
<h1 id="🎉-마치며">🎉 마치며</h1>
<p>휴! 드디어 최적화 이전의 로직을 짜는 포스트를 마쳤네요.
사실 짜는 건 어렵지 않은데, 아무래도 더 좋은 코드를 설계하고자 하는 욕심에 더 많이 생각하느라 구현이 늦은 감이 있었네요!</p>
<blockquote>
<p>그렇지만 더 안정적인 설계를 완료했으니, 이에 관련한 최적화 글 역시 더 빠르게 업데이트할 수 있지 않을까요? 😉</p>
</blockquote>
<p>미리 스포를 드리자면, 이제 시리즈로 연재할 &lt;최적화 이후 메타볼 애니메이션 포스트&gt;에서는 다음을 포기합니다.</p>
<ul>
<li>메타볼이 합칠 때 더이상 커지지 않아요.</li>
<li>합치는 로직이 살짝 부자연스러워요.</li>
</ul>
<p>대신 다음을 획득할 수 있어요.</p>
<ul>
<li>어림잡아 약 20배의 성능을 개선할 수 있어요.</li>
<li>벡터를 사용하기에 메타볼이 융합하는 과정에서 발생하는 매끄럽지 않은 선들을 개선할 수 있어요.</li>
</ul>
<blockquote>
<p>힌트를 미리 드리자면, <a href="http://paperjs.org/examples/meta-balls/">PaperJS - Meta Ball</a>로부터 많은 영감을 받았어요. 😉
메타볼 2D 애니메이션을 만드시려던 분들께, 좋은 도움이 되는 포스트가 된다면 좋겠네요. 이상!</p>
</blockquote>
<h1 id="참고자료">참고자료</h1>
<p><a href="https://greentec.github.io/shadertoy-metaball/">메타볼 분석에 관한 도움을 얻었던 글</a>
<a href="https://en.wikipedia.org/wiki/Metaballs">Wikipedia - metaball</a>
<a href="http://paperjs.org/examples/meta-balls/">PaperJS - Meta Ball</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 7. 메타볼 그리는 로직 전략 패턴으로 리팩토링하기]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-7.-%EB%A9%94%ED%83%80%EB%B3%BC-%EA%B7%B8%EB%A6%AC%EB%8A%94-%EB%A1%9C%EC%A7%81-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-7.-%EB%A9%94%ED%83%80%EB%B3%BC-%EA%B7%B8%EB%A6%AC%EB%8A%94-%EB%A1%9C%EC%A7%81-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 27 Mar 2023 13:35:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue | 이슈내용</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">PR | 해당 코드에 대한 commit(0204ad4)</a></li>
</ul>
<p><strong>🗒️ 이 글의 수정 내역</strong> (마지막 수정 일자: 없음)</p>
</blockquote>
<h2 id="draw에도-전략-패턴을-적용하자"><code>draw</code>에도 전략 패턴을 적용하자.</h2>
<p>지난 글에서는 <code>move</code>에 관한 여러 알고리즘들을 쉽게 교체할 수 있도록 전략 패턴을 이용했어요.</p>
<p>그런데 또 고민이 발생했답니다.</p>
<blockquote>
<p>음... 그리는 로직도 결국 전략 패턴을 적용하면 어떨까?</p>
</blockquote>
<p>이유는 다음과 같습니다.</p>
<ul>
<li><p>현재는 아직 그리는 shape에 대해 딱시 생각하지 않아 기본적인 색상으로 완료한 상태입니다. 이런 상황에서 이미 그리는 색상 등을 배정시켜놓는 것은 유연성이 매우 떨어집니다.</p>
</li>
<li><p>수정 비용이 비쌉니다. 현재는 <code>StaticMetaball</code>, <code>DynamicMetaball</code>로 나눠져 있는데, 이러한 <code>Shape</code>을 적용하기 위해서는 데코레이터 패턴을 사용해야 합니다. 이는 또 확장 및 상속을 진행해야 한다는 것인데, 확장에 대한 코드의 복잡성 대비 얻는 효과가 그렇게 큰 지 의문이었습니다. 또한, 다시 수정할 때의 로직 역시 전부 건드려야 하므로 비용이 비쌉니다.</p>
</li>
</ul>
<p>전략 패턴은 이러한 단점 대비 상대적으로 유연하고, 전략만 추가하면 되는 형태이므로 비용이 싸죠. 또한, 결과물이 그렇게 큰 상태가 아닙니다. 그렇기에 전략패턴으로 리팩토링을 진행해보았습니다.</p>
<h2 id="metaballts">Metaball.ts</h2>
<p>일단 기존 로직을 지워주죠!</p>
<pre><code class="language-ts">export class DynamicMetaball implements Metaball {

    // 기존 코드 생략 ...

    // 기존 메서드 내 로직을 모두 복사 후 지운다.
    draw() {}
}</code></pre>
<h2 id="strategiests">Strategies.ts</h2>
<p>이후에는 기존의 전략 추상 클래스 인터페이스에 맞춰 구현해줍시다.
간단하죠?</p>
<pre><code class="language-ts">
export class DrawStrategy implements Strategy {
  before?: (...args: unknown[]) =&gt; void;

  after?: (...args: unknown[]) =&gt; void;

  constructor() {}

  setBefore(callback: (...args: unknown[]) =&gt; void) {
    this.before = callback.bind(this);
  }

  setAfter(callback: (...args: unknown[]) =&gt; void) {
    this.after = callback.bind(this);
  }

  exec(metaball: DynamicMetaball) {
    this.before?.();

    const ctx = metaball.getCtx();

    const {x, y, r} = metaball;

    ctx.save();

    ctx.beginPath();

    ctx.fillStyle = &#39;#f7f711&#39;;
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();

    ctx.closePath();

    ctx.restore();

    this.after?.();
  }
}</code></pre>
<p>사실 이 역시 <code>fillStyle</code>에 들어갈 <code>color</code> 등도 인자로 받는 것이 더 좋습니다.
이는 추후 리팩토링을 하셔도 좋을 것 같아요!</p>
<h3 id="metaballsts">Metaballs.ts</h3>
<p><code>draw</code>에 관한 전략을 설정해줄 수 있도록 코드를 넣어줍시다!</p>
<pre><code class="language-ts">export class DynamicMetaballs implements Metaballs&lt;DynamicMetaball&gt; {
  balls: DynamicMetaball[];

  constructor(
    public moveStrategy?: MoveStrategy,
    public drawStrategy?: DrawStrategy,
  ) {
    this.balls = [];
  }

  setDrawStrategy(drawStrategy: DrawStrategy) {
    this.drawStrategy = drawStrategy;
  }

  moveAll() {
    if (!this.moveStrategy || !this.drawStrategy) return;

    const {moveStrategy, drawStrategy} = this;

    /* eslint-disable-next-line no-console */
    this.balls.forEach(ball =&gt; {
      const move = moveStrategy.exec.bind(moveStrategy);
      const draw = drawStrategy.exec.bind(drawStrategy);

      move(ball);
      draw(ball);
    });
  }</code></pre>
<h3 id="animationsubjectts">AnimationSubject.ts</h3>
<pre><code class="language-ts">export class AnimationSubject implements Subject {
  // 기존 코드 생략...

  public notifyUpdateDrawStrategy({
    drawStrategy,
    key,
  }: IDynamicMetaballDrawStrategy): void {
    this.observers.forEach(observer =&gt; {
      if (observer.key === key) {
        (observer as DynamicMetaballsObserver).updateDrawStrategy(drawStrategy);
        observer.update();
      }
    });
  }
}</code></pre>
<h3 id="animationobserverts">AnimationObserver.ts</h3>
<pre><code class="language-ts">
export class DynamicMetaballsObserver implements MetaballsAnimationObserver {
  // 기존 코드 생략...

  updateDrawStrategy(drawStrategy: DrawStrategy) {
    this.metaballs.setDrawStrategy(drawStrategy);
  }
}</code></pre>
<h3 id="canvasts">Canvas.ts</h3>
<p>이렇게 옵저버까지 전달하기 위해서는 상단의 객체들 역시 메서드를 추가해줘야 해요.
그렇지만 기존 코드를 수정할 일은 전혀 발생하지 않죠. </p>
<blockquote>
<p>즉, 확장에는 열려 있고, 기존 코드의 수정에는 닫혀 있으니 OOP로 잘 설계했다고 생각할 수 있어요 🥰</p>
</blockquote>
<pre><code class="language-ts">  setDynamicMetaballDrawStrategy({
    drawStrategy,
    key,
  }: IDynamicMetaballDrawStrategy) {
    this.metaballAnimationSubject.notifyUpdateDrawStrategy({
      drawStrategy,
      key,
    });
  }
</code></pre>
<h3 id="appts">App.ts</h3>
<p>그러면 App에도 달아줄까요?</p>
<pre><code class="language-ts">
export class MetaballAnimation implements CanvasAnimation {
     // 기존 코드 생략...

    setDynamicMetaballDraw({drawStrategy, key}: IDynamicMetaballDrawStrategy) {
    this.canvas.setDynamicMetaballDrawStrategy({drawStrategy, key});
  }
}</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/db5922f1-6f57-4a14-b43f-20cb5d6bdcc9/image.png" alt=""></p>
<p>생각보다 많이 복잡한 작업임에도 불구하고, 다음과 같은 결과가 나왔어요.
기존의 로직을 전혀 건드리지 않고도 충분히 리팩토링했고, 정상적으로 작동함을 확인했습니다!
꽤나 성공적인 리팩토링 경험이군요 유후!😉</p>
<h1 id="🎉-마치며">🎉 마치며</h1>
<p>전략 패턴은 사실 유연성을 가져다주기에 제 글에서는 무슨 만병통치약처럼 작성되었지만, 단점 역시 많습니다.</p>
<p>저와 같이 따라오면서 느끼셨을텐데, 초기 설정을 해줄 게 굉장히 많아지구요! 또 클래스를 생성하는 것 및 적용하는 데 콜백을 호출하는 데 있어 오버헤드 역시 증가합니다.</p>
<p>따라서 항상 모든 디자인 패턴을 적용할 때는 득과 실을 잘 적용하면서 개발하는 게 좋은 자세인 것 같아요. 🙇🏻‍♂️</p>
<blockquote>
<p>벌써 우리, 이제 최적화 이전의 움직이는 로직까지 모두 구현을 완료했어요.
마지막으로 대망의 융합하는 로직을 구현할 때가 왔군요.
그렇다면, 다시 힘차게 달려보자구요. 이상! 🙆🏻‍♀️🙆🏻</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 6. 메타볼 움직임 구현하기]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-6.-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%9B%80%EC%A7%81%EC%9E%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-6.-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%9B%80%EC%A7%81%EC%9E%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 27 Mar 2023 13:05:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue | 이슈내용</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">PR | 해당 코드에 대한 commit(b74cc4a, f412823, 0204ad4, 5d8d5cd)</a></li>
</ul>
<p><strong>🗒️ 이 글의 수정 내역</strong> (마지막 수정 일자: 없음)</p>
</blockquote>
<p>지난 주는 강아지가 내내 아파서 개발하기가 곤란했어요. 🥲
늦었지만, 부랴부랴 움직임 구현에 대한 글을 써보고자 합니다.</p>
<h2 id="움직임-정의">움직임 정의</h2>
<p>지난 글까지 잘 살펴보셨다면, 이제 animation을 캔버스에서 동작시키는 로직까지 정상적으로 동작할 거에요!</p>
<p>그렇다면, 이 animation이 어떤 것을 연속적으로 보여주는 건지 생각해봅시다.
우리가 이 애니메이션을 동작시킴으로써 달성하려 하는 것은 무엇일까요? 바로 메타볼이 움직이는 거겠죠?</p>
<p>오늘은 이 움직임을 살펴보고자 합니다.
자. 우리가 <code>Metaball</code>에서 <code>x, y, r</code> 등을 다양하게 설정했는데요.
여기서 메타볼의 캔버스 내에서의 position을 담당하고 있는 것은 바로 <code>x</code>, <code>y</code>입니다.</p>
<p>그러니 매우 간단해요. 그냥 <code>x</code> 값과 <code>y</code>값을 변화시켜주면 돼요.
이 과정을 담당하는 메서드를 <code>move</code>라 하겠습니다.    </p>
<h3 id="metaballts">Metaball.ts</h3>
<pre><code class="language-ts">export class DynamicMetaball implements Metaball {
  // ...

  move() {
    this.setX(this.x + this.vx);
    this.setY(this.y + this.vy);
    this.draw();
  }
}</code></pre>
<p>그리고 우리가 값을 변경했으니, 이에 맞는 결과값을 캔버스에 그려내주어야 합니다.
이것을 <code>draw</code>라고 하겠습니다.</p>
<pre><code class="language-ts">export class DynamicMetaball implements Metaball {
  // ...

  draw() {
    this.ctx.save();

    this.ctx.beginPath();

    this.ctx.fillStyle = &#39;#f7f711&#39;;
    this.ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    this.ctx.fill();

    this.ctx.closePath();

    this.ctx.restore();
  }
}</code></pre>
<h2 id="과연-이것이-최선일까">과연 이것이 최선일까</h2>
<p>그런데 말이죠. 이것이 과연 최선일까요?
지금처럼 하게 된다면 어떤 문제가 발생할 수 있을지 다음 리스트를 보며 고민해봅시다.</p>
<ul>
<li><code>DynamicMetaball</code>의 <code>move</code>는 항상 똑같이 정의될까</li>
<li><code>DynamicMetaball</code>의 <code>draw</code>는 항상 똑같이 정의될까</li>
<li>그렇지 않다면 SOLID 원칙에 위배되지는 않는가</li>
</ul>
<p>저는 이번에 이전 로직들의 퍼포먼스를 비교하는 과정에 있어 이러한 문제들을 유념해야 한다는 것을 여실히 깨달았어요. 그리고 현재의 메서드들을 그대로 사용한다면, 현재의 <code>Metaball</code>을 그대로 사용하기는 곤란하죠.</p>
<p>그렇다면 여기서 파생되는 문제는 어떤 게 있을까요? 👀</p>
<ul>
<li><code>DynamicMetaball</code>을 그대로 확장해버리면 <code>draw</code> <code>move</code> 메서드에 대해 리스코프 치환의 원칙을 위배할 가능성이 높아요.</li>
<li><code>DynamicMetaball</code>의 움직임을 그렇다고 다시 또 정의한다는 것은 개방-폐쇄 원칙에 위배되죠.</li>
</ul>
<blockquote>
<p>즉, 확장에 있어서도, 재정의에 있어서도 유연하지가 않아요. 이는 좋은 설계라 보기 힘들었어요.</p>
</blockquote>
<p>따라서 우리는 이러한 문제들을 해결할 방법이 필요하겠군요! 🙇🏻‍♂️</p>
<h2 id="전략-패턴을-사용하자">전략 패턴을 사용하자</h2>
<p>사실 이러한 문제를 해결할 수 있는 방안들이 몇 개가 있습니다.
데코레이터 패턴을 사용할 수도 있을 것 같고, 전략 패턴 등 다양한데요.</p>
<p>그 중 저는 전략패턴을 사용해보려 합니다.
전략패턴을 사용하려는 이유는 다음과 같아요.</p>
<ul>
<li>상속을 통한 오버라이딩으로부터 알고리즘을 분리할 수 있어요. 계속해서 메서드를 오버라이딩하는 과정은 리스코프 치환의 원칙을 위배할 확률이 높습니다. 이것이 항상 나쁜 것은 아니나, 언젠가 유지보수를 할 때 원인추적을 쉽게하기 힘든 문제가 있죠.</li>
<li>또한, 추후 전략에 대해 문제를 수정할 때도 마치 모듈처럼 해당 전략에 대한 객체만 수정해주면 되니 개방-폐쇄 원칙을 만족하죠. 즉 유지보수가 쉬워집니다.</li>
</ul>
<h3 id="strategyts">Strategy.ts</h3>
<p>이제 전략을 만들어볼까요?</p>
<pre><code class="language-ts">abstract class Strategy {
  abstract exec(...args: unknown[]): void;

  abstract before?: (...args: unknown[]) =&gt; void;

  abstract after?: (...args: unknown[]) =&gt; void;
}

export class MoveStrategy implements Strategy {
  before?: (...args: unknown[]) =&gt; void;

  after?: (...args: unknown[]) =&gt; void;

  constructor() {}

  setBefore(callback: (...args: unknown[]) =&gt; void) {
    this.before = callback.bind(this);
  }

  setAfter(callback: (...args: unknown[]) =&gt; void) {
    this.after = callback.bind(this);
  }

  exec(metaball: DynamicMetaball) {
    // console.log(&#39;this: &#39;, metaball, this);
    this.before?.();

    const {
      x,
      y,
      v: {x: vx, y: vy},
    } = metaball;

    metaball.setX(x + vx);
    metaball.setY(y + vy);

    metaball.draw();

    this.after?.();
  }
}</code></pre>
<h3 id="전략-객체에-대해-옵저버에-적용하기">전략 객체에 대해 옵저버에 적용하기</h3>
<p>그렇다면, 이제 우리는 옵저버로부터 이를 적용해주면 되겠죠?
한 번 적용해봅시다.</p>
<h3 id="animationsubject">AnimationSubject</h3>
<pre><code class="language-ts">export class AnimationSubject implements Subject {
  // 이전 코드 중략... 

  public notifyUpdateMoveStrategy({
    moveStrategy,
    key,
  }: IDynamicMetaballMoveStrategy): void {
    this.observers.forEach(observer =&gt; {
      if (observer.key === key) {
        (observer as DynamicMetaballsObserver).updateMoveStrategy(moveStrategy);
        observer.update();
      }
    });
  }
}</code></pre>
<h3 id="animationobserver">AnimationObserver</h3>
<pre><code class="language-ts">export class DynamicMetaballsObserver implements MetaballsAnimationObserver {
  constructor(public metaballs: DynamicMetaballs, public key: string) {}

  updateMoveStrategy(moveStrategy: MoveStrategy) {
    this.metaballs.setMoveStrategy(moveStrategy);
  }

  // 이전 코드...
}</code></pre>
<p>이렇게 하면 우리는 <code>Subject</code>를 통해 구독한 옵저버들에게 <code>MoveStrategy</code>에 대한 것을 전달하여 해당 알고리즘을 시킬 수 있죠.</p>
<p>이때, 나머지들의 로직을 구체화하여 <code>move</code>를 실제로 만들어봅시다!</p>
<h3 id="appts">App.ts</h3>
<pre><code class="language-ts">
export class MetaballAnimation implements CanvasAnimation {
  // 이전 코드 생략...

  setDynamicMetaballMove({moveStrategy, key}: IDynamicMetaballMoveStrategy) {
    this.canvas.setDynamicMetaballMoveStrategy({moveStrategy, key});
  }

  // 이전 코드 생략...
}

const $target = document.body;

const app = new MetaballAnimation({
  canvas: new MetaballCanvas({
    gradients: [&#39;#123141&#39;, &#39;#235234&#39;],
    width: window.innerWidth,
    height: window.innerHeight,
    type: ECanvasGradientType.linear,
    options: {
      autoplay: true,
    },
  }),
  dataset: {
    static: [{x: 30, y: 100, r: 20}],
    dynamic: [
      {
        x: 120,
        y: 60,
        r: 20,
        v: {x: 0.1, y: 0.1},
        vWeight: 1,
      },
    ],
  },
});

function main() {
  const moveStrategy = new MoveStrategy();

  app.setDynamicMetaballMove({
    moveStrategy,
    key: EMetaballObserverKeys.dynamic,
  });

  app.mount($target);
}

main();
</code></pre>
<h3 id="canvasts">Canvas.ts</h3>
<pre><code class="language-ts">
export class MetaballCanvas implements GradientCanvas {
  // 이전 코드 생략...

  constructor({
    type,
    width,
    height,
    gradients,
    options,
  }: Omit&lt;
    GradientCanvas,
    | &#39;$canvas&#39;
    | &#39;ctx&#39;
    | &#39;render&#39;
    | &#39;mount&#39;
    | &#39;draw&#39;
    | &#39;getLinearGradient&#39;
    | &#39;getRadialGradient&#39;
  &gt;) {
    // 이전 코드 생략...

    this.init();
  }

  init() {
    this.$canvas.width = this.width;
    this.$canvas.height = this.height;
  }

  setDynamicMetaballMoveStrategy({
    moveStrategy,
    key,
  }: IDynamicMetaballMoveStrategy) {
    this.metaballAnimationSubject.notifyUpdateMoveStrategy({
      moveStrategy,
      key,
    });
  }

  draw(background: CanvasGradient) {
    this.ctx.clearRect(0, 0, this.width, this.height);

    this.ctx.fillStyle = background;

    this.ctx.fillRect(0, 0, this.width, this.height);

    // 기존의 컨텍스트 정보를 바뀌지 않도록 저장해줍시다.
    this.ctx.save();
  }
}
</code></pre>
<p>이렇게 하면 이제, 우리는 <code>main</code>을 통해 호출하여 원하는 <code>moveStrategy</code>를 바깥쪽에서 달아줄 수 있게 되었어요 😄</p>
<p>한 번 동작시켜볼까요?</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/51a833f6-76b0-4823-a613-f21d0115df1b/image.gif" alt=""></p>
<p>잘 동작하는군요!</p>
<h1 id="🎉-마치며">🎉 마치며</h1>
<p>가장 힘든 주말을 보냈어요.
강아지가 아픈 바람에, 오늘까지 해야 할 개발도 제대로 하질 못했네요. </p>
<p>물론 지금도 전혀 낫지를 않아서 마음을 졸이고 있지만, 
마음이 아픈 만큼 더 간절하게, 열심히 개발해야겠다는 생각을 갖게 되었어요.</p>
<blockquote>
<p>이번에 꽤나 긴 글로 작성되었는데, 끝까지 읽어주셔서 감사드려요 🥰
전략 패턴은 생각보다 많은 곳에서 사용되는 패턴이니, 알아두면 정말 좋은 것 같아요.</p>
<p>다음에는, <code>draw</code> 로직에 대해 좀 더 재사용할 수 있게 할 수 있지 않을까 고민하며, 이를 리팩토링하는 시간을 갖도록 할게요. 이상!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 5.  캔버스 애니메이션 실행하기]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-4.-%EC%BA%94%EB%B2%84%EC%8A%A4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-4.-%EC%BA%94%EB%B2%84%EC%8A%A4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 24 Mar 2023 11:36:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue | 객관적인 퍼포먼스 비교를 위해 메타볼 애니메이션 OOP로 재설계한다. #36</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">PR | 해당 코드에 대한 commit(c699507, 7cca503 참고!)</a></li>
</ul>
<p><strong>🗒️ 이 글의 수정 내역</strong> (마지막 수정 일자: 23.03.27)</p>
<ul>
<li><code>initializeMetaballs</code>의 분기처리를 수정했어요. (<code>else if</code> → <code>if</code>)</li>
</ul>
</blockquote>
<blockquote>
<p>혹시나 따라오는 데 오류가 있으시다면, 위 URL의 레포지토리의 <code>PR</code> 및 코드들을 살펴봐주세요!</p>
</blockquote>
<h2 id="requestanimationframe">requestAnimationFrame</h2>
<p>자, 이제 본격적으로 캔버스 애니메이션을 동작시켜볼 거에요.
일단 먼저 <code>window.requestAnimationFrame</code>을 알아보아야 합니다.
이 함수는 말 그대로 프레임을 요청합니다. 그리고 이를 호출하면, 1프레임을 실행한 상태 변경을 만들어내죠.</p>
<p>이 글은 메타볼 애니메이션 구현과 리팩토링하는 것이 주가 되는 글이기에, 설명은 생략하겠습니다. 자세한 것은 <a href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame">MDN - Window.requestAnimationFrame()</a>을 참고해주세요.</p>
<p>우리는 이것을 캔버스에서 실행함으로써, 애니메이션을 구동할 거에요.
다음과 같이 2번 시리즈에서 만들어낸 <code>Canvas</code> 클래스를 수정해주세요.</p>
<p>간단히 설명 드리자면 <code>mount</code>와 <code>render</code>, <code>animate</code> 코드가 중요합니다.</p>
<pre><code class="language-ts">  mount($target: Element) {
    $target.appendChild(this.$canvas);

    this.render();

    if (!this.options?.pause &amp;&amp; this.options?.autoplay) {
      this.animate();
    }
  }

  render() {
    this.draw(this.canvasGradient);

    this.metaballAnimationSubject.notify();
  }

  animate() {
    if (this.options?.pause) return;

    this.render();

    requestAnimationFrame(this.animate.bind(this));
</code></pre>
<p>설정된 옵션에 따라서 <code>mount</code> <code>render</code> <code>animate</code>를 진행해주는데요.</p>
<p>이때, <code>animate</code> 안에는 또 <code>requestAnimationFrame</code>의 콜백으로 같은 함수가 호출됩니다.
이를 통해, 재귀적으로 실행하며 무한하게 애니메이션을 호출할 수 있는 거에요.</p>
<p>그런데 이것이 싫을 수 있잖아요?! 그럴 때를 대비하여 메타 정보로 <code>options</code>에 <code>pause</code>를 추가해주었어요. 이렇게 하면 애니메이션은 원할 때 해당 <code>property</code>를 변경하여 끌 수 있겠죠. 😉</p>
<p>잘 동작하는지 metaballs의 <code>moveAll</code> 메서드에 콘솔을 넣어 확인해보죠!</p>
<pre><code class="language-ts">export class StaticMetaballs implements Metaballs&lt;StaticMetaball&gt; {
  // ...

  moveAll() {
    /* eslint-disable-next-line no-console */
    console.log(&#39;moveAll!&#39;);
  }
}
export class DynamicMetaballs implements Metaballs&lt;DynamicMetaball&gt; {
  // ...

  moveAll() {
    /* eslint-disable-next-line no-console */
    console.log(&#39;moveAll!&#39;);
  }
}</code></pre>
<p><strong>윽! 아직 동작하지 않죠?</strong> 😖
정상입니다. 아직 메타볼에 대한 초기값을 넣어주지 않았기 때문이에요.</p>
<h2 id="메타볼-정보-넣어서-초기화하기">메타볼 정보 넣어서 초기화하기</h2>
<p>자 그러면 이제 메타볼의 속성들을 세팅해줘서 정상적으로 동작하는지를 보겠습니다! 🙆🏻 </p>
<p>먼저 <code>App.ts</code>에 다음과 같이 맨 아래에 코드를 넣어 호출해볼게요.</p>
<pre><code class="language-ts">
const app = new MetaballAnimation({
  canvas: new MetaballCanvas({
    gradients: [&#39;#123141&#39;, &#39;#235234&#39;],
    width: 400,
    height: 400,
    type: ECanvasGradientType.linear,
    options: {
      autoplay: true,
    },
  }),
  dataset: {
    static: [{x: 30, y: 100, r: 20}],
    dynamic: [
      {
        x: 30,
        y: 100,
        r: 20,
        v: {x: 0.1, y: 0.1},
        vWeight: 1,
      },
    ],
  },
});

app.mount($target);</code></pre>
<p>여기서 <code>dataset</code>이라는 property가 추가되었네요. 이 친구가 세팅되는 순간, <code>static</code>과 <code>dynamic</code> 메타볼들이 만들도록 호출될 거에요.</p>
<p>사실 <code>Canvas</code>라는 걸 생성할 때 만들어줄까 생각해보았어요.
하지만 가장 구체적인 클래스가 데이터를 가지고, 하위 클래스들에게 메서드를 통해 전달하는 것이 하위 클래스와 데이터에 대한 결합성을 낮추고, 좀 더 유연하게 가져갈 수 있다고 생각하여 다음과 같이 설계했습니다. 😉</p>
<p>그러면 이를 동작시키기 위한 코드를 구현하면 되겠죠?
나머지를 입력해주죠.</p>
<pre><code class="language-ts">
export class MetaballAnimation implements CanvasAnimation {
  public canvas: CanvasAnimation[&#39;canvas&#39;];

  public dataset: IMetaballDataset;

  constructor({
    canvas,
    dataset,
  }: {
    canvas: CanvasAnimation[&#39;canvas&#39;];
    dataset?: IMetaballDataset;
  }) {
    this.canvas = canvas;

    this.dataset = dataset ?? {
      static: [],
      dynamic: [],
    };

    if (this.dataset?.static?.length || this.dataset?.dynamic?.length) {
      this.initializeMetaballs(this.dataset);
    }
  }

  initializeMetaballs(dataset: IMetaballDataset) {
    this.canvas.initializeMetaballs(dataset);
  }

  // ... 생략
}</code></pre>
<p>자. 그러면 우리는 <code>initializeMetballs</code>라는 메서드를 <code>options</code>가 시작할 때 데이터가 들어와 있으면 호출해주죠?!</p>
<p>이제 <code>canvas</code>에서는 어떻게 써야 할까요? 한 번 살펴봅시다.</p>
<pre><code class="language-ts">// Canvas.ts

export class MetaballCanvas implements GradientCanvas {
  $canvas: GradientCanvas[&#39;$canvas&#39;];

  ctx: GradientCanvas[&#39;ctx&#39;];

  type: GradientCanvas[&#39;type&#39;];

  width: GradientCanvas[&#39;width&#39;];

  height: GradientCanvas[&#39;height&#39;];

  gradients: GradientCanvas[&#39;gradients&#39;];

  metaballAnimationSubject: AnimationSubject;

  staticMetaballsFactory: StaticMetaballsFactory;

  dynamicMetaballsFactory: DynamicMetaballsFactory;

  options: GradientCanvas[&#39;options&#39;];

  constructor({
    type,
    width,
    height,
    gradients,
    options,
  }: Omit&lt;
    GradientCanvas,
    | &#39;$canvas&#39;
    | &#39;ctx&#39;
    | &#39;render&#39;
    | &#39;mount&#39;
    | &#39;draw&#39;
    | &#39;getLinearGradient&#39;
    | &#39;getRadialGradient&#39;
  &gt;) {
    this.$canvas = document.createElement(&#39;canvas&#39;);

    this.ctx = this.$canvas.getContext(&#39;2d&#39;) as CanvasRenderingContext2D;

    this.type = type;

    this.width = width;
    this.height = height;

    this.gradients = gradients;

    this.metaballAnimationSubject = new AnimationSubject({ctx: this.ctx});

    this.staticMetaballsFactory = new StaticMetaballsFactory();
    this.dynamicMetaballsFactory = new DynamicMetaballsFactory();

    this.options = options ?? {
      radialGradient: {r0: 0, r1: 0},
      autoplay: false,
      pause: false,
    };
  }

  initializeMetaballs(dataset: IMetaballDataset) {
    if (dataset.static) {
      const staticMetaballs = this.staticMetaballsFactory.create({
        options: {
          ctx: this.ctx,
          data: dataset.static,
        },
      });

      this.metaballAnimationSubject.subscribe(
        new StaticMetaballsObserver(staticMetaballs, &#39;static&#39;),
      );
    } 

    if (dataset.dynamic) {
      const dynamicMetaballs = this.dynamicMetaballsFactory.create({
        options: {
          ctx: this.ctx,
          data: dataset.dynamic,
        },
      });

      this.metaballAnimationSubject.subscribe(
        new DynamicMetaballsObserver(dynamicMetaballs, &#39;dynamic&#39;),
      );
    }
  }

  // ...
}</code></pre>
<p>이제 여기서 <code>Factory</code>들을 쓰게 되는군요!
이 친구들을 덕분에 일련의 생성 로직들을 생각하지 않고 팩토리 메서드만으로 만들어내니 굉장히 코드가 간결해졌죠?</p>
<p>이후 <code>subscribe</code> 내부에서 옵저버를 생성함과 동시에 달아주는군요! 어썸합니다 🚀</p>
<p>자. 그러면 이제 결과를 볼까요?</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/c9bc40bf-184d-41f1-aba0-1c24b0bfe99e/image.png" alt=""></p>
<p>오! 잘 작동되는군요.</p>
<h2 id="결과-코드">결과 코드</h2>
<p>아무래도 긴 글이다 보니 따라오기 쉽지 않았을 것 같아요.
결과 코드를 공유 드리며, 이마저도 안된다면, <a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">commit</a>을 살펴봐주세요!</p>
<h3 id="canvas"><code>Canvas</code></h3>
<pre><code class="language-ts">export class MetaballCanvas implements GradientCanvas {
  $canvas: GradientCanvas[&#39;$canvas&#39;];

  ctx: GradientCanvas[&#39;ctx&#39;];

  type: GradientCanvas[&#39;type&#39;];

  width: GradientCanvas[&#39;width&#39;];

  height: GradientCanvas[&#39;height&#39;];

  gradients: GradientCanvas[&#39;gradients&#39;];

  metaballAnimationSubject: AnimationSubject;

  staticMetaballsFactory: StaticMetaballsFactory;

  dynamicMetaballsFactory: DynamicMetaballsFactory;

  options: GradientCanvas[&#39;options&#39;];

  constructor({
    type,
    width,
    height,
    gradients,
    options,
  }: Omit&lt;
    GradientCanvas,
    | &#39;$canvas&#39;
    | &#39;ctx&#39;
    | &#39;render&#39;
    | &#39;mount&#39;
    | &#39;draw&#39;
    | &#39;getLinearGradient&#39;
    | &#39;getRadialGradient&#39;
  &gt;) {
    this.$canvas = document.createElement(&#39;canvas&#39;);

    this.ctx = this.$canvas.getContext(&#39;2d&#39;) as CanvasRenderingContext2D;

    this.type = type;

    this.width = width;
    this.height = height;

    this.gradients = gradients;

    this.metaballAnimationSubject = new AnimationSubject({ctx: this.ctx});

    this.staticMetaballsFactory = new StaticMetaballsFactory();
    this.dynamicMetaballsFactory = new DynamicMetaballsFactory();

    this.options = options ?? {
      radialGradient: {r0: 0, r1: 0},
      autoplay: false,
      pause: false,
    };
  }

  initializeMetaballs(dataset: IMetaballDataset) {
    if (dataset.static) {
      const staticMetaballs = this.staticMetaballsFactory.create({
        options: {
          ctx: this.ctx,
          data: dataset.static,
        },
      });
      this.metaballAnimationSubject.subscribe(
        new StaticMetaballsObserver(staticMetaballs, &#39;static&#39;),
      );
    } 

    if (dataset.dynamic) {
      const dynamicMetaballs = this.dynamicMetaballsFactory.create({
        options: {
          ctx: this.ctx,
          data: dataset.dynamic,
        },
      });

      this.metaballAnimationSubject.subscribe(
        new DynamicMetaballsObserver(dynamicMetaballs, &#39;dynamic&#39;),
      );
    }
  }

  getLinearGradient() {
    const result = this.ctx.createLinearGradient(0, 0, 0, this.height);

    this.gradients.forEach((gradient, idx) =&gt; {
      result.addColorStop(idx, gradient);
    });

    return result;
  }

  getRadialGradient({r0 = 0, r1 = 0}: IRadialGradientOptions) {
    const result = this.ctx.createRadialGradient(0, 0, r0, 0, this.height, r1);

    this.gradients.forEach((gradient, idx) =&gt; {
      result.addColorStop(idx, gradient);
    });

    return result;
  }

  get canvasGradient(): CanvasGradient {
    switch (this.type) {
      case ECanvasGradientType.linear: {
        return this.getLinearGradient();
      }
      case ECanvasGradientType.radial: {
        return this.getRadialGradient(
          this.options?.radialGradient ?? {r0: 0, r1: 0},
        );
      }
      default: {
        return this.getLinearGradient();
      }
    }
  }

  draw(background: CanvasGradient) {
    this.ctx.clearRect(0, 0, this.width, this.height);

    this.ctx.fillStyle = background;

    this.ctx.fillRect(0, 0, this.width, this.height);
  }

  mount($target: Element) {
    $target.appendChild(this.$canvas);

    this.render();

    if (!this.options?.pause &amp;&amp; this.options?.autoplay) {
      this.animate();
    }
  }

  render() {
    this.draw(this.canvasGradient);

    this.metaballAnimationSubject.notify();
  }

  animate() {
    if (this.options?.pause) return;

    this.render();

    requestAnimationFrame(this.animate.bind(this));
  }
}</code></pre>
<h3 id="app">App</h3>
<pre><code class="language-ts">import {MetaballCanvas} from &#39;./Canvas&#39;;

import {ECanvasGradientType, IMetaballDataset} from &#39;./types&#39;;

export abstract class CanvasAnimation {
  abstract canvas: MetaballCanvas;

  abstract mount($target: Element): void;

  abstract render(): void;
}

export class MetaballAnimation implements CanvasAnimation {
  public canvas: CanvasAnimation[&#39;canvas&#39;];

  public dataset: IMetaballDataset;

  constructor({
    canvas,
    dataset,
  }: {
    canvas: CanvasAnimation[&#39;canvas&#39;];
    dataset?: IMetaballDataset;
  }) {
    this.canvas = canvas;

    this.dataset = dataset ?? {
      static: [],
      dynamic: [],
    };

    if (this.dataset?.static?.length || this.dataset?.dynamic?.length) {
      this.initializeMetaballs(this.dataset);
    }
  }

  initializeMetaballs(dataset: IMetaballDataset) {
    this.canvas.initializeMetaballs(dataset);
  }

  mount($target: Element) {
    const $metaballAnimation = document.createElement(&#39;div&#39;);
    $metaballAnimation.className = &#39;metaball-animation&#39;;

    $target.appendChild($metaballAnimation);

    this.canvas.mount($metaballAnimation);
  }

  render() {
    this.canvas.render();
  }
}

const $target = document.body;

const app = new MetaballAnimation({
  canvas: new MetaballCanvas({
    gradients: [&#39;#123141&#39;, &#39;#235234&#39;],
    width: 400,
    height: 400,
    type: ECanvasGradientType.linear,
    options: {
      autoplay: true,
    },
  }),
  dataset: {
    static: [{x: 30, y: 100, r: 20}],
    dynamic: [
      {
        x: 30,
        y: 100,
        r: 20,
        v: {x: 0.1, y: 0.1},
        vWeight: 1,
      },
    ],
  },
});

app.mount($target);</code></pre>
<h1 id="🎉-마치며">🎉 마치며</h1>
<p>생각보다 깔끔한 설계란 어려운 것 같아요.
저 역시 이번에 리팩토링을 한 번 거칠 수밖에 없었습니다. 데이터를 전달하는 로직에 대한 착각들이 주였어요.</p>
<p>그렇기에 이번에 깨달은 건, data를 실제로 handle하는 상황을 미리 만든 다음, 이를 구현시킬 수 있도록 짜는 방식으로 해야겠다는 것을 깨달았어요.</p>
<p>그런데 정말 뿌듯한 경험을 했어요.
확실히 의존성을 느슨하게 결합하면서 디자인 패턴을 토대로 구현하니, 각 컴포넌트의 변경에 따라 다른 컴포넌트의 변경이 현저히 줄어들었다는 점입니다.</p>
<blockquote>
<p>이게 정말 좋은 설계를 통해 만들어낸 코드의 매력이 아닐까요?
싶으며 혼자 만족하고 취하고(...) 이만 마칩니다. 🤣
이제 모든 설계는 끝났네요. 다음부터는 본격적으로 메타볼들을 만들어보죠! 이상 🌈</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 4. Metaballs의 업데이트 로직을 옵저버 패턴으로 설계하기]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-4.-Metaballs%EC%9D%98-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%A1%9C%EC%A7%81%EC%9D%84-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-4.-Metaballs%EC%9D%98-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%A1%9C%EC%A7%81%EC%9D%84-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 24 Mar 2023 04:12:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue | 객관적인 퍼포먼스 비교를 위해 메타볼 애니메이션 OOP로 재설계한다. #36</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">PR | 해당 코드에 대한 commit(1b64f2, 1ba731 참고!)</a></li>
</ul>
<p><strong>🗒️ 이 글의 수정 내역</strong> (마지막 수정 일자: 23.03.24)</p>
<ul>
<li><strong>23.03.24</strong> - 코드 상에서 key 프로퍼티의 initialization이 누락되었음을 발견했고 수정하였습니다. (commit: <code>c699507</code>)</li>
</ul>
</blockquote>
<h2 id="설계-배경">설계 배경</h2>
<p>자. 이전 포스트까지 <code>Metaballs</code>를 생성 패턴을 이용해서 구현해봤어요.
하지만 이 객체를 그대로 쓰기에는 좋지 않다고 생각했어요. 왜냐구요?</p>
<ul>
<li><code>Metaballs</code>의 상태를 직접적으로 일일이 다루는 건, 각 객체간의 결합도가 너무 커요.</li>
<li>결합도가 크면, 변화하기 쉬운 객체(<code>Metaball</code>)에 대한 의존성이 커지겠군요. 즉, Side Effect가 발생할 가능성이 높다는 이야기이며, 이는 추후 유지보수의 증가로 이어질 거 같아요.</li>
</ul>
<p>따라서 저는 옵저버 패턴을 이용하여, 상태 업데이트 로직을 좀 더 객체지향적으로 설계해보려 합니다.</p>
<h2 id="왜-옵저버-패턴을-썼을까">왜 옵저버 패턴을 썼을까</h2>
<p>옵저버 패턴은 특정 상태가 변경되는 시점을 마치 &#39;이벤트&#39;라고 생각합니다.
그리고 이 이벤트가 발생하면, 주제를 구독하는 것들에게 업데이트하라고 알림을 주게 되는 거죠.</p>
<p>이렇게 관리하면, 결과적으로 상태와 상태에 따른 객체들의 변화를 좀 더 분리해서 사용할 수 있어요. 그리고 그리고 이러한 분리는 곧 객체간의 의존성을 느슨하게 결합할 수 있다는 의미기도 하지요!</p>
<p>따라서, 각 객체가 각자의 일을 하면서도, 상태 변화에 있어서는 일관성 있게 설계해보면 어떨까 싶어 옵저버 패턴을 채택했습니다.</p>
<h2 id="subject-observer-추상-클래스-구현">Subject, Observer 추상 클래스 구현</h2>
<p>자. 일단 인터페이스들을 추상적으로 구현한 클래스를 만들어보죠.</p>
<p>이 두 개는 다음과 같은 역할을 담당하는 객체를 추상화한 것입니다.</p>
<ul>
<li><code>Subject</code>: <code>Observer</code>가 구독하고 있는 일종의 &#39;State Theme&#39;입니다.</li>
<li><code>Observer</code>: <code>Subject</code>의 알림에 따라 반응하는 객체입니다.</li>
</ul>
<pre><code class="language-ts">export abstract class Subject {
  public abstract observers: Set&lt;Observer&gt;;

  public abstract subscribe(observer: Observer): void;

  public abstract unSubscribe(observer: Observer): void;

  public abstract notify(): void;
}

export abstract class Observer {
  abstract key: string;

  abstract update(...args: unknown[]): void;
}</code></pre>
<p>몇 줄만에 우리는 데이터 상태 변화에 따른 로직을 느슨하게 관리할 객체를 만들어버렸어요. 매우 직관적이고 간단하죠?
그리고 이 간단한 인터페이스가 곧, 느슨한 결합을 가능케 하므로, 개발에 있어서 많이 애용하는 패턴이기도 합니다.</p>
<blockquote>
<p>물론 옵저버 패턴 역시 많아지면 순서에 따른 Side Effect 발생이라던지, 디버깅에 있어 복잡성이 증가하는 단점이 있지만, 우리가 만들 애니메이션은 그 정도의 복잡성이 발생하지는 않을 것 같았어요. 따라서 단점이 크게 제약을 주진 않을 것 같군요! 🥰</p>
</blockquote>
<h2 id="구체-클래스-구현">구체 클래스 구현</h2>
<p>자. 이제 우리는 <code>animation</code>의 동작에 따라 메타볼들을 업데이트하기 위해, 이를 옵저버 패턴을 이용하여 구현해볼 거에요!</p>
<h3 id="animationsubject">AnimationSubject</h3>
<p>이 친구는 애니메이션이 동작할 때마다 구독한 옵저버들에게 업데이트를 하라고 알려줄 거에요.</p>
<pre><code class="language-ts">import {Observer} from &#39;~/src/design-pattern/observer/Observer&#39;;
import {Subject} from &#39;~/src/design-pattern/observer/Subject&#39;;

interface IAnimationSubjectParams {
  ctx: CanvasRenderingContext2D;
}

export class AnimationSubject implements Subject {
  ctx: CanvasRenderingContext2D;

  observers: Set&lt;Observer&gt;;

  constructor({ctx}: IAnimationSubjectParams) {
    this.ctx = ctx;
    this.observers = new Set&lt;Observer&gt;();
  }

  public subscribe(observer: Observer): void {
    this.observers.add(observer);
  }

  public unSubscribe(observer: Observer): void {
    this.observers.delete(observer);
  }

  public notify(): void {
    this.observers.forEach(observer =&gt; {
      observer.update();
    });
  }
}</code></pre>
<h3 id="metaballsanimationobserver">MetaballsAnimationObserver</h3>
<blockquote>
<p><strong>23.03.24</strong> - 해당 코드는 <a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">해당 commit</a>에서 key가 누락되었음을 확인하여, 이를 수정하였습니다.</p>
</blockquote>
<pre><code class="language-ts">export abstract class MetaballsAnimationObserver {
  public abstract metaballs: StaticMetaballs | DynamicMetaballs;

  public abstract update(): void;
}

export class StaticMetaballsObserver implements MetaballsAnimationObserver {
  constructor(public metaballs: StaticMetaballs, public key: string) {}

  update() {
    this.metaballs.moveAll();
  }
}

export class DynamicMetaballsObserver implements MetaballsAnimationObserver {
  constructor(public metaballs: DynamicMetaballs, public key: string) {}

  update() {
    this.metaballs.moveAll();
  }
}
</code></pre>
<p>자! 이제 우리는 상태 변화를 하기 위한 설계를 모두 완료했군요!</p>
<h1 id="🎉-마치며">🎉 마치며</h1>
<p>아무래도 크게 이러한 구현과 실제 적용 로직까지 하면 글이 길어질 거고, 집중도가 낮아지기 때문에 이만 글을 마치려 합니다.</p>
<blockquote>
<p>다음에는 이제 본격적으로 캔버스에서 <code>requestFrame</code>을 동작시킬 거에요. 야호!
이제 우리가 만든 것들을 레고 조립하듯이, 서로 연결시켜보자구요! 🙆🏻🙆🏻‍♀️</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 3. 팩토리 메서드 패턴으로 메타볼 핸들링 객체 생성하기]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-3.-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%A9%94%ED%83%80%EB%B3%BC-%ED%95%B8%EB%93%A4%EB%A7%81-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-3.-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%A9%94%ED%83%80%EB%B3%BC-%ED%95%B8%EB%93%A4%EB%A7%81-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 23 Mar 2023 14:58:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue | 객관적인 퍼포먼스 비교를 위해 메타볼 애니메이션 OOP로 재설계한다. #36</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">PR | 해당 코드에 대한 commit(c699507, b24089b 참고!)</a></li>
</ul>
<p><strong>🗒️ 이 글의 수정 내역</strong> (마지막 수정 일자: 22.03.24)</p>
<ul>
<li><strong>23.03.24</strong> - 이후 시리즈 5번을 만드는 과정에서, 실제로 엮는데 불편했던 로직을 리팩토링하여 개선했어요. (commit - <code>c699507</code>)</li>
</ul>
</blockquote>
<h2 id="메타볼-핸들링-객체-생성">메타볼 핸들링 객체 생성</h2>
<p>제 캔버스에는 수많은 메타볼 도형들이 위치하고 있습니다.
그리고 이 핸들링 객체들은 각각의 특징을 담고 있어요.</p>
<ul>
<li><code>Static</code>한 메타볼이 있는가 하면,</li>
<li><code>Dynamic</code>한 메타볼이 있어요.<ul>
<li>그런데 이 <code>Dynamic</code>한 메타볼 중에서는 일부는 주변을 벗어나지 않고</li>
<li>일부는 주변에서 벗어나면서, 일정 범위를 벗어나면 터집니다.</li>
</ul>
</li>
</ul>
<p>그리고 이러한 옵션들은 매우 수많이 존재할 수 있겠죠?
따라서, 이러한 메타볼 객체들을 관리해야 합니다.</p>
<p>그런데 봅시다. <code>Canvas</code>의 역할은 무엇일까요?
&#39;어떤 일련의 애니메이션을 동작시키는 역할&#39;이라는 확실한 단일 책임을 갖고 있어요.</p>
<p>이때, &#39;메타볼까지 책임지고 다 핸들링해줘!&#39;라고 한다면 어떨까요?
이는 결합도를 높일 뿐 아니라, 캔버스에 대한 확장성을 떨어뜨릴 가능성, 그리고 메타볼의 일부가 변경되는 순간 <code>Canvas</code>의 로직이 실수로 변경될 위험에도 노출되겠죠.</p>
<p>따라서 이를 생성하기 위한 고민을 하게 되었는데요.
이번에는 팩토리 메서드 패턴을 통해 이를 핸들링하기로 결심했답니다.</p>
<h2 id="metaballs-클래스-구현"><code>Metaballs</code> 클래스 구현</h2>
<p><code>Metaballs</code>의 경우 다음과 같은 구조로 구현했어요!</p>
<blockquote>
<ul>
<li><code>Metaballs</code>(추상 클래스)<ul>
<li><code>StaticMetaballs</code>(서브 클래스)</li>
<li><code>DynamicMetaballs</code>(서브 클래스)</li>
</ul>
</li>
</ul>
</blockquote>
<p>사실상 <code>Metaball</code>과 같은 느낌의 구조죠?
정말 순수하게 일련의 <code>Metaball</code>들의 핸들링만을 책임지는 친구라서, 똑같이 가져가는 게 맞다고 생각했어요!</p>
<p>코드는 다음과 같아요.</p>
<blockquote>
<p><strong>23.03.24</strong> - 해당 코드는 <a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">해당 commit</a>에서 Metaballs를 완전히 추상 클래스로 해야겠다고 결정하였고, 따라서 <code>push</code> 메서드를 추상화했습니다. 처음 보시는 분들은 무시하셔도 돼요! 🙇🏻‍♂️</p>
</blockquote>
<pre><code class="language-ts">import {DynamicMetaball, StaticMetaball} from &#39;./Metaball&#39;;

abstract class Metaballs&lt;MetaballType&gt; {
  abstract balls: MetaballType[];

  abstract push(metaball: MetaballType): void;

  abstract moveAll(): void;
}

export class StaticMetaballs implements Metaballs&lt;StaticMetaball&gt; {
  balls: StaticMetaball[];

  constructor() {
    this.balls = [];
  }

  push(metaball: StaticMetaball) {
    this.balls.push(metaball);
  }

  moveAll() {
    /* eslint-disable-next-line no-console */
    console.log(&#39;moveAll!&#39;);
  }
}
export class DynamicMetaballs implements Metaballs&lt;DynamicMetaball&gt; {
  balls: DynamicMetaball[];

  constructor() {
    this.balls = [];
  }

  push(metaball: DynamicMetaball): void {
    this.balls.push(metaball);
  }

  moveAll() {
    /* eslint-disable-next-line no-console */
    console.log(&#39;moveAll!&#39;);
  }
}</code></pre>
<p>그러면 우리는 생성 패턴을 이용해서 한 번 이 <code>Metaballs</code>를 생성해볼게요.</p>
<h2 id="왜-생성-패턴을-쓰나요">왜 생성 패턴을 쓰나요?</h2>
<p>가령 다음과 같은 코드를 다양한 객체에서 쓴다고 가정합시다.</p>
<pre><code class="language-ts">class A {
    // ...
      bar() {
        const a = new Metaballs(options1);
    }
}

class B {
    // ...
      foo() {
        const a = new Metaballs(options2);
    }
}

class ZZZ {
    // ...
      baz() {
        const a = new Metaballs(options3);
    }
}</code></pre>
<p>이때, <code>Metaballs</code>의 이름이 바뀌었다던지, <code>Metaballs</code>라는 클래스에서 수정할 게 발생한다면, 모든 클래스 내부를 수정해야 하죠. 즉 이는 개방-폐쇄의 원칙을 위배하게 됩니다.</p>
<p>하지만 이러한 생성의 책임을 하나의 클래스가 맡게 된다면 어떨까요?</p>
<pre><code class="language-ts">class A {
    // ...
      bar() {
        const a = new MetaballsFactory();

          MetaballsFactory.create();
    }
}

class B {
    // ...
      foo() {
        const a = new MetaballsFactory();

          MetaballsFactory.create();
    }
}

class ZZZ {
    // ...
      baz() {
        const a = new MetaballsFactory();

          MetaballsFactory.create();
    }
}</code></pre>
<p>내부의 생성 로직들 모두가 추상화가 되어 선언적이면서도 <code>MetaballsFactory</code>만 수정하면 되므로 더욱 SOLID 원칙에 부합해집니다.</p>
<p>애초부터 객체지향적으로 설계하지 않아 트라우마가 생겨 시작한 포스트니(...) 한 번 생성 패턴을 적용하기로 했어요! 🙆🏻‍♀️🙆🏻</p>
<blockquote>
<p>그리고 저는 이들 중, 팩토리 메서드 패턴을 사용하였습니다.</p>
</blockquote>
<h2 id="왜-팩토리-메서드-패턴으로-했는가">왜 팩토리 메서드 패턴으로 했는가</h2>
<p>일단 생성 패턴에 관하여 직관적으로 당장 떠오르는 건 다음과 같았어요.</p>
<ul>
<li>빌더 패턴</li>
<li>추상 팩토리 패턴</li>
<li>팩토리 메서드 패턴</li>
</ul>
<p>사실 어떠한 패턴을 쓰던지 간에 별 문제가 발생하지 않습니다.
다만 팩토리 메서드 패턴을 쓴 이유는, 다음과 같은 이유에서 좀 더 가장 적합하다 생각했기 때문입니다.</p>
<ul>
<li><code>Metaballs</code> 클래스를 생성할 때, 다른 클래스를 추가적으로 결정할 일이 없습니다. 즉, 생성될 서브 클래스가 1개였죠! 따라서 추상 클래스를 일부 가져가되, 완전히 추상 팩토리 패턴처럼 만들 이유가 없었어요.</li>
<li><code>Metaballs</code>는 그렇게 유연하게 가져갈 필요가 없는 클래스입니다. 따라서 빌더 패턴의 경우 오히려 쓸데없이 메모리를 낭비하여 불필요한 부하를 만드는 느낌이 들었어요.</li>
</ul>
<blockquote>
<p>그렇다면, 어떻게 <code>Metaballs</code>를 구현했는지 살펴보시죠!</p>
</blockquote>
<h2 id="구현하기">구현하기</h2>
<blockquote>
<p><strong>23.03.24</strong> - 해당 코드는 <a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">해당 commit</a>에서 생성 로직을 좀 더 추상화하고자 리팩토링되었습니다. 처음 보시는 분들은 무시하셔도 돼요! 🙇🏻‍♂️</p>
</blockquote>
<pre><code class="language-ts">import {DynamicMetaball, StaticMetaball} from &#39;./Metaball&#39;;

import {DynamicMetaballs, StaticMetaballs} from &#39;./Metaballs&#39;;

import {
  IPushMetaballPayload,
  TDynamicMetaballDataset,
  TStaticMetaballDataset,
} from &#39;./types&#39;;

export abstract class MetaballsFactory&lt;T&gt; {
  abstract createMetaballs(): T;

  abstract createMetaballByCount(
    metaballs: T,
    {
      options,
    }: IPushMetaballPayload&lt;TStaticMetaballDataset | TDynamicMetaballDataset&gt;,
  ): void;

  create(
    options: IPushMetaballPayload&lt;
      TStaticMetaballDataset | TDynamicMetaballDataset
    &gt;,
  ) {
    const metaballs = this.createMetaballs();

    this.createMetaballByCount(metaballs, options);

    return metaballs;
  }
}

export class StaticMetaballsFactory extends MetaballsFactory&lt;StaticMetaballs&gt; {
  constructor() {
    super();
  }

  createMetaballByCount(
    metaballs: StaticMetaballs,
    {options}: IPushMetaballPayload&lt;TStaticMetaballDataset&gt;,
  ) {
    if (options.data) {
      options.data.forEach(data =&gt; {
        metaballs.push(new StaticMetaball({ctx: options.ctx, ...data}));
      });
    }
  }

  createMetaballs(): StaticMetaballs {
    const metaballs = new StaticMetaballs();

    return metaballs;
  }

  create(
    options: IPushMetaballPayload&lt;TStaticMetaballDataset&gt;,
  ): StaticMetaballs {
    return super.create(options);
  }
}

export class DynamicMetaballsFactory extends MetaballsFactory&lt;DynamicMetaballs&gt; {
  constructor() {
    super();
  }

  createMetaballByCount(
    metaballs: DynamicMetaballs,
    {options}: IPushMetaballPayload&lt;TDynamicMetaballDataset&gt;,
  ) {
    if (options.data) {
      options.data.forEach(data =&gt; {
        metaballs.push(new DynamicMetaball({ctx: options.ctx, ...data}));
      });
    }
  }

  createMetaballs(): DynamicMetaballs {
    const metaballs = new DynamicMetaballs();

    return metaballs;
  }

  create(
    options: IPushMetaballPayload&lt;TDynamicMetaballDataset&gt;,
  ): DynamicMetaballs {
    return super.create(options);
  }
}
</code></pre>
<p>간단하죠?
즉 어떤 객체를 만들지를 서브 클래스의 <code>create</code>를 통해 결정할 수 있게 됩니다.
그리고 서브 클래스는 생성 과정을 추상화시키게 되는 거죠!</p>
<p>위의 코드를 보면, 실제로 생성을 하는 로직은 <code>Metaball</code>까지도 초기에 만들어줘야 했습니다.
만약 저 코드를 팩토리 없이 각 객체마다 추가했다면, 생성 로직과 다른 클래스들과의 결합도가 높아졌겠죠?</p>
<p>따라서 좀 더 유연하게, 다양한 <code>Metaballs</code>를 생성할 수 있게 되었군요!</p>
<h1 id="🎉-마치며">🎉 마치며</h1>
<p>사실 아직은 큰 객체들을 설계 및 구현하는 과정이라, 디자인 패턴 공부에 가까운(?) 느낌이 드네요. 하지만 이러한 설계가 뒷받침 되어야, 안정적으로 개발을 할 수 있다고 믿어요. 그리고 백스토리를 알게 되기 때문에, 추후 더 잘 이해할 수 있을 거에요.</p>
<p>그렇기 때문에 분명 이렇게 천천히 하나하나 설계해나가는 것이, 이 글을 보는 분들께서도 왜 이렇게 코드를 짰는지에 대해 납득할 수 있다고 믿기에, 문서화로 남겨놓습니다.</p>
<blockquote>
<p>디자인 패턴을 공부한다고 생각하고, 천천히 곱씹으면서 메타볼을 구현해나가보아요.
아마 다음에는 이 여러 개의 메타볼들을 어떻게 객체지향적으로 핸들링할지를 살펴볼 것 같아요. 
좀만 더 참으면 재미있는 애니메이션을 만들 수 있겠군요. 화이팅! 🙇🏻‍♂️</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 2. 확장성을 고려하며 Canvas 설계하기
]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-2.-Canvas-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-2.-Canvas-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 23 Mar 2023 02:26:23 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>이 글을 코드로 간편하게 보고싶나요? 다음을 참고하시길 바라요!</strong></p>
<ul>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue - [♻️ Refactor] 객관적인 퍼포먼스 비교를 위해 메타볼 애니메이션 OOP로 재설계한다. #36</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits/918a5ad7210414cf787c28744c0350c0d84daf7e">PR - commit(✨ create canvas, facade)</a></li>
</ul>
</blockquote>
<h2 id="캔버스를-어떻게-설계할-것인가">캔버스를 어떻게 설계할 것인가</h2>
<p>일단 먼저 백그라운드를 설명 드리자면, 저는 <code>javascript-utils</code>라는 레포지토리에서 작업 중이에요. 이 레포지토리는 일련의 자바스크립트 소스들을 모아놓는 역할을 하고 있죠.</p>
<p>따라서, 캔버스의 경우 제가 추후에도 2D 애니메이션을 만들 때 적용할 일들이 많아요.
그렇기에 작업에 앞서, 귀찮더라도 재사용성과 객체 지향적인 설계를 위해 추상클래스 및 클래스를 확장하는 방식으로 <code>MetaballCanvas</code>라는 것을 설계할 것입니다.</p>
<p>따라서 다음과 같은 결과물이 나오겠어요.</p>
<ul>
<li><code>Canvas</code>: 가장 추상적인 인터페이스<ul>
<li><code>GradientCanvas</code>: <code>Canvas</code> 추상 클래스와 공통 인터페이스를 가지며, 추가적으로 캔버스의 배경을 <code>Gradient</code>으로 적용할 수 있는     추상 클래스<ul>
<li><code>MetaballCanvas</code>: <code>GradientCanvas</code>를 상속받는, 실질적으로 이번 프로젝트에서 사용할 서브 클래스</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>따라서, 결과적으로 <code>MetaballCanvas</code>라는 서브 클래스에서 작업할 거에요. 이를 통해 개방-폐쇄 원칙을 지키며 상위 클래스에서의 인터페이스 변경을 최소화하는 방식으로 구현할 예정입니다. 😉</p>
<h2 id="설계하기">설계하기</h2>
<h3 id="추상-클래스-인터페이스-설계">추상 클래스 인터페이스 설계</h3>
<p>먼저 두 개의 추상 클래스는 다음과 같이 정의했어요.</p>
<pre><code class="language-ts">
export interface CanvasShape {
  width: number;
  height: number;
}

export enum ECanvasGradientType {
  &#39;linear&#39; = &#39;linear&#39;,
  &#39;radial&#39; = &#39;radial&#39;,
}

export abstract class Canvas implements CanvasShape {
  // 타입을 정의합니다.
  abstract type: ECanvasGradientType;

  // canvas 엘리먼트를 생성 및 정의합니다.
  abstract $canvas: HTMLCanvasElement;

  // canvas의 컨텍스트를 메모리에 저장합니다.
  abstract ctx: CanvasRenderingContext2D;

  // canvas 너비를 조정합니다.
  abstract width: CanvasShape[&#39;width&#39;];

  // canvas 높이를 조정합니다.
  abstract height: CanvasShape[&#39;height&#39;];

  // canvas를 그려냅니다.
  abstract draw(background: CanvasGradient | string | CanvasPattern): void;

  // 초기에 상위 엘리먼트에 추가합니다. 이후 rendering을 진행합니다.
  abstract mount($target: Element): void;

  // rendering을 진행합니다.
  abstract render(): void;
}

// radialGradient를 생성할 때 발생하는 추가적인 옵션을 지정합니다.
export interface IRadialGradientOptions {
  r0?: number;
  r1?: number;
}

interface MetaballCanvasOptions {
  radialGradient?: IRadialGradientOptions;
}

export abstract class GradientCanvas extends Canvas {
  abstract gradients: string[];

  abstract options?: MetaballCanvasOptions;

  abstract draw(background: CanvasGradient): void;

  abstract getLinearGradient(): CanvasGradient;

  abstract getRadialGradient(options: IRadialGradientOptions): CanvasGradient;
}</code></pre>
<p>생각보다 간결하죠?
여기서 옵션의 경우, 다음과 같은 의미를 지닙니다.</p>
<blockquote>
<ul>
<li><code>gradient</code> 방식을 현재는 <code>linear</code>하게 주지만, 언젠가 <code>radial</code>하게 바꾼다면?</li>
<li>결과적으로 이를 외부에서 바꿔줄 수 있게 옵셔널하게 주는 건 어떨까?</li>
</ul>
</blockquote>
<p>물론 <code>RadialGradientCanvas</code> <code>LinearGradientCanvas</code> 등으로 나누는 게 더 좋을 것 같기는 하나, 생각보다 <code>Linear</code>, <code>Radial</code> 여부에 따라 파생되는 파급효과가 크지는 않을 것 같다고 생각했어요. (단순히 배경의 차이라 생각하고 있습니다.) 또한, 전자에 대해 많이 쓸지에 대한 여부가 의문이었습니다.</p>
<p>따라서 어느정도의 유도리(?)를 가지고 당장 필요한 것들만 설계했어요! 😉</p>
<h3 id="metaballcanvas-인터페이스-설계">MetaballCanvas 인터페이스 설계</h3>
<p>이제 추상 클래스를 구현했으니, 메타볼에서는 어떻게 정의할지를 살펴보죠!</p>
<pre><code class="language-ts">import {GradientCanvas, IRadialGradientOptions} from &#39;./types&#39;;

export class MetaballCanvas implements GradientCanvas {
  $canvas: GradientCanvas[&#39;$canvas&#39;];

  ctx: GradientCanvas[&#39;ctx&#39;];

  type: GradientCanvas[&#39;type&#39;];

  width: GradientCanvas[&#39;width&#39;];

  height: GradientCanvas[&#39;height&#39;];

  gradients: GradientCanvas[&#39;gradients&#39;];

  constructor({
    type,
    width,
    height,
    gradients,
  }: Omit&lt;
    GradientCanvas,
    | &#39;$canvas&#39;
    | &#39;ctx&#39;
    | &#39;render&#39;
    | &#39;mount&#39;
    | &#39;draw&#39;
    | &#39;getLinearGradient&#39;
    | &#39;getRadialGradient&#39;
  &gt;) {
    this.$canvas = document.createElement(&#39;canvas&#39;);

    this.ctx = this.$canvas.getContext(&#39;2d&#39;) as CanvasRenderingContext2D;

    this.type = type;

    this.width = width;
    this.height = height;

    this.gradients = gradients;
  }

  getLinearGradient() {
    const result = this.ctx.createLinearGradient(0, 0, 0, this.height);

    this.gradients.forEach((gradient, idx) =&gt; {
      result.addColorStop(idx, gradient);
    });

    return result;
  }

  getRadialGradient({r0 = 0, r1 = 0}: IRadialGradientOptions) {
    const result = this.ctx.createRadialGradient(0, 0, r0, 0, this.height, r1);

    this.gradients.forEach((gradient, idx) =&gt; {
      result.addColorStop(idx, gradient);
    });

    return result;
  }

  draw(background: CanvasGradient) {
    this.ctx.clearRect(0, 0, this.width, this.height);

    this.ctx.fillStyle = background;

    this.ctx.fillRect(0, 0, this.width, this.height);
  }

  mount($target: Element) {
    $target.appendChild(this.$canvas);

    this.render();
  }

  render() {
    const canvasGradiation = this.getLinearGradient();

    this.draw(canvasGradiation);
  }
}</code></pre>
<p>제대로 렌더링이 되는지 결과를 살펴볼까요?</p>
<pre><code class="language-ts">
const $target = document.body;

const app = new MetaballAnimation({
  canvas: new MetaballCanvas({
    gradients: [&#39;#123141&#39;, &#39;#235234&#39;],
    width: 400,
    height: 400,
    type: ECanvasGradientType.linear,
  }),
});

app.mount($target);
</code></pre>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/04e96d1e-1947-46b6-9111-261c131f0be1/image.png" alt=""></p>
<p>오! 멋진 밤하늘을 닮은 캔버스가 생성되는군요. 🙆🏻🙆🏻‍♀️</p>
<h1 id="🌈-마치며">🌈 마치며</h1>
<p>지금까지 메타볼 애니메이션을 구현하기에 앞서 캔버스를 설계 및 구현하는 과정을 만들었어요.
글을 쓰는 과정은 꽤나 호흡이 길어서, 자칫 페이스를 잃기도 하지만 요새는 점차 이런 글쓰기에 갈증이 나기 시작했어요.</p>
<p>개발에 있어서 기록은 분명, 가장 제 성장을 객관적으로 살펴볼 수 있는 지표라는 것을 이번 시리즈를 연재하며 다시금 느끼네요. 🥰</p>
<blockquote>
<p>이 시리즈 글들이 캔버스를 생성하거나, 추후 메타볼 애니메이션에 관심을 가지실 분들에게 도움이 되었으면 좋겠네요. 그럼, 다음에는 <code>Metaballs</code> 설계에 대해 살펴볼게요. 다시 만나요! 🎉</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2D 메타볼 애니메이션 구현] 1. 메타볼 객체 설계하기.]]></title>
            <link>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-1.-%EB%A9%94%ED%83%80%EB%B3%BC-%EA%B0%9D%EC%B2%B4-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_pallete/2D-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84-1.-%EB%A9%94%ED%83%80%EB%B3%BC-%EA%B0%9D%EC%B2%B4-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 22 Mar 2023 11:58:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>이 글을 코드로 간편하게 보고싶나요? 다음을 참고하시길 바라요!</strong></p>
<ol>
<li><a href="https://github.com/JengYoung/javascript-utils/issues/36">Issue - [♻️ Refactor] 객관적인 퍼포먼스 비교를 위해 메타볼 애니메이션 OOP로 재설계한다. #36</a></li>
<li><a href="https://github.com/JengYoung/javascript-utils/pull/37/commits/522fdc4f4b8bd110457fef40adf09533b831c738">PR commit - ✨ create Metaballs</a></li>
</ol>
<p>🗒️ <strong>이 글의 수정내역</strong> (마지막 수정 일자 - 23.03.27)</p>
<ul>
<li><code>DynamicMetaball</code>에서 현재 당장 필요하지 않음에도 전달되던 props(<code>moveStrategy</code>)를 제거했어요. (commit - <code>c699507</code>)</li>
<li><code>setY</code>의 세부 로직이 잘못되어, 이를 수정했어요. (commit - <code>b74cc4a</code>)</li>
</ul>
</blockquote>
<h1 id="설계가-꼬였다">설계가 꼬였다.</h1>
<blockquote>
<p>사실 이 글을 쓰게 된 이유는, 제가 막구현을 했기 때문입니다. 😭</p>
</blockquote>
<p>본격적으로 이력서를 작성함에 있어서, 퍼포먼스 비교가 필요했어요.
그리고 비교 결과, <strong>최적화 이전과 이후의 스크립트 속도가 약 20배가 발생</strong>했어요.</p>
<p>그런데 이는 명백한 허점이 있었습니다.</p>
<ol>
<li>잠깐! <code>move</code> 메서드가 다르잖아? - 최종 결과는 제가 쓰기 좋게 바꿔놓았고, <code>move</code>의 경우 화면 내에서 핑퐁이 이루어집니다.</li>
<li>잠깐! 객체 자체가 다르잖아? - 맞아요. 그때 과감히 포기한답시고, 처음부터 새롭게 만들었거든요 🥲</li>
</ol>
<p>따라서 객관적으로 퍼포먼스 비교를 하고자 했는데... 객체를 재사용할 수 없었어요.
안 그래도 요새 시간도 없는데, 정말 좌절스러웠습니다.</p>
<p>그렇지만 역시 배운 것들은 또 써먹어야 까먹지 않죠!
이참에 다시 디자인 패턴을 복습한다는 생각으로, 위기를 기회로 만들어야겠다는 생각을 하게 됐어요. <strong>또한, 제 코드도 더 단단해지겠죠 😎</strong></p>
<blockquote>
<p><strong>실수는 반복되지 않아야 하니, 클린하게 설계부터 제대로 적용해보자는 생각이 들었습니다.</strong></p>
</blockquote>
<hr>
<h2 id="어떻게-설계할-것인가">어떻게 설계할 것인가</h2>
<p>일단 우리는 메타볼 객체를 만들어야겠죠?
저는 메타볼의 경우, 기본적으로 원형으로 되어 있다고 가정하고 구현했다는 점을 미리 말씀 드릴게요.</p>
<p>따라서 저는 &lt;최적화 이전&gt; 편에서는 다음과 같이 메타볼을 다룰 거에요.</p>
<ol>
<li><code>Canvas</code>에서 원을 만듭니다.</li>
<li>이 원을 토대로 가중치들을 고려합니다.</li>
<li>가중치에 따라 메타볼이 융합되고, 확대됩니다.</li>
</ol>
<p>그럼 간단하죠?
그냥 메타볼 하나 생성하면 그만입니다.</p>
<blockquote>
<p><strong>23.03.24</strong> - 해당 코드는 <a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">해당 commit</a>에서 수정되었어요. 
기존에 따라하시던 분들께서는 주의해주세요. 처음 보셨다면 무시하고 진행하세요! 🙇🏻‍♂️</p>
</blockquote>
<pre><code class="language-ts">export class DynamicMetaball implements Metaball {
  public ctx: CanvasRenderingContext2D;

  public x: number;

  public y: number;

  public r: number;

  public v: IXYWeight;

  public vWeight: number;

  constructor({
    ctx,
    x,
    y,
    r,
    v,
    vWeight,
  }: IDynamicMetaballParams) {
    this.ctx = ctx;
    this.x = x;
    this.y = y;
    this.r = r;
    this.v = v;
    this.vWeight = vWeight ?? 1;
  }

  get vx() {
    return this.v.x;
  }

  setX(value: number) {
    this.x = value;
  }

  setY(value: number) {
    this.y = value;
  }

  setR(value: number) {
    this.x = value;
  }

  setVx(value: number) {
    this.v.x = value;
  }

  get vy() {
    return this.v.y;
  }

  setVy(value: number) {
    this.v.y = value;
  }

  move() {}
}</code></pre>
<p>코드를 잠시 설명드리자면 다음과 같아요.</p>
<table>
<thead>
<tr>
<th>프로퍼티, 메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>x</code>, <code>y</code></td>
<td>좌표</td>
</tr>
<tr>
<td><code>v</code></td>
<td>속력</td>
</tr>
<tr>
<td><code>ctx</code></td>
<td>Canvas 컨텍스트입니다!</td>
</tr>
<tr>
<td><code>r</code></td>
<td>메타볼을 원으로 가정했다고 했죠? 반지름입니다!</td>
</tr>
</tbody></table>
<p>사실 이렇게만 작성해도, 움직일 수 있는 메타볼은 구현할 수 있어요.
그러나, 다음과 같은 문제가 기다리고 있었습니다.</p>
<h2 id="잠깐-뭔가-불안한데">잠깐! 뭔가 불안한데?</h2>
<p>이것은 제가 최종적으로 구현한 결과물인데요!
저는 메타볼을 크게 3가지로 구현했어요.</p>
<ol>
<li>고정된 메타볼</li>
<li>중심에서 벗어나지 않고 꿈틀대는 메타볼</li>
<li>벗어나서, 이후 일정 거리가 되면 터지는 메타볼</li>
</ol>
<p>사실 엄밀히 말하자면 기존 정의된 메타볼은 아니지만, 확장한 결과물을 만들어낸 거죠!</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/31d736df-befa-4dc9-be5b-51caa06fc259/image.png" alt=""></p>
<p>그럼 여기서 <strong>1, 2, 3</strong>번은 과연 위의 메타볼 객체 하나에서 구현할 수 있는 걸까요?
음... 아무래도 힘들 것 같죠? 이유는 다음과 같습니다.</p>
<ol>
<li>각자의 객체 역할이 다르다. 이를 처리하기 위해서는 또 별개의 분기처리들을 <code>move</code>마다 해야하며, 이는 객체 메서드 하나에 들어간 역할이 너무 많아져 단일 책임의 원칙에 위배될 것 같군요.</li>
<li>언제까지 분기처리만으로 얼렁뚱땅 넘길 수는 없겠죠? 예컨대 A라는 메서드가 어떤 곳에서는 필요한데, 어떤 곳에서는 사용되지 않고, 이것이 Side Effect를 발생시킨다면 매우 유지보수가 어렵겠어요. (순수하다고 하더라도, 결합도가 높은 코드는 보지 않아도 될 코드들까지 선별해야 하므로 어지럽죠 😵‍💫)</li>
</ol>
<p>따라서 우리는 메타볼 객체를 제대로 설계해야 해요.
고민 끝에 저는 다음과 같이 설계를 했답니다.</p>
<blockquote>
<ul>
<li>Metaball (추상 클래스)<ul>
<li>StaticMetaball (서브 클래스) - 좌표 상에서 움직이지 않는 메타볼</li>
<li>DynamicMetaball (서브 클래스) - 좌표 상에서 움직이는 메타볼</li>
</ul>
</li>
</ul>
</blockquote>
<p>그리고 코드는 다음과 같이 짰어요.</p>
<blockquote>
<p><strong>23.03.24</strong> - 해당 코드는 <a href="https://github.com/JengYoung/javascript-utils/pull/37/commits">해당 commit</a>에서 수정되었어요. 
기존에 따라하시던 분들께서는 주의해주세요. 처음 보셨다면 무시하고 진행하세요! 🙇🏻‍♂️</p>
</blockquote>
<pre><code class="language-ts">export abstract class Metaball {
  abstract ctx: CanvasRenderingContext2D;

  abstract x: number;

  abstract y: number;

  abstract r: number;

  abstract setX(value: number): void;

  abstract setY(value: number): void;

  abstract setR(value: number): void;
}

export class StaticMetaball implements Metaball {
  public ctx: CanvasRenderingContext2D;

  public x: number;

  public y: number;

  public r: number;

  constructor({ctx, x, y, r}: IStaticMetaballParams) {
    this.ctx = ctx;
    this.x = x;
    this.y = y;
    this.r = r;
  }

  setX(value: number) {
    this.x = value;
  }

  setY(value: number) {
    this.x = value;
  }

  setR(value: number) {
    this.x = value;
  }
}

export class DynamicMetaball implements Metaball {
  public ctx: CanvasRenderingContext2D;

  public x: number;

  public y: number;

  public r: number;

  public v: IXYWeight;

  public vWeight: number;

  constructor({ctx, x, y, r, v, vWeight}: IDynamicMetaballParams) {
    this.ctx = ctx;
    this.x = x;
    this.y = y;
    this.r = r;
    this.v = v;
    this.vWeight = vWeight ?? 1;
  }

  get vx() {
    return this.v.x;
  }

  setX(value: number) {
    this.x = value;
  }

  setY(value: number) {
    this.x = value;
  }

  setR(value: number) {
    this.x = value;
  }

  setVx(value: number) {
    this.v.x = value;
  }

  get vy() {
    return this.v.y;
  }

  setVy(value: number) {
    this.v.y = value;
  }

  move() {}
}</code></pre>
<h2 id="잠깐-왜-dynamicmetaball은-staticmetaball을-상속받지-않았나요-😔">잠깐! 왜 <code>DynamicMetaball</code>은 <code>StaticMetaball</code>을 상속받지 않았나요? 😔</h2>
<p>물론 상속했다면 중복되는 코드를 줄이고, 그러면 저는 굉장히 좋았을 것 같아요!
그렇지만 다음과 같은 단점이 존재했습니다.</p>
<blockquote>
<p><strong>상속을 받을 수 있는 형태일까?</strong></p>
</blockquote>
<p><code>StaticMetaball</code>은 <code>move</code>를 하지 않는다고 해서, 더이상 변하지 말라는 보장이 존재할까요? 예컨대 고정된 채로 갑자기 줄어드는 <code>translate</code>라는 변형에 관한 메서드가 발생한다면 어떨까요? 상속을 하는 순간, <code>DynamicMetaball</code> 역시 위 메서드가 발생합니다.</p>
<p>이때, <code>DynamicMetaball</code>은 위의 <code>translate</code>와 다르게 설계되면, 리스코프 치환 원칙에 위배됩니다. 시간적 여유에 따라 반드시 SOLID할 필요는 없지만, 이왕이면 서로가 확장에 있어 독립적이고 자유롭도록 이를 지켜주고 싶었어요.</p>
<p><strong>따라서 만약 굳이 상속을 해야만 했다면... 두 객체 위에 <code>BaseMetaball</code>이라는 것을 만들고, 이를 상속받게 했을 것 같아요.</strong></p>
<p>그렇지만 여기서는 그렇게 하지 않았습니다. 추상 클래스면 충분하다고 생각했거든요.
결과적으로, 이렇게 간단하게! SOLID 원칙에 부합한 객체 생성을 했답니다. 🙆🏻🙆🏻‍♀️</p>
<h1 id="마치며">마치며</h1>
<p>객체를 잘못 설계하면 저같이 새롭게 짜야하는 순간이 오게 됩니다. (...)
그러한 순간이 오지 않도록, 충분히 재사용성과 확장성 좋은 코드를 짜야 한다고 생각해요.</p>
<p>물론 객체 설계에 1도 관심 없던 당시에 짠 코드였기에 어쩔 수 없지만, 설계의 중요성을 다시금 느낀 하루였네요.</p>
<blockquote>
<p>그러면, 다음에는 캔버스에 관하여 글을 작성해볼게요. 🙇🏻‍♂️
비판은 제 모자란 지식을 채우기 위해 언제든지 환영입니다 👐 읽어주셔서 감사드려요!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] 포트폴리오 제작기 - 6. (2) 메타볼 애니메이션 구현기]]></title>
            <link>https://velog.io/@young_pallete/Project-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%A0%9C%EC%9E%91%EA%B8%B0-6.-2-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%EA%B8%B0-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%9D%B4%EC%A0%84</link>
            <guid>https://velog.io/@young_pallete/Project-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%A0%9C%EC%9E%91%EA%B8%B0-6.-2-%EB%A9%94%ED%83%80%EB%B3%BC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%EA%B8%B0-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%9D%B4%EC%A0%84</guid>
            <pubDate>Wed, 22 Mar 2023 11:18:10 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<blockquote>
<p>이 포스트는 단순히 여러 글들을 한데 모으기 위한 문서 정리용 포스트입니다 🥰</p>
</blockquote>
<p>되게 오랜만에 포트폴리오 사이트 제작기를 써보고자 합니다.
이번에 쓸 글은, 최적화 이전의 메타볼 애니메이션 구현기에요.</p>
<p>사실 이 글을 빠르게 쓰고 싶었지만... 최근에 굉장히 바빴어요. 😭😭
시리즈 글을 작성하지 않은 동안, 저는 OOP와 디자인 패턴에 대해 관심을 많이 가졌어요.
그 외적으로도 전반적인 개발 인프라들을 공부하느라 바빴고, 프로젝트를 하느라 바쁘기도 했고요.</p>
<p>그렇지만 이제 정리의 시기가 온 것 같습니다.
여태까지 배웠던 것들을 다시 글로 풀어나가면서, 되짚어 보는 시간을 가지려 해요.</p>
<p>이 시리즈는 스스로도 많은 기대를 하고 있어요.
단순한 애니메이션 구현 뿐만 아니라, 디자인 패턴에 대해 나름대로 생각해보면서 풀어나갈 것이기 때문이죠!</p>
<blockquote>
<p><strong>물론, 저 역시 공부하는 입장에서 많은 실수가 있을 거에요.</strong>
항상 냉철한 판단과 비판적 사고로 제 글을 바라보시길 추천드려요.
또한, 멋진 분들께서 잘못된 글을 바로 지적해주시길 기대해요! (비판 환영) 🥰</p>
</blockquote>
<p>따라서, 다음과 같은 사람들은 이 글을 보시기를 추천드립니다.</p>
<ul>
<li><strong>메타볼 애니메이션 궁금한데?</strong> 🙆🏻</li>
<li><strong>디자인 패턴? 그게 뭐야?</strong> 🙆🏻‍♀️</li>
</ul>
<blockquote>
<p><strong>그럼 시작해볼까요!</strong>
이 글이 누군가에게는 도움이 되었으면 좋겠네요. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] TDD를 해보았는데요]]></title>
            <link>https://velog.io/@young_pallete/%ED%9A%8C%EA%B3%A0-TDD%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%95%98%EB%8A%94%EB%8D%B0%EC%9A%94</link>
            <guid>https://velog.io/@young_pallete/%ED%9A%8C%EA%B3%A0-TDD%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%95%98%EB%8A%94%EB%8D%B0%EC%9A%94</guid>
            <pubDate>Tue, 14 Mar 2023 15:18:55 GMT</pubDate>
            <description><![CDATA[<h1 id="🌈-시작하며">🌈 시작하며</h1>
<p>최근에 애플리케이션을 만들 일이 있었고, <code>jest</code>를 사용하여 단위/통합테스트를 했다.<br>사실 테스트 코드를 짜보지 않은 건 아니다. 하지만 약 3일간 온전히 TDD로 처음부터 모든 개발 환경을 세팅하고, 끝까지 TDD로 구현한 경험은 이번이 처음이었다.</p>
<blockquote>
<p>이 글은, 그때의 TDD에 대한 회고와, 앞으로의 테스트에 대해 생각해보고자 남 몰래 남긴다. 😎</p>
</blockquote>
<p>회고의 방식은... 음 </p>
<ul>
<li>간단한 내 TDD 방식을 소개하고, </li>
<li>KPT의 방식으로 전개해야겠다.</li>
</ul>
<h1 id="🚦-본론">🚦 본론</h1>
<h2 id="tdd-그게-무엇일까">TDD, 그게 무엇일까</h2>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/ea56896b-ecbb-448d-a03d-d707c0d9b156/image.png" alt="">
(사진 출처: <a href="https://hanamon.kr/tdd%EB%9E%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C/">하나몬 - TDD란?</a>)</p>
<p>애초부터 테스트 코드를 먼저 짜고, 이를 충족하며 개발해나가는 방식이다.
즉, 애초부터 주어진 시나리오 대로 필수적으로 행해져야 할 테스트들을 미리 코드로 작성한 다음, 이를 만족하는 기능을 구현하는 것이다.</p>
<p>결과적으로 작은 단위에서부터 시작한 테스트들은 점차 컴포넌트의 모든 로직들이 &#39;정상적으로 돌아간다&#39;는 근거가 되고, 결과적으로 코드의 유지보수로 인한 위험을 최소화할 수 있기에 리팩토링 등의 생산성 역시 올라가는 효과가 있다.</p>
<blockquote>
<p><strong>처음 시작하는 내게, 분명 TDD라는 개발 방법론은 꽤나 달콤한 유혹이었다.</strong></p>
</blockquote>
<h2 id="tdd-왜-시작했을까">TDD, 왜 시작했을까</h2>
<p><strong>Test Driven Development</strong>, 테스트 주도 개발 방법론을 택한 건 다음과 같은 이유에서였다.</p>
<blockquote>
<p><strong>음... 이 기능은 정말 비즈니스 로직이 깨지면 위험한데...</strong></p>
</blockquote>
<p>아직 실무 경험도 턱없이 부족하지만, 적어도 지난 개발자로서의 삶을 살며 느꼈던 건, <strong>개발자는 비즈니스와 리소스</strong>를 끊임없이 생각해야 한다는 것이었다.</p>
<p>그렇기에 TDD를 택했다. 적어도 인증 절차에서 조금이라도 깨지면 애플리케이션이 보안상 취약점이 노출될 터이니, 애초부터 제대로 만들자는 것이 내 생각이었다.</p>
<p>리소스는 분명 많이 들겠지만, &#39;수 십 개의 로직 점검에 대한 비용 역시 덜어지는 건 마찬가지지 또이또이하겠지.&#39; 라는 생각으로 TDD를 시작했다.</p>
<h2 id="라이브러리-선택">라이브러리 선택</h2>
<p>일단 먼저, 나는 단위 테스트로 시작해서, 통합 테스트를 통해 전체 애플리케이션의 동작을 테스트하는 절차를 거쳤다.</p>
<p>이를 수행하기 위해, <code>jest</code>라는 라이브러리를 썼다.
사실 <code>playwright</code>를 사용할까도 고민했다. 내게 상대적으로 익숙하고, 자신있는 테스트 도구 중 하나였기 때문이다. (써본 적이 없다면 나중에 한 번 써보기를 추천한다. <code>codegen</code>을 비롯한 브라우저별 테스트는 정말 신세계다.)</p>
<p>그렇지만 일단 컴포넌트 및 핸들러를 독립적으로 아주 작은 단위의 기능까지 테스트하기에는 E2E Testing에 강한 <code>playwright</code>보다는, 단위 테스트로 많이 사용하는 <code>jest</code>가 안성맞춤이라 생각했다.</p>
<h3 id="세부-테스팅-방법">세부 테스팅 방법</h3>
<blockquote>
<p>일단 이 방법은 나중에 후술하겠지만, 꽤나 실패했다고 생각하는 편이며, TDD에 대한 많은 생각을 하게 만들었다.</p>
</blockquote>
<h4 id="단위-테스트">단위 테스트</h4>
<p>나의 경우, TDD를 할 때 다음과 같이 단위테스트를 작성했다.</p>
<ul>
<li><strong>컴포넌트</strong>: 가장 추상적인 <code>UI</code>만 담당하는 컴포넌트들부터 개발했다. 여기서 <code>UI</code>가 제대로 렌더링이 되는지, 인증 시 에러 메시지는 정확히 나오는지를 테스트했다.</li>
<li><strong>커스텀 훅</strong>: 결국 커스텀 훅의 핵심은 상태를 변경하는 비즈니스 로직이었다. 이 비즈니스 로직이 원하는대로 상태값으로 변하는지를 테스트했다.</li>
<li><strong>전역상태</strong>: <code>contextAPI</code>를 사용했다. 전역 상태 역시 독립적으로 관리한 후, 상태 변화가 제대로 동작하는지를 간단히 살펴봤다.</li>
<li><strong>유틸</strong>: 아무래도 내가 맡은 기능이 고객 입력에 있어 꽤나 중요하기에 <code>validator</code> 등에 대한 모든 엣지케이스들을 탐색했다.</li>
</ul>
<h4 id="통합-테스트">통합 테스트</h4>
<p>통합테스트의 경우에는, 가장 구체적인 컴포넌트로부터 API가 내려지면, 가장 추상적인 하위 컴포넌트들까지 렌더링이 제대로 되는지를 검증하는 절차로 이어졌다.</p>
<p>이를 진행하기 위해서는, API를 모킹해야했다.
왜? 기본적으로 테스팅 환경은 모든 의존성을 제거해야 한다.
이때, <code>API</code>의 응답환경은 서버의 &#39;상태&#39;에 결과값인 <code>View</code>가 의존성을 갖게 된다. 즉, 테스트 결과에 대해 원치 않는 <code>Side Effect</code>가 발생한다는 것이다.</p>
<p>그렇기에 <code>msw</code>라는, 서비스 워커를 통해 <code>API</code> 요청을 가로채서, 이에 대한 반환값을 <code>mock</code>화 시켜주는 라이브러리를 가져왔다. 이를 통해 API 요청에 대한 로딩/결과/에러 등이 컴포넌트에서 동작하는지 테스트했다.</p>
<h3 id="결과">결과</h3>
<p>결과적으로 인증에 관한 애플리케이션 페이지 구축 작업은 3일만에 약 200개의 테스트 케이스 추가라는 지독한 결과를 만들었다. (3일은 정말 3시간만 잔 것 같다.)</p>
<p>그만큼 TDD에 대해 정말 알고 싶었고, 정말 많이 도전해봤고, 그렇기에 이제는 글로 말할 수 있었던 것 같다.</p>
<blockquote>
<p>일단 여기까지가 내 TDD 방법에 대한 기본적인 context이다.
기본 환경을 보여주고 싶으나, 내게는 권한이 없다 😭</p>
</blockquote>
<h2 id="keep---잘한-점">Keep - 잘한 점</h2>
<h3 id="상대방이-믿을-수-있는-코드를-짰다">상대방이 믿을 수 있는 코드를 짰다</h3>
<p>테스트 코드들이 나의 모든 로직을 검증할 수 있다. 그렇기에 상대방에게도 안정성을 줄 수 있다고 생각한다.</p>
<p>특히 이러한 방법론은 실무의 PR에서 더 효과적이지 않을까 싶다. </p>
<p>코드는 팀원들이 다같이 책임져야 한다. 그렇기에 PR 문화가 존재한다.
하지만 때로는 이러한 &#39;공동 책임&#39;이라는 말로 인해 PR 문화가 움츠러들 때가 있다.
실제로 나 역시 실무에서 PR 문화를 만드려 했으나 꽤나 어려웠던 기억이 있다. 
예컨대 다음과 같은 동료들의 질문에 부딪힌 경험이 있다.</p>
<blockquote>
<ul>
<li>온전히 개발에 집중하고 싶은데, 시간이 너무 많이 걸려요.</li>
<li>근데, 이거 결국에 누가 봐요? 결국에 이 문서 찾을 수 있을까요?</li>
<li>만약 누군가가 이 문서를 못 찾는다면, 우리끼리 열심히 적어봤자 아닌가요?</li>
</ul>
</blockquote>
<p><strong>TDD는 이러한 의문과 PR의 한계를 말끔히 해결해줄 수 있다.</strong></p>
<blockquote>
<ul>
<li>온전히 개발에 집중하고 싶은데, 시간이 너무 많이 걸려요.</li>
<li><blockquote>
<p>PR로 일일이 로직이 맞다는 것을 설명하지 않아도, 테스트 코드가 증명한다.</p>
</blockquote>
</li>
<li>근데, 이거 결국에 누가 봐요? 결국에 이 문서 찾을 수 있을까요?</li>
<li><blockquote>
<p>해당 모듈이 위치한 곳에 테스트 파일만 놓으면 쉽게 로직의 유효성을 입증할 수 있다.</p>
</blockquote>
</li>
<li>만약 누군가가 이 문서를 못 찾는다면, 우리끼리 열심히 적어봤자 아닌가요?</li>
<li><blockquote>
<p>지속적인 테스팅을 통해 미래의 동료나, 현재의 동료 모두에게 개발할 용기를 준다.</p>
</blockquote>
</li>
</ul>
</blockquote>
<p>여러 명이 관리하기 위해서는, 각자에게 해당 코드의 동작에 대한 지속적인 감시를 요청하기보다는, 테스트 코드를 작성함으로써, 이 로직이 분명 성공한다는 믿음을 주는 것이 더욱 좋은 것 같다고 생각했다.</p>
<p>그러한 측면에서, 분명 내 테스트 코드는, 다른 사람들에게 이 코드가 잘못되지 않음을 증명할 수 있는 든든한 근거가 되었다고 생각했고, 이는 잘했다고 생각한다.</p>
<h3 id="최소한의-qa로-개발에-집중할-수-있다">최소한의 QA로 개발에 집중할 수 있다</h3>
<p>보통 로직을 고치거나 변경하면, 항상 Side Effect를 염두해야 한다.
그렇기에 나도 보통 개발을 할 때에는 코드를 치는 만큼이나 <code>QA</code>를 많이 하는 경향이 있다. </p>
<p>그런데 이번에 TDD를 하며 정말 신기한 경험을 했다.
<strong>평소에 로컬 서버를 켜고 실제 동작을 테스트하는 시간이 거의 없었던 것이다.</strong></p>
<p>이미 이 기능에 대한 테스트 코드를 만족하면 기능이 된다는 것을 확신하기 때문에, 실제 동작을 개발 서버를 켜고 확인하지 않아도 됐다.</p>
<p>이러한 점은, 온전히 내게 개발할 시간을 부여하게 했다는 점에서 잘했다 생각한다.</p>
<h3 id="테스트하며-얻은-좋은-코드-품질">테스트하며 얻은 좋은 코드 품질</h3>
<p>정말 작은 단위부터 점차 올라가니, 오히려 구체적인 컴포넌트를 제작하는 데 더 수월했던 것 같다. 단순히 추가만 해도, 동작할 것 같은 확신이 든다고 해야 하나.</p>
<p>또한, 컴포넌트를 테스팅하는 데 있어 <code>mock</code>을 넣어주기 위해서는 <code>props</code>를 통해 넘겨 받는 방식으로 개발해야 했다. 이렇게 <code>props</code>로 넘겨받는 컴포넌트를 개발한 결과, UI 컴포넌트는 더욱 로직과 분리되었다. </p>
<p>결과적으로 TDD를 진행하면서 더욱 추상성 높은 컴포넌트 개발이 가능했고, 좋은 컴포넌트 개발이란 무엇인지 많이 생각하게 됐다.</p>
<h2 id="problem---못한-점">Problem - 못한 점</h2>
<h3 id="테스트-코드-작성에-시간이-너무-많이-걸렸다">테스트 코드 작성에 시간이 너무 많이 걸렸다</h3>
<p>아무래도 이건 내가 초보인 것도 있었다. 
<code>jest</code>를 거의 사용하지 않았다고 보아도 무방할 정도로 단위 테스트에 대한 지식이 없었다.
(그렇기에 커뮤니티의 힘을 받고자, 가장 많이 사용하는 <code>jest</code>를 택한 것도 있다)</p>
<p>그렇지만, 모든 단위 테스트들을 하나하나 써야 한다는 강박관념으로 인해, 테스트하는 시간이 실제 개발 시간의 2배 이상을 웃돌 정도로 힘들었던 것 같다. 나중에 모든 기능을 완료했을 때, 3일 동안 테스트 코드가 220개 돌아갈 정도로 작성했다는 건 충격적이었다.</p>
<p>어떻게 보면 정말 열정적이었지만, 아까도 말했듯이 <strong>개발자는 리소스</strong>를 고려해야 한다.
분명 내가 이 테스트 코드를 덜 작성했다면? 더 많은 기능을 최적화하고 추가하지 않았을까.</p>
<p>그런 것들 역시 정말 좋아하는 것들인데, 짧은 시간 내에 TDD를 하면서 꽤나 포기하고 타협했다는 점은 스스로도 <strong>좋은 개발자인지 의구심이 들기도 했고, 마음이 아팠다.</strong></p>
<blockquote>
<p>물론 이제는 홀로 피드백을 하며, 내가 테스트 코드하면서 부족했던 점들을 생각하고, 좀 더 잘 작성할 용기와 자신감을 얻었다. 😎 역시 결국 헤맨 만큼 자기 땅이 아닐까 싶다.</p>
</blockquote>
<h3 id="테스트-코드의-nesting이-과했다">테스트 코드의 Nesting이 과했다</h3>
<p>당시에는 정말 <code>jest</code>를 잘 써보질 못했으니, 다양한 코드들을 살펴봤다.
그중 <code>beforeEach</code>, <code>afterAll</code>과 같은 작업들로 이루어진 걸 많이 접했다.</p>
<p>당시에는 이게 맞다고 생각했다. 왜냐하면 다른 개발자들이 이렇게 많이 쓰기 때문이다.
그렇기에 내 <code>Nesting</code>은 정말 4~5 중첩까지 이어질 정도로 심해졌다.</p>
<p>중첩할 때에는 잘 몰랐는데, 나중에 나를 정말 곤혹스럽게 한 것은, 테스트 코드를 유지보수할 때였다.</p>
<p>곳곳에 도처한 <code>beforeEach</code>는 분리 및 리팩토링할 때마다 말썽이었고, 결과적으로 이를 유지보수하는 데 꽤나 애를 먹었던 기억이 난다.</p>
<h2 id="try---좋은-테스트하기">Try - 좋은 테스트하기</h2>
<h3 id="이렇게-테스트하지-않을-것이다">이렇게 테스트하지 않을 것이다.</h3>
<p>이후 스스로 현타(?)를 느끼면서 좋은 테스트 코드란 뭘까...하며 인터넷을 서칭했는데, 좋은 글이 보였다.</p>
<blockquote>
<p><a href="https://kentcdodds.com/blog/avoid-nesting-when-youre-testing">Kent - Avoid Nesting when you&#39;re Testing</a></p>
</blockquote>
<p>이 글을 만약 시작할 때부터 보았다면 내 코드는 달라졌을까. 😭 싶었다.
따라서 앞으로는 다음과 같이 하기로 맹세했다.</p>
<ol>
<li><code>beforeEach</code>, <code>afterAll</code>과 같은 공통의 작업을 처리하는 함수는 지양한다.</li>
<li><strong>다양한 컨텍스트</strong>를 중첩함으로써 생겨난 콜백은 최소화한다.</li>
</ol>
<p>이 두 가지는 가독성도 매우 좋지 않을 뿐더러 유지보수가 매우 힘들다. 그렇기에 하지 않을 것이다.</p>
<h3 id="이렇게-테스트할-것이다">이렇게 테스트할 것이다</h3>
<p>그리고 다음 2가지는 하려 노력할 것이다.</p>
<ol>
<li>공통의 로직을 재사용가능한 함수로 빼낸다.</li>
<li>최대한 인라인으로 <code>Given</code> <code>When</code> <code>Then</code>을 작성한다.</li>
</ol>
<p>이렇게 하면 분명 훨씬 가독성도 좋아지고, 각자의 컨텍스트가 독립되어 있어 테스팅이 더 용이한 것 같다는 확신이 들었다. </p>
<p>또한 마지막으로 한 가지 더 추가한다.</p>
<ol start="3">
<li><strong>당장 프레임워크를 바꿀 일이 없다면</strong> 비즈니스 로직 테스팅 -&gt; 컴포넌트 작업 -&gt; UI 단위 테스팅의 순서로 진행한다.</li>
</ol>
<p>난 정말 이번에 누구보다 열심히 TDD를 했다. 그리고 이 질문을 3일 동안 100번은 한 것 같다.</p>
<blockquote>
<p><strong>TDD가 과연 생산성에 도움을 줄까</strong></p>
</blockquote>
<p>누구보다 열심히 했기에 나는 이 질문에 대한 답을 찾았다고 생각한다.</p>
<blockquote>
<p><strong>&quot;테스트 코드 짜기 나름이다&quot;</strong></p>
</blockquote>
<h4 id="내가-착각했던-것---tdd는-모든-테스트를-각각-해야-한다">내가 착각했던 것 - TDD는 모든 테스트를 각각 해야 한다</h4>
<p>나는 TDD는 모든 경우에 대해 떠올리고, 이를 다 만족해야 한다고 생각했다.
그렇기에 정말 사소한 것들도 테스트 코드를 짰다. 예컨대, UI에서 클릭하면 함수가 호출되어야 한다던지, 라벨이 보여야 한다던지... 등등 말이다.</p>
<p>물론 일일이 작성한다는 것, 커버리지를 올린다는 것은 전체 품질에 있어 좋으면 좋지 결코 나쁘지 않다.
그런데 중요한 건, <strong>이 테스트 코드 때문에 다른 기능을 더 추가할 수 있지 않을까</strong>라는 기회를 놓친다는 생각이 들었다. </p>
<p>실제로도 분명, 나는 요구사항에 있는 기능들을 구현했지만, 다른 최적화들을 진행하지 못했었다. 결국, <strong>나는 안정된 앱을 만들기는 했지만, 알잘딱깔센하게 내 리소스를 최적화하여 사용하는 데는 실패한 셈이다.</strong></p>
<blockquote>
<p>테스트의 커버리지에 현혹되지 말자.
개발 리소스가 넉넉하다면 모르겠지만, 한정적인 자원에서 커버리지 100%는 독이다.
테스트 코드 역시 결국 리소스를 고려해야 하는 업무 중 하나라는 것을 깨달았다.</p>
</blockquote>
<p><code>TDD</code>는 정말 이 앱에서 필수적인 기능이 깨지지 않도록 보호한다는 느낌으로 할 것이다.</p>
<h3 id="tdd-해야-하나">TDD... 해야 하나?</h3>
<p>테스트 코드를 먼저 작성하는 방법은 우아했지만, 이런 생각들이 머리 속에 무수히 생겨났다.</p>
<blockquote>
<p>TDD는 정말 &#39;완벽한 방법론&#39;일까?</p>
</blockquote>
<p>음... 나는 아닌 것 같다.
취지는 이해한다. 결국 우리는 일정 요구사항을 충족하는 기능을 구현해야만 하고, 이를 계속해서 확인하는 작업은 비효율적이기에 이러한 방법이 유효하다는 것은 인정한다.</p>
<p>그렇지만 좀 엉뚱한 면이 있다.
<code>TDD</code>는 결국 테스트 코드를 먼저 짜고, 이를 검증하는 방식으로 전개한다. <strong>그렇지만, 테스트 코드를 나중에 짜도 요구사항에 대한 충족 여부를 검토할 수 있지 않은가?</strong></p>
<p>또한, 프론트엔드를 개발하다 보면, 하루하루 회의마다 컴포넌트 요구사항이 엎어지는 경우가 허다하다. 이런 상황에서 테스트 코드를 먼저 짤 수 있을까? </p>
<p>나아가 애자일하게 짧은 시간에 스프린트하는 IT 문화에서, 이 코드를 먼저 짜는 것이 개발자의 생산성에 피해를 주지는 않을까? 팀에게 생산성을 줄 수 있기 때문에 필요하다면, 이를 할 수 있는 여건이 보장되어 있을까?</p>
<p>각 기능들을 테스트 코드를 작성하며 검증하는 방식이 얼마나 효과적인 것일까? 과연 단기간에 다양한 기능을 구현하고, 추후 필요한 테스트만 빠르게 검증하는 개발보다 더 좋은 것일까?</p>
<blockquote>
<p>이런 계속된 물음 속에서, 다음과 같은 생각을 하게 됐다.
<strong>테스트 코드를 믿는다. 하지만 나는 TDD를 믿지는 않는다.</strong></p>
<p>내게는 테스트 코드를 짜는 시간만큼이나, 일단 구현사항을 빠르게 올리고, 이것이 믿을 수 있는지 검증할 수 있는 테스트 코드를 짜는 방식이 더 유효하다고 생각하기 때문이다.</p>
</blockquote>
<p>내가 잘못된 건지 다양한 자료들을 찾아보아야 했다. 
그리고 <a href="https://jbee.io/react/testing-1-react-testing/">해당 글</a>이 가장 나와 비슷하다는 생각이 들었다.</p>
<p>우리는 생산성을 위해 추상화를 위한 라이브러리를 쓰고 있고, 이를 &#39;어느 정도&#39; 믿어주는 게 필요하다. (내가 해당 UI 렌더링 테스트를 위한 리소스를 확보하기 전까지 말이다.) 중요한 것은, 이벤트들로 일어나는 비즈니스 로직에 관련한 함수들을 검증하는 것이다.</p>
<p>생각보다 테스트 코드는 비싸다는 것을, 나는 그 시간에 더 많은 것을 할 수 있음을 이번에 값지게 배웠다.</p>
<h1 id="👋🏻-마치며">👋🏻 마치며</h1>
<p>내가 작성했던 코드들을 중심으로 예시를 들고 싶지만, 안타깝게도 내게는 권한이 없기 때문에 그저 모양 없는 회고만 진행하게 됐다.</p>
<p>지금 2시간 내내 노트북으로 회고를 정신없이 썼음에도 불구하고, 피로감을 전혀 느끼지 않는 것을 보아 꽤나 나는 테스트와 이 글에 진심이었구나를 깨달았다.</p>
<blockquote>
<p>여튼, TDD든 뭐가 중요하랴. 
결국 나는 어떠한 방식으로든, <strong>내가 작성한 코드가 최대한 효율적이고 안정적으로 돌아가도록 모든 방법을 다 할 것이다.</strong> 난 그런 개발자니까. 😎</p>
</blockquote>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://jbee.io/react/testing-1-react-testing/">[Testing] 1. 프론트엔드, 무엇을 테스트 할 것인가</a></li>
<li><a href="https://kentcdodds.com/blog/avoid-nesting-when-youre-testing">Kent - Avoid Nesting when you&#39;re Testing</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 억억단을 외우자(Lv.3, 자바스크립트)]]></title>
            <link>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%96%B5%EC%96%B5%EB%8B%A8%EC%9D%84-%EC%99%B8%EC%9A%B0%EC%9E%90Lv.3-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</link>
            <guid>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%96%B5%EC%96%B5%EB%8B%A8%EC%9D%84-%EC%99%B8%EC%9A%B0%EC%9E%90Lv.3-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</guid>
            <pubDate>Mon, 06 Feb 2023 06:26:16 GMT</pubDate>
            <description><![CDATA[<h1 id="🌈-시작하며">🌈 시작하며</h1>
<p>알고리즘 스터디가 있어 간만에 문제를 풀어보았어요 :)
이번 문제는 이전 문제 대비 비교적 쉬운 난이도인 <a href="https://school.programmers.co.kr/learn/courses/30/lessons/138475">억억단을 외우자</a>였습니다.
결론부터 이야기를 하자면, 저는 DP로 풀었어요.
무엇보다 이 문제를 제대로 이해하면, DP가 가장 직관적이라는 생각이 들었습니다.</p>
<blockquote>
<p>그렇다면, 어떻게 풀었는지 살펴볼까요? 😉</p>
</blockquote>
<h1 id="🚦-본론">🚦 본론</h1>
<h2 id="설계-과정">설계 과정</h2>
<p>우리가 보통 구구단을 하면 <code>3 * 4 = 12</code> 처럼 하나의 방정식이 나오죠.
이러한 방정식들의 모음이 좀 더 큰 단위로 표현되어, 곧 억억단이라는 게 나왔다고 볼 수 있겠어요.</p>
<p>이때 우리는 한 가지 힌트를 얻을 수 있습니다.</p>
<blockquote>
<p>12라는 것은 <strong>3에서 나왔다면 무조건 4에서도 나온다</strong>는 것이죠!</p>
<h4 id="q-이게-왜-힌트인가요-😖">Q. 이게 왜 힌트인가요? 😖</h4>
<p>구구단에서 많이 나왔다는 것은, 결국 어떤 <code>e</code> 이하의 숫자에서의 곱셈 식의 결과가 해당 값이 되었다는 이야기입니다.
가령 예시에서 나온 e = 8일 때 8이 될 수 있었던 이유는 1을 곱할 때, 2를 곱할 때, 4를 곱할 때, 8을 곱할 때 나왔기 때문이죠.</p>
</blockquote>
<p>오, 그렇다면 우리는 문제의 접근을 <strong>약수</strong>로부터 찾으면 되지 않을까요?</p>
<h2 id="약수를-구하는-방법">약수를 구하는 방법</h2>
<p>약수를 구하는 방법은 정말 많습니다.
그 중 가장 직관적인 방법은, <strong>저장할 배열을 만들어, 그곳에 약수를 직접 다 곱하는 방법입니다.</strong></p>
<p>또한, 직관적일 뿐만 아니라 일일이 번거로운 계산을 하지 않기 때문에 약수를 구하는데 있어 효율적인 방법이기도 하죠.</p>
<blockquote>
<p>이때, 1은 어차피 모든 곳에서 곱하기 때문에 처음부터 반영해주면, 최악의 경우 5000000만큼 연산을 줄일 수 있겠죠? 😉</p>
</blockquote>
<p>따라서 다음과 같이 약수를 구해주는 로직을 생성해줍시다.
시간복잡도는 얼핏 대략적으로 따져보면 <code>O(NlogN)</code> 정도는 될 것 같아요. (정확하지는 않으나, 결국 제곱만큼 곱하는 로직이 반복되기 때문입니다.)</p>
<pre><code class="language-js">const counts = new Array(e + 1).fill(1); // 1 반영

for (let num = 2; num &lt;= e; num += 1) {
  for (let divider = num; divider &lt;= e; divider += num) {
    counts[divider] += 1;
  }
}</code></pre>
<h2 id="배열-안에-주어진-starts-내에서-최대-약수-개수를-가진-인덱스-값-찾기">배열 안에 주어진 <code>starts</code> 내에서 최대 약수 개수를 가진 인덱스 값 찾기</h2>
<p>여기에서 꽤나 골치 아프실 수 있었을 것 같아요.
하지만 여기서 <code>DP</code>를 사용한다면 문제를 굉장히 단순화시킬 수 있습니다.</p>
<h3 id="dp로-문제-해결하기">DP로 문제 해결하기</h3>
<p>사실 직관적으로 떠오르는 것이 무엇일까요? 바로 <strong>누적 합</strong>입니다.
일단 문제의 예시에서처럼 <code>e = 8, starts = 1, 3, 7</code>로 예시로 들어보겠습니다.</p>
<p>여기서 <code>starts</code>에 걸맞는 값을 풀이하면 다음과 같을 거에요.</p>
<pre><code class="language-js">const results = [
    `1~8 중 가장 약수가 많은 숫자 중 작은 숫자`, 
    `3~8 중 가장 약수가 많은 숫자 중 작은 숫자`, 
    `7~8 중 가장 약수가 많은 숫자 중 작은 숫자`
]</code></pre>
<p>엇, 그런데 뭔가 좀 불편한 게 보이지 않나요?
바로 <strong>합이 겹치는 구간이 존재</strong>한다는 것입니다.</p>
<p>이러한 경우에는 DP를 사용하여 <code>8~8</code>에서의 조건에 맞는 값을 구하여 거꾸로 반영해주면 되지 않을까요?</p>
<p>바로 다음과 같이 말입니다.</p>
<pre><code class="language-js">const results = new Array(e + 1).fill(1);

for (let i = results.length - 1; i &gt;= 0; i -= 1) {
  if (i === results.length - 1) {
    results[i] = i;
    continue;
  }

  const prevMaxIdx = results[i + 1];

  results[i] = counts[i] &gt;= counts[prevMaxIdx] ? i : prevMaxIdx;
}</code></pre>
<p>그러면 결과적으로 <code>results[i]</code>에는 <code>start = i</code>일 때 해당 구간에서 가장 약수가 많으면서도, 작은 값이 업데이트가 될 수 있습니다. (<code>counts[i] &gt;= counts[prevMaxIdx]</code>이기 때문입니다.)</p>
<blockquote>
<p>어떤가요? 
결과값을 배열 형식으로 만들기 위해 얼핏 최소한 정렬을 해야할 것 같던 문제가, <code>O(N)</code>으로 해결되었습니다.
N = 5000000이라는 점에서 문제가 어려워보이지만, 막상 뜯어보면 매우 간단하고 별거 아닌 문제가 되었군요! 🚀</p>
</blockquote>
<h2 id="결과-코드">결과 코드</h2>
<pre><code class="language-js">const solution = (e, starts) =&gt; {
  const counts = new Array(e + 1).fill(1);
  const results = new Array(e + 1).fill(0);

  for (let num = 2; num &lt;= e; num += 1) {
    for (let divider = num; divider &lt;= e; divider += num) {
      counts[divider] += 1;
    }
  }

  for (let i = results.length - 1; i &gt;= 0; i -= 1) {
    if (i === results.length - 1) {
      results[i] = i;
      continue;
    }

    const prevMaxIdx = results[i + 1];

    results[i] = counts[i] &gt;= counts[prevMaxIdx] ? i : prevMaxIdx;
  }

  return starts.map((start) =&gt; results[start]);
};</code></pre>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/e237b7a5-0de0-45e2-a052-afb9cd034639/image.png" alt=""></p>
<p>매우 빠르게 문제를 해결했군요! 🙆🏻</p>
<h1 id="🚀-마치며">🚀 마치며</h1>
<p>사실 저는 이 문제를 초반에 다소 헤맸어요.
아무래도 시간복잡도를 고려할 때 <code>N = 5000000</code>이라서 무조건 <code>O(N)</code>이내로 풀어야 한다고 생각했는데요.</p>
<p>결국 그렇게 고민하다, 설마 되겠어?하던 <code>O(NlogN)</code>으로 문제를 해결하니 매우 허탈했답니다 😖</p>
<blockquote>
<p>앞으로는 일단 비슷한 시간복잡도라면 일단 트라이해보는 연습을 해봐야겠어요.
다들 즐거운 공부하시길 바라며. 이상!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 오버엔지니어링에 대한 단상]]></title>
            <link>https://velog.io/@young_pallete/%EC%98%A4%EB%B2%84%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EB%8B%A8%EC%83%81</link>
            <guid>https://velog.io/@young_pallete/%EC%98%A4%EB%B2%84%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EB%8B%A8%EC%83%81</guid>
            <pubDate>Sun, 05 Feb 2023 13:18:52 GMT</pubDate>
            <description><![CDATA[<h1 id="🌈-시작하며">🌈 시작하며</h1>
<p>최근에 야심찬 목표를 갖고 동료와 함께 개발하고 있는 프로젝트가 있다. 
프론트엔드 쪽 프로젝트를 책임지고 하다 보니 좋은 코드를 집착하게 됐고, 결과적으로 이 코드가 과한 건 아닌지, 오버엔지니어링에 대한 생각이 많아졌다.</p>
<p>결국 타협을 하다 보니 가장 적정선을 타협하게 됐지만, 이 고민을 결정하기까지 걸린 시간에 대한 나름대로의 현타(?)가 발생했다.</p>
<blockquote>
<p>그렇다. 이 글은 오버엔지니어링에 대해 스트레스 받은, 
어느 우주 건너편에 위치한 개발자의 생각을 정리한 글이다.</p>
</blockquote>
<h1 id="🚦본론">🚦본론</h1>
<h2 id="나를-괴롭게-하는-오버엔지니어링">나를 괴롭게 하는 오버엔지니어링</h2>
<p>최근에 좋은 코드를 집착하면서 내 코드에 대한 엄격함이 심해졌고, 다음과 같은 생각의 분열(?)이 일어나게 됐다.</p>
<blockquote>
<p><strong>분열된 생각 1.</strong> 왜 쉬운 문제를 어렵게 다가가려 하지? 이건 오버엔지니어링 같은데.
<strong>분열된 생각 2.</strong> 그렇지만 나중에 A한 상황에서는 이러한 코드가 더 효율적이지 않아? 이런 상황이 곧 발생할 거 같은데.</p>
</blockquote>
<p>어쩌면 다양한 생각을 사고하고 적합한 패턴과 로직을 만들어낸다는 측면에서는 분명 좋은 일이긴 하다. 하지만 이에 따라 마치 당장 바빠 죽겠는데 발생하는 수많은 고민들은, 마치<code>chore</code>을 하는 것만 같은 허탈함과 고통이 수반되기도 했다.</p>
<blockquote>
<p><strong>분명 쉽게 생각하면 그만일 것을, 적어도 끊임없이 생성되는 오버엔지니어링에 대한 번뇌는 마치 가스라이팅처럼 나를 괴롭게했다.</strong></p>
</blockquote>
<h3 id="피할-수-없는-오버엔지니어링의-고통">피할 수 없는 오버엔지니어링의 고통</h3>
<p>그렇다면 문제는 오버엔지니어링이니 사실 고통으로부터 벗어나는 방법은 간단하다.
<strong>애초부터 오버엔지니어링을 생각하지 않고 만들어내면 된다.</strong> 어떤 로직이든 그냥 단순한 풀이를 하면 그만이다.</p>
<p>실제로 쉬운 문제로 풀이를 해버리면 간단하게 문제를 해결할 수 있다. 
예컨대 우리가 서울에서 판교까지 물건을 배송하는 로직을 코드로 짠다고 생각해보자.
이럴 때에는 그저, 간단히 자동차로 이동하면 그만이다.
따라서 이때, 만약 비행기로 짜는 로직까지 고려한 개발자의 코드는 이상하게 보일지 모른다. (때로는 <del>왜 그렇게 코드를 짜셨나요?</del>라는 물음을 수반하기도 한다.) 우리는 이러한 <strong>과한 디자인을 오버엔지니어링</strong>이라 한다.</p>
<p>하지만 그렇다고 오버엔지니어링이 나쁠까? 그렇지 않다.
갑자기 재수가 없게도 물건을 미국까지 옮겨야 하는 상황이 속출했다고 가정하자. </p>
<p>이때에는 기존의 로직을 모두 버리고 새로운 코드로 바꿔야 할 것이다.
이럴 때에는 슬슬 골치가 아파지기 시작한다. 잘 돌아가는 코드를 다 갈아 엎어야 할지 말이다.</p>
<p>반면, 비행기로 이동하는 로직까지를 고민해서 전략 패턴으로 개발해놓았다면? 개발자는 웃으며 코드 몇 줄 추가하고 끝낼 것이다.</p>
<blockquote>
<p>확장성은 당장 내게 보이지 않지만 먼 미래의 나를 위해 마치 투자하는 느낌이 들었다.
그래서 난 이 마약같은 오버엔지니어링과 적정엔지니어링의 경계의 달콤함을 벗어날 수 없었다.</p>
</blockquote>
<h2 id="그렇다-오버엔지니어링은-블랙박스의-영역이다">그렇다. 오버엔지니어링은 블랙박스의 영역이다.</h2>
<p>이 세상에 다양한 블랙박스가 존재한다. 주식시장이든, 비트코인이든, 토너먼트 경기이든. 결국 확률의 싸움은 이 지구에서 쉴 새 없이 발생하고 있다.</p>
<p>우리는 이러한 <strong>까고 봐야 알 수 있는 영역을 블랙박스</strong>라고 말한다. 오버엔지니어링은 엄연히 이 블랙박스의 영역이다. </p>
<p>사고가 터지기 전까지는 이 과정이 정말 과해보일지 모르나, 막상 터지고 나면 수많은 사람들의 의심을 견디며 이겨낸 그의 혜안에 눈물을 흘리며 공중제비 3바퀴를 돌 수 있지 않을까?</p>
<h3 id="그렇지만-투자는-많은-비판과-회의감을-감당해야-한다">그렇지만 투자는 많은 비판과 회의감을 감당해야 한다.</h3>
<p>하지만 모든 투자가 그러하듯, 이러한 결과가 일어나기 전까지는 많은 의문과 질타를 견뎌내야만 한다. 오버엔지니어링 역시 이러한 범주에서 벗어나지 않는다. 특히 혼자서 하는 프로젝트면 성향과 상황에 따라 모르겠지만, 여럿이서 할 때는 더 그러하다. 우리의 코드는 엄연히 사유재가 아닌, 공공재이기 때문이다. </p>
<p>가끔 나는 당연히 확장성 있게 설계해야 한다고 생각하고 구축한 로직이, assertive한 비판이 날아올 경우 당혹감이 발생하기도 한다. 그럴 때면, 왜 나는 이렇게까지 비효율적으로 일하는가?라는 회의감이 들 때도 있다.</p>
<blockquote>
<p>사실 이에 대한 답을 고민하는 시간을 갖기 위해 긴 글을 쓰고 있다.
그리고 얼추 이 문제에 대한 해결 방법을 조금은 찾은 것 같다.</p>
</blockquote>
<h2 id="코드에-대한-투자를-아끼지-않은-개발자에게">코드에 대한 투자를 아끼지 않은 개발자에게</h2>
<h3 id="분명-그대는-틀리지-않았다">분명, 그대는 틀리지 않았다</h3>
<p>내가 내린 결론은, 오버엔지니어링은 개발자의 숙명이라 생각한다. 아이너리하다. 개발자의 숙명이 곧 과한 방법론을 따른다는 것이라니.</p>
<p>하지만 나는 오버엔지니어링이라는 말을 듣고 기죽지 않았으면 한다. (이건 나에게 하는 말이기도 하다.) 오버엔지니어링을 생각한 개발자에게는 질타보다는 우선적으로 고마움을 느껴야 한다고 생각한다.</p>
<p>갇혀있을 때에는, 결코 확장적인 생각을 할 수 없다. 
알고리즘 문제를 풀다 보면, 가끔은 이 수십 줄 때문에 왜 내가 며칠을 고민했을까하는 허탈함이 들지 않는가? <strong>그렇다. 갇혀진 생각은 스스로 새로운 영역으로 확장하기까지 엄청 오랜 시간이 걸린다.</strong></p>
<p>오버엔지니어링은 그러한 생각을 넓혀주는 데 일조한다. 막상 A만 생각했던 팀원들에게, B와 C라는 선택지도 있음을 코드로 보여줄 수 있는 것이다. 비판이라는 것도  결국에는 비즈니스를 사고하기 때문에 가능한 것이다. 만약 누군가가 반복되는 오버엔지니어링으로 인해 싫증이 났다면, 내가 이 비즈니스에 대한 확장성을 어디까지 생각해보아야 할지를 다시 고민해보자. 알고 보면, 내가 우물 안 개구리였음을 가끔 깨닫고는 다른 사람의 코드에 놀라게 된다.</p>
<h3 id="다만-코드는-항상-비즈니스와-가용-자원을-먼저-생각하자">다만, 코드는 항상 비즈니스와 가용 자원을 먼저 생각하자.</h3>
<p>그렇지만 모든 낙관적인 엔지니어링은 결과적으로 기준선을 모호하게 만들 수 있다. 결국에는 과하게 디자인하는 것이 최고라고 묻는다면, 그건 아니다. </p>
<p>1주일의 시간을 줬는데 1달만에 완벽하게 마무리했다면, 그것은 좋은 결과물을 얻었을지언정, 좋은 결과를 얻었다고 말할 수는 없기 때문이다. (모든 것은 기회비용을 따르기 때문이다.) 그렇기에 오버엔지니어링은 부분적으로는 관용적이면서, 또한 남용되지 않도록 경계를 해야 한다.</p>
<p>다만 &#39;관용적인 오버엔지니어링&#39;은 어느 정도의 범위인지 그 모호함에 의문을 품게 됐고, 마침내 핵심을 정하게 됐다.</p>
<blockquote>
<p>그것은 바로, <strong>이 코드가 현재의 자원과 비즈니스를 생각하는가</strong>이다.</p>
</blockquote>
<h3 id="비즈니스를-생각하는-코드">비즈니스를 생각하는 코드</h3>
<p>결국 개발자도 회사의 일원이며, 개발은 회사의 문제를 해결하기 위한 솔루션일 뿐이다. 따라서 회사의 비즈니스의 상황과 미래를 생각하고 개발해야 한다.</p>
<p>어떤 코드가 정상적으로 돌아갈 때, 우리가 납득할 수 없는 코드는 내 생각에는 둘 중 하나이다. </p>
<ol>
<li>그 코드를 본 사람의 context가 부족하거나, </li>
<li>그 코드를 짠 사람의 context가 부족하거나이다. </li>
</ol>
<p>만약 전자라면 전자는 이 코드가 왜 적정엔지니어링이라고 판단했는지 설명해주어야 한다. 결과는 결국 같이 코드를 공유하는 사람이 내려줄 것이다. 설령 오버엔지니어링이라는 평가를 받아도, 결국 이 문제가 회사의 문제를 해결하기 위한 과정이라고 사람들이 생각했다면, 아쉬워하지 말자. 당신은 오늘도 팀원에게 더 폭넓은 시각을 제공해준 고마운 팀원이다.</p>
<p>후자라면 오히려 팀원들에게 또 감사해야 한다. 당신은 미처 알지 못했던 문제의 본질을 다른 팀원들을 통해 더욱 확실히 알게 되었다.</p>
<blockquote>
<p>결국 비즈니스는 각자가 추상화한 개념이 다르며, 이를 서로 합쳐야 할 순간이 온다. 따라서, 오버엔지니어링을 고민하는 순간은 단지 그 순간이 왔을 뿐이다. 이를 부정적으로 받아들이기보다는, 서로가 생각하는 비즈니스의 상태를 일관성 있게 맞춘다는 느낌으로 받아들이면 어떨까 싶다.</p>
</blockquote>
<h3 id="자원은-한정적임을-이해하자">자원은 한정적임을 이해하자</h3>
<p>또한, 시간, 인적 자원 역시 생각해봐야 한다. 가끔, 오버엔지니어링이 질타를 받는 경우는 정말 할 일이 많은데, 주어진 문제를 딜레이하는 경우이다. 이를 미연에 방지하려면, 코드 품질은 어느정도 타협선을 가져야 한다는 것을 인정해야 한다. </p>
<p>어느 누구도 무한의 자원이 주어졌을 때, 문제를 깔끔한 코드로 해결하지 못하리라는 법이 없다. 우리가 가치를 가질 수 있는 이유는, 제한된 시간적, 인적 자원 안에, 충분히 만족할 만한 생산성을 갖고 있기 때문이다.</p>
<p>일단 주어진 자원 안에 문제를 해결할 수 있는 리스트를 찾고, 가장 모두가 만족할 만한 코드로 결과를 만들어내자. 안타깝게도, 이 역량은 결국 경험이라는 다소 두루뭉술한 답변을 내놓아야 하겠지만 말이다.</p>
<p>그래서 요새는 과한 디자인을 보면, 오히려 그 코드를 짠 개발자들에게 고생했다는 마음이 든다. (이는 끊임없이 좋은 설계인지 의심하며 자신을 괴롭히면서도 스스로에게 느낀다.)</p>
<blockquote>
<p><strong>모두가 쉬고 싶다는 욕심을 가지고 있다. 그럼에도 이 코드를 짠 사람은 그러한 욕심을 뒤로한 채, 자원을 고려해서 최선을 다해 짠 코드라고 생각하기 때문이다.</strong></p>
</blockquote>
<p>이런 마음이 든다면, 어느 순간 다른 사람들의 코드에 좀 더 너그러워진다.
그렇기에 오버엔지니어링은 비즈니스와 자원(시간, 사람)에서 어느정도 허용해야 하지 않을까.</p>
<p>우리는 오버엔지니어링에 대한 경계를 하면서도, 동시에 관용적이어야 하지 않을까. 왜? 이것은 블랙박스이며, <strong>미래에 대한 투자</strong>이기 때문이다.</p>
<h1 id="🚀-마치며">🚀 마치며</h1>
<p>사실 내 프로젝트가 더 잘되고 싶은 마음에, 더 코드를 확장적이면서도 안정적으로 설계하면서도, 하여금 이렇게 짜여진 코드가 너무 과하지는 않은지 걱정하다 보니 이러한 생각들을 머리 속에 수놓게 됐다.</p>
<p>나를 변명(?)하는 것일 수도 있지만, 비즈니스와 자원 속에서 아직은 과하다 싶은 디자인으로 엔지니어링된 코드는 비즈니스적인 고려가 있었다면 충분히 서로 맛볼 기회가 있어야 한다고 생각한다.</p>
<blockquote>
<p><strong>결국에 오버엔지니어링인지, 적정엔지니어링인지 판단하는 것은 다같이 고민해야 할 일이기 때문이다. 그 과정이 어떻게 되든, 서로 고민하면서 성장하는 것이 결국 개발자로서의 삶이 아닐까.</strong> </p>
</blockquote>
<p>여튼, 이 글을 쓰는데 꽤나 오랜 시간이 걸렸지만, 충분히 이 의식의 흐름을 따라가본 것은 충분히 가치있는 일이었던 것 같다.</p>
<blockquote>
<p>오늘도 설계와 씨름을 하는 개발자 분들 모두 화이팅하시길 바라며. 이상.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 행렬과 연산 (Lv 4, 자바스크립트)]]></title>
            <link>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%96%89%EB%A0%AC%EA%B3%BC-%EC%97%B0%EC%82%B0-Lv-4-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</link>
            <guid>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%96%89%EB%A0%AC%EA%B3%BC-%EC%97%B0%EC%82%B0-Lv-4-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</guid>
            <pubDate>Fri, 20 Jan 2023 08:24:26 GMT</pubDate>
            <description><![CDATA[<h1 id="🌈-시작하며">🌈 시작하며</h1>
<blockquote>
<p>이 문제 정말 재밌어요. 
저는 개인적으로 이 문제가 요근래 푼 문제 중 가장 우아하다고 생각했습니다.</p>
</blockquote>
<p>정답률이 꽤 낮아서, 이번에 몸이 아파서 개발을 잠시 쉬는 와중에 한 번 풀어봤는데, 푸는 시간 내내 시간 가는 줄 몰랐답니다 🙃</p>
<p>그렇다면, 어떻게 풀었는지 살펴볼까요?</p>
<h1 id="🚦-본론">🚦 본론</h1>
<h2 id="상황-가정">상황 가정</h2>
<p>저는 요새 문제를 풀 때 저만의 문제를 구현해야 하는 상황을 가정하고 푸는 연습을 합니다.
이유는, 결국 알고리즘을 공부하며 우리가 달성하고자 하는 것은, <strong>이런 문제가 생기면 어떻게 풀 것인가</strong>를 익히는 것이라고 생각했습니다.</p>
<p>이를 확장하면, 결국 <strong>문제를 해결하기 위한 구현 방식을 어떻게 설계할 것인가</strong> 역시 거시적인 관점에서 알고리즘 해결 과정에 포함된다고 생각합니다.</p>
<p>따라서 문제에 주어진 상황과 더불어 오버엔지니어링을 허락하는 상황을 가정해보기로 했어요.</p>
<blockquote>
<p><strong>주어진 상황</strong></p>
<ul>
<li>행렬에 대한 계산 솔루션을 구현해야 한다.</li>
<li>이 행렬 계산기는 현재 앞으로도 구현할 사항이 많아질 것으로 예상하여, 초기부터 잘 설계해야 하기로 팀 간에 합의가 되었다.</li>
<li>행렬의 출력 방법은 클라이언트의 요구에 따라 앞으로도 다양해질 수 있다.</li>
</ul>
</blockquote>
<p>어차피 몸이 아파서 더이상 공부도 못하겠고, 시간도 많겠다! 😆
문제를 푸는 데 추가 상황까지 생각하니 정말 재밌었어요.</p>
<blockquote>
<p>🚨 이 문제 해결과정은 주어진 배경 자체가 남들이 보기에 오버엔지니어링스러운 답이 나오겠다는 느낌이 들 거에요. 대신 이해하기 쉽게끔 자세하게 풀이로 서술해볼게요! 🙆🏻</p>
</blockquote>
<h3 id="시간복잡도">시간복잡도</h3>
<p>사실 이 문제를 봤을 때부터 어떻게 풀지는 금방 느낌이 왔어요.
<strong>대충 N = 100000(행렬의 총 행/렬 셀 개수)인데, 연산이 100000개라니!</strong>
(여기서부터 눈치를 채셔야 합니다. 2차원 배열을 업데이트하며 푸는 건 안 되겠다는 걸 말이죠.)</p>
<p>일단 대충 <code>O(NlogN)</code>이어야 풀 수 있을 거 같은데, 일반적인 로직으로는 연산을 처리하는 도중에 끝나버릴 것 같다는 생각이 들었어요.</p>
<blockquote>
<p>즉, 이건 최적화를 위한 고민을 잘 해야 하는 문제라는 것을 단번에 직감했습니다.</p>
</blockquote>
<p>따라서 다음과 같은 가정을 했습니다.</p>
<ul>
<li><code>Rotate</code>를 하면 어떻게 바뀌는 걸까?</li>
<li><code>ShiftRow</code>를 하면 어떻게 바뀌는 걸까?</li>
</ul>
<p>핵심은, <strong>이 메서드를 어떻게 최적화하는지</strong>라는 것을 알 수 있는 대목입니다.</p>
<h3 id="메서드-분석">메서드 분석</h3>
<h4 id="rotate"><code>Rotate</code></h4>
<p>우리가 하나의 객체를 만들게 되고, 여기에는 <code>Rotate</code>, <code>ShiftRow</code>라는 2개의 연산을 관장한다고 생각해봅시다.</p>
<p>그렇다면, <code>Rotate</code>는 어떻게 매트릭스가 바뀔까요?</p>
<p>가령 이런 배열이 있다고 상상해봅시다.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/fa4f5bee-73ab-45bf-9301-86912fe967ae/image.png" alt=""></p>
<p>이 배열이 <code>Rotate</code>하면 어떻게 될까요?</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/057b4c2e-b523-4865-882c-6f77462eea39/image.png" alt=""></p>
<p>이런 식으로 바뀌게 될 것입니다.
그런데 생각해보자구요. </p>
<blockquote>
<p>이 연산을 1000번 했을 때, 검은색 부분이 바뀔 가능성이 있을까요?</p>
</blockquote>
<p>절대 없을 겁니다.
즉, 우리는 <code>Rotate</code> 여러 번 할 때, 이를 해당 <strong>분홍색 구간</strong>만 제어하면 된다는 이야기입니다.</p>
<p>일단 이정도의 영감만 얻고, 다음 메서드를 살펴보죠!</p>
<h4 id="shiftrow"><code>ShiftRow</code></h4>
<p>연속으로 위의 변화한 배열에서 <code>ShiftRow</code>를 한다고 생각해봅시다.
기존의 다음과 같은 배열은</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/350a3429-42c8-402f-ba28-d373781373f7/image.png" alt=""></p>
<p>이렇게 변하게 될 것입니다.
이중, 가장 두드러진 특징은 <strong>맨 아래쪽이 윗쪽으로 이동한다</strong>는 것인데요.
사실 이 메서드는 모든 셀을 변화시키는데요. 가장 두드러진 특징은 맨 아래의 줄이 삭제되고, 맨 앞에 넣어진다는 것입니다. 이는 <code>Deque</code>의 특징과 같군요?!</p>
<p>따라서 덱의 느낌을 잘 주는 가장 관련된 부분을 녹색으로 표시했습니다.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/7c921acc-1220-4af6-b03a-58ba0becc7d1/image.png" alt=""></p>
<h3 id="그래서-하고-싶은-말이-무엇인가요-😖">그래서 하고 싶은 말이 무엇인가요? 😖</h3>
<p>어찌 보면 여기까지 봤을 때, 쉽게 설계에 접근하지 못할 수 있습니다.
왜냐구요? 일반적인 인지사고로는 자연스럽게 <strong>주어진 문제가 행렬이라는 측면에서 직관적으로 2차원 배열을 떠올리기 때문입니다.</strong></p>
<p>하지만 다르게 보면 어떨까요?
비슷한 것들끼리 한 번 묶는 방법 중에는, 다음과 같이 묶을 수 있을 것 같아요.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/5c8874e2-4826-4666-965a-9004b622f080/image.png" alt=""></p>
<blockquote>
<p>엇... 뭔가 느낌이 들지 않나요?
<strong>위/아래 줄 값에서 삽입/삭제가 이루어지는 뭔가 특이한 자료구조</strong>가 3개가 탄생했군요!</p>
</blockquote>
<p>우선 이 분류된 3개의 그룹을 다음과 같이 정의하겠습니다.</p>
<ul>
<li>왼쪽 그룹: <code>Left</code></li>
<li>중앙 그룹: <code>Main</code></li>
<li>오른쪽 그룹: <code>Right</code></li>
</ul>
<h4 id="left">Left</h4>
<p>자. 그러면 이 <code>Left</code>는 <code>Rotate</code>할 때에는 다음과 같이 되겠군요.</p>
<ul>
<li><code>Rotate</code>할 때 맨 윗쪽이 <code>Main</code>의 맨 첫번째 줄 앞으로 빠져나가겠네요.</li>
<li><code>Rotate</code>할 때 맨 아랫쪽에서는 <code>Main</code>의 맨 아랫 줄 맨 앞의 것이 들어오겠네요.</li>
<li><code>ShiftRow</code>할 때 자체적으로 맨 아랫 것이 맨 윗쪽으로 가겠네요.</li>
</ul>
<p>어? 그렇다면, <code>ShiftRow</code>를 효율적으로 구현하기 위해 <strong>Deque</strong>의 자료구조로 구현하면 어떨까요?</p>
<p>그럼 결과적으로 <code>O(1)</code>만큼의 삽입과 삭제가 이루어지므로 연산을 최적화할 수 있겠군요!</p>
<h4 id="main">Main</h4>
<p>그렇다면 Main도 따져보자구요. (Main의 경우 좀 어려울 수 있어요! 🙇🏻‍♂️)
Main의 경우는 한 줄에 또 여러 개의 값을 갖고 있음에 주의하면서 봅시다.</p>
<ul>
<li><code>Rotate</code>할 때 맨 윗쪽에서는 <code>Left</code>의 맨 앞이 들어오겠네요.</li>
<li><code>Rotate</code>할 때 맨 마지막 값은<code>Right</code>의 맨 앞으로 빠져나가겠네요</li>
<li><code>Rotate</code>할 때 맨 아랫쪽의 맨 뒷쪽 값으로 <code>Right</code>의 맨 뒤의 값이 들어오겠네요.</li>
<li><code>Rotate</code>할 때 맨 아랫쪽의 맨 앞쪽 값이 <code>Left</code>의 맨 아랫쪽 값으로 빠져나가겠네요.</li>
<li><code>ShiftRow</code>할 때 자체적으로 맨 아랫 것이 맨 윗쪽으로 가겠네요.</li>
</ul>
<p>결과적으로 *<em><code>Main</code>의 각 줄 역시 <code>Rotate</code>를 할 때 삽입, 삭제가 양쪽 모두 이루어져야 합니다. *</em>(<code>shiftRow</code>하다 보면 언제 맨 위/아래가 될지 모르기 때문이죠)
따라서 <code>Main</code>의 각 <code>row</code>는 <code>Deque</code>로 구현하면 되겠네요.</p>
<p>그렇다면 <code>Main</code> 자체는 어떨까요?
이 역시 <code>ShiftRow</code>할 때, 맨 앞쪽을 삽입해야 하므로 <code>Deque</code>로 구현해야 합니다.
<strong>즉, <code>Main</code>은 <code>Deque[Deque[]]</code>의 2차원으로 이루어진 덱 구조겠네요.</strong></p>
<h4 id="right">Right</h4>
<p>마지막으로 <code>Right</code> 역시 <code>Left</code>와 마찬가지의 형태를 띄고 있죠?
따라서 <code>Deque</code>로 이루어진 자료구조로 쉽게 두 연산을 최적화할 수 있어요.</p>
<h3 id="끝이-아니다-커맨드-최적화가-가능하다">끝이 아니다! 커맨드 최적화가 가능하다!</h3>
<p>그런데 말이죠. 커맨드를 압축시킨다면 어떨까요?</p>
<p>예컨대, <code>shiftRow</code>는 총 행렬의 <code>Row</code>개만큼 적용하면 원래의 행렬 상태로 돌아옵니다. 또한, <code>rotate</code> 역시 테두리만큼 연산이 적용되면 원래의 행렬 상태로 돌아오죠.</p>
<blockquote>
<p>즉, 최적화할 수 있는 부분이 존재하지요!</p>
</blockquote>
<h3 id="중간-정리---연산-최적화를-위한-자료구조-선택">중간 정리! - 연산 최적화를 위한 자료구조 선택</h3>
<p>결과적으로 우리는 연산 최적화를 할 수 있는 방법을 찾아냈어요.
<strong>그것도 우아하게, 자료구조를 잘 선택하는 것만으로요. 😉</strong></p>
<blockquote>
<ol>
<li>행렬이 아닌, 2차원 배열을 3개의 덱으로 분할하고 연산마다 로직에 맞게 적용한다.</li>
<li>이는 각 연산이 덱을 통해 이루어지므로 O(1)의 시간복잡도로 연산을 해결한다.</li>
<li>모든 커맨드들을 똑같은 것들끼리 압축함으로써 최적화가 가능하다.</li>
</ol>
</blockquote>
<h2 id="코드-구현-과정">코드 구현 과정</h2>
<blockquote>
<p>🚨 여기는 앞서 말한, 저 나름대로의 문제를 가정하고 해결하면서 나온  <strong>오버 엔지니어링</strong> 구간입니다.
만약 단순히 답을 참고하고 싶다면 맨 아랫 쪽의 &lt;결과 코드&gt;로 이동해주세요.</p>
</blockquote>
<p>따라서 이제는 로직을 설계해야 해요.
어떻게 하면 이 문제를 부분적으로 잘 나누고, 객체 지향적인 설계로 해결할 수 있을까요?</p>
<h3 id="세부-구현---내부-클래스-시스템의-전체적인-동작을-핸들링하는-matrixcalculator">세부 구현 - 내부 클래스 시스템의 전체적인 동작을 핸들링하는 <code>MatrixCalculator</code></h3>
<p>저는 일단 이 문제를 바라봤을 때, <strong>퍼사드 패턴</strong>으로 큰 객체를 만드는 게 가장 바람직하다고 생각했습니다.</p>
<p>이유는, 모든 서브 클래스 시스템들의 로직을 통틀어 &quot;계산기&quot; 자체를 구동하는 인터페이스로 제공하는 게 더욱 직관적이기 때문입니다.</p>
<p>그런 면에서 퍼사드 패턴이 가장 깔끔하게 문제를 푸는 <code>Best Practice</code>라고 생각했습니다.
(다만 아직 디자인 패턴을 공부하면서 푸는 거라, 제대로 구현했는지는 모르겠군요. 😭)</p>
<p>따라서 어떤 서브 시스템 클래스들을 핸들링하기 위해 이 로직의 맨 앞쪽을 담당하는 객체를 <code>MatrixCalculator</code>라고 하겠습니다.</p>
<p>그리고 이 객체가 다루는 서브 시스템 클래스에는 다음과 같은 것들이 있습니다.</p>
<ul>
<li><code>Matrix</code>: 데이터(행렬)를 책임지며, 연산에 맞춰 바뀐 결과를 적용합니다.</li>
<li><code>RotateMatrixArrayPrinterStrategy</code>: 출력을 담당합니다. 추후 클라이언트의 요구에 따라 계산 결과를 나타내는 방법이 달라질 수 있다고 문제에서 명시되었습니다. 따라서 같은 메서드로 다른 알고리즘을 적용하기 위해 전략 패턴을 적용했습니다.</li>
<li><code>MatrixCommander</code>: 이 행렬에 주어진 커맨드들을 같은 것끼리 압축시켜서 전달하는 로직만 책임지는 객체입니다. 이에 대한 최적화 방법 역시 다양해질 수 있으니 분리하는 게 맞다 판단했어요.</li>
</ul>
<h4 id="해당-코드-구현">해당 코드 구현</h4>
<pre><code class="language-js">class MatrixCalculator {
  constructor({ commands, matrix, printerStrategy }) {
    this.commander = commands;
    this.matrix = matrix;
    this.printerStrategy = printerStrategy;
  }

  run() {
    while (this.commander.commandLength) {
      const [command, count] = this.commander.command();

      if (command === this.commander.TYPE_SHIFT_ROW) {
        this.matrix.shiftRow(count);
      }

      if (command === this.commander.TYPE_ROTATE) {
        this.matrix.rotate(count);
      }
    }
  }

  getResult() {
    return this.printerStrategy.print();
  }
}</code></pre>
<h4 id="solutionjs">solution.js</h4>
<p>결과적으로 반환할 코드에서는 계산기에 대한 인터페이스만 입력하면 끝! 더욱 직관적으로 로직을 해석할 수 있게 되었어요.</p>
<pre><code class="language-js">const solution = (rc, operations) =&gt; {
  const matrix = new Matrix(rc);

  const matrixCalculator = new MatrixCalculator({
    commands: new MatrixCommander({ commands: operations }),
    matrix,
    printerStrategy: new RotateMatrixArrayPrinterStrategy(matrix),
  });

  matrixCalculator.run();

  return matrixCalculator.getResult();
};</code></pre>
<h3 id="세부-구현---일련의-커맨드를-최적화하여-전달하는matrixcommander">세부 구현 - 일련의 커맨드를 최적화하여 전달하는<code>MatrixCommander</code></h3>
<p>커맨더는 말 그대로 행렬의 커맨더만 압축하며, 내부 상태를 담을 자료구조는 큐로 구현되었습니다.</p>
<pre><code class="language-js">class Queue {
  constructor(queue) {
    this.queue = Array.isArray(queue) ? queue : [];
    this.rear = this.queue.length;
    this.front = 0;
  }

  enqueue(val) {
    this.queue.push(val);
    this.rear += 1;
  }

  dequeue() {
    const value = this.queue[this.front];
    delete this.queue[this.front];

    this.front += 1;
    return value;
  }

  get length() {
    return this.rear - this.front;
  }
}

class MatrixCommander {
  constructor({ commands }) {
    this.taskQueue = new Queue();
    this._init(commands);
  }

  get TYPE_SHIFT_ROW() {
    return &#39;ShiftRow&#39;;
  }

  get TYPE_ROTATE() {
    return &#39;Rotate&#39;;
  }

  _init(commands) {
    let prev = null;
    let count = 0;

    for (let i = 0; i &lt; commands.length; i += 1) {
      const nowCommands = commands[i];

      if (prev === null || prev === nowCommands) {
        count += 1;
      } else {
        this.taskQueue.enqueue([prev, count]);

        count = 1;
      }

      prev = nowCommands;

      if (i === commands.length - 1) {
        this.taskQueue.enqueue([prev, count]);
      }
    }
  }

  command() {
    if (!this.taskQueue.length) return;

    // [command, runCount]
    return this.taskQueue.dequeue();
  }

  get commandLength() {
    return this.taskQueue.length;
  }
}</code></pre>
<h3 id="세부-구현---데이터를-저장하고-연산에-따라-조작하는-matrix">세부 구현 - 데이터를 저장하고 연산에 따라 조작하는 Matrix</h3>
<p><code>Matrix</code> 객체의 경우 각 커맨더에서 나온 현재의 커맨드에 따라 <code>MatrixCalculator</code>가 메서드를 호출시키는데요. 이에 맞춰 연산을 하고 데이터를 반영합니다.</p>
<p>내부 데이터의 경우 위에서 서술했던 3개의 그룹(<code>left</code>, <code>right</code>, <code>main</code>)
으로 분류하고 구현했어요!</p>
<pre><code class="language-js">class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

class Deque {
  constructor() {
    this.init();
  }

  init() {
    this.count = 0;
    this.front = null;
    this.rear = null;
  }

  unshift(value) {
    const node = new Node(value);

    if (!this.front) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevFront = this.front;
      cachedPrevFront.prev = node;

      this.front = node;

      node.next = cachedPrevFront;
    }

    this.count += 1;
    return this.count;
  }

  shift() {
    if (this.count === 0) return null;

    const value = this.front.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.front = this.front.next;
      this.front.prev = null;
      this.count -= 1;
    }

    return value;
  }

  push(value) {
    const node = new Node(value);

    if (this.count === 0) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevRear = this.rear;
      cachedPrevRear.next = node;

      node.prev = cachedPrevRear;

      this.rear = node;
    }

    this.count += 1;

    return this.count;
  }

  pop() {
    if (this.count === 0) return;

    const value = this.rear.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.rear = this.rear.prev;
      this.rear.next = null;
      this.count -= 1;
    }

    return value;
  }

  getValue(idx) {
    if (idx &gt;= this.count) return;
    let node = this.front;

    for (let i = 0; i &lt; idx; i += 1) {
      node = node.next;
    }

    return node.value;
  }

  get rawArray() {
    let arr = [];
    let node = this.front;

    for (let i = 0; i &lt; this.count; i += 1) {
      arr.push(node.value);
      node = node.next;
    }

    return arr;
  }

  get length() {
    return this.count;
  }
}
class Matrix {
  constructor(matrix) {
    this.left = new Deque();
    this.right = new Deque();
    this.main = new Deque();

    this._init(matrix);
  }

  _init(matrix) {
    for (let i = 0; i &lt; matrix.length; i += 1) {
      const row = matrix[i];
      const deque = new Deque();

      row.forEach((val) =&gt; {
        deque.push(val);
      });

      this.left.push(deque.shift());
      this.right.push(deque.pop());
      this.main.push(deque);
    }
  }

  rotate(count) {
    let remainCount = count % (this.main.front.value.length * 2 + this.left.length + this.right.length);

    while (remainCount) {
      remainCount -= 1;

      this.main.front.value.unshift(this.left.shift());
      this.right.unshift(this.main.front.value.pop());
      this.main.rear.value.push(this.right.pop());
      this.left.push(this.main.rear.value.shift());
    }
  }

  shiftRow(count) {
    let remainCount = count % this.main.length;

    while (remainCount) {
      remainCount -= 1;

      this.main.unshift(this.main.pop());

      this.left.unshift(this.left.pop());
      this.right.unshift(this.right.pop());
    }
  }
}</code></pre>
<h3 id="세부-구현---연산에-따른-최종-결과를-반환하는-rotatematrixarrayprinterstrategy">세부 구현 - 연산에 따른 최종 결과를 반환하는 <code>RotateMatrixArrayPrinterStrategy</code></h3>
<p>이를 상속할 객체의 경우, 다른 하위 클래스들이 아직은 구현되지 않아 구현하지 않았어요. 이를 상속하는 로직을 만드는 건 어렵지 않으니까요!</p>
<p><code>matrix</code> 객체의 데이터를 상태로 갖고 있으며, 원할 때 이 <code>print</code> 내부 알고리즘을 적용하여 결과 값을 배열 형태로 반환하도록 구현했어요.</p>
<pre><code class="language-js">
class RotateMatrixArrayPrinterStrategy {
  constructor(matrix) {
    this.matrix = matrix;
  }

  print() {
    let result = [];

    const leftArr = this.matrix.left.rawArray;
    const mainArr = this.matrix.main.rawArray;
    const rightArr = this.matrix.right.rawArray;

    for (let i = 0; i &lt; mainArr.length; i += 1) {
      const row = [];

      row.push(leftArr[i]);
      row.push(...mainArr[i].rawArray);
      row.push(rightArr[i]);

      result.push(row);
    }

    return result;
  }
}</code></pre>
<h2 id="결과-코드">결과 코드</h2>
<p>전체 코드를 쓴 결과는 다음과 같구요!</p>
<pre><code class="language-js">class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

class Deque {
  constructor() {
    this.init();
  }

  init() {
    this.count = 0;
    this.front = null;
    this.rear = null;
  }

  unshift(value) {
    const node = new Node(value);

    if (!this.front) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevFront = this.front;
      cachedPrevFront.prev = node;

      this.front = node;

      node.next = cachedPrevFront;
    }

    this.count += 1;
    return this.count;
  }

  shift() {
    if (this.count === 0) return null;

    const value = this.front.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.front = this.front.next;
      this.front.prev = null;
      this.count -= 1;
    }

    return value;
  }

  push(value) {
    const node = new Node(value);

    if (this.count === 0) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevRear = this.rear;
      cachedPrevRear.next = node;

      node.prev = cachedPrevRear;

      this.rear = node;
    }

    this.count += 1;

    return this.count;
  }

  pop() {
    if (this.count === 0) return;

    const value = this.rear.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.rear = this.rear.prev;
      this.rear.next = null;
      this.count -= 1;
    }

    return value;
  }

  getValue(idx) {
    if (idx &gt;= this.count) return;
    let node = this.front;

    for (let i = 0; i &lt; idx; i += 1) {
      node = node.next;
    }

    return node.value;
  }

  get rawArray() {
    let arr = [];
    let node = this.front;

    for (let i = 0; i &lt; this.count; i += 1) {
      arr.push(node.value);
      node = node.next;
    }

    return arr;
  }

  get length() {
    return this.count;
  }
}

class Queue {
  constructor(queue) {
    this.queue = Array.isArray(queue) ? queue : [];
    this.rear = this.queue.length;
    this.front = 0;
  }

  enqueue(val) {
    this.queue.push(val);
    this.rear += 1;
  }

  dequeue() {
    const value = this.queue[this.front];
    delete this.queue[this.front];

    this.front += 1;
    return value;
  }

  get length() {
    return this.rear - this.front;
  }
}

class MatrixCommandar {
  constructor({ commands }) {
    this.taskQueue = new Queue();
    this._init(commands);
  }

  get TYPE_SHIFT_ROW() {
    return &#39;ShiftRow&#39;;
  }

  get TYPE_ROTATE() {
    return &#39;Rotate&#39;;
  }

  _init(commands) {
    let prev = null;
    let count = 0;

    for (let i = 0; i &lt; commands.length; i += 1) {
      const nowCommands = commands[i];

      if (prev === null || prev === nowCommands) {
        count += 1;
      } else {
        this.taskQueue.enqueue([prev, count]);

        count = 1;
      }

      prev = nowCommands;

      if (i === commands.length - 1) {
        this.taskQueue.enqueue([prev, count]);
      }
    }
  }

  command() {
    if (!this.taskQueue.length) return;

    // [command, runCount]
    return this.taskQueue.dequeue();
  }

  get commandLength() {
    return this.taskQueue.length;
  }
}

class Matrix {
  constructor(matrix) {
    this.left = new Deque();
    this.right = new Deque();
    this.main = new Deque();

    this._init(matrix);
  }

  _init(matrix) {
    for (let i = 0; i &lt; matrix.length; i += 1) {
      const row = matrix[i];
      const deque = new Deque();

      row.forEach((val) =&gt; {
        deque.push(val);
      });

      this.left.push(deque.shift());
      this.right.push(deque.pop());
      this.main.push(deque);
    }
  }

  rotate(count) {
    let remainCount = count % (this.main.front.value.length * 2 + this.left.length + this.right.length);

    while (remainCount) {
      remainCount -= 1;

      this.main.front.value.unshift(this.left.shift());
      this.right.unshift(this.main.front.value.pop());
      this.main.rear.value.push(this.right.pop());
      this.left.push(this.main.rear.value.shift());
    }
  }

  shiftRow(count) {
    let remainCount = count % this.main.length;

    while (remainCount) {
      remainCount -= 1;

      this.main.unshift(this.main.pop());

      this.left.unshift(this.left.pop());
      this.right.unshift(this.right.pop());
    }
  }
}

class RotateMatrixArrayPrinterStrategy {
  constructor(matrix) {
    this.matrix = matrix;
  }

  print() {
    let result = [];

    const leftArr = this.matrix.left.rawArray;
    const mainArr = this.matrix.main.rawArray;
    const rightArr = this.matrix.right.rawArray;

    for (let i = 0; i &lt; mainArr.length; i += 1) {
      const row = [];

      row.push(leftArr[i]);
      row.push(...mainArr[i].rawArray);
      row.push(rightArr[i]);

      result.push(row);
    }

    return result;
  }
}

class MatrixCalculator {
  constructor({ commands, matrix, printerStrategy }) {
    this.commandar = commands;
    this.matrix = matrix;
    this.printerStrategy = printerStrategy;
  }

  run() {
    while (this.commandar.commandLength) {
      const [command, count] = this.commandar.command();

      if (command === this.commandar.TYPE_SHIFT_ROW) {
        this.matrix.shiftRow(count);
      }

      if (command === this.commandar.TYPE_ROTATE) {
        this.matrix.rotate(count);
      }
    }
  }

  getResult() {
    return this.printerStrategy.print();
  }
}

const solution = (rc, operations) =&gt; {
  const matrix = new Matrix(rc);

  const matrixCalculator = new MatrixCalculator({
    commands: new MatrixCommandar({ commands: operations }),
    matrix,
    printerStrategy: new RotateMatrixArrayPrinterStrategy(matrix),
  });

  matrixCalculator.run();

  return matrixCalculator.getResult();
};</code></pre>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/30660e98-9a4d-4ffa-b94e-220cba8158e9/image.png" alt=""></p>
<p>정말 빠른 속도로 문제를 해결할 수 있게 되었군요! 😉</p>
<h1 id="👋🏻-마치며">👋🏻 마치며</h1>
<p>이 문제는 우아해요. 왜냐하면 반드시 2차원 배열이라는 관점을 비틀면서도, 해답은 간단하게 자료구조를 사용하여 해결하기 때문입니다. 마치 기본에 집중하라고 알려주는 듯한 느낌이 들었어요!
심지어, 이게 인턴 문제이기 때문에 이러한 메시지가 정말 완벽하다고 생각합니다.
이 문제를 낸 분은 정말 지혜로운 프로그래머라는 생각이 들었어요! 👍</p>
<p>괜히 쉽게 풀 것을 어렵게 돌아간 감도 있기는 합니다. 하지만 연습 차원에서 무리해봤어요. 😉</p>
<p>저는 실무를 잘하고 싶고, <strong>실무에서 오버엔지니어링을 피하기 위해서는 아이러니하게도 오버엔지니어링을 연습해야 하기 때문에 이렇게 풀어봤어요.</strong></p>
<p>혹시나 제가 잘못 이해한 디자인 패턴이라던지, 더 좋은 방법이 있다면 공유해주시면 무한 감사드립니다! 🙆🏻</p>
<blockquote>
<p>결국 나름대로 배운 것들을 적용해보고 복습하며 문제를 해결해서 기분이 좋아요! 
다들 몸 챙기시면서, 즐거운 개발하시길 바랍니다 🙆🏻 이상!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조] Deque 구현 (자바스크립트, JavaScript)]]></title>
            <link>https://velog.io/@young_pallete/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Deque-%EA%B5%AC%ED%98%84-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-JavaScript</link>
            <guid>https://velog.io/@young_pallete/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Deque-%EA%B5%AC%ED%98%84-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-JavaScript</guid>
            <pubDate>Fri, 20 Jan 2023 07:39:56 GMT</pubDate>
            <description><![CDATA[<h1 id="🚀-시작하며">🚀 시작하며</h1>
<p>덱을 구현해볼까 합니다.
사실 단일 연결 리스트로 구현한 것이라고 봐도 되지만, (...)
생각보다 <code>shift</code> 메서드로 구현된 글들이 많아 답답해서 만들었습니다.</p>
<p>만약 제가 덱을 구현하라는 문제가 주어진다면 이렇게 구현했을 것 같아요.
그렇다면, 어떻게 구현했는지 근거와 코드를 살펴볼까요?</p>
<h1 id="🚦-본론">🚦 본론</h1>
<h3 id="deque-덱">Deque, 덱</h3>
<p>무난하게 덱에 대한 설명은 나무위키에서 갖고 와보았습니다.</p>
<blockquote>
<p>덱은 양쪽 끝에서 삽입과 삭제가 모두 가능한 자료 구조의 한 형태이다. 두 개의 포인터를 사용하여, 양쪽에서 삭제와 삽입을 발생시킬 수 있다. 큐와 스택을 합친 형태로 생각할 수 있다.</p>
</blockquote>
<p>핵심은 <strong>양쪽 끝에서 삽입/삭제가 가능하며, 큐와 스택을 합친 형태이다</strong>입니다.</p>
<ul>
<li>즉, 삽입과 삭제를 양쪽에서 해야 하며,</li>
<li>이에 대한 시간복잡도는 <code>O(1)</code>로 유지해야 합니다.</li>
</ul>
<h3 id="잘못된-구현-형식---do-not-use-shift">잘못된 구현 형식 - Do not use shift</h3>
<p>그렇기에 이를 제대로 구현하기 위해서는 다음과 같은 구현방식은 바람직하지 못합니다.</p>
<pre><code class="language-js">class Deque {
    앞에서_빼내는_메서드() {
        return this.arr.shift();
    }
      앞에_추가하는_메서드(value) {
        return this.arr.unshift(value);
    }
}</code></pre>
<p>왜일까요?
자바스크립트에서 <code>shift</code>의 시간복잡도는 <code>O(N)</code>이기 때문입니다.</p>
<p>이에 대한 근거는 <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/shift">MDN의 설명</a>을 통해 충분히 유추 가능합니다.</p>
<blockquote>
<p>shift 메서드는 0번째 위치의 요소를 제거 하고 연이은 나머지 값들의 위치를 한칸 씩 앞으로 당깁니다. 그리고 제거된 값을 반환 합니다. 만약 배열의 length가 0이라면 undefined를 리턴 합니다.</p>
</blockquote>
<p>즉, 한칸씩 당기는 로직을 구현하려면 결과적으로 모든 배열의 요소들을 탐색해야 하고 이 시간복잡도는 MDN의 말이 거짓이 아니라면 <code>O(N)</code>입니다.</p>
<h3 id="그렇다면-어떻게-구현해야-할까">그렇다면 어떻게 구현해야 할까</h3>
<p>해결 방법은 직관적으로 <strong>단일 연결 리스트</strong>가 떠오릅니다.
이유는 삽입과 삭제를 <code>O(1)</code>만큼 보장해주며, 배열처럼 순서를 일관성 있게 유지해주기 때문입니다.</p>
<p>물론 단점은 존재합니다. <code>Array</code>의 형태를 raw하게 가져가는 게 아닌 <code>Object</code>의 자료구조를 가져가야 하기 때문에 전체 배열의 결과로 반환하는 데에는 <code>O(N)</code>이 듭니다.</p>
<p>하지만, <code>Deque</code>의 자료구조 특징을 위배하는 것은 아니기 때문에 충분히 사용할 만한 방법이라는 생각이 들었습니다.</p>
<h2 id="상세-구현">상세 구현</h2>
<p>사실 연결리스트를 사용하면 된다는 것을 알았으면 끝난 거라고 해도 무방합니다.
왜냐하면, 정말 연결리스트처럼 만들어지기 때문이죠. (어떻게 보면 허망합니다.)</p>
<h3 id="node-생성"><code>Node</code> 생성</h3>
<p>연결리스트처럼, 연결할 포인터 및 값을 알려주는 책임을 갖는 <code>Node</code> 객체를 생성합니다.</p>
<pre><code class="language-js">class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}</code></pre>
<h3 id="deque-생성"><code>Deque</code> 생성</h3>
<p>덱은 크게 다음과 같은 상태를 갖고 있어요.</p>
<ul>
<li><code>count</code>: 인덱스 갯수</li>
<li><code>front</code>: 맨 앞의 노드</li>
<li><code>rear</code>: 맨 뒤의 노드</li>
</ul>
<p>그리고 메서드로는 다음을 지원할 것입니다.</p>
<ul>
<li>덱 앞의 값 추가/삭제</li>
<li>덱 뒤의 값 추가/삭제</li>
<li>특정 인덱스 조회</li>
<li>배열로 전환한 값 반환</li>
<li>전체 길이 조회</li>
</ul>
<h4 id="덱-앞의-값-추가삭제">덱 앞의 값 추가/삭제</h4>
<p>그냥 자바스크립트 문법처럼, <code>shift</code>와 <code>unshift</code>라는 메서드로 이름을 만들어주었습니다.</p>
<pre><code class="language-js">class Deque {
  constructor() {
    this.init();
  }

  init() {
    this.count = 0;
    this.front = null;
    this.rear = null;
  }

  unshift(value) {
    const node = new Node(value);

    if (!this.front) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevFront = this.front;
      cachedPrevFront.prev = node;

      this.front = node;

      node.next = cachedPrevFront;
    }

    this.count += 1;
    return this.count;
  }

  shift() {
    if (this.count === 0) return null;

    const value = this.front.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.front = this.front.next;
      this.front.prev = null;
      this.count -= 1;
    }

    return value;
  }
}</code></pre>
<h4 id="덱-뒤의-값-추가삭제">덱 뒤의 값 추가/삭제</h4>
<pre><code class="language-js">class Deque {
  // ... 위의 내용 생략

  push(value) {
    const node = new Node(value);

    if (this.count === 0) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevRear = this.rear;
      cachedPrevRear.next = node;

      node.prev = cachedPrevRear;

      this.rear = node;
    }

    this.count += 1;

    return this.count;
  }

  pop() {
    if (this.count === 0) return;

    const value = this.rear.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.rear = this.rear.prev;
      this.rear.next = null;
      this.count -= 1;
    }

    return value;
  }
}</code></pre>
<h4 id="특정-인덱스-조회">특정 인덱스 조회</h4>
<pre><code class="language-js">class Deque {
  // ... 위의 내용 생략 

  getValue(idx) {
    if (idx &gt;= this.count) return;
    let node = this.front;

    for (let i = 0; i &lt; idx; i += 1) {
      node = node.next;
    }

    return node.value;
  }
}</code></pre>
<h4 id="배열-값으로-변환">배열 값으로 변환</h4>
<pre><code class="language-js">class Deque {
  // ... 위의 내용 생략

  get rawArray() {
    let arr = [];
    let node = this.front;

    for (let i = 0; i &lt; this.count; i += 1) {
      arr.push(node.value);
      node = node.next;
    }

    return arr;
  }
}</code></pre>
<h4 id="덱-길이-반환">덱 길이 반환</h4>
<pre><code class="language-js">class Deque {
  // ... 위의 내용 생략

  get length() {
    return this.count;
  }
}</code></pre>
<blockquote>
<p>사실 단일 연결 리스트만 잘 안다면 덱을 구현하는 데 어려움이 없습니다.
다만 배열로 만들 수 없다는 점은 아쉽군요. 😖
혹시나 만들 수 있는 방법을 아신다면, 꼭 알려주세요! 🙆🏻</p>
</blockquote>
<p>결과적으로 삽입/삭제에 있어 <code>O(1)</code>이며, 
앞 뒤로 데이터를 뺄 수 있는 덱의 구조를 구현할 수 있습니다.</p>
<h2 id="전체-코드">전체 코드</h2>
<pre><code class="language-js">class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

class Deque {
  constructor() {
    this.init();
  }

  init() {
    this.count = 0;
    this.front = null;
    this.rear = null;
  }

  unshift(value) {
    const node = new Node(value);

    if (!this.front) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevFront = this.front;
      cachedPrevFront.prev = node;

      this.front = node;

      node.next = cachedPrevFront;
    }

    this.count += 1;
    return this.count;
  }

  shift() {
    if (this.count === 0) return null;

    const value = this.front.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.front = this.front.next;
      this.front.prev = null;
      this.count -= 1;
    }

    return value;
  }

  push(value) {
    const node = new Node(value);

    if (this.count === 0) {
      this.front = node;
      this.rear = node;
    } else {
      const cachedPrevRear = this.rear;
      cachedPrevRear.next = node;

      node.prev = cachedPrevRear;

      this.rear = node;
    }

    this.count += 1;

    return this.count;
  }

  pop() {
    if (this.count === 0) return;

    const value = this.rear.value;

    if (this.count === 1) {
      this.init();
    } else {
      this.rear = this.rear.prev;
      this.rear.next = null;
      this.count -= 1;
    }

    return value;
  }

  getValue(idx) {
    if (idx &gt;= this.count) return;
    let node = this.front;

    for (let i = 0; i &lt; idx; i += 1) {
      node = node.next;
    }

    return node.value;
  }

  get rawArray() {
    let arr = [];
    let node = this.front;

    for (let i = 0; i &lt; this.count; i += 1) {
      arr.push(node.value);
      node = node.next;
    }

    return arr;
  }

  get length() {
    return this.count;
  }
}
</code></pre>
<h1 id="마치며">마치며</h1>
<p>어쩌다 보니 아픈 덕에(?) 알고리즘의 재미에 요즘 맛 들렸어요.
현실의 문제를 조그맣게 추상화하고, 이를 해결하는 과정이 정말 매력적이라는 생각이 들어요.</p>
<p>그리고 그 해결 방법 중에는 자료구조의 비중이 꽤나 많습니다.
<code>shift</code>와 같은 간단한 메서드가 제공된다 할지라도, 항상 이것이 좋은 방법인지 고민하는 자세가 우리가 문제를 효율적으로 해결하는 데 꼭 필요한 것 같아요.</p>
<blockquote>
<p>누군가에게 이 글이 도움이 되었다면 좋겠네요. 
즐거운 코딩하시길. 이상 🌈</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 숫자 타자 대회 (Lv.3, JavaScript)]]></title>
            <link>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%88%AB%EC%9E%90%ED%83%80%EC%9E%90%EB%8C%80%ED%9A%8C</link>
            <guid>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%88%AB%EC%9E%90%ED%83%80%EC%9E%90%EB%8C%80%ED%9A%8C</guid>
            <pubDate>Mon, 02 Jan 2023 10:30:03 GMT</pubDate>
            <description><![CDATA[<h1 id="풀이-방법">풀이 방법</h1>
<h2 id="문제-접근-방식">문제 접근 방식</h2>
<h3 id="brute-force-vs-dp">Brute Force vs DP</h3>
<p>이 역시 가장 무식한 방법인 <code>Brute Force</code>로 먼저 푸는 방법을 고려해봅시다.
과연 이에 대한 방법은 총 몇개일까요?</p>
<p>이는 다음 2가지 방법을 10만 번 한다고 할 수 있겠어요.</p>
<ul>
<li>오른손으로 누르지 않고 왼손으로 누르는 경우</li>
<li>왼손으로 누르지 않고 오른손으로 누르는 경우</li>
</ul>
<p>즉 <code>O(2^100000)</code>이라고 할 수 있겠죠!</p>
<p>이를 숫자로 환산한다면, 대략 <code>log 2 = 0.3</code>, <code>0.3 * 100000 = 약 30000</code>
따라서 약 30000자리 수의 수가 나옵니다. 절대 풀 수 없는 경우의 수에요 😖</p>
<p>따라서 이 문제는 DP로 풀어야 한다는 것을 직감할 수 있어야 해요.
왜냐! 결국 <code>이전에 내가 내린 최적의 답안에서 오른손을 택한다 vs 왼손으로 택한다</code>로 경우의 수를 최소화할 수 있기 때문입니다.</p>
<h3 id="시간복잡도와-공간복잡도를-계산해보자">시간복잡도와 공간복잡도를 계산해보자</h3>
<p>그렇다면 <code>DP</code>로 푼다면, 시간복잡도와 공간복잡도는 어떻게 나올까요?
간단합니다. 결과적으로 다음과 같이 도출할 수 있겠어요.</p>
<blockquote>
<p><code>N = numbers.length</code>, <code>M = 왼손, 오른손이 누를 수 있는 경우의 수(10)</code>일 때
공간 복잡도 = O(<code>N</code> x <code>M^2</code>)
시간 복잡도 = O(<code>N</code> x <code>M^2</code>) = <code>O(N)</code>
(M은 작은 수에 해당하므로 O(N)이라고 해도 무방할 듯합니다!)</p>
</blockquote>
<p>오! 비록 <code>N=100000</code> 이지만 거의 <code>O(N)</code>에 근접하므로 여유롭게 문제를 풀 수 있겠군요 😉</p>
<h3 id="어떻게-dp-배열을-정의할-것인가">어떻게 DP 배열을 정의할 것인가</h3>
<p>저는 3차원 배열로 생성해주었어요.
이유는, 이 문제는 좀 특이한 게 왼손/오른손이라는 케이스를 각각 독립적으로 유지해야 하기 때문입니다.</p>
<p>따라서 다음과 같이 DP 배열을 생성해줍니다.</p>
<p><code>DP[N][LEFT][RIGHT] = N번째 숫자를 누르고, 왼손이 LEFT, 오른손이 RIGHT에 위치할 때 가능한 최소값</code></p>
<p>따라서 우리가 구해야 할 값도 유추해보죠.
바로 <code>DP[numbers.length]에 존재하는 모든 인덱스 값의 최솟값</code>이겠죠? 😉</p>
<h3 id="점화식을-어떻게-정의할-것인가">점화식을 어떻게 정의할 것인가.</h3>
<h4 id="접근-1-결국-가중치는-정해져-있다">접근 1) 결국 가중치는 정해져 있다!</h4>
<p>이 문제를 일일이 가중치를 구해주기는 상당히 귀찮을 것 같아요.
따라서 저는 2차원 배열을 생성해주었어요. 이는 다음과 같은 값을 의미해요.</p>
<blockquote>
<p><code>weight[A][B]</code> = <code>A</code>번호에서 <code>B</code>번호로 이동할 때 들어가는 가중치</p>
</blockquote>
<p>따라서 이를 생성해주면, 왼손이든, 오른손이든 번호가 같은 건 매한가지니, 반복해서 사용할 수 있겠죠?</p>
<h4 id="접근-2-따라서-점화식을-생성하자">접근 2) 따라서 점화식을 생성하자.</h4>
<p>이는 이전에 풀었던 <a href="https://velog.io/@young_pallete/twin-building-forest">쌍둥이 빌딩 숲</a>보다 간단합니다.
결국 제가 <code>N</code>번째 숫자를 눌렀을 때, 왼손이 <code>L</code>에 있고, 오른손이 <code>R</code>에 있는 경우의 값을 <code>f(N,L,R)</code>이라 정의하면 나올 수 있는 점화식은 다음과 같을 거에요.</p>
<blockquote>
<p>직전에 다른 손이 누른 위치를 K라고 할 떄,</p>
<pre><code>f(N, L, R) = Math.min(f(N-1, L, K), f(N-1, K, R))</code></pre></blockquote>
<ul>
<li>직전에 왼손이 L에 있을 때 가능한 값의 경우 중 최솟값과 </li>
<li>직전에 오른손이 R에 있을 때 가능한 값의 경우 중 최솟값</li>
</ul>
<p>이중 가장 작은 값을 택하면 돼요.</p>
<h3 id="🚨-예외처리를-해줍시다">🚨 예외처리를 해줍시다!</h3>
<p>하지만 이대로 문제를 푼다면, 문제를 해결할 수 없어요.
왜냐! 이 문제에는 조건이 걸려있습니다. 아주 간사하게 말이죠.😈</p>
<blockquote>
<p>단, 숫자 자판은 버튼의 크기가 작기 때문에 같은 숫자 버튼 위에 동시에 두 엄지 손가락을 올려놓을 수 없습니다. 즉, 어떤 숫자를 눌러야 할 차례에 그 숫자 위에 올려져 있는 손가락이 있다면 반드시 그 손가락으로 눌러야 합니다.</p>
</blockquote>
<p>따라서 위 점화식은 모든 문제에 대한 최적의 해를 구할 수 있지만, 이 문제에는 제약조건이 있기 때문에, 왼손과 오른손이 같을 경우를 제외해주면 문제를 풀 수 있답니다.</p>
<p>이를 자바스크립트로는 다음과 같이 풀 수 있어요!</p>
<h2 id="결과">결과</h2>
<pre><code class="language-js">/**
 * [x] 최소 이동 가중치 배열을 생성한다.
 * [x] DP[N][L][R]을 생성한다. 이는 CASE가 N번째인 경우 왼손이 누른 위치, 오른 손이 누른 위치를 기억한다.
 * [x] 따라서 DP[N + 1] = Math.min(DP[N][L][R], DP[N][L][R])일 것이다.
 * [x] 결과를 반환한다.
 *
 * [x] 공간 복잡도 = 11000000 (n + 1)
 * [x] 시간 복잡도 = O(N * M * 2) = 100000 * 10 * 10 = 10000000
 * [x] 따라서 문제 통과할 듯...?
 */

const weights = [
  [1, 7, 6, 7, 5, 4, 5, 3, 2, 3],
  [7, 1, 2, 4, 2, 3, 5, 4, 5, 6],
  [6, 2, 1, 2, 3, 2, 3, 5, 4, 5],
  [7, 4, 2, 1, 5, 3, 2, 6, 5, 4],
  [5, 2, 3, 5, 1, 2, 4, 2, 3, 5],
  [4, 3, 2, 3, 2, 1, 2, 3, 2, 3],
  [5, 5, 3, 2, 4, 2, 1, 5, 3, 2],
  [3, 4, 5, 6, 2, 3, 5, 1, 2, 4],
  [2, 5, 4, 5, 3, 2, 3, 2, 1, 2],
  [3, 6, 5, 4, 5, 3, 2, 4, 2, 1],
];

const solution = (numbers) =&gt; {
  const DP = Array.from({ length: numbers.length + 1 }, () =&gt;
    Array.from({ length: 10 }, () =&gt; new Array(10).fill(Infinity))
  );

  DP[0][4][6] = 0;

  for (let idx = 0; idx &lt; numbers.length; idx += 1) {
    const num = numbers[idx];

    const prevDP = DP[idx];
    const nowDP = DP[idx + 1];

    for (let i = 0; i &lt; 10; i += 1) {
      for (let j = 0; j &lt; 10; j += 1) {
        const prevValue = prevDP[i][j];
        if (i === j || prevValue === Infinity) continue;

        if (nowDP[num][j] &gt; prevValue + weights[i][num]) {
            nowDP[num][j] = prevValue + weights[i][num]
        }

        if (nowDP[i][num] &gt; prevValue + weights[num][j]) {
            nowDP[i][num] = prevValue + weights[num][j]
        }
      }
    }
  }

  return Math.min(...DP[numbers.length].flat().flat());
};</code></pre>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/83a49213-3037-49e2-951c-dd1c24cf23eb/image.png" alt=""></p>
<h1 id="마치며">마치며</h1>
<p>이 문제도 풀고 나서 약 30분 동안 최적화를 고민해보았는데... 별 방법이 떠오르지 않는군요.
알고리즘 스터디가 끝난 후, 동료들과 한 번 의논해보고, 최적화 방법을 내놓아봐야겠어요.</p>
<blockquote>
<p>이 글이 누군가에게 도움이 되었으면 좋겠네요. 이상! 😉</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 쌍둥이 빌딩 숲 (Lv.4, JavaScript)]]></title>
            <link>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8C%8D%EB%91%A5%EC%9D%B4%EB%B9%8C%EB%94%A9%EC%88%B2</link>
            <guid>https://velog.io/@young_pallete/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8C%8D%EB%91%A5%EC%9D%B4%EB%B9%8C%EB%94%A9%EC%88%B2</guid>
            <pubDate>Mon, 02 Jan 2023 07:54:03 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>최근에 알고리즘 스터디에서 제가 문제를 출제하게 됐습니다.
요새 저는 알고리즘 문제를 선별할 때 다음과 같은 생각으로 문제를 내고 있어요.</p>
<blockquote>
<p><strong>내가 해결할 수 있는 난이도보다 좀 더 어려운 문제를 내고 다같이 고민해보자!</strong></p>
</blockquote>
<p>이 기준은 항상 가변적이기는 하지만, 대개 정답률과 문제 내용을 보고 내는데요!
이 문제는 그런 의미에서 좀 더 탐났습니다.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/3ea3f80e-1708-4708-a7fc-dd4fc75a72d5/image.png" alt=""></p>
<p>오! Lv4에, 정답률이 5%라니! 문제를 정말 풀고 싶었어요.
그래서 문제를 보고, 약 2시간이 걸려 혼자서 문제를 해결하는 데 성공했습니다 😭
그럼, 어떻게 풀었는지 살펴볼까요?</p>
<h1 id="문제-해결-과정">문제 해결 과정</h1>
<h2 id="알고리즘-선택">알고리즘 선택</h2>
<h3 id="dp-vs-brute-force">DP vs Brute Force</h3>
<p><code>n = 100</code>입니다. 따라서 문제에 대한 시간복잡도를 좀 널널하게 가져갈 수 있지 않을까 싶었어요.</p>
<p>하지만 이 문제는 경우의 수를 따져야 하는 문제입니다. 또한, 조합의 문제가 아닌, 순열의 문제이지요. </p>
<blockquote>
<p>정확히 말하자면 1~n에서의 순열은 아닙니다. 왜냐하면, 수 사이에 다른 수가 끼어있는 경우가 있기 때문입니다. 
다만 같은 수들이, 다른 곳에 배치되면 서로 독립적인 것으로 인정된다는 관점에서 그냥 순열이라고 가정해봅시다!</p>
</blockquote>
<p>따라서 이 문제를 Brute Force로 풀게 되면 얼마나 걸릴지 생각해봅시다. </p>
<ol>
<li>전체 순열을 구한 다음, </li>
<li>반복문을 돌리고, </li>
<li>또 문자열을 반복해서 <code>count</code>가 일치하는지 찾은 다음</li>
<li>해당되는 배열의 개수를 구해야겠네요.</li>
</ol>
<p>즉 어림 잡아봐도 약 <code>O(N^2 * N!)</code>이 걸린다는 무지막지한 결과가 나오겠군요!
공간복잡도 역시 저만큼은 나올 것 같아서, 수가 커질 수록 매우 부담이 크겠군요.</p>
<p>즉, 어떻게 봐도 최적의 답안이 아니며, 시간 초과 및 런타임 에러가 예상되기 때문에 보자마자 이 문제는 DP로 풀어야겠다고 생각했습니다.</p>
<h3 id="어떻게-배열을-생성할-것인가">어떻게 배열을 생성할 것인가</h3>
<p>저는 이 문제를 보았을 때, 직관적으로 다음과 같이 메모이제이션을 할 배열을 생성했어요.</p>
<blockquote>
<p>어떤 배열을 <code>arr</code>이라 할 때, 
<code>arr[A][B]</code> = <code>arr[n개][현재 count]</code> = <code>현재 n개 중에서 count개의 쌍둥이 빌딩이 나올 수 있는 경우의 수</code></p>
</blockquote>
<p>또한, 점화식을 위한 함수는 다음과 같이 정의하겠습니다.</p>
<blockquote>
<p><code>f(A, B)</code> = <code>현재 n개 중에서 count개의 쌍둥이 빌딩이 나올 수 있는 경우의 수</code></p>
</blockquote>
<p>즉, <code>arr[A][B] = f(A, B)</code>라고 정의하겠습니다.
예를 들면, 문제에서 주어진 예시인 <code>f(3, 1) = 8</code>입니다.</p>
<h2 id="어떻게-점화식을-생성할-것인가">어떻게 점화식을 생성할 것인가</h2>
<h3 id="쌍둥이-빌딩의-정의">쌍둥이 빌딩의 정의</h3>
<p>결과적으로 쌍둥이 빌딩이라는 건, 다음과 같은 조건을 갖고 있어요.</p>
<blockquote>
<p>n = 3일 때, 나올 수 있는 빌딩의 높이를 <code>1,2,3</code>이라고 하겠습니다.</p>
</blockquote>
<ul>
<li>빌딩은 항상 2쌍이 나와야 한다.</li>
<li>높은 빌딩 사이에는 더 높은 빌딩이 위치할 수 없다.</li>
</ul>
<p>언뜻 봐서는 사실 점화식이 떠오르질 않아요.
그래서 더는 좀 더 그려보기로 했습니다. 바로 다음과 같이 말이죠!</p>
<h3 id="문제-접근-1-n--4일-때-쌍둥이-빌딩이-가능한-경우의-수는-어떨까">문제 접근 1) n = 4일 때, 쌍둥이 빌딩이 가능한 경우의 수는 어떨까?</h3>
<p>이를 하나하나 그리고, 답을 도출하기가 굉장히 까다로웠어요.
왜냐하면, 위에서 정의한, <code>높은 빌딩 사이에는 더 높은 빌딩이 위치할 수 없다.</code>는 조건 때문이었어요. </p>
<p>글로 설명하면 이해가 되지  않을 것 같아 예시를 들어 볼게요.
<code>n = 3, count = 2</code>일 때 나올 수 있는 모든 경우의 수는 어느 값에 영향을 미칠까요?</p>
<p>막상 생각만 해도, 다음 3가지 케이스에 영향을 미치게 됩니다.</p>
<ul>
<li><code>O(4,1)</code> - 맨 앞에 높이가 4인 빌딩 쌍이 위치할 때</li>
<li><code>O(4,2)</code> - 맨 앞에 높이가 <code>1 || 2 || 3</code>인 빌딩을 놓고, 뒤에 바로 높이가 4인 빌딩 쌍이 위치할 때</li>
<li><code>O(4,3)</code> - 높이가 4인 빌딩이 맨 뒤에 위치할 때</li>
</ul>
<p>즉, 1개의 케이스가 이전/현재/다음까지 영향을 미치게 되는 점화식을 고른다?
이건 제게 너무 불합리하다고 생각했어요. <code>f(n,c)</code>라는 함수로부터 3개의 다른 함수 <code>f(n+1, c-1)</code>, <code>f(n+1, c)</code> <code>f(n+1, c+1)</code>을 따지는 게 생각만 해도 너무 복잡하달까요? 😖
그래서 약 40분의 고민 끝에 다음과 같은 가설을 세웠어요. 바로 다음과 같이 말이죠.!</p>
<h3 id="문제-접근-2-이전의-식이-다음-식에-영향이-미치는-정도를-최소화하자">문제 접근 2) 이전의 식이, 다음 식에 영향이 미치는 정도를 최소화하자!</h3>
<p>이를 위해 제가 접근한 문제의 접근 원리는 바로 이것입니다.</p>
<blockquote>
<p>높이가 가장 작은 크기를 <code>1</code>이라 할 때,
<strong><code>11</code>이라는 쌍둥이 빌딩을 하나하나의 빌딩 사이로 넣어서 도출해보는 건 어떨까?</strong></p>
</blockquote>
<p>이유는 다음과 같습니다.</p>
<ul>
<li><code>11</code>이 놓이는 경우는 굉장히 단순합니다! <strong>이전의 빌딩 숲 중 아무데나 놓을 수 있기 때문이죠.</strong> 이는 <code>높은 빌딩 사이에는 더 높은 빌딩이 위치할 수 없다.</code>는 조건을 언제든 만족할 수 있습니다.</li>
<li><code>4</code>인 빌딩의 쌍이 놓이는 경우의 수는 <code>44</code> 외에도 <code>41122334</code> 등 위치해야 할 곳을 찾기 까다로운데 반해, <code>11</code>을 찾는 로직으로 바꿔버리면 경우의 수를 찾기 쉽습니다.</li>
</ul>
<p>따라서 만약 이전의 결과값이 <code>332211</code>이었다면, 저는 이를 <code>443322</code>로 해석하고, <code>11</code>을 넣는 방법을 찾게 됐어요. </p>
<blockquote>
<p>ex: <code>11443322</code>, <code>41143322</code>, <code>44113322</code>, <code>44311322</code>, ...</p>
</blockquote>
<p>즉, <strong>어디에다가 더 큰 빌딩을 놓을지</strong>가 아닌, <strong>어디에다가 더 작은 빌딩을 놓을지</strong>로 치환하여 문제를 풀어버린 셈이죠.</p>
<p>결과적으로 표를 볼까요? 다음과 같은 결과가 나옵니다.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/03a88bf6-6f9c-43d6-b541-94b7b430c8d8/image.png" alt=""></p>
<p>어때요. 문제에 달려 있던 제약 조건을 다른 방법으로 단순화해버리니, 이제 표로 깔끔하게 정리할 수 있죠? 다만 아직 직관적으로 점화식이 와닿지 않아요. 이를 엑셀의 색상으로 경우의 수를 정리해볼게요.</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/97f4dfc0-a0c9-4748-b6a8-01de6f8ff557/image.png" alt="">
어?
뭔가 규칙이 보이지 않나요?</p>
<blockquote>
<p>즉, <code>f(n, c)</code>가 영향을 받는 곳은 다음과 같다고 할 수 있어요!</p>
<ul>
<li><code>f(n-1, c-1)</code>인 곳에서 <code>11</code>을 맨 앞에 놓는 경우</li>
<li><code>f(n-1, c)</code>인 곳에서 <code>11</code>을 맨 앞에 놓지 않는 경우</li>
</ul>
</blockquote>
<p>오! 엄청 문제에 대한 접근이 단순화되었어요.
이제 점화식을 생성할 수 있을 것 같아요. 바로 다음과 같이 말이죠!</p>
<pre><code>// f(n, 0)인 경우는 0이라고 해석합니다. 왜냐하면, 그럴 경우는 존재하지 않기 때문이죠.
// 2(n-1): 이전 빌딩의 총 갯수를 의미해요 😉
// f(n, c) = f(n-1, c-1) + f(n-1, c) * 2(n-1)

arr[n][c] = arr[n-1][c-1] + 2 * (n - 1)* arr[n-1][c]</code></pre><p>이제 끝났네요. 최종적으로 문제를 풀어봅시다.</p>
<pre><code class="language-js">const MODULAR_ARITHMETIC_DIVIDE_NUMBER = 1000000007;

function solution(n, count) {
  const arr = Array.from({ length: n + 1 }, () =&gt; new Array(n + 1).fill(0));
  arr[1][1] = 1;

  for (let row = 2; row &lt; n + 1; row += 1) {
    const prevRow = row - 1;

    for (let col = 1; col &lt;= row; col += 1) {
      arr[row][col] =
        (arr[prevRow][col - 1] + 2 * prevRow * arr[prevRow][col]) %
        MODULAR_ARITHMETIC_DIVIDE_NUMBER;
    }
  }
  return arr[n][count];
}</code></pre>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/16b8dee7-5bb0-4e4c-94fe-00d45567dede/image.png" alt=""></p>
<h2 id="최적화">최적화</h2>
<p>야호! 문제를 풀었어요 😆
하지만 저는 찝찝했어요. 결국 DP의 최대 단점은 <strong>쓸 데 없는 값까지 모두 계산</strong>해버린다는 점입니다.</p>
<p>문제를 굉장히 직관적으로 풀어냈지만, 더 효율적으로 풀 수 있을 것 같은 자신감이 들었어요.</p>
<blockquote>
<p>어떻게 하면 문제를 최적화할 수 없을까?</p>
</blockquote>
<p>결과적으로, 다음과 같은 엄청난 꼼수(?)를 발견하게 됩니다.</p>
<h3 id="최적화-접근-잠깐-count가-주어졌을-때-더-큰-값을-계산할-필요가-있을까">최적화 접근: 잠깐! <code>count</code>가 주어졌을 때, 더 큰 값을 계산할 필요가 있을까?</h3>
<p>아까 우리가 도출했던 점화식의 조건이 무엇이었죠?</p>
<blockquote>
<p>즉, <code>f(n, c)</code>가 영향을 받는 곳은 다음과 같다고 할 수 있어요!</p>
<ul>
<li><code>f(n-1, c-1)</code>인 곳에서 <code>11</code>을 맨 앞에 놓는 경우</li>
<li><code>f(n-1, c)</code>인 곳에서 <code>11</code>을 맨 앞에 놓지 않는 경우</li>
</ul>
</blockquote>
<p>결과적으로 이것이 상징하는 것은, <code>f(n-1, c+1)</code>인 경우를 애초부터 고려할 필요가 없었다는 이야기이군요?</p>
<p>즉, 사소한 것일지라도, 어쨌든 계산을 하지 않는다는 것은 희소식이겠군요. 따라서 이를 다시 바꾸었습니다.</p>
<pre><code class="language-js">const MODULAR_ARITHMETIC_DIVIDE_NUMBER = 1000000007;

function solution(n, count) {
  const arr = Array.from({ length: n + 1 }, () =&gt; new Array(count + 1).fill(0));
  arr[1][1] = 1;

  for (let row = 2; row &lt; n + 1; row += 1) {
    const prevRow = row - 1;
    const nextColLength = Math.min(count, row);

    for (let col = 1; col &lt;= nextColLength; col += 1) {
      arr[row][col] = (arr[prevRow][col - 1] + 2 * prevRow * arr[prevRow][col]) % MODULAR_ARITHMETIC_DIVIDE_NUMBER;
    }
  }
  return arr[n][count];
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/68eb6762-08f3-41c8-a711-dd204453ee0d/image.png" alt=""></p>
<h2 id="비교-결과">비교 결과</h2>
<p>대충 <code>n = 10000</code>이고, 절반인 <code>count = 5000</code>의 경우를 비교해볼게요.</p>
<pre><code class="language-js">const MODULAR_ARITHMETIC_DIVIDE_NUMBER = 1000000007;

function before(n, count) {
  const arr = Array.from({ length: n + 1 }, () =&gt; new Array(n + 1).fill(0));
  arr[1][1] = 1;

  for (let row = 2; row &lt; n + 1; row += 1) {
    const prevRow = row - 1;

    for (let col = 1; col &lt;= row; col += 1) {
      arr[row][col] =
        (arr[prevRow][col - 1] + 2 * prevRow * arr[prevRow][col]) %
        MODULAR_ARITHMETIC_DIVIDE_NUMBER;
    }
  }
  return arr[n][count];
}

const beforeStart = +new Date();
before(10000, 5000);
const beforeEnd = +new Date();
console.log(beforeEnd - beforeStart);

function after(n, count) {
  const arr = Array.from({ length: n + 1 }, () =&gt; new Array(count + 1).fill(0));
  arr[1][1] = 1;

  for (let row = 2; row &lt; n + 1; row += 1) {
    const prevRow = row - 1;
    const nextColLength = Math.min(count, row);

    for (let col = 1; col &lt;= nextColLength; col += 1) {
      arr[row][col] = (arr[prevRow][col - 1] + 2 * prevRow * arr[prevRow][col]) % MODULAR_ARITHMETIC_DIVIDE_NUMBER;
    }
  }
  return arr[n][count];
}


const afterStart = +new Date();
after(10000, 5000);
const afterEnd = +new Date();
console.log(afterEnd - afterStart);

console.log((beforeEnd - beforeStart) / (afterEnd - afterStart));</code></pre>
<p>결과는 다음과 같이 차이가 나는군요!</p>
<p><img src="https://velog.velcdn.com/images/young_pallete/post/33cbd840-abf8-447a-a1df-b61764c75683/image.png" alt=""></p>
<p><code>n = 100, count = 100</code>인 경우에는 추가적인 연산으로 인해 효율성이 떨어질 수 있겠으나, 경우의 수가 늘어나고, 주어진 <code>count</code>가 작을 수록 분명 효율성이 더욱 증가될 것입니다.</p>
<p>그 중, <code>count</code>가 절반일 경우에는 약 1.33배의 성능 개선을 이루었습니다!</p>
<blockquote>
<h4 id="정리">정리</h4>
<p>이전의 방법: 최악-최선일 때의 시간 복잡도 <code>O(NlogN)</code> (2번째 반복문이 <code>row</code>만큼 반복됩니다.)
이후의 방법: 최악 - <code>O(NlogN)</code>, 최선 - <code>O(N)</code>(count = 1일 때)</p>
</blockquote>
<h1 id="결론">결론</h1>
<p>만약 직관적인 방법을 좋아하신다면, 최적화 하기 이전의 방법을 추천드려요.
모든 경우의 수가 계산되기 때문에, 다른 사람들에게 설명하기 적합할 것입니다 🙆🏻</p>
<p>하지만 나는 좀 더 효율적으로 문제에 접근하고 싶어!하시는 분들은 이후의 방법을 추천드려요.
이는 모든 경우의 수를 계산하지는 않기에 더 빠른 해답입니다 🙆🏻‍♀️</p>
<h1 id="마치며">마치며</h1>
<p>역시 생각보다 글을 쓴다는 것은 오랜 호흡이 걸리네요.
하지만 정말 글을 쓰고 나니, 제가 이 문제를 제대로 풀었다는 쾌감이 들어서 기분이 좋았어요.</p>
<p>요새 알고리즘 중 <code>DP</code>와 재귀함수에 많은 관심이 생기고 있어요.
<code>chore</code>한 계산 식을 컴퓨팅 사고 기반으로 해결한다는 점이 매력 있는 알고리즘이라는 생각이 들게끔 합니다. 너무 재밌어요 😆</p>
<p>비록 2시간이 걸렸지만... 그래도 오늘 또 한 걸음 성장했다는 뿌듯함에 기분이 좋군요!</p>
<blockquote>
<p>이 글이 누군가에겐 도움이 되었으면 좋겠군요!
<strong>그럼 다들 즐거운 코딩하시길 바라며. 이상 :)</strong></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>