Congratulations!

[Valid RSS] This is a valid RSS feed.

Recommendations

This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.

Source: https://tech.remember.co.kr/feed

  1. <?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
  2.    <channel>
  3.        <title><![CDATA[remember-tech - Medium]]></title>
  4.        <description><![CDATA[어서오세요! 리멤버 기술 블로그입니다. - Medium]]></description>
  5.        <link>https://tech.remember.co.kr?source=rss----307f82e3ebfb---4</link>
  6.        <image>
  7.            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
  8.            <title>remember-tech - Medium</title>
  9.            <link>https://tech.remember.co.kr?source=rss----307f82e3ebfb---4</link>
  10.        </image>
  11.        <generator>Medium</generator>
  12.        <lastBuildDate>Sun, 11 May 2025 16:44:41 GMT</lastBuildDate>
  13.        <atom:link href="https://tech.remember.co.kr/feed" rel="self" type="application/rss+xml"/>
  14.        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
  15.        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
  16.        <item>
  17.            <title><![CDATA[코드 한 줄로 경험하는 React 동시성의 마법]]></title>
  18.            <link>https://tech.remember.co.kr/%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EB%A1%9C-%EA%B2%BD%ED%97%98%ED%95%98%EB%8A%94-react-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%98-%EB%A7%88%EB%B2%95-5ff18aee148d?source=rss----307f82e3ebfb---4</link>
  19.            <guid isPermaLink="false">https://medium.com/p/5ff18aee148d</guid>
  20.            <category><![CDATA[concurrent-rendering]]></category>
  21.            <category><![CDATA[front-end-development]]></category>
  22.            <category><![CDATA[react18]]></category>
  23.            <category><![CDATA[engineering]]></category>
  24.            <category><![CDATA[react]]></category>
  25.            <dc:creator><![CDATA[성정민]]></dc:creator>
  26.            <pubDate>Wed, 16 Apr 2025 11:32:19 GMT</pubDate>
  27.            <atom:updated>2025-04-17T08:28:17.006Z</atom:updated>
  28.            <content:encoded><![CDATA[<p>안녕하세요! 리멤버앤컴퍼니 파운데이션 크루에서 Web Frontend Software Engineer로 일하고 있는 성정민입니다.</p><p>최근 저는 사용자가 검색창에 키워드를 입력할 때마다 실시간으로 UI를 업데이트하는 기능을 개발했습니다. 그런데 문제가 발생했어요. 검색창에 글자를 입력할 때마다 수천 개의 데이터를 필터링하고 화면에 표시해야 하는데, 이 과정에서 입력이 멈칫거리는 현상이 나타났습니다. 예를 들어, 사용자가 “안녕”을 입력할 때 “안”까지는 괜찮다가 “녕”을 입력하는 순간 브라우저가 잠시 멈추는 식이었습니다.</p><p>처음에는 이 문제를 어떻게 해결할지 고민하다가 React 18의 동시성 모드에서 소개된 useTransition API를 적용해보았습니다. 처음 적용했을 때는 &quot;와, 이게 해결되네!&quot; 싶었지만, 단순히 &quot;마법&quot;이라고 넘기기엔 궁금한 점이 많았습니다. 그래서 React의 내부 동작을 좀 더 깊이 들여다보며 이 기능의 원리를 제대로 이해하고 싶었고, 그 내용을 여러분과 함께 나누고자 합니다.</p><h3>React와 동시성 렌더링의 이해</h3><p>리멤버의 웹 서비스는 대부분 React로 만들어져 있습니다. React는 2017년 Fiber 아키텍처를 도입한 이후로 Suspense, 동시성 같은 기능을 꾸준히 발전시켜왔고, 2024년엔 React Compiler라는 멋진 변화를 선보이기도 했죠. 오늘은 그중에서도 <strong>React 18의 동시성 기능</strong>과 이를 통해 UI를 최적화한 이야기를 해보려고 합니다.</p><h4>동시성과 병렬성: 비슷하지만 다른 개념</h4><p>동시성이란 무엇일까요? 단어 자체만 보면 여러 작업을 동시에 수행한다는 의미로 보입니다. 그렇다면 병렬성과 같은 개념일까요? 이 두 개념의 차이를 명확히 이해하는 것이 중요합니다.</p><blockquote>Go 언어 창시자의 <a href="https://go.dev/blog/waza-talk">Concurrency is not parallelism</a> 발표에 따르면…</blockquote><blockquote>“동시성은 독립적으로 실행되는 프로세스들의 조합이다.”<br>“병렬성은 연관된 복수의 연산들을 동시에 실행하는 것이다.”<br>“동시성은 여러 일을 한꺼번에 다루는 문제에 관한 것이다.”<br>“병렬성은 여러 일을 한꺼번에 실행하는 방법에 관한 것이다.”</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tnmNLr5W-tjRxMP8Bw9wQQ.png" /></figure><p>병렬성은 멀티 코어 환경에서 실제로 여러 작업이 동시에 처리되는 개념입니다. 반면, 동시성은 싱글 코어에서도 작동할 수 있으며, 여러 작업이 동시에 실행되는 것처럼 보이지만 실제로는 번갈아 가며 실행됩니다. 마치 여러분이 동영상을 보다가 코드를 작성하는 것처럼, 작업 간에 전환하면서 여러 일을 처리하는 방식이에요.</p><p>정리하자면, <strong>동시성은 2개 이상의 독립적인 작업을 잘게 나누어 마치 동시에 실행되는 것처럼 프로그램을 구조화</strong>하는 방법입니다.</p><h4>React가 동시성을 도입한 이유: 브라우저의 렌더링 블로킹 문제</h4><p>브라우저의 메인 스레드는 JavaScript 실행, DOM 조작, 스타일 계산, 레이아웃, 페인팅을 모두 담당하는 싱글 스레드로 동작합니다. 메인 스레드가 어떤 작업을 시작하면, 그 작업이 완료될 때까지 다른 작업을 수행할 수 없습니다. 일반적으로는 이것이 문제가 되지 않아요. React의 Virtual DOM과 diff 알고리즘이 매우 빠르기 때문이죠. 하지만 렌더링 과정이 길어지는 상황에서는 문제가 발생합니다. 예를 들어, <strong>수천 개의 데이터가 있는 복잡한 목록을 필터링하거나 정렬할 때 화면이 버벅</strong>거리게 됩니다.</p><p>간단한 예를 들어볼까요? 아래 예제에서는 입력창에 글자를 입력할 때마다 입력된 글자 수의 비례해서 셀의 수가 증가하며 화면이 업데이트됩니다.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodesandbox.io%2Fembed%2Fgtw6kl&amp;display_name=CodeSandbox&amp;url=https%3A%2F%2Fcodesandbox.io%2Fp%2Fsandbox%2Fgtw6kl&amp;image=https%3A%2F%2Fcodesandbox.io%2Fapi%2Fv1%2Fsandboxes%2Fgtw6kl%2Fscreenshot.png&amp;type=text%2Fhtml&amp;schema=codesandbox" width="1000" height="500" frameborder="0" scrolling="no"><a href="https://medium.com/media/a4f61d912f663e645c3262694e15bff8/href">https://medium.com/media/a4f61d912f663e645c3262694e15bff8/href</a></iframe><p>입력값이 적을 때는 문제가 없지만, 입력값이 늘어날수록 색상 목록과 사용자 입력의 렌더링 속도가 느려지는 것을 볼 수 있습니다. 이것이 바로 <strong>블로킹 렌더링</strong>입니다. JavaScript 연산이 길어지면서 프레임이 드롭되고, 사용자 입력은 뒤로 밀려나게 됩니다. 60fps로 부드럽게 동작해야 할 애니메이션이 끊기고, 버튼 클릭이나 입력이 지연되는 현상이 발생하죠.</p><p>전통적으로 이 문제는 디바운싱(debounce)이나 스로틀링(throttle)과 같은 기법으로 해결해 왔습니다. 하지만 이런 방식은 지연시간을 임의로 설정해야 하므로 사용자의 입력 속도를 예측해야 하는 한계가 있었습니다.</p><h4>사용자 경험을 위한 우선순위 체계</h4><p>동시성이 중요한 또 다른 중요한 이유는 사용자 경험(UX) 때문입니다. React 팀은 이를 최적화하기 위해 인간의 인지와 기대에 관한 연구를 진행했습니다.</p><p><a href="https://github.com/reactwg/react-18/discussions/41">useTransition PR</a>에서 사용자 경험에 대한 중요한 통찰을 확인할 수 있습니다.</p><blockquote><strong><em>”사용자는 물리적인 행위에 대해서 즉각적인 반응을 기대한다. 그렇지 않다면 사용자는 뭔가 잘못되고 있다고 느낄 수 있다. 반면 A0 -&gt; A1의 전환은 느릴 수 있다고 무의식적으로 인지하고 있으며, 모든 전환에 대한 즉각적인 반응을 기대하지 않는다.”</em></strong></blockquote><p>버튼 클릭, 키보드 입력, 화면 터치와 같은 물리적 상호작용에서는 사용자가 실제 세계의 물체를 조작할 때처럼 즉각적인 반응을 기대합니다. 이러한 행동에 지연이 발생하면 시스템이 고장 났거나 작동하지 않는다고 느끼게 됩니다. 반면, 검색 결과 로딩이나 화면 전환과 같은 상태 변화(A0 -&gt; A1)에서는 사용자가 무의식적으로 지연을 예상하게 됩니다. 약간의 지연에도 큰 불편함을 느끼지 않아요.</p><p><strong>React 18은 동시성을 이용해 이 문제를 해결</strong>합니다. 이를 도로에 비유해 설명해 볼게요.</p><h4>1차선 도로의 한계</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pzSJUl72EiDbRXx0cUEubA.png" /></figure><p>여기 1차선을 달리는 두 차량이 있습니다. 도로가 1차선이므로 빨간 차는 초록 차가 지나가는 속도에 맞춰 함께 달릴 수밖에 없습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J2qye_-E5SthlPBmq7guBQ.png" /></figure><p>차량이 적으면 문제가 없지만, 차량이 증가할수록 뒤에 있는 차는 쉽사리 이동할 수 없습니다.</p><p>렌더링할 컴포넌트가 한두 개라면 싱글 스레드에서도 빠르게 처리할 수 있습니다. 하지만 입력이 증가할수록 목록을 렌더링하는 데 CPU가 많은 시간을 소비하면서 뒤에 쌓인 입력 이벤트를 처리할 수 없게 되어 <strong>블로킹 렌더링</strong>을 유발합니다.</p><h4>2차선 도로의 유연함</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/904/1*9SqYWc_2zq2fVK58djx4Rg.png" /></figure><p>다른 쪽에는 2차선 도로가 있습니다. 다른 차량에게 방해받지 않기 때문에 길이 막히지 않습니다. 이걸 동시성이라 표현 해볼게요. 두 개의 작업이 각각의 차선으로 나뉘어 처리됩니다. <strong>동시성 덕분에 두 작업의 스택이 분리됐네요.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WI3A-s8WwtJktElfhc41Cw.png" /></figure><p><strong>동시성 렌더링의 핵심은 메인 스레드에게 일정 시간을 양보(Yield)한다는 점</strong>입니다. 렌더링 작업을 하다가 잠시 멈추고 메인 스레드가 다른 작업을 처리할 수 있는 시간을 줍니다. 이때 브라우저는 이벤트를 처리할 수 있고, 이 덕분에 응답성이 빨라집니다.<br>또한 효과적인 양보를 위해, 하나의 컴포넌트 렌더링을 잘게 분해하여 처리합니다. 전체 렌더링 작업을 작은 단위로 나눠서 각 단위 사이에 메인 스레드가 다른 작업을 처리 하는거죠.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wxa1b-VbFM8wDqS0OA-ibQ.png" /></figure><p>어떻게 분해된 렌더링 방식이 사용자 입력과 같은 이벤트 처리 문제를 해결할까요? <br>만약 <strong>렌더링 중에 새로운 사용자 입력이 발생하면 기존의 낮은 우선순위 렌더링을 잠시 멈추고 높은 우선순위 작업(입력 처리)과 페인팅을 먼저 수행</strong>합니다. 그런 다음 보류 상태였던 목록 렌더링 작업을 중단하고 새로운 작업을 우선 처리한 뒤 재개합니다.</p><p>예를 들어 사용자가 검색창에 “반응”이라고 입력했다고 가정해볼게요. React는 이 키워드에 맞는 검색 결과 목록을 렌더링하기 시작합니다. 이때 사용자가 추가로 타이핑하여 “반응형”으로 검색어를 수정했다면 어떻게 될까요?</p><p>일반적인 렌더링 방식에서는 “반응” 검색어에 대한 모든 결과가 계산되고 화면에 표시될 때까지 사용자의 추가 입력이 화면에 반영되지 않아요. 사용자는 앱이 멈춘 것처럼 느낄 수 있죠.</p><p>반면 동시성 렌더링에서는 사용자의 키보드 입력이 감지되면 진행 중이던 검색 결과 렌더링을 일시 중단하고, 사용자의 입력을 화면에 즉시 반영해요. <strong>사용자는 입력이 지연 없이 화면에 나타나는 것</strong>을 확인할 수 있고, 그 후에 React는 새로운 검색어에 맞는 결과를 렌더링합니다.</p><p>여기서 빨간색 차가 다니는 차선은 고속 차선(높은 우선순위), 초록색 차가 다니는 차선은 저속 차선(낮은 우선순위)입니다. React에서는 이를 내부적으로 레인(Lane)이라고 부르며 우선순위를 제어하는 데 사용합니다.</p><h3>레인(Lane) 모델과 동시성 렌더링</h3><p>이번 섹션에서는 레인 모델이 어떻게 동작하고, 어떻게 동시성 렌더링을 가능하게 하는지 알아보겠습니다. <br>Lane 모델은 2020년 3월에 구현되었으며(<a href="https://github.com/facebook/react/pull/18796">관련 PR</a>), Expiration Time 모델의 한계를 해결하기 위해 설계되었습니다.</p><p>“레인”은 무엇일까요? 영어로 “Lane”은 “차선”을 의미합니다. 도로에서 차선마다 다른 속도로 차량이 움직이듯이, React에서도 각 업데이트는 그 중요도에 따라 서로 다른 “차선”에 배치됩니다. 가장 중요한 업데이트는 “고속 차선”에, 덜 중요한 업데이트는 “저속 차선”에 배치되어 처리되는 방식이죠.</p><p>레인은 업데이트의 우선순위를 표시하는 것으로, 어떤 작업이 얼마나 중요한지를 나타냅니다.<strong> 이 우선순위를 사용하면 어떤 업데이트를 먼저 처리할지, 어떤 업데이트를 잠시 미룰지 결정</strong>할 수 있습니다.</p><h4>레인은 어떻게 구현되는가?</h4><p>레인은 32비트 정수로 구현되어 있습니다. 각 비트는 하나의 “차선”을 나타내며, 오른쪽에 있는 낮은 비트 위치일수록 우선순위가 높습니다. 마치 실제 도로에서 1번 차선이 가장 빠른 차선인 것과 유사하게 생각할 수 있습니다.</p><p>소스 코드에서는 레인을 다음과 같이 이진 형식으로 표현합니다.<br><a href="https://github.com/facebook/react/blob/9e3b772b8cabbd8cadc7522ebe3dde3279e79d9e/packages/react-reconciler/src/ReactFiberLane.new.js#L34-L71%3E"><em>react-reconciler/src/ReactFiberLane.new.js</em></a></p><pre>const TotalLanes = 31;<br><br>const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;<br>const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;<br><br>const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;<br><br>const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000010;<br>const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;<br><br>const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;<br>const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000010000;<br><br>const TransitionHydrationLane: Lane = /*         */ 0b0000000000000000000000000100000;<br>const TransitionLanes: Lanes = /*                */ 0b0000000001111111111111111000000;<br>const TransitionLane1: Lane = /*                 */ 0b0000000000000000000000001000000;<br><br>const RetryLanes: Lanes = /*                     */ 0b0000111110000000000000000000000;<br>const RetryLane1: Lane = /*                      */ 0b0000000010000000000000000000000;<br><br>const IdleHydrationLane: Lane = /*               */ 0b0010000000000000000000000000000;<br>const IdleLane: Lane = /*                        */ 0b0100000000000000000000000000000;<br><br>const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;</pre><p>코드를 보면 각 레인이 비트로 정의되어 있음을 알 수 있습니다. 가장 오른쪽 비트(SyncLane, 0b...001)가 가장 높은 우선순위를 가지며, 왼쪽으로 갈수록 우선순위가 낮아집니다.</p><h4>레인 모델이 비트 연산을 사용하는 이유</h4><p>레인 모델을 비트 단위로 설계한 선택에는 여러 기술적 이점이 있습니다.</p><p>먼저, 메모리 효율성이 두드러집니다. 단일 32비트 정수 하나로 최대 31개의 서로 다른 우선순위 레벨을 표현할 수 있어 복잡한 UI 업데이트 상황에서도 메모리 사용량을 최소화할 수 있습니다.</p><p>또한 비트 연산은 컴퓨터 아키텍처의 가장 기본적인 수준에서 직접 처리되기 때문에 CPU가 효율적으로 처리할 수 있습니다. 밀리초 단위의 성능이 중요한 사용자 상호작용에서 이는 큰 이점이 됩니다.</p><p>실제 개발 관점에서는 OR(|), AND(&amp;), NOT(~) 같은 비트 연산자를 활용해 여러 레인을 논리적 그룹으로 쉽게 묶거나 분리할 수 있습니다. 이를 통해 관련된 업데이트들을 함께 관리하거나 특정 업데이트만 선별적으로 처리하는 복잡한 시나리오를 간결한 코드로 구현할 수 있죠.</p><h4>이벤트와 레인의 우선순위</h4><p>레인 모델 내에는 다양한 종류의 레인이 있으며, 각각은 특정 유형의 업데이트를 처리합니다. 이 시작점은 DOM 이벤트일 수도 있고 비동기, 전환 이벤트일 수도 있으며, 레인은 하나의 이벤트에 대응합니다.</p><p>레인은 오른쪽에 있을수록 우선순위가 높으므로 비트 위치를 기준으로 우선순위를 정렬하면 다음과 같은 계층이 형성됩니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3AR91oFeL8LEUVfFDumbkg.png" /></figure><p>우선순위 체계는 사용자 경험에 대한 이해를 바탕으로 설계되었습니다. <br>예를 들어, 버튼 클릭과 같은 직접적인 사용자 입력은 가장 높은 우선순위로 처리되어 즉각적인 응답을 보장하는 반면, 대량 데이터의 필터링과 같은 무거운 작업은 낮은 우선순위로 처리되어 UI의 응답성을 유지할 수 있습니다.</p><h4>세 가지 우선순위 시스템</h4><p>React는 <strong>Lane 우선순위, 이벤트 우선순위, 스케줄러 우선순위</strong>라는 세 가지 서로 연결된 시스템을 통해 작업의 중요도를 관리합니다.</p><ol><li><strong>Lane 우선순위: </strong>업데이트의 중요도를 나타냅니다.</li><li><strong>이벤트 우선순위: </strong>사용자 이벤트의 중요도를 나타냅니다.</li><li><strong>스케줄러 우선순위: </strong>스케줄러에서 작업 예약 시 사용되는 우선순위입니다.</li></ol><p>이 세 가지 우선순위 시스템은 서로 연결되어 있으며, Lane 우선순위를 이벤트 우선순위로 변환하고, 다시 스케줄러 우선순위로 매핑하여 작업을 처리합니다.</p><p>먼저 이벤트 우선순위는 직접 특정 레인값에 매핑됩니다.<br><a href="https://github.com/facebook/react/blob/f9d78089c6ec8dce3a11cdf135d6d27b7a8dc1c5/packages/react-reconciler/src/ReactEventPriorities.js#L24C1-L28C58">react-reconciler/src/ReactEventPriorities.js</a></p><pre>export const NoEventPriority: EventPriority = NoLane;<br>export const DiscreteEventPriority: EventPriority = SyncLane;<br>export const ContinuousEventPriority: EventPriority = InputContinuousLane;<br>export const DefaultEventPriority: EventPriority = DefaultLane;<br>export const IdleEventPriority: EventPriority = IdleLane;</pre><p>레인에서 이벤트 우선순위로의 변환은 lanesToEventPriority 함수를 통해 이루어집니다.<br><a href="https://github.com/facebook/react/blob/707b3fc6b2d7db1aaea6545e06672873e70685d5/packages/react-reconciler/src/ReactEventPriorities.js#L55-L67">react-reconciler/src/ReactEventPriorities.js</a></p><pre>export function lanesToEventPriority(lanes: Lanes): EventPriority {<br>  const lane = getHighestPriorityLane(lanes);<br>  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {<br>    return DiscreteEventPriority;<br>  }<br>  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {<br>    return ContinuousEventPriority;<br>  }<br>  if (includesNonIdleWork(lane)) {<br>    return DefaultEventPriority;<br>  }<br>  return IdleEventPriority;<br>}</pre><p>lanesToEventPriority 함수는 주어진 레인 집합에서 가장 높은 우선순위를 가진 레인을 찾아 해당하는 이벤트 우선순위로 매핑합니다. 숫자가 작을수록 우선순위가 높으므로, !isHigherEventPriority(DiscreteEventPriority, lane)은 “레인이 DiscreteEventPriority보다 높거나 같은 우선순위인가?”를 확인합니다.</p><p>우선순위 결정 로직을 통해 가장 높은 우선순위부터 낮은 우선순위까지 차례로 검사하며 해당하는 이벤트 우선순위를 반환합니다. 이 과정에서 DiscreteEventPriority -&gt; ContinuousEventPriority → DefaultEventPriority → IdleEventPriority 순으로 우선순위가 결정된다는 것을 확인할 수 있습니다.</p><p>특히 흥미로운 점은 TransitionLane이 별도의 이벤트 우선순위를 가지지 않고 DefaultEventPriority로 매핑된다는 것입니다. UI 전환 작업이 사용자 직접 입력보다는 낮지만, 백그라운드 작업보다는 높은 우선순위로 처리되어야 함을 시사함을 보여줍니다.</p><p>다음으로는 결정된 이벤트 우선순위를 다시 스케줄러 우선순위로 매핑하는 과정을 거칩니다.<br><a href="https://github.com/facebook/react/blob/39cad7afc43fcbca1fd2e3a0d5b7706c8b237793/packages/react-reconciler/src/ReactFiberRootScheduler.js#L397-L435">react-reconciler/src/ReactFiberRootScheduler.js</a></p><pre>// 가장 높은 우선순위 레인 찾기<br>const newCallbackPriority = getHighestPriorityLane(nextLanes);<br><br>// 레인을 스케줄러 우선순위로 매핑<br>switch (lanesToEventPriority(nextLanes)) {<br>  case DiscreteEventPriority:<br>    schedulerPriorityLevel = ImmediateSchedulerPriority;<br>    break;<br>  case ContinuousEventPriority:<br>    schedulerPriorityLevel = UserBlockingSchedulerPriority;<br>    break;<br>  case DefaultEventPriority:<br>    schedulerPriorityLevel = NormalSchedulerPriority;<br>    break;<br>  // ... 생략 ...<br>}</pre><p>우선순위 체계는 다양한 작업의 중요도를 효과적으로 관리하여 사용자 경험을 최적화하면서도 시스템 성능을 유지할 수 있게 합니다. <br>예를 들어, 키보드 입력이나 클릭과 같은 사용자 상호작용은 높은 우선순위로 처리되어 즉각적인 반응성을 보장하고, 페이지 전환과 같은 작업은 중간 우선순위로, 데이터 프리페칭과 같은 작업은 낮은 우선순위로 처리됩니다.</p><p>특정 업데이트를 낮은 우선순위로 전환하는 startTransition API를 사용하는 이벤트 핸들러를 보면서 매핑이 어떻게 이루어지는지 생각해볼까요?</p><pre>function handleSearch(e) {<br>  // 이벤트 핸들러는 DefaultEventPriority로 실행됨<br>  setSearchQuery(e.target.value); // 즉시 업데이트 (SyncLane)<br>  <br>  startTransition(() =&gt; {<br>    // 이 안의 상태 업데이트는 TransitionLane으로 처리됨<br>    setSearchResults(searchData(e.target.value)); // 지연된 업데이트<br>  });<br>}</pre><p><strong>startTransition 내부의 상태 업데이트에는 </strong><strong>TransitionLane이 할당되지만, 이벤트 핸들러 자체는 </strong><strong>DefaultEventPriority로 처리됩니다. </strong>이렇게 함으로써 이벤트 핸들러는 적절한 시점에 실행되면서도, 실제 렌더링 작업은 낮은 우선순위로 예약됩니다. 결과적으로 전환 업데이트는 더 중요한 업데이트(e.g. 사용자 입력)가 처리된 후에만 백그라운드에서 실행됩니다.</p><p>이 두 우선순위 시스템의 분리는 이벤트 처리와 렌더링을 더 세밀하게 제어할 수 있게 해줍니다. <strong>이것이 바로 React가 멀티스레드 환경처럼 동작하면서도 사용자 경험을 우선시할 수 있는 이유</strong>입니다.</p><h4>Expiration Time 모델의 한계와 레인 모델의 장점</h4><p>레인 모델이 도입되기 전에는 Expiration Time 모델을 사용했습니다. 해당 모델에서는 각 업데이트에 시간 기반의 만료시간을 할당하여, 이 값이 작업의 우선순위와 배치 처리를 모두 결정했습니다. 예를 들어, 우선순위가 A &gt; B &gt; C인 경우, A 작업이 완료될 때까지 B나 C 작업을 시작할 수 없었습니다.</p><p>이러한 방식은 Suspense와 같은 기능이 도입되면서 한계가 드러났습니다. 예를 들어, Suspense를 통해 데이터를 가져오는 작업(IO-Bound)은 우선순위가 높더라도 데이터가 도착할 때까지 대기해야 했습니다. 이 때문에 즉시 실행 가능한 낮은 우선순위의 UI 업데이트(CPU-Bound)를 차단하는 우선순위 역전 문제가 발생했습니다. (여기서 IO-Bound 작업은 주로 네트워크 요청과 같은 입출력(I/O) 대기로 인해 지연되는 작업을 의미하고, CPU-Bound 작업은 주로 계산 처리에 의존하는 작업을 뜻합니다.) <br>또한, Expiration Time 모델에서는 우선순위와 배치 처리 개념이 단일 값에 혼합되어 있어, 다양한 유형의 작업을 세밀하게 제어하기 어려웠습니다.</p><p>반면, <strong>레인 모델은 32비트 정수와 비트 연산을 활용하여 더 세밀한 우선순위 제어를 가능</strong>하게 합니다. 각 업데이트는 고유한 레인에 할당되며, 이를 통해 서로 다른 유형의 작업(e.g. 사용자 입력, 데이터 페칭, UI 전환)을 독립적으로 스케줄링할 수 있습니다. 예를 들어, IO-Bound 작업이 데이터를 기다리는 동안 CPU-Bound 작업을 먼저 처리하여 메인 스레드를 효율적으로 활용할 수 있습니다.</p><h4>비트 연산을 활용한 레인 관리</h4><p>비트 연산을 사용하여 레인을 관리하면 다음과 같은 작업을 효율적으로 수행할 수 있습니다.</p><pre>/** <br> *가장 높은 우선순위 레인 찾기<br> * 해당 연산은 비트 집합에서 가장 낮은 위치에 있는 1비트만을 남기는 연산입니다. <br> * 이는 레인 비트 체계에서 가장 높은 우선순위를 가진 레인을 식별합니다. <br> */<br>function getHighestPriorityLane(lanes) {<br>  return lanes &amp; -lanes; <br>}<br><br>/* <br> *레인 병합하기<br> * OR 연산자(|)를 사용하여 두 레인 집합을 병합합니다. <br> * 여러 업데이트의 레인을 하나의 집합으로 쉽게 결합할 수 있습니다. <br> */<br>function mergeLanes(a, b) {<br>  return a | b; // 두 레인의 비트별 OR 연산<br>}<br><br>/* <br> * 레인 교차하기<br> * AND 연산자(&amp;)를 사용하여 두 레인 집합의 교집합을 찾습니다. <br> * 두 집합에 공통으로 존재하는 레인만 추출할 수 있습니다. <br> */<br>function intersectLanes(a, b) {<br>  return a &amp; b; // 두 레인의 비트별 AND 연산<br>}<br><br>/** <br> * 레인 제거하기<br> * AND와 NOT 연산자를 조합하여 특정 레인을 집합에서 제거합니다. <br> */<br>function removeLanes(set, subset) {<br>  return set &amp; ~subset; // subset의 비트를 반전하고 AND 연산<br>}<br><br>/**<br> * 부분집합 확인하기<br> * 한 레인 집합이 다른 집합의 부분집합인지 확인합니다. <br> * 특정 업데이트가 다른 업데이트 그룹에 포함되는지 판단하는 데 사용됩니다.<br>*/<br>function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane): boolean {<br>  return (set &amp; subset) === subset;<br>}</pre><h4>레인 모델이 동시성을 지원하는 방식</h4><p>레인 모델의 핵심 기능 중 하나는<strong> 현재 렌더링 중인 작업보다 더 높은 우선순위의 업데이트가 발생했을 때, 이전 렌더링을 중단하고 새 업데이트를 처리할 수 있는 능력</strong>입니다. 이 기능은 getNextLanes 함수에서 구현됩니다.<br><a href="https://github.com/facebook/react/blob/39cad7afc43fcbca1fd2e3a0d5b7706c8b237793/packages/react-reconciler/src/ReactFiberLane.js#L226-L342">react-reconciler/src/ReactFiberLane.js</a></p><pre>export function getNextLanes(root: FiberRoot, wipLanes: Lanes, rootHasPendingCommit: boolean,<br>): Lanes {<br>  // 보류 중인 작업이 없으면 일찍 반환<br>  const pendingLanes = root.pendingLanes;<br>  if (pendingLanes === NoLanes) {<br>    return NoLanes;<br>  }<br><br>  // ... 중략 ...<br><br>  // 이미 렌더링 중이라면, 새 레인이 더 높은 우선순위인 경우에만 전환<br>  if (wipLanes !== NoLanes &amp;&amp; wipLanes !== nextLanes) {<br>    const nextLane = getHighestPriorityLane(nextLanes);<br>    const wipLane = getHighestPriorityLane(wipLanes);<br>    if (<br>      // nextLane이 wipLane보다 우선순위가 같거나 낮은지 확인<br>      nextLane &gt;= wipLane ||<br>      // 기본 우선순위 업데이트는 전환 업데이트를 중단해서는 안 됨<br>      (nextLane === DefaultLane &amp;&amp; (wipLane &amp; TransitionLanes) !== NoLanes)<br>    ) {<br>      // 기존 진행 중인 작업을 계속 진행. 중단하지 않음.<br>      return wipLanes;<br>    }<br>  }<br><br>  // ... 중략 ...<br><br>  return nextLanes;<br>}</pre><p>getNextLanes은 현재 진행 중인 렌더링(wipLanes)보다 우선순위가 높은 업데이트가 있는지 확인합니다. 만약 더 높은 우선순위의 업데이트가 발생하면, 현재 렌더링을 중단하고 새로운 업데이트를 처리합니다. 이것이 바로 <strong>React의 ‘중단 가능한 렌더링’의 핵심 메커니즘</strong>입니다.</p><h4>만료 시간 관리</h4><p>업데이트가 너무 오래 지연되는 것을 방지하기 위해 만료 시간도 관리합니다.</p><pre>function computeExpirationTime(lane: Lane, currentTime: number) {<br>  switch (lane) {<br>    case SyncLane:<br>    case InputContinuousLane:<br>      // 사용자 상호작용은 더 빨리 만료되어야 함<br>      return currentTime + 250;<br>    case DefaultLane:<br>    case TransitionLane:<br>      return currentTime + 5000;<br>    // ... 기타 케이스<br>  }<br>}</pre><p>사용자 상호작용 관련 레인은 250ms 후에 만료되는 반면, 기본 및 전환 레인은 5000ms 후에 만료됩니다. 이를 통해 사용자 입력에 더 빠르게 응답할 수 있습니다.</p><h4>얽힘(Entanglement) 메커니즘</h4><p>또 다른 중요한 개념은 “얽힘(Entanglement)”입니다. 얽힘은 관련된, 혹은 의존적인 업데이트들이 함께 처리되도록 보장합니다.<br><a href="https://github.com/facebook/react/blob/39cad7afc43fcbca1fd2e3a0d5b7706c8b237793/packages/react-reconciler/src/ReactFiberLane.js#L959-L988">react-reconciler/src/ReactFiberLane.js</a></p><pre>export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {<br>  // 직접적으로 얽힌 레인뿐만 아니라, 전이적으로 얽힌 레인도 고려<br>  // 만약 C가 A와 얽혀 있고, A가 B와 얽히게 되면, C도 B와 얽힘<br><br>  const rootEntangledLanes = (root.entangledLanes |= entangledLanes);<br>  const entanglements = root.entanglements;<br>  // ... 얽힘 관계 처리 로직<br>}</pre><p><strong>얽힘의 핵심 특징은</strong> <strong>전이적 속성</strong>입니다. 이는 한 레인이 다른 레인과 얽히면, 그와 관련된 모든 레인이 자동으로 서로 얽히게 되는 특성을 의미합니다. 예를 들어, 레인 C가 이미 레인 A와 얽혀 있고, A가 레인 B와 얽히게 된다면, C도 자동으로 B와 얽히게 됩니다. 이런 방식으로 복잡한 상태 업데이트 간의 관계를 효율적으로 추적하고, 관련된 모든 변경 사항이 UI에 일관되게 반영되도록 보장할 수 있습니다.</p><p>만약 useTransition이나 startTransition을 사용할 때, 해당 범위 내의 모든 상태 업데이트는 서로 얽히게 됩니다. <strong>이를 통해 전환 과정에서 발생하는 모든 업데이트가 동일한 우선순위로 처리</strong>되며, 부분적으로 완료된 불완전한 UI 상태가 사용자에게 보이지 않도록 합니다. <br>사용자가 검색 필터를 변경할 때를 생각해보세요. 필터 UI와 검색 결과가 동시에 업데이트되어야 하는데, 얽힘 메커니즘이 없다면 필터 UI는 즉시 바뀌고 검색 결과는 지연될 수 있습니다. 이 경우 사용자는 잠깐 동안 최신 필터와 맞지 않는 오래된 검색 결과를 보게 되어 혼란스러울 수 있습니다. 하지만 얽힘을 통해 이런 불일치를 방지하고, UI가 항상 일관된 상태를 유지하도록 보장합니다.</p><p>지금까지 간단하게 React의 세 가지 우선순위 시스템과 레인 모델에 대해서 알아봤습니다. <br>레인 모델의 가장 큰 장점은 <strong>우선순위에 따라 작업을 처리할 수 있다는 점</strong>입니다. 버튼 클릭이나 키보드 입력 같은 사용자의 직접적인 상호작용은 높은 우선순위로 즉시 처리하고, 대량의 데이터 필터링과 같은 무거운 계산은 낮은 우선순위로 미룰 수 있죠.</p><p>또한 <strong>이미 진행 중인 렌더링을 필요에 따라 중단</strong>할 수 있게 해줍니다. 예를 들어 사용자가 검색 결과를 필터링하는 도중에 새 키를 입력하면, 진행 중이던 필터링을 잠시 멈추고 키 입력을 먼저 처리한 후 다시 필터링을 계속할 수 있습니다. 중단 가능한 렌더링 덕분에 UI가 버벅거리지 않고 부드럽게 동작합니다.</p><p><strong>비트 연산을 통한 세밀한 우선순위 제어</strong>도 가능합니다. 덕분에 복잡한 업데이트 시나리오에서도 각 작업의 중요도를 효율적으로 관리할 수 있습니다.</p><p>다음 섹션에서는 동시성을 실제로 어떻게 활용하여 사용자 경험을 개선할 수 있는지 알아보겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2ebBqYOG6tqb-DswWta1yg.png" /></figure><h3>useDeferredValue와 useTranstion</h3><p>React 18에서 추가된 대표적인 API는 useDeferredValue와 useTransition입니다. <strong>이 두 API는 복잡하거나 무거운 UI 업데이트를 처리할 때 애플리케이션의 응답성을 유지하는 데 도움</strong>을 줍니다.</p><p>웹 애플리케이션이 점점 복잡해지면서 프론트엔드 엔지니어는 대량의 데이터를 처리하거나 복잡한 계산을 수행하면서도 UI가 반응적으로 유지되어야 하는 과제에 직면하고 있어요. 종종 사용자가 입력 필드에 타이핑하거나 UI 요소와 상호작용을 할 때, 이러한 무거운 작업이 메인 스레드를 차단하면 화면이 버벅거리고 사용자 경험이 저하되는 경험을 할 때가 있습니다.</p><p>먼저 이 API들이 왜 필요한지 이해하기 위해, 흔히 마주치는 문제 상황을 살펴보겠습니다.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodesandbox.io%2Fembed%2Fgtw6kl&amp;display_name=CodeSandbox&amp;url=https%3A%2F%2Fcodesandbox.io%2Fp%2Fsandbox%2Fgtw6kl&amp;image=https%3A%2F%2Fcodesandbox.io%2Fapi%2Fv1%2Fsandboxes%2Fgtw6kl%2Fscreenshot.png&amp;type=text%2Fhtml&amp;schema=codesandbox" width="1000" height="500" frameborder="0" scrolling="no"><a href="https://medium.com/media/a4f61d912f663e645c3262694e15bff8/href">https://medium.com/media/a4f61d912f663e645c3262694e15bff8/href</a></iframe><p>이 코드에선 사용자가 입력 필드에 타이핑할 때마다 generateGridData 함수가 호출되어 무거운 계산 작업을 수행합니다. 레인 모델 관점에서 이 과정을 살펴보면,</p><ol><li>사용자가 키보드로 타이핑하면 onChange 이벤트가 발생합니다.</li><li>setNormalInput 호출은 <strong>SyncLane</strong>(최고 우선순위)에 할당됩니다.</li><li>상태 업데이트로 때문에 컴포넌트가 다시 렌더링되고, 데이터 계산이 동일한 <strong>SyncLane</strong>에서 수행됩니다.</li></ol><p>계산이 완료될 때까지 브라우저는 다음 사용자 입력을 처리할 수 없게 되며, 이 때문에 사용자는 타이핑 시 심각한 지연을 경험하게 됩니다. 각 키 입력 후에 계산이 완료될 때까지 기다려야 다음 키를 입력할 수 있기 때문이죠. 이는 매우 답답한 사용자 경험을 만듭니다.</p><h4>useDeferredValue</h4><p>useDeferredValue는 UI 응답성을 유지하면서 무거운 렌더링 작업을 처리할 수 있게 해줍니다. 이<strong> 훅은 값의 &#39;지연된 버전&#39;을 생성</strong>하여, 무거운 계산 작업은 브라우저가 여유 있을 때 처리하도록 합니다.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodesandbox.io%2Fembed%2F5xhdgv&amp;display_name=CodeSandbox&amp;url=https%3A%2F%2Fcodesandbox.io%2Fp%2Fsandbox%2F5xhdgv&amp;image=https%3A%2F%2Fcodesandbox.io%2Fapi%2Fv1%2Fsandboxes%2F5xhdgv%2Fscreenshot.png&amp;type=text%2Fhtml&amp;schema=codesandbox" width="1000" height="500" frameborder="0" scrolling="no"><a href="https://medium.com/media/b6ed9fa19e8fdc633b9ad234513ffc2d/href">https://medium.com/media/b6ed9fa19e8fdc633b9ad234513ffc2d/href</a></iframe><p>이번엔 deferredValue를 사용하여 무거운 계산 로직을 최적화했습니다. 수정된 코드는 어떻게 작동하는지 살펴볼까요?</p><ol><li>사용자가 입력할 때 setDeferredInput 호출은 여전히 <strong>SyncLane</strong>(최고 우선순위)에 할당됩니다.</li><li>해당 고우선순위 작업을 즉시 처리하여 입력 필드를 업데이트합니다.</li><li>deferredValue 값은 내부적으로 <strong>TransitionLane</strong>(낮은 우선순위)에 할당됩니다.</li><li>무거운 계산 작업(generateGridData)은 이제 deferredValue를 기반으로 하므로, <strong>TransitionLane</strong>의 우선순위로 처리됩니다.</li><li>사용자가 계속 타이핑하면 진행 중인 계산 작업을 잠시 중단하고 새로운 입력을 먼저 처리합니다.</li></ol><p>useDeferredValue는 내부적으로 두 개의 렌더링 패스를 생성합니다. 첫 번째 패스에서는 deferredInput만 업데이트하고, 두 번째 패스에서는 deferredValue를 업데이트합니다. 이 두 번째 패스는 <strong>TransitionLane</strong>에서 처리되므로, 사용자 입력과 같은 높은 우선순위 작업에 방해되지 않습니다.</p><p>isDeferredPending = deferredInput !== deferredValue 구문은 현재 <strong>TransitionLane</strong> 작업이 진행 중인지 확인하는 효과적인 방법입니다. 두 값이 다른 경우, 지연된 렌더링이 아직 완료되지 않았다는 의미입니다.</p><blockquote><strong>⚠️주의사항</strong><br>useDeferredValue는 API 호출에는 적절하지 않습니다. useDeferredValue를 통해 지연되는 타이밍은 React가 내부적으로 결정하는 것으로, 주로 브라우저가 유휴 상태일 때 처리됩니다. 만약 deferredQuery를 기반으로 API 호출을 하게 되면, React의 내부 스케줄링에 따라 예상치 못한 빈도로 API가 호출될 수 있습니다. API 호출을 제어하려면 debounce나 throttle과 같은 기법을 사용하는 것이 더 적절합니다.</blockquote><h4>useTransition</h4><p>useDeferredValue가 값에 초점을 맞춘다면, useTransition은 상태 업데이트 자체에 초점을 맞춥니다. 이 API를 사용하면 <strong>특정 상태 업데이트를 명시적으로 낮은 우선순위로 표시</strong>할 수 있습니다.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodesandbox.io%2Fembed%2Fxywycf&amp;display_name=CodeSandbox&amp;url=https%3A%2F%2Fcodesandbox.io%2Fp%2Fsandbox%2Fxywycf&amp;image=https%3A%2F%2Fcodesandbox.io%2Fapi%2Fv1%2Fsandboxes%2Fxywycf%2Fscreenshot.png&amp;type=text%2Fhtml&amp;schema=codesandbox" width="1000" height="500" frameborder="0" scrolling="no"><a href="https://medium.com/media/93e288be5551aab0b90206ef28760d9a/href">https://medium.com/media/93e288be5551aab0b90206ef28760d9a/href</a></iframe><p>handleTransitionInput 함수가 호출되면, 다음과 같은 과정이 진행됩니다.</p><ol><li>setTransitionInput(value)는 <strong>SyncLane</strong>(최고 우선순위)에 배치됩니다.</li><li>startTransition 내부의 setTransitionGrid(generateGridData(value))는 <strong>TransitionLane</strong>(낮은 우선순위)에 배치됩니다.</li><li><strong>SyncLane</strong>에 있는 transitionInput 업데이트를 먼저 처리하고, 이후에 <strong>TransitionLane</strong>에 있는 transitionGrid 업데이트를 처리합니다.</li><li>isPending 상태 변수는 현재 <strong>TransitionLane</strong> 작업이 진행 중인지 알려줍니다.</li></ol><p>여기서 startTransition의 역할은 &quot;이 상태 업데이트는 <strong>TransitionLane</strong>에 배치해 주세요&quot;라고 지시하는 것입니다. 명시적으로 우선순위를 낮추고 싶을 때 useTransition API를 사용합니다.</p><h4>useDeferredValue vs useTransition</h4><p>useDeferredValue와 useTransition은 각각 다른 방식으로 지연 처리를 구현합니다.</p><ul><li>useDeferredValue는 특정 값의 업데이트를 지연시켜, 해당 값에 의존하는 렌더링 작업을 TransitionLane에 배치합니다. 무거운 계산이나 렌더링을 간단히 지연시키고 UI 응답성을 유지하려는 경우 적합합니다.</li><li>useTransition은 상태 업데이트를 명시적으로 TransitionLane에 배치하여 지연 처리합니다. 여러 상태 업데이트를 그룹화하거나 지연 동작을 세밀히 제어할 때 유용합니다.</li></ul><p>두 API는 동일한 레인 모델을 기반으로 작동하지만, 사용 목적에 따라 선택됩니다. 단일 값의 지연 처리가 필요하면 useDeferredValue가 간편하고, 복잡한 상태 업데이트를 관리하려면 useTransition이 적합합니다. 일반적으로 한 가지 API만으로도 충분한 경우가 많습니다.</p><blockquote><em>위 예시의 전체 코드를 확인하고 실제로 동작하는 데모를 경험하고 싶다면, </em><a href="https://codesandbox.io/p/sandbox/concurrent-mode-2tfwzf"><em>CodeSandbox 데모</em></a><em>를 참조하세요. 각 입력 필드에 타이핑해보면 일반 모드와 동시성 모드의 차이를 명확하게 느낄 수 있을 것입니다.</em></blockquote><h3>리멤버에서의 실제 적용 사례</h3><p>마지막으로 리멤버의 제품에서 동시성 기능을 적용하여 사용자 경험을 개선했던 사례를 공유해 드리고 마치도록 하겠습니다. 리멤버 채용솔루션 제품 중 하나인 헤드헌팅 PRO는 전통적인 노동집약적이고 아날로그 방식의 헤드헌팅 산업을 혁신하여, 기술 기반의 디지털 솔루션으로 재정립함으로써 업무 생산성을 향상한다는 미션을 가지고 있어요.</p><p>이에대한 기술적 솔루션으로 헤드헌팅 PRO는 사용자가 수백만 개의 방대한 데이터를 직관적이고 효율적으로 필터링할 수 있는 기능을 제공하고 있어요. 복잡한 검색 조건에서도 사용자가 마치 간단한 작업을 수행하는 것처럼 느낄 수 있도록 최적화된 인터페이스와 사용자 경험을 구현하여, 헤드헌터들이 후보자 데이터를 보다 신속하고 정확하게 탐색할 수 있도록 지원하고 있습니다. (리멤버의 채용에도 이 서비스가 사용되고 있답니다! 😁)</p><p>우리 웹프론트엔드팀은 복잡한 검색 조건을 직관적으로 표현하는 인터페이스를 구성하고, 웹 기술을 통한 향상된 사용자 경험을 제공하여 인재를 더 효과적으로 찾을 수 있도록 노력하고 있습니다. 이러한 노력의 일환으로 검색 성능 최적화를 진행했으며, <strong>이 과정에서 도입한 React의 동시성 기능이 어떻게 사용자 경험을 개선했는지</strong> 도입 전/후를 비교하며 실제 적용 사례를 살펴보겠습니다.</p><pre>export const useSearch = () =&gt; {<br>  const [params, setParams] = useSearchParams();<br>  const searchController = useController(SearchController, {<br>    params,<br>    onUpdate: applyMiddleware([clearPage], (nextParams) =&gt; {<br>      setParams(nextParams);<br>      Storage.syncWithStorage(nextParams);<br>    }),<br>  });<br>  <br>  const controller = useMemo(<br>    () =&gt; searchController,<br>    [JSON.stringify(searchController.getResults())]<br>  );<br>  <br>  return controller;<br>};</pre><p>기존 코드에서는 검색 조건이 변경될 때마다 <strong>setSearchParams를 통해 URL이 즉시 변경되고, 로컬 스토리지도 함께 업데이트 되었습니다</strong>. 이러한 작업들이<strong> 모두 메인 스레드에서 동기적으로 처리되면서 UI가 버벅거리는 현상</strong>이 발생했습니다. 특히 사용자가 여러 필터를 빠르게 전환하는 경우, 각 필터 클릭마다 URL 변경과 관련된 무거운 작업들이 즉시 실행되어 화면이 순간적으로 멈추곤 했습니다.</p><p>또한 URL 변경과 관련된 작업들이 아직 완료되지 않았는데도 UI가 먼저 변경되어 사용자는 필터가 적용된 것처럼 보였지만 , 실제 데이터와 화면은 그 후에야 변경되는 문제도 있었습니다. 이 때문에<strong> 사용자는 실제 전환 속도보다 필터링이 더 느리게 적용된다고 느꼈고</strong>, 때로는 UI 상태와 데이터 상태 사이에 불일치가 발생하기도 했죠.</p><p><strong>이 문제를 해결하기 위해</strong> useTransition<strong>을 도입했습니다.</strong> 코드를 함께 살펴보겠습니다.</p><pre>export const useSearch = () =&gt; {<br>  const [isPending, startTransition] = useTransition();<br>  const [params, setParams] = useSearchParams();<br>  const searchController = useController(SearchController, {<br>    params,<br>    onUpdate: applyMiddleware([clearPage], (nextParams) =&gt; {<br>      // startTransition을 적용한 부분<br>      startTransition(() =&gt; {<br>        setParams(nextParams);<br>        Storage.syncWithStorage(nextParams);<br>      });<br>    }),<br>  });<br>  <br>  const controller = useMemo(<br>    () =&gt; searchController,<br>    [JSON.stringify(searchController.getResults())]<br>  );<br>  <br>  return { ...controller, isPending };<br>};</pre><p>가장 중요한 변화는 URL 업데이트와 로컬 스토리지 동기화 작업을 startTransition 함수로 감싸는 것입니다. 이 변경의 <strong>핵심 아이디어는 &quot;데이터 변경을 먼저 완료하고, 그 후에 UI 업데이트를 낮은 우선순위로 처리한다&quot;</strong> 였습니다.</p><p>사용자가 필터 버튼을 클릭하면 컨트롤러를 통해 어떤 데이터가 필요한지 즉시 계산하게 됩니다. 이 계산은 빠르게 이루어지므로 메인 스레드를 오래 점유하지 않아요. 그런 다음 <strong>계산된 결과를 바탕으로 URL 업데이트와 스토리지 동기화 같은 무거운 작업들은</strong> startTransition <strong>내부에서 낮은 우선순위로 처리하게 됩니다</strong>. 이렇게 하면 급한 사용자 입력이 있으면 진행 중이던 낮은 우선순위 작업이 일시 중단되고, 사용자 입력이 먼저 처리될 수 있습니다.</p><p>isPending 상태를 활용해 트랜지션이 진행 중인지 여부를 UI에 반영할 수도 있었습니다. 예를 들어, 필터 버튼의 활성 상태를 계산할 때 isPending을 함께 고려하여 트랜지션이 완료되기 전까지는 버튼이 활성화되지 않도록 했습니다. 이를통해<strong> 사용자에게 더 일관된 UI 상태를 제공</strong>하고, 데이터와 UI 간의 불일치를 방지하는 효과를 볼 수 있었습니다.</p><pre>const FilterButton = ({ type, label }) =&gt; {<br>  const { controller, update, isPending } = useSearch();<br>  const isActive = !isPending &amp;&amp; controller.getFilter(&#39;type&#39;) === type;<br>  <br>  return (<br>    &lt;Button <br>      isActive={isActive}<br>      onClick={() =&gt; {<br>        if (!isActive) {<br>          update(ctrl =&gt; ctrl.setFilter(&#39;type&#39;, type).getParams());<br>        }<br>      }}<br>    &gt;<br>      {label}<br>    &lt;/Button&gt;<br>  );<br>};</pre><p>변경을 적용한 후의 사용자 인터페이스가 훨씬 더 부드러워졌어요. 여러 필터를 빠르게 연속해서 클릭해도 UI가 버벅거리지 않았고, 항상 일관된 상태를 유지할 수 있었죠. 이전에는 UI가 변경된 후에도 데이터 처리가 지연되어 체감 속도가 느렸지만, 이제는 <strong>UI 업데이트와 데이터 처리가 함께 완료되므로 사용자는 변경이 즉시 적용되는 것처럼 느끼게 되었습니다.</strong></p><p>단 몇 줄의 코드 변경으로 마법 같은 성능 개선을 이룬 것처럼 보이지만, 이 간단한 훅 사용 뒤에는 React 팀의 세심한 설계가 있었습니다. 혹시 여러분도 버벅이는 UI로 고민하고 계신다면, 이처럼 간결하면서도 효과적인 솔루션을 한번 시도해보는 건 어떨까요?</p><h3>마치며</h3><p>이번 글에서는 React의 렌더링 패러다임에서 일어난 근본적인 변화인 <strong>동시성 렌더링</strong>을 살펴보고, 주요 API의 활용 방법도 함께 탐구했습니다.</p><p>내부 코드를 들여다보며 느낀 점은, 기술의 발전이 단순한 성능 향상을 넘어 사용자 경험을 우선시하는 방향으로 나아가고 있다는 것입니다. 특히, 사용자 상호작용의 중요도에 따라 작업의 우선순위를 정교하게 관리하는 방식과, 모든 UI 업데이트가 동등하지 않다는 인식을 코드에 반영한 접근법에서 인사이트를 얻을 수 있었습니다.</p><p>가끔 일상의 코딩에서 멈춰 서서 <strong>“이 기술이 본질적으로 해결하려는 것은 무엇일까?”</strong>라는 질문을 던져보는 것도 의미 있을 것입니다.</p><p>이제 저희 팀의 채용 홍보로 글을 마무리 짓도록 하겠습니다. <strong>리멤버 웹프론트엔드팀에서는 사용자 중심의 가치 있는 서비스를 효율적으로 구현하기 위한 기술을 개발하고 다양한 실험에 도전할 열정적인 동료를 찾고 있습니다</strong>. 저희와 함께 다양한 문제에 도전하고 싶으신 분은 언제든 <a href="https://hello.remember.co.kr/job_posting/5HqLRCwJ">채용공고</a>에 지원 해주세요!</p><p>감사합니다😀</p><h3>참고자료</h3><p><a href="https://youthfulhps.dev/react/react-concurrent-mode-01/#%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98-%EC%96%91%EB%B3%B4">리엑트 동시성 매커니즘들은 어떻게 구현되어 있을까 — 01</a><br><a href="https://tv.naver.com/v/23652451">NAVER D2</a><br><a href="https://goidle.github.io/react/in-depth-react18-intro/">React 18 톺아보기 — 01. Intro | Deep Dive Magic Code</a><br><a href="https://www.youtube.com/watch?v=5MZbkow5U8o&amp;t=1355s">Tejas Kumar — I Wrote a Book on React — Things You Should Know</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5ff18aee148d" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/%EC%BD%94%EB%93%9C-%ED%95%9C-%EC%A4%84%EB%A1%9C-%EA%B2%BD%ED%97%98%ED%95%98%EB%8A%94-react-%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%98-%EB%A7%88%EB%B2%95-5ff18aee148d">코드 한 줄로 경험하는 React 동시성의 마법</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  29.        </item>
  30.        <item>
  31.            <title><![CDATA[리멤버 개발실을 소개합니다]]></title>
  32.            <link>https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EA%B0%9C%EB%B0%9C%EC%8B%A4%EC%9D%84-%EC%86%8C%EA%B0%9C%ED%95%A9%EB%8B%88%EB%8B%A4-e765b99be68d?source=rss----307f82e3ebfb---4</link>
  33.            <guid isPermaLink="false">https://medium.com/p/e765b99be68d</guid>
  34.            <category><![CDATA[리멤버]]></category>
  35.            <category><![CDATA[engineering]]></category>
  36.            <category><![CDATA[remember]]></category>
  37.            <dc:creator><![CDATA[HanByul Lee]]></dc:creator>
  38.            <pubDate>Mon, 24 Mar 2025 13:24:34 GMT</pubDate>
  39.            <atom:updated>2025-03-25T01:45:47.205Z</atom:updated>
  40.            <content:encoded><![CDATA[<p>안녕하세요? 저는 일하는 프로들과 비즈니스 기회를 연결하는 리멤버의 제품본부 개발실에서 리더 역할을 맡고 있는 이한별이라고 합니다. 😁</p><p>제가 리멤버에 합류한 지 5년이 지나가는데, 그동안 상당히 많은 제품 변화도 있었지만 저희 조직의 변화도 있었는데요. 이번 글에서는 그러한 변화들을 거쳐 온 현재의 개발실의 모습을 <strong>가볍게</strong> 소개드리고자 합니다.</p><h3>매트릭스 조직 구조</h3><p>오늘날의 리멤버의 제품본부는 매트릭스 조직 구조라고도 표현되는 조직 구조로 구성돼 있어요.</p><h4>기능 조직(팀)</h4><p>1개의 팀은 같은 직무를 가진 구성원들의 집합이에요.</p><p>현재 개발실에는 백엔드 팀, 웹 프론트엔드 팀, iOS 팀, Android 팀, DevOps 팀의 5개 팀으로 구성돼 있습니다.</p><h4>목적 조직(크루)</h4><p>1개의 크루는 Product Owner(PO), Product Manager(PM), Product Designer(PD), Quality Assurance(QA), Data Intelligence(DI), Software engineers(개발자) 가 서로 다른 직무를 가졌지만 하나의 목적을 공유하고 업무를 추진하는 조직이에요.</p><p>현재 리멤버 제품본부는</p><ul><li>Foundation crew</li><li>채용혁신 crew</li><li>구직Wow crew</li><li>신사업 crew (<a href="https://app.rmbr.in/6KJCEl6P0Rb">리멤버 커넥트</a>)</li><li>자소설 crew</li></ul><p>이렇게 5개의 크루를 조직하여 운영하고 있어요.</p><p>크루는 회사의 상황에 따라 신규 결성이 되거나 목적이 바뀌는 것처럼 유연성을 갖고 있습니다. 한 개 크루에서는 평균적으로 15명의 동료들로 구성돼 있어요.</p><p>이렇게 매트릭스 조직구조로 돼 있는 것을, 개발실이라는 기능 조직 관점에서 도식화해보면 이런 모습으로 나타낼 수 있어요.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dFz6P3v5qEjmpTZ_Sjcwxg.png" /></figure><p>크루와 팀은 각각 목표를 설정하고, 업무를 진행하게 됩니다.</p><p>한 명의 개발자는 각 팀과 크루에 동시에 속해있다보니, 일하다 보면 우선순위 판단이 어렵거나 지금 해도 되는지와 같은 고민에 빠지게 되기도 하는데요. 조직 전체 차원에서 최대한 이 고민을 덜기 위해 시간 분배를 통해서 균형을 맞추고 있습니다. 균형을 맞추기 위한 여러 장치들이 있어요. 😉</p><h3>개발실 분위기는</h3><p>다양하고 많은 사람들이 모여있기 때문에 딱 “어떻다! 이렇다!” 라고 설명하기는 어렵지만 평상시의 대화들이 존중을 바탕으로 하는 수평적이에요.</p><p>직급이 없는데다 &lt;Tech Lead&gt; 나 &lt;팀 Leader&gt; 역할과 같이 직함이 있는 일부 동료들을 제외하면 주니어든 시니어든 따로 구분하지 않고 더 나은 방법을 찾으면서 일하고 있어요.</p><p>즉, 좀 더 경험이 많다고 해서 그 분의 의견이 무조건 맞다고 고집 부리지도 않고, 경험이 좀 더 부족한 분이 내린 의사결정에 대해서도 한 번 내린 결정이라면 다른 동료들도 스스로 얼라인할 줄 알고 따르는 분위기랄까요? 그런 존중과 신뢰 기반으로 논의를 하고, 대화를 합니다.</p><h3>어떤 사람들이 있나요?</h3><p>개발자들이 하는 업무는 “문제 풀이&quot; 라고 표현하고 싶습니다.</p><p>문제 풀이에서 나오는 문제들은 항상 쉽지만도, 항상 어렵지만도 않은데요. 그렇지만 어려운 문제를 풀어야 할 때는 지치기도 하고, 힘들기도 합니다. 저는 개인적으로 이 과정속에서 어떻게든 해내고야 말겠다는 긍정적인 사고방식을 가지신 분들이라면 이 문제 풀이에 유리함을 가졌다는 견해를 갖고 있는데요.</p><p>저희 리멤버 개발실에는 모두 이런 긍정적인 사고방식을 가지신 분들로 구성돼 있다고 자신있게 말씀드릴 수 있습니다.</p><p>이러한 표현이 &lt;어떤 사람들이 있는지?&gt; 에 대한 속 시원한 답은 안 될 거란 걸 알기에 몇몇 개발실 동료분의 특징들도 남겨놓아 봅니다. 🙂</p><ul><li>개발 서적 출판한 사람</li><li>오픈소스 Contributor</li><li>글로벌 기업 출신</li><li>이모지(emoji) 장인</li></ul><h3>일하는 방식</h3><p>말로 설명하기보다는 그림으로 보여드리는 것이 더 이해하기 좋을 것 같아 그림 그리는 것에 전혀 재능이 없지만 용기내 그려서 설명드려 보겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/960/1*TGdqtwbb1HRdps-xe5A79g.png" /><figcaption>리멤버 개발실에서 일하는 개발 파이프라인</figcaption></figure><p>우선, 위 그림과 같은 순서대로 일이 만들어지고, 개발을 진행하게 됩니다.</p><p>그림에서는 다 표현못하는 중간중간 커뮤니케이션이라든지, 다른 프로세스들(like 회고, A/B테스트, 성과 측정 후 다시 기획 등), 로고로 표시못한 다른 도구들도 있지만 큰 틀에서는 이 순서대로 개발하고 있습니다.</p><p>위 그림의 거의 모든 단계에서 개발자가 참여하고 있으며 직무와 무관하게, 필요하다면 개발자가 직접 소통을 하면서 불명확한 부분을 제거하고, 더 나은 설계와 기획이 되도록 끊임없이 소통하면서 일하고 있어요.</p><p>그림도 그리고 텍스트로도 설명을 해보았지만, 역시나 충분한 설명이 되지는 않는 것 같은데요. 좀 더 상세하게 궁금하신 경우, 저에게 커피챗하자고 말씀해주시면 제가 연락드리고 설명드리도록 하겠습니다!</p><h3>어떤 문화가 있나요?</h3><p>리멤버 개발실의 문화를 어떻게 설명드려야 할 지 고민이 되는데요.</p><h4>&lt;Over communication&gt;</h4><p>이건 개발실의 문화라기보다는 리멤버 전사의 문화인데요.</p><p>명확하게 소통하기 위한 수단으로써 over communication 을 적극적으로 활용하고 권장합니다!!</p><p>추상적인 over communication 을 한다는 것의 구체적인 사례로는 어떤 것들이 있는지를 나열하자면 끝도 없겠지만, 대표적으로는</p><ul><li>구두로 나눈 대화를 텍스트로 정리하고 모두가 볼 수 있는 곳에 게시하는 것</li><li>누가 물어보지 않아도 기록해두고 공유하기(나중에 누군가 또는 미래의 자기 자신에게 도움이 되리라 믿고!)</li></ul><p>로 설명드릴 수 있어요.</p><p>저도 영어를 잘 못하지만, FTR(For the record)라는 표현이 있고 자주 사용하는데요.</p><p>여러 뜻이 있는 표현이지만, 저희 회사에서는 메시지를 쓴 목적이 말 그대로 “기록을 위한 것&quot;이라는 것을 드러내기 위해 말머리로 많이 달곤 한답니다.</p><p>처음엔 한 두 명이 자주 쓰던 표현인데 이 간편한 표현의 참맛을 알게 된 분들께서 점점 더 많이 사용해주셔서 문화화가 되었답니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/810/1*F5m5vkv2G-_ztVBxKJN1xw.png" /><figcaption>Over communication 을 실천하는 동료분들</figcaption></figure><h4>&lt;My user manual&gt;</h4><p>‘나&#39;라는 사람을 사용하는 방법!!! 을 작성하고 나를 사용할 동료들에게 나눠주는 개발실만의 문화에요. 이 문화의 시작에는 제가 있어서 개인적으로 뿌듯하기도 한 문화인데요.</p><p>이런걸 하지 않더라도 일하다보면 서로가 서로를 알게 되겠지만, 아직 직접 겪지 않더라도 상대방의 가치관이나 선호하는 소통 방식, 일하는 방식이 무엇인지를 미리 알 수 있다면 더 안정감을 갖고 일할 수 있을 것이란 생각으로 시작하게 된 것이에요.</p><p>좀 더 구체적으로 이해를 돕기 위해 제가 작성한 저의 My user manual 의 일부 내용을 첨부해 봅니다. 😂</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UEw4Wgxx6iNfW71c2zXOew.png" /><figcaption>이한별의 my user manual 일부</figcaption></figure><h4>&lt;테크톡&gt;</h4><p>우선, 개발실에서만 단독으로 진행하는 것은 아니고 전사의 개발자, 엔지니어라 할 수 있는 분들이 모두 모여 한 달에 한 번씩 진행하는 테크톡을 진행하고 있습니다.</p><p>주로 신규 입사자 소개, 특정 주제로 발표(a.k.a. Remember’s Nerd), 밍글링 활동 등을 진행해요.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/276/1*_MjAasXgNeVuJAnzPojBJw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fuu-oPbaovKih5GvfaOP8Q.png" /><figcaption>테크톡 현장 사진</figcaption></figure><h4>&lt;주 1회 팀 시간&gt;</h4><p>이 시간을 어떻게 불러야 할 지 모르겠지만, 매주 금요일 오후 1시부터 3시까지 기능 조직에서 서로의 지식과 경험을 공유하는 시간이에요. 때로는 회고를 진행하며 서로의 경험과 생각을 나눕니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Djm2CmXg4-X106ztQX2egw.png" /></figure><h4>&lt;코드 리뷰&gt;</h4><p>문화라고 불러도 될 지 모르겠지만, 코드 리뷰를 업무의 주요 프로세스 중 하나로 인식하고 있으며 꽤 많은 시간과 에너지를 사용하고 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/877/1*oLEwJna684NwV9kSErFf3Q.png" /><figcaption>웹 프론트엔드 팀의 PR 리뷰 알리미</figcaption></figure><h3>현재의 리멤버 개발실에서는</h3><h4>&lt;현재까지의 성장과 앞으로의 방향성&gt;</h4><p>제가 처음 리멤버에 합류했던 2019년 12월에는 사내의 개발자는 18명 규모였던 것으로 기억하는데요.</p><p>약 5년이 지난 지금은 43명 규모의 팀으로 성장해 있습니다.</p><p>그 사이에 회사도 아주 많이, 빠르게 변했고 앞으로도 변할 것으로 기대하는데요.</p><p>당장은 아니지만 지금보다 개발자의 규모도 가파르게 성장해 있을 것이고, 그 과정에서 풀어야 하는 기술적/제품적인 문제의 복잡도도 더 올라가 있으리라 생각해요.</p><p>이 문제들을 풀다보면 많은 개발자 분들께서 ‘나의 성장’과 ‘회사의 성장’이 일치하는 경험을 하실 수 있으리라 확신합니다.</p><p>이 성장의 변화 속에서 변화에 적응하면서, 기존의 가치들은 잘 지키고 여기에 더 좋은 가치들을 제품에 녹여내는 것이 중요한 상황을 맞이하고 있습니다.</p><p>말은 참 쉬운데 빠른 변화속에서 이렇게 하는 것이 어렵고 도전적으로 느껴지는데요.</p><p>현재의 개발 속도를 유지하고, 나아가 더 빠르게 해야 하며 기술 경쟁력도 높이면서도 사용자와 고객에게는 더 만족하고 사용하실 수 있게 안정적으로, 세심하게 다룬다는 것은 그저 이상적인 이야기로 취급되기도 하는데요.</p><p>저는 우리 리멤버의 제품을 만드는 제품본부와 개발실은 이 어려운 것을 해낼 수 있다고 믿습니다. 도전적인 만큼 이것을 해냈을 때에 얼마나 기쁘고 뿌듯할 지가 벌써부터 기대가 되고 가슴이 뛰어요. 🤩</p><p>역시나 이런 소개글의 마무리는 채용 홍보로 돼야 뻔하지만 속이 시원한 것 같습니다. 😂</p><p>저희 리멤버의 <a href="https://hello.remember.co.kr/">채용 소개 페이지</a>를 소개드리는 것을 끝으로 마칩니다.</p><p>읽어주셔서 감사합니다. 🤗</p><p>앞으로도 저희 리멤버 개발실에 많은 관심 가져주시고 응원해주시길 부탁드립니다! 🫡</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e765b99be68d" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EA%B0%9C%EB%B0%9C%EC%8B%A4%EC%9D%84-%EC%86%8C%EA%B0%9C%ED%95%A9%EB%8B%88%EB%8B%A4-e765b99be68d">리멤버 개발실을 소개합니다</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  41.        </item>
  42.        <item>
  43.            <title><![CDATA[사용자 모르게 리멤버 UI icon 개선하기]]></title>
  44.            <link>https://tech.remember.co.kr/%EC%82%AC%EC%9A%A9%EC%9E%90-%EB%AA%A8%EB%A5%B4%EA%B2%8C-%EB%A6%AC%EB%A9%A4%EB%B2%84-ui-icon-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-12b2274761b8?source=rss----307f82e3ebfb---4</link>
  45.            <guid isPermaLink="false">https://medium.com/p/12b2274761b8</guid>
  46.            <category><![CDATA[product-design]]></category>
  47.            <category><![CDATA[design]]></category>
  48.            <category><![CDATA[ui-design]]></category>
  49.            <dc:creator><![CDATA[Heekyeong Kim]]></dc:creator>
  50.            <pubDate>Thu, 20 Feb 2025 05:22:37 GMT</pubDate>
  51.            <atom:updated>2025-02-20T10:03:22.519Z</atom:updated>
  52.            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J6mzj45IdwQ1MwSeaJOkEw.png" /></figure><h3>배경</h3><h4>2022년 리멤버 리브랜딩</h4><p>리멤버는 2022년 7월 ‘기회가 열린다, 리멤버’라는 슬로건과 함께 명함 앱을 넘어 비즈니스의 다양한 기회가 열리는 직장인 슈퍼앱으로 나아가고자 리브랜딩이 진행되었습니다.</p><p>이 과정에서 리브랜딩 심볼의 큰 변화가 있었는데요. 명함을 연상시키는 기존 사각형을 벗어나 다양한 기회가 열리는 ‘문’을 형상화한 스퀘어로 진화했고, 이 스퀘어가 리멤버 R의 한 획을 받쳐주면서 대표성 있는 새로운 심볼로 탄생하게 되었습니다.</p><figure><img alt="리멤버 심볼" src="https://cdn-images-1.medium.com/max/1024/1*RJetgfwYxods2-mweiw8hA.png" /><figcaption>리멤버 심볼</figcaption></figure><p>리멤버 스퀘어는 서비스 곳곳에 반영이 되었고, UI icon에도 역시 리멤버 스퀘어가 적용이 되었습니다.</p><p>리브랜딩과 함께 개발된 UI icon은 기존의 Material design을 참고하여 디자인 된 ver1의 아이콘보다 확실한 개성을 가진 형태로 완성되었습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-lt-r-Y06D-zdVItRDh-gQ.png" /><figcaption>리멤버 UI icon ver 1.5</figcaption></figure><p>리브랜딩과 함께 새롭게 만들어진 ver1.5의 리멤버 UI icon은 Filled type의 아이콘이라는 점과 모서리의 깎임 효과 그리고 -3°의 기울임이라는 큰 특징들이 있었습니다. 각각의 특징 역시 리멤버 스퀘어라는 메타포에서 가져온 특징으로 리멤버의 브랜드 아이덴티티를 시각화 하는데 있어 두드러지는 포인트라고 볼 수 있었습니다.</p><p>새롭게 제작된 UI icon은 2022년 7월 리멤버 서비스 전체에 적용되었습니다.</p><h3>그런데 문제가 있었습니다.</h3><h4>Filled 타입과 Outline 타입의 밸런스 차이</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pBtfeJyLAciCw_D16z3i_w.png" /></figure><p>ver1.5의 아이콘은 Filled 타입을 기본으로 디자인 되었습니다. 그러나 Filled 타입으로 표현할 수 없는 아이콘이 존재하기도 하는데요.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u3vyyjODMF5GSXcKPJDemg.png" /><figcaption>Fill type과 Outline type이 혼재되었던 as-is</figcaption></figure><p>동일한 영역에 Filled 타입과 Outline 타입의 아이콘이 함께 사용되면 같은 아이콘 셋트임에도 불구하고 밸런스 차이가 나다보니 하나의 아이콘 셋트처럼 보이지 않는 문제가 있었습니다.</p><h4>아이콘이 찌그러져 보여요</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NCztFuCnFR6w4jwbtQqZdg.png" /></figure><p>아이콘들 자체가 -3°의 기울임과 모서리 깎임 효과들이 적용되어 있는데, 사용자 눈에는 의도된 디자인이 아니라 잘못된 형태로 인지되기 시작했습니다.</p><p>가령 네비게이션 아이콘의 기울여진 각도가 아이콘이 잘못 틀어져 있다고 인지한다던가 작은 사이즈의 Filled 타입 아이콘의 모서리가 깎인 부분을 사용자는 찌그러진 형태로 본다던가 하는 부분이었습니다.</p><p>새로운 아이콘이 적용된 서비스의 배포 비율이 올라가면서 관련 VOC도 증가하기 시작했습니다.</p><h4>UI icon을 만드는데 너무 많은 시간이 필요해요</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Nqiwup-6vZqq_6imp7-CEQ.png" /><figcaption>as-is 아이콘 제작 프로세스</figcaption></figure><p>유저들의 사용성 문제 외에도 또다른 문제가 있었습니다. 새로운 기능들이 생기다보면 기존의 아이콘이 아닌 새로운 아이콘이 추가로 필요할때가 있는데요. 새로운 아이콘들은 가이드에 맞춰 제작하면 되는데 이 가이드가 너무나 복잡하고 번거로운 과정들을 거쳐야 하는것이 문제였습니다.</p><p>아이콘을 제작하기 위해서는</p><ol><li>Figma에서 아이콘의 원형을 그린 후</li><li>Adobe illustrator의 아이콘 가이드 파일로 옮겨</li><li>모서리를 깎고</li><li>-3°의 기울임을 준 뒤</li><li>SVG로 내보내야 합니다.</li><li>SVG 파일을 다시 Figma의 디자인 시스템에 등록하고,</li><li>작업중이던 화면에 적용해야</li></ol><p>비로소 아이콘 1개의 제작이 완료되는 과정이었습니다.</p><h3>개선 방향</h3><h4>폰트와 UI icon의 상관관계</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aMmYbbL8Ym1S1B8YOyzp-A.png" /></figure><p>UI icon을 개선하기에 앞서 중요한 두가지를 먼저 짚고 넘어가야 했습니다.</p><ol><li>UI icon이 어떤 환경에서 쓰이는지 다시 생각해보자</li><li>UI icon의 기능적인 역할을 충실히 하면서도 리멤버 브랜드 가치를 담을 수 있는 방향을 고민하자</li></ol><p>UI icon은 대부분의 화면에서 폰트와 함께 사용됩니다. 리스트라던지 버튼이라던지 앱바와 탭바를 제외하면 대부분 폰트와 함께 나열되어 사용된다고 봐도 될 정도이기 때문에 폰트와의 밸러스가 매우 중요해 집니다.</p><p>브랜드를 설명할 수 있는 요소들은 심볼도 있겠지만 서비스 전반에 사용되는 컬러, 폰트 또한 브랜드를 설명할 수 있습니다. 게다가 아이콘은 대부분 폰트와 함께 사용될테니 리멤버 브랜드 가치가 담긴 폰트와 닮은 아이콘을 만드는것이 가장 적절하다고 판단했습니다.</p><p>이미 다양한 글로벌 브랜드들이 폰트에 맞춘 아이콘을 제작하여 서비스에 반영중인 사례들도 있었기 때문에 글로벌 기업들의 공식문서(<a href="https://www.ibm.com/design/language/iconography/ui-icons/usage">IBM Design Language — UI icons</a> , <a href="https://m3.material.io/styles/icons/overview">Icons — Material Design 3</a>)를 참고해서 서비스에 적용된 폰트를 기반으로 UI icon을 다시 제작하기로 했습니다.</p><p>또한 아이콘 제작의 리소스를 절감하기 위해 프로덕트 디자이너 누구나 가이드를 보고 피그마안에서 UI icon을 빠르게 만들 수 있도록 가이드 문서와 작업용 템플릿 파일을 만들기로 했습니다.</p><h4>서비스에 적용된 폰트를 기반으로 UI icon 디자인 하기</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wq_Ayv51h8mEZ8rwHd5dMQ.png" /></figure><p>리멤버는 기본 시스템 폰트대신 커스텀 폰트를 사용하며 Pretendard를 지정서체로 사용하고 있습니다. (참고 : <a href="https://tech.remember.co.kr/pretendard-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%8F%B0%ED%8A%B8-%EB%8F%84%EC%9E%85%EA%B8%B0-beece1515ebc">Pretendard 커스텀 폰트 도입기</a>)</p><p>프리텐다드를 사용하는 기술적인 이유도 있지만 리멤버의 기업 가치 관점에서 왜 사용하는지를 조금 더 들여다보고 해석하여 리멤버 브랜드 관점에서 볼 수 있는 프리텐다드의 특징들을 골라내 컨셉에 맞는 아이콘 디자인을 진행했습니다.</p><h4>Grid Concept</h4><p>리멤버에서 아이콘과 폰트과 사용되는 여러가지 사례들을 모아보았고, 다수의 상황에서 Midium 굵기의 폰트와 아이콘이 가장 많이 사용된다는 사실을 발견했습니다. 폰트의 weight나 크기에 따라 아이콘도 weight와 크기가 함께 맞춰지면 가장 좋겠지만 제작 리소스와 현재의 상황을 고려하여 가장 많이 사용되는 weight를 기준으로 아이콘 weight를 결정했고, 그 기준으로 가이드를 제작했습니다.</p><h4>Icon Style</h4><p>가이드의 일부 내용은 다음과 같습니다.</p><ul><li><strong>Base grid</strong> : 리멤버 아이콘은 32px X 32px의 픽셀 기반 그리드에 그려지며 1px 단위의 격자를 기본 가이드로 사용합니다.</li><li><strong>Padding</strong> : 그리드에는 4px의 패딩이 포함되어 있습니다. 이렇게 하면 아이콘을 내보낼 때 원하는 배율과 주변 공백이 유지됩니다.</li><li><strong>Key shapes</strong> : 키 라인은 아이콘 세트 전체에서 기본 모양 또는 비율에 대해 일관된 크기를 제공합니다.</li><li><strong>Style</strong> : 리멤버 아이콘의 형태적 특징은 리멤버의 아이덴티티인 카드에서 볼 수 있는 각진 형태와 Pretendard 폰트에서 볼 수 있는 형태적 특징에서 가져왔습니다. 때문에 아이콘 모서리는 radius를 적용하지 않습니다.</li><li><strong>Stroke</strong> : 리멤버 아이콘은 리멤버 시스템 폰트인 Pretendard와 밸런스를 맞추도록 합니다. 각 아이콘의 stroke는 리멤버 앱에서 아이콘과 가장 많이 사용되는 Title2의 두께와 맞춰져 있습니다.</li><li><strong>Angles</strong> : 리멤버 아이콘에서 일관된 형태를 가져가기 위해 45° 각도를 사용합니다. 필요 시 15°의 각도를 활용합니다.</li></ul><p>가이드를 문서화 하고 팀에 공유하여 공식화 하는 과정을 거쳐 기존의 아이콘들을 개선했고, 점진적으로 각 디바이스 와 OS에 적용하게 되었습니다.</p><h3>리멤버 UI icon은 이렇게 개선되었습니다.</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*At7J7dLdG-16NCHGsRpnmQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2DTforh_kNfUHAqbr2UeLg.png" /></figure><h4>AS-IS / TO-BE</h4><ul><li>사용자에게 오류로 보였던 기울기와 모서리 깍임 요소를 제거하였습니다.</li><li>리멤버 제품에 사용된 폰트와의 조화를 위해 폰트의 시각적 요소를 참고하여 아이콘에 반영하였습니다.</li><li>누구나 쉽게 이해하고 아이콘을 만들 수 있도록 명확한 가이드와 템플릿을 만들었습니다.</li></ul><h4>개선 효과</h4><ol><li><strong>UI icon 관련 VOC 감소</strong> : 배포 이후 UI icon과 관련된 VOC가 0건으로, 사용자에게 UI icon으로 인해 사용성을 저해하는 경험은 없어졌다고 봐도 될 수치였습니다.</li><li><strong>제작 리소스 절감</strong> : 사용자 뿐만 아니라 그동안 여러 단계를 거쳐 아이콘을 제작해야했던 프로덕트 디자인 팀에도 좋은 변화가 생겼습니다. 아이콘 개선 이후 체감 작업량이 기존 대비 3배 이상 감소했다고 평가했습니다.</li></ol><h3>글을 마치며</h3><p>2022년 리브랜딩 이후 적용된 UI icon을 개선하는 부분은 사실 조심스럽기도 했습니다. 그럼에도 불구하고 개선해야 했던 이유는 명확했습니다.</p><blockquote><em>사용자 경험을 해치면서까지 브랜드 가치를 전달할 수 있는 요소는 없다</em></blockquote><p>이번 UI icon 개선은 브랜드 가치를 제품에 어떻게 담아낼 것인지 다시 한 번 고민해 볼 수 있는 시간이었습니다.</p><p>또한, 사용자에게 UI의 변화는 ‘불편할 때’ 비로소 체감하게 되는 요소라는 것을 확인할 수 있었습니다. 마치 공기처럼 자연스러울 때에는 의식하지 못하다가 눈에 거슬리거나, 어딘가 불편하다고 느끼는 순간 의식하게 되고 신경쓰이는 것 처럼요.</p><p>명함앱을 넘어 다양한 비즈니스 기회를 만들 수 있는 서비스로 성장중인 리멤버의 브랜드 가치를 사용자가 서비스를 경험하며 더욱 체감할 수 있도록 프로덕트 디자이너 관점에서 계속해서 고민해야 하는 숙제라는 생각이 들었습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=12b2274761b8" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/%EC%82%AC%EC%9A%A9%EC%9E%90-%EB%AA%A8%EB%A5%B4%EA%B2%8C-%EB%A6%AC%EB%A9%A4%EB%B2%84-ui-icon-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-12b2274761b8">사용자 모르게 리멤버 UI icon 개선하기</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  53.        </item>
  54.        <item>
  55.            <title><![CDATA[리멤버에서 UT(사용자 테스트)는 어떻게 진행하나요?]]></title>
  56.            <link>https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84%EC%97%90%EC%84%9C-ut-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A7%84%ED%96%89%ED%95%98%EB%82%98%EC%9A%94-b3755ce073d2?source=rss----307f82e3ebfb---4</link>
  57.            <guid isPermaLink="false">https://medium.com/p/b3755ce073d2</guid>
  58.            <category><![CDATA[user-interviews]]></category>
  59.            <category><![CDATA[user-experience-design]]></category>
  60.            <category><![CDATA[user-experience]]></category>
  61.            <category><![CDATA[design]]></category>
  62.            <category><![CDATA[ux-research]]></category>
  63.            <dc:creator><![CDATA[Heekyeong Kim]]></dc:creator>
  64.            <pubDate>Mon, 17 Feb 2025 04:45:50 GMT</pubDate>
  65.            <atom:updated>2025-02-20T04:14:27.404Z</atom:updated>
  66.            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KkBsmBBbnaGKAA1NL39kKw.png" /></figure><p>안녕하세요. 리멤버 프로덕트 디자이너 김희경입니다.</p><p>명함관리 서비스로 시작해 구인구직 서비스와 직장인 커뮤니티 서비스, 데일리 뉴스 콘텐츠까지 직장인을 위한 다양한 서비스를 리멤버에서 제공해왔는데요. 이 모든 서비스를 한 화면에서 경험할 수 있는 새로운 화면을 신설하기에 앞서 사용성 테스트(Usability Test)를 진행했었어요. 새로운 피쳐가 배포되기 이전에 사용성 테스트를 진행하며 경험한 UT 설계와 모더레이팅 과정에서 느낀 인사이트를 공유드릴게요.</p><h3>UT(사용성 테스트)는 언제 진행하나요?</h3><p>UT 즉 사용성 테스트는 신규 기능을 기획하는 단계 또는 신규 기능을 론칭하기 전 실제 리멤버 사용자에게 보여드리면서 우리 생각대로 사용하시는지, 문제점은 없는지 검증하기 위해 진행합니다.</p><p>특히 이번처럼 첫 화면을 신규로 만드는 경우는, 기존 대비 사용자가 불편하게 느끼시는 부분은 없는지 사전에 확인하는 것이 매우 중요했어요.</p><h3>UT 설계</h3><p>UT는 ‘설계가 80%다.’라는 생각이 들 정도로 설계 과정이 매우 중요하다고 생각해요.</p><p>UT 대상자는 누구를 어떤 과정으로 모집할 것인지, 인터뷰는 온라인으로 할 것인지, 오프라인으로 할 것인지, 인터뷰 관찰자는 어떤 방식으로 참여할지 등등 준비할 것이 생각보다 많았습니다.</p><p>그럼 어떤 순서와 과정을 거쳐 UT 설계를 했었는지 알려드릴게요.</p><h4>01. 인터뷰 대상자 선정</h4><p>사용자 행위에 영향을 미치는 조건을 리스트업 합니다.</p><ul><li>우리 서비스를 사용한 기간이 얼마나 되었는지</li><li>그중 어떤 서비스를 중점적으로 사용하는지</li><li>우리 서비스를 방문하는 주기는 어떻게 되는지</li><li>IT 기기 조작이 익숙한 사람인지</li></ul><p>조건에 따라 사용자군을 나누기도 하고 하나의 사용자군을 대상으로 인터뷰를 진행할 수 있습니다.</p><p>이번 인터뷰에서는 사용자 행위에 따라 군을 나누어, 총 4개의 사용자군을 대상으로 인터뷰를 진행했어요.</p><h4>02. 인터뷰 대상 리크루팅</h4><p>우리가 선정한 대상자들에게 인터뷰 정보를 안내하고 모집합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PDleM_e4lxiqSyv-.png" /><figcaption>Tally를 활용해서 인터뷰 대상자에게 사용성 테스트 요청을 진행</figcaption></figure><ul><li>인터뷰 환경 안내 : 온라인인지 오프라인인지</li><li>일정 스케줄링 : 가능한 날짜와 시간을 모두 받은 뒤 스케줄 조정 후 안내</li><li>인터뷰 동의 항목 안내 및 동의 받기</li><li>인터뷰 사례금 안내</li></ul><p>인터뷰 모집에 응답을 주신 분들 중에서 직무나 추가 조건 및 다양성을 고려하여 인터뷰 대상자를 선정했습니다.</p><h4>03. 역할 분담</h4><p>이번 인터뷰는 신규 피처를 함께 기획했던 PO와 PD가 협업하여 진행했으며 아래의 역할을 나눠 UT를 준비했습니다.</p><ul><li>인터뷰 설계 및 모더레이팅</li><li>인터뷰 대상자 리쿠르팅 및 사례금 지급 담당자</li><li>서기, 관찰자</li></ul><h4>04. Task 작성</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xPQCtTmqCzpRSHBV.png" /><figcaption>사용자 인터뷰 진행에 사용되었던 Task 초안</figcaption></figure><p>사용성 테스트를 통해 확인하고 싶은 내용을 중심으로 Task를 설계하고 성공 기준과 실패 기준을 정합니다.</p><p>신규 화면에서도 기존에 자주 사용하던 기능을 빠르게 찾을 수 있는지, 몰랐던 기능들을 어떻게 인지하고 사용하게 되는지 등 검증하고 싶은 내용을 플로우 별로 나누고 Task와 질문을 작성했습니다.</p><p>실제 인터뷰에서는 사용자군에 따라 Task의 순서를 변경하고 자연스러운 진행을 위해 사용자에게 몰입할 수 있는 시나리오와 함께 Task를 안내했습니다.</p><h4>05. 프로토타입 준비</h4><p>인터뷰에서 검증하고 싶은 가설을 확인할 수 있는 형태로 프로토타입을 준비합니다. 참고로 모든 기능이 동작하지 않아도 됩니다. 이번에는 인터뷰 대상 사용자군 별로 Task를 진행할 수 있는 프로토타입을 준비하고, Task 내용에 따라 프로토타입 접속 계정도 다르게 세팅하여 인터뷰 대상자에게 제공했습니다.</p><h4>06. 리허설 진행</h4><p>실제 환경과 동일한 환경을 조성하고 사내에서 인터뷰 대상자를 1명을 선정하여 진행했습니다.</p><p>이때 관찰자로 디자인팀 동료들을 초대하여 인터뷰 과정을 지켜보고 이후 피드백을 받았습니다. 피드백을 바탕으로 인터뷰 대본 수정과 Task의 추가 질문들을 수정하여 실제 인터뷰를 진행하게 되었습니다.</p><p>실제 인터뷰를 진행하기 전 리허설 인터뷰는 필수로 진행하는 것을 추천해요. 예상하지 못한 현장에서의 변수들을 확인할 수 있고, 제3자의 입장에서 인터뷰 질문에 대한 피드백을 받을 수 있어 현장에서 당황하지 않을 수 있었습니다.</p><h4>07. 인터뷰 환경 세팅</h4><p>이번 UT는 사용자가 직접 인터뷰 장소로 방문하는 오프라인 대면 인터뷰로 진행했어요. 오프라인 대면 인터뷰를 위한 환경 세팅은 다음과 같았습니다.</p><ul><li>미팅룸 세팅</li><li>인터뷰 대상자의 행동을 관찰할 수 있도록 촬영용 카메라 세팅</li><li>인터뷰 대상자가 어떤 플로우로 Task를 진행하는지 확인할 수 있도록 테스트 기기의 화면 공유 세팅</li><li>진행자가 질문을 볼 수 있고, 서기가 답변을 작성할 수 있으며 관찰자가 메모를 할 수 있는 인터뷰 대상자의 정보가 포함된 질문지 준비</li></ul><p>인터뷰를 진행하는 모더레이터와 서기는 오프라인 미팅룸에 인터뷰 대상자와 직접 대면하여 질문을 하며 메모를 작성하고, 관찰자는 구글 밋을 통해 공유되는 화면으로 인터뷰 과정을 관찰했습니다. 관찰자 입장에서도 중간중간 궁금한 점이 있으면 공유된 시트에 추가 질문을 작성할 수 있도록 해서 모더레이터가 질문들을 확인 할 수 있도록 준비했습니다.</p><h3>UT 진행</h3><p>저는 이번 사용성 테스트에서 UT 설계와 모더레이팅을 담당했어요. UT를 진행하는 모더레이터 입장에서 인터뷰를 어떻게 진행하고 이 과정에서 유의해야 할 부분들을 알려드릴게요.</p><h4>01. 인터뷰 안내 및 사전 질문</h4><p>사전에 미리 인터뷰 방식에 대해 안내하지만 인터뷰를 시작 전 대상자에게 인터뷰 과정에 대해 한번 더 안내합니다. 또한 현장에서 궁금해하실 수 있는 촬영에 대한 부분과 관찰자가 어떤 방식으로 어떻게 참여하는지에 대해서도 안내를 합니다.</p><ul><li>지금 진행되는 인터뷰가 어떤 방식으로 진행되는지</li><li>촬영 동의 받기 및 관찰자 존재 안내</li><li>인터뷰 시작 전 스몰토크를 진행하여, 인터뷰 대상자의 배경과 상황을 파악하고 라포(rapport)를 형성</li></ul><p>인터뷰 시작 전 스몰토크를 하며 어떻게 인터뷰에 참여하게 되었는지 우리 서비스에 대해 평소에 어떻게 생각하고 사용하셨는지 대해 이야기를 나누기도 했습니다. 짧은 대화지만 우리가 의도한 리크루팅 대상자가 맞는지 스크리닝이 가능하고, 미리 준비해둔 Task와 질문을 가감하는 판단을 할 수 있습니다.</p><h4>02. 인터뷰 진행</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CxO_LZ_xHquI7h9P.png" /><figcaption>실제 인터뷰 당시 모더레이터 화면에 띄워두었던 화면 세팅</figcaption></figure><p><strong>충분히 관찰하기</strong></p><p>참여자마다 Task를 수행하는 시간이 다를 수 있기 때문에 모더레이터는 충분히 인터뷰 대상자가 Task를 수행하도록 기다립니다. 이 과정에서 서기는 사용자가 Task를 수행하는데 시간이 얼마나 걸렸는지, 어떤 부분에서 머뭇거리는지 어떤 과정을 거쳐 Task를 수행하는지 수행하는 과정에서 어떤 질문을 하는지 등등 모든 과정을 꼼꼼하게 관찰하고 기록합니다. 모더레이터는 실시간으로 서기가 기록하는 내용을 보면서 다음에 어떤 질문을 할지 미리 준비합니다.</p><p><strong>질문하기</strong></p><p>수행 과정에서 발견된 부분을 중심으로 행동에 대한 이유를 묻습니다. 이때 유도 질문을 하지 않도록 주의합니다. 실제 인터뷰를 진행할 때는 이 점에 유의해서 사용자가 직접 자신의 행동 이유나 감정 느낀 점을 설명할 수 있는 질문을 중점적으로 했습니다.</p><p>이때 준비된 질문 외에도 관찰자가 추가로 요청하는 질문을 물어보기도 합니다.</p><p><strong>대답하기</strong></p><p>인터뷰를 진행하다 보면 인터뷰 참가자가 제품에 대한 질문을 하기도 합니다. 이때는 바로 답변을 하기보다 ‘왜’ 그런 질문을 하게 되었는지 역으로 질문하여 사용자가 진짜 하고 싶은 이야기가 무엇인지 파악하려 했습니다. 충분히 의중을 파악한 후에 사용자가 궁금해했던 질문에 답변을 하거나 인터뷰가 완전히 끝난 뒤에 답변을 드리기도 했습니다.</p><h4>03. 인터뷰 종료</h4><p>준비된 Task를 진행하며 인터뷰가 충분히 진행이 되었다고 판단되면 인터뷰 대상자에게 종료 안내를 합니다. 이때 인터뷰 내용은 어떻게 보관되고 관리되는지, 사례금은 어떤 방식으로 전달되는지 한 번 더 안내를 드립니다.</p><h4>04. 인터뷰 종료 후</h4><p>인터뷰 종료 직후 모더레이터와 서기는 방금 진행된 인터뷰에 대해 정리하는 시간을 가집니다. 인터뷰 과정에서 부족한 부분이나 개선점은 없었는지 이야기하고 이후 진행되는 인터뷰에서 참고할 부분들을 확인합니다. 시간 여유가 있다면 실시간으로 작성했던 인터뷰 내용을 정리하기도 합니다.</p><h3>UT 분석 및 인사이트 도출</h3><p>인터뷰 종료 후 녹화본과 작성했던 관찰 문서를 토대로 UT 내용을 분석합니다.</p><p><strong>Task 달성 결과 지표 분석</strong></p><ul><li>각 Task 별로 사전에 정의한 성공 지표를 기준으로 인터뷰 대상자들이 얼마나 성공했는지, 공통적으로 보인 행동은 어떠했는지 내용을 작성하고 분석합니다.</li></ul><p><strong>Key Findings</strong></p><ul><li>관찰 문서를 토대로 공통적으로 발견된 현상들을 정리합니다.</li></ul><p><strong>인사이트 도출</strong></p><ul><li>위의 내용들을 조합하여 현재 피처에서 유지하는 부분과 개선이 필요한 부분들을 파악합니다.</li></ul><h3>UT 이후 어떤 부분이 개선되었을까요?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pQhxVvcPhzJS_DWQ.png" /></figure><p>UT를 통해 변경된 대표적인 예는 ‘오늘의 소식’ 영역입니다.</p><p>저희는 매일 사용자가 가장 먼저 확인해야 할 소식을 가장 잘 보이는 곳에 안내함으로써 빠르게 소식을 확인하고 상세 내용에 접근할 수 있도록 의도했지만 실제 UT에서는 사용자가 해당 영역 자체를 인지하지 못하거나 광고로 인지하는 경우가 많았습니다.</p><p>이후 기존안과 해당 영역을 다른 콘텐츠와 동일한 형태로 제공하는 안을 비교하는 A/B 테스트를 진행했습니다. 기존의 상단 영역에서 안내할 때보다 콘텐츠 형태로 제공했을 때 60% 더 많은 전환율(노출 고객 대비 클릭 률)이 일어나는 것을 확인하고 개선안을 100% 배포하는 결정을 할 수 있었습니다.</p><h3>글을 마치며</h3><p>UX리서처없이 리멤버 프로덕트 디자이너는 어떻게 사용자 테스트를 하는지 소개했어요. 리멤버에서는 UT뿐만 아니라 설문조사, 인터뷰, A/B 테스트 등 다양한 리서치를 진행하며 사용자를 좀 더 이해하고 사용자를 위한 의사결정을 하기 위한 노력들을 하고 있어요. 앞으로도 더 많은 사용자의 피드백을 통해 발전하는 리멤버를 기대해 주세요.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b3755ce073d2" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84%EC%97%90%EC%84%9C-ut-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A7%84%ED%96%89%ED%95%98%EB%82%98%EC%9A%94-b3755ce073d2">리멤버에서 UT(사용자 테스트)는 어떻게 진행하나요?</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  67.        </item>
  68.        <item>
  69.            <title><![CDATA[RDS MySQL에서 RDS Aurora로 DB이전 다운타임 최소화 하기]]></title>
  70.            <link>https://tech.remember.co.kr/rds-mysql%EC%97%90%EC%84%9C-rds-aurora%EB%A1%9C-db%EC%9D%B4%EC%A0%84-%EB%8B%A4%EC%9A%B4%ED%83%80%EC%9E%84-%EC%B5%9C%EC%86%8C%ED%99%94-%ED%95%98%EA%B8%B0-9cd3e887caf8?source=rss----307f82e3ebfb---4</link>
  71.            <guid isPermaLink="false">https://medium.com/p/9cd3e887caf8</guid>
  72.            <category><![CDATA[aws-aurora]]></category>
  73.            <category><![CDATA[database-migration]]></category>
  74.            <dc:creator><![CDATA[Remember tech]]></dc:creator>
  75.            <pubDate>Tue, 14 Jan 2025 02:43:30 GMT</pubDate>
  76.            <atom:updated>2025-01-15T01:58:33.960Z</atom:updated>
  77.            <content:encoded><![CDATA[<p>얼마 전 저희 <a href="http://rememberapp.co.kr">리멤버</a> 의 DB서버 이전이 있었습니다. 기존엔 AWS RDS에서 MySQL을 사용하고 있었으나 AuroraDB로 서버 이전을 하였고, 손쉽게(?) 작업을 마무리 할 수 있었습니다. 이전을 할 때 데이터 소실없이 이전 하는 것이 첫 번째로 중요했고, 두 번째로 중요했던 건 서비스의 다운타임을 최소화 하는 것 이었습니다. 첫 번째로 중요했던 데이터의 소실 없이 이전 하는건 철저한 검증을 통해 확인과 복원을 하면 되었지만, 두번째 중요했던 다운타임 최소화 문제는 조금 난감했습니다. DB이전과 함께 여러가지 밀려있던 작업을 위해 주말 새벽 5시간의 서비스 다운타임을 이용자들에게 공지했고, 저희는 이 시간 안에 예정된 모든 작업을 끝내야 했습니다. 결과적으로 공지 했던 시간 보다 2시간을 단축해 3시간만에 모든 작업을 끝낼 수 있었습니다. DB이전 작업엔 거의 시간이 들지 않았으니까요.</p><p>DB이전을 할 때 다운타임을 최소화 하기 위한 방법은 놀라울 정도로 간단합니다.</p><ol><li>RDS MySQL의 최신 스냅샷을 Migrate기능을 이용해 AuroraDB로 이전</li><li>Migrate하는 동안의 데이터 변경분처리. (AuroraDB를 Replica server로 활용)</li><li>각 Applications에서 DB Endpoint를 AuroraDB의 Endpoint로 변경.</li></ol><p>참 쉽죠? 2번에 대해 좀 더 정리해보자면, AuroraDB가 migrate되는 시간 동안 기존에 사용하던 MySQL DB에는 계속 데이터가 변경되고, 쌓여가고, 사라지고 있을 겁니다. Migrate하는 시간 동안의 데이터 Gap은 AuroraDB의 Migrate작업이 완료 된 후 이를 MySQL Master DB의 replica server화 하여 두 서버간 데이터의 동기화 합니다.</p><p>사실, AWS에서 이러한 방법에 대해 도큐먼트를 제공하고 있긴 하지만 자세한 방법까지는 다루고 있지 않기 때문에 이 글에서는 조금 전 설명드린 과정에 대해 그 방법을 풀어 볼 생각입니다.</p><h3>0. 시작하기 전에</h3><p>본격적으로 서버 이전을 진행하기 전에 RDS의 MySQL에서 한 가지 설정을 바꿔주고 시작 해야 하는데, “binlog retention hours”라는 RDS의 configuration 값을 바꿔 주어야 합니다. AWS Console의 Parameter Groups의 항목이 아니기때문에 브라우저가 아닌 터미널을 열어서 MySQL에 Master계정으로 접속을 해 주세요. RDS MySQL은 기본적으로 binlog를 삭제하는 주기가 빠르기 때문에 AuroraDB에 Migrate하는 동안의 binlog는 기록되어 보관될 수 있도록 설정 값을 바꿔야합니다. 아래와 같은 프로시저를 호출 해주세요.</p><blockquote><em>call mysql.rds_set_configuration(‘binlog retention hours’, 48);</em></blockquote><p>혹시나 하는 마음에 binlog 보관 주기를 48시간으로 설정 했지만 24시간이면 충분 할 것 같습니다. <strong><em>위 설정을 변경하기 전 스토리지가 충분한지 확인 후 변경하시기 바랍니다.</em></strong></p><h3>1. AuroraDB 인스턴스 생성</h3><p>우선 AuroraDB의 인스턴스를 생성해 보겠습니다. 생성 시 기존에 사용하던 MySQL Master DB의 최신 Snapshot을 이용해 AuroraDB로 Migrate합니다. MySQL MasterDB를 선택하고, 상단의 Instance Actions탭에서 “Take Snapshot”을 눌러 현재 시점의 스냅샷을 준비해주세요.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/289/0*FQRvech0a7SStwfy.png" /></figure><p>스냅샷이 준비되었다면, “Migrate Latest Snapshot”를 눌러줍니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/611/0*86SM85QH7Io-mN1v.png" /></figure><p>다음과 같은 화면이 나오면 기본적인 설정을 해 줍니다. 기존 MySQL Master DB의 설정을 따라가기 때문에 DB Instance Identifier와 Availability Zone지정 외에 별다른 작업이 필요 없을 것 같습니다.</p><p><strong><em>자, Migrate버튼을 누르기 전에 이쯤에서 정말 중요한 메모를 하나 할 것입니다.</em></strong> 이 메모를 한 후 재빠르게 Migrate버튼을 눌러 AuroraDB인스턴스를 생성 할 겁니다. MySQL에 접속한 후 다음과 같은 명령어를 통해 binary log 정보를 확인합니다. <strong><em>File과 Position을 잘 메모해 둡니다.</em></strong></p><blockquote><em>SHOW MASTER STATUS;</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/670/0*42FbpbeWx8b7A3GL.png" /></figure><p>확인 하셨나요? 그러면 재빨리 Migrate버튼을 눌러 인스턴스를 생성합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/192/0*VQk3oXQMIsobmgQH.png" /></figure><p>마이그레이션이 완료 되고 인스턴스가 생성될 때 까지 잠시(?) 기다려 줍니다. <a href="https://aws.amazon.com/ko/blogs/aws/now-available-amazon-aurora/">아마존 블로그</a> 의 문구를 인용해보자면, 소요되는 시간은 대략 다음과 같습니다.</p><blockquote><em>a coffee break might be appropriate, depending on the size of your database</em></blockquote><p>데이터 용량에 따라 걸리는 시간이 다르겠지만, 커피 한잔 즐길시간이면 될 것이라 하니 한 30분정도 기다려 봅니다. 사실 전 커피 한잔 마시는데 10분이면 되는데 말이죠.</p><p>1시간 더 기다려 봅니다.</p><p>1시간 더 기다려 봅니다!!</p><p>1시간 더 기다려 봅니다!!!!</p><p><strong>1시간 더 기다려 봅니다!!!!!!!!!!!</strong></p><p>드디어 되었군요. 4시간 반의 티 타임 끝에 AuroraDB 인스턴스가 준비되었습니다. 이 글을 읽으시는 분 들은 마이그레이션이 언제 완료되나 무작정 기다리지 마시고, “DB Cluster Details”탭의 “Migration Progress”를 참고하시면 현재 진행 상황을 자세하게 알려주니, 저처럼 무작정 기다리지 않으셔도 될 것 같습니다.(있는 줄 몰랐습니다..)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/683/0*NNMUTO2VipNxiyY5.png" /></figure><p>AuroraDB 인스턴스가 준비되었다면, 다음 단계로 넘어가도록 하겠습니다.</p><h3>2. VPC 설정</h3><p>앞서 생성 한 AuroraDB를 MySQL의 Replica DB로 구성하기 위해선, AuroraDB가 MySQL에 접근할 수 있도록 VPC설정이 필요합니다.</p><p>먼저, AuroraDB의 IP를 확인합니다. AuroraDB의 Endpoint를 확인 한 후 console에서 host명령을 이용해 IP를 확인할 수 있습니다.</p><blockquote><em>$ host [aurora-db-end-point]</em></blockquote><p>IP를 잘 적어둔 후, RDS Instances 콘솔에 접속한 후 MySQL의 Security Groups에서 사용하고 있는 해당 그룹 링크를 클릭합니다. 이동 한 Security Groups에서 해당 그룹을 선택한 후 상단 Actions탭의 “Edit inbound rules”를 클릭합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/302/0*gxNgwdYT5_mqFKlk.png" /></figure><p>창이 열리면, “Add Rule”를 해 Row하나를 추가 한 후 Type은 “<strong><em>All traffic</em></strong>”으로 지정하고, 아까 적어 둔 AuroraDB의 아이피를 다음과 같이 적어줍니다. “1.1.1.1/32” (DB 이전을 완료 한 후에는 방금 추가한 Rule을 삭제해 주세요.)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/782/0*e7aLFKr8CX-DDn5v.png" /></figure><p>이제 VPC 설정은 다 되었습니다.</p><h3>3. MySQL에 Replication전용 계정 추가</h3><p>이제 AuroraDB가 MySQL에 Replication을 위해 접근할 계정을 만들도록 하겠습니다. (DB 이전을 완료 한 후 방금 추가한 계정은 삭제를 해 주세요.)</p><blockquote><em>CREATE USER ‘repl_user’@’%’ IDENTIFIED BY ‘yourpassword’;</em></blockquote><p>계정을 추가한 후 replication 권한을 부여합니다.</p><blockquote><em>GRANT REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO ‘repl_user’@’%’ IDENTIFIED BY ‘yourpassword’;</em></blockquote><h3>4. Replication설정 및 시작</h3><p>이제 마지막 입니다. RDS에서 제공하는 procedure를 사용해 replication설정을 마무리 할 것입니다. 우선 MySQL의 Endpoint와 앞서 메모해 두었던 binlog의 “File”과 “Position”을 이용해 프로시저에 파라미터를 채워줍니다. AuroraDB에 접속한 후 아래의 명령을 수행합니다.</p><blockquote><em>CALL mysql.rds_set_external_master(‘mysql end point’, 3306, ‘repl_user’, ‘yourpassword’, ‘mysql-bin-changelog.188412’, 788, 0);</em></blockquote><p>external master설정을 마치셨으면 이제 복제를 시작하겠습니다.</p><blockquote><em>CALL mysql.rds_start_replication;</em></blockquote><h3>5. Replica Error 처리하기</h3><p>무사히 Replication 설정까지 마쳤으나 한가지 시련이 남았습니다. 복제 설정이 완료 된 AuroraDB에 접속해 “show slave status”라는 질의를 해 보시기 바랍니다. 정말 운이 좋다면 에러 없이 정상적으로 리플리케이션이 진행중일 테지만 십중 팔구는 에러가 발생해 리플리케이션이 중단 된 상태 일 것입니다. 1번 과정에서 인스턴스 생성 시 메모해 두었던 Binlog의 위치 정보가 유효하지 않기 때문에 발생한 에러인데, MasterDB의 정확한 스냅샷 시점과 맞지 않아 발생하는 문제입니다. 이를 해결하기 위해서 조금 야매(?) 스럽지만 한번 헤쳐나가 보겠습니다.</p><blockquote><em>call mysql.rds_skip_repl_error;</em></blockquote><p>위와 같은 프로시저를 slave 에러가 발생하지 않을 때 까지 실행해 주세요. 한 건 마다 에러발생 유무를 확인하고 실행하면 시간이 얼마나 걸릴지 모르기 때문에 저 같은 경우는 프로시저를 10,000번 수행하는 스크립트를 만들어서 돌렸습니다. 프로시저를 호출했는데 발생한 에러가 없는 경우는 그냥 PASS하니 저렇게 실행해도 괜찮을 것 같습니다. 다만, 에러 처리 완료 시점까지의 로그는 반드시 기록을 해 두고, 그 근처의 데이터들을 전수 조사하여 문제가 없는지 반드시 확인을 해야합니다. 아마 대부분의 경우 문제가 없을텐데, 문제가 있을 경우 해당 시점의 binlog파일을 열어 하나씩 정합성을 맞추어 주거나, 위 작업을 새로 시도를 하시는게 좋을 것 같다는 생각입니다.</p><h3>마치며</h3><p>MySQL에서 Aurora로 Migrate를 할 때 걸리는 4시간 반이 매우 부담스러운 시간이었습니다. DB이전만 할 게 아니라 이전 후에도 할 작업이 많았기 때문에 장시간의 다운타임이 필요했던 상황 이었지만 덕분에 4시간 반을 아낄 수 있었습니다. 다만, 주의 할 점은 Replication을 구성하기 전 binlog의 리플레이 시점을 잘 기록을 해 두셔야 합니다. 이 내용은 RDS MySQL to RDS AuroraDB에만 가능한 방법이 아니라 RDS MariaDB로 이전시에도 적용이 가능합니다.</p><h3>References</h3><p><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MySQL.Procedural.Importing.NonRDSRepl.html">Importing Data to an Amazon RDS MySQL or MariaDB DB Instance with Reduced Downtime</a></p><p><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.html">Virtual Private Clouds (VPCs) and Amazon RDS</a></p><p><a href="https://aws.amazon.com/ko/blogs/aws/highly-scalable-mysql-compat-rds-db-engine/">Amazon Aurora — New Cost-Effective MySQL-Compatible Database Engine for Amazon RDS</a></p><p><em>Originally published at </em><a href="https://blog.dramancompany.com"><em>https://blog.dramancompany.com</em></a><em> on January 1, 2016.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9cd3e887caf8" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/rds-mysql%EC%97%90%EC%84%9C-rds-aurora%EB%A1%9C-db%EC%9D%B4%EC%A0%84-%EB%8B%A4%EC%9A%B4%ED%83%80%EC%9E%84-%EC%B5%9C%EC%86%8C%ED%99%94-%ED%95%98%EA%B8%B0-9cd3e887caf8">RDS MySQL에서 RDS Aurora로 DB이전 다운타임 최소화 하기</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  78.        </item>
  79.        <item>
  80.            <title><![CDATA[AWS re:Invent 2023 참관기]]></title>
  81.            <link>https://tech.remember.co.kr/aws-re-invent-2023-%EC%B0%B8%EA%B4%80%EA%B8%B0-e17133dd1a1a?source=rss----307f82e3ebfb---4</link>
  82.            <guid isPermaLink="false">https://medium.com/p/e17133dd1a1a</guid>
  83.            <category><![CDATA[aws-reinvent-2023]]></category>
  84.            <category><![CDATA[engineering]]></category>
  85.            <dc:creator><![CDATA[Remember tech]]></dc:creator>
  86.            <pubDate>Mon, 13 Jan 2025 08:30:56 GMT</pubDate>
  87.            <atom:updated>2025-01-15T02:26:52.446Z</atom:updated>
  88.            <content:encoded><![CDATA[<p>안녕하세요? PSE(Platform Server Engineering) 파트 DevOps 이정민, 빅데이터센터 AI Lab ML engineer 박민규입니다. 이렇게 2명은 2023 AWS re:Invent를 다녀왔는데요, event 참관기를 들려 드리려 합니다!</p><p>목차는 다음과 같습니다.</p><p>1. AWS re:Invent 소개</p><p>2. AWS re:Invent를 준비하며(Tip 포함)</p><p>3. 현장 소개</p><p>4. 인상적이었던 세션 소개</p><h3>1. AWS re:Invent 소개</h3><p>AWS는 다양한 크기의 이벤트들을 개최하지만, 그중에서도 주목할 만한 메이저 이벤트는 세 가지입니다. 이 중 re:Inforce는 클라우드 보안, 컴플라이언스, 신원 및 프라이버시에 초점을 맞춘 행사로, 규모는 크지 않지만 매년 그 중요성이 커지고 있습니다. 반면, re:Mars는 Machine Learning (ML), Automation, Robotics, 및 Space (MARS) 분야에 중점을 두고 있어 기술의 최전선을 엿볼 수 있는 장입니다. 그러나 이 모든 것을 아우르는 가장 크고 포괄적인 이벤트는 바로 re:Invent입니다.</p><p>re:Invent는 AWS가 제공하는 모든 솔루션에 대해 다루며, 그 규모와 다양성에서 독보적입니다. 일반적으로 미국 네바다 주 라스베가스에서 열리는 이 이벤트는, 그 크기만큼이나 화려하고 방대합니다. re:Inforce와 같은 작은 이벤트들이 해마다 장소를 옮기는 것과 달리, re:Invent는 라스베가스의 메인 호텔들의 세미나룸을 전용으로 사용할 정도로 대규모입니다. 특히, 이번 2023년 re:Invent는 AI/ML에 큰 비중을 두었습니다. 다양한 세션들 중에서도 Amazon Bedrock에 관한 내용이 주목을 받았습니다.</p><h3>2. AWS re:Invent를 준비하며(Tip 포함)</h3><p>저희 드라마앤컴퍼니는 MSP 메가존클라우드와 파트너십을 맺고 있고, 메가존클라우드와 함께 이번 2023 AWS re:Invent를 참석하게 됐습니다. 메가존클라우드에서는 드라마앤컴퍼니 담당 매니저를 배정하여 비행기 예약, 호텔 예약 등 현지에서 필요한 부분들을 지원해주었습니다. 다만 event에서 들을 세션을 예약하는 것은 개인이 해야 하기 때문에 관심있는 세션을 놓치지 않고 예약하는게 중요합니다. re:Invent를 제대로 즐기기 위해서 세션 예약하는 방법과 Tip을 소개합니다.</p><h4>1) 세션 예약 날짜와 시간 잘 check하기</h4><p>세션 예약 일시에 대한 정보가 공식 홈페이지에 잘 안 보일 수 있으니 한국 AWS 공식 사이트나 커뮤니티를 잘 확인하고 회원가입 할 때 사용한 이메일도 잘 확인하여 정확한 예약 일시를 확인합니다. 예약이 시작되면 실시간으로 인기있는 세션들은 금방 예약이 완료되니 우선순위에 따라 개인의 전략대로 예약하는 것을 권장드립니다! re:Invent는 미국에서 열리기 때문에 예약 일시도 보통 미국 시간에 맞춰서 정해집니다. 그래서 한국 시간으로는 새벽에 예약해야 한다는 것..! 잊지 말아 주세요.</p><h4>2) 동선에 따른 세션 즐겨찾기</h4><p>공식 홈페이지에서 로그인하고 세션 목록을 즐겨찾기 해놓으면 예약할 때 바로 내가 원하는 세션을 골라 예약할 수 있습니다. 그런데 라스 베가스 strip(도시 중심부에 위치한, 세계적으로 유명한 호텔, 카지노, 레스토랑, 쇼핑몰, 엔터테인먼트 장소가 밀집된 지역)에서 세션이 열리는 호텔들이 멀리 떨어진 경우도 있고 호텔도 워낙 커서 세션장을 옮겨 다니는 데도 여유가 필요합니다. AWS에서는 참석자들의 편의를 위해 호텔에서 호텔 간의 셔틀 버스를 제공해줍니다. 그래서 하루 안에 너무 많은 호텔을 이동하는 것은 비효율적이고 1개에서 2개 정도 호텔을 골라 세션을 예약하는 것이 좋습니다. (아래 Figure 1을 보면 베니션, 시저스포럼은 가깝지만 만달레이베이와는 거리가 꽤 됩니다!)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*l7POxTCbb_vLmHw4" /><figcaption>Figure 1. AWS re:Invent Campus Map</figcaption></figure><h4>3) 티켓 구매 후 내 사진 등록하기</h4><p>온라인으로 티켓을 구매한 뒤에 자신의 사진을 등록할 수 있습니다. 이 사진은 현장에서 수령받는 뱃지에 이미지로 들어가게 됩니다. 아래는 현장에서 수령한 뱃지를 찍은 사진인데요, 저(민규)는 사전에 사진을 등록하는 것을 깜빡해 현장에서 급하게 사진을 찍었습니다. 사람들의 뱃지를 보면 다들 멋진 사진을 미리 등록했던데 저는 그러지 못해 아쉬웠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/706/0*PFk-MK3UDU7w3QfL.png" /><figcaption>Figure 2. 수령한 뱃지</figcaption></figure><h3>3. 현장 소개</h3><h4>1) 현장 분위기</h4><p>IT 관련 event 중 거의 가장 큰 event인 만큼 현장의 규모는 압도적으로 느껴졌고, 그 규모를 모두 감당하기 위해 이뤄지는 시스템은 상당히 효율적이었습니다. 처음 등록 할 때 뱃지와 후드를 수령하게 되는데 매우 빠르게 진행되다보니 사람들이 많아도 기다리는 시간이 길지 않았습니다. 등록한 사람들 한정으로 제공하는 무료 식사와 간식을 먹는 데도 수월했고 호텔 간 이동도 매우 편리했습니다. 여러번 event를 진행하면서 쌓인 노하우가 느껴졌던 것 같습니다. Event 기간 동안 느낀 불편함은 전혀 없었기에 세션에 집중할 수 있었고 event를 오로지 즐길 수 있었습니다.</p><p>AWS re:Invent 기간동안 strip에는 re:Invent에 참여하는 엔지니어들로 가득 차있었습니다. 지나가는 대부분의 사람들이 뱃지를 목에 걸고 있었고 특히 참여자들에게 굿즈로 나눠주는 기모 후드집업을 다들 입고 다녀서 약간의 소속감(?)을 느낄 수도 있었습니다. 물론 화려함으로 유명한 도시 답게 패셔너블한 사람들도 종종 보였지만 전세계의 너드들이 도시를 가득 채우고 있다보니 신기하기도 하고 재밌기도 했습니다. 그렇게 도시에 같은 목적을 가진 사람들이 많이 있다보니 좋았던 점도 있었는데요, 처음 도시를 적응 할 때 도움이 많이 되었습니다. 뱃지를 수령하러 갈 때나 호텔 간의 이동을 할 때 길이 복잡해서 찾기가 어려운 상황이 발생 했는데 후드티를 입은 사람들이 가는 길을 따라가다보면 자연스럽게 제가 원하는 장소에 도착해있기도 했습니다! 저희가 묵었던 호텔은 시저스팰리스라는 호텔이었는데요, 숙소에서 세션장이 모여있는 베니션 호텔이나 시저스포럼은 거리가 가까워서 걸어 다녔습니다. 그런데 가는 길이 어렵다 보니 조금 헤매기도 했습니다. 가는 길이 여러 방법이 있어 가장 짧게 걸리는 길을 미리 파악해 놓는 것도 도움이 되니 현장에 일찍 도착하셨다면 미리 투어를 해보시는 것을 추천해 드립니다!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/840/0*Nt0FwP1c4kzwf0Nd.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/840/0*tz054DMr5KVpwiGB.png" /><figcaption>Figure 3. 시저스 포럼 내부 통로 / Figure 4. 세션장 내부 분위기</figcaption></figure><h4>2) 세션장 소개</h4><p>세션을 시작하기 전에 AWS 행사장에 도착하면, 예약자와 미예약자로 나누어진 두 줄에 서게 됩니다. 인기 있는 세션의 경우, 일찍 줄을 서지 않으면 서 있을 공간조차 없어 세션에 참여하지 못할 수 있습니다. 실제로 정말 인기 있는 세션에는 사람들이 30분에서 1시간 전부터 줄을 서기 시작합니다. 만약 세션에 들어가지 못한다면, 화면으로 세션을 시청할 수 있는 공간이 마련되어 있으며, 각 의자에 비치된 무선 헤드셋을 통해 스피커의 목소리를 더욱 선명하게 들을 수 있습니다. 그럼에도 현장에서 직접 듣는 경험은 또 다른 매력을 가지고 있어, 이 분위기를 직접 느껴보는 것이 좋습니다. 대부분의 세션은 나중에 유튜브를 통해 들을 수 있지만, 영상 녹화를 하지 않는 세션도 있어, 현장에서 녹음하거나 슬라이드를 사진으로 찍어 기록하는 것도 좋은 방법이 될 수 있습니다. 세션은 호텔마다 주로 다루는 주제가 있어서 관심 있는 주제가 주로 오픈된 호텔에서 집중적으로 세션을 듣는 것도 좋을 것 같습니다. 한 호텔에서 여러 세션을 연이어 듣다 보면 휴식이 필요한데, 로비마다 과자나 커피 등의 다과가 제공되니 이런 휴식공간을 잘 즐기며 체력을 보충하는 것도 좋은 방법입니다!</p><p>만약 호텔이 아직 익숙하지 않아 세션장을 찾기 어려울 때는 ‘Ask me’라고 쓰인 노란 티셔츠를 입은 사람을 찾아 질문하면 친절하게 안내해줍니다. 여담으로 노란 티셔츠를 입은 분들은 대부분 라스베이거스 거주민으로 이러한 행사가 있을 때 단기 알바로 지원해 행사를 돕는다고 합니다. 세션의 종류에 따라 분위기가 다양한데, Q&amp;A 시간을 많이 주는 활발한 세션도 있고, 거의 강연식으로 정보만 전달하고 끝나는 세션도 있습니다. 너무 듣기만 하는 세션으로 일정을 구성하면 지루할 수 있으니, 다양한 형식의 세션을 경험해 보는 것을 추천해 드립니다!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/840/0*0wptYEBWyf9iHuT5.png" /><figcaption>Figure 5. Ask me!를 입고있는 안내원</figcaption></figure><h4>3) Expo 소개</h4><p>Expo는 AWS의 파트너 및 기술 솔루션 제공 업체들을 직접 만나보고 체험 할 수 있는 공간입니다. 업체들은 부스를 만들어 자신들의 서비스와 솔루션을 전시합니다. 업체마다 부스 규모가 상이한데 어떤 부스에서는 자신들만의 프리젠테이션 타임을 갖고 작은 세션을 진행하기도 합니다. 저는(민규) 특히 AI와 관련된 솔루션에 관심이 있어 LightningAI나 DATASTAX와 같은 업체에서 제공하는 솔루션에 대해 이야기를 나누며 의미있는 시간을 가졌습니다. 원래 Lightning은 AI를 개발할 때 사용하는 라이브러리인데 해당 팀에서 AI 데이터, 학습, 배포 등을 편리하게 하나의 솔루션으로 관리할 수 있도록 LightningAI라는 앱을 만들었습니다. 또한, 드라마앤컴퍼니는 vector search에도 관심이 많은데 DATASTAX가 관련한 솔루션을 제공하고 있어 소개받은 기능들이 흥미롭다고 느꼈습니다.</p><p>또 눈길을 끌었던 것은 DeepRacer의 자율주행 모델 competition이었는데요, 세계 각국의 AI engineer들이 각자가 만든 강화학습 모델을 대회에 등록하고 자신의 알고리즘으로 장난감 자동차를 자율주행하도록 했습니다. 누가 더 빠르게 경기장을 도는지 rap time을 재고 순위가 실시간으로 변경되는 것을 보니 알고리즘을 만든 engineer들이 멋지다는 생각이 들었습니다.</p><p>부스를 돌아다니다 보면 각 업체에서 굿즈(Swag)를 주는데요, 보통 티셔츠를 주는 곳이 많고 스티커, 모자 등을 얻을 수 있습니다. Expo는 보통 월~목까지 진행되지만, 수요일이나 목요일 즈음에 가게 되면 제공하는 굿즈들이 품절되어 못 받을 수 있으니 굿즈에 진심이라면 일찍 Expo를 돌아보시는 것을 추천해 드립니다! 굿즈를 받을 때는 목에 걸고 있는 뱃지를 찍게 되는데요, 뱃지를 찍으면 나의 이메일 주소가 해당 업체에 등록되고 업체는 event 이후 등록한 이메일에 cold mail을 보내게 됩니다. 굿즈를 많이 수집하면 그만큼 나의 이메일 주소가 많이 노출되는 것이니 cold mail을 걱정한다면 처음 event 등록 할 때 sub 이메일 주소를 적는 편이 더 나을 것 같습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/840/0*Oil8HuxNJpM28OjV.png" /><figcaption>Figure 6. Expo 내부 분위기</figcaption></figure><h4>4) Networking</h4><p>AWS re:Invent는 다양한 나라, 다양한 기업에의 engineer들이 모이는 곳이니 networking도 정말 중요한 부분입니다. Networking을 통해 나누는 대화에서 좋은 insight를 얻을 수도 있고 맺어 놓은 좋은 관계가 나중에 나의 업무에 도움이 될 수도 있기 때문입니다. 첫날에는 AWS 한국인의 밤이 열렸는데요. 많은 한국인이 넓은 bar에 모여 라이브 음악을 들으면서 음식을 즐기고 테이블에 앉아 networking을 했습니다. 서로 명함을 주고받고. 서로가 하는 일을 소개했고, 회사 분위기나 업무에 대한 고충 같은 이야기를 나눴습니다. 또 저희 드라마앤컴퍼니는 벤더사인 메가존클라우드에서 만든 자리에 참석하여 다양한 Engineer들을 만나 흥미로운 대화들을 나눴습니다. 낮에는 세션을 듣고 밤에는 이런 networking 자리에 참여하다 보니 체력관리도 중요하다는 것을 깨달았습니다.</p><p>진행했던 세션 중에 한국어 세션들도 몇개가 있어 참석해봤는데 거기서도 좋은 networking 기회들이 있었습니다. 세션이 마무리되고 speaker를 찾아가 몇가지 질문을 하면서 명함을 주고받고 친분을 쌓을 수 있었습니다. 심지어 비행기에서 만난 인연들도 있었는데요, 돌아가는 첫 비행에서는 AWS에서 일하는 Account Manager와 대화를 나누며 링크드인을 교환하고 두 번째 비행에서는 AI 관련 논문을 읽고 있던 soft engineer와 networking을 했습니다. 먼저 다가가는 것이 어려운 것도 있고 언어의 장벽도 있지만 이렇게 주어진 기회들을 놓치지 않고 networking에 적극적인 자세로 임한다면 돌아봤을 때 후회가 없을 것 같다는 생각이 들었습니다.</p><h3>4. 인상적이었던 세션 소개</h3><h4>정민</h4><p><strong>마이그레이션</strong></p><p>저는 다양한 주제의 세션들에 참가하였는데요. 첫 번째로는 얼마 전 마무리된 AWS 리전 마이그레이션 작업을 대비하기 위해 Multi-region 아키텍처 구성, Mass Migration과 관련된 세션을 여럿 참석했습니다. 다소 뻔한 얘기일 수 있지만 Multi-region 아키텍처를 잘 구성하기 위해서는 ‘모든 인프라 리소스를 코드화’하고, ‘리전 간 인프라 배포에 소요되는 시간(bake time)’에 여유를 두어야 한다는 것을 강조하였는데요. 저희도 리전 마이그레이션을 위해 모든 인프라 리소스를 Terraform으로 관리하고 있었고, 덕분에 지난 1월 무사히 마이그레이션 작업을 마무리할 수 있었습니다. (Figure 7)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Z_lj5YabxlWUYZZn.png" /><figcaption>Figure 7</figcaption></figure><p><strong>기조연설</strong></p><p>re:Invent에서 또 빼놓을 수 없는 것이 바로 기조연설인데요. 매일 아침 진행되는 기조연설은 AWS의 CEO, CTO를 비롯한 임원들이 등장하여 현재 AWS가 집중하고 있는 서비스와 신기능에 대한 Summary를 제공합니다. 올해에도 여러가지 흥미로운 주제가 있었지만, 특히나 LLM과 GenAI 등 AI 기술에 대한 내용이 돋보였는데요. 행사 둘째날 기조연설에서 공개된 Amazon Q는 AWS가 제공하는 GenAI 서비스로, 공개 당일에 프리뷰 버전이 AWS 콘솔에서 사용 가능하도록 바로 배포가 되었습니다. 저도 세션을 듣던 중간에 AWS 콘솔에 임베딩된 Amazon Q와 몇 마디 대화를 나눠보았는데요. 대부분의 질문에 잘 답변하는 모습을 확인할 수 있었습니다. (Figure 8)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mpYyuXFm7SeQSsWx.png" /><figcaption>Figure 8. Amazon Q로 네트워크 라우팅 이슈를 디버깅하는 모습(출처: <a href="https://aws.amazon.com/ko/blogs/networking-and-content-delivery/introducing-amazon-q-support-for-network-troubleshooting/">https://aws.amazon.com/ko/blogs/networking-and-content-delivery/introducing-amazon-q-support-for-network-troubleshooting/</a>)</figcaption></figure><p><strong>Amazon Q</strong></p><p>Amazon Q는 나, 혹은 우리 조직의 Private repository나 Wiki 등의 데이터와 연동하여 Personalized된 답변을 받아볼 수도 있고, Amazon Connect와 연동하여 고객센터의 상담이 더 빠르고 정확하게 진행될 수 있도록 연동하는 기능도 제공하고 있습니다. (OpenAI의 ChatGPT또한 비슷한 기능들을 모두 지원하고 있어요. 아쉬운 점은 ChatGPT는 한국어를 지원하지만 Amazon Q는 아직 한국어를 지원하지 않습니다.) 저희 팀에서도 올해 내부 로그 분석과 장애대응 Runbook에 GenAI를 통합하는 테스트를 진행할 계획을 가지고 있는데요. 더 편리한 연동을 위해 Amazon Q의 한국어 지원이 빠르게 릴리즈되기를 희망하고 있습니다.</p><p>마지막으로 리멤버가 엄청난 사용자 데이터와 뛰어난 AI 모델을 가지고 있는 만큼, 저도 자연스럽게 데이터 파이프라인과 ML 워크로드에 대한 관심을 많이 가지게 되었는데요. 이번 re:Invent를 기점으로 AWS Glue, Redshift와 같은 관리형 서비스에서 Apache Iceberg를 지원한다고 하여, Iceberg 기반의 분석 플랫폼을 구성하는 세션에 참석해보았습니다. 기존 Hadoop 시스템의 HDFS와 다르게 설계부터 S3와 같은 Object Storage를 고려하였다는 점이 인상적이었고, 이러한 장점들을 배경으로 Netflix가 Hive에서 Iceberg 생태계로 전환하는 과정 역시 흥미로운 주제였습니다. (Figure 9)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9qJexOf35uS1Clnf.jpeg" /><figcaption>Figure 9</figcaption></figure><p><strong>Express One Zone</strong></p><p>또, 새롭게 출시된 S3 Storage Class인 Express One Zone에 관련된 세션에 참석했는데요. Express One Zone Class는 S3 Standard Class와 다르게, 데이터를 1개 AZ에만 저장하는 대신, 훨씬 빠른 Access 속도를 제공하는 스토리지 클래스입니다. 데이터를 1개 AZ에만 저장하기 때문에 같은 AZ에 위치한 컴퓨팅 리소스에서 접근하는 경우 데이터 액세스 속도가 빠르고, GetObject 비용이 Standard class 대비 50%까지 저렴하다는 장점을 가지고 있습니다. 세션에서는 HA가 필요하지 않고, 작은 사이즈의 분석용 데이터(ex. parquet와 같은)를 잠시 올려두는 용도의 사용을 예시로 들어주었는데요. 최근 발표된 S3 MountPoint 기능과 결합하여 어플리케이션 레이어에서도 Temporary한 파일을 로딩하는 용도로 운영하기에 괜찮을 것 같다는 아이디어를 얻었습니다. (Figure 10)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BxZBl-xwRFLnscxd.jpg" /><figcaption>Figure 10</figcaption></figure><h4>민규</h4><p>저는 정민님처럼 여러 세션을 소개하기 보다 한 세션을 집중적으로 소개할까 합니다. 소개 할 세션 제목은 ‘What’s new in Amazon OpenSearch Service’ 입니다. 저는(민규) ML Engineer로서 AI와 관련된 여러 세션을 들었는데 저희 팀에서 사용하고 있는 OpenSearch에 대한 신기능이 있다고 하여 기대감을 가지고 참석했습니다. 해당 세션을 듣기 전까지 AI 관련 세션을 들었지만 AWS Bedrock에 대한 설명이 많고 중복되어 흥미가 떨어진 상태였습니다. 하지만 해당 세션을 듣고 다시 즐겁게 event를 참여할 수 있었고, 들었던 내용을 동료들과 공유하면 어떨까 하고 생각했습니다. 해당 세션은 AWS OpenSearch에서 새롭게 추가된 feature를 소개하는 세션이었습니다. Speaker는 두 명으로 파트를 나눠서 발표했습니다. 세션 도중 질의응답이 서로 오가는 시간도 있어 더욱 풍부하게 세션을 즐길 수 있었습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2400fYuIiKLj-8fF.jpeg" /><figcaption>Figure 11. What’s new in OpenSearch 세션 현장</figcaption></figure><p><strong>OpenSearch란?</strong></p><p>OpenSearch에 대해서 간단히 설명하자면, 2021년 오픈 소스 프로젝트로 Elasticsearch의 기능을 더욱 확장한 업그레이드 버전입니다. Elasticsearch는 검색 서비스에서 주로 사용하는 솔루션으로 빠른 검색을 가능하게 해줍니다. 구체적으로 Lucene의 역인덱스(Inverted Index) 기술을 활용하는데요, 역인덱스란 문서를 찾을 때 단어마다 인덱스(단어가 가지는 고유한 번호)를 지정해놓고 단어로 검색했을 때 해당 단어를 가진 문서를 빠르게 찾는 방법을 의미합니다. 역인덱스는 단어 기반으로만 문서를 찾기에 문서와 문서의 의미를 반영하지 못한다는 단점을 갖습니다. 이 단점을 극복한 방법이 바로 semantic search입니다. Semantic search는 검색 쿼리와 찾고자 하는 문서의 의미를 임베딩 벡터로 표현하고 벡터의 유사도 점수를 구해 점수가 높은 문서를 상위에 큐레이션 해주는 방법입니다. 보통 모든 문서 임베딩 벡터를 OpenSearch DB에 미리 저장하고, 검색 요청이 들어왔을 때 검색 쿼리 임베딩 벡터를 OpenSearch에 던져서 저장된 문서 임베딩 벡터와 계산하여 문서 목록을 뽑아주게 됩니다. 저희도 해당 기술을 활용해 공고(쿼리)에 적합한 프로필(문서)을 기업에 추천해주거나, 반대로 프로필(쿼리)에 적합한 공고(문서)를 추천해주고 있습니다. 이번 세션에서는 OpenSearch에서 제공하는 Data Engineering에 관한 부분과 Semantic Search(Search Engineering)에 관한 부분을 주로 다뤘습니다.</p><p><strong>Data Engineering</strong></p><p>새롭게 소개되는 Amazon OpenSearch Ingestion은 Amazon Web Services(AWS)에서 제공하는 특별한 기능으로, 사용자가 실시간 로그, 지표, 추적 데이터 등을 Amazon OpenSearch Service 도메인과 서버리스 컬렉션에 쉽게 제공할 수 있게 해주는 완전 관리형 서버리스 데이터 수집기입니다. 이 기능을 사용하면 Logstash나 Jaeger와 같은 타사 솔루션에 의존하지 않고도 데이터를 수집하고 처리할 수 있습니다. Amazon OpenSearch Ingestion은 오픈소스 DataPrepper를 사용하여 구현되며, 이는 데이터를 추적, 변환, 병합, 샘플링하는 등의 다양한 데이터 파이프라인 작업을 가능하게 합니다. 특히 DynamoDB와의 zero-ETL(integration) 통합을 통해, 사용자는 DynamoDB 테이블을 데이터 수집을 위해 직접 사용할 수 있게 되어, 데이터 처리 과정이 대폭 간소화됩니다. DynamoDB는 AWS가 제공하는 완전 관리형 NoSQL 데이터베이스 서비스로, 이를 통한 데이터 수집은 AWS Management Console에서 쉽게 설정할 수 있으며, 이는 데이터 관리와 분석 작업을 더욱 효율적이고 간편하게 만들어 줍니다. Amazon OpenSearch Ingestion을 사용함으로써, 기술적인 복잡성을 크게 줄이면서도 강력한 데이터 분석 및 검색 기능을 활용할 수 있게 됩니다.</p><p>그리고 OpenSearch에 대한 Migration Assistant에 대한 기능도 소개되었는데요, 자체 관리형 ES(Elasticsearch)나 OS(OpenSearch) 클러스터를 Amazon OpenSearch Service의 관리형 클러스터나 serverless collection으로 migration하는 데 도와주는 솔루션입니다. 기존 데이터 및 실시간 데이터를 자동으로 migration 할 수 있게 해줍니다. 특히 기존 클러스터에 방해되지 않게 migration 가능하기에 migration을 생각하고 있다면 좋은 feature가 될 것 같습니다.</p><p><strong>Search Engineering</strong></p><p>앞서 설명한 것처럼 OpenSearch에서는 역인덱스를 사용하는 Text Search를 넘어서 Semantic Search 기능을 제공합니다. 각 검색 쿼리와 문서는 텍스트 뿐만 아니라 이미지, 영상, 음성 등의 데이터로 이뤄질 수도 있는데요, 핵심은 semantic(의미론적) 유사성을 찾아주는 것입니다. 임베딩 벡터는 이 semantic 정보를 가지고 있습니다. 그래서 벡터 공간이라고 불리는 수학적 공간에 아래의 그림과 같이 표현 될 수 있는거죠!</p><p>그래서 Semantic Search는 Vector Search라고도 표현 할 수 있는데요, 아래의 그림(Figure?)을 보면 Vector Search의 workflow를 확인 할 수 있습니다. Raw data는 chuck(문서)로 구분되고 각 chuck는 머신러닝 모델에 의해 숫자로 이루어진 n차원의 임베딩 벡터로 표현됩니다. OpenSearch vector database에 저장되고 각 문서는 index를 갖게 됩니다. 마지막으로 검색이나 분석에 필요할 때마다 불러와 사용됩니다. AI chatbot(Figure?)으로 예를 들어보겠습니다. 사용자가 chatbot에 어떤 question을 던졌을 때 이 question text는 LLM의 모델에 입력되고 임베딩 벡터를 얻게 됩니다. 여기서 Semantic Search를 수행하여 가장 유사한 임베딩 벡터(OpenSearch에 이미 저장된)를 찾아 정렬합니다. 해당 결과를 다시 LLM에 입력하여 Question에 알맞게 다시 자연어로 출력합니다. Semantic Search 기능은 이러한 chatbot 말고도 비슷한 데이터를 빠르게 찾는 기능이 필요한 서비스라면 적용이 가능합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fOPgzcEMYr86obyK.png" /><figcaption>Figure 12. Vector Search workflow</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*US8EhnfRY4rN92fe.png" /><figcaption>Figure 13. Chatbot workflow</figcaption></figure><p><strong>추가적인 Search Engineering의 새로운 features</strong></p><p>또한, OpenSearch는 Semantic Search와 같은 방법과 기존의 전통적인 방법(역인덱스와 같은)을 혼합해서 사용할 수 있는 Hybrid Search, AWS에서 제공하는 pre-trained 모델을 fine tuning 할 수 있는 기능, Semantic Search보다 정확도는 떨어지지만 속도와 메모리에서 이점이 있는 Sparse vector retrieval 기능, 이미지와 텍스트를 혼합해서 사용 할 수 있는 Multimodal search 기능을 새롭게 제공합니다. 특히 AWS 2023 야심작인 Bedrock은 OpenSearch와 함께 손쉽게 사용 가능합니다. Bedrock은 여러개의 Gen AI모델(Anthropic, Meta 등에서 출시한 모델들)을 지원하고 fine tuning하여 새로운 나만의 모델을 만들 수도 있습니다. S3에 나만의 데이터를 저장하고 RAG 또한 활용 할 수 있습니다. 기존에 AI 모델을 사용할 때는 외부에서 모델을 가져와 AWS 시스템에 적용하는 일이 굉장히 복잡하고 쉽지 않았다면 AWS Bedrock을 사용하면 AWS 내에서 아주 편리하고 빠르게 사용할 수 있게 되었습니다.</p><p><strong>그 외 features</strong></p><p>OpenSearch는 Jaeger(분산 추적 시스템으로, 마이크로서비스 아키텍처에서 소프트웨어의 성능 문제를 진단하고 모니터링하는 데 사용)를 통해서 로그를 추적하고 메트릭을 추출하는 자동화 기능을 추가하는 등 완전한 시각화 기능을 구축했습니다. 그리고 OpenSearch Assistant도 소개했는데요, 오픈소스로 제공하고 있고 아직은 베타 버전이지만 자연어를 통해서 다룰 수 있고, toolkit을 customizing 할 수 있고, AI 모델을 연결 할 수 있습니다. 그 외 Security Analytics 기능도 제공합니다. Security log 응답 시간을 줄이고 잠재적 위험을 빠르게 인지하며 이해관계자들에게 알림을 보냄으로써 빨리 대응 할 수 있도록 합니다. 마지막으로 Zero-ETL integration을 제공합니다. 오픈서치 서비스 간에 전환할 필요 없이 Amazon S3에 저장된 운영 로그를 쿼리할 수 있는 새로운 방법인데요. 대용량의 로그 데이터를 저장하고 관리하는 경우 s3에 데이터를 저장하고 오픈서치에서 인덱싱하고 분석할 수 있습니다. 인터렉티브하게 분석할 수 있고, zero etl을 통해 중복을 최소화하는 쿼리가 가능하고, 가속화 기능을 통해서 쿼리 퍼포먼스를 증가시킬 수 있습니다. 가속화 기능에는 인덱스 건너뛰기, 인덱스 포함 등이 있습니다. 복잡한 설정이나 추가 개발 없이 시각화가 가능합니다.</p><h3>마무리하며</h3><p>저희는 이번 행사를 통해 얻은 지식과 경험을 바탕으로 앞으로의 업무에 새로운 아이디어와 솔루션을 적극적으로 도입하고, 개선해 나갈 계획입니다. 또한, 이번 행사에서 만난 다양한 분야의 전문가들과의 네트워킹을 통해 얻은 인사이트와 연결고리를 통해 지속적인 교류와 협력의 기회를 모색할 것입니다. AWS re:Invent는 단순한 기술 컨퍼런스를 넘어서, 최신 클라우드 기술의 동향을 파악하고, 글로벌 네트워크를 확장하며, 미래의 혁신을 모색하는 중요한 장이었습니다. 이번 참관기가 AWS re:Invent에 관심 있는 분들에게 유용한 정보와 인사이트를 제공했기를 바라며, 앞으로도 계속해서 새로운 기술과 트렌드에 대해 공유하고 소통하는 기회를 가지길 희망합니다. 지금까지 긴 글을 읽어주셔서 감사합니다!</p><p><em>Originally published at </em><a href="https://blog.dramancompany.com"><em>https://blog.dramancompany.com</em></a><em> on March 4, 2024.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e17133dd1a1a" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/aws-re-invent-2023-%EC%B0%B8%EA%B4%80%EA%B8%B0-e17133dd1a1a">AWS re:Invent 2023 참관기</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  89.        </item>
  90.        <item>
  91.            <title><![CDATA[리멤버 웹 서비스 좌충우돌 Yarn Berry 도입기]]></title>
  92.            <link>https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A2%8C%EC%B6%A9%EC%9A%B0%EB%8F%8C-yarn-berry-%EB%8F%84%EC%9E%85%EA%B8%B0-0e01c9531079?source=rss----307f82e3ebfb---4</link>
  93.            <guid isPermaLink="false">https://medium.com/p/0e01c9531079</guid>
  94.            <category><![CDATA[yarn-berry]]></category>
  95.            <category><![CDATA[engineering]]></category>
  96.            <dc:creator><![CDATA[Remember tech]]></dc:creator>
  97.            <pubDate>Mon, 13 Jan 2025 08:22:01 GMT</pubDate>
  98.            <atom:updated>2025-01-15T02:25:45.039Z</atom:updated>
  99.            <content:encoded><![CDATA[<p>안녕하세요. 드라마앤컴퍼니에서 현재 채용 서비스를 개발하고 있는 웹 프론트엔드 개발자 오종택입니다. 이전에는 동료 분들의 비즈니스 임팩트를 극대화 하기 위한 UTS(User Targeting System, 조건에 맞는 유저를 찾아주는 쿼리 빌더) 등의 인터널 제품을 만들기도 했습니다.</p><p>리멤버 웹 팀은 리멤버 블랙, 리멤버 채용 솔루션 등 모든 서비스의 웹 애플리케이션을 개선 발전시키는 업무를 담당하고 있습니다. 이 과정에서 고객의 제품 경험을 개선하고, 이러한 개선 활동을 원활하게 지원할 수 있도록 팀의 생산성을 개선하는 일을 중요한 어젠다로 보고 있습니다.</p><p>최근에는 기존에 사용하던 패키지 매니저인 yarn 을 최신 버전인 yarn berry 로 마이그레이션 하기도 했습니다. 처음 yarn berry 라는 키워드를 접했을 땐, 빌드 시간을 일부 단축 시켜주고 개발 과정에서의 안정성을 높여줄 수 있다는 점에서 관심을 가지게 되었습니다.</p><p>새로운 기술은 이후에 일어날 수 있는 변화를 미리 생각해서 신중하게 도입해야 할 것입니다. 웹 파트는 지속적으로 기술을 습득하고 지식을 공유하며, 팀이 효과적으로 일할 수 있다고 판단한 기술은 빠르게 개념 증명(PoC)을 진행하여 기술의 이해도를 높여가고 있습니다.</p><p>현재 리멤버 웹 파트는 모노레포 도입을 준비하면서 pnpm + Turborepo( <a href="https://turbo.build/">링크</a>) 조합을 선택하게 되어 yarn berry 는 사용하지 않게 되었습니다. 이러한 시도가 있었기에 프로덕션 레벨에서 패키지 매니저를 교체하면서 해당 기술에 대한 이해도를 높였고 이를 바탕으로 최종적으로는 가장 만족스러운 결론에 다다를 수 있었습니다.</p><p>이번 글에서는 리멤버 웹 서비스에 점진적으로 yarn berry 를 적용한 과정과, 트러블 슈팅 과정을 겪으며 느꼈던 점들을 공유드리고자 합니다.</p><h3>1. Yarn Berry를 써야 할 결심</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4uY4tC97TRYGDVeu.png" /><figcaption>제로 인스톨로 탄소 감축 :가보자고:</figcaption></figure><p>여러 아티클을 살펴보니 yarn berry 를 사용하면 빌드 시간을 평균적으로 1분 정도 단축할 수 있는 것으로 보였습니다. 뿐만 아니라 종속성들을 보다 안전하게 관리하면서도 기존 node_modules에 딸려오는 여러가지 골치 아픈 문제들을 근본적으로 해결할 수도 있을 것으로 봤습니다.</p><p>업무 기록을 살펴보니 유사한 고민이 과거에도 있었으나 당시 몇 명 없는 인원으로 운영되던 터라 개발 인프라 단에 리소스를 투자하기 어려운 상황이었기 때문에 우선 순위에서 밀렸던 것 같았습니다. 이제 그 때에 비해 프론트엔드 개발자도 늘었고, 개선 시 얻을 수 있는 임팩트도 커졌으니 명분도 충분했습니다. yarn berry 기술 자체도 처음 발표 때와 비교하여 어느 정도 관련 자료도 늘고 성숙해졌다는 판단 또한 있었습니다.</p><p>기존 리멤버의 웹 서비스는 yarn 1.x 버전의 패키지 매니저를 사용해왔습니다. 현재 yarn 1.x 버전은 classic 으로 명명되었으며, 새로운 기능 개발은 이루어지지 않고 유지보수만 이루어지는 레거시 프로젝트가 되었습니다. 즉, yarn classic을 사용하고 있는 상황이고, yarn berry냐 pnpm이냐 하는 선택지를 고민하는게 아니라면 점진적으로 berry로 마이그레이션하는게 바람직하다고 생각합니다.</p><p>(참고로 새 프로젝트를 만드는 시점이라면 yarn init -2 명령으로 간단하게 프로젝트를 생성할 수 있습니다. 해당 프로젝트에 대한 보다 상세한 정보는 <a href="https://github.com/yarnpkg/berry">yarnpkg/berry</a> 에서 확인하실 수 있습니다.)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g7M1bJeoGFNoyu1G.png" /><figcaption>yarn classic 레포지토리. 기능 개발이 중단되었음을 알리는 디스크립션이 붙어있다.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*w5L7QcpUjZoAyHUW.png" /><figcaption>yarn berry 레포지토리. yarn classic과는 대조되는 ‘Active development’ 문구가 붙어있다.</figcaption></figure><h3>2. Why ‘Yarn Berry’</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Fhz6u5GXMFIuXLJv.png" /><figcaption>빠른 찍먹 후 공부를 곁들인…</figcaption></figure><p>도입을 결심한 뒤 개발이 비교적 적게 일어나고, 변경으로 인한 리스크가 적은 어드민 및 내부 라이브러리 프로젝트 부터 점진적으로 적용을 시작해나갔습니다. yarn berry 를 적용하는 한편으로는 내부 공유를 위한 스터디 자료 준비가 이루어졌습니다.</p><p>적용 과정에서 패키지 매니저의 특징에 대해 자세히 살펴볼 수 있었습니다. yarn의 새로운 버전은 node_modules 라는 설계 그 자체로 인해 생기는 막대한 비효율을 해결하고자 기획 되었습니다. 물론 npm 은 그간 Node 생태계를 위해 많은 일을 해왔지만 가장 첫 번째로 꼽힐 용량 문제를 제외하고서라도 많은 문제를 안고 있었습니다. 아래는 <a href="https://yarnpkg.com/features/pnp">yarn 공식 문서</a> 에 언급되어 있는 내용에 대한 정리입니다.</p><h4>1) 모듈 탐색 과정의 비효율</h4><p>node_modules 구조 하에서 모듈을 검색하는 방식은 기본적으로 디스크 I/O 작업입니다. 이는 node_modules가 가진 문제이기 때문에 yarn classic과 npm 모두에 해당되는 내용입니다.</p><p>개발자가 node_modules 내부에서 특정 라이브러리를 불러오는 상황을 가정해보겠습니다. Node.js가 모듈을 불러올 때 경로 탐색에 사용하는 몇 가지 규칙이 있는데요. 이 규칙은 <a href="https://nodejs.org/api/modules.html#loading-from-node_modules-folders">Node.js 공식 문서</a>에서 확인할 수 있습니다. require() 의 경우 1) fs, http 등의 코어 모듈이 아니면서, 2) 절대 경로를 사용할 경우 대략 아래와 같은 순서로 순회하며 모듈을 검색합니다.</p><p>다음은 &#39;/home/ry/projects/foo.js&#39; 에서 require(&#39;bar.js&#39;) 를 탐색할 경우입니다.</p><ul><li>/home/ry/projects/node_modules/bar.js</li><li>/home/ry/node_modules/bar.js</li><li>/home/node_modules/bar.js</li><li>/node_modules/bar.js</li></ul><p>이처럼 매 탐색마다 수 많은 폴더와 파일을 실제로 열고 닫으면서 검색할 수 밖에 없으며, node_modules 중첩 등 경우에 따라서는 순회해야 하는 경로가 이보다 복잡해질 수 있습니다.</p><p>패키지 설치 과정의 경우에도 마찬가지 입니다. 설치 과정에 필요한 최소 동작만으로도 이미 비용이 많이 들고 있기 때문에 각 패키지 간 의존 관계가 유효한지 등의 추가적인 검증에 리소스를 할당하기 어렵습니다.</p><p>이처럼 모듈 탐색을 메모리 상에서 자료구조로 처리하지 않고 I/O로 직접 처리하다보니 추가적인 최적화가 어렵습니다. 실제로 yarn 개발진은 이러한 이유들로 더 이상 최적화 할 여지가 없었다고 문서에서 밝히고 있습니다. yarn berry에서는 이 뒤에서 언급될 PnP 라는 기술을 통해 이를 개선합니다.</p><h4>2) 유령 의존성 (Phantom Dependency)</h4><p>물론 npm은 속도 문제를 개선하기 위해 호이스팅 등 최적화 알고리즘을 도입하였으나 부작용으로 유령 의존성 이라는 문제를 새로 낳고 말았습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YFSEQ0HuKnL2WGCsHQVQfA.png" /><figcaption><a href="https://classic.yarnpkg.com/blog/2018/02/15/nohoist/">https://classic.yarnpkg.com/blog/2018/02/15/nohoist/</a></figcaption></figure><p>npm, yarn classic 등은 중복 설치를 방지하기 위해 위 그림처럼 종속성 트리 아래에 존재하는 패키지들을 호이스팅 &amp; 병합합니다. 그렇게 하면 패키지 최상위에서 트리 깊이 탐색하지 않고 루트 경로에서 원하는 패키지를 탐색할 수 있으므로 효율적입니다.</p><p>하지만 이런 효율의 반대 급부로는 직접 설치하지 않고, 간접 설치한 종속성에 개발자가 접근할 수 있게 되는 상황이 벌어지기도 합니다. 존재하지 않는 종속성에 의존하는 코드가 왕왕 발생할 수 있다는 뜻입니다. 이를 유령 의존성 이라고 합니다. 앞서 언급한 node_modules의 단점으로 인해 의존성 트리의 유효성을 검증하기 어렵다는 것도 한 몫을 했습니다.</p><p>yarn berry에서는 이런 식의 호이스팅 동작이 일어나지 않도록 nohoist 옵션이 기본적으로 활성화 되어 있습니다.</p><h4>3) Plug’n’Play (PnP)</h4><ul><li><a href="https://yarnpkg.com/features/pnp">https://yarnpkg.com/features/pnp</a></li><li><a href="https://classic.yarnpkg.com/lang/en/docs/pnp/">https://classic.yarnpkg.com/lang/en/docs/pnp/</a></li><li><a href="https://github.com/yarnpkg/berry/issues/850">https://github.com/yarnpkg/berry/issues/850</a></li></ul><p>yarn berry는 <strong>Plug’n’Play(PnP)</strong> 라는 기술을 사용하여 이러한 문제들을 해결합니다. yarn berry는 node_modules를 사용하지 않습니다. 대신 .yarn 경로 하위에 의존성들을 .zip 포맷으로 압축 저장하고, .pnp.cjs 파일을 생성 후 의존성 트리 정보를 단일 파일에 저장합니다. 이를 인터페이스 링커 (Interface Linker) 라고 합니다.</p><blockquote>Linkers are the glue between the logical dependency tree and the way it’s represented on the filesystem. Their main use is to take the package data and put them on the filesystem in a way that their target environment will understand (for example, in Node’s case, it will be to generate a .pnp.cjs file).</blockquote><blockquote><a href="https://yarnpkg.com/api/interfaces/yarnpkg_core.linker.html"><em>https://yarnpkg.com/api/interfaces/yarnpkg_core.linker.html</em></a></blockquote><p>링커를 논리적 종속성 트리와 파일 시스템 사이에 있는 일종의 접착제로도 비유할 수 있습니다. 이러한 링커를 사용함으로서 패키지를 검색하기 위한 비효율적이고 반복적인 디스크 I/O로부터 벗어날 수 있게 되었습니다. 의존성 또한 쉽게 검증할 수 있어 유령 의존성 문제도 해결 가능해졌습니다.</p><p>아래 코드는 pnp.cjs의 일부입니다</p><pre>[&quot;@babel/helper-module-transforms&quot;, [\<br>        [&quot;npm:7.19.6&quot;, {\<br>          &quot;packageLocation&quot;: &quot;./.yarn/cache/@babel-helper-module-transforms-npm-7.19.6-c73ab63519-c28692b37d.zip/node_modules/@babel/helper-module-transforms/&quot;,\<br>          &quot;packageDependencies&quot;: [\<br>            [&quot;@babel/helper-module-transforms&quot;, &quot;npm:7.19.6&quot;],\<br>            [&quot;@babel/helper-environment-visitor&quot;, &quot;npm:7.18.9&quot;],\<br>            [&quot;@babel/helper-module-imports&quot;, &quot;npm:7.18.6&quot;],\<br>            [&quot;@babel/helper-simple-access&quot;, &quot;npm:7.19.4&quot;],\<br>            [&quot;@babel/helper-split-export-declaration&quot;, &quot;npm:7.18.6&quot;],\<br>            [&quot;@babel/helper-validator-identifier&quot;, &quot;npm:7.19.1&quot;],\<br>            [&quot;@babel/template&quot;, &quot;npm:7.18.10&quot;],\<br>            [&quot;@babel/traverse&quot;, &quot;npm:7.19.6&quot;],\<br>            [&quot;@babel/types&quot;, &quot;npm:7.20.2&quot;]\<br>          ],\<br>          &quot;linkType&quot;: &quot;HARD&quot;\<br>        }]\<br>      ]],\<br>      [&quot;@babel/helper-optimise-call-expression&quot;, [\<br>        [&quot;npm:7.18.6&quot;, {\<br>          &quot;packageLocation&quot;: &quot;./.yarn/cache/@babel-helper-optimise-call-expression-npm-7.18.6-65705387c4-e518fe8418.zip/node_modules/@babel/helper-optimise-call-expression/&quot;,\<br>          &quot;packageDependencies&quot;: [\<br>            [&quot;@babel/helper-optimise-call-expression&quot;, &quot;npm:7.18.6&quot;],\<br>            [&quot;@babel/types&quot;, &quot;npm:7.20.2&quot;]\<br>          ],\<br>          &quot;linkType&quot;: &quot;HARD&quot;\<br>        }]\<br>      ]],\</pre><p>위와 같이 .pnp.cjs는 의존성 트리를 중첩된 맵으로 표현하였습니다. 기존 Node 가 파일시스템에 접근하여 직접 I/O 를 실행하던 require 문의 비효율을 자료구조를 메모리에 올리는 방식으로 탐색을 최적화한 것입니다. 의존성 압축을 통하여 디스크 용량 절감 효과도 볼 수 있습니다. du -sh 명령어로 확인해보았을 때, Next.js 기반 어드민 서비스 기준 913MB → 247MB 로 기존 패키지 용량 대비 약 27% 수준으로 패키지 관련 용량이 감소한 것을 확인할 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tk7IDKtzquKkElwR" /><figcaption>.yarn/cache에 다운로드 된 종속성들</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/0*pxIgtXkR715y5nGG" /><figcaption>yarn classic 에서의 node_modules 크기 (913MB)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/0*EJKMHtiXxB5HStN0" /><figcaption>yarn berry 에서의 .yarn 크기 (247MB)</figcaption></figure><p>다만 .yarnrc.yml의 링커 설정을 pnp가 아닌 node-modules 로 하게 된다면 기존처럼 node_modules를 설치하여 의존성을 관리하게 됩니다. 하지만 이렇게 사용할 경우 앞서 설명드린 PnP의 장점들을 활용하지 못하게 됩니다.</p><p>이에 대한 예시로 최근 `Vercel` 에서 모노레포 툴링으로 발표한 Turborepo 의 경우 패키지 매니저 중 pnpm 의 pnp 모드만 지원하고 있고, 메인테이너는 yarn berry의 경우 지원 계획을 취소한 상태입니다. 이 경우 앞서 말씀드린 방식으로 berry를 사용해야 합니다. 관련 이슈는 <a href="https://github.com/vercel/turbo/issues/693#issuecomment-1278886166">여기</a> 에서 확인하실 수 있습니다.</p><h4>4) Zero-Installs</h4><p><a href="https://yarnpkg.com/features/zero-installs">Cache strategies | Yarn</a></p><p>.yarn 폴더에 받아놓은 파일들은 오프라인 캐시 역할 또한 할 수 있습니다. 커밋에 포함시켜 github에 프로젝트 코드와 함께 올려두면 어디서든 같은 환경에서 실행 가능할 것을 보장할 수 있으며 별도의 설치 과정도 필요가 없습니다.</p><p>만약 의존성에 변경이 발생하더라도 git 상에서 diff로 잡히므로 쉽게 파악 가능합니다. 개발자들 간 node_modules가 동일한지 체크할 필요가 없다는 뜻입니다.</p><p>제가 생각했을 때 Yarn berry 도입 시 가장 강조되어야 할 중요한 지점이라고 생각합니다. 우리가 작성한 코드들이 여러 툴체인을 거치는 동안 많은 파일들이 generate 되는데, 만약 로컬에 설치된 파일과 리모트(CI 환경, 실서비스 등)에 설치된 파일이 달라 디버깅을 어렵게 한다면 대응하기 매우 어려워질 것입니다. Zero Install을 사용하게 된다면 어떤 설치 환경에서든 같은 상황임을 명시적으로 보장할 수 있습니다.</p><p>부가적인 장점으로 현재 브랜치에 맞는 package.json에 맞게 node_modules를 갱신하기 위한 반복적인 yarn install을 할 필요 또한 없습니다. 브랜치를 체크아웃할 때마다 .yarn/cache 폴더에 있는 의존성도 커밋으로 잡혀있기 때문에 여타 파일들처럼 파일로 취급되어 함께 변경되기 때문입니다.</p><h3>3. 적용 방법</h3><p><a href="https://yarnpkg.com/features/pnp#compatibility-table">호환성 테이블</a>에서 지원하는 버전에 해당만 한다면 마이그레이션 자체는 어렵지 않습니다. yarn이 이미 설치되어 있다는 가정 하에 <a href="https://yarnpkg.com/getting-started/migration">yarn 공식 문서</a> 에서 설명하는 대로 진행합니다.</p><p>정말 해결할 수 없는 문제가 있다면 .yarnrc.yml 에서 nodeLinker 설정을 loose 혹은 node-modules 로 바꿔야(<a href="https://yarnpkg.com/getting-started/migration#if-required-enable-the-node-modules-plugin">링크</a>) 합니다.</p><pre>nodeLinker : &quot;pnp&quot; # 혹은 &quot;node-modules&quot;</pre><p>위에서 설명 드린 유령 의존성 문제 등의 이유로 현재 깨져 있는 종속성 트리를 수동으로 추가해주어야 하는 경우가 있을 수 있습니다. 이때는 일반적인 설치 방식으로 package.json에 종속성으로 추가해주거나, packageExtensions를 사용(<a href="https://yarnpkg.com/getting-started/migration#a-package-is-trying-to-access-another-package-">링크</a>)하여 보완해줄 수 있습니다.</p><pre>packageExtensions:<br>  &quot;debug@*&quot;:<br>    peerDependenciesMeta:<br>      &quot;supports-color&quot;:<br>        optional: true</pre><h4>1) yarn 버전 변경</h4><pre>&gt; yarn set version berry</pre><p>이미 .yarnrc.yml 등 berry 관련된 파일이 생성되어 있으면 작동하지 않습니다. 만약 v1.x로 돌아가려면 yarn set version classic 을 입력합니다.</p><h4>2) .gitignore 설정</h4><p><a href="https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored">문서</a>(Zero-Installs 기준)를 따라 .gitignore 에 아래 경로를 추가해줍니다. ! 는 제외할(gitignore) 경로에서 빼달라는 뜻이므로 .yarn 이하 경로 중 포함시킬 경로들을 명시한 것으로 생각하시면 됩니다. 부정의 부정이라 좀 혼란스러울 수는 있을 것 같습니다. 각 경로의 역할에 대한 자세한 설명은 공식 문서에서 확인하실 수 있습니다.</p><pre>.yarn/*<br>!.yarn/cache<br>!.yarn/patches<br>!.yarn/plugins<br>!.yarn/releases<br>!.yarn/sdks<br>!.yarn/versions</pre><h4>3) .npmrc, .yarnrc를 .yarnrc.yml 로 마이그레이션</h4><p>berry로 버전을 변경하게 되면 루트 경로에 .yarnrc.yml 파일이 생성됩니다. 기존에 있던 .npmrc, .yarnrc는 지원하지 않기 때문에 <a href="https://yarnpkg.com/configuration/yarnrc">문서</a> 를 확인하여 각각의 옵션을 마이그레이션 해주면 됩니다.</p><p>.yarnrc.yml에서 nodeLinker를 node-modules 로 입력하면 classic에서 하던 대로 종속성들을 node_modules에서 관리하게 됩니다. pnp 모드를 사용할 것이므로 pnp 라고 적혀 있는지 확인합니다.</p><p>또한 저희 프로젝트에서는 github packages로 배포한 종속성을 포함하고 있어 yarnrc.yml 에 추가적으로 관련 설정을 추가해줍니다. 배포 시에는 Dockerfile 에서 토큰 값을 넣어줍니다.</p><pre>nodeLinker: pnp<br><br>yarnPath: .yarn/releases/yarn-3.3.0.cjs<br><br>npmScopes:<br>  organization이름(ex. dramancompany):<br>    npmAlwaysAuth: true<br>    # NOTE: 로컬에서 설치 시 터미널에 &#39;export NPM_AUTH_TOKEN=...&#39; 명령어로 환경변수를 설정<br>    npmAuthToken: ${NPM_AUTH_TOKEN} # https://github.com/yarnpkg/berry/pull/1341<br>    npmRegistryServer: &#39;https://npm.pkg.github.com&#39;</pre><p>상기 세팅이 모두 끝나면 `yarn install` 을 입력하여 yarn classic에서 berry로 마이그레이션을 진행합니다.</p><pre>&gt; yarn install</pre><p>이 과정에서 깨진 의존성들이 발견되곤 합니다. styled-components 사용 시 react-is 를 설치하라는 에러 메시지가 뜨는 것이 대표적인 케이스입니다. 터미널에 뜨는 에러 메시지를 확인하여 필요한 의존성들을 추가 설치해줍니다.</p><pre>packageExtensions:<br>  styled-components@*:<br>    dependencies:<br>      react-is: &#39;*&#39;</pre><p>설치 시 ~/.yarn/berry/cache 전역 경로에도 함께 설치가 되므로 yarn cache clean 등의 명령어를 통해 의존성이 완전히 설치되지 않은 상황을 재현하고 싶을 경우 yarn cache clean --mirror 를 입력( <a href="https://yarnpkg.com/features/offline-cache#cleaning-the-cache">관련 문서</a>)해야 하므로 유의할 필요가 있습니다.</p><p>여기까지 마무리 되었으면 한번 커밋하여 진행 상황을 저장합니다.</p><h4>4) yarn berry를 IDE와 통합 (with. TypeScript)</h4><p>지금까지는 패키지 매니저 레벨에서 마이그레이션 할 것들을 처리해주었습니다. 이제 IDE에 의존성과 타입 정보를 node_modules가 아닌 .yarn에서 읽어오도록 알려주어야 합니다. VSCode 기준으로 설명하겠습니다.<br> 일단 아래 세 가지 요소들을 설치합니다.</p><ol><li>VSCode Extension에서 ZipFS 설치 (zip 파일로 설치된 종속성을 읽어올 수 있도록)</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/744/0*rdcghyUkLNDeeOHL" /><figcaption>ZipFS 설치</figcaption></figure><p>2. yarn install -D typescript eslint prettier</p><p>3. yarn dlx @yarnpkg/sdks vscode (yarn dlx = npx) 를 실행하여 관련 세팅을 포함한 .vscode 폴더를 생성합니다.</p><pre>{<br>  &quot;recommendations&quot;: [<br>    &quot;arcanis.vscode-zipfs&quot;,<br>    &quot;dbaeumer.vscode-eslint&quot;,<br>    &quot;esbenp.prettier-vscode&quot;<br>  ]<br>}</pre><pre>{<br>  &quot;search.exclude&quot;: {<br>    &quot;**/.yarn&quot;: true,<br>    &quot;**/.pnp.*&quot;: true<br>  },<br>  &quot;eslint.nodePath&quot;: &quot;.yarn/sdks&quot;,<br>  &quot;prettier.prettierPath&quot;: &quot;.yarn/sdks/prettier/index.js&quot;,<br>  &quot;typescript.tsdk&quot;: &quot;.yarn/sdks/typescript/lib&quot;,<br>  &quot;typescript.enablePromptUseWorkspaceTsdk&quot;: true<br>}</pre><p>4. 설치가 완료되면 아무 타입스크립트 파일이나 들어간 다음</p><p>5. 우측 하단 TypeScript 클릭 or cmd + shift + p 를 눌러 TypeScript 검색</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/624/0*CYzLLwdxEXbuorcZ.png" /></figure><p>6. Use Workspace Version 클릭</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/623/0*8YI1EtUjPVFVow4z.png" /></figure><p>여기까지 진행 한 뒤 다시 한번 진행 상황 저장을 위해 커밋합니다.</p><h4>5) Dockerfile 수정</h4><p>이상 로컬에서 필요한 작업들은 모두 완료를 해주었습니다. 배포 시 도커를 사용하고 있으므로 Dockerfile 에도 필요한 작업을 해줍니다. 아래와 같이 작업을 해주었으며 필요한 설명은 파일 내 주석으로 추가해두었습니다.</p><p>세 가지 정도의 주의사항이 있습니다. 하나는 yarn berry 가 Node 16.14+ 버전에서 동작한다는 것이고, 다른 하나는 yarn berry 에서 zero install 을 사용할 때 yarn install 시 --immutable 옵션을 사용해야 하고, Docker로 배포 시 Dockerfile에 다음과 같이 작성하여 관련 파일들을 working directory에 복사해줘야 합니다.</p><pre>COPY package* yarn.lock .pnp*     ./<br>COPY .yarnrc.yml                  ./<br>COPY .yarn                        ./.yarn</pre><p>위와 같은 과정을 거치면 yarn berry의 zero install을 사용할 수 있게 됩니다. 개선된 빌드 시간은 프로젝트마다 차이가 있으나 지금까지 적용해본 케이스들에서는 약 50초 ~ 1분 정도의 시간 단축이 있었습니다.</p><h3>4. 트러블 슈팅 (with Github issues)</h3><p>yarn berry 가 처음 발표되었을 때에 비해 관련 자료도 많아지고, <a href="https://yarnpkg.com/features/pnp#compatibility-table">여러 대규모 프로젝트</a> 에서 지원하기 시작하는 등 꾸준한 개선이 있어왔지만, 기존 패키지 매니저와의 구조적인 차이 때문에 맞닥뜨리게 되는 낯선 이슈들이 여전히 존재합니다. 이번 섹션에서는 그러한 문제들을 해결했던 경험을 이야기해보겠습니다.</p><h4>1) 커밋에 포함되지 않는 종속성 문제</h4><p>yarn install 시 커밋에 포함되지 않는 파일들이 있습니다. .yarn/install-state.gz 같은 경우 최적화 관련 파일이기 때문에 애초에 커밋할 필요 없다고 공식 문서에서 안내하고 있습니다.</p><p>한편 예상치 못한 예외 케이스도 있었습니다. Next.js 등에 포함되는 `swc`의 경우 운영체제에 종속되는 부분이 있다보니 커밋에 포함시킬 경우 실행환경에 따라 문제를 일으킬 수 있어 커밋에서 제외되고 있습니다. 따라서 swc를 사용한다면 정상적인 빌드를 위해 최초 1회 설치 명령어가 필요합니다.</p><p>만약 설치를 실행하지 않으면 아래와 같은 오류가 발생합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Spzijb7dwyqMQFS5.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/356/0*YkjMfe8Q39CYD25J.png" /></figure><p>설치 후 추가된 파일들이 .gitignore에 등록되어 있으며, 설치된 종속성의 폴더명으로부터 플랫폼 종속적이라는 사실을 추측해볼 수 있습니다. 참고로 .yarn/unplugged 는 zip으로 묶이지 않고 압축해제 된 종속성들이 설치되는 경로입니다. yarn unplug 등의 명령어를 사용하면 압축된 종속성 들을 풀어서 확인할 수 있습니다.</p><h4>2) ESLint import/order 관련 이슈</h4><p>eslint 에서 import 관련 룰을 사용하고 있다면 추가 세팅이 필요합니다. eslint-plugin-import 에서 제공하는 import/order 옵션을 활용해 다음과 같이 외부 의존성과 내부 의존성을 구분지어 줄바꿈 해주고 있었습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/529/0*NF-Fi5vs_VuSJGkc.png" /></figure><p>이 Lint 규칙이 yarn berry 적용 후 제대로 작동하지 않는 것을 발견하였습니다. 프로젝트 내부에서 가져온 모듈과, 외부 라이브러리에서 가져오는 모듈을 구분하는 기준이 node_modules가 경로에 포함되어 있는지 여부였을 것이라 생각하여 검색해보았습니다. <a href="https://github.com/import-js/eslint-plugin-import/issues/2164">관련 이슈</a>로부터 힌트를 얻어 <a href="https://github.com/import-js/eslint-plugin-import#importexternal-module-folders">README</a> 내에 언급된 해결책을 찾을 수 있었습니다.</p><blockquote>If you are using yarn PnP as your package manager, add the .yarn folder and all your installed dependencies will be considered as external, instead of internal.</blockquote><p>이 문제를 해결하려면 .eslintrc.js 에 다음과 같이 옵션을 추가하여 .yarn 경로를 외부 의존성으로 인식시켜주면 됩니다.</p><pre>// .eslintrc.js<br>// ...<br>  settings: {<br>    &#39;import/external-module-folders&#39;: [&#39;.yarn&#39;],<br>// ...</pre><h4>3) yarn berry에서 pre-hook 지원하지 않음</h4><p>yarn 2.x 버전 부터는 pre-hook(ex. preinstall , prepare 등) 을 지원하지 않습니다. <a href="https://yarnpkg.com/advanced/lifecycle-scripts#gatsby-focus-wrapper">문서</a>에 따르면 이는 사이드 이펙트를 줄이기 위한 의도적인 변경이라고 하며, 호환성을 위해서 preinstall 과 install 은 postinstall 의 일부로서 실행됩니다.</p><p>기존에 husky 등을 사용하기 위해 걸어둔 pre-hook이 있었다면 yarn berry 업그레이드 후 작동하지 않을 것이므로 이에 대한 처리가 필요합니다.</p><pre>&quot;postinstall&quot;: &quot;husky install&quot;</pre><h4>4) yarn berry와 vite를 함께 사용할 때 storybook이 실행되지 않는 문제</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1008/0*SBuldCOfaUoY2Atf.png" /></figure><p>이 경우는 누락된 devDependencies를 다 깔아주면 되는 문제로 간단하게 해결할 수 있었습니다. 관련 이슈는 <a href="https://github.com/storybookjs/builder-vite/issues/141">여기</a> 에서 찾아볼 수 있습니다.</p><p>하지만 이 종속성들을 설치하고 나서도 storybook이 정상적으로 실행되지는 않았는데, 스토리북으로 띄운 화면 상의 콘솔에 `”Cannot access “./util.inspect.custom” in client code.”` 라는 에러가 발생했습니다. pnp와 vite 사이에서만 발생하는 문제로 build 과정에서 서버 / 클라이언트 환경에서 실행되는 코드들이 적절히 처리되지 않아서 생기는 문제로 이해했습니다. vite 측에서 폴리필을 추가하여 해결한 것으로 보이며, 관련 이슈는 <a href="https://github.com/vitejs/vite/issues/9238">여기</a>, <a href="https://github.com/vitejs/vite/issues/7576">여기2</a> 에서 찾아볼 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Sq1DWY-FvBKPT1du.png" /></figure><p>이 외에도 vite와의 조합에서 생기는 문제는 또 있었는데요. build를 실행했을 시 종속성을 제대로 찾지 못하는 문제였습니다. 이 문제는 yarn berry를 3.3.0으로 올리고 vscode sdk를 재설치한 뒤, vite를 3.2.0 버전으로 업데이트 하여 해결했습니다. 관련 이슈는 <a href="https://github.com/yarnpkg/berry/issues/4872#issuecomment-1284318301">여기</a> 에서 찾아볼 수 있습니다.</p><h3>5. 개선 결과</h3><p>결과적으로 개선된 빌드 시간은 프로젝트마다 차이가 있으나 지금까지 적용해본 케이스들에서는 yarn berry 단독으로만 따졌을 때 평균적으로 약 50초 ~ 1분 정도의 시간 단축이 있었습니다. 만약 앞서 언급한 swc 관련 설치 시간을 생략할 수 있다면 20초 정도를 추가로 단축할 여지가 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UGutzCLFSiZjpAw4" /><figcaption>swc 설치에 소요되는 시간</figcaption></figure><p>빌드 시간 단축 이외에도 실제 도입해보고 나서 체감할 수 있었던 장점들이 많았습니다. 레포지토리 설치 시 종속성 크기 감소, 로컬과 리모트 환경에서의 빌드 결과물의 동일성 보장, 엄격한 종속성 트리 관리로 인한 안정성 향상 등의 이점이 기존 버전 대비 방법론적인 개선을 이룰 수 있었습니다.</p><p>개발 단계에서 git branch 변경 시 반복적으로 install 스크립트를 실행하여 node_modules를 업데이트 해줘야 하거나, 종종 잘못 설치된 종속성 때문에 한참 디버깅을 하다 결국 node_modules를 지우고 재설치 해야 하는 번거로움이 줄어든 것도 체감되는 부분이었습니다.</p><p>추가로 Dockerfile에 들어있던 세팅에서 불필요한 부분을 걷어내고, <a href="https://docs.aws.amazon.com/ko_kr/codebuild/latest/userguide/build-caching.html#caching-local">AWS Codebuild에서 Docker Layer가 로컬 캐싱</a> 될 수 있는 방법을 찾아 적용하였습니다. yarn berry와는 직접적으로 관련은 없지만 배포 성능을 개선하는 도중에 진행했던 변경이라 함께 언급해두겠습니다.</p><p>도커 파일 내 각 레이어는 변경사항이 생기지 않는 이상 새롭게 생성될 필요가 없습니다. 변경이 사항 없을 시 CodeBuild의 빌드 호스트가 자연스럽게 Docker Layer 캐싱을 이용할 수 있도록 세팅을 변경해주었습니다.</p><p>현재 AWS CodePipeline을 사용하여 배포하고 있으며, 빌드는 AWS Codebuild를 사용하고 있으므로, 해당 단계에서 생성된 빌드 결과물을 캐싱에 사용할 수 있도록 아래와 같이 체크해줍니다. 다만 이 로컬 캐시의 정확한 유효 시간(약 5 ~ 15분)이나 히트 조건에 대해서는 조금 더 확인이 필요합니다. (<a href="https://stackoverflow.com/questions/58793704/aws-codebuild-local-cache-failing-to-actually-cache">링크</a>)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/841/0*38XI5ZxnlRePFs3s" /><figcaption>AWS Codebuild — 편집 — 아티팩트 — 캐싱 메뉴 하단</figcaption></figure><p>이 과정에서 기존에 잘못 세팅되어 있던 Codebuild의 Buildspec을 바로 잡고 로컬 캐싱이 작동하도록 함으로서 추가적으로 빌드 시간을 단축하였습니다. Next.js 기반의 어드민 프로젝트 기준 최대 2분 가량 추가 단축한 것으로 추정됩니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8986sVDTon2QUkr0" /><figcaption>캐시 히트 시 빌드 소요 시간 2분 37초 (직전 빌드 4분 28초 — yarn berry 적용 분 포함)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Xo7Edrk0wlmh_xyN" /><figcaption>도커 레이어 레벨에서 캐싱이 일어났을 때의 빌드 로그</figcaption></figure><p>한 프로젝트는 언급드린 두 가지 조치를 통하여 프로젝트에 따라 빌드 시간이 5분 30초 → 1분 50초로 드라마틱하게 감소하기도 했는데, 프로젝트별 인스턴스 세팅과 빌드 시점, 캐시 여부에 따라 개선되는 폭은 상이할 것으로 생각되어 절대적인 수치로는 참고하지 말아주시고 대략적인 수치로만 봐주시면 좋을 것 같습니다.</p><h3>6. 마치며</h3><p>한 명의 아이를 기르기 위해 온 마을이 필요하다는 말 처럼, 하나의 웹 서비스가 만들어지기 위해서는 정말 다양한 기술이 필요합니다. 어느 개발이나 그렇겠지만 특히 최근 자바스크립트 생태계는 특히 이런 툴체인(Toolchain) 의 조합을 여러 방향으로 실험해보는 활발한 분위기가 느껴집니다. npm 과 함께 오랫동안 사랑 받아온 패키지 매니저 yarn 또한 개발에 필요한 라이브러리들의 설치와 종속성을 담당하는 점에서 툴체인의 중요한 일부를 담당하고 있습니다.</p><p>그 중에서도 yarn berry와 pnp는 과감하면서도 멋진 진전이라고 생각합니다. 물론 위에 말씀드린 이슈들처럼 아직 사용자들이 직접 부딪혀야 하는 문제들이 산재해있지만, 기술이 처음 공개되었을 때와 비교하면 현재 이 리스크들은 감내할 수 있는 수준이라고 생각합니다. yarn unplug , yarn why , yarn patch 등의 기능을 활용하여 디버깅하고, 긴급한 상황에서는 nodeLinker: node-modules 로 되돌리거나 pnpMode: loose 등의 절충점이 존재하므로 자신 있게 도입해볼 수 있을 것 같습니다.</p><p>사실 yarn berry 를 도입하는 과정에서 가장 좋았던 점은 성능상의 이점보다도, yarn이나 여러 패키지 내부의 코드를 살펴보며 동작 원리를 가늠하거나, Github에 올라온 issue들을 싹싹 긁어가며 읽고, peerDependencies 등 종속성들 간의 관계를 생각하며 패키지를 이리저리 설치해보던 시간들이었습니다. 가벼운 마음으로 시작했지만 역시 어떤 기술이든 직접 만져보고 굴려볼 때 이해도가 더 높아진다는 사실을 새삼스레 곱씹어보게 됐던 것 같네요.</p><p>서두에서 말씀드렸듯 결국 저희 팀에서는 비록 Turborepo 와의 호환 이슈로 pnpm 을 선택하게 되었지만 yarn berry는 충분히 매력적인 기술인만큼 검토해보시고 도입을 적극 고려해보셨으면 좋겠습니다.</p><p>이제 저희 팀의 채용 홍보로 글을 마무리 짓도록 하겠습니다. <strong>리멤버 웹 파트에서는 이러한 기술적인 고민을 함께 나누며 성장하실 동료분을 모시고 있습니다.</strong> 서류 검토, 기술 면접, 컬처핏 면접 절차 세 단계로 간소하게 프로세스를 진행하고 있습니다. 보다 상세한 내용은 <a href="https://hello.remember.co.kr/recruit/web">채용 공고</a> 를 확인 부탁드리며, 많은 관심과 지원 부탁드리겠습니다.</p><p>지금까지 긴 글 읽어주셔서 감사합니다.</p><p><a href="https://hello.remember.co.kr/recruit">리멤버 팀에 합류하세요 (채용 공고)</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zjE2LxVyU03t45SX" /><figcaption>도입 초기에는 모든 에러로부터 Yarn Berry가 의심의 눈초리를 받을 수 있으니 주의 요망</figcaption></figure><h3>7. 참고 자료</h3><ul><li><a href="https://yarnpkg.com/">https://yarnpkg.com/</a></li><li><a href="https://toss.tech/article/node-modules-and-yarn-berry">node_modules로부터 우리를 구원해 줄 Yarn Berry</a></li><li><a href="https://medium.com/wantedjobs/yarn-berry-%EC%A0%81%EC%9A%A9%EA%B8%B0-1-e4347be5987">yarn berry 적용기(1)</a></li><li><a href="https://medium.com/teamo2/yarn-berry-%EA%B5%B3%EC%9D%B4-%EB%8F%84%EC%9E%85%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C-d6221b9beca6">Yarn Berry, 굳이 도입해야 할까?</a></li><li><a href="https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm">npm, yarn, pnpm 비교해보기</a></li></ul><p><em>Originally published at </em><a href="https://blog.dramancompany.com"><em>https://blog.dramancompany.com</em></a><em> on Feburary 22, 2023.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0e01c9531079" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A2%8C%EC%B6%A9%EC%9A%B0%EB%8F%8C-yarn-berry-%EB%8F%84%EC%9E%85%EA%B8%B0-0e01c9531079">리멤버 웹 서비스 좌충우돌 Yarn Berry 도입기</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  100.        </item>
  101.        <item>
  102.            <title><![CDATA[AI 명함촬영인식 ‘리오(RIO)’ 적용기 2부 — ML Model Converter와 안드로이드 앱 적용기]]></title>
  103.            <link>https://tech.remember.co.kr/ai-%EB%AA%85%ED%95%A8%EC%B4%AC%EC%98%81%EC%9D%B8%EC%8B%9D-%EB%A6%AC%EC%98%A4-rio-%EC%A0%81%EC%9A%A9%EA%B8%B0-2%EB%B6%80-ml-model-converter%EC%99%80-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1-%EC%A0%81%EC%9A%A9%EA%B8%B0-d04287f2cb86?source=rss----307f82e3ebfb---4</link>
  104.            <guid isPermaLink="false">https://medium.com/p/d04287f2cb86</guid>
  105.            <category><![CDATA[ml-model-deployment]]></category>
  106.            <category><![CDATA[ai]]></category>
  107.            <dc:creator><![CDATA[Remember tech]]></dc:creator>
  108.            <pubDate>Mon, 13 Jan 2025 08:08:36 GMT</pubDate>
  109.            <atom:updated>2025-01-15T02:25:13.240Z</atom:updated>
  110.            <content:encoded><![CDATA[<h3>AI 명함촬영인식 ‘리오(RIO)’ 적용기 2부 — ML Model Converter와 안드로이드 앱 적용기</h3><p>안녕하세요. 빅데이터센터 AI Lab 강민석입니다.</p><p>이번 <strong>AI 명함 촬영 인식 ‘리오(RIO)’</strong> 적용기 2부에서는 리멤버 앱에 AI 명함 촬영 인식 ‘리오(RIO)’의 모델을 Client-Side Computing로 적용하기 위한 다양한 시행착오들을 공유하고자 합니다. 학습된 PyTorch Model을 ONNX와 Tensorflow 모델을 거쳐 TF Lite Model로의 변환과정과 모델 추론 안드로이드 샘플 환경에서의 테스트까지 내용을 소개 하고자 합니다. 이 글에서 AI 명함 촬영 인식 ‘리오(RIO)’ 적용을 위해 최종적으로 진행된 <strong>ML 모델의 변환 방법</strong>과 <strong>모델 추론을 위한 안드로이드 테스트 환경</strong> 에 대해 설명 드리고자 합니다.</p><p>이 AI 명함 촬영 인식 ‘리오’ 적용기 포스팅은 1부와 2부로 나누어 포스팅 되어 있습니다. 이번 AI 명함 촬영 인식 ‘리오’ 적용기 2부에서는 ML Model Converter와 안드로이드 앱 적용기에 대해 작성 되어있습니다.</p><h3>Client-Side Computing (또는 Edge Computing)</h3><p><strong>Client-Side Computing</strong> 은 데이터를 클라우드 서버로 보내는 대신 로컬 기기에서 처리하는 것입니다. 기계 학습과 관련하여 Client-Side Computing은 장치에서 추론이 직접 수행하는 것을 의미합니다. AI 명함 촬영 인식 ‘리오(RIO)’가 Client-Side Computing 사용 해야 하는 데에는 4가지 주된 이유가 있습니다.</p><ol><li><strong>실시간 추론</strong> : 리멤버 앱이 동작하는 모바일 장치의 ML 모델 추론 계산은 네트워크를 통해 API 결과를 기다리는 것보다 빠릅니다. 실시간으로 사용자에게 명함의 위치를 보여주고 명함의 영역을 잘라내 제공합니다.</li><li><strong>오프라인 기능</strong> : Client-Side Computing은 인터넷 연결 없이도 명함이 촬영되는 순간부터 명함의 위치와 명함의 영역 잘라서 명함의 이미지만을 제공하게 됩니다.</li><li><strong>데이터 프라이버시</strong> : 인터넷을 통해 전송되거나 클라우드 데이터베이스에 추가로 저장되는 위험을 낮춰 줍니다.</li><li><strong>비용 절감</strong> : ML 서버가 필요하지 않으므로 서버 비용이 절감됩니다. 이 외에도 명함 이미지가 서버로 전송되지 않기 때문에 데이터 전송 네트워크 비용이 절감됩니다.</li></ol><p>Client-Side Computing의 경우 큰 장점들이 있지만 다양한 제약 사항들이 생기게 됩니다. 모바일 기기의 처리능력의 제한, 모델 크기(용량)에 대한 제한, 모바일 적용을 위한 ML 프레임워크의 의존성에 대한 제약 사항들이 생기게 되고 이를 해결 하려는 다양한 리서치를 진행 하게 되었습니다. 이 과정에서 생긴 시행착오와 다양한 고민을 공유하여 관련 연구자 또는 개발자들의 시간을 절약하게 되었으면 좋겠습니다.</p><h3>ML Model Converter</h3><p>학습된 ML 모델을 모바일과 같은 사용자의 장치에서 적용하기 위해서는 지연 시간, 개인 정보 보호, 다양한 기기와의 연결성, 모델의 크기, 전력 소비 등을 고려해야 한다는 문제가 존재합니다. 저희는 Pytorch Model을 TF Lite Model로 최종 모델로 변환하기로 하였는데, 그 이유는 산업과 관련된 제한 사항과 요구 사항에 대해 오랜 기간 긴밀히 발전해오고 있는 Tensorflow Lite의 안정성과 Arm CPU 연산에 최적화된 더 많은 사례가 있으므로 타 프레임워크 환경들과 비교하여 더 나은 성능을 쉽고 빠르게 적용 가능 하다고 판단했습니다.</p><p>아래의 글에서 Pytorch Model을 TF Lite Model로 변환하는 과정을 설명하고자 합니다. 변환 과정은 <strong>PyTorch Model → ONNX Graph(+TorchScript)→ Tensorflow Model → TF Lite Model</strong> 순으로 진행하였습니다. 관련 기술을 소개하고 변환하는 방법에 대해 설명해 드리도록 하겠습니다.</p><h4>PyTorch Model을 ONNX Graph(+TorchScript)로의 변환</h4><p>최종 모델인 Tensorflow Lite 모델로 변환하기 위해서는 중간 과정에 Tensorflow 모델로 변환이 필요한데, Pytorch Model을 직접 Tensorflow 모델로 변환하는 기능이 제공되어 있지 않습니다. 그래서 먼저 상호 운용가능한 ONNX 그래프로 변환을 하게 됩니다. PyTorch에서 제공되는 ONNX 그래프로 변환하는 <strong>torch.onnx.export()</strong> 함수를 통해 PyTorch 모델을 ONNX*로 변환하였습니다.</p><blockquote><strong><em>ONNX</em></strong><em> 는 Open Neural Network Exchange의 줄인 말로서 이름과 같이 다른 ML 프레임워크 환경(Tensorflow, PyTorch 등)에서 만들어진 모델들을 서로 호환될 수 있도록 만들어진 공유 플랫폼입니다.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*OKW1vRdbiWsGhO-R.png" /><figcaption>그림 1. TorchScript의 Script Mode 변환[1]</figcaption></figure><p><strong>torch.onnx.export()</strong> 함수의 실제 동작은 내부적으로 TorchScript를 통해 코드를 Eager Mode에서 Script Mode로 변환하여 중간 표현(Intermediate Representation, IR)* 그래프를 ONNX 그래프로 변환하기 전에 생성하게 됩니다. 그 이후에 Pytorch의 Script Mode의 중간 표현(IR)*을 ONNX 그래프로 변환하여 반환하게 됩니다.</p><blockquote><strong><em>중간표현(Intermediate Representation, IR)</em></strong><em> 은 소스 코드를 나타내기 위해 컴파일러 또는 가상 머신에서 내부적으로 사용하는 데이터 구조 또는 코드입니다. IR은 최적화 및 모델 변환과 같은 추가적인 처리 과정에 도움이 되도록 설계되어 있습니다.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Mp_4bufLh4ctv5BQ.png" /><figcaption>그림 2. ONNX의 상호 운용성 [2]</figcaption></figure><p>ONNX 그래프로 모델 표현 하게 되면 다른 프레임 워크로 모델 표현(Framework Interoperability)이 가능한 것 뿐만 아니라, 이 과정에서 ONNX에서 제공되는 것 이외에도 최적화된 구조의 그래프 표현이 유지된 상태(Shared Optimization)로 변환될 수 있다는 이점이 있습니다. 이 과정에서 산업영역의 여러 하드웨어와 ML 컴파일러에 대한 다양한 선택지를 제공 받았던 것 같습니다.</p><h4>ONNX Graph를 Tensorflow Lite model로의 변환</h4><p>ONNX Graph를 Tensorflow Lite model(.tflite 파일 확장자로 식별되는 최적화된 FlatBuffer 형식)로 변환하기 위해서는 중간 과정으로 Tensorflow Model로의 변환이 필요합니다. 이를 위해서 ONNX를 위한 Tensorflow 백엔드 onnx-tensorflow[3]에서 제공되는 <strong>onnx_tf.backend.prepare(onnx_model)</strong> 함수를 사용하여 ONNX Graph를 Tensorflow 모델로 변환해줍니다. 그 이후에 TensorFlow Lite*[4]에서 제공되는 <strong>tf.lite.TFLiteConverter.from_saved_model(tf_model_path)</strong> 함수를 사용하여 Tensorflow 모델을 Tensorflow Lite model로 변환해주는 작업을 진행합니다.</p><blockquote><strong><em>TensorFlow Lite</em></strong><em> 는 Android 및 iOS, 내장형 Linux 및 마이크로 컨트롤러 등의 기기에서 모델을 실행할 수 있는 기능을 제공하기 위해 On-device에서 ML을 위한 해석기(Interpreter)와 라이브러리를 지원하는 프레임워크 입니다.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*FNyXlZzzswEXRIRH.png" /><figcaption>그림 3. 모델 변환을 위한 high-level workflow[4]</figcaption></figure><p>Pytorch Model을 TF Lite Model로의 변환 과정을 코드로 보면 20줄 내외의 간단한 API 호출 몇 줄로 표현되지만, 내부적인 동작에 대해 상세하게 설명해 보았습니다. Quantization을 추가로 실험하였지만 정확도가 다소 떨어지는 경향이 있었습니다. 아래의 표에서는 모델의 크기를 단순 비교한 테이블입니다. 각 모델에 대한 속도에 대한 비교는 각각 목표 하드웨어에 따라 최적화 방법이 달라 비교하지 않고 Quantization을 제외한 모든 비교 모델에서의 정확도 차이가 크게 나타나지 않아 표기하지 않았습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1rqQHoEdTGzarINx.png" /><figcaption>표 1. 모델 표현 방법에 따른 모델 사이즈 비교</figcaption></figure><h3>모델 추론을 위한 안드로이드 테스트 데모 앱</h3><h4>On-deivce 모델 추론을 위한 Tensorflow Lite Interpreter</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xRzrBgzwMzb_rHfs.png" /><figcaption>그림 4. On-device에서의 TensorFlow Lite를 사용하여 모델 배포 [5]</figcaption></figure><p>위의 내용에서 AI 명함 촬영 인식 ‘리오(RIO)’를 Client-Side Computing에서 사용하기 위해 ML Model Converter에 관해 소개했습니다. 실제 모바일 기기에서 ‘리오(RIO)’ 모델의 추론을 진행하기 위해 Tensorflow Lite Interpreter를 활용하게 되는데 위의 그림 4와 같이 모바일 기기의 하드웨어를 사용하게 됩니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WJndzatSGRAMR_s6.png" /><figcaption>그림 5. TensorFlow Lite의 아키텍처 디자인 [6]</figcaption></figure><p>앱 내에서는 그림 5와 같이 Tensorflow Lite는 앱 내에서 추론 모델을 로드하고 인터프리터를 호출하는 C++ API , 주변의 Convenience Wrapper 역할을 하는 Android 앱용 Java API가 제공됩니다. Java API를 통해 모델을 호출하고 Tensorflow Lite 인터프리터를 사용하여 Tensorflow Lite 모델의 앱내 추론을 진행하게 됩니다.</p><h3>JNI과 OpenCV를 활용한 Post Processing</h3><p>Tensorflow Lite Interpreter 모델 추론을 통해 얻게 된 결과는 아웃풋 차원 변환, NMS(Non-maximum Suppression), 다양한 후처리가 필요한 이전의 저차원의 피처 값들로 반환하게 되어 있습니다. 이를 처리하기 위해서는 Python 인터프리터의 사용 없이 자바에서 구현이 되어야 하는데 자바보다는 속도 측면의 이점이 있고 OpenCV 활용이 수월한 C++로 구현한 다음 JNI 통해 호출하도록 구현하였습니다.</p><p>다양한 차원 변환 함수와 NMS(Non-maximum Suppression)는 직접 구현하여 호출하도록 설계 하였고 AI 명함 촬영 인식 ‘리오(RIO)’ 적용기 1부에서 설명된 Post Processing의 여러 함수는 OpenCV를 활용하여 구현하게 되었습니다.</p><h3>AI 명함 촬영 인식 ‘리오(RIO)’ 테스트용 데모 앱</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sqfriFPV-FTFy6j9.png" /><figcaption>그림 6. AI 명함 촬영 인식 ‘리오(RIO)’ 테스트용 데모앱 그림</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*tl4jh_4DaZk9tT22.gif" /><figcaption>그림 7. AI 명함 촬영 인식 ‘리오(RIO)’ 배포 버전</figcaption></figure><p>AI 명함 촬영 인식 ‘리오(RIO)’를 안드로이드 앱에서 실험하기 위해 그림 6과 같이 테스트용 앱을 제작해 다양한 실험을 진행했습니다. 주로 안드로이드 앱에서의 모델 로드, 모델 추론, 다양한 후처리에 대한 동작 테스트를 진행하고 보다 다양한 환경에서 촬영해가며 리멤버 앱 사용자가 촬영할만한 상황을 연출하여 테스트를 진행했습니다. 이 과정에서 안드로이드 테스트용 앱 제작부터 실험 작업까지 동료분들의 많은 도움을 받아 진행하게 되었습니다.</p><h3>AI 명함 촬영 인식 ‘리오(RIO)’ 적용기 1부~2부를 마치며</h3><p>AI 명함 촬영 인식 ‘리오(RIO)’ 적용기 1부 — 명함 촬영 인식 위한 Instance Segmentation &amp; Computer Vision을 거쳐, 2부 — ML Model Converter와 안드로이드 앱 적용기까지 내용을 설명해 드렸습니다. AI 명함 촬영 인식 ‘리오(RIO)’ 를 개발하면서 다양한 기술을 실험하고 테스트해나가면서 많은 시행착오를 겪었던 것 같습니다. 일련의 과정을 해결해 나가는 데 있어서 리멤버의 동료가 지니고 있는 “고객 WOW를 위한 빠른 실행을 팀웍으로”의 리멤버 Way를 직접 느꼈던 프로젝트였습니다. 프로젝트 관련해 도움을 주신 많은 분들께 다시 한번 감사드립니다.</p><h3>Reference</h3><p>[1] TorchScript — PyTorch documentation</p><p>[3] onnx/onnx-tensorflow: Tensorflow Backend for ONNX | github.com</p><p>[5]Tensorflow Lite- machine learning at the edge!! | by Maheshwar Ligade | techwasti | Medium</p><p>[6] Google Developers Blog: Announcing TensorFlow Lite | googleblog.com</p><p><em>Originally published at </em><a href="https://blog.dramancompany.com"><em>https://blog.dramancompany.com</em></a><em> on November 23, 2022.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d04287f2cb86" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/ai-%EB%AA%85%ED%95%A8%EC%B4%AC%EC%98%81%EC%9D%B8%EC%8B%9D-%EB%A6%AC%EC%98%A4-rio-%EC%A0%81%EC%9A%A9%EA%B8%B0-2%EB%B6%80-ml-model-converter%EC%99%80-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1-%EC%A0%81%EC%9A%A9%EA%B8%B0-d04287f2cb86">AI 명함촬영인식 ‘리오(RIO)’ 적용기 2부 — ML Model Converter와 안드로이드 앱 적용기</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  111.        </item>
  112.        <item>
  113.            <title><![CDATA[리멤버 유저에게 보다 깨끗한 명함 이미지 제공을 위한 이미지 복원 방법]]></title>
  114.            <link>https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EC%9C%A0%EC%A0%80%EC%97%90%EA%B2%8C-%EB%B3%B4%EB%8B%A4-%EA%B9%A8%EB%81%97%ED%95%9C-%EB%AA%85%ED%95%A8-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%9C%EA%B3%B5%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B3%B5%EC%9B%90-%EB%B0%A9%EB%B2%95-7b52084d1f1a?source=rss----307f82e3ebfb---4</link>
  115.            <guid isPermaLink="false">https://medium.com/p/7b52084d1f1a</guid>
  116.            <category><![CDATA[image-processing]]></category>
  117.            <category><![CDATA[ai]]></category>
  118.            <dc:creator><![CDATA[Remember tech]]></dc:creator>
  119.            <pubDate>Mon, 13 Jan 2025 08:01:15 GMT</pubDate>
  120.            <atom:updated>2025-01-15T02:24:54.654Z</atom:updated>
  121.            <content:encoded><![CDATA[<p>안녕하세요. 드라마앤컴퍼니의 빅데이터센터 AILab 박호림입니다.</p><p>드라마앤컴퍼니 빅데이터 센터의 AI Lab은 Recommendation System, Ranking Model, Graph Neural Network, Natural Language Processing, Document Understanding, Computer Vision 등 연구 영역을 넓혀가고 있으며, 기반 연구를 통해 고객의 비즈니스에서 WOW 하는 경험을 제공하고자 노력하고 있습니다.</p><p>리멤버를 사용하는 많은 유저들은 본인의 명함 또는 주고받은 명함을 직접 촬영하여 등록하고 있습니다. 직접 촬영을 하다 보면 밝거나 어두운 또는 명함을 책상에 두거나 손으로 들거나 등의 다양한 환경에서 촬영하다 보니 명함 이미지를 복원하거나 다양한 후처리가 필요합니다. 따라서 이번 포스팅에서는 유저가 촬영한 명함 이미지에 대한 복원 방법에 대해 알아보고자 합니다.</p><h3>1. 명함 이미지 복원의 필요성과 이미지 복원 Task</h3><h4>명함 이미지 복원의 필요성</h4><ol><li>명함을 촬영하면 명함 외 배경 부분을 잘라내고 명함만 확대하여 사용자에게 보이도록 합니다. 여기서 원본이미지에서 명함 부분만 잘라내어 확대하기 때문에 저해상도(낮은 품질)로 유저에게 보이는 문제가 있어 고해상도로 복원을 필요로 합니다.</li><li>리멤버 앱을 통한 명함 촬영은 자연히 다양한 환경에서 촬영된 명함 이미지들이 포함되어 있습니다. 하지만 촬영 시 주변 환경에 따라, 카메라의 성능에 따라 명함을 촬영한 이미지에 노이즈가 내포되곤 합니다. 촬영한 이미지에서 명함을 잘라내는 데 큰 문제는 없지만, 사용자에게 깨끗한 명함 이미지를 제공하기 위해서는 노이즈를 제거하는 명함 이미지 복원 작업이 필요합니다.</li><li>실제 명함을 촬영하여 명함 이미지를 저장하기 때문에 명함 자체가 오염되거나 명함을 잡고 찍은 명함 이미지라면 해당 부분이 반영되어 저장됩니다. 많은 유저들은 리멤버 상에서 명함을 통한 다양한 교류와 활동을 하기에 오염된 명함을 깨끗한 명함으로 복원할 필요가 있습니다.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*OCYnRcS_nh1GuAqX.png" /><figcaption>그림 1. 저해상도 명함 이미지(좌), 흐릿한 명함 이미지(중), 명함 정보 일부가 가려진 명함 이미지(우)</figcaption></figure><h4>이미지 복원 Task</h4><p>리멤버에서 명함 이미지를 촬영하고 난 후 이미지 복원에 대한 문제에 대해 3가지 정도 Task로 정의할 수 있습니다.</p><h4>Super-Resolution</h4><p>Super-Resolution(SR)은 저해상도 이미지를 고해상도 이미지로 변환하는 Task를 의미합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/0*5EEzjb9cuDBEI_V4" /><figcaption>그림 2. Super-Resolution 예시 이미지</figcaption></figure><h4>Denoising, Deblurring</h4><p>Denoising 이란 입력 이미지로부터 이미지에 존재하는 노이즈를 제거하는 Task를 의미합니다.</p><p>Deblurring 이란 흐릿한 이미지에서 Blur를 제거하여 깔끔한 이미지로 만드는 Task 입니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*l1Sweh39qJCJ2seE.png" /><figcaption>그림 3. Denoising, Deblurring 예시 이미지</figcaption></figure><h4>Inpainting</h4><p>Inpainting 이란 오래된 사진 또는 화질이 번진 이미지에서 손실된 영역을 복구하는 Task입니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/0*k3FAA39kXDofth89.png" /><figcaption>그림 4. Inpainting 예시 이미지</figcaption></figure><h3>2. 전통적 이미지 복원 방법</h3><h4>Super-Resolution</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*FuVoi0omAuAmEtwo.png" /><figcaption>그림 5. Interpolation 종류</figcaption></figure><p>Super-Resolution Task를 해결하기 위해 전통적으로 많이 쓰는 방법의 하나는 Interpolation입니다. Interpolation이란 알고 있는 값을 가지는 두 점 사이의 어떠한 지점의 값을 추정하는 방법을 의미합니다.</p><p>Super-Resolution을 해결하기 위한 Interpolation으로는 Bilinear interpolation, Bicubic interpolation 등이 있습니다. linear interpolation이란 두 점 사이의 직선을 그리는 방법, 즉 1차 함수에 해당하며 Cubic interpolation은 3차 함수 그래프 기준으로 값을 추정하는 방법입니다. 이 방법들을 2차원으로 확장해 Super-Resolution으로 적용하여 저해상도 이미지를 고해상도 이미지로 변환시켜줍니다. 이러한 방법 외에도 Nearest, Area, Lanczos 등의 Interpolation이 존재합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*05nQUTVtQFMuQ1nQ.png" /><figcaption>그림 6. Bilinear, Lanczos Interpolation 테스트 이미지</figcaption></figure><p>그림 6은 360x270 사이즈의 명함 이미지를 두 가지 interpolation으로 변환한 결과 이미지입니다. Bilinear, Lanczos interpolation을 거친 두 결과 이미지를 보면 Lanczos interpolation 결과가 조금 더 선명하게 나타납니다. 하지만 두 결과 모두 좋지 않은 해상도를 보여주고 있습니다.</p><p>사실 Interpolation은 Opencv에서 이미지 사이즈를 축소, 확대 시킬때 사용하는 방법으로 Super Resolution으로 부르기 애매한 부분이 있으며 Interpolation을 통한 Resize는 좋은 결과를 얻기 어려운 부분이 있습니다. 또 다른 Super-Resolution solution으로는 저해상도 이미지를 만드는 Image degradation model을 정의하고 Inverse problem으로 접근하는 방법이 존재합니다.</p><h4>Denoising</h4><p>De-noising Task를 해결하는 전통적 방법 중 필터링을 이용해 노이즈를 제거하는 방법이 존재합니다. 대중적인 방법으로는 Gaussian, Bilateral, Median filtering, Non-Local means filtering 등이 존재합니다.</p><ol><li>Gaussian filtering : Gaussian filtering은 현 픽셀값과 이웃 픽셀 값들의 가중 평균을 이용해 현재 픽셀 값을 교체하는 방법입니다. Gaussian filtering은 이미지가 공간적으로 천천히 변하기 때문에 가까이 있는 픽셀들은 비슷한 값을 갖는다는 가정하에 만들어진 방법으로, 현재 픽셀에 가까울 수록 더 큰 가중치 값을 갖고 멀수록 작은 가중치 값을 갖습니다. 하지만 이 방법은 Edge 같은 부분을 뭉그러트리기 때문에 노이즈 제거에는 단점으로 나타납니다.</li><li>Bilateral filtering : Gaussian filtering을 보완한 방법으로 Bilateral filtering이 등장했는데, Edge 정보를 보존하면서 노이즈를 제거하는 방법으로 현재 픽셀과 이웃 픽셀 사이의 거리와 픽셀 값의 차이를 동시에 가중치에 반영하여 픽셀간의 거리만 반영한 Gaussian filtering을 보완한 방법입니다.</li><li>Non-Local means filtering[1] : 2번의 Bilateral filtering 방법도 문제점이 있습니다. Bilateral filtering은 픽셀간 거리와 픽셀 값 차이를 사용하는데, 픽셀값만을 비교하게 되어 위치적 요소를 고려하지 못하기 때문에 너무 심한 노이즈를 갖는 이미지의 경우 픽셀의 평균을 구해 연산하는데 문제점이 존재합니다. 이런 Bilateral Filtering 을 보완한 방법으로 Non-Local means filtering이 등장합니다. Non-Local means filtering은 비교하고자 하는 지점의 픽셀만 보는 것이 아니라 해당 픽셀에 대한 주변을 patch로 잘라내어 patch 사이의 거리를 계산합니다. patch까지 비슷하다고 판단되면 두 픽셀 간의 평균을 구해 노이즈를 제거하는 방식으로 진행됩니다. 따라서 Bilateral filtering과 비슷하지만 보완되어 성능이 뛰어나며, 딥러닝이 등장하고 Restoration에 적용되기 전까지 대중적으로 사용되어 왔습니다. 그림 7은 노이즈가 있는 4032x3024 명함 이미지에 대해 Non-Local means filtering을 적용한 결과에 대한 이미지입니다.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kXqnqYL1q0TSsTmh.png" /><figcaption>그림 7. 명함 이미지 Non Local Means Filtering 테스트 결과</figcaption></figure><h4>Inpainting</h4><p>Inpainting이란 이미지의 손상, 열화, 누락된, 가려진, 보이지 않는 부분을 채워 완전한 이미지를 복원, 생성하는 것을 의미합니다. 전통적 Inpainting 방법으로는 Patch 기반, Diffusion 기반 두 가지 방법으로 볼 수 있습니다.</p><p>Patch 기반의 방법은 이미지에서 손상되지 않는 부분에서 가장 일치하는 후보 패치를 찾아 손상된 위치에 복사하는 방법입니다. Diffusion 기반의 방법은 이미지 컨텐츠에서 누락된 영역의 경계로부터 시작하여 누락된 영역 내부로 점차 채워가는 방법을 의미합니다.</p><h3>3. 딥러닝 기반 이미지 복원 방법</h3><p>딥러닝이 등장하고 발전함에 따라 전통적인 복원 방법에서 딥러닝을 활용한 이미지 복원 방법이 등장하기 시작했습니다.</p><h4>SRCNN[2]</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*95rPafYvI8XmDNRZ.png" /><figcaption>그림 8. SRCNN의 전체 구조</figcaption></figure><p>본 모델은 지도학습 모델이며, 저해상도 이미지와 고해상도 정답 이미지를 제공한 후 모델이 변환한 고해상도 이미지와 정답 이미지 간의 차이를 좁히도록 학습하는, 정답에 가까워지도록 하는 맵핑 함수를 찾는 모델입니다. 그림 8은 SRCNN의 전체 구조입니다. Bicubic interpolation으로 고해상도 이미지 사이즈와 동일하게 만든 후 임의의 Patch size 기준으로 분할하여 각 Patch의 High resolution에서의 Convolution 연산을 통해 추정합니다. 이후 Convolution 연산을 통해 고해상도 이미지로 Reconstruction 하는 구조로 설계된 모델입니다.</p><h4>DnCNN[3]</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sbT_PJ8WdAsgeoF9.png" /><figcaption>그림 9. DnCNN의 전체 구조</figcaption></figure><p>위 SRCNN은 Super-Resolution task에 대한 모델이며 DnCNN은 Denoising task에 대한 모델입니다. 그림 9는 DnCNN의 전체 구조입니다. 정답 이미지에 Noise를 입혀 Noisy image를 생성하고 CNN 네트워크를 통해 Residual Image를 생성한 다음 정답 이미지와 평균제곱오차(MSE) 계산을 통해 차이를 학습하는 모델입니다.</p><h4>SRGAN[4]</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mwbaYJh1xSv1hYt1.png" /><figcaption>그림 10. SRGAN의 전체 구조</figcaption></figure><p>본 모델은 처음으로 GAN을 이용하여 Super-Resolution task에 적용한 논문입니다. Generator는 저해상도 이미지를 고해상도 이미지로 만들고, Discriminator가 생성된 고해상도 이미지와 정답 고해상도 이미지를 판별하며, 진짜를 가려내는 학습을 진행하는 구조의 모델입니다.</p><h4>SwinIR[5]</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MvB19eQpYZHQ-ZRX.png" /><figcaption>그림 11. SwinIR의 전체 구조</figcaption></figure><p>본 논문은 21년도에 나온 논문에 해당 연도 기준으로 Super-Resolution, Denoising Task에 대해 SOTA를 달성한 논문입니다. Vision transformer를 이용한 Image Restoration 모델들은 보통 이미지를 Patch 단위로 나누어 각 Patch 독립적으로 보아서 패치 단위에서의 이웃 픽셀에 대한 정보를 이용하지 못한 문제점이 있습니다. 본 논문에서는 Patch 단위에서만 Attention이 이루어지는 것이 문제점으로 보고 Patch 간의 Attention이 가능하도록 하는 Swin transformer 구조의 Image reestoration 모델입니다. 하지만 Transformer 모델이기에 리소스나 추론 시간 등 상대적으로 더 많은 자원을 사용하기에 실 서비스에 사용되기 위해서는 고려되야 할 점이 많습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NkqkCcolwueahoBH.png" /><figcaption>그림 12. SwinIR 테스트 결과</figcaption></figure><p>그림 12는 저해상도의 테스트 이미지를 SwinIR 모델의 입력으로 하여 추론된 출력 간 비교 이미지입니다. 위에 Interpolation으로 얻은 결과와 눈으로도 확연히 보이는 좋은 결과를 보여줍니다.</p><h4>NAFNet[6]</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_gtqTEhrrE2E_VET.png" /><figcaption>그림 13. NAFNet baseline Architecture(좌), GLU 기반의 NAFNet Activation Fuction(우)</figcaption></figure><p>22년도 ECCV에 발표된 논문으로 Denoising, Deblurring Task에 대한 이미지 복원 논문입니다. 새로운 모델 디자인을 제안하는 논문이 아닌, UNet[7] 모델 구조를 채택하여 Gated Linear Unit을 활용해 비선형 활성화 함수를 사용하지 않고 Denoising, Deblurring Task를 해결하는 논문입니다. 현재 Denoising, Deblurring Task에서 SOTA를 달성한 모델입니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nTXVr14N2CGCOOgE.png" /><figcaption>그림 14. 노이즈 명함 이미지, 흐릿한 명함 이미지에 대한 NAFNet 모델 출력 결과</figcaption></figure><p>그림 14는 논문 저자가 학습한 NAFNet Pre-trained Model로 노이즈가 존재하는 명함 이미지와 흐릿한 명함 이미지를 각각 추론한 결과입니다. 아주 깨끗한 또는 글자가 선명하게 복원된 이미지는 아니지만 입력 이미지로부터 어느 정도 좋은 복원 결과를 보여주고 있습니다.</p><h3>4. 결론</h3><p>지금까지 전통적인 이미지 복원 방법부터 딥러닝 기반의 이미지 복원 방법을 간단하게 살펴보았습니다. 딥러닝 기반의 이미지 복원 방법들이 기존 전통적인 방법에 비해 월등한 성능을 보여주고 있지만, 다만 각각 해결해야 하는 문제에 대한 필요 정도의 성능, 리소스, 시간 등을 고려하여 가장 적절한 방법론을 선택하고 적용하는 것이 좋은 방향으로 판단됩니다.</p><p>리멤버 빅데이터 센터 AI Lab에서는 꾸준히 최신 연구를 활용하여 인재 추천 서비스, 광고 추천 서비스, 명함 인식 등 다양한 연구를 수행하고 계속해서 블로그에 글을 포스팅하려고 하고 있습니다. 지속적인 관심 부탁드립니다.</p><p>궁금하신 사항은 댓글을 통해 문의 부탁드리며 긴 글 읽어주셔서 감사합니다.</p><h3>Reference</h3><p>[1] Buades, Antoni, Bartomeu Coll, and J-M. Morel. “A non-local algorithm for image denoising.” <em>2005 IEEE computer society conference on computer vision and pattern recognition (CVPR’05)</em>. Vol. 2. Ieee, 2005.APA</p><p>[2] Dong, Chao, et al. “Image super-resolution using deep convolutional networks.” <em>IEEE transactions on pattern analysis and machine intelligence</em> 38.2 (2015): 295–307.</p><p>[3] Zhang, Kai, et al. “Beyond a gaussian denoiser: Residual learning of deep cnn for image denoising.” <em>IEEE transactions on image processing</em> 26.7 (2017): 3142–3155.</p><p>[4] Ledig, Christian, et al. “Photo-realistic single image super-resolution using a generative adversarial network.” <em>Proceedings of the IEEE conference on computer vision and pattern recognition</em> . 2017.</p><p>[5] Liang, Jingyun, et al. “Swinir: Image restoration using swin transformer.” <em>Proceedings of the IEEE/CVF International Conference on Computer Vision</em> . 2021.</p><p>[6] Chen, Liangyu, et al. “Simple baselines for image restoration.” <em>arXiv preprint arXiv:2204.04676</em> (2022).</p><p>[7] Ronneberger, Olaf, Philipp Fischer, and Thomas Brox. “U-net: Convolutional networks for biomedical image segmentation.” <em>International Conference on Medical image computing and computer-assisted intervention</em>. Springer, Cham, 2015.</p><p><em>Originally published at </em><a href="https://blog.dramancompany.com"><em>https://blog.dramancompany.com</em></a><em> on November 18, 2022.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7b52084d1f1a" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/%EB%A6%AC%EB%A9%A4%EB%B2%84-%EC%9C%A0%EC%A0%80%EC%97%90%EA%B2%8C-%EB%B3%B4%EB%8B%A4-%EA%B9%A8%EB%81%97%ED%95%9C-%EB%AA%85%ED%95%A8-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%9C%EA%B3%B5%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B3%B5%EC%9B%90-%EB%B0%A9%EB%B2%95-7b52084d1f1a">리멤버 유저에게 보다 깨끗한 명함 이미지 제공을 위한 이미지 복원 방법</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  122.        </item>
  123.        <item>
  124.            <title><![CDATA[AI 명함 촬영 인식 ‘리오(RIO)’ 적용기 1부 — 명함촬영인식 위한 Instance Segmentation & Computer Vision]]></title>
  125.            <link>https://tech.remember.co.kr/ai-%EB%AA%85%ED%95%A8-%EC%B4%AC%EC%98%81-%EC%9D%B8%EC%8B%9D-%EB%A6%AC%EC%98%A4-rio-%EC%A0%81%EC%9A%A9%EA%B8%B0-1%EB%B6%80-%EB%AA%85%ED%95%A8%EC%B4%AC%EC%98%81%EC%9D%B8%EC%8B%9D-%EC%9C%84%ED%95%9C-instance-segmentation-computer-vision-80fafb05ea37?source=rss----307f82e3ebfb---4</link>
  126.            <guid isPermaLink="false">https://medium.com/p/80fafb05ea37</guid>
  127.            <category><![CDATA[ai]]></category>
  128.            <category><![CDATA[image-processing]]></category>
  129.            <dc:creator><![CDATA[Remember tech]]></dc:creator>
  130.            <pubDate>Mon, 13 Jan 2025 07:41:51 GMT</pubDate>
  131.            <atom:updated>2025-01-15T02:24:32.139Z</atom:updated>
  132.            <content:encoded><![CDATA[<h3>AI 명함 촬영 인식 ‘리오(RIO)’ 적용기 1부 — 명함촬영인식 위한 Instance Segmentation &amp; Computer Vision</h3><p>안녕하세요. 빅데이터센터 AI Lab 강민석입니다.</p><p>리멤버의 명함 촬영 인식은 유저가 명함을 등록하기 위한 촬영 순간에 명함을 인식하고 배경이 제거된 명함만을 사용자에게 보여주는 기술 입니다. 지금 이 시간에도 많은 사용자들이 명함을 촬영하고 있어 리멤버에서 더 정확하고 선명한 명함을 사용자에게 제공하고자 꾸준히 노력을 해왔습니다.</p><p>이번에 포스팅에서 소개할 <strong>명함 촬영 인식 AI 모델 ‘리오(RIO)’</strong> 는 기존의 전통적인 컴퓨터 비전 기술인 Edge Detection, Hough Transform과 같은 기술에서 Deep Learning을 활용한 Instance Segmentation 기술로의 교체를 통해 사용자에게 더 다양한 환경에서 촬영할 수 있게 하고 선명하고 깨끗한 명함을 사용자에게 제공하고자 합니다.</p><p>이 AI 명함 촬영 인식 ‘리오’ 적용기 포스팅은 1부와 2부로 나누어 포스팅되어 있으며 이번 포스팅인 1부에서는 명함 촬영 인식 위한 Instance Segmentation &amp; Computer Vision 적용 방법을 다루고 2부에서는 ML Model Converter와 안드로이드 앱 적용기에 대해 포스팅 되어있습니다.</p><p>2부의 ML Model Converter와 안드로이드 앱 적용기 또한 많은 관심 부탁드립니다. 🙂</p><p><a href="https://medium.com/remember-tech/ai-%EB%AA%85%ED%95%A8%EC%B4%AC%EC%98%81%EC%9D%B8%EC%8B%9D-%EB%A6%AC%EC%98%A4-rio-%EC%A0%81%EC%9A%A9%EA%B8%B0-2%EB%B6%80-ml-model-converter%EC%99%80-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1-%EC%A0%81%EC%9A%A9%EA%B8%B0-d04287f2cb86">AI 명함촬영인식 ‘리오(RIO)’ 적용기 2부 — ML Model Converter와 안드로이드 앱 적용기</a></p><h3>명함 촬영의 다양한 환경 및 촬영 어려움</h3><p>리멤버의 기존 명함 촬영 인식 방법에서 사용자들의 꾸준한 개선 요구가 있었습니다. 그 원인은 크게 3가지 정도라고 생각됩니다. 첫 번째로는 다양한 배경에서 명함 촬영, 두 번째로는 다양한 촬영 상황, 세 번째로는 촬영 시 제약 사항이 있습니다.</p><ol><li><strong>다양한 배경에서의 명함 촬영</strong> : 많은 리멤버 앱 사용자들이 명함을 촬영할 당시에 명함을 테이블 위에 놓고 찍는 환경 찍습니다. 하지만 생각보다 많은 사용자가 차량 안, 손에 들고, 키보드 위에 놓고 명함 사진을 찍는 경우도 많다 보니 촬영에 어려움이 있었습니다.</li><li><strong>다양한 촬영 상황</strong> : 너무 어둡거나 밝은 조명에서 촬영하거나 그림자가 짙은 곳에서의 촬영, 사용자가 걷거나 움직이는 촬영 환경에서도 촬영에 어려움을 겪고 있었습니다.</li><li><strong>촬영 시 제약사항</strong> : 환경적인 요소를 제한하기 위한 근접한 명함, 배경과 대조되는 환경에서의 제약이 실제 사용자들의 재촬영 또는 촬영시간의 증가로 이어졌었습니다.</li></ol><p>이러한 3가지 요소들을 해결하고자 AI 명함 촬영 인식 ‘리오(RIO)’는 리멤버의 명함촬영을 하는 사용자에게 더욱 더 쉽고 빠르게 깨끗한 명함 촬영을 제공하고자 개발되었습니다. 이후 아래의 글에서는 AI 명함 촬영 인식 모델을 설명하고자 합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1MLpo9r5vnKlAjfZ.png" /><figcaption>그림 1. 다양한 환경 및 촬영 상황 (왼쪽-키보드 위 명함, 가운데-어두운 환경의 명함, 오른쪽-멀리서 찍은 명함)</figcaption></figure><h3>AI 명함 촬영 인식의 Task</h3><p>AI 명함 촬영 인식 리오(RIO)를 개발 하기에 앞서 다양한 문제 해결방법에 대해 리서치를 진행하게 되었습니다. 명함 촬영 인식이라는 문제를 해결하기 위한 Task들로 <strong>Rotated Object Detection, Keypoint Detection, Instance Segmentation</strong> 으로 추려졌습니다. 따라서 각 Task는 서로 장단점들이 존재하고 저희 명함에 적합한 Task는 Instance Segmentation이라 판단했습니다.</p><p>우선적으로 <strong>Rotated Object Detection</strong> 은 다른 Task에 비해 쉬운 테스크를 갖고 있어 학습에 유리하고 명함의 방향까지 찾아 줄 수 있다는 장점이 있습니다. 하지만 명함 촬영 시에 카메라 특성 때문에 실제 사물이 이미지에 투영되는 2차원 이미지로 표현되는데 이는 명함이 직사각형으로 표현되지 않는 한계를 갖는 것을 의미하며 저희가 해결하고자 하는 문제를 해결할 수 없었습니다.</p><p><strong>Keypoint Detection</strong> 은 명함의 4개의 점을 바로 찾아 사용자에게 제공되므로 모델 결과물에 대한 후처리가 따로 필요 없다는 장점을 갖고 있습니다. 하지만 Keypoint Detection으로 찾는 4개의 점의 정확도와 리멤버 촬영시 원하는 정확도보다는 못 미치는 결과를 얻었습니다. 또한, 동그라미 및 다각형등의 다양한 명함 Shape를 표현하지 못하는 문제가 존재 했습니다.</p><p><strong>Instance Segmentation</strong> 의 경우 실제 사물이 이미지에 투영되는 특성에 상관없이 다양한 모양의 명함의 Shape을 찾아주고 위에 설명한 다른 Task들보다 정확하게 명함을 찾아주는 결과를 보여줬습니다. 하지만 Segmentation 결과 자체를 사용하지 않고 추가적인 후처리를 통해 사용자에게 제공해주어야 했습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4Q7gU2f4yocHZewr.png" /><figcaption>그림 2. Rotated Object Detection[1], Keypoint Detection[2], Instance Segmentation[3] 각 Task의 결과</figcaption></figure><h3>AI 명함 촬영 인식 모델의 파이프 라인</h3><p>이번에 새롭게 배포되는 AI 명함 촬영 인식 모델 ‘리오(RIO)’는 아래의 흐름으로 입력된 이미지를 처리하게 됩니다. 사용자에게 실시간으로 명함을 찾아 사용자 화면에 표현해주기 위해 명함의 위치를 <strong>RIO Detector</strong>가 이미지 위에 동그라미로 표현해주게 됩니다. 사용자가 촬영 버튼을 클릭하게 되면 촬영된 명함 이미지를 <strong>RIO Segmentor</strong> 를 통해 명함의 영역을 찾아내고 사용자에게 명함 이미지만으로 보이게끔 배경을 잘라내고 명함의 형태를 변형하여 명함의 이미지를 보여주게 됩니다. 아래의 내용에서 기능을 하나씩 설명하겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-5W7d5jlU1ogU3CQ.png" /><figcaption>그림 3 AI 명함 촬영 인식 모델의 파이프 라인</figcaption></figure><h3>RIO Detector(Object Detection)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/262/0*kS82rRW5xb2eZuFm.gif" /><figcaption>그림 4. RIO Detector 적용된 리멤버 앱</figcaption></figure><p><strong>RIO Detector</strong> 는 사용자가 촬영한 이미지에서 명함의 위치 및 크기를 직사각형 형태로 찾는 작업입니다. 이는 사용자에게 명함의 위치를 인지하게 해주고 명함의 촬영이 준비 되었다는 의미를 갖습니다. 명함 촬영시에 가이드 역할을 합니다.</p><h3>RIO Segmentor(Instance Segmentation)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2-scFz9ED5InOUsd.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9WvQ0NmqTZYwwMiq.png" /><figcaption>그림 5. 이진화된 명함의 영역(세그멘테이션 결과) 그림 6. 원본 이미지와 세그멘테이션 매핑(예상) 결과</figcaption></figure><p><strong>RIO Segmentor</strong> 는 사용자가 촬영 버튼 클릭 시 동작하게 됩니다. 사용자가 촬영한 이미지에서 입력 이미지의(640x640) 모든 픽셀에 대해 명함 인지 아닌지를 판단하여 이진화된 명함의 영역을 찾게 됩니다. RIO Segmentor로 찾게 된 이진화된 명함의 영역(명함의 Segment)을 후처리를 통하여 정제된 명함 이미지로 변환하게 됩니다.</p><h3>Post-Processing</h3><p>RIO Segmentor로 찾아낸 이진화된 명함의 영역(명함의 Segment)을 통해 사용자에게 보여주는 명함 이미지로 변환하는 일련의 작업을 <strong>Post-Processing</strong> 이라고 부르고, 내부적으로 외곽선 검출→최소 넓이 사각형 검출→최소 거리 꼭짓점 검출→투영 변환 순으로 진행하게 됩니다.</p><h4>외곽선 검출 (Contour)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*odUBhiA3yjP5PR9V.png" /><figcaption>그림 6. 명함이미지의 외곽선 검출 예시</figcaption></figure><p>RIO Segmentor으로 찾게 된 이진화된 명함의 영역(Segment)의 가장 밖에 있는 외곽선을 찾는 작업입니다. OpenCV의 <strong>findContours()</strong> 를 활용하여 외곽선을 찾습니다.</p><h4>최소 넓이 직사각형 검출 (Minimum Area Rectangle)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1DkFDUjW-pU6A4oj.png" /><figcaption>그림 7. minAreaRect() 결과 이미지 — 왼쪽 OpenCV Contour Feature 예시[4], 오른쪽 명함이미지 결과 예시</figcaption></figure><p>OpenCV 함수인 <strong>minAreaRect()</strong> 를 사용하여 명함의 영역(Segment)을 감싸는 <strong>Minimum Area Rectangle(최소 넓이 직사각형)</strong> 을 찾습니다. 위 그림에서 빨간색의 사각형이 회전을 포함한 최소 넓이를 갖는 직사각형입니다. 연두색의 사각형이 일반적으로 사용되는 Bounding Box(Object Detection의 결과)를 나타내고 회전이 없는 최소 넓이를 갖는 직사각형입니다.</p><h4>최소 거리 꼭짓점 검출 (Minimum Distance Point)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZrtTYoNfoUD0I6KG.png" /><figcaption>그림 8. 최소거리 포인트 검출 방법</figcaption></figure><p><strong>최소거리 꼭짓점 검출</strong> 은 외곽선과 최소 넓이 직사각형을 활용해 최종적인 명함의 4개의 꼭짓점을 찾는 작업입니다. 찾아낸 최소 넓이 직사각형의 4개의 꼭짓점(위 그림의 파란색 동그라미)과 외곽선을 이루는 점 간의 가장 가까운 4개의 꼭짓점(빨간색 동그라미)을 찾게 됩니다. 찾아낸 4개의 점(빨간색 동그라미)을 명함의 꼭짓점이라고 판단합니다.</p><h4>투영 변환 (Projection Transform)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*rI9K2PFSzDCb2uG9.png" /><figcaption>그림 9. 명함 이미지의 투영 변환 예시</figcaption></figure><p><strong>투영 변환</strong>은 사용자가 촬영한 사진 속 명함을 2차원 평면으로 펴주는 작업으로 OpenCV 함수인 <strong>getPerspectiveTransform()</strong> 를 활용해 투영 변환 행렬을 구하고 <strong>warpPerspective()</strong> 를 통해 촬영된 이미지를 명함을 명함 사이즈라고 판단되는 정방향으로 회전된 최소 넓이 직사각형의 형태로 변형하여 사용자에게 정제된 명함 이미지로 보이게 됩니다.</p><h3>AI 명함촬영인식 ‘리오(RIO)’ 적용기 1부를 맺으며</h3><p><strong>AI 명함촬영인식 ‘리오(RIO)’ 적용기 1부</strong>에서는 AI 명함촬영인식 ‘리오(RIO)’를 개발하게 된 이야기를 시작으로 명함촬영에서 해결하고자 하는 문제를 풀기 위한 다양한 방법들은 검토하고 리멤버 만의 명함촬영인식 방법으로 고안한 AI 명함촬영인식 ‘리오(RIO)’의 방법을 설명했습니다. <strong>AI 명함촬영인식 ‘리오(RIO)’ 적용기 2부 — ML Model Converter와 안드로이드 앱 적용기</strong> 에서는 AI 명함촬영인식 ‘리오(RIO)’의 파이프 라인을 안드로이드에 적용하는 방법들과 다양한 시도들을 공유하고자 합니다.</p><p>리멤버 빅데이터 센터 AI Lab에서는 꾸준히 최신 연구를 활용하여 인재 추천 서비스, 광고 추천 서비스, 명함 인식 등 다양한 연구를 수행하고 계속해서 블로그에 글을 포스팅하려고 하고 있습니다. 지속적인 관심 부탁드립니다. 궁금하신 사항은 댓글을 통해 문의 부탁드리며 긴 글 읽어주셔서 감사합니다.</p><h3>Reference</h3><p>[1] Zhang, Luyang, et al. “Constraint Loss for Rotated Object Detection in Remote Sensing Images.” <em>Remote Sensing</em> 13.21 (2021): 4291.</p><p>[2] Xu, Yufei, et al. “ViTPose: Simple Vision Transformer Baselines for Human Pose Estimation.” <em>arXiv preprint arXiv:2204.12484</em> (2022).</p><p>[3] Bolya, Daniel, et al. “Yolact: Real-time instance segmentation.” <em>Proceedings of the IEEE/CVF international conference on computer vision</em>. 2019.</p><p>[4] OpenCV: Contour Features Official Tutorials</p><p><em>Originally published at </em><a href="https://blog.dramancompany.com"><em>https://blog.dramancompany.com</em></a><em> on November 16, 2022.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=80fafb05ea37" width="1" height="1" alt=""><hr><p><a href="https://tech.remember.co.kr/ai-%EB%AA%85%ED%95%A8-%EC%B4%AC%EC%98%81-%EC%9D%B8%EC%8B%9D-%EB%A6%AC%EC%98%A4-rio-%EC%A0%81%EC%9A%A9%EA%B8%B0-1%EB%B6%80-%EB%AA%85%ED%95%A8%EC%B4%AC%EC%98%81%EC%9D%B8%EC%8B%9D-%EC%9C%84%ED%95%9C-instance-segmentation-computer-vision-80fafb05ea37">AI 명함 촬영 인식 ‘리오(RIO)’ 적용기 1부 — 명함촬영인식 위한 Instance Segmentation &amp; Computer Vision</a> was originally published in <a href="https://tech.remember.co.kr">remember-tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
  133.        </item>
  134.    </channel>
  135. </rss>

If you would like to create a banner that links to this page (i.e. this validation result), do the following:

  1. Download the "valid RSS" banner.

  2. Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)

  3. Add this HTML to your page (change the image src attribute if necessary):

If you would like to create a text link instead, here is the URL you can use:

http://www.feedvalidator.org/check.cgi?url=https%3A//tech.remember.co.kr/feed

Copyright © 2002-9 Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda