<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>east.log</title>
        <link>https://velog.io/</link>
        <description>앞만 보고 가</description>
        <lastBuildDate>Mon, 09 Feb 2026 07:59:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>east.log</title>
            <url>https://velog.velcdn.com/images/ea_st_ring/profile/d4858ab6-c45e-43fd-9787-e94fed1f5862/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. east.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ea_st_ring" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[LocatorJS For Angular 만들기 - (완)]]></title>
            <link>https://velog.io/@ea_st_ring/LocatorJS-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%99%84</link>
            <guid>https://velog.io/@ea_st_ring/LocatorJS-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%99%84</guid>
            <pubDate>Mon, 09 Feb 2026 07:59:39 GMT</pubDate>
            <description><![CDATA[<p>이전글
<a href="https://velog.io/@ea_st_ring/Locator-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-1">Locator For Angular 만들기 - (1)</a>
<a href="https://velog.io/@ea_st_ring/Locator-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-2">Locator For Angular 만들기 - (2)</a></p>
<p>드디어 라이브러리로 만들었다.
2/4에 첫 배포도 했다. </p>
<p>이거 만들면 세계 70억 앵귤러 개발자가 환영해줄줄? 알았는데 그런건 없더라</p>
<p>HN, GN 등에 홍보글이라도 올려서 그나마 좀 weekly download가 있는듯?
이슈나 피드백이 올라오길 기대했는데 그런 것도 아직 없다..</p>
<p>주변에 앵귤러 개발자가 있다면 추천 및 즐겨찾기, 알림 설정까지~..</p>
<p><a href="https://github.com/Ea-st-ring/ngx-locator">https://github.com/Ea-st-ring/ngx-locator</a></p>
<p>라이브러리로 만들면서 크게 달라진 점은,</p>
<h2 id="selector-기반-탐색에서-angular-공식-api를-활용한-탐색으로-전환">Selector 기반 탐색에서 Angular 공식 API를 활용한 탐색으로 전환</h2>
<h3 id="기존">기존</h3>
<p>컴포넌트 파일의 selector를 읽어 스캔한 파일에서 경로를 찾는 방식</p>
<pre><code class="language-ts">// todo.ts

@Component({
  selector: &#39;app-todo&#39;, // 이걸 읽어서 찾기!
  templateUrl: &#39;./todo.html&#39;,
  styleUrls: [&#39;./todo.scss&#39;],
})
export class TodoComponent {
...
}
</code></pre>
<hr>
<h3 id="지금">지금</h3>
<p><a href="https://angular.dev/api/core/globals/getOwningComponent">ng.getOwningComponent</a>, <a href="https://angular.dev/api/core/globals/getComponent">ng.getComponent</a>와 같은 앵귤러 공식 API를 활용하여 런타임 단계에 컴포넌트에 붙는 클래스네임을 가져오는 방식</p>
<pre><code class="language-ts">// todo.ts

@Component({
  selector: &#39;app-todo&#39;,
  templateUrl: &#39;./todo.html&#39;,
  styleUrls: [&#39;./todo.scss&#39;],
})
export class TodoComponent { // 이걸 읽어서 찾기!
...
}
</code></pre>
<hr>
<h3 id="이점">이점</h3>
<ul>
<li>런타임 인스턴스 기반 매칭이라는 점, 프레임워크가 보장하는 API를 사용한다는 점에서 더욱 안정적.</li>
</ul>
<hr>
<h3 id="한계점">한계점</h3>
<ul>
<li>중복되는 클래스네임에 대해서는, 현재 URL 경로와 파일 경로를 비교해서 점수를 부여하고 가장 관련성이 높은 파일을 선택하는 애매한 로직이 있다.(=정확도가 100%는 아님)</li>
<li>SSR 환경에서 제한된다.</li>
</ul>
<hr>
<h2 id="근황-그리고-앞으로">근황 그리고 앞으로</h2>
<p>출시 5일차..weekly downloads 66인 응애 라이브러리지만~
점점 더 angular 개발자들이 찾아보고 사용하지 않을까? 라는 기대감을 품고 있다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/a533bc34-ab1e-4c9d-b63f-c3a3eb0b96e5/image.png" alt=""></p>
<p>개인적으로는 아직 초반 세팅 과정이 매끄럽지 않은 것 같아서, 관련 flow를 보수할 예정이다.
Maintain하는 라이브러리가 있다는 건 제법 즐거운 일인 것 같다 아직 아무 피드백도 못 받았지만 ! !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSR 디버깅인줄 알았으나 Angular 런타임 이벤트 디버깅]]></title>
            <link>https://velog.io/@ea_st_ring/SSR-%EC%9D%B4%EC%8A%88-%EB%94%94%EB%B2%84%EA%B9%85-%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@ea_st_ring/SSR-%EC%9D%B4%EC%8A%88-%EB%94%94%EB%B2%84%EA%B9%85-%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Wed, 28 Jan 2026 09:32:20 GMT</pubDate>
            <description><![CDATA[<h2 id="발단">발단</h2>
<p>사이트 호스팅을 제공하는 회사(shopify, 카페24, 아임웹 등) 특성상 제공 받은 업체의 사이트를 SSR로 서빙할 때, 적절한 메타 태그와 <a href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data?hl=ko">구조화된 데이터</a>를 삽입하는 것은 중요하다.</p>
<p>SEO 측면은 말할 것도 없고, 제대로 설정해두지 않으면 다음과 같은 문제가 발생할 수 있다. 
우리 회사 이름을 <strong>딩딩</strong>, 호스팅 제공받는 업체 이름을 <strong>탐조월드</strong>라고 가정, 
구글에 탐조월드를 검색했으나, 아래처럼 우리 회사 이름이 뜰 수도 있다..</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/a0fab0e8-d243-4535-bb14-b87f7435002c/image.png" alt=""></p>
<hr>
<h2 id="전개">전개</h2>
<p>그래서 구조화 데이터를 추가하여 이를 보강하는 작업을 해주다 문제가 발생했다.</p>
<pre><code class="language-ts">async start(){
     /************** 새로 추가한 로직 ****************/

     // 사이트 관련 정보를 fetch하고, 구조화 데이터를 삽입한다.
     const {siteName, siteUrl} = await this._retrieveSiteFullUrl();
     this._updateSchemaOrgWebsite({ siteName, siteUrl });

     //--------------------------------------------/

    this.router.events
    .pipe(filter((event) =&gt; event instanceof NavigationEnd))
    .subscribe((event) =&gt; {
       void 메타태그삽입(e.url);
    }
  }

 /**
  * document body에 마이크로데이터(구조화 데이터) 추가하는 로직, 간소화함
  */
  private _updateSchemaOrgWebsite({ siteName, siteUrl }: { siteName: string; siteUrl: string }): void {
      const schemaDiv = ...
      const body = this.renderer.selectRootElement(&#39;body&#39;, true);
      this.renderer.appendChild(body, schemaDiv);
  }</code></pre>
<p>위와 같이 코드를 짜니까, 구조화 데이터는 추가가 되는데 메타 태그 삽입이 안됐다.
처음에는 문제가 뭐지? renderer에 문제가 있나? 싶었는데 아니었다.
문제는 간단한 거였는데..사수님이 알려주시기 전까지 정확히 문제가 뭔지도 몰랐다.</p>
<ul>
<li>renderer -&gt; Angular에서 SSR document를 조작하도록 돕는 API</li>
</ul>
<hr>
<h2 id="절정">절정</h2>
<p>문제는 <code>await this._retrieveSiteFullUrl();</code> 호출 시점에 있었다.</p>
<pre><code class="language-ts">async start(){

      // 여기서 await을 소비하는 동안 NavigationEnd 이벤트가 일어남!
     const {siteName, siteUrl} = await this._retrieveSiteFullUrl();
     this._updateSchemaOrgWebsite({ siteName, siteUrl });

    // NavigationEnd 이벤트가 이미 일어난 이후 구독을 시작해서,
    // 메타 태그 삽입이 이루어지지 않음
    this.router.events
    .pipe(filter((event) =&gt; event instanceof NavigationEnd))
    .subscribe((event) =&gt; {
       void 메타태그삽입(e.url);
    }
  }</code></pre>
<p>정리하면,</p>
<ol>
<li>신나게 <code>await this._retrieveSiteFullUrl()</code> 하는 동안 첫번째 <code>NavigationEnd</code>가 일어났다.</li>
<li>구독 발생 시점 이전에 일어났고, 이미 일어난 <code>NavigationEnd</code> 이벤트를 감지하는 방법도 딱히 없었기 때문에 처음에 메타 태그 삽입 로직이 아예 일어나지 않는다.</li>
<li>그래서 다른 페이지로 navigate하면 그제서야 잘 반영된다.</li>
</ol>
<p>알고나면 되게 기본적이고 간단한 것인데... 문제를 너무 어렵게 본 것 같다.
그럼 해결은 어떻게 할까?</p>
<hr>
<h2 id="결말">결말</h2>
<h3 id="1-router-구독-선언을-항상-start-최상위에-둔다">1. router 구독 선언을 항상 start 최상위에 둔다.</h3>
<pre><code class="language-ts">async start(){
    // 항상 최상위에
    this.router.events
    .pipe(filter((event) =&gt; event instanceof NavigationEnd))
    .subscribe((event) =&gt; {
      void 메타태그삽입(e.url);
    }

    await 어쩌구();
  }</code></pre>
<p>문제점 : 혹여혹여혹여혹여혹여나 subscription 이전에 꼭 실행해야 하는 작업이  있으면 어떡할 것인가?</p>
<h3 id="2-rxjs의-startswith-사용">2. rxjs의 startsWith 사용</h3>
<p>startsWith는 선언 시점에 무조건 값을 하나 내뱉어준다.</p>
<pre><code class="language-ts">async start(){
    await 어쩌구(); // NavigationEnd 발생!

     this.router.events
      .pipe(
        filter((e): e is NavigationEnd =&gt; e instanceof NavigationEnd),
        startWith({ url: this.router.url, urlAfterRedirects: this.router.url } as NavigationEnd),
      // 현재 url로 값을 무조건 뱉으면, 이미 일어난 걸 감지 못하더라도 올바른 url을 획득 가능
      )
      .subscribe((e) =&gt; {
        void 메타태그삽입(e.url);
      });
}</code></pre>
<p>문제점: 처음 발생하는 <code>NavigationEnd</code>를 잘 감지하는 케이스의 경우에는 <code>this.router.url</code>이 비어 있기 때문에 메타 태그에 이상한 값이 삽입될 수 있다. 그러나 바로 <code>NavigationEnd</code>가 일어나니 크게 문제는 없다. 그러나 찜찜해. . . .</p>
<h3 id="3-더-이른-시점에-구독해버리기">3. 더 이른 시점에 구독해버리기</h3>
<pre><code class="language-ts">  private navigationEndReplaySubject = new ReplaySubject&lt;NavigationEnd&gt;(1);
// 1은 버퍼 사이즈, 저장할 이벤트 수

constructor(...) {
    // constructing 시점부터 NavigationEnd 구독하여 저장
    this.router.events
      .pipe(filter((e): e is NavigationEnd =&gt; e instanceof NavigationEnd))
      .subscribe((e) =&gt; this.navigationEndReplaySubject.next(e));
 }

 async start(){
     await 어쩌구();

     // 여기서 구독을 이어가면, 이전 이벤트가 일어나더라도 버퍼에 남아있기 때문에 놓치는 값 없이 수행 가능!
     this.navigationEndReplaySubject.subscribe((e) =&gt; {
         void 메타태그삽입(e.url);
    });
 }</code></pre>
<h3 id="4-그냥-구독하는-코드-내부에서-해버리기">4. 그냥 구독하는 코드 내부에서 해버리기</h3>
<pre><code class="language-ts">async start() {
    this.router.events
    .pipe(filter((event) =&gt; event instanceof NavigationEnd))
    .subscribe(async (event) =&gt; {
      if (!중복) {
        await 어쩌구();
        중복 = true;
      }

      void 메타태그삽입(e.url);
    }
}</code></pre>
<p>방법은 또 더 많겠지만 이것저것 알아본 건 여기까지..</p>
<hr>
<h2 id="느낀-점">느낀 점</h2>
<p>요즘은 AI 코드 리뷰를 하는 일이 잦다보니, 코드를 읽고 생각하는 집중력이 떨어지는 것 같다. 차근히 코드의 흐름을 잘 살펴보자. 늘 기본에 충실히...
도움 많이 된다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Supabase를 활용한 화면 미러링 서비스 개발 - 1 ]]></title>
            <link>https://velog.io/@ea_st_ring/Supabase%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%99%94%EB%A9%B4-%EB%AF%B8%EB%9F%AC%EB%A7%81-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-1</link>
            <guid>https://velog.io/@ea_st_ring/Supabase%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%99%94%EB%A9%B4-%EB%AF%B8%EB%9F%AC%EB%A7%81-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-1</guid>
            <pubDate>Sun, 30 Nov 2025 12:20:03 GMT</pubDate>
            <description><![CDATA[<p>회사에서 미러링 서비스를 도맡아 개발하게 되었다.</p>
<h2 id="미러링-서비스란">미러링 서비스란?</h2>
<p>웹사이트를 편집하면서 편집중인 화면이 모바일에서 어떻게 보이는지 실시간으로 보고 싶을 때 이용 가능한 서비스이다. 예를 들어 피그마 앱에도 해당 기능이 있는데, 디자인하며 실시간으로 스마트폰에서 비춰지는 모습을 확인하는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/c5ab166e-7981-412e-9d5d-c93f90abf7d2/image.png" alt=""></p>
<h2 id="기술-스택-정하기">기술 스택 정하기</h2>
<p>우선 미러링 서비스를 위해서는 앱과 웹 사이 연결이 이루어져야 한다.
Firebase를 이은 차세대 강자 Supabase의 Realtime 서비스를 기존에도 활용하고 있었기 때문에, 이번에도 사용하기로 했다.</p>
<p>필요한 요구사항은, </p>
<ol>
<li>앱과 웹의 미러링 상태 상호 구독</li>
<li>블록*이 변경될 때 정보를 웹 -&gt; 앱에 전달한다.</li>
<li>블록 변경 정보뿐 아니라, 블록 편집 정보를 웹 -&gt; 앱에 전달한다.</li>
</ol>
<p>블록: 웹사이트 편집 간 하나하나의 섹션을 블록이라고 부른다. 헤더, 히어로, 헤더, 상품, 공지사항 등등..
편집 정보: 캐러셀 블록을 편집중이라면 몇 번째 슬라이드를 편집중인지 등</p>
<p>세 가지 방법이 있었다.</p>
<ol>
<li>Realtime DB(블록 변경 상태) + Presence Service(미러링 , 블록 편집 상태)</li>
<li>Presence(미러링, 블록 편집 상태) + Broadcast Service(블록 변경 상태)</li>
<li>RealtimeDB Only</li>
</ol>
<p>•    <a href="https://supabase.com/docs/guides/realtime/broadcast">Broadcast</a>: 클라이언트 간 임의의 메시지를 실시간으로 주고받는 PUB/SUB 브로드캐스트 채널.
•    <a href="https://supabase.com/docs/guides/realtime/postgres-changes">RealtimeDB</a>: Postgres의 테이블 변경사항을 실시간으로 스트리밍
•    <a href="https://supabase.com/docs/guides/realtime/presence">Presence</a>: 같은 채널에 접속한 사용자들의 온라인 상태·참여 정보(존재 상태)를 추적하는 기능, 피그마에서 접속되어있는 사용자 정보를 보는 것과 동일한 개념이다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/0f843fb4-b507-46c9-9120-f2bc9906c69e/image.png" alt=""></p>
<p>편집 정보를 주고 받는데 갑자기 웬 Postgres changes가 나오는지 의아할 수 있겠는데, <strong>정보를 잃어버리지 않기 위한 목적</strong>이었다.</p>
<p>예를 들어 Broadcast나 Presence는 폰이 시간이 지나 잠금 상태가 되거나, 백그라운드 상태가 되면 연결이 끊길 수 있다. 이외에도 와이파이 상태 불량 등 연결이 끊어질 수 있는 많은 케이스가 존재하고, 이는 정보의 불일치를 불러일으키게 된다.</p>
<p>그래서 웹에서 DB에 정보를 넣어두면, 앱에서 정보를 받아오는 식으로 구현하면 이를 해소할 수 있다. Mermaid를 사용하여 그려보면 이런 느낌이다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/9c9d08cd-638f-49d4-88cc-a4a55b61eb3c/image.png" alt=""></p>
<p>그러나 나는 DB를 사용한다는 게 뭔가뭔가였다.
결국 우리가 필요한 것은 블록들의 최종 상태이며, 블록이 변해가는 중간 과정까지 읽어와야 할 필요가 있나? 싶었다.
또한 실시간 미러링 정보 또한 최신 상태가 가장 중요한 것이고, 누가 나갔다 들어왔는지, 미러링을 켜둔 상태인지 아닌지 과거의 기록은 필요하지 않다고 생각했다.
DB 사용 -&gt; 불필요한 오버헤드만 늘어난다고 생각했기 때문에 Presence + Broadcast Service를 사용하는 것을 건의드렸고, 회의실에 모여 모든 요구 사항을 만족하는지 1-2시간 정도 얘기를 나누게 되었다.</p>
<ol>
<li><p>에디터(뷰어)에서 미러링 버튼을 눌렀을때 뷰어(앱)에 메시지가 전달되어야함. 앱은 이때 꺼진 상태였다가 다시 연결될 가능성 있음</p>
</li>
<li><p>에디터에서 페이지 뷰를 바꾸었을때 이 상태가 뷰어에 전달되어야함. 앱은 이때 꺼진 상태였다가 다시 연결될 가능성 있음</p>
</li>
<li><p>에디터에서 특정 블록에 대해 slide를 바꾸었을때 이 상태가 앱에
전달되어야함. 여러개의 블록의 슬라이드가 각각의 값을 가지고 있을 수 있음. 앱은 이때 꺼진 상태였다가 다시 연결될 가능성 있음</p>
</li>
<li><p>뷰어가 먼저 미러링을 눌렀을때, 에디터가 켜진 상태라면 미러링이 시작되어야함</p>
</li>
<li><p>뷰어에서 의도적으로 미러링을 닫았을때(미러링 나가기 버튼), 에디터에서는 이를 인지할 수 있어야함. 반대도 마찬가지.</p>
</li>
<li><p>유저 액션 없이 확실히 한쪽의 커넥션이 끊겼다고 판단될때(앱 강제 종료 등). 일정시간 후 다른 한쪽에서는 이를 인지할 수 있어야함.</p>
</li>
</ol>
<p>말고도 정말 여러 케이스들이 있는데...(웹에서 다중 탭 켜져있는 경우? 앱이 여러 디바이스로 여러 개 켜져있는 경우?) 이건 너무 길어지니 다음 글에 적겠다..</p>
<p>일단은 여러 논리 검증을 통해 <strong>Presence + Broadcast Service로 충분히 가능할 것 같다!</strong> 는 결론을 내렸다.</p>
<p>이후 결정해야하는 것은 Presence에 각각 클라이언트(뷰어, 에디터)는 몇 개의 미러링 상태를 지니고 있어야 하는가?이다.</p>
<pre><code class="language-ts">interface EditorPresenceState {
  mirroring_state: &#39;mirroring&#39; | &#39;idle&#39; | &#39;rejected&#39; ...
}</code></pre>
<p>에디터에서 미러링이 켜져있는 상태이고, 이후에 뷰어가 접속한 상태라면 &quot;미러링을 연결하시겠습니까?&quot; 모달을 띄워야 한다.  아니오를 누르면 그 이후에는 뷰어가 다시 미러링 연결을 누를 때까지 모달이 떠서는 안된다.</p>
<p>뭐 이런 여러 UI 플로우와 앞서 나열한 케이스들에 따라 필요한 상태값 개수가 달라질 수 있다. 과연 내가 내린 결론은 무엇이었을까?</p>
<p>다음 글에 계속 . . .</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LocatorJS For Angular 만들기 - (2)]]></title>
            <link>https://velog.io/@ea_st_ring/Locator-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@ea_st_ring/Locator-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Tue, 30 Sep 2025 09:00:56 GMT</pubDate>
            <description><![CDATA[<p>크롬 확장 프로그램을 가지고 Angular 전용 LocatorJs를 만드는 것이 목표였다.
잘 만들면 부자가 될 수 있을 줄 알았다. 아직은 월급쟁이로 살아야 하나보다.
컴포넌트의 파일 위치를 알아내는데 실패했다..</p>
<p>&quot;런타임에 컴포넌트 Tree만 보고 컴포넌트의 파일 위치를 얻어내기&quot;는 사실상 Angular 자체만으로는 불가능했다. 이러니 아무도 안 만들었지..</p>
<p>정확히는 모든 &quot;앵귤러 프로젝트&quot;로 범위를 설정하니 너무 막히는 부분이 많았다.</p>
<p>이를 사내 프로젝트로만 한정한다면?
80억 앵귤러 개발자들의 찬사를 포기한다면?
먼저 사내용으로 개발 후에, 지금 당장이 아닌 나중에라도 부자가 되기로 마음먹는다면?</p>
<p>LocatorJs For Angular, 그리 불가능해보이지만은 않는다</p>
<h2 id="다시-시작">다시 시작</h2>
<p>크롬 익스텐션은 때려친다. 어차피 우리만 쓸 것이다. (일단은..)
사내 라이브러리로 만들까도 고민해봤지만 어차피 사내 앱 프로젝트는 여기저기 분리되어있지 않고, 메인 APP들이 모노레포로 구성되어 있기 때문에 레포에 직접 구성하는 것이 낫겠다고 판단했다.</p>
<h2 id="파일-위치-알아내기">파일 위치 알아내기</h2>
<p>이게 가장 힘든 점이었는데, 사내 프로젝트에 코드를 작성하게 되면서 파일을 돌며 component map을 하나 생성하는게 제일 간단했다. 
이것저것 다 시도해봐도 이게 가장 정확도가 높았다 (당연히도). </p>
<h3 id="모든-파일을-돌면서-생성하면-너무-오래-걸리지-않을까">모든 파일을 돌면서 생성하면 너무 오래 걸리지 않을까?</h3>
<p>생각보다 얼마 안 걸린다. 추가로 캐싱 최적화를 통해 최초 1회를 제외하고는 빠르게 작업을 수행할 수 있었다. (최초 실행 시. 1-2초, 이후 500ms 내외)
컴포넌트 파일이 천 개가 넘어가도 이 정도니까..적당한 규모의 프로젝트에는 대부분 대응 가능해 보인다.</p>
<h2 id="구조">구조</h2>
<p>전체적인 구조는 다음과 같이 잡아준다.</p>
<p>devtools/
├── cmp.scan.ts      # 컴포넌트 트리 스캔 로직
├── angular-locator.ts    # 클라이언트
├── file-opener.js    # 서버
└── README.md # 설명</p>
<h2 id="cmpscants">cmp.scan.ts</h2>
<p>LocatorJS의 경우 컴포넌트를 클릭하면 해당 컴포넌트 파일(<code>e.g. LoginComponent.ts</code>)로 이동한다. 반면 앵귤러에서는 크게 두 가지 선택지가 있다. </p>
<ul>
<li>html 템플릿 파일</li>
<li>ts 파일</li>
</ul>
<p><code>cmp.scan.ts</code>에는 ts 파일을 스캔하여 해당 파일의 메타데이터를 저장하기 위한 로직이 담겨있으며. 최초 실행 및 변화 감지 시에 실행된다.</p>
<blockquote>
<p>앵귤러는 MVW (Model-View-Whatever) 패턴이라고 보통 불리우고, 우리는 대부분 MVVM 패턴으로 구성되어 있다. 쉽게 말하면 <strong>컴포넌트 별로 HTML 파일 하나, TS 파일 하나씩 알아내면 된다</strong>는 것이다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/0991b46c-22ed-4948-af28-ffb1a1daf694/image.png" alt=""></p>
</blockquote>
<p>코드를 왕창 적어놓으면 별로 읽고 싶어지지 않으니 workflow를 적어보겠따</p>
<h3 id="초기화">초기화</h3>
<ul>
<li><code>open-in-editor.config.json</code> 설정</li>
<li>TypeScript 파일들을 AST로 로드 (<code>ts-morph</code> 이용)</li>
</ul>
<p><code>open-in-editor.config.json</code>에는 다음과 같은 내용을 적어준다. </p>
<ul>
<li>스캔에 포함 / 미포함시킬 디렉토리</li>
<li>파일을 열 IDE (cursor, claude code, webstorm 등)</li>
<li>서버 port</li>
</ul>
<pre><code class="language-ts">// open-in-editor.config.json
{
  &quot;port&quot;: 4123,
  &quot;editor&quot;: &quot;cursor&quot;,
  &quot;fallbackEditor&quot;: &quot;webstorm&quot;,
  &quot;scan&quot;: {
    &quot;includeGlobs&quot;: [&quot;projects/**/*.{ts,tsx}&quot;],
    &quot;excludeGlobs&quot;: [&quot;**/node_modules/**&quot;, &quot;**/dist/**&quot;, &quot;**/.angular/**&quot;]
  }
}</code></pre>
<p><code>ts-morph</code> 라이브러리를 통해 컴포넌트 스캔 시에 구문 트리(Abstract Syntax Tree)를 만들어 클래스, 함수, 인터페이스 같은 걸 코드에서 직접 찾아낼 수 있다. TS 코드 접근 및 조작이 용이해진다는 말.</p>
<p>예시</p>
<pre><code class="language-ts">import { Project } from &#39;ts-morph&#39;;


const project = new Project();

project.addSourceFilesAtPaths(includeGlobs); // 스캔 범위 설정

const sourceFiles = project
  .getSourceFiles()
  .filter(
    (sf) =&gt;
      !excludeGlobs.some((x) =&gt;
        sf.getFilePath().includes(x.replace(&#39;**/&#39;, &#39;&#39;)),
      ), // config 설정에 따라 제외할 파일들은 제외시켜준다
  );</code></pre>
<h3 id="스캔-최적화">스캔 최적화</h3>
<p>컴포넌트 스캔은 최초 프로젝트 실행 시, 그리고 런타임 변경 감지 시에 실행한다.</p>
<p>변경이 감지되었을 때 항상 모두 다시 스캔하면 짜치니까.. <code>node fs</code>에서 제공하는 <code>mtimeMs</code>(마지막 파일 수정 시간)을 가지고 다시 스캔할지 말지를 결정한다. 혹시 파일이 삭제된 경우 에러 케이스도 처리해준다.</p>
<ul>
<li><strong>변경 감지</strong>: 파일 수정시간 비교</li>
<li><strong>조기 종료</strong>: 변경사항 없으면 스캔 건너뛰기</li>
</ul>
<pre><code class="language-ts">function getFileStats(filePaths: string[]): Record&lt;string, number&gt; {
  const stats: Record&lt;string, number&gt; = {};
  for (const filePath of filePaths) {
    try {
      const stat = fs.statSync(filePath);
      stats[filePath] = stat.mtimeMs;
    } catch {
      // 파일이 삭제된 경우
    }
  }
  return stats;
}

const filePaths = sourceFiles.map((sf) =&gt; sf.getFilePath());
const currentStats = getFileStats(filePaths);
const previousCache = loadCache();

// 변경된 파일 확인
const hasChanges = filePaths.some(
  (filePath) =&gt;
    !previousCache[filePath] ||
    previousCache[filePath] !== currentStats[filePath],
);

// 새로운 파일, 삭제된 파일 확인
const cachedPaths = Object.keys(previousCache);
const hasNewOrDeletedFiles =
  filePaths.length !== cachedPaths.length ||
  filePaths.some((path) =&gt; !previousCache[path]) ||
  cachedPaths.some((path) =&gt; !currentStats[path]);

if (!hasChanges &amp;&amp; !hasNewOrDeletedFiles &amp;&amp; fs.existsSync(outFile)) {
  process.exit(0);
}
</code></pre>
<h3 id="컴포넌트-추출-및-결과-생성">컴포넌트 추출 및 결과 생성</h3>
<p>이 뒤로는 별 거 없다. 앵귤러의 <code>@Component</code> 데코레이터를 확인하고, 같이 작성되어 있는 메타데이터(selector, templateUrl, styleUrls) 등을 절대 경로로 변환하여 저장한다.</p>
<p>결과는 <code>component-map.json</code>에 고이 저장해둔다. 이 때 캐시도 같이 업데이트한다.</p>
<p>아래와 같은 메타데이터들이 저장되어, 에디터에서 파일을 열 때 사용하게 된다.</p>
<pre><code class="language-ts">{
  &quot;generatedAt&quot;: &quot;2025-01-15T10:30:00.000Z&quot;,
  &quot;byClass&quot;: {
    &quot;HeaderComponent&quot;: &quot;/abs/path/header.component.ts&quot;,
    &quot;FooterComponent&quot;: &quot;/abs/path/footer.component.ts&quot;
  },
  &quot;bySelector&quot;: {
    &quot;app-header&quot;: &quot;/abs/path/header.component.ts&quot;,
    &quot;app-footer&quot;: &quot;/abs/path/footer.component.ts&quot;
  },
  &quot;detail&quot;: {
    &quot;HeaderComponent&quot;: {
      &quot;className&quot;: &quot;HeaderComponent&quot;, // 클라이언트에 노출할 정보
      &quot;selector&quot;: &quot;app-header&quot;, // 이거 가지고 찾을 거다
      &quot;filePath&quot;: &quot;/abs/path/header.component.ts&quot;,
      &quot;templateUrl&quot;: &quot;/abs/path/header.component.html&quot;,
      &quot;styleUrls&quot;: [&quot;/abs/path/header.component.scss&quot;],
      &quot;isStandalone&quot;: false
    }
  }
}</code></pre>
<h2 id="file-openerjs">file-opener.js</h2>
<p>클라이언트와 에디터를 연결하는 브릿지 역할 express 서버다.
클라이언트에서 http 요청을 날리면, 해당 요청에 맞게 에디터에서 파일을 연다.</p>
<p>에디터 설치 여부 등을 확인하고, 클라이언트에서 요청한 파일을 여는 로직이 작성되어 있다. 파일 열기만 하면 아쉬우니까 html 템플릿 파일을 여는 경우에 해당 요소가 있는 line을 탐색, 해당 라인을 열어주도록 로직을 추가했다.</p>
<p>어차피 regex가 대부분이라 구체적인 로직은 생략.</p>
<pre><code class="language-ts">function findBestLineInFile(filePath, searchTerms) {
  try {
    const content = fs.readFileSync(filePath, &quot;utf8&quot;);
    const lines = content.split(&quot;\n&quot;);
    const scores = new Array(lines.length).fill(0);

    // 각 검색어에 대해 점수 계산
    searchTerms.forEach((term, termIndex) =&gt; {
      const weight = Math.max(1, searchTerms.length - termIndex); // 앞쪽 검색어에 더 높은 가중치 부여

      lines.forEach((line, lineIndex) =&gt; {
        const lowerLine = line.toLowerCase();
        const lowerTerm = term.toLowerCase();

        if (lowerLine.includes(lowerTerm)) {
          // term에 대해 exact 매칭,단어 경계 매칭, 시작 부분 매칭을 통해 가중치 부여
          // searchTerm이 &quot;header&quot;일때 
          // &quot;header&quot;가 포함된 줄에 가장 높은 점수, 
          // &quot;my-header&quot;와 같은 경우 그 다음으로 높은 점수...
        }
      });
    });

    // 가장 높은 점수의 라인 찾아 반환
    let bestLine = 1;
    let bestScore = 0;
    scores.forEach((score, index) =&gt; {
      if (score &gt; bestScore) {
        bestScore = score;
        bestLine = index + 1; // 1-based
      }
    });

    return bestScore &gt; 0 ? bestLine : 1; // 없으면 첫 줄에서 열기
  } catch (e) {
    console.warn(`[file-opener] Failed to search in file: ${e.message}`);
    return 1;
  }
}</code></pre>
<h2 id="angular-locatorts">angular-locator.ts</h2>
<p>컴포넌트를 찾아 <code>file-opener.js</code>에 에디터를 열어달라고 요청을 보낸다.
제공되는 옵션은 아래 세 가지다.</p>
<ul>
<li>Alt + hover (컴포넌트 이름 노출)</li>
<li>Alt + Click (html 파일 열기 요청)</li>
<li>Alt + Shift + Click (ts 파일 열기 요청)</li>
</ul>
<p>다음과 같은 로직들이 작성되어 있다.</p>
<ol>
<li><code>file-opener.js</code> 서버에서 컴포넌트 맵 미리 받아오기</li>
<li>EventListner 등록 + 각 로직 작성 (호버 시 효과, 클릭 시 피드백 등)</li>
<li>요소 클릭 시 컴포넌트 selector를 찾아 <code>file-opener.js</code>에 http 요청 </li>
</ol>
<blockquote>
<p>Angular 컴포넌트는 <code>main-header</code> 와 같이 &quot;-&quot; 가 포함되어 있는 선택자 패턴을 사용한다. selector가 <code>main-header</code>면 컴포넌트 트리에도 <code>&lt;main-header&gt;</code> 와 같이 노출되어 있다. 이걸 가지고 앵귤러 컴포넌트인지 아닌지를 알아낸다. </p>
</blockquote>
<ul>
<li><code>&lt;div&gt;, &lt;h1&gt;, ...</code> -&gt; x </li>
<li><code>&lt;main-header&gt;, &lt;my-app&gt;</code> -&gt; o </li>
</ul>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/b88ea1a1-08f0-4888-921a-1bde89cb8146/image.png" alt=""></p>
<h2 id="실행">실행</h2>
<p><code>nodemon</code> 라이브러리를 설치, 아래와 같이 package.json에 script를 추가해준다.</p>
<pre><code class="language-ts">&quot;cmp:scan&quot;: &quot;ts-node -P tsconfig.tools.json devtools/cmp.scan.ts&quot;,
&quot;cmp:watch&quot;: &quot;nodemon --delay 2.5 -e ts,html -w projects --ignore &#39;**/node_modules/**&#39; --ignore &#39;**/dist/**&#39; --ignore &#39;**/*.spec.ts&#39; --ignore &#39;**/*.test.ts&#39; -x \&quot;npm run cmp:scan\&quot;&quot;,
&quot;dev:opener&quot;: &quot;node devtools/file-opener.js&quot;,</code></pre>
<p><code>cmp:watch + cmp:scan</code> </p>
<ul>
<li>루트 폴더에서 .ts, .html 파일 변경이 일어나는지 추적하고, 변경이 있으면<code>cmp.scan.ts</code>를 실행</li>
</ul>
<p><code>dev:opener</code> </p>
<ul>
<li>파일 열기 서버를 실행시킨다. 우리 프로젝트에서는 4123 포트에서 실행되도록 했다. </li>
</ul>
<blockquote>
<p>개발 서버와 포트 번호가 다르기 때문에 CORS 에러가 발생할 수 있다.
개발 서버를 열 때 다음과 같이 proxy 설정도 추가해줬다.</p>
</blockquote>
<pre><code class="language-ts">// proxy.config.json
{
  &quot;/__open-in-editor&quot;: {
    &quot;target&quot;: &quot;http://localhost:4123&quot;,
    &quot;secure&quot;: false,
    &quot;changeOrigin&quot;: true
  },
  &quot;/__open-in-editor-search&quot;: {
    &quot;target&quot;: &quot;http://localhost:4123&quot;,
    &quot;secure&quot;: false,
    &quot;changeOrigin&quot;: true
  },
  &quot;/__cmp-map&quot;: {
    &quot;target&quot;: &quot;http://localhost:4123&quot;,
    &quot;secure&quot;: false,
    &quot;changeOrigin&quot;: true
  }
}
</code></pre>
<p>전체 workflow는 아무튼 다음과 같다. (고맙다 커서야..)
<img src="https://velog.velcdn.com/images/ea_st_ring/post/08ce1286-f7df-41a6-8644-4eee25291af1/image.png" alt=""></p>
<h2 id="테스트">테스트</h2>
<ul>
<li>IDE: Cursor
<img src="https://velog.velcdn.com/images/ea_st_ring/post/597c671e-e0b2-41fa-a5a1-ceef8937c626/image.gif" alt=""></li>
</ul>
<ul>
<li>IDE: WebStorm 
<img src="https://velog.velcdn.com/images/ea_st_ring/post/9ad03cef-19f3-4ac8-903c-2a0897ee82cc/image.gif" alt=""></li>
</ul>
<p>가끔 Line matching이 제대로 이루어지지 않아 그냥 컴포넌트의 첫줄로 갈 때도 있지만, 대체로 잘 작동한다.</p>
<p>하면서 느낀 점은 충분히 라이브러리화까지는 가능할 것 같다. 그러나 유저가 수행해야할 작업이 좀 있다는게 조금 불편한 것 같다.(config 파일 작성, 프로젝트 root 지정, script 추가 등등..) 그래도 쓸 사람이 있으려나</p>
<p>다음에는 라이브러리로 만들어서 npm 배포를 한 번 해보자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI Image Prompting - (1) / 
모델 선정,  프롬프팅,  도움 사이트]]></title>
            <link>https://velog.io/@ea_st_ring/AI-Image-Tips-Replicate-Langgraph-%ED%94%84%EB%A1%AC%ED%94%84%ED%8C%85-%EB%AA%A8%EB%8D%B8-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@ea_st_ring/AI-Image-Tips-Replicate-Langgraph-%ED%94%84%EB%A1%AC%ED%94%84%ED%8C%85-%EB%AA%A8%EB%8D%B8-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Tue, 30 Sep 2025 03:23:59 GMT</pubDate>
            <description><![CDATA[<p>이번에 회사에서 AI 이미지 생성 및 편집 프롬프팅 + LangGraph 구축 등의 작업을 맡게 되었다. 이미지 프롬프팅은 모델 별로도 가이드가 다르고, 이렇다 할 답이 없는 문제이다보니 오랜 시간과 비용을 들여 최적의 결과를 찾아나가야만 했다.
많은 Trial 동안 얻게 된 노하우들을 정리해보자.</p>
<h1 id="모델-선정">모델 선정</h1>
<p>우선 AI 이미지로 무엇을 할 지에 따라 어떤 모델을 사용할지 달라진다. 무작정 인기 많은 모델만 사용할 게 아니라, 요구사항과 상황에 맞는 모델을 잘 선택해야 한다.
나같은 경우에는 아래와 같은 작업들이 필요했다.</p>
<ul>
<li>Text to Text Generation</li>
<li>Text to Image Generation</li>
<li>Image background edit</li>
</ul>
<h2 id="선정-기준">선정 기준</h2>
<p>모델을 고를 때는 아래와 같은 사항들이 중요하다.</p>
<ul>
<li>기능 요구사항을 만족하는지 </li>
<li>비용이 적절한지</li>
<li>속도가 너무 느리진 않은지</li>
</ul>
<p>위 사항들을 쉽게 탐색, 비교해보기 위해 
아래와 같은 사이트들을 이용해볼 수 있다.</p>
<h2 id="도움이-되는-사이트">도움이 되는 사이트</h2>
<h3 id="imarena">Imarena</h3>
<p>익명의 AI 모델 두 개를 가지고 결과물을 만들어보고, 잘 만들어진 쪽에 투표한다.
어떤 모델이 많은 득표를 했는지 확인할 수 있다.</p>
<blockquote>
<ul>
<li><a href="https://lmarena.ai/">https://lmarena.ai/</a> </li>
</ul>
</blockquote>
<p>이를 참고하여 테스트해볼 모델들을 결정 / 탐색해볼 수 있다.
많은 득표 수를 받은 모델들도 확인 가능하다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/b11ce847-f1d9-4d3e-9fc2-1d497965e0ab/image.png" alt=""></p>
<h3 id="replicate">Replicate</h3>
<blockquote>
<p><a href="https://replicate.com/">https://replicate.com/</a></p>
</blockquote>
<p>강력 추천한다. 모델 탐색하기에 굉장히 편하고, replicate 자체 토큰을 충전하여 여러 모델을 직접 실행하며 비교할 수 있다. 인기 있는 모델들이 많이 준비되어 있으니 여기서 한 번 찾아보고, 마음에 드는 게 없으면 그 때 다른 모델들을 탐색해보면 된다.</p>
<p>모델 탐색
<img src="https://velog.velcdn.com/images/ea_st_ring/post/a78530a2-6943-49eb-b6b8-8ee24a47a713/image.png" alt=""></p>
<p>Playground 제공
<img src="https://velog.velcdn.com/images/ea_st_ring/post/d12c4d4f-d9dc-48f1-aa06-c48145a7a2f3/image.png" alt=""></p>
<h3 id="huggingface">HuggingFace</h3>
<blockquote>
<p><a href="https://huggingface.co/">https://huggingface.co/</a></p>
</blockquote>
<p>AI 모델계의 깃허브라고 불리는 허깅페이스다. 모델 탐색하기에도 괜찮은 것 같고(엄청 많다), 모델을 직접 훈련시킬 생각이 있다면 한 번 살펴보면 좋겠다. 물론 Replicate에서도 훈련 기능이 있으니 더 좋아보이는 쪽을 고르면 되겠다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/0f0f723d-77bc-4e38-a62d-85586f5c9491/image.png" alt=""></p>
<h2 id="내가-선택한-모델">내가 선택한 모델</h2>
<p>여러 모델을 테스트해보면서 모델별 느낀 점들을 정리할까 했는데, 어차피 1달만 지나도 새로운 모델들이 쏟아져 나와서 지금 적어놔도 크게 의미가 없을 확률이 높다고 생각한다. 그냥 어떤 모델을, 왜 골랐는지에 대해서만 적겠다.</p>
<h3 id="text-model">Text Model</h3>
<p>이미지 생성 프롬프트를 생성(?)하기 위해 Text 모델이 필요했다.
User Input을 받아서, 이를 이미지 프롬프트로 확장시키는 개념을 택했는데,
예를 들어 유저가 아래의 값들을 입력했다면</p>
<pre><code class="language-ts">// Input
{
  domain: &quot;커피샵&quot;,
  color: &quot;오트밀&quot;,
  mood: &quot;모던 &amp; 미니멀&quot;,
}</code></pre>
<pre><code class="language-ts">// Output
&quot;테이블 위에 모던 &amp; 미니멀한 커피 머신이 배치되어있고, 
앞에는 컵이 오트밀 색상의 행주 위에 놓여있다. ~~~...&quot;</code></pre>
<p>이에 걸맞는 이미지 프롬프트를 뱉도록 하는 것이다. (물론 다 영어)
요구사항에 걸맞는 프롬프트를 안정적으로 도출하기 위해 아래 Text To Image 모델과 함께 테스트를 하며 <a href="https://www.promptingguide.ai/kr/techniques/fewshot">few-shot</a>을 확보해두면 좋다.</p>
<p>가장 무난한 <code>gpt-4.1</code> 모델과 <code>gpt-5.0</code> 중 4.1을 골랐다. 
비용과 성능은 비슷하지만 5.0은 너무 시간이 오래 걸리는게 문제였다.</p>
<p>더욱 간단한 작업에는 <code>gpt-4.1-nano</code>도 추천한다. nano인만큼 가격이 저렴하고 속도가 빠르다. 나 같은 경우에는 예를 들어 잘못된 input이 들어왔을 때 검증하는 용도 등으로 사용했다.</p>
<h3 id="text-to-image-generation">Text to Image Generation</h3>
<p>수많은 후보들을 뚫고 구글의 <code>Imagen4</code> 모델을 선택했다.
유력한 상대는 <code>flux-context-pro</code> 모델이었는데, Imagen4가 더 빨랐고, 퀄리티도 우세했다. (비용은 동일)</p>
<p>사실 요즘 핫한 nano-banana로 다시 테스트해본다면 결과가 어찌될 지 모르겠다</p>
<p>아래는 위 Input을 가지고 생성한 결과이다. 
Hero 섹션에 쓰일 이미지로 이 정도면 제법 괜찮아 보인다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/1528f77f-3bbf-45fd-a0ca-030b930ef3c5/image.png" alt=""></p>
<h3 id="image-background-edit">Image Background edit</h3>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/9c21862a-aaab-4956-a700-9eb1b9c938d5/image.png" alt=""></p>
<p>위와 같이 누끼가 따져 있는 특정 Object 이미지를 유저가 갖고 있다고 가정해보자.</p>
<p>이 이미지를 가지고 AI에 부탁하여 단색 혹은 나무 테이블, 부엌 등 다채로운 배경으로 바꾸고 싶다.</p>
<p>이 때 중요한 것은</p>
<ul>
<li>원본 이미지가 깨지지 않을 것 (Object에 텍스트가 있는 경우 더욱 민감)</li>
<li>자연스러운 배경과 그림자를 생성할 것</li>
</ul>
<p>이런 점들이 있다.</p>
<p>이외에도 요구사항에 맞게 여러 가지를 고려했을 때 가장 적합한 모델은
Photoroom에서 제공하는 API였다.</p>
<blockquote>
<p><a href="https://www.photoroom.com/api">https://www.photoroom.com/api</a></p>
</blockquote>
<blockquote>
<p>bria에서 제공하는 api도 쓸만하다.
<a href="https://bria.ai/ai-product-shot-editing">https://bria.ai/ai-product-shot-editing</a></p>
</blockquote>
<p>욕실 세면대를 요청했을 때 만들어준 이미지. 
글자도 깨지지 않고, 배경도 적절하다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/54cfd32f-94a5-4450-961c-828435525a42/image.png" alt=""></p>
<h1 id="프롬프트-작성">프롬프트 작성</h1>
<h3 id="1-부정보다는-묘사를-사용하기">1. 부정보다는 묘사를 사용하기</h3>
<p>내가 스니커즈를 만들어달라고 했더니 나이키 로고가 떡하니 박힌 이미지를 만들어준다. 이를 방지하기 위해 다음과 같이 추가했다고 하자.</p>
<blockquote>
<p>Must not Include visual elements like logo, text on the product.</p>
</blockquote>
<p>&quot;코끼리를 생각하지 말라&quot;고 하면 사람은 코끼리를 머릿속에 떠올리게 된다.
AI도 똑같은 것 같다. 로고를 대문짝만하게 박아서 이미지를 만들어줬다.</p>
<p>&quot;~~를 하지마라&quot; 와 같은 프롬프트가 먹힐 때도 있지만, 그게 잘 안될 때는 다음과 같이 해보자.</p>
<blockquote>
<p>Product with clean and simple surface, ~~</p>
</blockquote>
<p>&quot;로고를 만들지마!&quot; 보다는 &quot;깨끗하고 간결한 표면을 만들어줘.&quot; 라고 요청하는 것이 더욱 잘 먹힐 때가 많다. 굉장히 중요한 점이니 명심..</p>
<h3 id="2-단어-대체">2. 단어 대체</h3>
<p>이미지의 배경을 천 질감으로 바꾸고 싶다. 간단히 입력해본다.</p>
<blockquote>
<p>Background is a plain fabric surface in ivory color...</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/0e46c7c3-9e3d-43d4-a33d-a97b5d2d061c/image.png" alt=""></p>
<p>천이 물론 잘 생성되긴 했는데, 뭔가 좀 아쉽다. 미사여구를 여럿 붙여봐도 보통 결과는 비슷하다. 단어에 따른 이미지 훈련 셋이 정해져 있어 결과가 크게 달라지지 않는다. 
이럴 때 &quot;fabric&quot; 이라는 단어를 유사 단어로 대체해보는 시도가 도움이 될 때가 
많다.
예를 들어 &quot;천&quot;을 &quot;바삭한 침대 시트&quot;로 바꾸게 되면 결과는 다음과 같이 달라진다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/b6a60871-761e-4445-ae87-ca74e0378f4f/image.png" alt=""></p>
<p>미묘한 차이로 보일 수도 있지만 이 차이를 조금씩 줄여나가는게 이미지 프롬프팅의 핵심이다. 원하는 느낌이 나지 않을 때는 단어 대체를 통해 새로운 결과를 만들어보자.</p>
<h3 id="3-좋은-프롬프트를-작성하기-위해-api-제공처의-공식-홈페이지-예시나-documentation-파라미터-등을-다-뒤져보자">3. 좋은 프롬프트를 작성하기 위해 API 제공처의 공식 홈페이지 예시나, Documentation, 파라미터 등을 다 뒤져보자.</h3>
<p>Imagen 홈페이지를 들어가보면 예시 이미지와 함께 프롬프트가 나열되어 있다.
프롬프트들이 어떤 체로 작성되어 있는지 확인하고, GPT에게 예시로 주어 이런 식으로 적어 라는 느낌을 학습시키는 것도 퀄리티를 높이는 데 도움이 된다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/0ff8d01a-d897-4412-9d36-4704601b9486/image.png" alt=""></p>
<p>Photoroom API 같은 경우에는 좋은 프롬프트를 작성하는 팁 등이 꼭꼭 숨겨져 있다.</p>
<blockquote>
<p><a href="https://docs.photoroom.com/image-editing-api-plus-plan/ai-backgrounds#how-to-write-a-high-quality-prompt">https://docs.photoroom.com/image-editing-api-plus-plan/ai-backgrounds#how-to-write-a-high-quality-prompt</a></p>
</blockquote>
<h3 id="4-일관성-유지하기">4. 일관성 유지하기</h3>
<p>이미지 생성에서 중요한 점은 일관성을 유지하는 것인데, 훈련을 시킬 수 있다면 좋겠지만 그렇지 못한 경우 다음과 같은 방법들을 고려해볼 수 있다.</p>
<ul>
<li>few-shot 제공 </li>
<li>템플릿형 프롬프트 구성 (GPT에게 프롬프트를 일정한 템플릿에 맞춰 생성시키도록 하기)</li>
<li>모델에 일관성 혹은 가이드를 주는 <code>seed</code>, <code>guidance_image</code> 등의 파라미터 존재 여부 확인</li>
</ul>
<h3 id="5-테스팅-환경-구성">5. 테스팅 환경 구성</h3>
<p>Replicate에서 테스트를 하다보면 여러 모델을 한꺼번에 비교하거나 여러 이미지를 넣어 동시에 결과를 보고 싶을 때가 있다. 그럴 때는 그냥 AI에게 부탁해서 나만의 테스트 Playground를 구성해보자. 정말 많은 테스트를 해보면서 최적의 파라미터를 찾아나가는 과정이기 때문에 쾌적한 테스트 환경은 중요하다.</p>
<p>아래는 Cursor로 구성한 테스트 환경이다. 이거 쓰고 DX 800% 향상했다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/57e733f3-4944-417d-999e-46ad33039a36/image.png" alt=""></p>
<p>프롬프트를 잘 작성했다면 이제 LangGraph를 이용하여 코드를 작성할 차례다.
너무 길어져서 이건 다음 편에. . .</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Locator For Angular 만들기 - (1)]]></title>
            <link>https://velog.io/@ea_st_ring/Locator-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@ea_st_ring/Locator-For-Angular-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Tue, 29 Jul 2025 11:53:53 GMT</pubDate>
            <description><![CDATA[<h3 id="0-만들어보자">0. 만들어보자</h3>
<p>리액트 개발을 하면서 굉장히 편리하게 사용했던 크롬 확장 프로그램 <a href="https://www.locatorjs.com/">LocatorJS</a>...</p>
<p>아쉽게도 해당 익스텐션은 Angular에서 지원되지 않는다.
Angular 개발을 시작한지도 벌써 두 달, LocatorJS가 있으면 DX가 크게 향상될 것 같다는 생각이 들었다.</p>
<p>우선 LocatorJS에서 Angular를 지원할 예정이 있을까?
<img src="https://velog.velcdn.com/images/ea_st_ring/post/0862220f-f424-49a5-8438-a649cd0d4a7e/image.png" alt=""></p>
<p>이슈가 올라온지도 약 3년이 흘렀다.. 지원을 기대하기는 쉽지 않아보였다.</p>
<p><strong>그러면 직접 한 번 만들어보자.</strong> 잘 만들기만 하면...</p>
<ol>
<li>DX가 크게 향상된다.</li>
<li>전세계 8억 Angular 개발자에게 찬양 받을 수 있다.</li>
<li>잘하면 억만장자가 되어 개발은 취미로 하게 될지도 모른다...ㄷㄷ</li>
</ol>
<p>크롬 익스텐션 개발을 해본 적은 없지만, 대부분의 크롬 익스텐션이 JS로 개발되어 있는 것을 보면 나도 도전해볼만하다고 생각했다. (해보니 진짜 별 거 없었다)</p>
<p>우선 필요한 기능부터 정리해보자면</p>
<h3 id="1-angular-감지-🔍">1. Angular 감지 🔍</h3>
<p>React, Preact, Vue, Svelte는 이미 LocatorJS에서 지원 중이기 때문에 나는 Angular-Only-Target으로 진행한다. (한국에선 우리 팀말고 아무도 안쓰겟군.)</p>
<p>개발 환경이 Angular로 구성되어 있는지 아닌지 알기 위한 방법은 여러가지가 있다.</p>
<ul>
<li><code>window</code> 객체에 등록된 Angular 관련 요소들(Zone, ng 등) 존재 여부</li>
<li>HTML Element 중 <code>ng</code> prefix의 존재 여부</li>
<li>HTML root 요소 확인
<img src="https://velog.velcdn.com/images/ea_st_ring/post/ffd0567d-abdd-47f6-a128-513b553840c5/image.png" alt=""></li>
</ul>
<p>위 요소들을 확인하여 Angular Project가 존재하는지 여부를 판단하여 지원 여부를 알려줄 수 있다.</p>
<h3 id="2-web-inspector-🎯">2. Web Inspector 🎯</h3>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/fd2bbb03-5088-48a8-b005-a84be6877d37/image.png" alt=""></p>
<p>LocatorJS와 같이 command 키를 누르고 마우스를 올렸을 때 해당 요소를 개발자 도구의 Inspector와 같이 HTML 요소를 콕 집어야한다. 이 기능도 정말 여러가지 구현 방법이 있다.</p>
<ol>
<li>Angular에서 지원하는 Angular DevTools API 사용 - o<pre><code class="language-ts">const component = window.ng.getComponent(element);</code></pre>
</li>
<li>컴포넌트 선택자 기반 탐지 - x</li>
</ol>
<p>Angular의 컴포넌트 선택자 생성 방식(<code>ex.app-user-card -&gt; UserCardComponent</code>)으로 알아내는 것인데, 선택자를 바꾸는 예외 케이스가 굉장히 많으므로 해당 방법은 적절치 않다.</p>
<ol start="3">
<li><p>ng.probe API 사용 - o</p>
<pre><code class="language-ts">const debugElement = window.ng.probe(element);
const component = debugElement.componentInstance;
const componentName = component.constructor?.name</code></pre>
</li>
<li><p>전역 컴포넌트 검색 - △</p>
<pre><code class="language-ts">const globalComponent = findNearestAngularComponent(element);</code></pre>
</li>
</ol>
<p>말 그대로 다 돌며 찾는 것이다. 이건 최악의 성능을 지니므로 최후의 수단이다..</p>
<p>위 방법들 중 1번과 3번이 그나마 쓸만한 것 같은데, 아직 무엇이 더 적절할 지는 모르겠다. 2편에서 계속 ...</p>
<h3 id="3-file-path-알아내기-☠️">3. File Path 알아내기 ☠️</h3>
<p>이게 좀 어려운 부분이다. 컴포넌트 이름 등 Angular의 컴포넌트 정보를 알아냈다 한들 이 컴포넌트가 정확히 어떤 폴더에 어떤 파일명으로 저장되어 있을지 알아내기가 쉽지 않은 것 같다.
현재 여러 방법을 통해 구현해보는 중인데 정확도가 높지 않다. stable한 방법을 찾으면 따로 글로 정리해보려 한다.</p>
<h3 id="4-에디터-열기-🧑🏿💻">4. 에디터 열기 🧑🏿‍💻</h3>
<p>해당 기능은 chrome에서 제공하는 API를 다음과 사용하면 된다.
자세한 레퍼런스는 <a href="https://developer.chrome.com/docs/extensions/reference/api/runtime?hl=ko#method-sendMessage">여기</a>를 참고</p>
<pre><code class="language-ts">    chrome.runtime.sendMessage({
        action: &#39;openFile&#39;,
        componentInfo: componentInfo
    }, (response) =&gt; {
        if (response &amp;&amp; response.success) {
            showNotification(&#39;파일이 열렸습니다!&#39;, &#39;success&#39;);
        } else {
            showNotification(&#39;파일 열기에 실패했습니다&#39;, &#39;error&#39;);
        }
    });</code></pre>
<h3 id="5-chrome-web-extension-개발-환경-구성-📦">5. Chrome Web Extension 개발 환경 구성 📦</h3>
<p>크롬 익스텐션에 등록하기 위해 <code>manifest.json</code> 파일을 루트 디렉토리에 작성해주면 된다.</p>
<blockquote>
<p><a href="https://developer.chrome.com/docs/extensions/reference/manifest?hl=ko">https://developer.chrome.com/docs/extensions/reference/manifest?hl=ko</a> </p>
</blockquote>
<p>위 링크에서 필요한 내용을 참고하여 <code>manifest.json</code> 파일을 작성한다.</p>
<blockquote>
<p>chrome://extensions/</p>
</blockquote>
<p>위 사이트에 접속하여 개발자 모드를 켜고, 프로젝트 디렉토리를 등록하면 직접 내가 만든 익스텐션을 사용해볼 수 있다.</p>
<h3 id="-1-앞으로">-1. 앞으로...</h3>
<p>아직 개발중이어서 잘 동작하지는 않지만... 우선 기반 구축은 완료했다.</p>
<p>배포할 수 있을 정도의 퀄리티로 만들 수 있을지는 모르지만, DX 향상을 위해 자발적으로 무언가를 만들어보는 경험이 처음이라 도키도키하다. 잘 만들고 대박 나서 개발은 취미로 할 수 있기를. . .</p>
<p>[개발 테스트 화면] <img src="https://velog.velcdn.com/images/ea_st_ring/post/b8b8fd12-8f9d-4b5a-b332-0eff8e12dd53/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드레슨좋은건너만알기]]></title>
            <link>https://velog.io/@ea_st_ring/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EB%A0%88%EC%8A%A8%EC%A2%8B%EC%9D%80%EA%B1%B4%EB%84%88%EB%A7%8C%EC%95%8C%EA%B8%B0</link>
            <guid>https://velog.io/@ea_st_ring/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EB%A0%88%EC%8A%A8%EC%A2%8B%EC%9D%80%EA%B1%B4%EB%84%88%EB%A7%8C%EC%95%8C%EA%B8%B0</guid>
            <pubDate>Thu, 24 Jul 2025 09:31:03 GMT</pubDate>
            <description><![CDATA[<p>평소 쓰는 개발자 도구 기능, 익스텐션 위주로 그냥 잡다하게 정리하려고 한다.
서론 끝</p>
<h2 id="☝️이건-첫번째-레슨-크롬-익스텐션">☝️이건 첫번째 레슨~ 크롬 익스텐션</h2>
<h3 id="pixelparallel">PixelParallel</h3>
<blockquote>
<p>디자이너: &quot;피그마랑 디자인이 미세하게 안 맞아요~&quot;
나: &quot;넹 ㅠ_ㅠ (어느 세월에  디자인 확인하고 값 체크하고 수정하지 귀차나 ㅠ ㅠ)&quot;</p>
</blockquote>
<p>이 때 <a href="https://chromewebstore.google.com/detail/pixelparallel-by-htmlburg/iffnoibnepbcloaaagchjonfplimpkob?hl=en">PixelParallel</a> 익스텐션을 써보자
쉽게 설명하면 웹에 PNG 띄워서 비교할 수 있는 익스텐션이다.</p>
<p><strong>사용법</strong></p>
<ol>
<li>익스텐션을 설치한다.</li>
<li>피그마에서 비교해보고 싶은 화면 혹은 요소를 export한다.</li>
<li>익스텐션을 키고 UPLOAD IMAGE를 눌러 export한 파일을 선택한다.</li>
<li>그럼 반투명한 요소가 떠서 실제 내가 만든 화면과 비교해보며 잘못 스타일링한 값이 있는지 편히 확인할 수 있다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/87663644-cafb-4c7e-9cb1-3a188c4f1fa2/image.png" alt=""></li>
</ol>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/d845ddd2-55b4-4d63-abae-2948491ffbaa/image.png" alt="">
앗 !!! 폰트 크기가 안맞는다 !!! 🤦‍♂️</p>
<h3 id="locatorjs">LocatorJS</h3>
<p>개발하다보면 이 컴포넌트 위치가 어디지?! 할 때가 있다.
이 때 <a href="https://www.locatorjs.com/">LocatorJS</a> 익스텐션을 써보자
<img src="https://velog.velcdn.com/images/ea_st_ring/post/d8a7a5e8-5109-435e-8680-d46ceaae5211/image.png" alt=""></p>
<p>활성화 후 클릭 한 번이면 Cursor Vscode 등 작업 환경에 해당 컴포넌트가 있는 파일로 슝~ 이동시켜준다.
이거 알고나서 개발 속도 800% 향상되었다</p>
<h2 id="✌️이제-두번째-레슨-개발자-도구">✌️이제 두번째 레슨~ 개발자 도구</h2>
<h3 id="api-response-override">API Response Override</h3>
<p>API 응답 갈아끼우고 싶을 때 가끔 사용했다.
지금은 MSW 사용중이어서 잘 안 쓴다</p>
<p>사용</p>
<ol>
<li>Network 탭 -&gt; 네트워크 요청 중 값을 바꾸고 싶은 응답을 선택, 우클릭하여 Override Content를 누른다.</li>
<li>바꾸고 싶은 값을 바꾸고 저장 -&gt; 새로고침하면 response가 갈아끼워져 있다. UI로 제어하기 힘든 값들을 바꾸고 싶을 때 써먹으면 좋다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/3da5f720-906f-464a-bd0d-05fe7b60bb48/image.png" alt=""></li>
</ol>
<p>자세한건 아래 링크에서</p>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=KxjGYcHZ_uI">https://www.youtube.com/watch?v=KxjGYcHZ_uI</a></p>
</blockquote>
<h3 id="html-요소-duplicate-delete-force-state">HTML 요소 duplicate, delete, force state</h3>
<p>이거 은근 모르는 분들이 있다.
간단히 html 요소를 늘리거나 삭제하고 싶을 때 사용한다.
조작하고픈 element를 선택하고 하고 싶은 행동 누른다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/5f141755-8957-46ff-81b7-18fa91e3071f/image.png" alt=""></p>
<p>침착맨 영상으로 가득 duplicate했다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/8a302229-4f96-47d0-8b2e-cc1a92fcce6a/image.png" alt=""></p>
<p>hover 상태 같은거 보고 싶을 땐 force state를 쓰자
Styles 탭에도 동일한게 있다
<img src="https://velog.velcdn.com/images/ea_st_ring/post/3855183e-96ba-43b8-8a0f-a456ceb12e47/image.png" alt=""></p>
<h3 id="0">$0</h3>
<p>Web Inspector로 찍은 요소는 $0에 저장되어 console에서 사용할 수 있다.
이걸 가지고 유튜브 동영상이 미리 몇 개나 로드되어 있는지 알아보자</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/315ba22f-563c-4b4f-8cf1-928c7bc025ca/image.png" alt=""></p>
<p>대충 동영상 목록의 부모를 선택해서</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/6e22be2c-60e2-4e04-84b3-ba92487e9625/image.png" alt=""></p>
<p>콘솔에 찍어보니 25개다.
참고로 $1은 그 이전에 찍은 요소이다.</p>
<p>그렇다면 $2는?...</p>
<p>여러분의 상상에 맡긴다..ㄷㄷ</p>
<h3 id="chrome-developer-tools-ai-assistant">Chrome Developer Tools AI Assistant</h3>
<p>크롬 개발자 도구에 AI Assistance가 있다.
왜 요청이 오래 걸리는지 네트워크 요청 분석 등 물어보면 알려준다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/a56580d4-1c34-49f8-9806-635d39512c64/image.png" alt=""></p>
<p>위 내용들을 다 알고 있었다면?
당신은 멋쟁이 ~</p>
<h2 id="🖖-드디어-세번째-레슨-모바일-디버깅">🖖 드디어 세번째 레슨~ 모바일 디버깅</h2>
<p>개발 환경마다 다를 수도 있는데, 나는 이렇게 한다.</p>
<ol>
<li><p>개발 중인 PC와 폰을 같은 WIFI에 연결</p>
</li>
<li><p>실행 명령어 + <code>--host 0.0.0.0</code> 로 개발 환경 실행</p>
</li>
</ol>
<p>ex. <code>npm run start --host 0.0.0.0</code></p>
<ol start="3">
<li><p>WIFI IP 주소를 확인하여 해당 페이지로 접속
ex. IP가 <code>1.1.1.1</code>, 포트번호가 3000이면 <code>http://1.1.1.1:3000</code></p>
</li>
<li><p>야호~ 접속 완료</p>
</li>
</ol>
<p>사파리의 경우에는 <a href="https://developer.apple.com/safari/technology-preview/">Safari Technology Preview</a>를 쓰는 것도 추천한다. 
PC에 폰 연결해서 편하게 개발자 도구로 디버깅할 수 있다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/327c5bac-cb02-4950-a9d3-3d095edd927f/image.png" alt=""></p>
<h3 id="bonus">BONUS</h3>
<p>마지막으로 내가 사용중인 Cursor Rule도 시원하게 공유한다. 어디선가 긁어와서 디벨롭해서 쓰고 있다. 여러분들은 더욱 디벨롭해서 쓰길 바라며 <code>context7</code> MCP 연결해서 써야한다.</p>
<pre><code>- Always respond in Korean
- Be casual unless otherwise specified
- Be terse
- Suggest solutions that I didn&#39;t think about—anticipate my needs
- Treat me as an expert
- Be accurate and thorough

1. Bug Fixes:
   - Analyze the problem thoroughly before suggesting fixes
   - Provide precise, targeted solutions
   - Explain the root cause of the bug

2. Keep It Simple:
   - Prioritize readability and maintainability
   - Avoid over-engineering solutions
   - Use standard libraries and patterns when possible

3. Code Changes:
   - Propose a clear plan before making changes
   - Apply all modifications to a single file at once
   - Do not alter unrelated files

use context7 for task which needs document (library or framework or something)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[switch(true) vs if else]]></title>
            <link>https://velog.io/@ea_st_ring/switchtrue-vs-if-else</link>
            <guid>https://velog.io/@ea_st_ring/switchtrue-vs-if-else</guid>
            <pubDate>Wed, 25 Jun 2025 07:48:17 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>이전 회사에서 <code>switch(true)</code> 패턴을 자주 사용했다.</p>
<p>최근 코드를 짜면서 관성적으로 해당 패턴을 사용한 뒤, 셀프 리뷰 도중 문득 그런 생각이 들었다. 이게 정말 괜찮은 코드인가? </p>
<p>회사에서 짠 코드를 보기 쉽게 각색하여 <code>switch(true)</code> 패턴과 <code>if-else</code> 패턴으로 적어보았다. </p>
<h2 id="switchtrue-버전">switch(true) 버전</h2>
<pre><code class="language-js">    switch (true) {
      case isOnlyOneItem:
        // Do Nothing
        return;

      case isFirst:
        const originalFirstItem = _items[1];
        newRank = originalFirstItem.prevRank();
        break;

      case isLast:
        newRank = _items.at(-1).nextRank();
        break;

      case isBetween:
        newRank = beforeRank.between(afterRank);
        break;

      default:
        // case for last index item and has more items to load    
        const newItem = await fetchNextItem();
        newRank = _items.at(-1).between(newItem.rank)
        break;
    }</code></pre>
<h2 id="if-else-버전">if-else 버전</h2>
<pre><code class="language-js">    if (isOnlyOneItem) {
      // Do Nothing
      return;
    } else if (isFirst) {
      const originalFirstItem = _items[1];
      newRank = originalFirstItem.prevRank();
    } else if (isLast) {
      newRank = _items.at(-1).nextRank();
    } else if (isBetween) {
      newRank = beforeRank.between(afterRank);
    } else {
      // case for last index item and has more items to load 
      const newItem = await fetchNextItem();
      newRank = _items.at(-1).between(newItem.rank)
    }</code></pre>
<p>코드는 드래그 앤 드랍 이후 정렬을 위해 새로운 rank를 결정하는 코드이다. </p>
<blockquote>
<p>정렬 간 rank 개념이 궁금하다면 Jira에서 만들었으며, 라인에서도 사용하는 <a href="https://techblog.lycorp.co.jp/ko/about-atlassian-jira-ranking-algorithm-lexorank">LexoRank</a> 개념을 찾아보삼! </p>
</blockquote>
<p>얼핏 보기에는 <code>switch(true)</code> 구문이 더 깔끔해보인다.
코드를 읽는게 아니라 그냥 그림 보듯이 슥 보면 말이다.</p>
<p>그런데 지금와서 생각해보면, Readability를 향상시키고자 적었던 이 패턴이 실제로는 오히려 해친다는 생각이 들었다.</p>
<h3 id="익숙하지-않고-직관적이지-않다">익숙하지 않고, 직관적이지 않다.</h3>
<p>나와 같이 <code>switch(true)</code>를 사용하는 개발자가 있다면 이 구문을 보고 슥 넘어갈지도 모르지만, 아마 처음 보는 사람이라면 <code>switch(true)? 이게 뭐지? 바본가?</code> 라는 생각부터 들 법하다.</p>
<p>이게 첫번째 문제라고 생각한다.</p>
<h3 id="설계-의도에-부합하지-않는다">설계 의도에 부합하지 않는다.</h3>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/825c2a96-fd78-4be8-b6e0-1af1ae694e2b/image.png" alt=""></p>
<p>MDN에도 친절히 나와있듯, <code>switch</code> 직후 인자인 <code>expression</code>에는 <code>case</code> 구문과 매칭될 것으로 기대되는 값을 넣어주어야 한다.
<code>true</code>를 넣는 것은 편법이며, <code>표현식을 평가해서 그 값과 일치하는 case 절을 찾아 실행한다</code> 라는 switch의 설계 의도에 부합하지 않다. (제일 큰 문제다)</p>
<h3 id="번외-switch는-type-narrowing이-제한적이다">(번외) switch는 Type Narrowing이 제한적이다.</h3>
<p>if else에서는 앞에서 평가한 expression의 타입이 다음 expression에 반영된다.</p>
<p>좀 어거지로 만들긴 했지만 아래 예시를 보면, 첫번째 if문에서 value가 undefined가 아닌 것을 알 수 있으므로, 두번째 else if에서 &#39;+&#39; 연산자를 써도 문제가 없다.
value의 타입이 number로 좁혀졌기 때문이다.</p>
<pre><code class="language-ts">    interface Value {
      value?: number;
    }


    if(value === undefined) {
        return value;
    } else if(value + 1 === 2) {
        return value;
    } else {
        return 0;
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/cb176191-678c-47f6-94da-4f0e52444e97/image.png" alt=""></p>
<p>그러나 switch 문에서는 case 블록의 조건을 Type Narrowing의 근거로 사용하지 않는다. 요게 은근 불편할 때가 많았다. </p>
<pre><code class="language-ts">    switch(true) {
        case value === undefined:
            return value;

        case value + 1 === 2:
            return value;

        default:
            return 0;</code></pre>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/8ea88cb3-ecfe-4549-bb6e-d8309649d1df/image.png" alt=""></p>
<p>Type Narrowing에 관한 내용은 <a href="https://www.typescriptlang.org/docs/handbook/2/narrowing.html#control-flow-analysis">타입스크립트 문서</a>에서 확인 가능하다.</p>
<h3 id="번외-우선순위가-중요한-if-else-문을-어떻게-사용해야-잘-사용했다고-소문이-날까">(번외) 우선순위가 중요한 if-else 문을 어떻게 사용해야 잘 사용했다고 소문이 날까?</h3>
<p>자주하는 고민 중 하나는 <code>if-else</code> 문에서 <strong>우선순위가 중요한 경우</strong> 어떻게 적는 것이 좋겠냐는 것이다.
순서가 중요한 경우는 조건1이 참이고 조건2도 참일 때 조건1에 해당하는 동작을 먼저 수행해야 할 때이다.</p>
<p>예를 들어, 로그인을 안한 경우 접근 불가능한 페이지가 있다. 그러나 맥북 M4 PRO 오너라면 로그인 없이 접근할 수 있는 사이트가 있다고 가정하자.</p>
<p>그렇다면 코드는 아래와 같이 짤 수 있다.</p>
<pre><code class="language-ts"> if (isMacbookM4ProOwner) { // 로그인 안해도 입장 가능
    return &#39;환영합니다&#39;
 } else if (!isLogin) {
    return &#39;로그인 해주세요&#39;
 }
</code></pre>
<p>그런데 만약 평가식의 순서를 바꾸게 된다면</p>
<pre><code class="language-ts">if (!isLogin) {
    return &#39;로그인 해주세요&#39;
} else if (isMacbookM4ProOwner) {
    return &#39;환영합니다&#39;
}
</code></pre>
<p>아뿔싸, 맥북 M4 프로 오너임에도 로그인하라는 메시지를 받게된다.
이와 같이 평가식에서 참인 케이스가 여럿인 경우는 개발하다보면 은근 있다.
지금은 평가식이 두 개여서 간단하지만 더 많은 케이스가 추가된다면?</p>
<pre><code class="language-ts">if (isLoading) {
    return &#39;로딩중...&#39;
 } else if (isMacbookM4ProOwner) { // 로그인 안해도 입장 가능
    return &#39;환영합니다&#39;
 } else if (isWindows) {
    return &#39;이 참에 바꿔보시는건 어떨까요?&#39;
 } else if (!isLogin) {
    return &#39;로그인 해주세요&#39;
 } 
</code></pre>
<p>그럴때는 그냥 if문을 분리하자. </p>
<pre><code class="language-ts">
if (isLoading) {
    return &#39;로딩중...&#39;
}

if (isMacbookM4ProOwner) {
    return &#39;환영합니다&#39;
} else if (isWindows) {
    return &#39;이 참에 바꿔보시는건 어떨까요?&#39;
}

if (!isLogin) {
    return &#39;로그인 해주세요&#39;
}
</code></pre>
<p>영역을 잘 분리하는 것이 가독성에 도움이 된다고 생각한다.
물론 위의 예시는 return을 해주기에 유의미하다. 안하면 환영도 하고 로그인도 하고 난리난다.</p>
<p>여러 우선순위가 있고 조건이 복잡하다면 조건을 변수로 분리하는 등 최대한 노력을 기울여보자. 거지같은 비즈니스 로직에서 비롯된 if else문 떡칠 속에서도 깨끗한 코드 한 줄을 적어내려고 노력하자.</p>
<h3 id="결론">결론</h3>
<p><code>switch(true)</code>는 이제 잊자. switch문은 <strong>평가 가능한 enum 혹은 value</strong>에 대해 사용하도록 하고, <code>if-else</code>는 예쁘게 잘 쓰면 되겠다.</p>
<p>최근에 알게된 건데, <code>useMount</code> <code>useEffectOnce</code> (<code>react-use</code> 라이브러리에서 제공하는 라이프사이클 관련 훅)은 <a href="https://react.dev/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks">리액트 공식 문서</a>에서 쓰지 말라고 언급되어 있다. 그것도 모르고 그동안 신나게 썼다. </p>
<p>관성적으로 적는 코드를 다시 한 번 잘 들여다보자. 나도 모르게 나쁜 습관이 배어 있을 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Angular에 관한 고찰 - 2주 체험기]]></title>
            <link>https://velog.io/@ea_st_ring/Angular%EC%97%90-%EA%B4%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-2%EC%A3%BC-%EC%B2%B4%ED%97%98%EA%B8%B0</link>
            <guid>https://velog.io/@ea_st_ring/Angular%EC%97%90-%EA%B4%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-2%EC%A3%BC-%EC%B2%B4%ED%97%98%EA%B8%B0</guid>
            <pubDate>Mon, 16 Jun 2025 07:52:40 GMT</pubDate>
            <description><![CDATA[<p>이번에 입사한 회사에서 Angular를 스택으로 사용하고 있었다.
사실 Angular 프레임워크를 새롭게 스택으로 쌓아나가도 될까에 대한 고민을 좀 했다.
우리나라는 엄연한 리액트 공화국이니까...</p>
<p>그래도 결국 앵귤러(회사)를 선택하게 된 이유는 세 가지 정도가 있다.</p>
<ol>
<li>면접 본 회사 중 지금 회사가 가장 마음에 들었다.(서비스의 완성도도 좋았고, 잘될 것 같다 + 성장할 수 있겠다는 느낌이 강했다) </li>
<li>무릇 개발자라면 일정 수치 이상의 &#39;개발력&#39;만 갖추게 되면 기술 스택은 중요치 않다고 생각한다.</li>
<li>새로운 스택을 배워보고 싶었다. 현 회사는 Angular뿐 아니라 Capacitor + ionic을 이용한 앱 빌드 등도 경험할 수 있는 환경이다. 원하면 Nest까지도..</li>
</ol>
<p>아무튼 그래서 앵귤러를 미리 일주일 정도 공부하고, 입사 이후 과제 등을 통해 앵귤러를 익혀가고 있다.</p>
<p>2주 따리가 무슨 감상평이냐고 할 수 있지만, 그 말이 맞다.
그래도 느낀 점 몇 개 적어보겠다.</p>
<hr>

<h1>왜 잘 안쓰일까?</h1>
채용공고를 한창 찾아보던 저번달 즈음 체감으로, 채용중인 프론트엔드 기술 스택 비율은 아래와 같았다.

<ul>
<li>리액트 / Next - 90%</li>
<li>뷰 혹은 .net, jsp 등등... - 9%</li>
<li>앵귤러 - 1% (우리 회사 ㅋㅋ)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/747aa091-9144-4749-9aab-6e16d459bc5f/image.png" alt="">이미지 출처: <a href="https://survey.stackoverflow.co/2024/technology">https://survey.stackoverflow.co/2024/technology</a></p>
<p>stackoverflow ranking을 보면, 24년 기준으로 리액트는 39.5%, 앵귤러 17.1%, 뷰 15.4%인데... 국내에서 이는 완전히 다른 듯 하다.</p>
<p>잘 쓰이지 않게 된 데에는 다음과 같은 이유들이 있는 것 같다.</p>
<h3>1. 타입스크립트 강제</h3>
AngularJs 시절에는 자바스크립트도 지원하였으나, Angular로 넘어오면서 타입스크립트를 기본으로 사용되도록 바뀌었고 이는 러닝 커브를 가파르게 만들었다.

<h3>2. 가파른 러닝 커브</h3>
Angular를 잘 사용하기 위해서는 앵귤러에서 지원하는 방대한 기능들과, Rxjs를 이용한 반응형 프로그래밍에 익숙해져야하는데, 이 또한 개발을 처음 접하는 사람이 배우기는 쉽지 않다. 나로써는 이전 직장에서 rxjs를 찍먹한게 참 많이 도움이 되었다.

<h3>3. 관성적인 선택</h3>
지극히 개인적인 의견이긴한데..대한민국이 꽤 오래전부터 스프링 공화국이 되었듯 리액트도 충분히 한국 메인 스택(?)으로 자리매김할 정도의 선을 넘었고, 부트캠프 붐 시절 너도나도 가장 핫한 리액트를 배우면서 다른 기술 스택들이 차차 줄어들게 된 것 같다.

<hr>

<h1>Angular 2주 체험 후기</h1>
무슨 Angular에서 제품을 지원받아 적은 글 같은 제목이 되긴 했는데, 내돈내산이다.

<p>앵귤러 맛보기를 하면서 느낀 점은 다음과 같다.</p>
<h3>1. 오히려 좋다</h3>

<p>리액트를 개발하면서 <strong>가장 재밌으면서도 열받는 점</strong>은, 라이브러리 선택 및 학습이다. 번들러는 무엇을 쓰고, 라우팅 전략은 뭘 쓰고, UI는 뭐 상태관리는 뭐 뭐뭐뭐 하다보면 시간 다 간다. </p>
<p>실컷 배워놔도 한 해 지나니 새로운 뭐가 떠오른다. 물론 끊임없이 공부해야하는게 숙명이라지만, 이 분야에서 리액트는 단연 레전드다.</p>
<p>앵귤러 <strong>프레임워크</strong>에서 가장 좋았던 점은 위의 것들을 신경쓰지 않고, 프레임워크에 내장된 기술을 쓰면 된다는 것.</p>
<p>이렇게 이렇게 해 ! 라고 공식 문서에 나와있으니, 레퍼런스가 부족하더라도 그냥 보고 쓰면 된다. 공식 홈페이지의 권장되는 코딩 스타일 가이드도 참 많이 도움이 되었다.</p>
<h3>2. 조금 더 명확한 패턴</h3>

<p>리액트는 새로운 회사에 가든, 혹은 회사의 다른 개발자분과 협업을 하든 각자마다 코딩 스타일이 꽤나 다른 경우가 많다. 물론 내부 컨벤션 및 가이드를 통해 어느정도 해소 가능하지만, 감안하더라도 차이가 꽤 크다.</p>
<p>앵귤러는 다 &#39;해줘&#39;서 그럴 일이 적다.
앵귤러는 MVVM (Model-View-ViewModel) 패턴에 가까운데, 이런 것이 개발할 때 좀 더 생각을 안해도 되게 만들어준다. 라우팅이나 상태관리 측면에서도 대부분 그렇다.</p>
<p>EX)
Component - 뷰의 로직, 상태관리
HTMl - 뷰모델
Service - 이외 로직 때려박기</p>
<h3>3. 환경이 척박하긴 하다</h3>

<p>Angular 개발을 위해서는 VSCode보다는 WebStorm이 더 낫다고 한다. vsCode 만년 인생을 살아온 나로써는 적응하기 좀 어려웠다. 지금은 제법 잘 쓰는듯? 현재는 Webstorm + Cursor 사용중이다.</p>
<p>최신 레퍼런스는 제로에 가깝다. 가끔 불친절한 공식 문서를 보고 모르는 것을 구글링하면 대부분 최소 n년전 자료들이 나온다.(though thank you stackoverflow)
그래도 요즘 AI 성능이 참 좋아서 이것도 어느정도 해결되는 문제다.</p>
<h2>총평</h2>

<p>리액트나 앵귤러나 라이브러리 / 프레임워크의 설계 의도에 부합하게 코드를 잘 작성하려고 노력하면 되는 것 같다. 처음엔 어렵대서 쫄았는데, React, Angular, Vue 아무거나 깊게 잘만 공부해놓으면 다른 스택으로 넘어가는건 금방이라고 느꼈다. </p>
<p>Nest 녀석도 자꾸만 눈길이 간다. 이러다 풀스택 개발자가 되어버릴지도..?</p>
<hr>

<p>&quot;본 포스팅은 Angular 홍보를 목적으로 Google로부터 제품 대여 및 원고료를 지원 받지 않은 내돈내산이며, 직접 사용한 후기를 바탕으로 작성되었습니다.&quot;</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[첫 회사, 1년 동안 배우고 느낀 점들]]></title>
            <link>https://velog.io/@ea_st_ring/%EC%B2%AB-%ED%9A%8C%EC%82%AC-1%EB%85%84-%EB%8F%99%EC%95%88-%EB%B0%B0%EC%9A%B0%EA%B3%A0-%EB%8A%90%EB%82%80-%EC%A0%90%EB%93%A4</link>
            <guid>https://velog.io/@ea_st_ring/%EC%B2%AB-%ED%9A%8C%EC%82%AC-1%EB%85%84-%EB%8F%99%EC%95%88-%EB%B0%B0%EC%9A%B0%EA%B3%A0-%EB%8A%90%EB%82%80-%EC%A0%90%EB%93%A4</guid>
            <pubDate>Fri, 30 May 2025 04:58:29 GMT</pubDate>
            <description><![CDATA[<p>엘리스그룹의 프론트엔드 개발자로 첫 경력을 쌓아나가게 되었다.
최근 1년 2개월의 경력을 끝으로 퇴사하였다.</p>
<p>회사 다니느라 너무 바빠서 글 1년동안 하나도 못 썼다.(핑계다)</p>
<p>기술적인 부분 외에 배우고 느낀 점들을 정리해보려고 한다.
지금부터 적는 내용들은 <strong>바쁘고 정신 없는 스타트업</strong>에서는 비슷하게 적용될 것 같다.</p>
<h3 id="1-실무-개발--이데아를-찾는-과정">1. 실무 개발 = 이데아를 찾는 과정</h3>
<p>그간 회사에서 짠 코드의 대부분은 다 짜고 난 후, 혹은 짜다가도 이런 생각이 든다.</p>
<p>&quot;아 훨씬 잘 짤 수 있을 것 같은데...&quot;</p>
<p>평소 작문에 흥미가 있는 나는 처음에 <strong>멋있는 글</strong>을 쓰고 싶다는 생각을 했었다. 
그렇지만 멋있는 글을 쓰는 것보다 더 어려운 것은 <strong>쉽게 읽히는 글</strong>을 쓰는 것이었다.</p>
<blockquote>
<p>👉 기습 책 추천 <a href="https://www.yes24.com/Product/Goods/124031669?pid=123487&amp;cosemkid=go17020268406973532&amp;utm_source=google_pc&amp;utm_medium=cpc&amp;utm_campaign=book_pc&amp;utm_content=ys_240530_google_pc_cc_book_pc_12312%EB%8F%84%EC%84%9C&amp;utm_term=%EB%82%B4%EA%B0%80%EC%83%9D%EA%B0%81%ED%95%9C%EC%9D%B8%EC%83%9D%EC%9D%B4%EC%95%84%EB%8B%88%EC%95%BC&amp;gad_source=1&amp;gad_campaignid=6762605740&amp;gbraid=0AAAAAD79IroWVvGjgHErfBgYhAFkyZTcY&amp;gclid=CjwKCAjwi-DBBhA5EiwAXOHsGTcdV2xDtlLd3BIMuLv8IWp6x8qBM-caAjb5kXIh7GsPBhQ0ZCFhmxoCWqIQAvD_BwE">내가 생각한 인생이 아니야. 저자 류시화</a></p>
</blockquote>
<p>유지보수에 용이한 코드, 확장성 및 Readability가 뛰어난 코드, 구조가 잘 설계된 코드 등등 항상 지키고 행하고 싶은 원칙들은 많다.</p>
<p>코드도 글과 같이 쉽게 읽히는 코드가 위 원칙들을 만족하는 경우가 많은 것 같다.
그렇지만 코드를 짜다보면 이는 생각처럼 잘 되지 않는다.</p>
<p>실무에서 이를 방해하는 요소가 크게 세 가지가 있다고 보았다.</p>
<ul>
<li>복잡한 요구사항</li>
<li>레거시 코드 (= 기술 부채)</li>
<li>촉박한 마감기한</li>
</ul>
<p>당연하게도 코딩을 할 때 온전히 코드에만 집중하기는 어려웠다. 결국 회사에서 짜는 대부분의 코드는 복잡한 비즈니스를 프로그래밍 언어로 빠르게 풀어나가는 과정이고, 그 과정에서 나는 수많은 타협을 하게 된다. </p>
<p>위 세 가지에 대해 하나하나 설명하지 않아도 대부분 감이 올 것이라고 생각하여 적지 않겠다..</p>
<p>아무튼 실무에서의 개발은 이런 험난함 속에서도 최적의 코드를 찾아나가는 과정이라는 것을 느꼈다.</p>
<p>특히 기한이 빡빡할 때는... 시작부터 잘못 짰다고 느낄 때가 있어도 뒤엎지 못하고 다시 또 부채로 쌓이는 경우가 많았던 것 같다. 꼭 전체적인 구조부터 잘 잡아나가자.</p>
<pre><code>세 줄 요약
1. 바쁘고 정신 없을 때 코드 짜기 힘들다
2. 그냥 몸에 체화시키고 계속 의식하며 개발해야한다.
2. 초반 구조를 잘 잡자. 기술 부채는... </code></pre><h3 id="2-도메인">2. 도메인</h3>
<p>나는 교육 도메인에 관심이 있었다.</p>
<p>한 때 꿈이 선생님이기도 했었고, 코딩 과외를 1년 정도 한 경험도 있었기 때문에 교육 도메인은 재미있을 것이라고 기대했다.
그러나 기업 대상으로 하는 코딩 교육, 디지털 교과서 등 교육 플랫폼을 만드는 일은 내가 생각하던 교육과는 달랐다.
내가 관심 있었던 부분은 실제 교육을 하는 교육자의 입장이었지, 교육자와 학습자를 이어주는 플랫폼이 아니라는 것을 깨달았다.</p>
<p>추가로 나는 유저 혹은 서비스와 좀 더 맞닿아있는 개발을 하고 싶다는 걸 알게되었다. 사실 회사를 다니면서도 내가 직접 유저가 되어 서비스를 사용하는 일이 적었고, 배포 이후에도 유저의 반응을 확인하기가 어려웠다.</p>
<p>결국 개발을 꾸준히 하게 만드는 원동력은 성취감이었는데, 1년 동안 내가 이런 서비스를 만들었다! 라는 데에서 오는 성취감은 적었고 이게 큰 퇴직 사유가 되기도 했다.</p>
<p>이직을 처음 마음 먹은 2월 즈음에는 아무 회사나 적당히 괜찮아 보이면 지원하곤 했는데, 최근에는 정말 관심이 가게되는 곳을 위주로 지원하였다.
다행히 그 중 가장 마음에 들었던 곳에 합격하게 되어 곧 출근 예정이다. 야호~</p>
<pre><code>세 줄 요약
1. 교육 도메인 내 스타일 아니다
2. 내가 만들어낸 것이 효과가 있던 없던 그 정도를 아는 것 자체가 중요하다. 
   임팩트가 있었다면 성취감을 얻고, 없었다면 절치부심하게 된다. (아님 말고)
3. 취업했다. 야호~</code></pre><h3 id="3-효율적인-커뮤니케이션을-못하게-하는건-능력보단-상황">3. 효율적인 커뮤니케이션을 못하게 하는건 능력보단 상황</h3>
<p>이미 인터넷에 널린 좋은 커뮤니케이션의 예시가 많지만, 나도 한 번 적어보련다.</p>
<p>한 번에 할 수 있는 말을 여러 핑퐁에 나누어 하게되는 경우를 지양하고, 같이 알아야하는 사람이 있다면 잘 전달하기 등만 지켜도 중간은 가지 않을까?</p>
<p>~ 비효율적인 예 ~</p>
<pre><code>000 디자이너님, 이 부분 디자인 스펙 개발 코스트가 커서 수정하여도 괜찮을까요?

&quot;네, 어떤 식으로 수정할까요?&quot;

원형 버튼은 디자인 시스템에 커스텀해야하는 부분이 많아서, 대신 기존의 토글 버튼으로 수정하면 좋을 것 같아요. 

&quot;네, 그러면 그렇게 수정하시죠! 피그마에도 수정해둘게요.&quot;

~ 몇 시간 혹은 며칠 후 ~
@기획자 님 여기서 논의되었습니다!
&quot;음 그런데 토글 버튼은 사용성 측면에서 ~~&quot;

...</code></pre><p>~ 개선해본 예 ~</p>
<pre><code>000 디자이너님, 이 부분 디자인 스펙 커스텀할 부분이 많아보여서요, 
원형 버튼 대신 디자인 시스템의 토글 버튼으로 수정하여도 문제 없을까요?
cc. @기획자님 @동료 프론트엔드 개발자

...</code></pre><p>일부만 보여주는 예시긴 하지만, 내가 생각하는 어느정도의 기본이 담겨있다고 생각한다.</p>
<p>바쁘고 정신없는 상황에서 특히 중요하게 생각하는 요소들을 정리해보자면</p>
<ol>
<li>멘션 하나에 질문 / 요구사항 / 그로 인한 효과 등을 적어 비효율적인 코스트 없애기
➡️ 글로 설명하기 힘들면 그냥 찾아가서 여쭤보고 이후 <strong>글로 다시 정리</strong>해두는 것도 좋은 것 같다.</li>
<li>서로 이해하고 있는 부분이 명확히 일치하는지 확인
➡️ 가끔 이해한 것 같아도 서로 다른 방향을 바라보고 있을 때가 많다.</li>
<li>작업에 관련된 사람들이 모두 최신의 정보를 알 수 있도록 신경쓰기
➡️ 최고는 처음에 딱 정하고 안 변하는 것이지만..그러기는 쉽지 않으니</li>
</ol>
<pre><code>세줄 요약
1. 개발이나 커뮤니케이션이나 한 걸 다시 하는게 최악의 코스트이다.
2. 커뮤니케이션으로 중간을 못 가는 경우가 연차 불구하고 많다. 
능력이 안된다기보다 상황 혹은 태도가 문제인 것 같다.
3. 나도 여전히 잘 못하지만 최소한 고치려고 한다. 이게 제일 중요하다고 생각한다.</code></pre><p>적다보니 스타트업 절망편처럼 적은 감이 없잖아 있는데, 결국 나도 사람인지라 좋았던 점보다는 아쉬웠던 점들이 더 남는 것 같다. 그럼에도 불구하고 그 아쉬웠던 점들 덕분에 나는 훨씬 성장할 수 있었다 like 나를 죽이지 못하는 고통은 나를 더욱 강하게 만들뿐. 좋은 점들은 배우고, 아쉬운 점은 스스로 개선해나가면 된다 like Deep Learning.</p>
<p>적고 싶은 것들이 더 많았던 거 같은데 기억이 안나서 일단 줄인다.
생각나면 추가로 적어야지</p>
<p>1년 동안 힘들고 지칠 때도 많았지만, 성장하고 즐거웠던 기억들도 정말 많이 남아있다. 특히 부족한 나에게 많은 것들을 가르쳐준 프로덕트 팀 동료들에게 무한 감사하다.</p>
<p>Thank you</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/d7e2b6f2-ad36-44d1-9022-149afde4e3ce/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS 독학 5차시]]></title>
            <link>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-5%EC%B0%A8%EC%8B%9C</link>
            <guid>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-5%EC%B0%A8%EC%8B%9C</guid>
            <pubDate>Sun, 21 Jan 2024 01:18:39 GMT</pubDate>
            <description><![CDATA[<p>수강중인 강의 : <a href="https://codingapple.com/">코딩애플 NextJS</a></p>
<h2 id="✅-세션-토큰-로그인">✅ 세션, 토큰 로그인</h2>
<h3 id="세션-방식">세션 방식</h3>
<ol>
<li>유저가 로그인을 한다.</li>
<li>서버 DB에 아이디, 로그인 날짜, 유효기간, 세션 ID를 기록한다.</li>
<li>이후 post, get 요청 시 쿠키에 session id가 기록되어 보내진다.</li>
<li>session id를 DB의 정보와 비교 후, 요청을 처리한다.</li>
</ol>
<h4 id="장점--매-요청마다-엄격하게-로그인-상태를-체크할-수-있다">장점 : 매 요청마다 엄격하게 로그인 상태를 체크할 수 있다.</h4>
<h4 id="단점--db에-부하가-걸린다-해소---redis를-이용한-입출력-부하-줄이기">단점 : DB에 부하가 걸린다. 해소 -&gt; Redis를 이용한 입출력 부하 줄이기</h4>
<h3 id="토큰-방식jwt">토큰 방식(JWT)</h3>
<ol>
<li>유저가 로그인을 한다.</li>
<li>쿠키에 아이디, 로그인 시간, 유효기간 등이 암호화되어 저장된다.</li>
<li>이후 요청 시 쿠키를 보고 처리한다.</li>
</ol>
<h4 id="장점--db-부담이-적다">장점 : DB 부담이 적다.</h4>
<h4 id="단점--입장권이-도용당하면-큰일난다-해소---나쁜-입장권-목록을-db에-저장한다">단점 : 입장권이 도용당하면 큰일난다. 해소 -&gt; 나쁜 입장권 목록을 DB에 저장한다.</h4>
<h2 id="✅-oauth">✅ OAuth</h2>
<p>로그인 시 다른 곳의 개인정보 권한을 대여한다. ex) 구글에게 유저정보에 대한 <strong>접근 권한을 위임</strong>받는다.</p>
<h3 id="예시">예시</h3>
<ol>
<li>유저가 구글 로그인을 진행한다.</li>
<li>유저에게 내 사이트에서 정보 권한을 이용해도 되는지 동의를 구한다.</li>
<li>구글에서 권한 증명에 사용되는 코드를 발급한다.</li>
<li>받은 코드로 유저 정보 등 자원을 구글에 요청하여 받아온다.
이 때 받는 정보에는 유저이메일, 이름, AccessToken 등이 포함된다.
이후 이를 가지고 JWT, 혹은 세션 방식으로 이후 과정을 이어나간다.</li>
</ol>
<h3 id="next에서의-oauth">Next에서의 OAuth</h3>
<p>Next에서는 NextAuth.js라는 라이브러리가 존재한다.
이를 이용하면 소셜 로그인, JWT 토큰, Session, DB Adapter 방식 등 모두 이용 가능하다.
단, 아이디, 비밀번호를 이용한 로그인 시에는 세션 방식은 사용 불가능하고, 토큰 방식으로만 이용할 수 있다. 보안 이슈를 막기 위함이라고 한다..</p>
<h3 id="깃허브-소셜-로그인-만들기">깃허브 소셜 로그인 만들기</h3>
<h4 id="1-깃허브-로그인-진행-후-settings---developer-settings---oauth-apps에-진입한다">1. 깃허브 로그인 진행 후, settings -&gt; Developer Settings -&gt; OAuth Apps에 진입한다.</h4>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/b23a40d2-4eb2-493e-b36d-5ea5dfb303d0/image.png" alt=""></p>
<p>이후 New OAuth App을 누르고, name, URL을 적어준다.
이 때 개발 환경이라면 Homepage URL과 Authorization callback URL에 모두 <a href="http://localhost:3000%EC%9D%84">http://localhost:3000을</a>, 배포된 환경이 있다면 해당 URL을 적어준다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/639324d9-111d-43a3-b695-eaac89392262/image.png" alt=""></p>
<p>이후 생성 시 발급되는 Client ID와, Generate a new client secret를 눌러 암호를 잘 저장해두자.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/89e9f1e2-bfb6-4a0b-a9cf-0e1511e2f9d0/image.png" alt=""></p>
<p>이후 Next-Auth 세팅을 위해</p>
<p><code>npm install nextauth</code> 입력,
이후 root/pages/api/auth/[...nextauth].js
위와 같은 위치에 폴더 및 파일을 생성한다.</p>
<pre><code class="language-ts">// [...nextauth].js
import NextAuth from &quot;next-auth&quot;;
import GithubProvider from &quot;next-auth/providers/github&quot;;

export const authOptions = {
  providers: [
    GithubProvider({
      clientId: &#39;Github에서 발급받은 ID&#39;,
      clientSecret: &#39;Github에서 발급받은 Secret&#39;,
    }),
  ],
  secret : &#39;jwt생성시 쓸 암호&#39;
};
export default NextAuth(authOptions);</code></pre>
<p>이 때 jwt 생성 시 쓰는 암호에는 복잡한 문자열을 알아서 입력해주면 된다. 나는 1q2w3e4r!로 적었다.</p>
<p>이후 간단한 로그인 버튼을 만들어주자.</p>
<pre><code class="language-ts">// LoginBtn.tsx
&quot;use client&quot;;

import { signIn } from &quot;next-auth/react&quot;;

export default function LoginBtn() {

  return (
    &lt;button
      onClick={() =&gt; {
        signIn();
      }}
    &gt;
      로그인
    &lt;/button&gt;
  );
}
</code></pre>
<p>로그인 시에는 <code>signIn</code>을, 로그아웃 시에는 <code>signOut</code>을 import하여 사용해주면 된다.
이후 정보가 잘 받아와졌는지 확인하기 위해</p>
<pre><code class="language-ts">let session = await getServerSession(authOptions);
console.log(session);</code></pre>
<p>session은 다음과 같다.</p>
<pre><code class="language-js">{
  user: {
    name: &#39;Donghyun Hwang&#39;,
    email: &#39;-----@soongsil.ac.kr&#39;,
    image: &#39;https://avatars.githubusercontent.com/u/95581482?v=4&#39;
  }
}</code></pre>
<p>나의 깃허브 정보들이 잘 넘어와진걸 볼 수 있다.</p>
<h2 id="💎-느낀-점">💎 느낀 점</h2>
<p>백엔드 팀과 리액트 + 구글 로그인 + JWT토큰 방식으로 OAuth를 구현하다 많이 헤맸던 기억이 난다. 그 과정에서 알게 됐던 것들 덕분에 강의를 들으며 복습하는 기분이었고, 직접 Next + 세션 혹은 토큰 방식으로 로그인을 구현하는 것에 기대를 가지고 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS 독학 4차시]]></title>
            <link>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-4%EC%B0%A8%EC%8B%9C</link>
            <guid>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-4%EC%B0%A8%EC%8B%9C</guid>
            <pubDate>Wed, 17 Jan 2024 15:50:04 GMT</pubDate>
            <description><![CDATA[<p>수강중인 강의 : <a href="https://codingapple.com/course/next-js/">코딩애플 NextJS</a></p>
<h2 id="✅-npm-run-build-결과">✅ npm run build, 결과</h2>
<p>개발 서버가 아닌, 실제 배포를 위해서는 <code>npm run build</code>를 통해 배포에 필요한 파일들을 준비해줘야 한다.
이 때 로그를 살펴보면</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/6a777920-5179-4721-b7ed-4f03b7d20e29/image.png" alt=""></p>
<p>위와 같이 페이지 앞에 o, λ와 같은 기호들이 있는 것을 볼 수 있다.
각각 페이지의 렌더링 방식을 나타낸다.
o : <code>Static Rendering</code>
λ : <code>Dynamic Rendering</code></p>
<h2 id="✅-dynamic-rendering-vs-static-rendering">✅ Dynamic Rendering VS Static Rendering</h2>
<h3 id="dynamic-rendering의-특징">Dynamic Rendering의 특징</h3>
<blockquote>
<ol>
<li>접속마다 새로 html을 만들어 보낸다</li>
<li>fetch(&#39;/URL&#39;, { cache: &#39;no-store&#39; }), useSearchParams(), headers, cookies(), [dynamic route] 등을 사용 시에 Dynamic Rendering 된다.</li>
</ol>
</blockquote>
<h3 id="dynamic-rendering의-장단점">Dynamic Rendering의 장단점</h3>
<blockquote>
<ol>
<li>실시간 데이터를 유지 가능하다.</li>
<li>서버 / DB에 부담이 될 수 있다.</li>
</ol>
</blockquote>
<h3 id="static-rendering의-특징">Static Rendering의 특징</h3>
<blockquote>
<ol>
<li>아무 설정도 하지 않으면 디폴트로 설정된다.</li>
<li>npm run build 시 만들어진 페이지를 그대로 유저에게 보낸다.</li>
</ol>
</blockquote>
<h3 id="static-rendering의-장단점">Static Rendering의 장단점</h3>
<blockquote>
<ol>
<li>만들어진 페이지를 보내기 때문에 전송이 빠르다.</li>
<li>실시간 데이터 반영이 되지 않는다.</li>
</ol>
</blockquote>
<h3 id="static---dynamic-rendering-변환">Static &lt;-&gt; Dynamic Rendering 변환</h3>
<p><code>Static Rendering</code>, <code>Dynamic Rendering</code> 여부는 <code>npm run build</code> 시에 자동으로 결정되지만, 이를 전적으로 신뢰해서는 안된다. 
해당 프로젝트의 <code>list</code> 페이지의 경우에도, 글을 수정, 삭제, 작성  시에 페이지에 실시간 적용이 되어야하나, 현재는 
<img src="https://velog.velcdn.com/images/ea_st_ring/post/2c31813c-9e29-45fd-9e93-62aa0568f414/image.png" alt="">
<code>Static Rendering</code>으로 설정되어 있는 것을 볼 수 있다.
이를 강제로 <code>Dynamic Rendering</code>으로 바꾸기 위해서는 페이지 상단에 다음과 같이 적어주면 된다.</p>
<pre><code class="language-ts">// list.tsx
export const dynamic = &#39;force-dynamic&#39;;

// Dynamic을 Static으로 바꾸고 싶은 경우
// export const dynamic = &#39;force-static&#39;;</code></pre>
<p>이렇게 적어준 후 <code>npm run build</code>를 다시 해보면, 성공적으로 <code>Dynamic Rendering</code>으로 바뀌어 있는 것을 볼 수 있다</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/d11eac99-2129-4589-97d2-539c6b5b32e2/image.png" alt=""></p>
<h2 id="✅-캐싱">✅ 캐싱</h2>
<p><code>Dynamic Rendering</code>의 단점인 서버 / DB에 부하를 줄이기 위한 방법으로 캐싱이 있다. 불러왔던 데이터를 저장해두고 잠시 가져다 쓰는 것이다.</p>
<h3 id="1-get-요청-결과-캐싱">1. GET 요청 결과 캐싱</h3>
<p>get 요청을 캐싱하기 위해 </p>
<pre><code class="language-ts">await fetch(&#39;/URL&#39;, {cache: &#39;force-cache&#39;})</code></pre>
<p>위와 같이 적는 방법이 있다.
사실 적지 않아도 디폴트로 get 요청 값이 캐싱된다.
실시간 데이터 fetching을 위해 이를 해제하고 싶다면</p>
<pre><code class="language-ts">await fetch(&#39;/URL&#39;, {cache: &#39;no-store&#39;})</code></pre>
<p>와 같이 이를 명시해주면 된다.</p>
<p>이 외에도</p>
<pre><code class="language-ts">await fetch(&#39;/URL&#39;, {next: {revalidate: 60}})
// 60초 단위 캐싱</code></pre>
<p>과 같은 옵션이 있다.
앱의 성격에 맞게 이를 잘 구분해주자.</p>
<h3 id="2-페이지-단위-캐싱">2. 페이지 단위 캐싱</h3>
<p>하나의 요청이 아닌 페이지 단위로도 캐싱할 수 있다.
DB 입출력을 위한 </p>
<pre><code class="language-ts">export default async function Home() {

  const client = await connectDB;
  const db = client.db(&#39;board&#39;);
  let result = await db.collection(&#39;post&#39;).find().toArray();


  return ...;
}</code></pre>
<p>아래와 같은 코드가 있을 때,</p>
<pre><code class="language-ts">import { connectDB } from &quot;@/util/db&quot;;

export const revalidate = 60;
// 60초 캐싱

export default async function Home() {

  const client = await connectDB;
  const db = client.db(&#39;board&#39;);
  let result = await db.collection(&#39;post&#39;).find().toArray();

  return ...;
}</code></pre>
<p>최상단에 미리 예약되어 있는 변수 revalidate를 설정해주면 페이지 단위 캐싱이 가능하다.
참고로 방문자가 있을 때 60초 단위고, 없으면 페이지 재생성은 이루어지지 않는다.</p>
<p><a href="https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration">on-demand Revalidation</a>을 통해 60초가 지나기 전에 강제로 캐시를 새로 생성하는 방법도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS 독학 3차시]]></title>
            <link>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-3%EC%B0%A8%EC%8B%9C-w44h6qua</link>
            <guid>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-3%EC%B0%A8%EC%8B%9C-w44h6qua</guid>
            <pubDate>Fri, 12 Jan 2024 08:06:08 GMT</pubDate>
            <description><![CDATA[<p>수강중인 강의 : <a href="https://codingapple.com/course/next-js/">코딩애플 NextJS</a></p>
<p>가보자</p>
<hr>
<p>새로 알게 된 내용은 몇 없었다(MongoDB 업데이트, 수정 등).
알던 지식을 다시 정리해보자.</p>
<h2 id="✅-mongodb-데이터-수정-삭제">✅ MongoDB 데이터 수정, 삭제</h2>
<p>클라이언트 페이지는 다음과 같이 만들었다.</p>
<pre><code class="language-ts">// edit/[id]/page.tsx
import { connectDB } from &#39;@/util/db&#39;;
import { ObjectId } from &#39;mongodb&#39;;
import React from &#39;react&#39;;

const Edit = async (props: any) =&gt; {
  const db = (await connectDB).db(&quot;board&quot;);
  let result = await db.collection(&quot;post&quot;).findOne({ _id: new ObjectId(props.params.id) });

  if (!result) {
    alert(&#39;잘못된 접근입니다.&#39;);
    window.location.href = &#39;/list&#39;;
    return;
  }
  return (
    &lt;div className&gt;
      &lt;h4&gt;수정 페이지&lt;/h4&gt;
      &lt;form action=&quot;/api/post/edit&quot; method=&quot;POST&quot;&gt;
        &lt;label htmlFor=&quot;title&quot;&gt;제목&lt;/label&gt;
        &lt;input type=&quot;hidden&quot; name=&quot;id&quot; value={props.params.id} /&gt;
        // id를 전송하기 위한 input, 유저에게 보여서는 안됨
        &lt;textarea name=&#39;title&#39; defaultValue={result.title}&gt;&lt;/textarea&gt;
        &lt;label htmlFor=&quot;content&quot;&gt;내용&lt;/label&gt;
        &lt;textarea name=&#39;content&#39; defaultValue={result.content}&gt;&lt;/textarea&gt;
        &lt;button type=&quot;submit&quot;&gt;수정&lt;/button&gt;
      &lt;/form&gt;

    &lt;/div&gt;
  );
};

export default Edit;</code></pre>
<p>여기서 눈여겨 볼 점은 글의 id를 담기 위한 input을 하나 더 만들어준다는 것이다. id 정보를 가지고 있어야 DB에서 올바르게 데이터를 수정할 수 있다.
<code>&lt;input type=&quot;hidden&quot;&gt;</code> 또는 <code>style={{display:&#39;none&#39;}}</code>을 통해 사용자에게 보이지 않도록 주의하자.</p>
<p>다음은 서버 페이지다.</p>
<pre><code class="language-ts">// pages/api/post/edit.tsx
import { connectDB } from &quot;@/util/db&quot;;
import { ObjectId } from &quot;mongodb&quot;;
import { NextApiRequest, NextApiResponse } from &quot;next&quot;;

const edit = async (request: NextApiRequest, response: NextApiResponse) =&gt; {
  if (request.method == &quot;POST&quot;){
  const db = (await connectDB).db(&quot;board&quot;);

  let result = await db.collection(&quot;post&quot;).updateOne(
    {
      _id: new ObjectId(request.body.id),
    },
    {
      $set: {
        title: request.body.title,
        content: request.body.content,
      },
    },
    { writeConcern: { w: &quot;majority&quot; } }
  );

  response.status(302).redirect(&quot;/list&quot;);
  } else {
    response.status(400).json(&quot;잘못된 접근입니다.&quot;);
  }

};

export default edit;</code></pre>
<p>MongoDB에서 하나의 데이터를 수정할 때는 <code>updateOne()</code> 메소드를 사용한다. </p>
<p>기본 형식은 다음과 같다.</p>
<pre><code class="language-ts">db.collection(&quot;테이블 이름&quot;).updateOne(
    {
      // 업데이트 시에 데이터를 식별할 값
    },
    {
      $set: {
        // 수정할 값과 내용
      },
    });</code></pre>
<p><code>$set</code>뿐만 아니라 <code>$inc</code>를 통해 값의 변경이 아닌 증가/감소 처리 또한 가능하다.</p>
<p>삭제 또한 비슷한 형식인데, 클라이언트에서 삭제 버튼을  누를 시, /api/post/delete로 글의 id를 담아 DELETE 요청을 보낸다고 가정해보자.</p>
<p>이 때 서버에서는 다음과 <code>deleteOne</code> 메소드를 통해 처리한다.</p>
<pre><code class="language-ts">// api/post/delete.tsx
const Delete = async (request: NextApiRequest, response: NextApiResponse) =&gt; {
  if (request.method == &quot;DELETE&quot;){
  const db = (await connectDB).db(&quot;board&quot;);

  let result = await db.collection(&quot;post&quot;).deleteOne(
    {
      _id: new ObjectId(request.body.id),
    }
  );

  response.status(302).redirect(&quot;/list&quot;);
  } else {
    response.status(400).json(&quot;잘못된 접근입니다.&quot;);
  }

};

export default Delete;</code></pre>
<p>수정할 값을 담았던 <code>updateOne</code>과 달리, 데이터를 식별할 수 있는 값을 담아 보내면 데이터 삭제가 이루어진다.</p>
<h2 id="✅-client-컴포넌트에서의-데이터-접근">✅ Client 컴포넌트에서의 데이터 접근</h2>
<h3 id="1-props를-통한-접근">1. props를 통한 접근</h3>
<p>클라이언트 컴포넌트가 서버 컴포넌트의 자식 요소로 존재할 때, 데이터 접근은 서버 컴포넌트에서 처리한 뒤, 클라이언트 컴포넌트에 props로 이를 넘겨주는 방식이다.</p>
<pre><code class="language-ts">// 서버 컴포넌트
import { connectDB } from &quot;@/util/db&quot;;
import Link from &quot;next/link&quot;;
import ListItem from &quot;./ListItem&quot;;

export default async function List() {

  const db = (await connectDB).db(&#39;board&#39;);
  let result = await db.collection(&#39;post&#39;).find().toArray();

  return (
    &lt;div className=&quot;list-bg&quot;&gt;
      &lt;ListItem result={result} /&gt;
       // 클라이언트 컴포넌트 ListItem에 result를 넘겨줌
    &lt;/div&gt;
  )
}</code></pre>
<pre><code class="language-ts">// ListItem.tsx, 클라이언트 컴포넌트
&quot;use client&quot;;

const ListItem = (props: any) =&gt; {

  const result = props.result;
  return (
    &lt;div&gt;
      {result.map(
      // ... 
      )}
    &lt;/div&gt;
  );
};</code></pre>
<h3 id="2-useeffect를-통한-접근">2. useEffect를 통한 접근</h3>
<pre><code class="language-ts">&quot;use client&quot;;

import { useEffect } from &quot;react&quot;;

const ListItem = () =&gt; {

  useEffect(() =&gt; {
    // 서버에 부탁해서 DB 게시물 가져오는 코드 작성
    // result에 저장
  }, []);

  return (
    // ...
  );
};

export default ListItem;
</code></pre>
<p>유의할 점은 <strong>DB에 직접 접근하는 것이 아닌, 서버에게 DB 데이터를 달라는 요청을 보낸다</strong>는 점이다. 클라이언트 컴포넌트는 클라이언트에게 노출되기 때문에, DB 접근과 같은 민감한 행위들은 금지된다.
더 궁금하다면 <a href="https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-2%EC%B0%A8%EC%8B%9C#-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%A2%8B%EC%9D%84%EA%B9%8C">NextJS 독학 2차시</a>를 참고해보자.</p>
<h3 id="3-각-방식의-장단점">3. 각 방식의 장단점</h3>
<p>props를 통한 접근은 <code>SEO</code>에 더욱 유리하다.
useEffect가 호출되는 시점은 html이 모두 렌더링된 이후이기 때문에, 검색 엔진 봇에게 제대로 된 결과를 제공할 수 없다.</p>
<p>그러나 컴포넌트 구조가 복잡해질수록, props를 통해 데이터를 주고 받는 것은 <a href="https://velog.io/@rachel28/Prop-Drilling">Props Drilling</a>과 같은 문제를 낳을 수 있다.</p>
<h2 id="💎느낀-점">💎느낀 점</h2>
<p>NextJS에서는 Redux와 같은 전역 상태 관리 툴이 따로 존재하는지 궁금해졌다.
또한 백엔드 지식이 없다시피 하다보니 더 상세한 서버의 데이터, 예외 처리 등에 대해 알아봐야겠다고 생각했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS + TypeScript + MongoDB 연동 초기 설정]]></title>
            <link>https://velog.io/@ea_st_ring/NextJS-TypeScript-MongoDB-%EC%97%B0%EB%8F%99-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@ea_st_ring/NextJS-TypeScript-MongoDB-%EC%97%B0%EB%8F%99-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 04 Jan 2024 14:07:07 GMT</pubDate>
            <description><![CDATA[<h2 id="mongodb-세팅">MongoDB 세팅</h2>
<h3 id="1-계정-생성">1. 계정 생성</h3>
<blockquote>
<p><a href="https://www.mongodb.com/ko-kr">MongoDB</a>에서 회원가입을 진행한다.</p>
</blockquote>
<p>무료로 500mb 용량의 DB를 사용할 수 있다.</p>
<h3 id="2-db-접속용-아이디--비밀번호-생성">2. DB 접속용 아이디 / 비밀번호 생성</h3>
<p>Database Access에서 아이디, 비밀번호를 만들어준다.
이 때 아이디, 비밀번호는 잘 기억해두자.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/5a10553d-8683-4af4-8fa9-a6dc44907107/image.png" alt=""></p>
<p>생성 후 edit을 눌러 built-in-role을 <code>Atlas admin</code>으로 설정해주자. DB 접속 시 무엇이든 할 수 있게 하기 위함이다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/13395328-06f6-467d-871f-2d0e3b64c3a7/image.png" alt=""></p>
<p>그 후 <code>Network Access</code> 메뉴를 찾아 IP를 등록한다.
이 때 어디서나 접속해도 상관없다면 Allow access from anywhere을 누르거나 0.0.0.0/0 을 추가한다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/3daaa18b-f0b5-4baa-9612-eb2c8d54cf65/image.png" alt=""></p>
<h3 id="3-데이터-추가">3. 데이터 추가</h3>
<p>데이터베이스와 Collection을 만들고, 데이터를 추가해주자.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/545c4385-1757-4940-8f4c-b7f18543c72d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/5ed8be9e-194f-49c0-a1eb-5f78783be86c/image.png" alt=""></p>
<hr>
<h2 id="nextjs에서-사용하기">NextJS에서 사용하기</h2>
<h3 id="1-mongodb-라이브러리-설치">1. MongoDB 라이브러리 설치</h3>
<blockquote>
<p>npm install mongodb</p>
</blockquote>
<h3 id="2-db-연결">2. DB 연결</h3>
<p>생성한 DB에서 다음 과정을 따른다.
Connect - Drivers - 그 후 url을 복사해주자. </p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/923604ae-eda1-48ba-ae06-a62dda8e8e62/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/c8e1931b-d8d1-4949-9ccd-3cf3ed45bfa5/image.png" alt=""></p>
<h3 id="3-환경-변수-설정">3. 환경 변수 설정</h3>
<p>NextJS에서 환경 변수를 사용하기 위해 프로젝트 루트 디렉토리에
<code>.env.local</code> 파일을 만들어준다.
그 안에 복사한 URL을 기입해준다.</p>
<pre><code class="language-ts">MONGODB_URL = mongodb+srv://아이디:비밀번호@cluster0...mongodb.net/?retryWrites=true&amp;w=majority&#39;</code></pre>
<h3 id="4-전역-타입-및-tsconfig-설정">4. 전역 타입 및 tsconfig 설정</h3>
<p>타입스크립트와 같이 사용하기 위해 몇 가지 설정을 해주어야한다.</p>
<p>_mongo 전역 타입 지정을 위해 다음 위치에 <code>global.d.t.ts</code>파일을 하나 만들어준다.</p>
<pre><code class="language-ts">// 루트 디렉토리/src/type/global.d.t.ts
export {};

declare global {
    var _mongo: Promise&lt;MongoClient&gt; | undefined;
}</code></pre>
<p>tsconfig.json에 다음과 같이 추가해준다.</p>
<pre><code class="language-ts">// tsconfig.json
.. 
&quot;typeRoots&quot;: [
      &quot;./src/types&quot;,
      &quot;./node_modules/@types&quot;
    ]
...
&quot;include&quot;: [
    &quot;.next/types/**/*.ts&quot;,
    &quot;src/**/*&quot;,
    &quot;global.d.ts&quot;
  ],</code></pre>
<h3 id="5-dbts-생성">5. db.ts 생성</h3>
<p>db 연결 코드는 계속해서 사용되므로 util 폴더를 만들고 db.ts 파일을 만든다.</p>
<pre><code class="language-ts">// util/db.ts

import {MongoClient} from &#39;mongodb&#39;

const url = process.env.MONGODB_URL;

if (!url) {
    throw new Error(&#39;The MONGODB_URL environment variable is not defined&#39;)
}
let connectDB: Promise&lt;MongoClient&gt;;
if (process.env.NODE_ENV === &#39;development&#39;) {
    if (!global._mongo) {
        global._mongo = new MongoClient(url).connect()
    }
    connectDB = global._mongo
} else {
    connectDB = new MongoClient(url).connect()
}
export {connectDB}</code></pre>
<h3 id="6-사용-방법">6. 사용 방법</h3>
<pre><code class="language-ts">import { connectDB } from &quot;@/util/db&quot;;

export default async function Home() {

  const client = await connectDB;
  const db = client.db(&#39;db이름&#39;);
  let result = await db.collection(&#39;collection 이름&#39;).find().toArray();
  console.log(result);

  return &lt;div&gt;...&lt;/div&gt;;
}</code></pre>
<p>위와 같이 사용하면 되며, 자세한 사용법은 추후 다시 정리하도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS 독학 2차시]]></title>
            <link>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-2%EC%B0%A8%EC%8B%9C</link>
            <guid>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-2%EC%B0%A8%EC%8B%9C</guid>
            <pubDate>Thu, 28 Dec 2023 08:06:51 GMT</pubDate>
            <description><![CDATA[<p>수강중인 강의 : <a href="https://codingapple.com/course/next-js/">코딩애플 NextJS</a></p>
<p>시작하자</p>
<hr>
<p>서버, 클라이언트 컴포넌트에 대해 간략하게 배웠다.
궁금해져서 더욱 찾아보았다.</p>
<h2 id="✅-server--client-component">✅ Server / Client Component</h2>
<p><a href="https://ko.legacy.reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html">React18</a>에서 공개된 서버 컴포넌트를 이어받아 NextJS 13에서도 Server / Client 컴포넌트가 탑재되었다.
우선 서버 컴포넌트와 클라이언트 컴포넌트가 무엇인지 알아보자.</p>
<h3 id="server-component">Server Component</h3>
<blockquote>
<p>서버 측에서 컴포넌트를 실행하여 미리 렌더링된 마크업을 생성 후,
클라이언트에게 전달되어 사용자에게 콘텐츠를 보여주는 방식.</p>
</blockquote>
<p>❗ 서버 사이드 렌더링과는 <strong>엄연히 다른 개념</strong>이다! 서버 컴포넌트는 SSR의 단점을 보완하기 위해 등장했다.</p>
<h4 id="특징">특징</h4>
<blockquote>
<ul>
<li>페이지 초기 로딩이 빠르다. (UX 향상)</li>
</ul>
</blockquote>
<ul>
<li>클라이언트에게 컴포넌트 JS 번들이 전달되지 않는다. </li>
<li>백엔드 리소스에 접근이 가능하다 ≒ 서버 인프라를 더욱 잘 활용할 수 있다.</li>
</ul>
<h3 id="client-component">Client Component</h3>
<blockquote>
<p>기존의 우리가 사용하던 리액트 컴포넌트</p>
</blockquote>
<p>❗ 클라이언트측만이 아닌, 클라이언트 / 서버 양측에서 모두 렌더링이 일어난다!</p>
<h4 id="사용법">사용법</h4>
<blockquote>
<ul>
<li>코드 최상단에 &#39;use client&#39;라고 명시한다. 적지 않을 시 기본적으로 서버 컴포넌트로 작동한다.</li>
</ul>
</blockquote>
<ul>
<li>내부에 서버 컴포넌트를 포함시킬 수 없다. <a href="https://yozm.wishket.com/magazine/detail/2271/">읽어보면 좋은 글</a></li>
</ul>
<h3 id="❓-언제-사용하면-좋을까">❓ 언제 사용하면 좋을까</h3>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/c7d48ecd-0c87-4a46-823e-2213f9a8a8bc/image.png" alt=""></p>
<p><a href="https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#server-component-patterns">공식 문서</a>에서 이를 표로 잘 알려주고 있다.</p>
<p>서버 컴포넌트는 <code>Data Fetching</code>이나, 민감한 정보를 숨기고 싶을 때 사용하면 되는 듯 하다.
대신 우리가 기존에 쓰던 <code>useState()</code>, <code>onClick()</code> 등의 이벤트 리스너, hooks를 사용하려면 클라이언트 컴포넌트를 사용해야 한다.</p>
<h2 id="✅-request-memoizaition">✅ Request Memoizaition</h2>
<p>NextJS에서는 여러 번의 동일한 <code>Data fetching</code>에 대해 캐싱을 지원한다.
강의에서는 deduplication이라고 잠깐 소개해주셨는데, <a href="https://nextjs.org/docs/app/building-your-application/caching#request-memoization">공식 문서</a>에서는 이를 <code>Request Memoization</code>이라고 표현하는 것 같다.
이는 NextJS만의 기능이 아닌 리액트에서 제공하는 기능이다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/04ea5e3c-94f2-4351-b30b-5c25cf467a87/image.png" alt=""></p>
<p>이렇게 그림으로 나와있는데, 와~ 이거 좋다 고 느꼈다.
참고로 메모이제이션은 <code>GET</code> 메소드에 한해서만 작동한다고 한다.</p>
<p>왜지? 라고 잠깐 생각했는데, POST 메소드에 대해서도 캐싱을 해버리면 동작이 비정상적으로 일어날 것 같다고 생각했다. 단순히 받아오는 게 아니라 보내는 데이터가 있기 때문에..</p>
<p>예시 코드는 아래와 같다.</p>
<pre><code class="language-ts">async function getItem() {
  // The `fetch` function is automatically memoized and the result
  // is cached
  const res = await fetch(&#39;https://.../item/1&#39;)
  return res.json()
}

// This function is called twice, but only executed the first time
const item = await getItem() // 첫 요청에 대한 데이터가 캐시된다. (cache MISS)

// The second call could be anywhere in your route
const item = await getItem() // 같은 요청이기 때문에 캐시된 데이터가 불러와진다. (cache HIT)</code></pre>
<h2 id="💎느낀-점">💎느낀 점</h2>
<p>Next를 어떤 용도로, 어떻게 써먹으면 좋을 지에 대해 다시 한 번 생각했다.</p>
<p>풀스택 웹개발 찍먹 + 자주 다루던 리액트를 사용하기 때문에 시작하긴 했지만, NextJS를 세세히 배워가며 어떤 성격의 프로젝트에 어떻게 활용하면 좋을지 더욱 고찰해봐야겠다.</p>
<p>또한 규모가 큰 프로젝트의 경우 서버 컴포넌트와 클라이언트 컴포넌트의 구조를 적절히 잘 배합하여 시작하는 것이 정말 중요하겠다고 느꼈다.</p>
<hr>
<h2 id="📚레퍼런스">📚레퍼런스</h2>
<blockquote>
<p><a href="https://yozm.wishket.com/magazine/detail/2271/">서버 컴포넌트란</a>
<a href="https://html-jc.tistory.com/657#1.%20What%20are%20Server%20Components-1">Server Components</a>
<a href="https://yiyb-blog.vercel.app/posts/look-around-server-components">리액트 서버 컴포넌트, 간단하게 알아보기</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS 독학 1차시]]></title>
            <link>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-1%EC%B0%A8%EC%8B%9C</link>
            <guid>https://velog.io/@ea_st_ring/NextJS-%EB%8F%85%ED%95%99-1%EC%B0%A8%EC%8B%9C</guid>
            <pubDate>Wed, 27 Dec 2023 08:29:33 GMT</pubDate>
            <description><![CDATA[<p>즐겨보는 유튜브 <a href="https://codingapple.com/course/next-js/">코딩애플</a>님의 강의를 통해 시작하였다.</p>
<p><a href="https://dev-ellachoi.tistory.com/28">SSR, 유니버셜 렌더링</a>이 중요해지는 요즘 나도 살아남기 위해 NextJS를 배워보기로 결심했다.</p>
<h2 id="nextjs란">NextJS란?</h2>
<p><a href="https://nextjs.org/">NextJS</a>는 Vercel이 개발한 <code>node.js</code> 기반 리액트 SSR 프레임워크이며, 현재 <strong>나이키, 틱톡, 트위치</strong> 등의 기업에서 사용되고 있다.</p>
<p>왜 NextJS가 사랑받는지, 어떻게 써먹으면 좋을지는 차차 알아가보자.</p>
<hr>
<h2 id="✅시작하기">✅시작하기</h2>
<p><code>NextJs</code>는 node 18 이상 버젼을 요구한다.
설치되어 있지 않다면 <a href="https://nodejs.org/en">node.js</a>에서 LTS 버젼을 다운받자.</p>
<p>node 설치를 완료하였다면, 작업용 폴더를 하나 만들어준다.
그 후 <code>shift + 우클릭</code> -&gt; <code>여기에 Powershell 창 열기</code>를 눌러준다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/931968c3-fe4c-4786-a4bd-743b8e828c33/image.png" alt=""></p>
<p>이후 터미널에 </p>
<pre><code>npx create-next-app@latest</code></pre><p>를 입력해준다.</p>
<p>이후 설정은 남자답게 선택해준다.
App Router 사용 -&gt; Y만 잘 해주자.</p>
<p>만들어진 폴더를 코드 에디터에서 열어주자.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/33f058a8-cc00-4e7b-9f46-07be7f8596a0/image.png" alt=""></p>
<p>그러면 위와 같은 폴더 구조가 생성된다.</p>
<p>여기서 짚고 넘어갈 점이 있다.</p>
<h2 id="✅앱-라우터">✅앱 라우터</h2>
<ul>
<li><code>layout.tsx</code> : 레이아웃 페이지(메인 페이지를 감싸고 있다)</li>
<li><code>page.tsx</code> : 각 페이지의 메인 페이지. 리액트 기반 코드를 작성한다.</li>
</ul>
<ol>
<li><code>page</code>는 <code>layout</code>에 항상 감싸져 있다.
따라서 최상위 layout에 헤더를 정의하면, 다른 폴더의 page에는 항상 헤더가 들어가게 된다. 굿</li>
<li><code>NextJS</code>는 폴더 기반으로 알아서 라우팅을 해준다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/3d335a10-1938-47d2-844d-b30d30866495/image.png" alt="">
위와 같이 &#39;app/cart&#39; 폴더를 만들어두면, 
<code>http://localhost:3000/cart</code>와 같이 접속했을 때 cart 디렉토리 내에 있는 page.tsx가 보여지게 된다!
이건 정말 편한 것 같다.
<img src="https://velog.velcdn.com/images/ea_st_ring/post/2532fc00-33c6-448c-94db-2a12fabe0451/image.png" alt="">
당연히 폴더 내 폴더 구조를 통해 중첩 라우팅도 가능하다.
<code>http://localhost:3000/cart/payment</code> 접속 시 payment 폴더의 page.tsx가 보이게 된다.</li>
</ol>
<h2 id="✅이미지-사용">✅이미지 사용</h2>
<p><code>image</code> 태그를 사용할 수도 있지만, NextJS에서는 사이즈 최적화, <span style="color:royalblue">레이아웃 시프트</span>, <span style="color:royalblue">Lazy Loading</span>를 위해 Image 태그를 지원한다.</p>
<pre><code class="language-ts">import Image from &quot;next/image&quot;;
import food0 from &quot;/public/images/food0.png&quot;;

export default function List() {
  return (
    &lt;div&gt;
      &lt;h4 className=&quot;title&quot;&gt;상품목록&lt;/h4&gt;     
          &lt;Image src={food0} alt=&quot;food&quot;/&gt;
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
}
</code></pre>
<p>위와 같이 <code>Image</code>를 임포트하여 써먹도록 하자.</p>
<hr>
<h2 id="💎최종-목표">💎최종 목표</h2>
<p>방학 동안 NextJS를 이용하여 개인 프로젝트를 하나 완성하고 싶다.
생각중인건 개인 블로그인데, 이후 더욱 구체화시켜야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Slack 클론코딩 6주차]]></title>
            <link>https://velog.io/@ea_st_ring/Slack-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-6%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ea_st_ring/Slack-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-6%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Wed, 27 Dec 2023 07:36:22 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.inflearn.com/course/%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%B1%84%ED%8C%85/dashboard">Slack 클론코딩</a></p>
<p>마무리 단계에 접어들었다.
짧게 구성된 섹션인만큼 배운 점이 많지는 않았던 것 같다.</p>
<hr>
<h2 id="📚배운-점">📚배운 점</h2>
<h3 id="1-npm-run-build">1. npm run build</h3>
<p>지금까지는 npm run dev를 통해 개발 모드로 진행했다면,
배포를 위해 이제는 npm run build를 사용한다.
이 때 총 프로젝트의 압축 버전인 dist 폴더가 나온다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/5a11c370-e6ab-48fb-9e8c-c963ac144b61/image.png" alt=""></p>
<p>위와 같이 dist 폴더가 나오는 것은 알고 있었지만,
이를 <span style='color:royalblue'>webpack-bundle-analyzer</span> 라이브러리 설치 후 npm run build를 실행하게 되면 </p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/40bf719b-946f-4703-b96d-d0c02f6a5c81/image.png" alt=""></p>
<p>위와 같이 각 번들이 어느 정도 공간을 차지하고 있는지 볼 수 있다는 점이 신기했다.
이를 잘 분석하여 </p>
<ol>
<li>Tree-shaking을 통해 공간을 확보하거나</li>
<li>Code-splitting을 통해 Lazy Loading을 구현할 수 있다는 점이 마음에 들었다.</li>
</ol>
<h3 id="2-타입-가드">2. 타입 가드</h3>
<p><a href="https://radlohead.gitbook.io/typescript-deep-dive/type-system/typeguard">타입가드</a>는 조건문에서 객체의 타입을 좁혀나가며 타입을 구분, 점검하는 역할을 한다.</p>
<ul>
<li><code>typeof</code> 연산자<pre><code class="language-ts">function doSomething(x: number | string) {
if (typeof x === &#39;string&#39;) { // TypeScript는 `x`가 무조건 `string`이란 걸 알고 있다.
console.log(x.subtr(1)); // Error: `subtr`은 `string`에 존재하지 않는 메소드다.
console.log(x.substr(1)); // 굿
}
x.substr(1); // Error: `x`가 `string`이라는 보장이 없다.
}</code></pre>
</li>
<li><code>in</code> 연산자
내부에 특정 property가 존재하는지 확인하는 연산자이다.<pre><code class="language-ts">
</code></pre>
</li>
</ul>
<p>interface IChat {
  // 채널의 채팅
  id: number;
  UserId: number;
  User: IUser;
  content: string;
}</p>
<p>export interface IDM {
  // DM 채팅
  id: number;
  SenderId: number;
  Sender: IUser;
}</p>
<p>interface Props {
  data: IDM | IChat;
}</p>
<p>const user = &#39;Sender&#39; in data ? data.Sender : data.User;
// data라는 객체 안에 Sender 속성이 있는지 검사함</p>
<pre><code>
이 외에도 instanceOf, 리터럴 타입 가드, 사용자 정의 타입 가드 등이 있다. 타입 점검 시에 유용하게 써먹을 듯 하다.

---

## ✅해결한 에러

1. 슬랙 아이콘이 제대로 보이지 않던 오류를 수정했다.
2. 가끔 input이 제대로 작동하지 않던 오류를 해결했다.

## 😡해결할 에러
1. 서버에서 온라인 유저 목록이 제대로 불러와지지 않는다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Slack 클론코딩 5주차]]></title>
            <link>https://velog.io/@ea_st_ring/Slack-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-5%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ea_st_ring/Slack-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-5%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Wed, 27 Dec 2023 07:20:25 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.inflearn.com/course/%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%B1%84%ED%8C%85/dashboard">Slack 클론코딩</a></p>
<p>한 번쯤 써보고 싶었던 <a href="https://socket.io/how-to/use-with-react#example">웹소켓</a>에 대해 배웠다.</p>
<hr>
<h2 id="📚배운-점">📚배운 점</h2>
<h3 id="1-socketio">1. socket.io</h3>
<p>웹소켓을 통해 서버, 클라이언트 간 실시간으로 데이터를 주고 받을 수 있다.
채팅 기능을 구현하기 위해 사용하였다.</p>
<p>소켓 사용 프로세스는 다음과 같다.
ㆍ <code>connect</code>을 통해 서버와 연결한다.
ㆍ <code>emit</code>을 통해 데이터를 보낸다.
ㆍ <code>on</code>을 통해 데이터를 받는다.
ㆍ <code>disconnect</code>을 통해 연결을 해제한다.</p>
<p>서버 코드는 이미 준비되어 있기 때문에, 아래와 같이 클라이언트에서 useSocket 훅을 구현하여 필요한 곳에서 사용할 수 있도록 하였다.</p>
<h3 id="usesocket-훅-구현">useSocket 훅 구현</h3>
<pre><code class="language-ts">
// 우선 알맞은 workspace에 대해 소켓 연결을 저장할 객체를 선언한다.
const sockets: { [key: string]: SocketIOClient.Socket } = {}; 

/* 
  useSocket에서 workspace 인자를 받고,
  해당 워크 스페이스에 연결된 소켓과 disconnect 함수를 반환한다.
*/
const useSocket = (workspace?: string): [SocketIOClient.Socket | undefined, () =&gt; void] =&gt; {
  // ...
  return [sockets[workspace], disconnect];
}
</code></pre>
<p>... 부분에 <strong>소켓 연결 생성, 연결 해제</strong> 관련 코드를 적는다.</p>
<pre><code class="language-ts">// 소켓 연결 해제를 위한 cleanup 함수
const disconnect = useCallback(() =&gt; {
    if (workspace) {
      sockets[workspace].disconnect();
    }
  }, [workspace]);

  if (!workspace) { 
    // workspace가 올바르게 제공되지 않은 경우 undefined를 반환한다
    return [undefined, disconnect];
  }

// socket 연결이 존재하지 않을 시 생성하며, transports를 통해 polling 방식이 아닌 웹소켓으로 업그레이드한다.
  if (!sockets[workspace]) {
    sockets[workspace] = io.connect(`${backUrl}/ws-${workspace}`, {
      transports: [&#39;websocket&#39;],
    });
  }

  return [sockets[workspace], disconnect];
</code></pre>
<p>최종 코드는 다음과 같다.</p>
<pre><code class="language-ts">// useSocket.ts
import io from &#39;socket.io-client&#39;;
import { useCallback } from &#39;react&#39;;

const backUrl = &#39;http://localhost:3095&#39;;

const sockets: { [key: string]: SocketIOClient.Socket } = {};
const useSocket = (workspace?: string): [SocketIOClient.Socket | undefined, () =&gt; void] =&gt; {
  const disconnect = useCallback(() =&gt; {
    if (workspace) {
      sockets[workspace].disconnect();
    }
  }, [workspace]);

  if (!workspace) {
    return [undefined, disconnect];
  }

  if (!sockets[workspace]) {
    sockets[workspace] = io.connect(`${backUrl}/ws-${workspace}`, {
      transports: [&#39;websocket&#39;],
    });
  }

  return [sockets[workspace], disconnect];
};

export default useSocket;</code></pre>
<p>이제 다른 컴포넌트에서 <code>useSocket</code> 훅을 통해 소켓을 사용할 수 있다.</p>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-ts">import useSocket from &#39;@hooks/useSocket&#39;;


const [socket] = useSocket(workspace);

  const onMessage = useCallback(
    () =&gt; {
      // 메세지가 왔을 때 필요한 기능 구현
    }
    [],
  );

  useEffect(() =&gt; {
    socket?.on(&#39;message&#39;, onMessage);
    return () =&gt; {
      socket?.off(&#39;message&#39;, onMessage);
    };
  }, [socket, onMessage]);
</code></pre>
<p>서버에서 message 이벤트를 수신할 시, <code>onMessage</code> 함수를 호출한다.
이 때 <code>useEffect</code>를 통해 컴포넌트 언마운트 시 리스너를 제거하여 메모리 누수, 원치 않은 동작을 방지한다.</p>
<h3 id="느낀-점">느낀 점</h3>
<p>생각보다 클라이언트에서 데이터를 주고 받는 것 자체에 필요한 코드가 많지 않은 것 같다. 오히려 스크롤 처리, 파일 업로드 등 부가적인 요소에 훨씬 할 일이 많은 것 같다.</p>
<h3 id="2-리버스-무한-스크롤">2. 리버스 무한 스크롤</h3>
<p>무한 스크롤은 구현해 본 경험이 있지만, 이를 역으로 구현하는 것은 처음이었다. 채팅의 경우 아래가 아닌 위로 계속 올려 이전 채팅 내역을 보기 때문에, 역방향의 무한 스크롤이 필요했다.
여기서는 <code>swr</code>의 도움을 받아 구현한다.</p>
<p>먼저 대략적인 구조는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/0ce4b55e-feae-4096-8871-e6a3b2a8bfc1/image.png" alt=""></p>
<ol>
<li><code>DirectMessage (혹은 Channel)</code>이 <code>ChatList</code>의 부모 컴포넌트이다.</li>
<li><code>ChatList</code>에는 채팅 박스가(스크롤 포함) 구현되어 있다.</li>
<li><code>DirectMessage</code>에 useSWRInfinite를 정의한 뒤, ChatList에서 스크롤바의 ref를 받는다.</li>
</ol>
<pre><code class="language-ts">// DirectMessage.tsx
const DirectMessage = () =&gt; {

  const { data: chatData, mutate: mutateChat, revalidate, setSize } = useSWRInfinite&lt;IDM[]&gt;(
    (index) =&gt; `/api/workspaces/${workspace}/dms/${id}/chats?perPage=20&amp;page=${index + 1}`,
    fetcher,
  );
  // index -&gt; page의 수를 의미한다.

  const isReachingEnd = isEmpty || (chatData &amp;&amp; chatData[chatData.length - 1]?.length &lt; 20) || false;
  // isReachingEnd를 통해 채팅 데이터를 다 불러왔는지 확인한다.

  const scrollbarRef = useRef&lt;Scrollbars&gt;(null);
  // scrollbar에 접근하기 위해 ref 선언

  return (    
    // ...    
      &lt;ChatList ref={scrollbarRef} setSize={setSize} isReachingEnd={isReachingEnd} /&gt;
    // setSize -&gt; page 수를 바꿔주는 함수이다.
    // ...    
  );
};

export default DirectMessage;</code></pre>
<ol start="4">
<li>ChatList에 스크롤, 페이징 관련 함수 <code>onScroll</code>을 정의한다.<pre><code class="language-ts">// ChatList.tsx
const ChatList = forwardRef&lt;Scrollbars, Props&gt;(({ chatSections, setSize, isReachingEnd }, scrollRef) =&gt; {
const onScroll = useCallback(
 (values) =&gt; {
   // 채팅 스크롤이 가장 위에 닿았는지 확인
   if (values.scrollTop === 0 &amp;&amp; !isReachingEnd) {
     // size를 변경하여 무한 스크롤이 되도록
     setSize((prevSize) =&gt; prevSize + 1).then(() =&gt; {
       // 스크롤 위치 유지 코드
       const current = (scrollRef as MutableRefObject&lt;Scrollbars&gt;)?.current;
       if (current) {
         current.scrollTop(current.getScrollHeight() - values.scrollHeight);
       }
     });
   }
 },
 [scrollRef, isReachingEnd, setSize],
);

</code></pre>
</li>
</ol>
<p>return (
    <ChatZone>
      <Scrollbars autoHide ref={scrollRef} onScrollFrame={onScroll}>
        // ...
      </Scrollbars>
    </ChatZone>
  );
});</p>
<p>export default ChatList;</p>
<pre><code>❗ **체크할 부분**

- ``forwardRef``를 통해 하위 컴포넌트(ChatList)의 ``scrollRef``를  상위 컴포넌트(DirectMessage)에서 접근할 수 있다.

- 스크롤 관련 작성 코드, ``Optimistic UI``를 위한 처리 등등이 추가로 필요한데, 이는 따로 정리하는 것이 좋을 것 같다.


## 💎 느낀 점
1. 좋은 라이브러리를 많이 알고 사용할 줄 아는 것.
프론트엔드 개발자의 미덕이다.
2. 우리가 흔히 쓰는 채팅 기능을 구현하기 위해 처리할 부분들이 한 두가지가 아니었다. 실제 프로젝트에 적용시키며 익숙해지자..

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[첫 프로젝트, 프론트엔드 개발자의 협업]]></title>
            <link>https://velog.io/@ea_st_ring/%EC%B2%AB-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%ED%98%91%EC%97%85</link>
            <guid>https://velog.io/@ea_st_ring/%EC%B2%AB-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%ED%98%91%EC%97%85</guid>
            <pubDate>Fri, 17 Nov 2023 09:45:08 GMT</pubDate>
            <description><![CDATA[<h2 id="✅들어가며">✅들어가며</h2>
<blockquote>
<p>❗ 본 글은 IT 연합동아리 코테이토의 프론트엔드 네트워킹 3차시 활동을 목적으로 작성된 글이며, 상당히 <strong>주관적인</strong> 내용을 담고 있습니다. 
기획자, 디자이너, 백엔드 개발자와 처음으로 협업을 진행하는 예비 프론트엔드 개발자분들에게 조금이나마 도움이 되길 바라면서 적는 글이며, 기업에서의 협업 방식은 저도 아직 경험해보지 못한 영역이기 때문에 혹시 기업에서의 협업 방식과 크게 다른 부분이 있다면 지적해주시면 감사하겠습니다.</p>
</blockquote>
<hr>
<h2 id="👨🏻🏫공통">👨🏻‍🏫공통</h2>
<h3 id="1-프론트엔드-개발자의-숙명-📢">1. 프론트엔드 개발자의 숙명 📢</h3>
<p>프론트엔드 개발자는 기획자, 디자이너, 백엔드 개발자와 모두 밀접하게 맞닿아 있기 때문에, 각 직군과의 의사소통이 가장 잦습니다.</p>
<p>그렇다면 프론트엔드 개발자는 어떻게 훌륭한 소통을 만들어갈 수 있을까요?
-&gt; 제 생각에 이상적인 의사소통에 있어 필수불가결한 두 가지는 
<strong>올바른 문서 작성 및 사용 + 정형화된 용어</strong> 라고 생각합니다.  </p>
<h3 id="2-자주-사용되는-툴에-대한-기초적인-사용법을-숙지해놓자">2. 자주 사용되는 툴에 대한 기초적인 사용법을 숙지해놓자</h3>
<p>저 같은 경우에 동아리 단위 프로젝트에서는 팀 단위에서 <span style=color:darkslategray>Notion</span>, 디자이너와는 <span style=color:deeppink>피그마</span>, 백엔드 개발자와는 <span style=color:forestgreen>API 명세서</span> (ex. Swagger)를 사용하여 소통하는 일이 잦았습니다. 
앞서 나온 툴들은 동아리 단위 프로젝트에서는 스탠다드에 가까우며 혹시 사용법을 잘 모르고 있다면 시간을 내서 조금이라도 숙지하도록 합시다.</p>
<p><span style="color:gray">저는 디자이너 한 분을 붙잡고 2시간 동안 피그마 특강을 들었습니다...</span></p>
<h3 id="3-모르면-물어보자">3. 모르면 물어보자</h3>
<p>저는 첫 프로젝트에서 회의를 진행하면서 모르는 단어, 처음 써보는 툴이 정말 많았습니다. </p>
<p>ERD? 포스트맨?(가수 X) LO-FI?(노래 장르 아님) Ideation? 피그마에 메모 남기는 법 등등.... </p>
<p>너무 기본적이거나 간단하게 느껴지는 것은 자기가 직접 찾아봅시다.
그래도 잘 모르겠으면, 회의의 흐름을 끊지 않는 선에서(쉬는 시간이나 회의 직후) 꼭 물어봅시다. 다들 처음이고 미숙한건 당연하니까요.</p>
<p>모르는 것은 잘못이 아니나 <strong>모르는 것을 알면서도 방치하는 것</strong>은 죄입니다..</p>
<h2 id="👨🏻🏫직군-별">👨🏻‍🏫직군 별</h2>
<h3 id="💻백엔드-개발자와의-협업">💻백엔드 개발자와의 협업</h3>
<p>백엔드와의 협업 = API 통신
그 중에서도 RESTful API 통신이 대표적인데요.
이게 어떤건지 잘 모르시겠다면 <a href="https://learn.microsoft.com/ko-kr/azure/architecture/best-practices/api-design">이 글</a>을 한 번 읽어보시면 좋을 것 같습니다. 저는 첫 프로젝트를 할 때 처음 들어봤어요..</p>
<p>통신 과정에서, 프론트나 백이나 한 번에 API 연결 딱딱 진행되서 값을 잘 넘겨주고 받아오면 좋겠지만 현실은 녹록지 않죠...😪
대표적인 예로 처음 프로젝트를 경험하게 되면 자주 겪는 CORS 에러, 상황에 따른 request 파라미터 변경 등 서로 소통하게 될 일이 많은데요. 이 때 2가지 정도만 주의하여 진행하면 적어도 욕먹을 일은 없을거에요!</p>
<blockquote>
<p><strong>1. 질문 / 문제 발생 시에는 상세히 알려주기</strong></p>
</blockquote>
<pre><code>FE : 로그인이 안돼요! - X 

FE : 로그인 해보니 이러이러한 에러가 떴어요! - △

FE : api/~~/login 으로 login, password 값 담아서 post 요청 보냈는데 ~~~한 에러가 돌아옵니다. 확인 부탁드려요 - O</code></pre><p>에러 등 문제상황이 생겼을 땐 최대한 모든 정보를 알려주세요! 그래야 원인을 찾기도 쉽고, 시간 낭비도 피할 수 있습니다. 이건 백엔드뿐 아니라 동료 개발자에게도 물론 적용되는 사항입니다.</p>
<blockquote>
<p><strong>2. 요청 사항이 있을 시에는 정중하게, 존중하며</strong></p>
</blockquote>
<p>위험한 생각 = <em>이거 하나 바꾸는게 어렵나?</em> </p>
<p>우리도 잘 알잖아요. 하나 바꾸는게 어렵고 귀찮은 경우가 많다는거..
넘겨주는 값, 넘겨받는 값 등 수정이 필요해 보일 땐 1에서와 같이 어떤 이유로, 어떻게 바꾸면 좋을 지 상세하게 부탁해보도록 합시다. 또한 개발자 간 의사소통에서는, <strong>명확하고 공식적인 용어</strong>를 사용하여 착오가 없도록 합시다. API는 프론트와 백엔드 간의 &quot;약속&quot; 입니다.</p>
<h3 id="🎨디자이너와의-협업">🎨디자이너와의 협업</h3>
<p>다들 개발을 배우는 요즘이지만, 대부분의 사람들에게 개발은 어렵고, 낯선 영역입니다. 디자이너와 협업 시에는 이 점을 명심하고 들어가면 좋을 것 같습니다! </p>
<blockquote>
<p><strong>1. 이해하기 쉬운 용어를 사용하자</strong></p>
</blockquote>
<p>백엔드 개발자와의 협업에서 공식적인 개발 용어를 사용했다면, 이젠 비유를 통해 설명할 때입니다. 디자이너가 어떤 요청을 했을 때</p>
<pre><code>개발하기 힘들어서 안돼요
렌더링 이슈도 있고 마땅한 라이브러리가 없어서 구현하기 힘들 것 같아요</code></pre><p>이런 식으로 얘기하면 능력이 없다거나, 예의가 없다고 뒷담화를 들을 수도 있겠죠? 객체에 대해 배울 때 붕어빵에 비유했던 것처럼, 우리도 비개발자가 알아듣기 쉽도록 설명해줍시다.</p>
<blockquote>
<p><strong>2. 존중, 또 존중</strong></p>
</blockquote>
<p>프론트엔드 개발자는 디자이너가 만든 화면을 <strong>토씨 하나 빠짐없이 그 대 로 퍼블리싱</strong>하도록 합시다. 개인의 의견을 넣어 수정, 추가, 삭제 등의 행위를 하는 것은 디자이너를 무시하는 행동이며, 좋은 프론트엔드 개발자가 아닙니다. 
그럼에도 불구하고 정말 이건 아니다 싶거나, 사정상 수정이 필요할 때가 있습니다. 그럴 때는 지겹도록 말했듯, 정중하게 물어보도록 합시다.</p>
<p>너무 프론트엔드 개발자가 상대적 약자(?)인 느낌으로 이것저것 적은 것 같은데요.
그래서 이번에는 프로젝트 경험이 적은 디자이너에게 요청하면 좋을 것
들을 알려드리려고 합니다.</p>
<blockquote>
<p><strong>1. 컬러 팔레트, 자주 사용되는 컴포넌트(ex.버튼) 따로 빼놓아 주세요!</strong></p>
</blockquote>
<p>페이지에서 자주 사용되는 색, 컴포넌트들을 미리 피그마 한 곳에 빼두면, 프로젝트 초기 설정 단계부터 색상을 변수화시키거나, 컴포넌트를 미리 만들어 두기 편하겠죠? 한 번 요청해보도록 합시다.</p>
<blockquote>
<p><strong>2. 여러 플로우에 따른 디자인 모두 만들기</strong></p>
</blockquote>
<p>그런 경우가 있었습니다. </p>
<p>// 화면에는 좋아요 버튼이 있는데 빈 좋아요 버튼만 있고 눌러진 좋아요 버튼은 없는 경우
// 로그인, 회원가입 중 누락된 입력 값에 대한 알림 텍스트 디자인이 없음</p>
<p>이와 같이 디자이너도 사람이다 보니, 사소한 것들을 빼먹는 경우가 발생합니다. 이런 경우를 미연에 방지하기 위해 한 번씩 미리 말씀드려 요청해보도록 합시다.</p>
<p>마지막으로 VSCode를 쓰시는 분들께 팁 하나 드리자면,
피그마에는 개발자 모드가 있으며, VSCode에도 피그마 익스텐션이 존재합니다.</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/8b492304-c95d-479b-8c0a-0fa98f923e1c/image.png" alt="">
저 버튼을 눌러 개발자 모드를 활성화시키면 </p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/17eb88ab-f372-4911-a942-58c26fdf4be9/image.png" alt=""></p>
<p>위와 같이 값들만 따로 보기 편하게 활성화됩니다!</p>
<p>또한 VSCode에서도 피그마 익스텐션을 설치하시면</p>
<p><img src="https://velog.velcdn.com/images/ea_st_ring/post/b56192d1-cba9-472b-815b-618b72f0e6d4/image.png" alt=""></p>
<p>위와 같이 피그마를 보면서 개발할 수 있습니다. 근데 이건 듀얼 모니터 쓰시는 분들에게 권장드려요</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Slack 클론코딩 3-4주차]]></title>
            <link>https://velog.io/@ea_st_ring/Slack-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-3-4%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ea_st_ring/Slack-%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-3-4%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 05 Nov 2023 10:51:05 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.inflearn.com/course/%ED%81%B4%EB%A1%A0%EC%BD%94%EB%94%A9-%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%B1%84%ED%8C%85/dashboard">인프런 Slack 클론 코딩</a></p>
<p>섹션 2~3 메뉴와 모달 만들기, DM 보내기를 수강했다.</p>
<hr>
<h2 id="📚배운-점">📚배운 점</h2>
<h3 id="1-input은-컴포넌트-분리">1. Input은 컴포넌트 분리</h3>
<p>어찌 생각하면 기본이다.
그렇지만 우리는 기본을 자주 잊곤 한다. 
규모가 큰 컴포넌트에서는 input을 분리시켜 글자 입력마다 리렌더링이 일어나지 않도록 조심하자.</p>
<h3 id="2-react-toastify">2. react-toastify</h3>
<p>쉽게 예쁜 알림창을 구현해주는 라이브러리.
상세한 내용은 따로 작성해보도록 하고, 간단한 사용법만 정리해보자면</p>
<blockquote>
<p>yarn add react-toastify</p>
</blockquote>
<pre><code class="language-ts">import {ToastContainer, toast} from &quot;react-toastify&quot;;
import &#39;react-toastify/dist/ReactToastify.css&#39;;

const notify = () =&gt; toast(&#39;toastify test&#39;);

// ...

return(
    &lt;div&gt;
      &lt;button onClick={notify}&gt;
        알림 노출
      &lt;/button&gt;
        &lt;ToastContainer /&gt;
    &lt;/div&gt;
)</code></pre>
<p>이런 식으로 사용해주면 알림이 잘 뜨게 된다.
프로젝트에 적용해봐야지..</p>
<h3 id="3-autosize-라이브러리">3. autosize 라이브러리</h3>
<p>textarea의 사이즈를 자동으로 늘려주는 라이브러리.
기존에 textarea에 글을 많이 입력하게 되면 사이즈가 아닌 스크롤로 늘어나게 된다.</p>
<p>사용방법</p>
<blockquote>
<p>yarn add autosize</p>
</blockquote>
<p>타입스크립트라면</p>
<blockquote>
<p>yarn add @types/autosize</p>
</blockquote>
<p>까지!</p>
<pre><code class="language-ts">import autosize from &quot;autosize&quot;;


const ref = useRef&lt;HTMLTextAreaElement&gt;(null);

useEffect(() =&gt; {
  if (ref) {
    autosize(ref.current as HTMLTextAreaElement);
  }
}, []);

// ...

return(
  &lt;textarea
    ref={ref}
    value={text}
    onChange={onChangeTextarea}
    placeholder={&quot;내용을 입력해주세요.&quot;}
  /&gt;
);
</code></pre>
<p>이런 식으로 사용하면 된다!
textarea도 나름 자주 쓰는 태그인데, 유용하게 잘 써먹을 듯 하다.</p>
<h2 id="💎느낀-점">💎느낀 점</h2>
<p>강의 중 크게 공감했던 말이 있다.
스타일드 컴포넌트 / 혹은 emotion으로 태그를 만들다보면 이름이 뭐 Section부터 시작해서 Wrapper Container Zone 등등 중구난방으로 다양해진다고...
아직까지 공식적인 분류는 없더라도 나만의 규칙을 어느정도 만들어놔야겠다고 생각하게 되었다.
(ex. 한 페이지의 제일 큰 부분은 컨테이너, Contents를 담고 있는 작은 부분에 Section 이런 식으로...)</p>
]]></description>
        </item>
    </channel>
</rss>