<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>CodePlay</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 개발자</description>
        <lastBuildDate>Fri, 20 Feb 2026 08:18:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>CodePlay</title>
            <url>https://velog.velcdn.com/images/dev_choco/profile/68d60af1-3dad-4b5b-a72a-850ffb628909/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. CodePlay. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_choco" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Jotai 파생 atom으로  useEffect 안티패턴 제거하기]]></title>
            <link>https://velog.io/@dev_choco/Jotai-%ED%8C%8C%EC%83%9D-atom%EC%9C%BC%EB%A1%9C-useEffect-%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_choco/Jotai-%ED%8C%8C%EC%83%9D-atom%EC%9C%BC%EB%A1%9C-useEffect-%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 20 Feb 2026 08:18:47 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>Jotai를 사용하다 보면 &quot;atom에 값을 저장하고, 컴포넌트에서 그 값을 읽는다&quot;는 단순한 흐름에서 시작해요. </p>
<p>그러다 보니 서로 다른 atom의 값을 조합해서 새로운 값을 만들어야 할 때, useEffect로 계산 후 또 다른 atom에 저장하는 안티패턴을 택하게 되는 경우가 있습니다.</p>
<p>저도 그랬던 경험이 있어 파생 atom으로 코드를 개선한 경험을 공유합니다.</p>
<h2 id="문제">문제</h2>
<p>상품 예약 화면에서 최종 할인가를 계산하는 <code>ProductDiscount</code> 컴포넌트가 있었어요.</p>
<p><code>calcDiscountPrice()</code> 라는 계산 함수가 두 개의 useEffect에서 각각 호출되고 있었습니다.</p>
<pre><code class="language-javascript">// ❌ 문제 코드

// 계산 로직
const calcDiscountPrice = () =&gt; {
  ... 계산값 
  setFinalPriceData({ percent: ..., amount: finalPrice });
};

// 1번 실행: 데이터 로딩 완료 시
useEffect(() =&gt; {
  if (discountIsLoading === false) {
    calcDiscountPrice();                    // 🔴 1회
    setSelectedCouponId(firstValidCoupon);  // → selectedCouponId 변경!
  }
}, [discountIsLoading, period]);

// 2번 실행: 선택한 쿠폰이 바뀌면 또 실행
useEffect(() =&gt; {
  calcDiscountPrice(); // 🔴 2회 (중복 계산)
}, [selectedCouponId]);</code></pre>
<p><code>discountIsLoading === false</code> 시점에 첫 번째 effect가 <code>setSelectedCouponId</code>를 호출하고, 이 상태 변경이 두 번째 effect를 연쇄적으로 트리거하기 때문입니다.</p>
<p>사이드 이펙트가 사이드 이펙트를 낳는 구조였죠.</p>
<h2 id="파생-상태derived-atom-추가">파생 상태(Derived Atom) 추가</h2>
<p>해결 방향은 단순합니다.
&quot;계산을 effect에서 하지 말고, atom 정의 시점에 명세하자&quot;</p>
<p>Jotai는 Read Only atom 을 통해 다른 atom의 값을 구독하고, 자동으로 재계산되는 파생 atom을 만들 수 있어요.</p>
<blockquote>
<p><a href="https://tutorial.jotai.org/quick-start/readonly-atoms">https://tutorial.jotai.org/quick-start/readonly-atoms</a></p>
</blockquote>
<p>먼저 계산의 입력이 되는 atom을 정의합니다.</p>
<pre><code class="language-javascript">
// 입력 atom
export const discountCalculationData = atom(null);
export const selectedCouponId = atom(null);</code></pre>
<p>이제 <code>finalPriceData</code>는 입력 atom들을 구독하는 파생 atom이 됩니다.</p>
<pre><code class="language-javascript">// ✅ 파생 atom: discountCalculationData나 selectedCouponId가 바뀌면 자동 재계산
export const finalPriceData = atom((get) =&gt; {
  const calcData = get(discountCalculationData);
  if (calcData === null) return { percent: 0, amount: 0 };

  const { priceData, ... } = calcData;
  // 분리된 계산 로직
  return calcFinalPriceResult(priceData);
});</code></pre>
<p>컴포넌트의 두 useEffect는 하나로 합쳐집니다.</p>
<pre><code class="language-javascript">// ✅ 개선 코드: useEffect 1개, calcDiscountPrice 함수 없음
useEffect(() =&gt; {
  if (discountIsLoading === false) {
    setDiscountCalculationData({
      // data fetching으로 얻은 계산 값들
      priceData,
      ...
    });
    setSelectedCouponId(firstValidCouponId); // 이후 파생 atom이 자동 재계산
  }
}, [discountIsLoading, period]);</code></pre>
<h2 id="효율성-측면에서-얻은-점">효율성 측면에서 얻은 점</h2>
<h3 id="1-계산-횟수-감소">1. 계산 횟수 감소</h3>
<p>기존에는 데이터 로딩 완료 시 계산 로직인 <code>calcDiscountPrice()</code> 가 무조건 두 번 실행 됐습니다.
파생 atom은 구독하는 atom이 변경될 때만 재계산 됩니다.</p>
<h3 id="2-데이터-흐름이-단방향으로-정리">2. 데이터 흐름이 단방향으로 정리</h3>
<pre><code>// 기존: 상태 → effect → 계산 → 상태 → effect → 계산 (순환)
discountIsLoading 변경
  → calcDiscountPrice() + setSelectedCouponId()
    → [selectedCouponId 변경]
      → calcDiscountPrice() (중복)

// 개선: 입력 atom 변경 → 파생 atom 자동 계산 (단방향)
discountCalculationData 변경
selectedCouponId 변경
  → finalPriceData 자동 재계산 (1회)</code></pre><p>side effect 남용으로 순환하던 데이터 흐름이 단방향으로 정리됐습니다.</p>
<h3 id="3-계산-로직의-테스트-가능성">3. 계산 로직의 테스트 가능성</h3>
<p>계산 로직이 <code>calcFinalPriceResult()</code> 함수로 분리되어,
atom 의존성 없이 단독으로 테스트할 수 있습니다.</p>
<hr>
<p>상태 관리 라이브러리를 쓸 때,
&quot;어떻게 저장할까&quot;보다 &quot;어떻게 계산을 정의할까&quot;를 먼저 생각하고
파생 상태가 그 답이 되는 경우가 될 수 있음을 유념 해야 겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 회고]]></title>
            <link>https://velog.io/@dev_choco/2025%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev_choco/2025%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 31 Dec 2025 00:59:38 GMT</pubDate>
            <description><![CDATA[<p>(지피티가 만들어준 이미지. &quot;버거운데, 진지하게 임하고 있고, 귀찮다고 던지진 않는다&quot; 라는 상태라고 합니다 😂)</p>
<p>올해는 유독 빠르게 지나간 것 같아요.
일 년 동안 개인적으로, 그리고 커리어 적으로 무엇을 했는지 돌아보며
앞으로의 목표를 세워보고자 합니다.</p>
<h3 id="취미-활동과-건강관리">취미 활동과 건강관리</h3>
<p>작년에는 유독 몸이 안좋았어요. 디스크가 좋지 않아 평소 생활이 힘들 정도로 두통을 겪으면서 운동까지 멀리하게 되니 더더욱 안좋아지기만 하고 자기 관리를 할 힘을 잃었었어요.
올해는 마음을 다잡고 몸의 유지보수를 했어요. </p>
<p>개인 필라테스 수업을 듣고, 자유수영을 하면서 체력을 키우니 취미활동을 할 여력이 생겼습니다. 
연말에 취미로 하는 사회인 밴드 공연을 성공적으로 마쳤고, 미디 프로그램도 조금씩 배우면서 재밌게 잘 보낼 수 있는 한 해가 됐습니다. 덤으로 취미를 살려 토이프로젝트를 만들어보는 경험도 얻었습니다.</p>
<h3 id="이력서-업데이트-및-sns-활동">이력서 업데이트 및 SNS 활동</h3>
<p>한 회사에서 오래 재직하다보니 이력서를 업데이트 할 생각을 하지 못했어요.
필요가 없는 상태에서 업데이트를 하자니 무엇부터 써야할지 감이 잘 안 잡히더라구요.
다행히 이전에 커리어 관련 강의를 참고해 개인 노션에 적어놓던 스케쥴 표가 남아있어 올해 내가 무엇을 했는지는 파악할 수 있었습니다.</p>
<p>업데이트를 하면서 든 생각은, 그동안 나라는 사람을 시장에 내놓을 생각이 너무 없었구나 하는 반성이었어요.
그래서 올해 하반기부터 링크드인을 통해 현업에 계신 훌륭한 분들의 아티클도 읽고, 제가 공부하거나 얻은 인사이트 들을 조금씩 써올려보기 시작했어요.
링크드인에 글을 올리는 것은 꽤 부끄럽지만 꾸준히 글쓰기를 할 수 있고, 또 다른 회사에 계신 분들과 커피챗을 할 기회를 만들어 주기도 해서 시작하길 잘 했다고 생각합니다.</p>
<h3 id="커리어에-대한-고민">커리어에 대한 고민</h3>
<p>5년차 개발자로서 동력이 너무 부족한가 하는 고민을 안고 있었습니다.
개인과 회사가 전부 정체 되어있다는 생각이 들어서 외부에 눈을 돌려보는 시간을 가지기도 했습니다.</p>
<p>그게 티가 났는지 저희 PO 이신 광일님 께서 커피챗을 하면서 회사에 대한 생각을 물어보기도 하셨어요.
프론트엔드를 좀 더 개선하고 싶은 욕심과 팀 차원에서 동의를 얻어야하는 부분 등을 이야기 할 수 있었고, 그 과정에서 내가 리본즈에 필요한 사람이구나 하는 확인을 받게 됐어요.</p>
<p>광일님께서는 저를 &quot;거절할 수 있는 개발자&quot;라고 평가해주셨는데요, 거절이라는 부정적인 뉘앙스가 아닌 요구사항에 대한 합리적인 우선순위 판단을 사업팀에 전달할 수 있는 능력이라고 하셨습니다.
같은 목표를 가진 조직 안에서 업무의 우선순위를 정하고 수행하는 것이 늘 어려운 일이었는데 그렇게 이야기해주셔서 정말 감사한 일이었어요.</p>
<h3 id="2026년의-목표">2026년의 목표</h3>
<p>지금처럼 꾸준히 배운 것들을 정리 하면서 주위를 보려고합니다. 아직은 미숙해서 인사이트를 드릴만한 정도는 아닌 것 같다는 생각에 수동적인 상태였지만 용기내서 외부 분들이랑 더 커피챗을 해보고 싶어요.</p>
<p>그리고 내년에는 취미활동을 좀 줄이고 업무 역량을 더 단단히 다져보려고 합니다.
관심있던 three js 도 건드려보고, 이전에 만들던 토이프로젝트의 개선점들을 보강해서 실제로 앱 출시를 해보기도 하면서 기술에 대한 공부를 놓지않고, 요즘같이 AI와 공존해야하는 시대에 멀티플레이어가 되기 위해 Figma 기초와 PM 실무 기초 교육을 듣는 등 서비스를 위한 넓고 얕은 지식을 습득해 보려고합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Rails 번들러 마이그레이션 전략 : Vite Ruby ]]></title>
            <link>https://velog.io/@dev_choco/Rails-%EB%B2%88%EB%93%A4%EB%9F%AC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%A0%84%EB%9E%B5-Vite-Ruby</link>
            <guid>https://velog.io/@dev_choco/Rails-%EB%B2%88%EB%93%A4%EB%9F%AC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%A0%84%EB%9E%B5-Vite-Ruby</guid>
            <pubDate>Tue, 30 Dec 2025 05:00:15 GMT</pubDate>
            <description><![CDATA[<p>실무 프로덕트는 Rails 환경 내에서 webpack 설정을 사용할 수 있게 해주는 Webpacker 라는 빌드 시스템 래퍼를 사용하고 있어요. </p>
<p>하지만 Webpacker는 공식적으로 지원이 중단 되었습니다. </p>
<blockquote>
<p>Webpacker has served the Rails community for over five years as a bridge to compiled and bundled JavaScript. This bridge is no longer needed for most people in most situations following the release of Rails 7. We now have three great default answers to JavaScript in 2021+, and thus we will no longer be evolving Webpacker in an official Rails capacity.
<a href="https://github.com/rails/webpacker?tab=readme-ov-file#webpacker-has-been-retired-">https://github.com/rails/webpacker?tab=readme-ov-file#webpacker-has-been-retired-</a></p>
</blockquote>
<p>Rails 7 버전 이후 부터는 Webpacker 없이 최신 자바스크립트를 구현할 수 있게 되었기 때문입니다. </p>
<p>현재 프로덕트는 Rails 5 버전을 사용하고 있고, 프로덕트 전체 버전을 업그레이드 하는 것은 전사적인 동의와 많은 리소스가 필요하기 때문에 다른 대안을 찾아야 합니다.</p>
<h1 id="두-가지-대안">두 가지 대안</h1>
<h3 id="jsbundling-rails">jsbundling-rails</h3>
<p>Rails 자체 기능만으로 구성된 간단한 통합 레이어 입니다. 선택한 번들러로 빌드하여 app/assets/builds/에 출력하고, Sprockets가 이를 제공하는 방식이라고 합니다.</p>
<h4 id="webpacker와의-주요-차이점">Webpacker와의 주요 차이점</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>Webpacker</th>
<th>jsbundling-rails</th>
</tr>
</thead>
<tbody><tr>
<td>번들러 선택</td>
<td>Webpack만</td>
<td>Webpack, esbuild, rollup, bun 중 선택</td>
</tr>
<tr>
<td>뷰 헬퍼</td>
<td>커스텀 헬퍼 (javascript_pack_tag)</td>
<td>표준 Rails 헬퍼 (javascript_include_tag)</td>
</tr>
<tr>
<td>HMR</td>
<td>지원</td>
<td>지원 안 함</td>
</tr>
<tr>
<td>Code Splitting</td>
<td>자동 최적화</td>
<td>수동 관리 필요</td>
</tr>
<tr>
<td>설정 파일</td>
<td>Webpacker 추상화 레이어</td>
<td>번들러 표준 설정 직접 사용</td>
</tr>
<tr>
<td>Asset Pipeline</td>
<td>대체 가능</td>
<td>Sprockets와 함께 작동</td>
</tr>
</tbody></table>
<h4 id="장점">장점</h4>
<ul>
<li>Rails에 최소한의 통합만 추가</li>
<li>번들러 공식 설정 방식 그대로 사용</li>
<li>기존 webpack.config.js 재사용 가능</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>HMR 가 없어 개발 속도 저하</li>
<li>Code Splitting 을 위해 현재 프로젝트의 splitChunks + runtimeChunk 설정을 수동으로 관리해야 함</li>
<li>Webpacker보다 기능이 적음</li>
</ul>
<h3 id="vite-ruby">Vite Ruby</h3>
<p>최신 프론트엔드 빌드 도구 Vite를 Rails에 통합했습니다. 개발 시 네이티브 ESM 활용으로 빠른 HMR을 제공하며, 프로덕션에서는 Rollup 기반 최적화가 되어있습니다.</p>
<h4 id="webpacker와의-주요-차이점-1">Webpacker와의 주요 차이점</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>Webpacker</th>
<th>Vite Ruby</th>
</tr>
</thead>
<tbody><tr>
<td>개발 서버 속도</td>
<td>보통 (Webpack dev server)</td>
<td>매우 빠름 (ESM 기반)</td>
</tr>
<tr>
<td>HMR</td>
<td>지원</td>
<td>지원</td>
</tr>
<tr>
<td>빌드 도구</td>
<td>Webpack</td>
<td>Vite (개발: ESM, 프로덕션: Rollup)</td>
</tr>
<tr>
<td>설정 방식</td>
<td>webpacker.yml + webpack config</td>
<td>config/vite.json + vite.config.ts</td>
</tr>
<tr>
<td>Auto-build</td>
<td>없음</td>
<td>Dev 서버 미실행 시 자동 빌드</td>
</tr>
<tr>
<td>Import 문법</td>
<td>확장자 생략 가능</td>
<td>확장자 필수 (.js, .jsx 명시)</td>
</tr>
</tbody></table>
<h4 id="장점-1">장점</h4>
<ul>
<li>Vite의 ESM 기반 dev server로 즉각적인 HMR</li>
<li>최신 개발 경험</li>
<li>자동 빌드: Dev 서버 없이도 변경 감지 시 자동 컴파일</li>
<li>React 지원 우수: Vite의 React Fast Refresh 내장</li>
<li>CSS 처리 우수: PostCSS, Sass 등 내장 지원</li>
</ul>
<h4 id="단점-1">단점</h4>
<ul>
<li>Webpack 설정을 Vite 설정으로 완전히 재작성이 필요하여 마이그레이션 비용이 높음</li>
<li>모든 import에 확장자 추가 필요</li>
<li>Webpack loader 비호환, Vite plugin으로 대체 필요</li>
</ul>
<hr>
<h1 id="vite-ruby-1">Vite Ruby</h1>
<p>Vite Ruby로 마이그레이션하는 방안을 골랐습니다.
이유는 아래와 같습니다.</p>
<ol>
<li><p>개발 경험 우선: 현재 프로젝트는 React 18 기반의 모던 프론트엔드를 가지고 있습니다. HMR은 React 개발에서 필수적인 기능이며, jsbundling-rails는 이를 제공하지 않습니다.</p>
</li>
<li><p>Webpacker에서 사용 중인 HMR, code splitting 등의 기능을 모두 유지하면서도 더 빠른 경험을 제공합니다.</p>
</li>
<li><p>미래 지향적: Vite는 프론트엔드 생태계의 표준으로 자리잡고 있으며, 지속적인 발전이 기대됩니다.</p>
</li>
<li><p>React 프로젝트에 최적: React Fast Refresh, JSX 지원 등이 내장되어 있어 추가 설정 없이 바로 사용 가능합니다.</p>
</li>
</ol>
<p>jsbundling-rails 에서 HMR을 지원하지 않고, code splitting을 수동으로 설정해야하는 단점과,</p>
<p>Vite Ruby 선택 시얻는 빠른 HMR 로 인한 개발 경험 향상과 Vite의 지속 가능성 등이 다소 번거로운 마이그레이션 과정이 있는 단점을 상쇄한다고 판단하여 Vite Ruby를 선택했습니다.</p>
<hr>
<h1 id="마이그레이션-전략">마이그레이션 전략</h1>
<h3 id="1단계-vite-ruby-설치">1단계: Vite Ruby 설치</h3>
<p>기존 Webpacker를 유지한 채로 Vite Ruby를 추가합니다.</p>
<pre><code># Gemfile
gem &#39;vite_rails&#39;

bundle install
bundle exec vite install</code></pre><blockquote>
<p><a href="https://github.com/ElMassimo/vite_ruby/tree/main/vite_rails">https://github.com/ElMassimo/vite_ruby/tree/main/vite_rails</a></p>
</blockquote>
<p>config/vite.json, vite.config.ts, 그리고 app/javascript/entrypoints 디렉토리가 생성됩니다.</p>
<h3 id="2단계-기존-설정-이전">2단계: 기존 설정 이전</h3>
<p>Webpacker에서 사용하던 설정을 Vite 형식으로 옮깁니다.</p>
<p><strong>Import Alias 설정</strong></p>
<pre><code class="language-javascript">// vite.config.ts
import { defineConfig } from &#39;vite&#39;
import RubyPlugin from &#39;vite-plugin-ruby&#39;
import react from &#39;@vitejs/plugin-react&#39;

export default defineConfig({
plugins: [RubyPlugin(), react()],
resolve: {
  alias: {
    &#39;@&#39;: &#39;/app/javascript&#39;,
    &#39;@Atom&#39;: &#39;/app/javascript/component/Atom&#39;,
    &#39;@Molecule&#39;: &#39;/app/javascript/component/Molecule&#39;,
    &#39;@Organism&#39;: &#39;/app/javascript/component/Organism&#39;,
    &#39;@Page&#39;: &#39;/app/javascript/component/Page&#39;,
  },
},
})</code></pre>
<h3 id="3단계-entrypoint-이전">3단계: Entrypoint 이전</h3>
<p>Webpacker의 packs 디렉토리에서 Vite의 entrypoints 디렉토리로 파일을 하나씩 옮깁니다.</p>
<pre><code># Before (Webpacker)
app/javascript/packs/application.js
...

# After (Vite)
app/javascript/entrypoints/application.js
...</code></pre><p>테스트를 위해 application 이전에 레거시 부터 옮기도록  합니다.</p>
<h3 id="4단계-import-문법-수정">4단계: Import 문법 수정</h3>
<p>Vite는 ESM 표준을 따르므로 import 문법 변경이 필요합니다.</p>
<p><strong>파일 확장자 명시</strong></p>
<pre><code class="language-javascript">// Before
import Button from &#39;@Atom/Button&#39;

// After
import Button from &#39;@Atom/Button.jsx&#39;</code></pre>
<p><strong>require.context 대체</strong></p>
<pre><code class="language-javascript">// Before (Webpack)
const images = require.context(&#39;../images&#39;, true)

// After (Vite)
const images = import.meta.glob(&#39;../images/**/*&#39;, { eager: true })</code></pre>
<h3 id="5단계-뷰-헬퍼-교체">5단계: 뷰 헬퍼 교체</h3>
<p>ERB 파일에서 헬퍼 태그를 교체합니다.</p>
<pre><code>&lt;%# Before (Webpacker) %&gt;
&lt;%= javascript_pack_tag &#39;application&#39; %&gt;
&lt;%= stylesheet_pack_tag &#39;application&#39; %&gt;

&lt;%# After (Vite) %&gt;
&lt;%= vite_client_tag %&gt;
&lt;%= vite_javascript_tag &#39;application&#39; %&gt;
&lt;%= vite_stylesheet_tag &#39;application&#39; %&gt;</code></pre><p>점진적 마이그레이션 중에는 페이지별로 다른 헬퍼를 사용할 수 있습니다. 특정 페이지만 먼저 Vite로 전환하고, 안정성을 확인한 후 나머지를 전환하도록 합니다.</p>
<blockquote>
<p><a href="https://vite-ruby.netlify.app/guide/migration.html#webpacker-%F0%9F%93%A6">https://vite-ruby.netlify.app/guide/migration.html#webpacker-%F0%9F%93%A6</a></p>
</blockquote>
<h3 id="6단계-webpacker-제거">6단계: Webpacker 제거</h3>
<p>모든 entrypoint 이전이 완료되면 Webpacker를 제거합니다.</p>
<pre><code># Gemfile에서 제거
# gem &#39;webpacker&#39;

# 관련 파일 정리
rm -rf config/webpacker.yml
rm -rf config/webpack/
rm -rf bin/webpack
rm -rf bin/webpack-dev-server</code></pre><hr>
<h1 id="주의-할-점">주의 할 점</h1>
<p><strong>Sentry 등 외부 서비스 연동</strong></p>
<p>Webpack 플러그인을 사용하던 서비스들은 Vite 플러그인으로 교체해야 합니다. 
@sentry/webpack-plugin 대신 @sentry/vite-plugin을 사용합니다.</p>
<p><strong>CSS 처리 방식</strong></p>
<p>Vite는 CSS를 자동으로 처리하지만, CSS Modules 파일명 규칙(.module.scss)은 동일하게 유지됩니다.</p>
<p><strong>환경 변수</strong></p>
<p>process.env 대신 import.meta.env를 사용합니다. 클라이언트에 노출할 변수는 VITE_ 접두사가 필요합니다.</p>
<pre><code>// Before
process.env.API_URL

// After
import.meta.env.VITE_API_URL</code></pre><p><strong>개발 워크플로우 변화</strong></p>
<p>마이그레이션 후 개발 서버 실행 방식이 달라집니다.</p>
<pre><code># Before
bin/webpack-dev-server

# After
bin/vite dev</code></pre><p>bin/dev를 사용하면 Rails 서버와 Vite 개발 서버를 동시에 실행할 수 있습니다. 첫 실행 시 node_modules 사전 번들링이 진행되며, 이후부터는 수 초 내에 개발 서버가 준비됩니다.</p>
<hr>
<h1 id="정리">정리</h1>
<p>현재 프로덕트에서 Vite ruby 가 필요한 이유를 확인하고, 마이그레이션 전략과 주의할 점에 대해 알아보았습니다.</p>
<p>점진적으로 진행해보고, 추후 결과에 대해 정리하는 시간을 가져보겠습니다.</p>
<blockquote>
<p>🔗 <strong>참고 문서 및 아티클</strong>
Webpacker - <a href="https://edgeguides.rubyonrails.org/webpacker.html">https://edgeguides.rubyonrails.org/webpacker.html</a>
Rails 7에서 자바스크립트를 적용하는 방법 - <a href="https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b">https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b</a>
jsbundling-rails - <a href="https://github.com/rails/jsbundling-rails/blob/main/docs/comparison_with_webpacker.md">https://github.com/rails/jsbundling-rails/blob/main/docs/comparison_with_webpacker.md</a>
Vite Ruby - <a href="https://vite-ruby.netlify.app/guide/">https://vite-ruby.netlify.app/guide/</a></p>
</blockquote>
<blockquote>
<p><strong>관련 포스트 - ESM 이해하기</strong>
<a href="https://velog.io/@dev_choco/ESMECMAScript-Modules-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">https://velog.io/@dev_choco/ESMECMAScript-Modules-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[ESM(ECMAScript Modules) 이해하기]]></title>
            <link>https://velog.io/@dev_choco/ESMECMAScript-Modules-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_choco/ESMECMAScript-Modules-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 18 Dec 2025 04:10:46 GMT</pubDate>
            <description><![CDATA[<p>최근 Webpacker에서 Vite Ruby 로 마이그레이션을 준비하고 있는데, 준비 과정에서 &quot;ESM 기반&quot;이라는 표현을 자주 마주하게 되었습니다. Vite 공식 문서에서도 &quot;Native ESM based dev server&quot;라고 소개하고 있죠.</p>
<p>ESM이란 정확히 무엇이고, 왜 Vite는 이를 핵심 기술로 내세우는 걸까요?</p>
<p>Vite Ruby 마이그레이션을 본격적으로 시작하기 전에, ESM의 개념과 그것이 프론트엔드 개발 경험에 어떤 변화를 가져오는지 살펴보겠습니다.</p>
<h1 id="javascript-모듈의-역사">JavaScript 모듈의 역사</h1>
<p>ESM을 이해하려면 먼저 JavaScript의 모듈 시스템이 어떻게 발전해왔는지 알아야 합니다.</p>
<h3 id="모듈이-없던-시절">모듈이 없던 시절</h3>
<p>2015년 이전, JavaScript에는 공식적인 모듈 시스템이 없었습니다. 여러 파일로 코드를 나누려면 HTML에 <code>&lt;script&gt;</code> 태그를 순서대로 나열해야 했습니다.</p>
<pre><code>&lt;script src=&quot;jquery.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;utils.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;app.js&quot;&gt;&lt;/script&gt;</code></pre><p>이 방식에는 여러 문제점이 있었는데,</p>
<ul>
<li>모든 변수가 전역 스코프를 공유하고,</li>
<li>파일 간 의존성을 수동으로 관리해야 했으며,</li>
<li>로딩 순서가 뒤바뀌면 에러가 발생했습니다.</li>
</ul>
<h3 id="commonjs의-등장">CommonJS의 등장</h3>
<p>Node.js가 등장하면서 서버 사이드 JavaScript에는 CommonJS라는 모듈 시스템이 자리 잡았습니다.</p>
<pre><code class="language-javascript">// math.js
function add(a, b) {
  return a + b
}
module.exports = { add }

// app.js
const { add } = require(&#39;./math&#39;)
console.log(add(1, 2))</code></pre>
<p>require와 module.exports를 사용하는 이 방식은 Node.js 생태계의 표준이 되었습니다. 하지만 CommonJS는 동기적으로 동작하도록 설계되어 있어서 브라우저 환경에는 적합하지 않았습니다.</p>
<h3 id="번들러의-시대">번들러의 시대</h3>
<p>브라우저에서 모듈 시스템을 사용하기 위해 Browserify, Webpack 같은 번들러가 등장했습니다. 번들러는 여러 개의 JavaScript 파일을 분석하고, 하나의 큰 파일로 합쳐주는 역할을 합니다.</p>
<pre><code>app.js ─┐
        │
math.js ├─→ [Webpack] ─→ bundle.js
        │
utils.js┘</code></pre><p>개발자는 CommonJS나 다양한 모듈 문법으로 코드를 작성하고, 번들러가 이를 브라우저가 이해할 수 있는 형태로 변환해주었습니다.</p>
<p>Webpacker 역시 이러한 번들러 기반 접근 방식을 Rails에 통합한 것입니다.</p>
<hr>
<h1 id="esm이란">ESM이란?</h1>
<p>ESM(ECMAScript Modules)은 2015년 ES6 표준에 포함된 JavaScript의 공식 모듈 시스템입니다.
import와 export 키워드를 사용합니다.</p>
<pre><code class="language-javascript">// math.js
export function add(a, b) {
  return a + b
}

export function multiply(a, b) {
  return a * b
}

// app.js
import { add, multiply } from &#39;./math.js&#39;
console.log(add(1, 2))</code></pre>
<p>ESM이 특별한 이유는 이것이 언어 차원의 표준이라는 점입니다. CommonJS가 Node.js 커뮤니티의 약속이었다면, ESM은 JavaScript 언어 명세 자체에 포함된 기능입니다.</p>
<h2 id="esm의-주요-특징">ESM의 주요 특징</h2>
<h3 id="정적구조">정적구조</h3>
<p>ESM의 import와 export는 파일의 최상위 레벨에서만 사용할 수 있고, 조건문 안에 넣을 수 없습니다.</p>
<pre><code class="language-javascript">// ESM - 불가능
if (condition) {
  import something from &#39;./module.js&#39;  // SyntaxError!
}

// CommonJS - 가능
if (condition) {
  const something = require(&#39;./module&#39;)  // 동작함
}</code></pre>
<p>이러한 정적 구조 덕분에 코드를 실행하기 전에 모듈 간의 의존성을 분석할 수 있습니다. 이는 Tree-shaking 같은 최적화를 가능하게 합니다.</p>
<h3 id="비동기-로딩">비동기 로딩</h3>
<p>ESM은 비동기적으로 모듈을 로드할 수 있도록 설계되었습니다. 이는 네트워크를 통해 모듈을 가져와야 하는 브라우저 환경에 적합합니다.</p>
<h3 id="명시적인-의존성">명시적인 의존성</h3>
<p>어떤 모듈에서 무엇을 가져오는지 코드만 보고 명확하게 알 수 있습니다.</p>
<hr>
<h1 id="브라우저-네이티브-esm">브라우저 네이티브 ESM</h1>
<p>ESM의 진정한 의미는 현대 브라우저가 이를 네이티브로 지원한다는 점입니다. 별도의 번들러 없이도 브라우저가 직접 모듈을 이해하고 로드할 수 있습니다.</p>
<pre><code>&lt;script type=&quot;module&quot; src=&quot;./app.js&quot;&gt;&lt;/script&gt;</code></pre><p><code>type=&quot;module&quot;</code> 속성을 추가하면 브라우저는 해당 스크립트를 ESM으로 처리합니다.</p>
<pre><code class="language-javascript">// app.js
import { Button } from &#39;./components/Button.js&#39;
import { Header } from &#39;./components/Header.js&#39;

// 브라우저가 이 import 문을 보고 직접 해당 파일들을 요청합니다</code></pre>
<p>브라우저의 동작 과정을 살펴보면:</p>
<ol>
<li>app.js 요청</li>
<li>파일 내용을 파싱하여 import 문 발견</li>
<li>Button.js와 Header.js를 추가로 요청</li>
<li>각 파일에서 또 다른 import 발견 시 재귀적으로 요청</li>
<li>모든 의존성이 로드되면 코드 실행</li>
</ol>
<p>이 과정에서 번들러의 개입이 필요 없습니다. 브라우저가 모듈 로더 역할을 직접 수행하는 것입니다.</p>
<hr>
<h1 id="webpack과-vite-번들링-vs-네이티브-esm">Webpack과 Vite: 번들링 vs 네이티브 ESM</h1>
<p>이제 Webpack과 Vite의 근본적인 차이를 이해할 수 있습니다.</p>
<h3 id="webpack의-접근-방식">Webpack의 접근 방식</h3>
<p>Webpack은 개발 환경에서도 모든 파일을 번들링합니다.</p>
<pre><code>개발 서버 시작
     ↓
전체 애플리케이션 의존성 분석
     ↓
모든 모듈을 하나의 번들로 합침
     ↓
메모리에 번들 유지
     ↓
서버 준비 완료 (수십 초 소요)</code></pre><p>파일이 수정되면 해당 부분을 다시 번들링하고 HMR(Hot Module Replacement)을 통해 브라우저에 전달합니다. 프로젝트 규모가 커질수록 초기 번들링 시간과 수정 반영 시간이 늘어납니다.</p>
<h3 id="vite의-접근-방식">Vite의 접근 방식</h3>
<p>Vite는 개발 환경에서 번들링을 하지 않습니다. 대신 브라우저의 네이티브 ESM을 활용합니다.</p>
<pre><code>개발 서버 시작
     ↓
서버 준비 완료
     ↓
브라우저가 페이지 요청
     ↓
필요한 모듈만 요청 시점에 변환하여 제공</code></pre><p>Vite 개발 서버는 요청이 들어올 때 해당 파일만 변환합니다. 1000개의 파일이 있는 프로젝트라도 현재 페이지에서 100개만 사용한다면 100개만 처리하면 됩니다.</p>
<pre><code>// 브라우저가 보내는 요청
GET /src/App.jsx
GET /src/components/Header.jsx
GET /src/components/Button.jsx
// ... 필요한 파일만</code></pre><p>파일이 수정되면 해당 파일 하나만 다시 변환하면 됩니다. 프로젝트 전체 크기와 관계없이 HMR 속도가 일정하게 유지되는 이유입니다.</p>
<hr>
<h1 id="esm이-표준이-되기까지">ESM이 표준이 되기까지</h1>
<p>네이티브 ESM이 이렇게 좋다면 왜 Webpack이 오랫동안 표준이었을까요?</p>
<h4 id="브라우저-지원">브라우저 지원</h4>
<p>ESM을 지원하는 브라우저가 충분히 보급되기까지 시간이 필요했습니다. 2020년 이후에야 주요 브라우저들이 모두 ESM을 안정적으로 지원하게 되었습니다.</p>
<h4 id="http11의-한계">HTTP/1.1의 한계</h4>
<p>과거에는 HTTP 요청 하나하나가 비용이 컸습니다. 수백 개의 모듈 파일을 개별 요청하는 것보다 하나로 합쳐서 요청하는 게 효율적이었습니다. HTTP/2의 멀티플렉싱 덕분에 이 문제가 완화되었습니다.</p>
<h4 id="node_modules의-크기">node_modules의 크기</h4>
<p>node_modules 안의 라이브러리들은 ESM이 아닌 경우가 많았고, 수천 개의 작은 파일로 구성되어 있습니다. Vite는 이 문제를 esbuild를 사용한 사전 번들링으로 해결합니다. 개발 서버 시작 시 node_modules의 의존성을 빠르게 하나로 묶어둡니다.</p>
<hr>
<h1 id="프로덕션-환경에서는">프로덕션 환경에서는?</h1>
<p>Vite가 ESM을 활용하는 것은 개발 환경에서의 이야기입니다.</p>
<p>프로덕션 빌드에서는 여전히 번들링이 필요합니다. 수백 개의 파일을 개별 요청하는 것은 프로덕션 환경에서 비효율적이기 때문입니다. </p>
<p>Vite는 프로덕션 빌드 시 Rollup을 사용하여 최적화된 번들을 생성합니다.</p>
<pre><code>개발: ESM 그대로 제공 → 빠른 서버 시작, 빠른 HMR
프로덕션: Rollup으로 번들링 → 최적화된 배포 파일</code></pre><p>이것이 Vite의 핵심 전략입니다. 개발과 프로덕션에서 서로 다른 최적화 전략을 적용하여 각 환경에서 최선의 경험을 제공합니다.</p>
<hr>
<h1 id="정리">정리</h1>
<p>ESM은 JavaScript의 공식 모듈 표준이며, 현대 브라우저는 이를 네이티브로 지원합니다.</p>
<p>Vite는 이 사실을 활용하여 개발 환경에서 번들링 과정을 생략하고, 브라우저가 직접 모듈을 로드하게 합니다. 그 결과 프로젝트 규모와 관계없이 빠른 개발 서버 시작과 즉각적인 HMR이 가능해집니다.</p>
<p>Webpacker에서 Vite로 마이그레이션한다는 것은 단순히 도구를 바꾸는 것이 아니라, 번들러 중심의 개발 방식에서 브라우저 네이티브 기능을 활용하는 방식으로 패러다임을 전환하는 것입니다.</p>
<p>다음 글에서는 실제 Rails 프로젝트에서 Webpacker를 Vite Ruby로 마이그레이션 하게된 사유와 전략에 관해 살펴보겠습니다.</p>
<blockquote>
<ul>
<li>Vite 공식 문서 - Why Vite: <a href="https://vitejs.dev/guide/why.html">https://vitejs.dev/guide/why.html</a><ul>
<li>MDN - JavaScript Modules: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules</a></li>
<li>Node.js - ESM과 CommonJS 차이: <a href="https://nodejs.org/api/esm.html">https://nodejs.org/api/esm.html</a></li>
</ul>
</li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Webpack4 Code splitting 과 동적 import]]></title>
            <link>https://velog.io/@dev_choco/Webpack4-Code-splitting-%EA%B3%BC-%EB%8F%99%EC%A0%81-import</link>
            <guid>https://velog.io/@dev_choco/Webpack4-Code-splitting-%EA%B3%BC-%EB%8F%99%EC%A0%81-import</guid>
            <pubDate>Thu, 04 Dec 2025 05:04:27 GMT</pubDate>
            <description><![CDATA[<p>실무 프로덕트의 프론트엔드는 Ruby on Rails 환경에서 Webpacker 를 통해, webpack 4 을 이용한 asset 번들링을 하고 있어요.</p>
<p>동적 import 사용해서 작업해주신 팀원 분과 논의하던 중 webpack code-splitting 과 동적 import 개념을 다시 살펴보았습니다.</p>
<h2 id="webpack의-생성방식과-chunk">webpack의 생성방식과 Chunk</h2>
<p>웹팩은 모던 자바스크립트 앱을 위한 정적 번들러입니다. 
Entry Point 를 통해 의존성을 추적하고, 모듈들을 Chunk로 그룹화해서 Bundle 파일로 만들어줘요.</p>
<p>모듈은 import 가능한 모든 파일이며, Chunk는 번들링 과정의 중간 산출물, Bundle은 브라우저에서 로드되는 실제 파일이라고 보시면 됩니다.</p>
<h3 id="chunks">Chunks</h3>
<p>중간 산출물인 Chunk는 두 가지의 생성 방식이 있어요.</p>
<p>1.<strong>Initial</strong>: Entry Point 기반, 페이지 로드 시 즉시 다운로드 되는 HTML에 직접 포함되는 번들.</p>
<blockquote>
<p>Initial chunks contain all modules and dependencies specified for an entry point.</p>
</blockquote>
<p>2.<strong>Non-Initial</strong>: 동적 로드를 위한 번들</p>
<blockquote>
<p>Non-initial chunks are lazy-loaded separately, typically created through dynamic imports or code-splitting strategies.</p>
</blockquote>
<p>아래 문서에도 적혀있지만, initial인 경우 이름을 따라 생성되지만, non-initial 인 경우는 중복되지 않는 id 값으로 자동 생성돼요. </p>
<blockquote>
<p>By default, there is no name for non-initial chunks so that a unique ID is used instead of a name.</p>
</blockquote>
<p><a href="https://webpack.js.org/concepts/">https://webpack.js.org/concepts/</a>
<a href="https://webpack.js.org/concepts/under-the-hood/#chunks">https://webpack.js.org/concepts/under-the-hood/#chunks</a></p>
<h2 id="code-spliting">Code-Spliting</h2>
<p>code splitting을 사용하면 코드를 여러 번들로 분할하여 필요에 따라 또는 병렬로 로드할 수 있습니다.
더 작은 번들로 쪼개서 활용할 수 있는 기능이라고 보시면 됩니다.</p>
<p>다음 세 가지 접근 방법으로 code splitting 기능을 구현할 수 있는데요,</p>
<blockquote>
<ul>
<li>Entry Points: Manually split code using entry configuration.</li>
<li>Prevent Duplication: Use the SplitChunksPlugin to dedupe and split chunks.</li>
<li>Dynamic Imports: Split code via inline function calls within modules.</li>
</ul>
</blockquote>
<p>이번에는 동적 import 를 통해 code splitting 을 구현했습니다.</p>
<pre><code class="language-javascript">  splitChunks: {
    chunks: &#39;all&#39;,
    name: true,
  },</code></pre>
<p>environment 에 name 값을 설정해서 chunks를 분리하도록 합니다.</p>
<p><a href="https://v4.webpack.js.org/guides/code-splitting/">https://v4.webpack.js.org/guides/code-splitting/</a>
<a href="https://v4.webpack.js.org/plugins/split-chunks-plugin/#splitchunksname">https://v4.webpack.js.org/plugins/split-chunks-plugin/#splitchunksname</a></p>
<h2 id="bundle-의-path와-이름은-어떻게-결정될까">Bundle 의 path와 이름은 어떻게 결정될까?</h2>
<p>non-initial chunk인 경우는 중복되지 않는 id 값으로 자동 생성된다고 했습니다.
그 전에 output은 어떤식으로 설정되는지 짚고 넘어갑시다.</p>
<h3 id="public-output-구조">Public output 구조</h3>
<pre><code>public/packs/js/page/MainPage-eea4ad22932cbfe2c26a.chunk.js
└─┬──┘ └─┬─┘└┬┘ └─────┬─────┘ └────────┬─────────┘└───┬───┘
  │      │   │        │                │              │
  ①     ②   ③        ④               ⑤              ⑥</code></pre><p>① public/ : Rails Public Root</p>
<pre><code>설정 위치: config/webpacker.yml
public_root_path: public</code></pre><p>② packs/ : Webpacker Output Path</p>
<pre><code>설정 위치: config/webpacker.yml
public_output_path: packs</code></pre><p>③ js/ : Chunk Filename Prefix</p>
<p>④ page/MainPage : Chunk Name
⑤ -eea4ad22932cbfe2c26a : 해시
⑥ .chunk.js : File 확장자</p>
<pre><code>설정 위치: config/webpack/environment.js
chunkFilename: &#39;js/[name]-[contenthash].chunk.js&#39;,</code></pre><p>Webpacker 설정 기준으로 정리한 점 참고 부탁드립니다.</p>
<h3 id="name을-설정하려면">name을 설정하려면?</h3>
<p>그러면 non-initial인 경우, name이 id 로 들어오지 않고 모듈의 이름으로 들어오게 하려면 어떻게 해야할까요?
그럴 때 주석으로 Magic Comment를 설정하면 됩니다.</p>
<h4 id="magic-comment-있을-때">Magic Comment 있을 때:</h4>
<pre><code class="language-javascript">const MainPage = React.lazy(() =&gt;
  import(/* webpackChunkName: &quot;page/MainPage&quot; */ &#39;./Page/MainPage&#39;)
  //                          ^^^^^^^^^^^^^^^
  //                          [name]에 들어갈 값
);

생성 과정:
chunkFilename 패턴: js/[name]-[contenthash].chunk.js
                       ^^^^^^
                       &quot;page/MainPage&quot; 대입

결과: js/page/MainPage-eea4ad22932cbfe2c26a.chunk.js</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_choco/post/bdd30a5f-c9ca-4383-8003-a9648e5e598a/image.png" alt=""></p>
<h4 id="magic-comment-없을-때">Magic Comment 없을 때:</h4>
<pre><code class="language-javascript">const MainPage = React.lazy(() =&gt;
  import(&#39;./Page/MainPage&#39;)
  // webpackChunkName이 없음!
);

생성 과정:
chunkFilename 패턴: js/[name]-[contenthash].chunk.js
                       ^^^^^^
                       Webpack이 자동으로 숫자 ID 할당 (1, 2, 3, ...)

결과: js/1-eea4ad22932cbfe2c26a.chunk.js
     js/2-f1b52411c84c06b72.chunk.js
     js/3-a2b3d6a34331c201b.chunk.js</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_choco/post/14c45a93-d482-489e-b824-979cd8ec9fe5/image.png" alt="magic comment 없을 때"></p>
<p><a href="https://v4.webpack.js.org/api/module-methods/#magic-comments">https://v4.webpack.js.org/api/module-methods/#magic-comments</a></p>
<h3 id="magic-comment-는-설정해야할까">Magic Comment 는 설정해야할까?</h3>
<p>Magic Comment 를 설정하면 동적 import 시 여러 유용한 설정을 할 수 있으나, 필수는 아닙니다.
webpackChunkName 을 설정하지 않는 경우 위 처럼 숫자 id 가 할당 되니 에러가 발생할 일도 없어요.</p>
<p>그러나 번들 출력물을 명시적으로 확인하거나 기타 설정을 위해서 사용하는 것이라고 봐야할 것 같습니다.
<del>(설정하는 것이 다소 귀찮으나...)</del></p>
<h2 id="정리">정리</h2>
<p>동적 import 와 함께 <code>React.lazy()</code>를 사용하면 SPA 에서 컴포넌트가 필요할 때 필요한 번들을 로딩하여 Code Splitting 을 구현할 수 있습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 상태관리, Jotai로 변경한 이유]]></title>
            <link>https://velog.io/@dev_choco/React-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Jotai%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@dev_choco/React-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Jotai%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Wed, 19 Nov 2025 06:20:18 GMT</pubDate>
            <description><![CDATA[<p>기존 프로덕트는 React 내장 hook인 useState 를 사용하면서, Props drilling 을 막기 위해서 Context API 를 사용하고 있었어요.</p>
<p>관심 범위 내에서 ContextProvider 로 상태를 제공하면 상기한 이슈에는 문제가 없었습니다.</p>
<p>그러나 몇가지 이슈가 있어 대체 방법을 찾아야 했는데요,</p>
<hr>
<h3 id="context-api-사용-시-발생한-이슈">Context API 사용 시 발생한 이슈</h3>
<h4 id="1-잦은-리렌더링을-발생-시킴">1. 잦은 리렌더링을 발생 시킴</h4>
<p>Context 내부 값이 업데이트 되면 리렌더링을 일으켜요.
Provider 가 상위에 위치하기 때문에 불필요한 리렌더링으로 인한 성능 저하가 발생합니다.</p>
<h4 id="2-provider-지옥">2. Provider 지옥</h4>
<p>개별 Provider 가 늘어남에 따라 Provider hell 이라고 불리는 Provider가 끝없이 내부를 감싸는 현상이 생겨요.</p>
<pre><code class="language-javascript">const App = () =&gt; {
    return (
        &lt;&gt;
              &lt;GlobalProvider value={globalValue}&gt;
                  &lt;OtherProvider value={otherValue}&gt;
                    &lt;OtherOtherProvider value={otherOtherValue}&gt;
                      {/** ... */}
                    &lt;/OtherOtherProvider&gt;
                &lt;/OtherProvider&gt;
              &lt;/GlobalProvider&gt;
          &lt;/&gt;
    )
}</code></pre>
<h4 id="3-단방향-참조의-한계">3. 단방향 참조의 한계</h4>
<p>부모 → 자식 방향으로만 전달이 가능한 구조적 한계가 있어 ContextProvider 를 무조건 상위로 올려야하나, Provider 가 여럿일 때 우선순위가 꼬이는 현상이 있었어요.</p>
<hr>
<p>상기한 이유로 상태관리 라이브러리를 새로 골라야했고,
의논 끝에 <a href="https://jotai.org/">Jotai</a> 를 채택하게 되었습니다.</p>
<h3 id="왜-jotai">왜 Jotai?</h3>
<p>최소 선택 조건은 아래와 같습니다.</p>
<ol>
<li>러닝 커브가 낮을 것</li>
<li>따라서 보일러 플레이트가 적을 것</li>
<li>점유율이 확보되고 유지보수가 되는 라이브러리 일 것</li>
</ol>
<p>이 조건을 만족하는 것이 Jotai 와 <a href="https://zustand-demo.pmnd.rs/">Zustand</a> 였고,
둘 중 무엇을 선택할지 고민했었는데요.</p>
<p>제가 이해하기로 둘의 컨셉은 약간의 차이가 있었어요.</p>
<p><strong>Zustand</strong></p>
<ul>
<li>Reducer-based</li>
<li>전체 구조(store)를 먼저 정의하고, 상태 + 액션을 한 곳에서 관리</li>
<li>selector로 필요한 부분만 구독</li>
</ul>
<p><strong>Jotai</strong></p>
<ul>
<li>Atom-based</li>
<li>원자 단위(atom)부터 시작 → 조합으로 복잡한 상태 구성</li>
<li>각 atom이 독립적</li>
</ul>
<blockquote>
<p>[참고1] <a href="https://stateofreact.com/en-US">https://stateofreact.com/en-US</a>
[참고2] <a href="https://dev.to/nguyenhongphat0/react-state-management-in-2024-5e7l">https://dev.to/nguyenhongphat0/react-state-management-in-2024-5e7l</a></p>
</blockquote>
<p>각 Provider 를 나눠서 사용하는 상황에서는 Zustand 가 매력적이었지만,
상태의 단위를 작개 쪼개 사용하는 Jotai 가 추후 구조 변경이 있더라도 간편하게 유지보수가 가능해 보였어요.</p>
<p>atom 의 갯수가 많아지면 관리가 어렵다는 단점은 존재했지만, 파일 단위로 관심사를 분리해서 사용하면 추적이 어렵지 않을 것으로 보았습니다.</p>
<hr>
<h3 id="작업-과정">작업 과정</h3>
<h4 id="as-is의-문제점">AS-IS의 문제점</h4>
<p>기존에 정의된 ContextProvider 내부의 구성은 다음과 같았어요.</p>
<pre><code class="language-javascript">export const GlobalContextProvider = ({ children }) =&gt; {
  // Tanstack QueryState
  const { data, isSuccess, ... } = useCustomQuery();

  // useState   
  const [value, setValue] = useState(null);
  ...

  // 재사용 function 및 handler
  const getNormalizeData = () =&gt; { 
    ... 
  };

  const handleOnClick = () =&gt; {
    ...
  };

  return (
      &lt;GlobalContext.Provider 
      value={{
        data,
        ...
      }}
    &gt;
      {childern}
      &lt;/GlobalContext.Provider&gt;
  );    
}</code></pre>
<p>Context 내부에서 Tanstack Query(React Query)의 fetching 데이터를 조회해서 내려주고 있고, 불필요하게 내려주는 함수나 핸들러가 존재하고 있는 구조였습니다.</p>
<p>단일 컴포넌트에만 적용되는 경우도 존재했는데, 굳이 Context 에 넣으면서 단일 책임 원칙에 위배되는 상황이 생겼죠.</p>
<p>(처음에는 컴포넌트를 보기좋고 간결하게 만들기 위해 사용하기 시작했는데 말입니다... 🥲)</p>
<h4 id="to-be">TO-BE</h4>
<p>Tanstack Query hook들은 필요한 컴포넌트에서 직접 호출하는 쪽으로 변경했습니다. Tantack Query 는 Jotai와 별개로 비동기 서버 상태를 관리해주는 라이브러리인 만큼, 클라이언트 상태관리안에 종속되지 않게 하는게 우선이라고 판단했어요.</p>
<p>그리고 컴포넌트 책임 단위를 분명히 하기 위해 리팩터링 작업도 같이 진행했습니다.</p>
<p>이 작업들을 정리하고 나니 atom 을 추가하는 것은 빠르고 간단했습니다.</p>
<h4 id="기타-이슈">기타 이슈</h4>
<ul>
<li>babel plugin 이슈<ul>
<li>webpack precompile 진행 중 babel 플러그인을 못찾는 현상 발생</li>
<li>Webpack4 에서 .mjs 와 .cjs를 자동 인식하지 못하는 이슈가 있어 evironments 설정 수정</li>
</ul>
</li>
<li>node version 문제<ul>
<li>배포 환경의 node version 이 너무 구 버전이라... 인프라 담당 개발자분의 도움을 받아 버전 업그레이드  </li>
</ul>
</li>
</ul>
<hr>
<h3 id="결과-및-회고">결과 및 회고</h3>
<ol>
<li><p><strong>Context Provider 정리</strong> : Context를 완전히 삭제하는 방향으로 잡지는 않았습니다. 예를 들어 상품 정보를 붙들고 있는 Provider 같은 경우, 개별 컴포넌트의 상위에 존재하며 다른 컴포넌트에 영향을 미치지 않기 때문에 삭제하는 것은 불필요하다고 판단했습니다.</p>
</li>
<li><p><strong>성능 향상</strong> : state를 atom 단위로 잘라 필요한 경우 사용하는 방식으로 변경, 이와 동시에 누락된 메모이제이션을 함께 수행하여 불필요한 리렌더링을 방지했으며, 약 20% 정도 감소시킨 것으로 측정했습니다.</p>
</li>
<li><p><strong>가독성 향상</strong> : 각 컴포넌트의 책임을 명확하게 하고, Props Drilling 을 제거했습니다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_choco/post/eeb685e7-01af-4d68-a528-d2e981b6d877/image.png" alt="PR을 올리면서도 죄송스러운 상황" title="PR을 올리면서도 죄송스러운 상황"></p>
<p>약 6,000줄의 코드를 올리면서 배포가 상당히 부담스럽기도 하고,
그 동안 너무 쫒겨서 코드를 짰다는 걸 느끼며 반성하는 시간을 가졌습니다.</p>
<p>React 생태계에서 어떤 아키텍쳐를 결정할지 논의하면서, 깊게 생각하지 않았던 것들을 정리할 수 있는 기회가 되어 좋았습니다.</p>
<p>꾸준히 더 나은 코드를 빚어내는 경험을 쌓아야 겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[과제 이슈 회고]]></title>
            <link>https://velog.io/@dev_choco/%EA%B3%BC%EC%A0%9C-%EC%9D%B4%EC%8A%88-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dev_choco/%EA%B3%BC%EC%A0%9C-%EC%9D%B4%EC%8A%88-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 14 Nov 2025 02:59:53 GMT</pubDate>
            <description><![CDATA[<p>기회가 생겨 Next.js 프레임워크를 사용한 3페이지 정도의 간단한 프로젝트를 작업하게 됐어요.
가입 화면(일반/등록 회원 구분) -&gt; 항목 목록 -&gt; 항목 등록 흐름의 페이지 였어요.</p>
<p>json-server 를 띄우는 형식으로 항목 DB json 파일을 관리하고,
회원 구분과 정보는 js cookie 를 사용해 저장하는 형식입니다.</p>
<p>대부분 문제 없이 작업했는데,  Next.js가 처음이라 이해도가 부족해 생긴 이슈가 몇가지 있어 기록합니다.</p>
<h2 id="1-server--client-컴포넌트의-구분">1. Server / Client 컴포넌트의 구분</h2>
<p>Next.js 는 SSR 을 지원하는 프레임워크예요.
그래서 컴포넌트가 서버와 클라이언트로 구분돼요.</p>
<p>명시적으로 &#39;use client&#39; 선언을 하지 않으면 서버 컴포넌트로 취급 되며, 
클라이언트 컴포넌트를 선언해야 상태관리와 라이프 사이클 로직, 이벤트 핸들러를 사용할 수 있습니다. </p>
<p>이번에 metaData 도 설정해보았는데, metaData 의 경우는 Server 컴포넌트에서만 설정할 수 있었어요.</p>
<p>그래서 page.tsx 를 서버 컴포넌트로 두고, 페이지에 해당하는 클라이언트 컴포넌트는 분리하는 패턴으로 구조를 잡았습니다. </p>
<h2 id="2-queryclient-격리">2. queryClient 격리</h2>
<blockquote>
<p>파일 루트 레벨에서 queryClient를 생성하면 모든 요청 간에 캐시가 공유되어, 모든 데이터가 모든 사용자에게 전달됩니다. 이는 성능에 나쁠 뿐만 아니라 민감한 데이터를 유출시킬 수 있습니다.
<a href="https://tanstack.com/query/latest/docs/framework/react/guides/ssr">https://tanstack.com/query/latest/docs/framework/react/guides/ssr</a></p>
</blockquote>
<pre><code class="language-javascript">export default function MyApp({ Component, pageProps }) {
  // 각 요청마다 고유한 캐시를 보장
  const [queryClient] = React.useState(
    () =&gt; new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 60 * 1000,
        },
      },
    })
  )

  return (
    &lt;QueryClientProvider client={queryClient}&gt;
      &lt;Component {...pageProps} /&gt;
    &lt;/QueryClientProvider&gt;
  )
}</code></pre>
<p>파일 레벨에서 queryClient를 생성하면 유저 데이터가 다른 유저에게 보일 수 있어요.</p>
<p>useState를 사용하면 컴포넌트 라이프사이클마다 새 인스턴스를 만들어 방지할 수 있다고 tanstack query 공식 문서에 적혀 있어 참고해서 수정했습니다.</p>
<h2 id="3-cookiesget-의-반환-시점">3. Cookies.get() 의 반환 시점</h2>
<p>일반적인 프로젝트면 유저정보를 Cookie에 넣지는 않겠지만, 적시적인 구현을 위해 임시로 사용했던 Cookie 에서 오류가 발생했어요.</p>
<p>목록에 대한 스켈레톤을 구현하다 발생한 에러인데,
Cookie 값에 따라 버튼 렌더링 여부를 결정하는 부분이 있어 Cookies.get()이 서버 렌더링 시점과 클라이언트 hydration 시점에 다른 값을 반환했기 때문에 값의 타입을 읽을 수가 없는 문제가 있었어요.</p>
<pre><code class="language-javascript">  useEffect(() =&gt; {
    setUserType(Cookies.get(DATA_KEY_USER_TYPE));
  }, []);</code></pre>
<p>이 문제는 useState + useEffect로 클라이언트에서만 쿠키를 읽도록 처리해서 해결했습니다.</p>
<h2 id="정리">정리</h2>
<p>Next.js 는 하이드레이션 이라는 과정으로 서버에서 미리 렌더링 된 HTML을 브라우저로 전송하고 클라이언트에서 컴포넌트를 재구성해요.</p>
<p>그래서 SEO 최적화가 가능한 건데, 이 때 서버 렌더링 내용과 클라이언트 첫 렌더링 내용이 다르면 에러가 발생하니 이 부분을 주의해야 했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리듬을 찾아서... Rhythm Helper 개발기 (1)]]></title>
            <link>https://velog.io/@dev_choco/%EB%A6%AC%EB%93%AC%EC%9D%84-%EC%B0%BE%EC%95%84%EC%84%9C...-Rhythm-Helper-%EA%B0%9C%EB%B0%9C%EA%B8%B0-1</link>
            <guid>https://velog.io/@dev_choco/%EB%A6%AC%EB%93%AC%EC%9D%84-%EC%B0%BE%EC%95%84%EC%84%9C...-Rhythm-Helper-%EA%B0%9C%EB%B0%9C%EA%B8%B0-1</guid>
            <pubDate>Mon, 10 Nov 2025 08:22:15 GMT</pubDate>
            <description><![CDATA[<p><img src="blob:https://velog.io/1dc7e101-0ba2-4f7a-98b1-629c108fbb51" alt="화면"></p>
<p>저는 취미로 건반을 치고 있어요.
입시 경험도 없고 오래된 취미도 아닌지라 성인 취미생의 한계를 느끼며,
낯선 악보를 독학해야할 땐 도대체 이 리듬이 어떤 리듬인지 손발을 다 써가며 추측해야만 했죠.</p>
<p>메트로놈 앱은 수천개가 있고, 기본에 충실한 훌륭한 앱이 많지만
정해져있는 비트를 세주는게 아니라 내가 입력한 한 마디의 음표들의 리듬을 표시해주는 어플은 없을까?  🤔</p>
<p>열심히 찾아보다가, 직접 만드는게 낫겠다는 판단을 했어요.</p>
<p>그래서 템포와 박자표를 설정하고, 한 마디에 들어갈 음표를 클릭해서 넣고 CTA 버튼을 누르면 소리와 시각적 요소를 통해 진행 리듬을 직관적으로 확인할 수 있는 앱을 만들었습니다.</p>
<p>이름하여,</p>
<blockquote>
<p><a href="https://rhythm-helper.vercel.app/">Rhythm Helper</a>
입력한 리듬을 직관적으로 보고 들을 수 있도록 도와주는 Rhythm Helper</p>
</blockquote>
<p>프로토타입 ux와 음표를 입력하고 재생하는 핵심 기능까지 개발을 완료했어요.</p>
<h3 id="사용-기술">사용 기술</h3>
<p>사용한 기술은 다음과 같습니다.</p>
<ul>
<li>TypeScript</li>
<li>Vite + React</li>
<li>Tailwindcss</li>
<li>Zustand</li>
<li>Web Audio API</li>
</ul>
<p>배포는 빠르고 편하게 보여주기 위해 Vercel 을 통해 간단하게 진행했습니다.</p>
<p>위 기술들을 사용한 이유는 다음과 같아요.</p>
<p><strong>TypeScript, React</strong>: 제가 현업에서 메인으로 사용하고 있는 언어가 JS이고, 웹 뷰는 React.js 라이브러리를 사용하고 있기 때문입니다. 현업 환경에서는 TS 대신 jsdoc 을 이용해 함수 등에 주석을 달아주고 있는데, 이번 기회에 TS로 타입지정을 해보고 싶어서 사용했습니다.</p>
<p><strong>Vite</strong>: 이번에 만드는 앱에서 많은 리소스를 요구하지 않기 때문에 간편한 빌드 툴인 vite 를 통해 프로젝트를 생성했습니다.</p>
<p><strong>Zustand</strong>: 사실 가능하면 이렇게 간단한 앱은 useState hook 으로 처리해도 되는데, 전역 상태가 필요하게 돼서 추가했습니다. Jotai 와 비슷하게 보일러플레이트가 거의 없는 직관적인 사용이 좋은 상태관리 도구였습니다. Store 단위로 묶어서 사용하는 점이 마음에 들었습니다.</p>
<p><strong>Web Audio API</strong>: 처음에는 어떤 샘플 보이스를 하나 찾아서 출력시키려고 했는데, 찾아보니 웹 오디오 API 라는게 있고 신디사이저 이론을 알고 있다면 원하는 클릭음을 생성하는게 가능해 보였어요. 구체적인 방법은 AI 도움을 받았고, 다행히 얼마전에 신디사이저 공부를 좀 해둔 덕에 금방 적용할 수 있었어요. (취미의 도움을 받는 순간...!)</p>
<hr>
<h3 id="작업-과정">작업 과정</h3>
<p>5년 전 취직준비하던 시절 이후로 기획부터 스스로 스케쥴을 짜는게 너무 오랜만이라, 재미도 있었지만 막막하기도 했어요.</p>
<h4 id="핵심-기능-정의">핵심 기능 정의</h4>
<p>맨 처음에는 핵심 기능부터 정의했습니다.</p>
<ul>
<li>박자표, 템포 선택 </li>
<li>음표를 선택하면 마디에 추가</li>
<li>시작버튼(CTA)를 누르면 마디에 등록된 음표를 박자표와 템포에 맞춰 시각적, 청각적으로 출력</li>
<li>32분음표, 한 마디까지 제한</li>
<li>추후 마디를 추가하거나 저장할 수 있는 기능을 염두하여 개발</li>
</ul>
<p>이 정도로 정리하고 나니까 어떤 시나리오로 작동할지, 어떤 ux 를 제공해야할지 머리에 그려졌어요.</p>
<h4 id="컴포넌트-구성">컴포넌트 구성</h4>
<p>UI 단위 컴포넌트를 구성하는건 원래 하던 일이라 쉽게 정리가 됐어요.</p>
<ul>
<li>박자 선택 버튼</li>
<li>템포 선택 버튼</li>
<li>마디 영역</li>
<li>음표 선택 버튼 영역</li>
<li>CTA (Start/Pause)</li>
<li>rollback 버튼</li>
<li>reset 버튼</li>
</ul>
<h4 id="이벤트-및-상태관리-처리">이벤트 및 상태관리 처리</h4>
<p>마디 영역에 들어가는 데이터는 음표/쉼표 여부와 전체 마디에서 점유하는 길이 값을 가진 객체의 배열로 설정했습니다.</p>
<p>그래야 출력시 정확한 속도와 소리를 낼 수 있었어요. </p>
<p>구조를 정하면 각 버튼에 대한 이벤트 핸들러를 등록하고,</p>
<p>현재 재생중 여부와 마디 데이터를 전역상태로 설정하여 재생 중일 경우 출력 효과를  setTimeout 을 활용한 hook 으로 보여주었어요.</p>
<p><img src="https://velog.velcdn.com/images/dev_choco/post/9f48167f-ff2a-44c6-8f40-d8385f33ea60/image.gif" alt="재생되는 모습" title="재생되는 모습"></p>
<hr>
<h3 id="추후-과제">추후 과제</h3>
<p>퇴근 후 제작해야 했기에 시간적인 제약은 있었지만, 완성하기 까지 일주일도 채 걸리지 않았어요.</p>
<p>제가 원하는 방향으로 컴포넌트를 리팩터링하거나 간단한 수정사항은 Claude Code 를 활용했기 때문에 생산성이 많이 향상 되었던 것 같아요.</p>
<p>모든걸 일임하지 않고 정확히 원하는 바를 프롬포트로 입력해서 주문한 다음, 완성한 코드를 꼼꼼히 확인 하는 방향으로 확인했는데, 이 정도로 의지했을때 AI와 좋은 관계가 형성(?) 되는 것 같았습니다.</p>
<p>최소 기능까지는 구현했는데,
확장하고 싶은 부분과 지인들을 통해 받은 피드백을 통한 수정사항들이 있습니다.</p>
<ol>
<li>마디에 있는 노트를 클릭할 수 있게 하고, 대체하거나 지울 수 있도록 할 것</li>
<li>마디를 더 추가할 수 있게 할 것 (예상했던 부분)</li>
<li>최근 작업한 마디를 저장할 수 있게 할 것 (localStorage를 통해 바로 구현 가능)</li>
<li>셋잇단 음표의 구현 (하려고 했던 부분)</li>
<li>이음줄의 구현 <del>(으악! 복잡해진다!)</del></li>
<li>마디 UI 에서 8분, 16분, 32분음표가 잇달아 저장되어있는 경우 클래식 악보처럼 묶어 표현되도록 처리 (하려고 했던 부분)</li>
</ol>
<p>1번이나 5번은 상정하지 못했던 부분이라 기술적, ux적으로 많이 고민해봐야하겠습니다.</p>
<p>그 외에도 디자이너의 부재로 더듬더듬 만든 디자인을 디벨롭 하고,
모바일 버전으로 최적화한 다음 스토어에 배포하겠다는 목표도 있습니다. (음악에 관심 있는 좋은 디자이너 분을 만나야겠죠...)</p>
<p>올해는 연말 정기공연이 있으니,
공연이 마무리되면 후속 작업에 박차를 가하고 다듬어 볼 예정입니다!</p>
<p>Github: <a href="https://rhythm-helper.vercel.app/">https://rhythm-helper.vercel.app/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회고: Rails Application에 React 심기]]></title>
            <link>https://velog.io/@dev_choco/Rails-App%EC%97%90-React-%EC%8B%AC%EA%B8%B0</link>
            <guid>https://velog.io/@dev_choco/Rails-App%EC%97%90-React-%EC%8B%AC%EA%B8%B0</guid>
            <pubDate>Thu, 18 Sep 2025 06:24:13 GMT</pubDate>
            <description><![CDATA[<p>재직중에 했던 프로젝트는 여러가지 있지만 가장 기억에 남는 것을 꼽으면
22년도 하반기에 진행했던 프론트엔드 개편 작업입니다.</p>
<p>당시에 회고 했어야 했는데 지금이라도 문서와 기억에 의존해 적어봅니다.</p>
<h3 id="당시-상황">당시 상황</h3>
<p>프로덕트 전면 리뉴얼에 따라 모던 JS 라이브러리를 사용해서 프론트엔드 영역을 개발해야 했습니다. 성능적인 부분에서도 필요했지만, 앞으로의 채용을 위해서도 당시 가장 점유율이 높았던 ReactJS 라이브러리를 사용하기로 했습니다.</p>
<p><strong>Ruby on Rails</strong> 버전 5(이하 RoR) 프레임워크를 사용하고 있는 상황이었고, 프론트엔드 어플리케이션 분리 없이 진행해야 했습니다. </p>
<blockquote>
<p>RoR은 MVC 패턴을 사용하는 프레임워크입니다. 
[참고] <a href="https://guides.rubyonrails.org/getting_started.html#model-view-controller-basics">https://guides.rubyonrails.org/getting_started.html#model-view-controller-basics</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_choco/post/e79633ac-10b2-4658-aba5-99b2b453020b/image.png" alt="Rails MVC 아키텍쳐"></p>
<p>프론트엔드만 분리해낼 수 있으면 정말 좋았겠지만, 메인 계열과  2depth, 검색페이지 한정으로 주어진 시간 내에 반영해야했고,</p>
<p>유저 관련 영역을 rails devise에 의존하고 있는 부분도 있었기 때문에  현실적으로 어려웠습니다.</p>
<hr>
<h3 id="모던-javascipt-환경-세팅">모던 JavaScipt 환경 세팅</h3>
<p><strong>Webpacker</strong>는 JS, CSS 및 Rails 애플리케이션의 Asset 을 패키징하는 webpack 기반 Rails wrapper 입니다.</p>
<p>RoR는 기본적으로 node를 필요로하지 않기 때문에, Webpacker를 설치해야 node 모듈을 사용할 수 있습니다.</p>
<pre><code># Gemfile 추가
gem &#39;webpacker&#39;, &#39;~&gt; 5.4&#39;

# bundle 설치
bundle install

# webpacker 초기화
rails webpacker:install

├── app/
│   └── javascript/          
│       └── packs/
│           └── application.js
├── babel.config.js
├── config/
│   ├── webpack/
│   │   ├── development.js
│   │   ├── environment.js
│   │   ├── production.js
│   │   └── test.js
│   └── webpacker.yml
├── package.json
├── yarn.lock
└── bin/
    ├── webpack
    └── webpack-dev-server</code></pre><p>Webpacker을 설치하니 JS 패키지 관리자인 yarn을 사용할 수 있게 됐습니다.</p>
<p>기존에 있던 RoR Asset pipeline 인 Sprocket 과 별개로 새로 구성하는 페이지의 JS 파일들, 특히 React 컴포넌트 등을 넣어 webpack 으로 번들링할 수 있게 됐습니다. 설치 이전에는 모듈 번들링을 하지 않아 JS 파일이 페이지 단위로 분산되고, 공통으로 사용되는 함수들은 전역에 배치되어 있었습니다.</p>
<p>React 라이브러리를 설치하고 webpack 설정 후 빌드 해보는 것도 처음이라 검색에 의존하며 많이 시도해봤던 것 같습니다. 꽤 길었지만 실서버에서 돌아갈 컴포넌트를 만들 준비를 마쳤습니다.</p>
<hr>
<h3 id="성과">성과</h3>
<p>이전 코드와 비교해서 사용하던 컨텐츠의 총량이 늘어났기 때문에, React 를 넣음으로서 각 페이지의 Load 시간이 개선되었는지는 측정할 수 없었습니다만, </p>
<p>비즈니스의 목표치였던 사용자의 절대적인 상품컨텐츠 View 가 증가했다는 사실만 알 수 있었습니다. 라이브러리를 도입해서 개선되었다는 수치를 확인할 수는 없었기 때문에 이 부분은 아쉽게 느껴집니다.</p>
<p>React 러닝커브가 없는 상태에서 런칭까지 해낸 것도 의미 있었지만, 당시 2년차 경력으로 5인 규모의 스케쥴 관리도 해볼 수 있어서 상당히 배운 점이 많은 시간이었습니다.</p>
<hr>
<h3 id="남은-문제들">남은 문제들</h3>
<p>배포는 마쳤으나 이후에 해야하는 숙제가 생겼습니다.</p>
<h4 id="빌드-시간-증가">빌드 시간 증가</h4>
<p>기존에 있는 Asset Pipeline 을 제거하지 못하고 Webpacker 를 설치했더니 빌드 시간이 2배이상 늘어나 버렸습니다.</p>
<p>그리고 webpack entry point에 파일을 여러 개 배치해서 컴파일 오버헤드가 증가했습니다.</p>
<blockquote>
<p>application.erb 에 단일 entry point 파일인 packs/application.js 만 헬퍼를 사용해 포함 시켜야합니다.</p>
</blockquote>
<pre><code>&lt;%= javascript_pack_tag &quot;application&quot; %&gt; </code></pre><p>그러나 여전히 Rails 프로젝트인 환경에서 legacy view 영역에서는 webpacker 경로의 js 를 불러야하는 경우가 있어 문제가 되었습니다. 
이 문제는 veiw 에 해당하는 컨트롤러와 메서드 값을 전역 상태로 받아 필요한 스크립트를 불러내는 형태로 변경해서 개선했습니다.</p>
<p>entry point 문제는 금방 해결할 수 있는 부분이었습니다. </p>
<p>그러나 Sprocket 에 남아있는 레거시를 정리하고, Webpacker로 옮기는 작업의 경우는 워낙 방대해서 부채로 남았습니다.</p>
<h4 id="끝나지-않는-리팩터링">끝나지 않는 리팩터링</h4>
<p>당시 팀 내 React에 대한 이해도가 저를 포함해서 전부 높은 수준이 아니었기 때문에, React에 익숙하지 않은 개발자들이 하는 실수, 대표적으로 hook 과 라이프사이클에 대한 이해 부족의 문제도 있었고, Context API만으로는 부족한 상태관리 라이브러리의 필요성으로 인한 추가 등 계속해서 유지보수 작업이 필요하게 되었습니다.</p>
<p>최근에도 기존에 사용하던 Context Provider를 들어내고 상태관리 라이브러리인 jotai를 새로 적용시키는 작업을 했습니다.</p>
<hr>
<h3 id="돌아보며">돌아보며</h3>
<p>이 작업을 하고나서 동기부여가 많이 된 한 편, 계속되는 코드개선과 레거시의 컴포넌트 작업으로 인해 많이 지쳤던 것 같습니다.</p>
<p>React 같은 라이브러리를 새로 배우고 제품에 적용하는 과정이 소위 도파민 터지는 작업이었다면, 이후에 해결해야하는 과제들은 회사 입장에서 중요하지 않은 작업으로 여겨질까봐 회피하고 싶은 마음이 있었습니다.</p>
<p>그런데 서비스라는게 계속해서 제공되는 것이고 혼자만의 제품이 아니기 때문에 _지속가능한 서비스_를 위해서는 지속적인 개선이 무엇보다 중요한 일임을 잊지 말고 좋은 코드를 깎아 나가야 겠습니다.</p>
<p>개선 과정에서 했던 고민들도 조금씩 적어 보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[어떻게 개발하고 살 것인가]]></title>
            <link>https://velog.io/@dev_choco/%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B3%A0-%EC%82%B4-%EA%B2%83%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@dev_choco/%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B3%A0-%EC%82%B4-%EA%B2%83%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Mon, 08 Sep 2025 12:17:03 GMT</pubDate>
            <description><![CDATA[<p>2020년 12월 부터 개발자 커리어를 시작하면서 프론트엔드 개발자 직무에 대해 무엇을 느꼈는지, 지금은 어떤지, 앞으로는 어떻게 할 것 인지 환기할 필요성을 느껴 글을 적습니다.</p>
<h3 id="시작">시작</h3>
<p>예전에 했던 일 덕분에 커머스 도메인에 대한 이해가 있어 구직 당시 그 점을 어필하여 커머스 플랫폼에서 개발자 커리어로 전환할 수 있었습니다. </p>
<p>취업을 위해 준비한 프로젝트는 Java Spring 프레임워크를 이용하고 프론트 영역은 무려 JSP 로 만들어진 웹 어플리케이션 이었는데, Ruby on Rails 프레임워크를 통해 MVC 패턴으로 뷰를 렌더링 하고 있던 회사의 기술 스택과 잘 맞았습니다.</p>
<p>처음에는 풀스택 개발자로 작은 단위의 DB 마이그레이션 부터 뷰 영역 태그 작성과 스타일시트, 스크립트 작성을 맡다가 프론트 위주 작업으로 옮겨갔습니다.</p>
<h3 id="클라이언트-가까운-곳으로">클라이언트 가까운 곳으로</h3>
<p>신입 시절부터 팬데믹이 전세계를 휩쓸고 있었지만, 시리즈 A 투자 유치가 이루어지는 등 회사의 규모가 확장됐습니다. 개발팀 인원을 충원하는 만큼 구식 기술스택에 이미 주류가 된 싱글 페이지 어플리케이션 라이브러리를 도입할 필요가 있다는 구성원들의 판단이 있었습니다.</p>
<p>기존 프로젝트 안에는 JQuery가 상당 부분 사용되었으며, 이전에 근무했던 개발자들이 남긴 Angular 의 흔적이 곳곳에 남아있었기 때문에 GNB 에 해당하는 메인계열 페이지 부터 UI를 아예 새롭게 변경하며 React.js를 조금씩 적용하게 되면서 풀스택보다는 프론트 영역에 집중했습니다.</p>
<p>화면 상의 디테일과 상호작용이 중요한 작업인 점이 재미있다고 생각했습니다.</p>
<h3 id="내-일이-된-순간">내 일이 된 순간</h3>
<p>회사 안의 개발자로 지내면서 새로 런칭되는 기능 개발 말고도 남겨져있는 코드의 개선 그리고 불과 몇 개월 전에 짰던 내 코드의 개선을 반복하며 유지보수로 큰 보람을 느꼈습니다.</p>
<p>실제로 사용자가 반응하는 영역에서 이루어져 눈에 보이기도 하고, 개편 당시 UX 디자이너 분들과 같이 협업하며 개발이라는 것이 혼자 컴퓨터만 만지면서 하는 것이 아니라 만드는 과정과 도달하는 과정에도 항상 사람이 필요하다는 점을 알게 되어 그 때 비로소 개발자의 역할과 책임감에 대해 생각할 수 있게 됐습니다.</p>
<h3 id="발전이-없으면-개발자가-아니다">발전이 없으면 개발자가 아니다</h3>
<p>워낙 알아야 할 것도 많고 트렌드에도 민감한 직무인 것은 알고 있었는데 이게 끝이 안나고, 나는 계속 부족한 것 같은 와중에 AI도구의 발전은 무서울 정도로 빠릅니다.</p>
<p>그런 마음이 들 때 쯤 외부로 나가서 확인을 받아야하는데, 회사 내에서는 인정 받는 것 같고 팀원들도 좋은데 굳이? 라는 잘못된 생각을 하고 제자리 걸음을 걷고 있습니다.</p>
<p>현실에 안주하고 지내니 작업 중 &quot;눈에 잘 띄지 않는 부분은 그냥 넘어갈까?&quot; 하는 마음이 들 때가 생기더군요. 그 순간 스스로가 부끄러웠습니다. 개발자는 디벨롭을 하기 때문에 개발자라고 불리는 것인데 말이죠.</p>
<h3 id="어떻게-develop하고-살-것인가">어떻게 Develop하고 살 것인가</h3>
<p>개선하고자 하는 태도는 직무 말고 삶을 사는데 있어서도 잊으면 안되는 중요한 자세라고 생각해왔습니다. 왜 잊고 있었을까요? 비대면 경제 수혜로 공급은 많았는데 채용시장이 얼어붙어서 불안해진 것인지, 갑자기 꿈에서 깬 것 같습니다. 가끔은 알고 있던 것도 놓치는 기분이 듭니다. </p>
<p>요즘 단순 작업이나 사소한 오류 검증에 대부분 AI의 도움을 받고있습니다. 잘쓰면 상당히 유용하다는 것을 경험으로 인정하지만 이 도구는 사람의 판단력을 흐리게 만드는 것 같습니다. 바이브 코딩은 그럴싸하지만 성능이 보장되지 않습니다. 그걸 고쳐쓰기 위해서라도 개발자가 존재해야 합니다.</p>
<p>결국 프로덕트를 제공하는 것도 이용하는 것도 사람입니다. 그렇기 때문에라도 사람의 관점에서 나음을 추구하는 개발자가 되어야할 것입니다.</p>
]]></description>
        </item>
    </channel>
</rss>