<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jidohyun.log</title>
        <link>https://velog.io/</link>
        <description>FE-Engineer</description>
        <lastBuildDate>Thu, 12 Mar 2026 11:44:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jidohyun.log</title>
            <url>https://velog.velcdn.com/images/do-hyun123/profile/9dcc04c0-fb26-48fc-9af5-49248adc23f0/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jidohyun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/do-hyun123" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[MOZU - 고등학생이 실제 기관에 납품하기까지]]></title>
            <link>https://velog.io/@do-hyun123/MOZU-retrospect</link>
            <guid>https://velog.io/@do-hyun123/MOZU-retrospect</guid>
            <pubDate>Thu, 12 Mar 2026 11:44:45 GMT</pubDate>
            <description><![CDATA[<h2 id="시작은-한-통의-의뢰였다">시작은 한 통의 의뢰였다</h2>
<p>2024년 12월, 대전시 진로융합교육원에서 내가 재학 중인 <strong>대덕소프트웨어마이스터고등학교</strong>에 하나의 의뢰가 들어왔다. 중학생들을 대상으로 한 창업경영 진로체험 수업에서 사용할 <strong>모의주식 투자 플랫폼</strong>을 만들어달라는 것이었다.</p>
<p>진로융합교육원은 초·중·고등학생들에게 다양한 진로 체험을 제공하는 기관이다. 그 안에 &#39;창업 경영 마을&#39;이라는 프로그램이 있고, 거기서 중학생들이 시나리오 기반으로 주식 투자를 체험하며 경제 개념을 배운다. 우리가 만들 서비스는 그 수업의 핵심 도구가 될 예정이었다.</p>
<p>모의주식의 줄임말, <strong>MOZU</strong>. 이렇게 산학협력 프로젝트가 시작됐다.</p>
<p><em>(초기 기획 당시의 유저 플로우)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/1afdcbca-11c6-409e-91c3-81a6b51f427f/image.jpg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/dd1bf91a-c1aa-499e-aec8-cf2d8c34831d/image.jpg" alt=""></p>
<p><em>(진짜 어려웠던 도메인 이해하기..)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/0a78f175-89f4-475f-84d7-9e4e2b384513/image.png" alt=""></p>
<h2 id="v1---프론트엔드-리드로서의-첫-단추">v1 - 프론트엔드 리드로서의 첫 단추</h2>
<p>v1 개발은 백엔드 3명, 프론트엔드 3명으로 구성된 6인 팀에서 시작됐다. 나는 프론트엔드 리드를 맡았다. 학교 프로젝트와는 결이 달랐다. 단순히 기능을 구현하는 게 아니라, 실제 수업에서 중학생들이 사용할 서비스를 만들어야 했기 때문이다.</p>
<p>선생님 화면(Admin)과 학생 화면(Student)을 나누어 설계했다. 선생님은 수업을 운영하고 학생들의 활동을 모니터링하고, 학생들은 시나리오에 따라 주식을 사고팔며 투자를 체험한다. 실시간 데이터 동기화를 위해 SSE를 도입했는데, 이것이 나중에 나를 밤새우게 만들 줄은 몰랐다.</p>
<p><em>(FE 리드시절 태스크 백로그 보드)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/7feed049-7e1c-43ee-8bb2-80737a5aa205/image.png" alt=""></p>
<h2 id="전환점---po가-되다">전환점 - PO가 되다</h2>
<p>2025년 8월, v2 프로덕트를 맡게 되면서 모든 게 달라졌다.</p>
<p>프론트엔드 리드에서 <strong>PO(Product Owner)이자 팀 리더</strong>가 됐다. 팀 규모도 커졌다. 프론트엔드 3명, 백엔드 5명. 총 8명의 팀을 이끌어야 했다. 고등학생이 8명의 개발팀을 통솔한다는 건, 생각보다 훨씬 어려운 일이었다.</p>
<p>나는 프로덕트의 체계화를 위해 많은 것들을 도입했다. 11일 단위 스프린트, Jira로 이슈와 백로그를 관리하고, PR 자동화까지 구축했다. 애자일 기반 프로세스를 직접 설계하고 운영하면서 &quot;개발만 잘한다고 되는 게 아니구나&quot;를 온몸으로 배웠다. 사람마다 일하는 속도가 다르고, 소통의 방식이 다르고, 동기부여의 포인트가 다르다. 코드를 짜는 것보다 사람을 이해하는 게 더 어려웠다.</p>
<p>하지만 이 시기가 나를 가장 성장시켰다. MOZU는 더 이상 과제가 아니었다. <strong>나의 프로덕트</strong>였다.</p>
<h4 id="열심히-했던-다양한-시도들">열심히 했던 다양한 시도들..</h4>
<p><a href="https://velog.io/@do-hyun123/issue-pr-workflow">[MOZU] Gemini 2.5 flash로 이슈 -&gt; PR 워크플로우 구축하기</a>
<a href="https://velog.io/@do-hyun123/MOZU-AWS-engineering-modeling">[MOZU] AWS 엔지니어링 도입기 - 타겟 선정과 모델링</a></p>
<p><em>(애자일하게 사용했던 Jira, 세팅하는 데 애좀 먹었다..)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/16e436bf-ce36-49ef-bf62-0bc42f03c5a7/image.png" alt=""></p>
<h2 id="선생님이-말씀하시는-건-이런-뜻일까요">&quot;선생님이 말씀하시는 건 이런 뜻일까요?&quot;</h2>
<p>가장 독특하고도 어려웠던 경험은 비전공자인 선생님과의 소통이었다.</p>
<p>기관에서 요구사항을 제시해주시는 선생님은 개발을 모르시는 분이었다. 당연한 일이다. 하지만 그 &quot;당연함&quot;이 실제 업무에서 얼마나 큰 간극을 만드는지, 직접 겪어보기 전까지는 몰랐다.</p>
<p>선생님이 &quot;학생들이 주식을 살 때 좀 더 실감나게 해주세요&quot;라고 말씀하시면, 그것이 UI 애니메이션을 의미하는 건지, 실시간 가격 변동 피드백을 의미하는 건지, 아니면 매수 확인 프로세스의 단계를 추가하라는 건지를 파악해야 했다. 모호한 요구사항을 구체적인 도메인 설계로 번역하는 일. 이건 어떤 수업이나 동아리에서도 가르쳐주지 않는 능력이었다.</p>
<p>면담을 거듭하면서 나만의 방식이 생겼다. 선생님의 말씀을 듣고, 화면 흐름으로 다시 그려서 보여드리고, &quot;이런 의미가 맞으실까요?&quot;라고 확인하는 과정을 반복했다. 비전공자의 언어를 코드로 번역하는 번역가. 그게 PO로서 내가 해야 할 가장 중요한 일이었다.</p>
<p><em>(요구사항과 다양한 문서들을 정리한 파일들)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/cbc71431-b592-4a86-97b2-a83cce2ee769/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/399543a1-41a7-4efd-8a81-f679e4e905c2/image.png" alt=""></p>
<h2 id="장학사님-시연-전날-나는-밤을-샜다">장학사님 시연 전날, 나는 밤을 샜다</h2>
<p>모주를 진행하면서 가장 기억에 남는 밤이 있다.</p>
<p>장학사님 앞에서 시연하는 날이 다음 날 아침이었다. 그런데 전날 오후, 프론트엔드에서 SSE 요청을 처리하던 중 장애가 터졌다. 실시간 데이터 스트리밍이 끊기면 수업 자체가 불가능하다. 시연에서 이 기능이 작동하지 않으면 전부 무너지는 거였다.</p>
<p>팀원들은 각자의 파트가 있었고, 이건 프론트엔드 SSE 처리의 문제였다. 나 혼자 해결해야 했다.</p>
<p>밤새 코드를 분석하고, 디버깅하고, 재현하고, 수정하고, 또 재현하고. 새벽을 넘기고, 동이 트기 시작할 무렵 드디어 원인을 잡았다. 수정을 마치고 테스트를 돌렸을 때 정상 작동하는 걸 확인한 그 순간의 안도감은 말로 설명하기 어렵다.</p>
<p>그리고 몇 시간 뒤, 시연은 성공했다.</p>
<p><em>(시연 전날 장애를 발견한 모습)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/f61e65fd-bd36-474e-b2e2-4ea6e7603864/image.png" alt=""></p>
<p><em>(시연을 도와주셨던 고마우신 선배님들 ㅎㅎ)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/99186cce-c4a0-458d-b815-f3c32ff8b8ac/image.png" alt=""></p>
<p><em>(성공적인 시연 당시의 사진)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/2f3936da-f8aa-4e77-90c9-58651b3a5c26/image.png" alt=""></p>
<p><em>(시연 후 선배님들과 선생님과 소고기 회식 ㅎㅎ)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/672aaa7c-c1e3-4fa7-bc14-1f32591914fb/image.png" alt=""></p>
<h2 id="소프트웨이브에서-cto-앞에-선-고등학생">소프트웨이브에서 CTO 앞에 선 고등학생</h2>
<p>장학사님 시연 외에도 많은 대외 활동이 있었다. 그중 소프트웨이브 전시가 특별했다.</p>
<p>소프트웨이브는 국내 소프트웨어 산업 전시회다. 거기엔 현직 개발자, CEO, CTO 등 실제 업계에서 일하는 사람들이 온다. 그 앞에서 고등학생인 내가 MOZU를 시연하고, 서비스를 소개해야 했다.</p>
<p>솔직히 긴장됐다. &quot;학생 프로젝트&quot;라고 가볍게 볼 수도 있는 자리에서, 나는 이게 가볍지 않다는 걸 보여주고 싶었다. 시나리오 기반 모의투자 시뮬레이션의 구조를 설명하고, 선생님 화면과 학생 화면의 실시간 연동을 직접 시연했다.</p>
<p>반응은 기대 이상이었다. 매우 흥미롭게 봐주셨고, 진지하게 질문도 해주셨다. <strong>&quot;나의 프로덕트&quot;</strong>를 세상에 꺼내놓고, 그것이 인정받는 경험. 그건 코드를 천 줄 짜는 것보다 더 큰 동기부여가 됐다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/1ff4db4a-285d-43ed-885d-8ecfca3a111a/image.png" alt=""></p>
<h2 id="클라우드에서-교실-한-대의-데스크톱으로">클라우드에서 교실 한 대의 데스크톱으로</h2>
<p>MOZU는 원래 학교의 자체 인프라인 <a href="https://github.com/team-xquare">xquare</a>를 통해 클라우드로 운영되고 있었다. 프론트엔드는 <code>Vercel</code>, 백엔드는 자체 인프라로 서비스했다.</p>
<p>그런데 실제 기관 납품을 준비하면서 문제가 생겼다. 예산, 네트워크 환경, 안정성. 복합적인 이유로 기관 자체 데스크톱에 온프레미스로 마이그레이션해야 했다. 클라우드 서비스를 교실 한 대의 컴퓨터 안에 집어넣어야 하는 것이다.</p>
<p>나는 프론트엔드 개발자다. 인프라는 내 영역이 아니었다. 하지만 PO로서 &quot;그건 내 일이 아닙니다&quot;라고 말할 수 없었다. <code>Docker Compose</code>로 <code>MySQL</code>, <code>Redis</code>, <code>Spring Boot</code> 백엔드, <code>nginx</code>를 하나의 스택으로 묶고, <code>nginx</code> 리버스 프록시로 <code>Admin(:80)</code>과 <code>Student(:3001)</code> 두 개의 SPA를 서빙하는 구조를 설계했다.</p>
<p>기관에 직접 출장을 가서 네트워크 환경을 조사했다. 유선과 무선이 서로 다른 서브넷에 있다는 걸 발견했다. 서버 데스크톱은 유선에, 수업용 노트북들은 WiFi에 연결되니 서로 통신이 안 되는 상황. 서버의 WiFi IP를 통해 같은 네트워크에서 접근하도록 아키텍처를 수정했다.</p>
<p>서브넷 마스크를 분석하고, <code>Docker</code> 포트 매핑을 설계하고, SSE 통신의 안정성을 걱정하며 <code>nginx</code> 설정을 만지는 경험. &quot;새로운 과제를 해결해야 한다는 부담&quot;과 &quot;새로운 걸 해볼 수 있다는 설렘&quot;이 동시에 밀려왔다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/774ca528-a567-4c46-affb-affc31e2add8/image.png" alt=""></p>
<h2 id="수업에-장애는-없어야-한다">수업에 장애는 없어야 한다</h2>
<p>온프레미스 전환에서 가장 신경 쓴 건 <strong>안정성</strong>이었다.</p>
<p>이건 개인 프로젝트가 아니다. 기관에서 수업에 실제 운영되는 서비스다. 수업 중간에 데이터가 유실되거나 서비스가 멈추면, 그건 단순한 버그가 아니라 수업 자체가 망하는 것이다. 중학생 20명이 투자 시뮬레이션을 하다가 화면이 멈추는 상황을 상상해보면, 그 책임감의 무게를 느낄 수 있다.</p>
<p>그래서 서비스 통신 설계에 큰 공을 들였다. SSE 연결이 끊어졌을 때의 재연결 로직, <code>MySQL</code>과 <code>Redis</code>의 헬스체크, 컨테이너 자동 재시작 정책, 데이터 백업 스크립트까지. 기관 담당자분이 개발자가 아니시기 때문에, <code>start.bat</code> 한 번 클릭으로 서비스가 올라가고, <code>backup.bat</code>으로 데이터를 백업할 수 있도록 만들었다.</p>
<p>기술은 결국 사용자를 위해 존재한다. 그리고 여기서 사용자는 중학생들과, 그 수업을 운영하시는 비전공자 선생님이다.</p>
<p><em>(실제 수업이 이루어지는 공간)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/fa9f7075-715c-4141-ae02-1c918c9f3670/image.png" alt=""></p>
<h2 id="아쉬움---fsd-아키텍처와-컴포넌트-패턴">아쉬움 - FSD 아키텍처와 컴포넌트 패턴</h2>
<p>완벽한 프로젝트는 없다. 나에게도 아쉬움이 있다.</p>
<p>v2에서 FSD(Feature-Sliced Design) 아키텍처로 마이그레이션했는데, 더 정교하고 깔끔한 컴포넌트 패턴을 만들고 싶었다. 하지만 납품 일정에 쫓기면서 이상과 현실 사이에서 타협해야 했다. &quot;지금 동작하는 코드&quot;와 &quot;내가 원하는 수준의 코드&quot; 사이의 차이. 이건 아마 모든 개발자가 가지고 있는 영원한 숙제라고 생각한다.</p>
<p>다시 한다면, 초기 설계 단계에서 컴포넌트 컨벤션을 더 엄격하게 잡고 시작했을 것이다. 하지만 그 아쉬움조차 &quot;다음엔 더 잘할 수 있다&quot;는 자산이 된다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/61d26fde-8c8b-46c5-ac7d-14a1ce3aeb07/image.png" alt=""></p>
<h2 id="납품">납품</h2>
<p>납품 당일, USB 하나에 Docker 이미지와 설정 파일을 담아서 기관으로 향했다.</p>
<p>예상대로 순탄하지만은 않았다. 서버 데스크톱에는 WiFi 어댑터가 없었다. 사전 조사에서 WiFi IP로 통신하려던 계획이 틀어졌다. 서버를 WiFi 공유기의 LAN 포트에 유선으로 연결해서 같은 네트워크에 진입시키는 방식으로 해결했다. Docker 이미지를 로드하는 과정에서 이미지 이름이 매칭되지 않는 문제가 생겼고, 포트 80을 다른 서비스가 점유하고 있어서 nginx가 올라가지 않는 문제도 있었다.</p>
<p>하나씩 해결했다. 이미지에 태그를 다시 달고, 포트를 점유한 서비스를 중지시키고, 컨테이너를 다시 올렸다. 장학사 시연 전날 밤새워 장애를 해결하던 그때와 같았다. 문제는 언제든 생기지만, 해결하지 못할 문제는 없다.</p>
<p>그리고 드디어, 선생님이 데스크톱을 열면 서비스가 자동으로 시작되고, 학생들이 노트북 브라우저에서 주소를 입력하면 모의투자 화면이 뜨는 것을 확인했다. 납품이 완료된 순간이었다.</p>
<p><em>(기사에 나온 모주 ㅎㅎ)</em>
<img src="https://velog.velcdn.com/images/do-hyun123/post/dacb57b0-7fbf-47c1-9658-203bc68b3310/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/f848694f-40d0-4fb8-bc2d-bd9e540451b3/image.png" alt=""></p>
<h2 id="15개월-그리고-성장">15개월, 그리고 성장</h2>
<p>2024년 12월부터 지금까지 약 15개월. MOZU는 내게 단순한 프로젝트가 아니었다.</p>
<p>프론트엔드 리드에서 시작해 PO이자 팀 리더가 됐다. 8명의 팀을 이끌며 스프린트를 운영하고, 비전공자 선생님의 요구사항을 도메인 설계로 번역하고, 장학사 앞에서 시연하고, 소프트웨이브에서 CTO에게 프로덕트를 소개하고, 클라우드 서비스를 교실 한 대의 데스크톱에 담았다.</p>
<p>기술적으로 <code>SSE</code>, <code>Docker</code>, <code>nginx</code>, 온프레미스 인프라까지 영역을 넓혔고, 그보다 더 크게는 <strong>&quot;나의 프로덕트를 성장시킨다는 것&quot;</strong>이 무엇인지를 배웠다. 코드를 짜는 건 그 과정의 일부일 뿐이었다. 사용자를 이해하고, 팀을 이끌고, 위기를 넘기고, 아쉬움을 안고도 전진하는 것. 그 전부가 프로덕트를 만드는 일이었다.</p>
<p>MOZU는 내 포트폴리오의 핵심이지만, 그보다 먼저 나라는 개발자를 만든 프로젝트다.</p>
<p>곧 교실에서 중학생들이 모주를 열 것이다. 그 순간을 기대하며, 이 회고를 마친다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA["돈 줄 테니 세팅 좀 해줘" - 친구의 말에 n8n 자동화를 SaaS로 만들어버린 이야기]]></title>
            <link>https://velog.io/@do-hyun123/NOD-archive-release</link>
            <guid>https://velog.io/@do-hyun123/NOD-archive-release</guid>
            <pubDate>Wed, 11 Mar 2026 02:15:14 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-글-그리고-예상-못-한-반응">지난 글, 그리고 예상 못 한 반응</h2>
<p>지난 12월에 <a href="https://velog.io/@do-hyun123/ipone-n8n-automation">아이폰 공유 한 번으로 끝내는 지식 관리 자동화 파이프라인</a>이라는 글을 올렸습니다.
iOS 단축어, n8n, Gemini, Obsidian을 엮어서 &quot;읽은 글을 자동으로 요약하고 저장하는&quot; 제 워크플로우를 소개했었죠.</p>
<p>감사하게도 생각보다 많은 분들이 관심을 가져주셨습니다. 벨로그 트렌딩에도 오르고, 좋아요도 48개나 받았고요.
  댓글로 개선 아이디어를 주신 분들도 계셨고, <a href="https://velog.io/@0tmddn8/n8n%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-GeekNews-%EC%9A%94%EC%95%BD%EC%9C%BC%EB%A1%9C-%EC%95%84%EC%B9%A8-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0">제 블로그를 참고해서 본인만의 워크플로우를 만들어주신 분</a>도 계셔서 뿌듯했습니다.</p>
<p>근데 진짜 전환점은 친구와의 대화였습니다.
제 워크플로우를 보여주니까 대뜸 이러더라고요.</p>
<p>&quot;야, 이거 진짜 좋다. 내가 돈 줄 테니까 내 거에도 세팅 좀 해줘.&quot;</p>
<p>이 말을 듣는 순간 머리를 한 대 맞은 것 같았습니다.
사람들은 <strong>&#39;복잡한 자동화 과정&#39;</strong>을 원하는 게 아니라, <strong>&#39;지식이 내 것이 되는 결과&#39;</strong>를 원한다는 걸 깨달았거든요.</p>
<p>솔직히 n8n 서버 띄우고, API 키 발급받고, 웹훅 연결하는 거... 개발자인 저도 귀찮습니다. 일반 사용자는 오죽할까요.</p>
<p>그래서 만들기 시작했습니다. 복잡한 과정을 전부 걷어내고, 누구나 바로 쓸 수 있는 서비스로.</p>
<p>그게 NOD입니다.</p>
<h2 id="n8n-워크플로우의-한계-nod가-바꾼-것">n8n 워크플로우의 한계, NOD가 바꾼 것</h2>
<p>기존 n8n 자동화는 강력했지만 문제가 있었습니다.</p>
<ul>
<li>서버를 직접 돌려야 합니다. n8n 구독 비용에 관리까지.</li>
<li>요약 결과를 보려면 Obsidian이나 노션을 다시 열어야 합니다.</li>
<li>브라우저에서 읽다가 바로 처리하기가 어렵습니다.</li>
</ul>
<p>NOD는 이걸 크롬 익스텐션 하나로 합쳤습니다.
설치하고 로그인하면 끝입니다. 설정할 게 없어요.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/1eec7162-a1ae-45ed-8c0d-90c435839302/image.png" alt=""></p>
<p>하는 일은 간단합니다.</p>
<p>웹서핑하다가 NOD를 실행하면 현재 페이지 내용을 읽고, 핵심 요약과 인사이트를 뽑아주고, 나중에 써먹을 수 있는 형태로 저장해줍니다.</p>
<p>&quot;나중에 읽어야지&quot; 하고 탭 30개 열어놓거나, 북마크에 묻어두고 까먹는 거. 저도 맨날 그랬는데, 이제 안 그럽니다.</p>
<h2 id="블로그만-읽는-게-아닙니다">블로그만 읽는 게 아닙니다</h2>
<p>단순 텍스트 요약 툴이었으면 굳이 안 만들었을 겁니다.
개발자랑 리서처가 실제로 많이 보는 소스들을 제대로 처리하고 싶었습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/cf118545-8841-462b-8e70-38477d730513/image.png" alt=""></p>
<ul>
<li>GitHub 레포: README만 봐서는 파악이 안 되는 프로젝트 구조와 핵심 로직을 분석해줍니다.</li>
<li>논문/PDF: ArXiv 논문이나 긴 기술 문서를 빠르게 훑을 수 있습니다.</li>
<li>YouTube 영상: 1시간짜리 컨퍼런스 영상 다 안 봐도 됩니다. 핵심만 뽑아줍니다.</li>
</ul>
<h2 id="왜-5인가">왜 $5인가</h2>
<p>처음엔 저 혼자 쓰려고 만든 건데, 친구가 &quot;돈 낼게&quot;라고 한 순간 생각이 바뀌었습니다.
사이드 프로젝트로 끝내지 말고, 제대로 운영해보자.</p>
<p>가격은 월 $5로 잡았습니다.
무료로 풀었다가 서버비 감당 못 해서 접는 것보다, 쓰는 만큼 값어치를 돌려드리는 게 맞다고 생각했습니다. $5면 커피 한 잔인데, 매일 아끼는 시간을 생각하면 싼 거라고 믿고 있습니다. (아직은 저만 그렇게 믿고 있을 수도 있지만요.)</p>
<p>대신, 코드는 다 공개합니다.
익스텐션이 브라우저에서 뭘 하는지 궁금하시면 직접 보시면 됩니다. 기능 개선 PR도 환영합니다.</p>
<p>GitHub: <a href="https://github.com/jidohyun/NOD">jidohyun/NOD</a> (PR 환영합니다!)</p>
<h2 id="써보시고-솔직하게-알려주세요">써보시고 솔직하게 알려주세요</h2>
<p>이제 막 첫 버전을 배포했습니다. 부족한 점이 많을 겁니다.</p>
<p>이전 n8n 글 보시고 &quot;나도 저런 거 필요한데 세팅이 너무 복잡하다&quot;고 느끼셨던 분들, 한번 써보시고 편하게 피드백 주세요. 칭찬보다 불편했던 점이 더 도움됩니다.</p>
<p><a href="https://chromewebstore.google.com/detail/nod-article-analyzer/lifmaapjkbpfbdppiaeidcnicidpfknp?hl=ko&amp;utm_source=ext_sidebar">NOD 크롬 웹스토어</a>
<a href="https://nod-archive.com">NOD 랜딩페이지</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[학교 워크스테이션으로 원격 AI 개발 환경 구축하기 (OpenClaw + Tailscale)]]></title>
            <link>https://velog.io/@do-hyun123/School-OpenClaw-Tailscale</link>
            <guid>https://velog.io/@do-hyun123/School-OpenClaw-Tailscale</guid>
            <pubDate>Thu, 05 Mar 2026 04:10:11 GMT</pubDate>
            <description><![CDATA[<h2 id="1-개꿀-ㅋㅋ">1. 개꿀 ㅋㅋ</h2>
<p>이 세팅의 시작은 3학년에 새롭게 수강을 시작한 과목인 &quot;인공지능실무&quot; 과목에서 전달된 반가운 공지에서 비롯되었습니다. 실습을 위해 학생들에게 우분투 워크스테이션을 한 학기 동안 대여해준다는 내용이었죠.</p>
<p>사양을 확인해 보니 Intel Xeon Silver 4208(8코어 16스레드), RAM 78GB, NVIDIA Quadro RTX 5000, 1TB급 디스크 2개와 500GB 시스템 디스크 등 학생 개인 작업용으로는 과분할 만큼 훌륭한 스펙이었습니다.</p>
<p>이 엄청난 자원을 단순히 학교 실습실에서만 쓰는 것은 아깝다는 생각이 들었습니다. 제 메인 작업 기기인 맥북에서 언제 어디서든 이 워크스테이션에 원격으로 접속해, Docker 기반의 OpenClaw를 자유롭게 활용하는 파이프라인을 구축하게 되었습니다.</p>
<h2 id="2-전체-아키텍처-개요">2. 전체 아키텍처 개요</h2>
<p>원격 접속 및 제어의 전체 데이터 플로우는 다음과 같습니다.</p>
<p>맥북(외부 네트워크) → Tailscale(가상 네트워크) → 우분투 데스크탑(SSH 터널링) ↔ OpenClaw(Docker)</p>
<ol>
<li>네트워크 연결: 맥북과 우분투 데스크톱를 Tailscale로 묶어 동일한 가상 네트워크 대역에 위치시킵니다.</li>
<li>보안 컨텍스트 확보: SSH 로컬 포트 포워딩을 통해 원격지의 18789 포트를 맥북의 로컬호스트로 터널링합니다.</li>
<li>인증 및 접속: CLI를 통해 발급받은 토큰과 디바이스 페어링 절차를 거쳐 OpenClaw Control UI에 접근합니다.</li>
</ol>
<p>결과적으로 &quot;어느 네트워크 환경에 있든, 맥북에서 안전하게 학교 컴퓨터의 OpenClaw 환경을 제어하는 구조&quot;가 완성됩니다.</p>
<h2 id="3-네트워크-문제-tailscale을-통한-가상-네트워크-구성">3. 네트워크 문제: Tailscale을 통한 가상 네트워크 구성</h2>
<p>가장 먼저 부딪힌 문제는 &#39;네트워크의 분리&#39;였습니다. 맥북은 집이나 카페의 Wi-Fi를 사용하고, 우분투 데스크톱은 학교의 내부망에 물려 있어 직접적인 SSH 접속이 불가능한 상태였습니다.</p>
<p>그래서 Tailscale을 활용해 두 기기를 물리적 위치에 상관없이 하나의 가상 네트워크로 묶어주었습니다.</p>
<ul>
<li>맥북 측 (Client): CLI를 통해 설치하고 로그인하여 <code>100.x.x.x</code> 대역의 IP를 할당받습니다.</li>
<li>우분투 측 (Server): 동일하게 Tailscale을 설치하고 같은 tailnet에 연결합니다.</li>
</ul>
<p>이제 맥북에서 우분투가 할당받은 Tailscale IP를 향해 안정적으로 SSH 접속이 가능해졌습니다.</p>
<h2 id="4-웹-ui-접속-secure-context와-포트-포워딩">4. 웹 UI 접속: Secure Context와 포트 포워딩</h2>
<p>SSH 접속에 성공한 뒤, 브라우저에서 <code>http://우분투의_Tailscale_IP:18789</code>로 접속해 보았지만 다음과 같은 에러가 발생했습니다.</p>
<p><code>control ui requires device identity (use HTTPS or localhost secure context)</code></p>
<p>OpenClaw Control UI는 보안상의 이유로 HTTPS 통신이나 로컬호스트(localhost) 환경만을 신뢰합니다. 원격 IP로 단순 접근하면 브라우저가 Device Identity를 생성할 수 없어 차단되는 것이죠.</p>
<p>이 문제는 SSH 로컬 포트 포워딩으로 해결했습니다. 맥북 터미널에서 아래와 같이 터널을 생성합니다.</p>
<pre><code class="language-bash">ssh -L 18789:127.0.0.1:18789 사용자명@우분투의_Tailscale_IP</code></pre>
<p>이 세션을 유지한 채 브라우저에서 <code>http://127.0.0.1:18789/</code>로 접속하면, 브라우저 입장에서는 로컬 접속이므로 Secure Context 요구사항을 완벽하게 충족하게 됩니다.</p>
<h2 id="5-인증-및-디바이스-페어링">5. 인증 및 디바이스 페어링</h2>
<p>보안 컨텍스트 문제를 넘고 나면 이번엔 <code>gateway token missing</code> 권한 오류가 뜹니다. 우분투 환경의 컨테이너 내부에서 토큰을 확인하고 기기 승인을 진행해야 합니다.</p>
<ul>
<li>토큰 확인: 우분투 쉘에서 아래 명령어를 실행하면 토큰이 포함된 URL(<code>http://127.0.0.1:18789/#token=...</code>)이 출력됩니다. 이를 그대로 브라우저에서 열어줍니다.</li>
</ul>
<pre><code class="language-bash">docker compose run --rm openclaw-cli dashboard --no-open</code></pre>
<ul>
<li>디바이스 페어링 승인: 브라우저에서 접속을 시도하면 Pending 상태가 됩니다. 다시 터미널에서 요청 목록을 확인하고 승인합니다. (이때 &#39;토큰&#39;이 아닌 &#39;Request ID&#39;를 입력해야 합니다.)</li>
</ul>
<pre><code class="language-Bash">docker compose run --rm openclaw-cli devices list
docker compose run --rm openclaw-cli devices approve &lt;requestId&gt;</code></pre>
<h2 id="6-개인-ai-비서-완성">6. 개인 AI 비서 완성</h2>
<p>이 워크플로우를 구축한 뒤로, 훌륭한 학교 인프라를 마치 제 개인 서버처럼 편안하게 활용할 수 있게 되었습니다.</p>
<p>현재는 가볍게 Codex 5.3 모델 위주로 OpenClaw를 세팅해 두고, Discord Bot과 연동하여 과제를 효율적으로 처리하는 데 유용하게 쓰고 있습니다. <del>(개꿀 ㅋㅋ)</del></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/37051fb0-de79-4b13-965e-82f7b769472c/image.png" alt=""></p>
<p>하지만 RTX 5000과 78GB RAM이라는 든든한 스펙과 CUDA 환경이 갖춰져 있는 만큼, 추후에는 다양한 로컬 LLM 모델들을 직접 올려보며 본격적인 실험용으로도 활용해 볼 계획입니다.</p>
<p>처음 환경을 세팅할 때는 네트워크 터널링이나 보안 인증 같은 요소들이 복잡하게 느껴질 수 있지만, 한 번 구축해 두면 강력한 자원을 장소에 구애받지 않고 다룰 수 있습니다. 여러분도 유휴 자원이 있다면 자신만의 &#39;개인 AI 비서&#39;를 한번 구축해 보시길 추천합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[컴퓨터는 '문자열'을 어떻게 '숫자'로 바꿀까? (Python vs C)]]></title>
            <link>https://velog.io/@do-hyun123/How-to-computer-number-to-string-Python-vs-C</link>
            <guid>https://velog.io/@do-hyun123/How-to-computer-number-to-string-Python-vs-C</guid>
            <pubDate>Sat, 07 Feb 2026 12:36:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스트는 CoreDumped의 <a href="https://www.youtube.com/watch?v=m8v_SRpxyN4">&quot;HOW COMPUTERS CAST STRINGS TO NUMBERS&quot;</a>를 보고 영감을 받아 작성한 글입니다.</p>
</blockquote>
<p>우리는 코딩을 할 때 너무나 당연하게 문자열을 숫자로 변환합니다.</p>
<pre><code class="language-python"># Python
num = int(&quot;12345&quot;)</code></pre>
<pre><code class="language-c">// C
int num = atoi(&quot;12345&quot;);</code></pre>
<p>하지만 컴퓨터는 <code>&quot;12345&quot;</code>라는 문자열을 봤을 때, 이것이 수학적 의미의 <strong>1만 2천 3백 4십 5</strong>라는 것을 직관적으로 알지 못합니다. 컴퓨터 입장에서 문자열은 그저 <strong>문자들의 배열</strong>일 뿐입니다. 아래 사진과 같이 말이죠.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/279e1290-7fea-4bc2-acc6-e0a372700c07/image.png" alt=""></p>
<p>그렇다면 내부적으로 도대체 어떤 연산이 일어나길래 이 문자 덩어리가 <strong>산술 연산이 가능한 숫자</strong>로 바뀌는 걸까요?</p>
<hr>
<h2 id="1-숫자는-숫자가-아니다">1. 숫자는 숫자가 아니다</h2>
<p>가장 먼저 이해해야 할 것은 <strong>&#39;문자 0&#39;과 &#39;숫자 0&#39;은 다르다</strong>는 점입니다. 컴퓨터는 모든 문자를 숫자로 매핑해서 저장하는데, 가장 표준적인 <strong>ASCII(아스키) 코드</strong>를 보면 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th><strong>문자 (Char)</strong></th>
<th><strong>ASCII 값 (Decimal)</strong></th>
<th><strong>이진수 (Binary)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>&#39;0&#39;</code></td>
<td><strong>48</strong></td>
<td>0011 0000</td>
</tr>
<tr>
<td><code>&#39;1&#39;</code></td>
<td><strong>49</strong></td>
<td>0011 0001</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>
<td><code>&#39;9&#39;</code></td>
<td><strong>57</strong></td>
<td>0011 1001</td>
</tr>
</tbody></table>
<p>만약 우리가 문자 <code>&#39;5&#39;</code>와 <code>&#39;3&#39;</code>을 더한다면 컴퓨터는 이렇게 계산합니다.</p>
<blockquote>
<p><code>&#39;5&#39;</code> (53) + <code>&#39;3&#39;</code> (51) = <strong>104</strong> (<code>&#39;h&#39;</code>)</p>
</blockquote>
<p>우리가 원하는 값인 <strong>8</strong>이 나오지 않습니다. 그래서 문자열을 숫자로 바꾸는 알고리즘의 <strong>첫 번째 핵심</strong>은 바로 <strong>ASCII 값 보정</strong>입니다.</p>
<p>어떻게 하면 ASCII값으로  정수 형태의 값을 얻을 수 있을까요?</p>
<blockquote>
<p>핵심 공식: <code>&#39;문자&#39; - &#39;0&#39;</code>
문자에서 <code>&#39;0&#39;</code>(48)의 아스키 값을 빼버리면, 실제 정수 값을 얻을 수 있습니다.</p>
</blockquote>
<ul>
<li><code>&#39;5&#39;</code> (53) - <code>&#39;0&#39;</code> (48) = <strong>5</strong> (Integer)</li>
<li><code>&#39;9&#39;</code> (57) - <code>&#39;0&#39;</code> (48) = <strong>9</strong> (Integer)</li>
</ul>
<hr>
<h2 id="2-자릿수-밀어내기">2. 자릿수 밀어내기</h2>
<p>단일 문자가 아니라 <code>&quot;123&quot;</code> 같은 여러 자리 문자열은 어떻게 처리할까요? 여기서 <strong>두 번째 핵심</strong>인 <strong>자릿수 누적</strong> 알고리즘이 등장합니다.</p>
<p>우리가 <code>&quot;123&quot;</code>을 읽는 순서는 <code>1</code> → <code>2</code> → <code>3</code>입니다.</p>
<p>이전 값에 <strong>10을 곱해서 자릿수를 하나 올리고</strong>, 새로운 숫자를 더하는 방식을 반복합니다.</p>
<ol>
<li><strong><code>&quot;1&quot;</code> 읽음</strong>: <code>0 * 10 + 1</code> = <strong>1</strong></li>
<li><strong><code>&quot;2&quot;</code> 읽음</strong>: <code>1 * 10 + 2</code> = <strong>12</strong> (기존 1이 10의 자리가 됨)</li>
<li><strong><code>&quot;3&quot;</code> 읽음</strong>: <code>12 * 10 + 3</code> = <strong>123</strong> (기존 12가 120이 됨)</li>
</ol>
<p>2진수로 설명하면 아래와 같죠</p>
<hr>
<h2 id="3-c언어-구현체-atoi">3. C언어 구현체 (<code>atoi</code>)</h2>
<p>C언어의 표준 라이브러리 함수인 <code>atoi</code>(ASCII to Integer)는 이 로직을 가장 날것(Raw) 그대로 보여줍니다. <code>glibc</code>나 임베디드 커널의 <code>atoi</code> 구현을 단순화하면 아래와 같습니다.</p>
<pre><code class="language-c">int my_atoi(const char *str) {
    int result = 0;
    int sign = 1;
    int i = 0;

    // 1. 공백 건너뛰기
    while (str[i] == &#39; &#39;) i++;

    // 2. 부호 확인 (+, -)
    if (str[i] == &#39;-&#39; || str[i] == &#39;+&#39;) {
        if (str[i] == &#39;-&#39;) sign = -1;
        i++;
    }

    // 3. 숫자 변환 (핵심 로직)
    while (str[i] &gt;= &#39;0&#39; &amp;&amp; str[i] &lt;= &#39;9&#39;) {
        // 기존 값에 10을 곱하고, 현재 문자에서 &#39;0&#39;을 뺀 값을 더함
        result = result * 10 + (str[i] - &#39;0&#39;);
        i++;
    }

    return result * sign;
}</code></pre>
<h3 id="포인트">포인트</h3>
<ul>
<li><strong>포인터 연산</strong>: 문자열을 배열 인덱스(<code>str[i]</code>)나 포인터(<code>ptr</code>)로 한 글자씩 접근합니다.</li>
<li><strong><code>str[i] - &#39;0&#39;</code></strong>: 위에서 설명한 ASCII 보정 로직이 그대로 들어갑니다.</li>
<li><strong>오버플로우 위험</strong>: C언어의 <code>int</code>는 크기(보통 4바이트)가 정해져 있어, 숫자가 너무 크면 오버플로우가 발생할 수 있습니다. (그래서 실무에선 <code>strtol</code>을 더 권장합니다.)</li>
</ul>
<hr>
<h2 id="4-python-구현체-int">4. Python 구현체 (<code>int</code>)</h2>
<p>Python은 C보다 훨씬 고수준 언어입니다. <code>int(&quot;12345678901234567890&quot;)</code> 처럼 엄청나게 큰 숫자도 오버플로우 없이 처리합니다. 어떻게 가능할까요?</p>
<p>Python(CPython)의 소스 코드 중 <code>Objects/longobject.c</code>를 살펴보면 그 비밀이 있습니다. Python의 <code>int</code>는 단순한 4바이트 메모리가 아니라 <strong><code>PyLongObject</code>라는 구조체(객체)</strong> 입니다.</p>
<h3 id="python의-int-변환-과정">Python의 <code>int()</code> 변환 과정</h3>
<ol>
<li><strong>전처리</strong>: 문자열의 앞뒤 공백을 제거(<code>strip</code>)하고 <code>_</code> (언더스코어) 같은 구분자를 처리합니다.</li>
<li><strong>진법 확인</strong>: <code>0x</code>(16진수) 등이 있는지 확인하여 진법(Base)을 결정합니다.</li>
<li><strong>변환 (<code>PyLong_FromString</code>)</strong>:<ul>
<li>숫자가 작을 때는 C와 비슷한 방식으로 변환합니다.</li>
<li>숫자가 시스템의 메모리 한계(<code>long</code>)를 넘어가면, 숫자를 <strong>여러 개의 큰 조각(Digit 배열)</strong>으로 나누어 저장합니다.</li>
</ul>
</li>
</ol>
<p>실제 구현체 소스코드: <a href="https://github.com/python/cpython/blob/f4364a51c1a8ce682fe9e4e96c6aba9f1b590422/Objects/longobject.c#L3044">https://github.com/python/cpython/blob/f4364a51c1a8ce682fe9e4e96c6aba9f1b590422/Objects/longobject.c#L3044</a></p>
<pre><code class="language-c">/* CPython 소스 코드 (Objects/longobject.c) 개념적 의사코드 */

PyObject *
PyLong_FromString(const char *str, char **pend, int base)
{
    // ... (공백 제거 및 부호 처리 생략) ...

    long x = 0;

    // 작은 숫자일 경우: C언어와 똑같은 방식(Accumulator)으로 빠르게 처리
    while (*str &gt;= &#39;0&#39; &amp;&amp; *str &lt;= &#39;9&#39;) {
        long dig = *str - &#39;0&#39;;

        // 오버플로우 체크를 하며 10을 곱하고 더함
        if ((x &gt; limit) || (x * 10 &gt; limit)) {
            // 오버플로우 발생 시 &#39;Big Integer&#39; 처리 로직으로 전환
            return PyLong_FromBigString(...); 
        }
        x = x * 10 + dig;
        str++;
    }

    // PyObject로 포장해서 반환
    return PyLong_FromLong(x);
}</code></pre>
<h3 id="포인트-1">포인트</h3>
<ul>
<li><strong>객체 반환</strong>: C는 날것의 숫자를 반환하지만, Python은 숫자를 감싼 <strong>객체 포인터(<code>PyObject*</code>)</strong>를 반환합니다.</li>
<li><strong>무한한 정수</strong>: Python은 내부적으로 배열을 사용해 숫자를 저장하므로 메모리가 허용하는 한 무한히 큰 숫자를 만들 수 있습니다.</li>
</ul>
<hr>
<h2 id="5-결론">5. 결론</h2>
<p>&quot;문자열을 숫자로 바꾼다&quot;는 한 줄의 코드 뒤에는 다음과 같은 CS의 기초가 숨어 있었습니다.</p>
<ol>
<li><strong>인코딩의 이해</strong>: 문자는 결국 숫자(ASCII)로 저장된다.</li>
<li><strong>알고리즘적 사고</strong>: 자릿수를 올리기 위해 <code>x10</code>을 반복한다.</li>
<li><strong>언어별 철학</strong>:<ul>
<li><strong>C</strong>는 성능과 하드웨어 접근성을 위해 오버플로우 위험을 감수하고 단순하게 처리합니다.</li>
<li><strong>Python</strong>은 개발자의 편의성을 위해 내부적으로 복잡한 객체와 메모리 관리를 수행하여 &#39;무한한 정수&#39;를 제공합니다.</li>
</ul>
</li>
</ol>
<p>우리가 무심코 쓰는 함수 하나에도, 선배 개발자들의 치열한 고민과 최적화가 담겨 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[엘릭서, 피닉스로 실시간 멀티플레이 실험 - Cursor Party (1)]]></title>
            <link>https://velog.io/@do-hyun123/elixir-phoenix-cursor-party</link>
            <guid>https://velog.io/@do-hyun123/elixir-phoenix-cursor-party</guid>
            <pubDate>Fri, 02 Jan 2026 10:00:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/do-hyun123/post/adcfc0ef-e442-40f7-a14a-74c9c502f82c/image.png" alt=""></p>
<h3 id="1-긱뉴스에서-얻은-영감">1. 긱뉴스에서 얻은 영감</h3>
<p>최근 긱뉴스를 둘러보다가 흥미로운 스레드를 발견했습니다.</p>
<p><a href="https://news.hada.io/topic?id=11762">Discord가 한대의 서버로 2백만명의 동시 사용자를 서빙하는 방법</a></p>
<p>평소 매일같이 사용하는 디스코드가 수백만 명의 동시 접속자와 실시간 음성/채팅 트래픽을 어떻게 감당하는지에 대한 내용이었습니다.  비결의 핵심에는 BEAM 위에서 동작하는 <a href="https://elixir-lang.org/">Elixir(엘릭서)</a>라는 언어가 있었고, 비슷한 구조를 웹 서비스에 적용할 때 많이 쓰이는 프레임워크인 <a href="https://www.phoenixframework.org/">Pheonix(피닉스)</a>도 있었습니다.</p>
<p>해당 스레드에서 설명한 “길드마다 하나의 프로세스, 세션마다 또 다른 프로세스”라는 아이디어를, 훨씬 작은 스케일의 실험으로 옮겨 보고 경험해보고 싶었습니다.</p>
<h3 id="2-cursor-party">2. Cursor Party</h3>
<p>그렇게 바이브코딩으로 개발한 프로젝트가 바로 Cursor Party입니다.</p>
<p><a href="https://cursor-party.koyeb.app/">Cursor Party 체험해보기</a>
<a href="https://github.com/jidohyun/cursor-party">Cursor Party 깃허브</a>
(14일 무료 호스팅플랜이 끝나 2026년 1월 9일부로 베타 테스트 서비스를 종료했습니다. 
관심 가져주셔서 감사했습니다!)</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/51d79070-b797-4eed-8ea8-1e554a10ca66/image.gif" alt=""> (Show GN에 올렸더니 어떤분이 오토클리커 돌려놓으셨네요 😂)</p>
<p>이 웹 게임은 아주 간단합니다.</p>
<ul>
<li>실시간 커서 공유: 접속한 모든 사용자의 마우스 커서 위치가 실시간으로 화면에 공유됩니다.</li>
<li>보스 레이드: 화면 중앙에 위치한 보스를 클릭하여 모든 유저가 협력해 HP를 깎습니다.</li>
</ul>
<p>핵심은 이 모든 상호작용이 지연 없이 모든 클라이언트에게 동기화되어야 한다는 점입니다.</p>
<h3 id="3-왜-elixir와-phoenix인가">3. 왜 Elixir와 Phoenix인가?</h3>
<p>직접 만들어보며 느낀 Elixir와 Phoenix LiveView의 경험은 기존의 웹 개발 방식과는 달랐습니다.</p>
<h4 id="3-1-자바스크립트-없는-실시간-처리">3-1. 자바스크립트 없는 실시간 처리</h4>
<p>보통 이런 실시간 기능을 구현하려면 React나 Vue로 복잡한 프론트엔드 상태 관리를 구축하고, WebSocket 서버와 통신하는 코드를 작성해야 합니다. 상태 동기화 로직만 짜다가 1시간이 다 갈 수도 있습니다.</p>
<p>하지만 Phoenix LiveView는 접근 방식이 완전히 달랐습니다.</p>
<ul>
<li>서버 중심 상태 관리: 유저의 위치, 보스의 체력 등 모든 상태를 Elixir 서버가 관리합니다.</li>
<li>자동 동기화: 상태가 변경되면 Phoenix가 알아서 변경된 부분만 추출하여 브라우저로 전송합니다.</li>
</ul>
<p>결과적으로 최소한의 JS hook만으로, 대부분의 인터랙션을 서버 렌더링(LiveView)만으로 구현할 수 있었습니다.</p>
<h4 id="3-2-강력한-동시성">3-2. 강력한 동시성</h4>
<p>수십~수백 명 규모로 좌표를 브로드캐스트하는 테스트를 해도 서버는 여유로웠습니다.
Elixir/BEAM이 설계상 수십만 프로세스를 동시에 다루도록 만들어져 있어서, 규모를 더 키워도 구조적으로는 같은 패턴을 그대로 적용할 수 있습니다.</p>
<p>Node.js나 Python으로 구현했다면 스레드 관리나 성능 최적화를 고민했겠지만, Elixir 환경에서는 BEAM이 프로세스·메시지 패싱·클러스터링을 언어/런타임 차원에서 지원해 주기 때문에, 최소한 이 규모의 토이 프로젝트에서는 성능 병목보다 도메인 로직에 더 집중할 수 있었습니다.</p>
<h3 id="4-마치며">4. 마치며</h3>
<p>이 프로젝트는 1시간 개발로 시작해 현재 오픈 베타 상태로 배포되었습니다. 기능은 단순하지만, Elixir가 자랑하는 분산 처리의 안정성을 직접 눈으로 확인하기엔 충분했습니다.</p>
<p>앞으로는 매주 새로운 기능을 하나씩 추가하며 이 프로젝트를 발전시켜 나갈 예정입니다.</p>
<p>더 궁금하신 분들은 아래 글을 읽어보시면 좋을 것 같습니다.
<a href="https://blog.yevgnenll.me/posts/why-we-study-elixir-erlang-otp-with-beam">왜 Elixir를 공부해야 할까? Erlang/OTP BEAM으로 살펴보는 엘릭서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[아이폰 공유 한 번으로 끝내는 지식 관리 자동화 파이프라인]]></title>
            <link>https://velog.io/@do-hyun123/ipone-n8n-automation</link>
            <guid>https://velog.io/@do-hyun123/ipone-n8n-automation</guid>
            <pubDate>Tue, 30 Dec 2025 02:49:36 GMT</pubDate>
            <description><![CDATA[<h3 id="1-링크드인에서-얻은-영감">1. 링크드인에서 얻은 영감</h3>
<p>이 프로젝트의 시작은 우연히 링크드인 피드를 보다가 얻은 영감이었습니다.</p>
<p>여러 개발자분들이 아이폰 &#39;단축어&#39; 기능을 활용해 기발한 방식으로 생산성을 높이는 사례들을 공유해주시고 계시더군요. 그 글들을 보면서 문득 &quot;나도 저 기능을 활용해서 내 고민을 해결해 볼 수 있지 않을까?&quot; 하는 생각이 들었습니다.</p>
<p>제 가장 큰 고민은 <strong>&#39;정보 쌓아두기&#39;</strong>이었습니다. 유튜브 영상이든 기술 블로그든, 읽고 끝나면 금방 잊어버리거나 &quot;나중에 정리해야지&quot; 하고 북마크만 쌓아두다가 다시는 안 보는 일이 반복되었습니다. 특히 Frontend나 AI 관련 글처럼 밀도가 높은 콘텐츠는 정리가 필수적인데도 말이죠.</p>
<p>그래서 링크드인에서 본 아이디어에 제 니즈를 더해, 아이폰에서 링크 한 번 공유하면 n8n이 내용을 긁어와 LLM으로 요약하고, 옵시디언에 마크다운 노트를 자동으로 만들어 주는 파이프라인을 구축하게 되었습니다.</p>
<p>이제는 &quot;좋은 콘텐츠를 발견했다&quot;는 생각이 들면, 고민 없이 공유 버튼만 누르고 나중에 옵시디언에서 편하게 리뷰하면 됩니다.</p>
<h3 id="2-전체-아키텍처-개요">2. 전체 아키텍처 개요</h3>
<p>자동화의 전체 데이터 플로우는 다음과 같습니다.</p>
<blockquote>
<p>아이폰(공유) → 단축어(Webhook) → n8n(처리 및 요약) → Google Drive(저장) ↔ 옵시디언(Sync)</p>
</blockquote>
<ol>
<li>아이폰(브라우저/유튜브)에서 공유 → 단축어 실행</li>
<li>단축어가 현재 URL을 n8n 웹훅으로 POST</li>
<li>Process (n8n):<ul>
<li>URL 분석 (유튜브 vs 일반 웹)</li>
<li>유튜브: Transcript API로 자막 추출</li>
<li>웹: HTML 파싱 후 본문 텍스트 추출</li>
</ul>
</li>
<li>정제된 텍스트를 Google Gemini에 넘겨 &quot;지식 관리용 마크다운&quot;으로 요약</li>
<li>결과물을 <strong>Google Drive(옵시디언 Vault 폴더)</strong>에 .md 파일로 저장</li>
</ol>
<p>결과적으로 <strong>&quot;어디에서 링크를 공유하든, 옵시디언 Inbox에 표준화된 요약 노트가 쌓이는 구조&quot;</strong>가 완성됩니다.</p>
<h3 id="3-아이폰-단축어">3. 아이폰 단축어</h3>
<p>가장 먼저 iOS 단축어를 만들었습니다. 이 단축어의 역할은 단순합니다. 공유 시트에서 넘어온 URL을 n8n에게 던져주는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/48fde8b1-9663-4656-a1ab-eb8dc4bac5c5/image.png" alt=""></p>
<p>설정 순서:</p>
<ol>
<li>단축어 앱에서 새 단축어 생성 → 설정에서 &quot;공유 시트에서 보기&quot; 활성화</li>
<li>입력 타입은 <strong>&quot;URL&quot;</strong>로 지정</li>
<li>액션 1: &quot;단축어 입력에서 URL 가져오기&quot;</li>
<li>액션 2: &quot;URL의 콘텐츠 가져오기&quot; (HTTP Request)<ul>
<li>URL: n8n Webhook URL (Production)</li>
<li>Method: POST</li>
<li>Body: JSON → {&quot;url&quot;: &quot;입력된 URL 변수&quot;}</li>
</ul>
</li>
</ol>
<p>이렇게 하면 유튜브 앱이나 브라우저에서 &quot;공유 → 단축어 실행&quot; 시 해당 URL이 n8n으로 전송됩니다.</p>
<h3 id="4-n8n-워크플로우">4. n8n 워크플로우</h3>
<p>n8n에서는 들어온 URL이 유튜브인지, 일반 블로그인지 판단하여 처리 방식을 다르게 가져갑니다.</p>
<h4 id="4-1-웹훅-및-분기">4-1. 웹훅 및 분기</h4>
<p>맨 앞단에는 Webhook 노드를 두고 POST 요청을 기다립니다. 들어오는 데이터는 <code>{&quot;url&quot;: &quot;https://...&quot;}</code> 형태입니다.</p>
<p>그다음 IF 노드를 사용하여 URL에 <code>youtube.com</code> 혹은 <code>youtu.be</code>가 포함되어 있는지 확인합니다.</p>
<ul>
<li>True: YouTube 브랜치로 이동</li>
<li>False: Web Page 브랜치로 이동</li>
</ul>
<p>각 브랜치의 끝에서는 <strong>동일한 스키마(type, content, title, url)</strong>로 데이터를 통일시켜, 이후 LLM 노드가 헷갈리지 않게 설계했습니다.</p>
<h3 id="5-유튜브-브랜치-영상-id와-자막-추출">5. 유튜브 브랜치: 영상 ID와 자막 추출</h3>
<p>유튜브 영상은 내용을 텍스트로 바꾸는 것이 핵심입니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/ff756dc1-5ba9-45fc-8165-d99819cd08e8/image.png" alt=""></p>
<ol>
<li>Extract Video ID: Code 노드(JS)를 이용해 URL에서 <code>v=</code> 뒤의 ID나 단축 URL의 ID를 파싱합니다.</li>
<li>Get Transcript: <a href="https://www.youtube-transcript.io/api">YouTube Transcript API</a>를 호출해 자막을 가져옵니다.</li>
<li>Transcript(데이터 정제): 자막 세그먼트들을 하나의 긴 텍스트(fullTranscript)로 합칩니다.</li>
<li>Prepare Content(Youtube 포메팅):<ul>
<li>type: &quot;youtube&quot;</li>
<li>content: 자막 전체 텍스트 (fullTranscript)</li>
<li>title: 영상 제목</li>
</ul>
</li>
</ol>
<h3 id="6-블로그-브랜치-블로그-스크래핑">6. 블로그 브랜치: 블로그 스크래핑</h3>
<p>웹페이지는 사이트마다 구조가 달라서 조금 까다롭습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/cb2d0e4e-0f29-498f-9daf-1cc337409285/image.png" alt=""></p>
<ol>
<li>HTML Request: HTTP Request (GET)로 원본 HTML을 긁어옵니다.</li>
<li>콘텐츠, 도메인 나누기: 긁어온 HTML에서 콘텐츠와 도메인을 추출합니다.</li>
<li>도메인별 Selector 분기:<ul>
<li>단순히 body 태그를 긁으면 메뉴나 광고까지 다 딸려옵니다.</li>
<li>velog.io, tistory.com, naver.com 등 자주 가는 사이트의 도메인을 추출해 Switch 노드로 분기했습니다.</li>
<li>Velog: div.atom-one</li>
<li>Naver: div.con_view</li>
<li>Toss: div.css-lvn47db</li>
<li>그 외: article, main, .post 등 일반적인 본문 태그 시도</li>
</ul>
</li>
<li>Prepare Content(Blog 포메팅): HTML 태그를 제거하고 순수 텍스트만 남겨 content 필드에 저장합니다.</li>
</ol>
<h3 id="7-llm으로-요약-gemini">7. LLM으로 요약 (Gemini)</h3>
<p>이제 가장 중요한 단계입니다. 정제된 텍스트를 Google Gemini에게 넘겨 옵시디언에 넣기 좋은 마크다운 포맷으로 변환합니다.</p>
<h4 id="7-1-프롬프트-엔지니어링">7-1. 프롬프트 엔지니어링</h4>
<p>n8n의 Basic LLM Chain 과 Google Gemini Chat Model(Flash 3.0 preview)을 사용했습니다. 프롬프트는 다음과 같이 구성했습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/e3e8c698-7948-4ab8-85a9-b9e6be8e4b90/image.png" alt=""></p>
<p><strong>User 프롬프트</strong></p>
<pre><code class="language-md">다음은 {{ $json[&quot;type&quot;] === &quot;youtube&quot; ? &quot;유튜브 영상&quot; : &quot;웹페이지/문서&quot; }}의 전체 내용입니다.
이를 읽고 옵시디언에 저장할 마크다운 노트를 생성해주세요.

요구사항:
- 한국어로 작성
- 코드블록 기호(```) 없이 순수 마크다운 본문만 출력
- 구조:
  ---
  title: &quot;{{ $json[&quot;title&quot;] || &quot;Untitled&quot; }}&quot;
  source: &quot;{{ $json[&quot;url&quot;] }}&quot;
  created: {{ $now }}
  type: {{ $json[&quot;type&quot;] }}
  tags: [reading, summary]
  ---
  # 핵심 요약
  - 3~5개의 핵심 포인트

  # 상세 내용
  - 중요한 개념, 정의, 설명을 bullet point로 정리

  # 인사이트 / 할 일
  - 내가 실행할 액션이나 느낀 점 정리

본문 텍스트:
{{ $json[&quot;content&quot;] }}</code></pre>
<p><strong>System 프롬프트</strong></p>
<pre><code class="language-md">당신은 사용자의 지식 자산화를 돕는 비서이다. 긴 텍스트를 받아 한국어로 정리된 마크다운 노트를 만들어라. </code></pre>
<h4 id="7-2-파일명-생성">7-2. 파일명 생성</h4>
<p>생성된 마크다운 내용 중 <code>title: &quot;...&quot;</code> 부분을 정규식으로 다시 추출하여, 파일명으로 사용할 safeTitle (특수문자 제거, 슬러그화)을 만듭니다.</p>
<pre><code class="language-js">const items = $input.all();

function slugify(str) {
  return (str || &quot;untitled&quot;)
    .toLowerCase()
    .replace(/[^a-z0-9가-힣]+/g, &quot;-&quot;)
    .replace(/(^-|-$)+/g, &quot;&quot;)
    .slice(0, 80);
}

return items.map(item =&gt; {
  // LLM 출력이 통째로 들어있는 필드 이름(예: text, result 등)에 맞춰서 바꾸세요.
  const md = item.json.text || item.json.result || &quot;&quot;;

  // --- 블록 안에서 title 라인 찾기
  let title = &quot;Untitled&quot;;
  const match = md.match(/title:\s*&quot;([^&quot;]+)&quot;/);
  if (match &amp;&amp; match[1]) {
    title = match[1];
  }

  return {
    json: {
      ...item.json,
      markdown: md,          // 나중에 파일 내용으로 쓸 필드
      title,                 // 파싱한 제목
      safeTitle: slugify(title),
    },
  };
});
</code></pre>
<h3 id="8-google-drive에-저장-옵시디언-sync">8. Google Drive에 저장 (옵시디언 Sync)</h3>
<p>마지막으로 파일을 저장할 차례입니다. 저는 옵시디언 볼트를 Google Drive로 동기화해서 쓰고 있습니다.</p>
<ol>
<li>인증: 개인 OAuth 대신 Service Account를 사용하여 인증 만료 걱정 없이 연결했습니다.</li>
<li>파일 생성: Google Drive 노드의 Create file from text 액션을 사용합니다.<ul>
<li>File Name: <code>{{ $json[&quot;safeTitle&quot;] }}.md</code></li>
<li>Content: Gemini가 뱉어준 마크다운 전체</li>
</ul>
</li>
<li>완료: n8n이 성공하면, 잠시 후 제 옵시디언에 정리한 파일이 생성됩니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/9ab6e6c7-b2a5-4dab-ae14-dba361e7d727/image.png" alt=""></p>
<h3 id="9-마치며-정보-소비-패턴의-변화">9. 마치며: 정보 소비 패턴의 변화</h3>
<p>이 워크플로우를 구축한 뒤로, 정보를 대하는 방식이 완전히 달라졌습니다.</p>
<ul>
<li>&quot;언젠가 봐야지&quot;가 아니라 &quot;지금 요약해서 저장해두자&quot;가 되었습니다.</li>
<li>긴 영상이나 글을 다시 처음부터 볼 필요 없이, 요약된 핵심과 내 인사이트를 먼저 훑어볼 수 있습니다.</li>
<li>모든 외부 지식이 동일한 템플릿으로 저장되니 검색과 관리가 훨씬 수월합니다.</li>
</ul>
<p>처음 구축할 땐 n8n 설정이 조금 복잡해 보일 수 있지만, 한 번 만들어두면 매일매일의 생산성이 달라집니다. 여러분도 자신만의 &#39;지식 파이프라인&#39;을 한번 구축해 보시길 추천합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Gemini 2.5 flash로 이슈 -> PR 워크플로우 구축하기]]></title>
            <link>https://velog.io/@do-hyun123/issue-pr-workflow</link>
            <guid>https://velog.io/@do-hyun123/issue-pr-workflow</guid>
            <pubDate>Tue, 16 Dec 2025 06:19:39 GMT</pubDate>
            <description><![CDATA[<p>최근 학교에서 졸업생 선배님과 함께하는 멘토링 시간이 있었습니다. 현업에서 활약하고 계신 선배님의 경험담을 들으며 가장 인상 깊었던 점은, <strong>&quot;요즘 실무에서는 반복적이고 단순한 작업은 과감하게 AI를 사용한 워크플로우를 사용해서 해결한다.&quot;</strong>는 것이었습니다.</p>
<p>단순히 &quot;좋은 이야기였다&quot;로 끝내고 싶지 않았습니다. 선배님의 조언을 내 프로젝트인 <strong>&#39;MOZU&#39;</strong>에 당장 적용해보고 싶다는 욕심이 생겼습니다.</p>
<p>&quot;단순한 버그 수정이나 스타일 변경 같은 이슈 처리를 자동화할 수는 없을까?&quot;</p>
<p>이 질문에서 시작해, GitHub Actions와 <strong>AI(Gemini)</strong>를 엮어 <strong>&quot;이슈가 등록되면 AI가 코드를 분석해 스스로 해결하고 PR까지 날리는 봇&quot;</strong>을 만들어보게 되었습니다. 멘토링에서 얻은 인사이트를 기술로 구현해낸 과정을 공유합니다.</p>
<h3 id="1-목표-아키텍처">1. 목표 아키텍처</h3>
<p>단순히 코드를 짜달라는 게 아니라, <strong>문맥</strong>을 파악하는 에이전트를 만들고 싶었습니다.</p>
<ol>
<li>Trigger: GitHub Issue가 생성되고 <code>🤖 ai-fix</code> 라벨이 붙음.</li>
<li>Analyze: 프로젝트 전체 파일 구조를 AI에게 보여주고 &quot;이 문제를 해결하려면 어떤 파일을 봐야 해?&quot;라고 물어봄.</li>
<li>Coding: AI가 지목한 파일의 내용을 실제로 읽어서 전달하고 수정을 요청.</li>
<li>PR: 수정된 코드를 바탕으로 브랜치를 따고 Pull Request 생성.</li>
</ol>
<h3 id="2-기술-스택-선정">2. 기술 스택 선정</h3>
<ul>
<li>CI/CD: GitHub Actions</li>
<li>Brain: Google Gemini 2.5 Flash (속도가 빠르고 무료 티어 한도가 넉넉함)</li>
<li>Runtime: Node.js</li>
<li>Project Env: Yarn Berry (Monorepo)</li>
</ul>
<h3 id="3-핵심-구현-내용">3. 핵심 구현 내용</h3>
<h4 id="3-1-2단계-추론">3-1. 2단계 추론</h4>
<p>처음에는 무턱대고 모든 코드를 다 넣으려 했지만, 토큰 비용과 정확도 문제가 있었습니다. 그래서 전략을 바꿨습니다.</p>
<ul>
<li>Step 1: 파일 탐색 -&gt; glob으로 파일 리스트만 추출해서 Gemini에게 &quot;관련 파일 경로&quot;를 JSON으로 리턴받음.</li>
<li>Step 2: 코드 수정 -&gt; 리턴받은 경로의 파일 내용만 읽어서 프롬프트에 주입.</li>
</ul>
<p>이 방식을 쓰니 AI가 엉뚱한 파일을 건드리는 할루시네이션이 확 줄었습니다.</p>
<h4 id="3-2-github-actions-워크플로우">3-2. GitHub Actions 워크플로우</h4>
<p>이슈에 <code>🤖 ai-fix</code> 라벨이 붙을 때만 동작하도록 설정했습니다.</p>
<pre><code>on:
  issues:
    types: [labeled]
jobs:
  solve-issue:
    if: github.event.label.name == &#39;🤖 ai-fix&#39;
    # ... (생략)</code></pre><h3 id="4-트러블-슈팅">4. 트러블 슈팅</h3>
<p>구현 과정은 순탄치 않았습니다. 특히 Monorepo 환경이 발목을 잡았습니다.</p>
<p><strong>문제 1: GitHub Actions Runner는 빈 환경이다</strong>
로컬에서는 잘 돌던 스크립트가 Actions에서는 <code>Cannot find module &#39;@google/generative-ai&#39;</code> 에러를 발생시켰습니다. 
👉 원인: Runner에는 내가 로컬에 설치한 패키지가 없기 때문.</p>
<p><strong>문제 2: Yarn Berry + Monorepo 환경의 제약</strong>
<code>npm install</code>을 하려니 프로젝트 루트의 <code>package.json</code>에 있는 <code>workspace:</code> 프로토콜 때문에 에러가 났고, <code>yarn install</code>을 하려니 시간이 너무 오래 걸리고 PnP 설정이 복잡했습니다.</p>
<p><strong>해결책: &quot;독립 실행 환경 구성&quot;</strong>
프로젝트의 의존성과 완전히 분리된 독립적인 환경을 만들어서 봇을 실행시켰습니다.</p>
<ol>
<li>bot-deps라는 임시 폴더 생성.</li>
<li>그 안에서 <code>npm init -y</code>로 깨끗한 환경 생성.</li>
<li>봇 구동에 필요한 라이브러리만 <code>npm install</code>.</li>
<li><code>NODE_PATH</code> 환경 변수로 스크립트가 임시 폴더의 모듈을 참조하게 설정.</li>
</ol>
<pre><code class="language-YAML"># 워크플로우 핵심 부분
run: |
  mkdir -p bot-deps
  cd bot-deps
  npm init -y
  npm install @google/generative-ai simple-git glob
  cd ..
  export NODE_PATH=$(pwd)/bot-deps/node_modules
  node .scripts/solve_issue.js</code></pre>
<p>이 방법으로 Monorepo 설정이나 패키지 매니저 종류와 상관없이 무조건 동작하는 환경을 구축했습니다.</p>
<p><strong>문제 3: 모델의 존재 유무 (404 &amp; 429)</strong>
처음에 gemini-2.5-pro-latest를 썼더니 404 에러가, gemini-2.5-pro를 호출했더니 429(요청 초과) 에러가 났습니다. 
👉 해결: 가장 안정적이고 속도가 빠른 <strong>gemini-2.5-flash</strong>로 모델을 변경하여 해결했습니다. 
코딩 능력도 준수하고 무료 사용량도 넉넉합니다.</p>
<h3 id="5-최종-결과">5. 최종 결과</h3>
<p>이제 이슈 템플릿에 기존에 문제를 겪었던 어드민 웹의 헤더 반응형을 만들어보겠습니다.</p>
<ol>
<li><p>⚠️ 이슈 템플릿에 따라서 이슈를 작성합니다.
<img src="https://velog.velcdn.com/images/do-hyun123/post/4d020f44-d202-4508-938b-0c71eb03f744/image.png" alt=""></p>
</li>
<li><p>🤖 Gemini가 이슈를 분석합니다.</p>
</li>
<li><p>📂 프로젝트를 뒤져서 Header.tsx와 스타일링 코드를 찾아냅니다.</p>
</li>
<li><p>📝 코드를 수정합니다.
<img src="https://velog.velcdn.com/images/do-hyun123/post/fecb4f70-d499-4daf-9c7c-a888fc4fd9f5/image.png" alt=""></p>
</li>
<li><p>🚀 ai-fix/issue-123 브랜치를 만들고 PR을 날립니다.</p>
</li>
<li><p>🏷️ PR에도 <code>🤖 ai-fix</code> 라벨을 예쁘게 붙여줍니다.
<img src="https://velog.velcdn.com/images/do-hyun123/post/960f2c66-6f30-4180-be31-47894944abd3/image.png" alt=""></p>
</li>
</ol>
<h3 id="6-마치며">6. 마치며</h3>
<p>이번 프로젝트를 통해 <strong>&quot;AI를 내 워크플로우에 어떻게 녹여낼 것인가&quot;</strong>에 대해 깊이 고민해볼 수 있었습니다. 특히 LLM을 단순 챗봇이 아니라 파일 시스템을 제어하고 Git을 다루는 에이전트로 활용했을 때의 생산성은 놀라웠습니다. <strong>(Claude sonnet을 사용하고 싶었지만.. 학생인지라 돈이 없었네요..)</strong></p>
<p>앞으로는 AI가 수정한 코드에 대해 자동으로 테스트 코드를 돌리고(Test Driven), 통과할 때만 PR을 생성하도록 고도화해볼 생각입니다.</p>
<p><a href="https://github.com/team-mozu/mozu-FE/pull/231">Merge한 AI PR</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MOZU] AWS 엔지니어링 도입기 - 타겟 선정과 모델링]]></title>
            <link>https://velog.io/@do-hyun123/MOZU-AWS-engineering-modeling</link>
            <guid>https://velog.io/@do-hyun123/MOZU-AWS-engineering-modeling</guid>
            <pubDate>Mon, 15 Dec 2025 08:10:42 GMT</pubDate>
            <description><![CDATA[<p>최근 AWS와 Microsoft가 클라우드 시스템의 결함을 잡기 위해 <strong>TLA+(Temporal Logic of Actions)</strong>라는 정형 명세 언어를 사용한다는 사실을 알게 되었습니다. 보통 우리는 기획서만 보고 바로 코드를 짜기 시작합니다. 그러다 보면 꼭 배포 후에야 알 수 없는 버그들(경쟁 상태, 데이터 불일치)을 마주하게 되죠.</p>
<p>현재 운영 중인 교육용 웹 MOZU에도 꽤 복잡한 주식 거래 로직이 들어갑니다. &quot;학생들이 사용하는 웹이라고 대충 만들지 말자. AWS처럼 설계해 보자.&quot; 라는 오기가 생겼습니다.</p>
<p>이 시리즈는 MOZU 프로젝트에 AWS식 엔지니어링(TLA+, 상태 머신, 정형 검증)을 도입하여 <strong>&#39;절대 고장 나지 않는 거래 시스템&#39;</strong>을 구축하는 과정의 기록입니다.</p>
<h3 id="1-타겟-선정-가장-위험한-놈을-찾아라">1. 타겟 선정: 가장 위험한 놈을 찾아라</h3>
<p>모든 코드를 TLA+로 검증할 순 없습니다. 프로젝트에서 가장 복잡하고, 버그가 터지면 치명적인 기능 하나를 골라야 했습니다.</p>
<p>MOZU의 <strong>&#39;주식 매수/매도 시스템&#39;</strong>은 다음과 같은 복잡한 요구사항을 가지고 있습니다.</p>
<ol>
<li>로컬 버퍼링: 학생이 매수/매도 버튼을 눌러도 서버에 즉시 전송되지 않고 로컬 스토리지에 쌓입니다(장바구니 개념).</li>
<li>가상 잔액: 아직 서버에 전송하지 않았지만, 로컬에서 추가한 주문만큼 잔액이 실시간으로 차감되어 보여야 합니다.</li>
<li>차수(Round) 제약: 1교시에 산 주식은 1교시 가격으로, 2교시엔 2교시 가격으로 처리되어야 하며, 같은 차수 내에서는 반대 매매(매수 후 매도)가 불가능합니다.</li>
<li>불변식(Invariant): 어떤 상황에서도 잔액은 마이너스가 될 수 없고, 보유하지 않은 주식을 팔 수 없습니다.</li>
</ol>
<p>만약 네트워크가 느리거나 사용자가 버튼을 광클했을 때, <strong>&#39;돈 복사 버그&#39;</strong>나 <strong>&#39;마이너스 통장&#39;</strong> 사태가 벌어진다면? 생각만 해도 끔찍합니다. 여기가 바로 TLA+가 필요한 지점입니다.</p>
<h3 id="2-모델링-aiclaude를-활용해-진입장벽-넘기">2. 모델링: AI(Claude)를 활용해 진입장벽 넘기</h3>
<p>사실 TLA+는 배우기가 다소 어렵습니다. 수학적 기호와 낯선 문법 때문에 시작조차 막막했죠. 그래서 저는 <strong>AI Agent(Claude Code)</strong>를 러닝 메이트로 활용하는 전략을 썼습니다.</p>
<p>맨땅에 헤딩하는 대신, 이미 작성되어 있던 MOZU의 프론트엔드 비즈니스 로직(React/TypeScript)을 Claude에게 분석시켰습니다.</p>
<blockquote>
<p><strong>🤖 Prompt</strong>: &quot;현재 MOZU 프로젝트의 BuySellModal.tsx와 useTradeStore.ts 코드를 분석해 줘. 이 로직을 검증할 수 있도록 TradeModel.tla 명세서를 작성해 줘.&quot;</p>
</blockquote>
<p>결과는 놀라웠습니다. Claude는 제 코드를 읽고 상태(State)와 전이(Transition)를 추출하여 TLA+의 기본 골격을 만들어주었습니다.</p>
<blockquote>
<p>TLA+를 처음부터 설계하신다면 기존 예시들을 참고하면 좋습니다.
tlaplus github: <a href="https://github.com/tlaplus/Examples?tab=readme-ov-file">https://github.com/tlaplus/Examples?tab=readme-ov-file</a>
List of TLA+ Examples: <a href="https://www.hillelwayne.com/post/list-of-tla-examples/">https://www.hillelwayne.com/post/list-of-tla-examples/</a></p>
</blockquote>
<p><strong>Claude가 분석하고 내가 다듬은 상태(Variables)</strong>
Claude는 UI 관련 코드는 다 쳐내고, 검증에 필요한 핵심 데이터만 정확히 뽑아냈습니다.</p>
<pre><code class="language-tla">VARIABLES
    cashMoney,        \* 현재 잔액
    holdings,         \* 보유 주식 수
    tradeOrders,      \* 로컬에 쌓인 주문 대기열 (장바구니)
    currentRound      \* 현재 차수</code></pre>
<p>AI가 초안을 잡아준 덕분에 저는 문법 공부에 시간을 쏟는 대신, &quot;논리가 맞는지&quot; 검토하고 Invariant(절대 깨지면 안 되는 규칙)를 정교하게 다듬는 데 집중할 수 있었습니다.</p>
<p><strong>행동(Action) 정의: 매수(Buy)</strong>
&quot;매수 버튼을 누른다&quot;는 행위를 TLA+ 수식으로 정의합니다. 여기서 중요한 건 CanBuy라는 전제 조건(Guard)입니다. 이 조건이 맞지 않으면 매수 행위 자체가 성립되지 않음을 수학적으로 못 박는 것입니다.</p>
<pre><code class="language-tla">\* 매수 가능 조건
CanBuy(stockId, quantity, price) ==
    /\ ~HasOppositeOrder(stockId, &quot;BUY&quot;)  \* 반대 주문(매도)이 없어야 함
    /\ (quantity * price) &lt;= cashMoney    \* 잔액이 충분해야 함
    /\ quantity &gt; 0

\* 매수 액션
Buy(stockId, quantity) ==
    LET cost == quantity * stockPrices[stockId]
    IN 
    /\ CanBuy(stockId, quantity, stockPrices[stockId]) \* 조건 체크
    /\ cashMoney&#39; = cashMoney - cost  \* 가상 잔액 즉시 차감
    /\ tradeOrders&#39; = Append(tradeOrders, newOrder) \* 대기열 추가
    /\ UNCHANGED &lt;&lt;holdings, currentRound&gt;&gt;</code></pre>
<h3 id="3-검증model-checking-모든-경우를-탐색하다">3. 검증(Model Checking): 모든 경우를 탐색하다</h3>
<p>이제 이 설계도가 완벽한지 확인할 차례입니다. TLA+의 모델 체커(TLC)는 발생 가능한 모든 경우의 수를 시뮬레이션합니다.</p>
<ul>
<li>주식 A를 샀다가 취소하고 다시 B를 샀을 때?</li>
<li>잔액이 딱 0원일 때 매도를 하면?</li>
<li>매수 주문을 100개 쌓아두고 전송하면?</li>
</ul>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/1627ba1f-a80b-482f-a204-bb2f5022efdd/image.png" alt=""></p>
<p>처음 모델을 돌렸을 때, NoNegativeBalance 위반 에러가 떴습니다. 확인해 보니, Buy 액션에서 잔액 체크를 하는 시점과 실제 차감하는 시점의 논리적 구멍이 있었습니다. 코드를 짜기 전에 설계 단계에서 치명적인 버그를 미리 잡아낸 것입니다.</p>
<p>수정 후, 최종적으로 모델 체킹이 통과되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/bc93fcde-e10a-4cf3-ba1b-b1e76b754be0/image.png" alt=""></p>
<p>이 메시지는 단순히 테스트 통과가 아닙니다. <strong>&quot;수학적으로 이 설계에서는 잔액이 마이너스가 되는 일이 불가능하다&quot;</strong>는 증명입니다.</p>
<h3 id="4-결론-및-다음-예고">4. 결론 및 다음 예고</h3>
<p>TLA+ 도입 과정에서 가장 큰 수확은 두 가지였습니다.</p>
<ol>
<li>AI 활용: Claude Code를 통해 기존 코드를 역으로 분석해 TLA+ 명세를 뽑아냄으로써 진입장벽을 획기적으로 낮췄습니다.</li>
<li>확신: 복잡한 로컬 버퍼링 로직을 수식으로 검증하고 나니, 구현에 대한 확신이 생겼습니다.</li>
</ol>
<p>이제 완벽한 설계도(Blueprint)가 준비되었습니다. 다음 단계는 이 설계를 실제 React + TypeScript 코드로 옮기는 일입니다.</p>
<p>TLA+가 정의한 &#39;상태 머신&#39;을 프론트엔드에서 어떻게 구현했을까요? 단순한 useState가 아닌, Zustand와 상태 패턴을 활용해 무결성 코드를 작성하는 과정을 다음 포스팅에서 다루겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS가 '절대 죽지 않는 시스템'을 만드는 법: TLA+, P]]></title>
            <link>https://velog.io/@do-hyun123/AWS-TLA-system-design</link>
            <guid>https://velog.io/@do-hyun123/AWS-TLA-system-design</guid>
            <pubDate>Mon, 15 Dec 2025 03:55:52 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/do-hyun123/post/cfffec56-2f72-4503-af22-4d3f138d13b1/image.png" alt=""></p>
<blockquote>
<p>이 포스트는 GeekNews의 <a href="https://news.hada.io/topic?id=21204">Amazon Web Services의 시스템 정확성 실천 사례</a>을 참고하여 게시한 글입니다.</p>
</blockquote>
<p>얼마전 TLA+에 대한 포스트를 게시하고 난 후 AWS의 시스템 정확성 설계에 대해 리서치하던 중, GeekNews에 흥미로운 글이 있어서 새로운 인사이트를 정리했습니다.</p>
<p>&quot;Amazon S3가 어떻게 수조 개의 객체에 대해 &#39;강력한 일관성(Strong Consistency)&#39;을 보장하게 되었을까요?&quot;</p>
<p>우리는 흔히 테스트 커버리지를 100% 채우면 안전하다고 착각합니다. 하지만 클라우드 규모의 분산 시스템에서는 테스트 케이스로 예측할 수 없는 <strong>&#39;비결정적 버그(Nondeterministic Bugs)&#39;</strong>들이 숨어 있습니다. 스레드 스케줄링이 꼬이거나, 네트워크 패킷 순서가 뒤바뀌는 순간 시스템은 멈춥니다.</p>
<p>AWS는 지난 15년 동안 이 문제를 해결하기 위해 <strong>수학적 증명(Formal Methods)</strong>을 현업에 가장 적극적으로 도입한 기업입니다. 오늘은 AWS가 TLA+의 한계를 넘어 P 언어, 결정적 시뮬레이션, 그리고 <strong>장애 주입(Fault Injection)</strong>까지 진화시켜 온 그들의 &#39;무결성 생태계&#39;를 파헤쳐 봅니다.</p>
<h3 id="1-tla의-한계와-p-언어의-등판">1. TLA+의 한계와 P 언어의 등판</h3>
<p>초기 AWS는 <strong>TLA+</strong>를 통해 시스템을 검증했습니다. 하지만 문제가 있었습니다. TLA+는 너무 수학적이라 일반 개발자들이 배우기엔 진입장벽이 높았죠. 개발자들은 수학 공식보다는 코드와 아키텍처 다이어그램에 익숙하니까요.</p>
<p>그래서 AWS는 P 언어를 도입했습니다.</p>
<p>왜 P인가? : &quot;개발자에게 친숙한 상태 기계(State Machine)&quot;
P 언어는 마이크로서비스(SOA) 구조에 익숙한 개발자들을 위해 &#39;상태 기계(State Machine)&#39; 모델링 방식을 제공합니다.</p>
<ul>
<li>TLA+: 수학적 논리 (Logic)</li>
<li>P 언어: 상태와 이벤트 (State &amp; Event)</li>
</ul>
<p>AWS의 핵심 서비스인 S3, EBS, DynamoDB 팀은 P를 사용하여 설계 초기 단계부터 버그를 잡아냈습니다. 특히 S3의 &#39;Read-After-Write&#39; 일관성 업데이트는 P로 프로토콜을 모델링하고 검증했기에 사고 없이 배포될 수 있었습니다.</p>
<p>최근에는 <strong>PObserve</strong>라는 도구까지 개발했습니다. 이는 실행 로그를 분석해 &quot;실제 코드가 설계 명세(P 모델)대로 동작했는지&quot; 사후 검증까지 해줍니다. 설계와 구현의 불일치를 막는 결정적 한 방입니다.</p>
<h3 id="2-무겁기만-한-건-싫다-경량-형식-기법의-확대">2. 무겁기만 한 건 싫다: 경량 형식 기법의 확대</h3>
<p>모든 코드에 수학적 증명을 적용할 순 없습니다. 그래서 AWS는 가성비 좋은 <strong>&#39;경량(Lightweight) 형식 기법&#39;</strong>을 적극 활용합니다.</p>
<ol>
<li><p>속성 기반 테스트 (Property-Based Testing)
&quot;1+1=2&quot;를 테스트하는 게 아니라, &quot;어떤 숫자를 더하든 결과는 숫자여야 한다&quot;는 속성을 테스트합니다. S3 ShardStore 팀은 이를 통해 예외 케이스를 무작위로 생성하고 실패 시나리오를 찾아냅니다.</p>
</li>
<li><p>결정적 시뮬레이션 (Deterministic Simulation)
분산 시스템 테스트가 어려운 이유는 &#39;재현 불가능한 랜덤 요소(스레드 타이밍 등)&#39; 때문입니다. AWS는 단일 스레드 시뮬레이터 안에서 시간을 통제하며 분산 시스템을 실행합니다. 즉, &quot;우연히 발생하는 버그&quot;를 &quot;언제든 재현 가능한 버그&quot;로 만듭니다. (참고: AWS는 이를 위해 shuttle, turmoil 같은 도구를 오픈소스로 공개하기도 했습니다.)</p>
</li>
<li><p>지속적 퍼징 (Continuous Fuzzing)
Aurora DB 팀은 SQL 쿼리를 무작위로 생성해 데이터베이스에 쏟아붓습니다. 이를 통해 파티셔닝 논리가 깨지지 않는지, 격리성(Isolation)이 유지되는지 검증합니다.</p>
</li>
</ol>
<h3 id="3-일부러-고장-내기-fis와-메타안정성">3. 일부러 고장 내기: FIS와 메타안정성</h3>
<p>시스템이 논리적으로 완벽해도, 현실 세계의 하드웨어는 고장 납니다. 여기서 <strong>FIS (Fault Injection Service)</strong>가 등장합니다.</p>
<p><strong>&quot;알고 맞는 매가 덜 아프다&quot;</strong>
AWS는 프라임 데이(Prime Day) 준비 기간에만 700번 넘게 일부러 서버를 망가뜨리고 API 오류를 주입합니다. 중요한 건, 그냥 망가뜨리는 게 아닙니다. <strong>&quot;형식 명세(Spec)와 대조&quot;</strong>합니다.</p>
<ul>
<li>명세: &quot;서버가 죽으면 3초 안에 백업 노드가 켜져야 함&quot;</li>
<li>실험: 서버를 죽여본다 -&gt; 3초 안에 켜지는지 확인한다.</li>
</ul>
<p><strong>메타안정성(Metastability)의 공포</strong>
가끔 시스템은 재시도(Retry) 폭풍으로 인해 &#39;영구적인 장애 상태&#39;에 빠집니다. 이를 메타안정성이라 부르는데, AWS는 P와 TLA+ 모델을 기반으로 시뮬레이션을 돌려 이런 최악의 좀비 상태가 발생할 확률까지 계산해 냅니다.</p>
<h3 id="4-증명이-필수인-곳-보안-security">4. 증명이 필수인 곳: 보안 (Security)</h3>
<p>타협할 수 없는 보안 경계(권한 관리, 가상화) 영역에서는 여전히 수학적 증명을 고집합니다.</p>
<ul>
<li>Cedar (권한 정책 언어): Dafny를 이용해 자동 증명.</li>
<li>Firecracker (가상화): Kani 같은 Rust 분석 도구로 메모리 안전성 증명.</li>
</ul>
<h3 id="마무리-ai가-열어줄-형식-검증의-미래">마무리: AI가 열어줄 형식 검증의 미래</h3>
<p>AWS는 지난 15년간 &quot;어떻게 하면 인간의 실수를 시스템적으로 막을까?&quot;를 고민해왔습니다. 하지만 여전히 TLA+나 P는 배우기 어렵습니다.</p>
<p>AWS는 이제 <strong>생성형 AI(LLM)</strong>를 바라보고 있습니다. 개발자가 자연어로 시스템을 설명하면, AI가 P 코드를 짜고 TLA+ 명세를 작성해 주는 시대. 그것이 AWS가 그리는 다음 15년의 청사진입니다.</p>
<h3 id="reference">Reference</h3>
<p><a href="https://cacm.acm.org/practice/systems-correctness-practices-at-amazon-web-services/">Systems Correctness Practices at Amazon Web Services</a>
<a href="https://news.hada.io/topic?id=21204">Amazon Web Services의 시스템 정확성 실천 사례</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[구현보다 설계가 먼저다: TLA+로 복잡한 시스템 설계하기]]></title>
            <link>https://velog.io/@do-hyun123/TLA-system-design</link>
            <guid>https://velog.io/@do-hyun123/TLA-system-design</guid>
            <pubDate>Sat, 13 Dec 2025 08:20:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스트는 워싱턴주 시애틀의 소프트웨어 엔지니어 Ben Congdon님의 <a href="https://benjamincongdon.me/blog/2025/12/12/The-Coming-Need-for-Formal-Specification/">The Coming Need for Formal Specification</a>를 읽던중 영감을 받아 작성한 글입니다</p>
</blockquote>
<p>열심히 코드를 짜고 배포한 뒤, 금요일 저녁에 알람이 울립니다. &quot;간헐적으로 데이터가 꼬입니다.&quot; 로그를 뒤져보지만 재현이 안 됩니다. 흔히 말하는 <strong>경쟁 상태(Race Condition)</strong>나 동시성(Concurrency) 이슈입니다.</p>
<p>테스트 코드를 짰는데도 왜 이런 일이 생길까요? 테스트 코드는 <strong>&#39;우리가 예상한 시나리오&#39;</strong>만 검증하기 때문입니다. 하지만 버그는 항상 우리의 상상력 밖, 그 미묘한 타이밍의 틈새에서 발생합니다.</p>
<p>오늘은 아마존(AWS)과 마이크로소프트가 클라우드 시스템의 심장을 설계할 때 사용하는 비밀 무기, <strong>TLA+</strong>에 대해 이야기해보려 합니다.</p>
<h3 id="1-tla가-뭔가요">1. TLA+가 뭔가요?</h3>
<p><strong>TLA+ (Temporal Logic of Actions)</strong>는 코드를 짜기 위한 언어가 아니라, <strong>시스템을 설계하고 검증하기 위한 정형 명세 언어(Formal Specification Language)</strong>입니다.</p>
<p>쉽게 비유하자면 <strong>&#39;초고정밀 설계도&#39;</strong>입니다. 우리가 건물을 지을 때 벽돌부터 쌓지 않습니다. 청사진을 먼저 그리고, 구조 역학적으로 무너지지 않을지 계산하죠. TLA+는 소프트웨어에서 그 역할을 합니다.</p>
<ul>
<li>코드(Code): &quot;어떻게(How)&quot; 동작할지 지시하는 것.</li>
<li>TLA+: &quot;무엇(What)&quot;이 되어야 하는지, 그리고 시스템이 <strong>&quot;절대 하지 말아야 할 것&quot;</strong>을 수학적으로 정의하는 것.</li>
</ul>
<h3 id="2-왜-tla를-써야-하나요-특히-로우레벨아키텍처-관심자라면">2. 왜 TLA+를 써야 하나요? (특히 로우레벨/아키텍처 관심자라면)</h3>
<p>단순한 웹페이지를 만들 때는 TLA+가 과할 수 있습니다. 하지만 분산 시스템, 멀티스레딩, 블록체인, OS 커널 같은 복잡한 시스템을 다룬다면 이야기가 다릅니다.</p>
<p>① 모든 경우의 수 탐색 (Model Checking)
사람은 머릿속으로 &quot;A 스레드가 이거 할 때 B 스레드가 저거 하면...&quot; 정도까지밖에 시뮬레이션을 못 합니다. TLA+의 <strong>Model Checker(TLC)</strong>는 시스템이 가질 수 있는 <strong>수십억 개의 모든 상태(State)</strong>를 전수 조사합니다. 이 과정에서 인간이 절대 발견 못 할 극단적인 에지 케이스(Edge case)를 찾아냅니다.</p>
<p>② &#39;구현&#39;이 아닌 &#39;설계&#39;의 버그를 잡는다
<a href="https://news.hada.io/topic?id=21204">AWS의 엔지니어들은 DynamoDB나 S3 같은 서비스를 만들 때 TLA+를 사용해, 코드를 한 줄도 짜기 전에 심각한 설계 결함을 찾아냈다고 보고했습니다.</a> 코드를 다 짜고 나서 구조를 뜯어고치는 것보다, 설계 단계에서 수학적으로 검증하는 것이 압도적으로 효율적입니다.</p>
<h3 id="3-어떻게-생겼나요">3. 어떻게 생겼나요?</h3>
<p>TLA+는 프로그래밍 언어라기보다 수학 수식에 가깝습니다. 핵심은 <strong>&quot;다음 상태(Next State)는 현재 상태와 어떤 관계가 있는가&quot;</strong>를 정의하는 것입니다.</p>
<p>예를 들어, 간단한 카운터 시스템을 TLA+로 표현하면 이런 느낌입니다. (개념적 코드)</p>
<pre><code>VARIABLE count
Init == count = 0  (* 초기 상태: 카운트는 0 *)
Next == count&#39; = count + 1  (* 다음 상태: 현재 카운트 + 1 *)</code></pre><p>여기서 중요한 건 <code>count&#39;</code> (프라임)입니다. 이는 <strong>&quot;다음 단계의 값&quot;</strong>을 의미합니다. 이렇게 시스템의 상태 전이를 정의해두면, 컴퓨터가 이 수식을 바탕으로 발생 가능한 모든 미래를 시뮬레이션합니다.</p>
<h3 id="4-프론트엔드-개발자에게도-의미가-있나요">4. 프론트엔드 개발자에게도 의미가 있나요?</h3>
<p>물론입니다. 최근 프론트엔드는 단순한 뷰어가 아닙니다.</p>
<p>복잡한 상태 관리: Redux나 XState 같은 상태 라이브러리는 사실 TLA+가 다루는 &#39;상태 머신(State Machine)&#39;의 축소판입니다.</p>
<p>비동기 처리: 사용자가 버튼을 광클할 때, 네트워크 요청이 꼬여서 데이터가 덮어씌워지는 문제 등은 TLA+적 사고방식으로 명쾌하게 해결될 수 있습니다.</p>
<p>무엇보다 시스템을 &#39;상태(State)&#39;와 &#39;전이(Transition)&#39;의 관점에서 바라보는 시각을 길러줍니다.</p>
<h3 id="5-최신-트렌드-ai와-정형-검증의-만남">5. 최신 트렌드: AI와 정형 검증의 만남</h3>
<p>최근에는 이 TLA+가 AI와 결합하여 새로운 국면을 맞이하고 있습니다.</p>
<ol>
<li>사람: 영어로 대략적인 설계를 쓴다.</li>
<li>AI: 이를 TLA+ 코드로 변환한다.</li>
<li>TLA+: 논리적 구멍이 없는지 수학적으로 검증한다.</li>
<li>AI/사람: 검증된 설계를 바탕으로 코드를 구현한다. (Rocq 등으로 중요 부품 증명)</li>
</ol>
<p>이 파이프라인은 그동안 &quot;너무 어려워서&quot; 학계의 전유물이었던 정형 검증(Formal Verification)을 실무 레벨로 끌어내리고 있습니다.</p>
<p>마무리: 로우레벨을 향한 첫걸음
여러분이 리눅스 커널이나 고성능 오픈소스에 기여하고 싶다면, 코드를 빨리 짜는 능력보다 <strong>&quot;전체 시스템의 정합성을 꿰뚫어 보는 눈&quot;</strong>이 중요합니다. TLA+는 그 눈을 뜨게 해주는 가장 강력한 도구입니다.</p>
<p>지금 당장 모든 프로젝트에 도입할 필요는 없습니다. 하지만 주말에 &quot;Learn TLA+&quot; 사이트를 한 번 둘러보는 건 어떨까요? 여러분의 엔지니어링 레벨을 한 단계 끌어올려 줄지도 모릅니다.</p>
<h3 id="reference">Reference</h3>
<p><a href="https://www.youtube.com/watch?v=p54W-XOIEF8">Leslie Lamport의 TLA+ Video Course</a>: TLA+ 창시자(튜링상 수상자)의 직강
<a href="https://www.hillelwayne.com/post/learntla/">Learn TLA+ (Hillel Wayne)</a>: 개발자 친화적인 입문 사이트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트 알고리즘 chunk 함수 구현]]></title>
            <link>https://velog.io/@do-hyun123/%ED%94%84%EB%A1%A0%ED%8A%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-chunk-%ED%95%A8%EC%88%98-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@do-hyun123/%ED%94%84%EB%A1%A0%ED%8A%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-chunk-%ED%95%A8%EC%88%98-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Thu, 13 Nov 2025 08:22:48 GMT</pubDate>
            <description><![CDATA[<h3 id="개요">개요</h3>
<p>Chunk 함수는 인자로 배열과 <code>size</code>를 받아서 배열 <code>size</code>만큼의 조각으로 나눈 후 새로운 배열을 return 하는 함수이다</p>
<p>예시:</p>
<pre><code class="language-ts">const arr = [1, 2, 3, 4, 5, 6, 7];

console.log(chunk(arr, 2));
// 👉 [[1, 2], [3, 4], [5, 6], [7]]

console.log(chunk(arr, 3));
// 👉 [[1, 2, 3], [4, 5, 6], [7]]

console.log(chunk(arr, 4));
// 👉 [[1, 2, 3, 4], [5, 6, 7]]</code></pre>
<h3 id="코드">코드</h3>
<pre><code class="language-ts">export function chunk&lt;T&gt;(arr: readonly T[], size: number): T[][] {
    if (!Number.isInteger(size) || size &lt; 1) {
        throw new Error(&#39;Size must be an integer greater than zero.&#39;);
    }

    const result: T[][] = [];

    for (let i = 0; i &lt; arr.length; i += size) {
        result.push(arr.slice(i, i + size));
    }
    return result;
}</code></pre>
<p>함수의 인자로 <code>arr</code>와 <code>size</code>를 받고 같이 받은 제네릭 타입 T의 2차원 배열을 리턴한다</p>
<h3 id="예외-처리">예외 처리</h3>
<p>함수의 라인1 에서 <code>size</code>가 정수가 아니거나 1 미만일때 Error를 리턴하여
<code>size</code>가 정수가 아니거나 문자열일때를 방지한다.</p>
<p>(<code>!Number.isInterger(size)</code>는 <code>size</code>가 정수가 아닐때 true를 리턴한다)</p>
<pre><code class="language-ts">Number.isInteger(10)      // true
Number.isInteger(10.5)    // false
Number.isInteger(&quot;10&quot;)    // false
Number.isInteger(NaN)     // false</code></pre>
<h3 id="구현체">구현체</h3>
<pre><code class="language-ts">const result: T[][] = [];

for (let i = 0; i &lt; arr.length; i += size) {
    result.push(arr.slice(i, i + size));
}

return result;</code></pre>
<p><code>const result: T[][] = [];</code>는 chunk함수가 실행되고 인자로 받은 <code>arr</code>가 쪼개진 후 chunk함수가 적용될 배열인 <code>result</code>를 선언한다.</p>
<p><code>for</code> 반복문은 전달받은 배열 <code>arr</code>의 길이가 <code>i</code>보다 클 동안 실행된다.<br>이때 <code>i</code>는 한 번에 나눌 조각의 크기(<code>size</code>)만큼 증가하며,<br><code>arr.slice(i, i + size)</code>로 잘라낸 배열 조각을 새로운 배열 <code>result</code>에 <code>push</code>한다.</p>
<p>이 과정을 반복하면 배열이 지정한 크기대로 나뉘어 <code>result</code>에 담기고,<br>마지막에 남은 요소들도 하나의 배열로 묶여 <code>result</code>에 추가된다.</p>
<p>모든 반복이 끝나면 <code>result</code>를 반환해 <code>chunk</code> 함수가 완성된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[모노레포에서 Vitest 세팅]]></title>
            <link>https://velog.io/@do-hyun123/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90%EC%84%9C-Vitest-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@do-hyun123/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90%EC%84%9C-Vitest-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Sat, 08 Nov 2025 13:10:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/do-hyun123/post/418ca76b-1f38-4bd3-b873-51a8271eec8f/image.jpg" alt=""></p>
<p>vitest 4.0.0 버전부턴 3.x 버전까지 지원하던 기존의 파일명을 <code>vitest.workspace</code>로 바꾸거나, <code>defineWorkspace</code>를 사용하여 모노레포의 패키지를 구분하는 기능을 지원 중단했다.</p>
<p>참고: <a href="https://github.com/vitest-dev/vitest/releases/tag/v4.0.0">vitest 4.0.0 릴리즈 노트</a></p>
<p>4.x 버전부턴 <code>test.project</code> 라는 키를 사용해서 배열로 구분한다.</p>
<pre><code class="language-ts">import { defineConfig } from &#39;vitest/config&#39;;

export default defineConfig({
    test: {
        projects: [&#39;packages/*&#39;],
    },
});</code></pre>
<p>참고: <a href="https://vitest.dev/guide/projects.html">https://vitest.dev/guide/projects.html</a></p>
<blockquote>
<p>Vitest는 내부에 구성 파일이 없더라도 <code>패키지의</code> 모든 폴더를 별도의 프로젝트로 취급합니다. glob 패턴이 파일과 일치하면 이름이 <code>vitest.config</code>/<code>vite.config</code>로 시작하는지 또는 <code>(vite|vitest).*.config.*</code> 패턴과 일치하는지 확인하여 Vitest 구성 파일인지 확인합니다. 예를 들어 다음 구성 파일은 유효합니다.</p>
</blockquote>
<p>추가로 패키지 내의 vitest 세팅은 vitest가 glob 패턴으로 파일을 확인해서 vitest 세팅 파일인지 확인한다.</p>
<pre><code>// ex: 아래 파일 모두 유효함
- `vitest.config.ts`
- `vite.config.js`
- `vitest.unit.config.ts`
- `vite.e2e.config.js`
- `vitest.config.unit.js`
- `vite.config.e2e.js`</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[강력한 AI기반 IDE - KIRO 사용 후기]]></title>
            <link>https://velog.io/@do-hyun123/%EA%B0%95%EB%A0%A5%ED%95%9C-AI%EA%B8%B0%EB%B0%98-IDE-KIRO-%EC%82%AC%EC%9A%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@do-hyun123/%EA%B0%95%EB%A0%A5%ED%95%9C-AI%EA%B8%B0%EB%B0%98-IDE-KIRO-%EC%82%AC%EC%9A%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 24 Aug 2025 07:38:26 GMT</pubDate>
            <description><![CDATA[<p>KIRO IDE를 사용하며 있었던 경험들을 공유하기 위해 작성되었습니다. 
학교 프로젝트의 인트로 페이지를 KIRO로 바이브 코딩한 흐름을 소개합니다.</p>
<h3 id="krio-ide">KRIO IDE?</h3>
<p><a href="https://kiro.dev/">KRIO</a>는 2025년 7월 15일 AWS에서 공개한 AI 기반 IDE 입니다.</p>
<p><strong>KIRO</strong>는 단순히 코드 작성 보조에 그치지 않고, 스펙 중심 개발(Spec-Driven Development) 방식을 활용해 프로젝트를 보다 체계적이고 효율적으로 진행합니다.</p>
<p>특히 저는 <code>계획 → 설계 → 구현</code> 단계로 이루어진 <strong>Spec-Driven Development</strong> 방식에 매력을 느꼈습니다.</p>
<h3 id="시작하기">시작하기</h3>
<p>우선 저는 <strong>KRIO</strong> Spec-Driven Development방식으로 기존 프로젝트를 소개하기 위한 인트로 웹앱 제작을 요청했습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/c1adb10d-e792-4b32-8429-09fcb9828956/image.png" alt=""></p>
<h3 id="계획">계획</h3>
<p><strong>KIRO</strong>는 이 요청을 정리하여 마치 요구사항 명세서와 같은 <code>requirements.md</code>를 생성했습니다.
문서에는 사용자 스토리와 수용 기준이 EARS 형식(Easy Approach to Requirements Syntax)으로 정리되어 있어 인상적이었습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/53e16edb-0134-4ae2-8429-c86ff51139e6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/0192662f-7bde-4fce-99f2-0df8ee7f2007/image.png" alt=""></p>
<h3 id="설계">설계</h3>
<p>추가적인 요구사항에도 깔끔하게 문서를 수정하는 모습입니다.
<code>requirements.md</code> 로 계획한 이후엔 웹앱의 전반적인 구조를 설계하는 문서인 <code>design.md</code> 를 생성했습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/137756d1-7d29-4079-a42f-02ef8fcee3f5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/a538c7d9-b542-4a23-86b3-c8131c986891/image.png" alt=""></p>
<p><code>design.md</code>에는 프로젝트의 전반적인 개요를 시작으로 아키텍처, 컴포넌트 인터페이스, 데이터 모델, 레이아웃 디자인 등이 매우 상세하게 설계되어 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/f4ab8a24-ca00-46d3-93be-071d0393d8ac/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/bcdb83b4-80d8-406f-b65e-65026a4848cf/image.png" alt=""></p>
<p>프로젝트를 구체적으로 설명할수록 설계 문서의 완성도가 점진적으로 향상되는 것을 느꼈습니다.</p>
<h3 id="구현-계획--구현-시작">구현 계획 &amp; 구현 시작</h3>
<p>이렇게 최종적으로 설계까지 완료하면 마지막으로 구현 계획을 <strong>태스크</strong>로 분배하여 <code>tasks.md</code> 를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/4c1e658c-a396-4458-8f6a-9987a928c3db/image.png" alt=""></p>
<p>모주는 총 17개의 태스크로 구성되었네요.</p>
<p>태스크의 <strong>Start task</strong> 버튼을 누르면 해당 태스크의 구현이 시작됩니다.
1번 태스크인 <strong>프로젝트 초기 설정 및 기본 구조 생성</strong>을 시작하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/b6785459-331b-4c59-b928-b0d45df52d16/image.png" alt=""></p>
<p>구현 중 에러가 발생하더라도 <strong>KIRO</strong> 가 문제를 판단해 해결합니다.
역시 <code>Claude Sonnet 4.0</code> 기반이라 그런지 에러를 잘 잡아주네요.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/4475863d-582c-4fda-9792-be9ddf2da6a7/image.png" alt=""></p>
<p>태스크가 완료되면 완료된 작업과 리스트를 정리하고, 다음 태스크 진행준비를 알립니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/ae270c11-a873-4b88-972c-4a46e3875f34/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/1e0e3087-548b-4fec-91e8-20d55ab834b0/image.png" alt=""></p>
<p>이와 같은 방식으로 17개의 태스크를 모두 진행하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/3b15d2a9-a84a-4efd-a3e0-2ee6a8d5294f/image.png" alt=""></p>
<p>17개의 태스크의 작업들과 약간의 수정들을 더해 대략 3시간동안 제작한 소개 페이지입니다.</p>
<h3 id="장단점">장단점</h3>
<p>사용해보며 느낀 장단점이 여러개가 있지만
커뮤니티에서의 다양한 의견과 토론기록들에 참고할만한 경험이 많아서 리뷰대신 링크를 첨부합니다.</p>
<p><a href="https://www.reddit.com/r/cursor/comments/1m0eusx/amazons_cursor_competitor_kiro_is_surprisingly/?tl=ko">r/CURSOR: 아마존의 Cursor 경쟁자 Kiro는 깜짝 놀랄 만큼 좋아요!!</a></p>
<h3 id="개인적-리뷰">개인적 리뷰</h3>
<p>2주간의 Free Trial을 사용해본 결과, KIRO 개발진이 커뮤니티의 의견을 빠르게 수렴하고 버그를 신속하게 해결하는 모습을 확인할 수 있었습니다. 이러한 점을 보아 KIRO는 앞으로도 꾸준히 발전하며 더욱 강력한 IDE로 자리매김할 것이라 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] zero-install 환경에서의 cannot be used as a JSX component. 해결방안 트러블 슈팅]]></title>
            <link>https://velog.io/@do-hyun123/TypeScript-zero-install-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-cannot-be-used-as-a-JSX-component.-%ED%95%B4%EA%B2%B0%EB%B0%A9%EC%95%88-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@do-hyun123/TypeScript-zero-install-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-cannot-be-used-as-a-JSX-component.-%ED%95%B4%EA%B2%B0%EB%B0%A9%EC%95%88-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Fri, 31 Jan 2025 10:56:53 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-상황">문제 상황</h3>
<p>팀 프로젝트를 모노레포로 관리하다가 발생한 오류인데, 모노레포를 사용하다 보면 한 번쯤 겪게 되는 타입 오류이다.</p>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/4bf811ea-b3c7-498b-99de-3f815899da00/image.png" alt=""></p>
<p><code>&#39;Wrapper&#39; cannot be used as a JSX component.
 Its type &#39;StyledComponent ...</code> </p>
<p>처음엔 타입과 관련된 오류라는 것은 확실하다고 생각했고, 다음과 같은 문제들을 예상했다. (이때까지만 해도 금방 해결될 줄 알았다.)</p>
<ol>
<li>워크스페이스마다 다른 TypeScript 버전 사용</li>
<li>워크스페이스마다 다른 Emotion 버전 사용</li>
<li><code>tsconfig.json</code> 작성 실수</li>
</ol>
<p>이 정도 문제일 것으로 예상하여 해결 방법을 정리해 보았다.</p>
<ul>
<li>문제 1 → 모든 워크스페이스의 TypeScript 버전 통합</li>
<li>문제 2 → 모든 워크스페이스의 Emotion 버전 통합</li>
<li>문제 3 → <code>tsconfig.json</code> 검토 후 수정</li>
</ul>
<p>바로 해결 방안들을 실행해 보았지만, 놀랍게도 오류는 그대로였다.</p>
<h3 id="원인-분석">원인 분석</h3>
<p>오류는 생각보다 쉽게 해결되지 않았고, 원인을 다시 분석해 보기로 했다.</p>
<p><code>cannot be used as a JSX component.</code></p>
<p>이 오류가 무엇을 의미하는지 검색해 보니, 나와 같은 문제를 겪고 해결한 분의 블로그를 발견했다.</p>
<p><a href="https://velog.io/@sweetpumpkin/ThemeProvider-cannot-be-JSX-element-%EC%97%90%EB%9F%AC">관련 블로그</a></p>
<p>이 블로그에는 다음과 같은 내용이 적혀 있었다.</p>
<blockquote>
<p>올바르지 않은 패키지 설치로 인해 React 17버전과 18버전의 타입 정의 파일(<code>d.ts</code>) 두 개가 충돌해서 발생한 오류였다. React 앱에는 해당 버전에 맞는 <code>d.ts</code> 파일이 하나만 존재해야 한다.</p>
</blockquote>
<p>이를 확인하기 위해 워크스페이스들의 <code>@types/react</code> 버전과 <code>./yarn/cache</code>의 <code>@types/react</code> 버전을 비교해 보았다.</p>
<pre><code>.
├── packages/
│   ├── package-a/        # 첫 번째 패키지
│   │   ├── src/          # 소스 코드
│   │   │   ├── index.ts
│   │   │   └── ...
│   │   ├── package.json  # @types/react: &quot;18.3.18&quot;
│   │   └── ...
│   ├── package-b/        # 두 번째 패키지
│   │   ├── src/          # 소스 코드
│   │   │   ├── index.ts
│   │   │   └── ...
│   │   ├── package.json  # @types/react: &quot;19.0.8&quot; &lt;&lt;&lt;&lt;&lt;&lt; 문제가 발생한 패키지
│   │   └── ...     
│   ├── package-c/        # 세 번째 패키지
│   │   ├── src/          # 소스 코드
│   │   │   ├── index.ts
│   │   │   └── ...
│   │   ├── package.json  # @types/react: &quot;18.3.18&quot;
│   │   └── ...
│   └── ...</code></pre><p><img src="https://velog.velcdn.com/images/do-hyun123/post/58ab37ac-1694-47fd-9d27-60a476d4eb9b/image.png" alt=""></p>
<p>문제의 원인은 워크스페이스 간 <code>@types/react</code> 버전 차이로 인해 발생한 것이었고, 문제의 패키지(<code>package-b</code>)의 <code>@types/react</code> 버전을 <code>18.3.18</code>로 맞추니 오류가 해결되었다.</p>
<h3 id="느낀-점">느낀 점</h3>
<p>이처럼 패키지 버전을 일관되게 유지해야 하는 프로젝트에서는, 라이브러리가 버전업될 때마다 관리가 골치 아파질 수 있음을 깨달았다. </p>
<p>이러한 상황을 방지하기 위해 워크스페이스의 버전을 자동으로 통합하는 방법이 있는지 찾아보았고, <strong>Peer Dependency를 활용하면 자식 워크스페이스의 버전이 상위 워크스페이스의 버전을 자동으로 따르도록 설정할 수 있다</strong>는 사실을 알게 되었다.</p>
<p>앞으로는 패키지를 무분별하게 설치하지 않고, 반드시 버전 통합을 고려한 후 설치해야겠다고 다짐했다.</p>
<p>참고:
<a href="https://jaddong.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EA%B0%9C%EB%B0%9C%ED%95%98%EB%A9%B4%EC%84%9C-%EC%95%8C%EA%B2%8C-%EB%90%9C-peerDependencies%EC%9D%98-%EC%97%AD%ED%95%A0">Peer Dependency 참고</a></p>
<p><a href="https://velog.io/@sweetpumpkin/ThemeProvider-cannot-be-JSX-element-%EC%97%90%EB%9F%AC">에러 디버깅 참고</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 자바스크립트: 인터프리터 vs 컴파일러]]></title>
            <link>https://velog.io/@do-hyun123/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-vs-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</link>
            <guid>https://velog.io/@do-hyun123/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-vs-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</guid>
            <pubDate>Sat, 28 Dec 2024 07:03:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 포스트는 JS 전공도서인 YDKJSY 시리즈의 1장을 읽고 정리한 내용이다.</p>
</blockquote>
<h2 id="자바스크립트는-인터프리터-언어일까-컴파일러-언어일까-깊이-있게-살펴보자">자바스크립트는 인터프리터 언어일까, 컴파일러 언어일까? 깊이 있게 살펴보자</h2>
<p>자바스크립트가 인터프리터 언어인지 컴파일러 언어인지에 대한 논쟁은 오랫동안 이어져 왔다. 이는 단순히 용어의 문제를 넘어, 언어의 특성과 실행 방식에 대한 깊이 있는 이해를 필요로 한다.</p>
<p><strong>왜 논쟁이 일어났을까?</strong></p>
<ul>
<li><strong>과거의 오해:</strong> 스크립트 언어는 컴파일 언어에 비해 성능이 낮고, 동적 타이핑으로 인해 안정성이 떨어진다는 인식이 강했다.</li>
<li><strong>배포 방식의 차이:</strong> 컴파일 언어는 바이너리 파일로 배포되는 반면, 자바스크립트는 소스 코드 자체를 배포한다.</li>
<li><strong>실행 방식의 다양성:</strong> 현대 프로그래밍 언어는 다양한 실행 방식을 지원하며, 단순히 인터프리터 또는 컴파일러로 분류하기 어려운 경우가 많다.</li>
</ul>
<p><strong>자바스크립트는 어떻게 실행될까?</strong></p>
<p>자바스크립트 엔진은 소스 코드를 실행하기 전에 몇 가지 단계를 거친다.</p>
<ol>
<li><strong>파싱:</strong> 소스 코드를 토큰으로 분해하고 구문 분석하여 추상 구문 트리(AST)를 생성한다.</li>
<li><strong>컴파일:</strong> AST를 실행 가능한 코드로 변환한다. 이 과정에서 최적화가 이루어질 수 있다.</li>
<li><strong>실행:</strong> 생성된 코드를 실행한다.</li>
</ol>
<p><strong>핵심은 바로 JIT(Just-In-Time) 컴파일이다.</strong> 코드가 실행되는 순간에 실시간으로 컴파일되고 최적화되는 방식이다.</p>
<p><strong>왜 자바스크립트를 인터프리터 언어라고 부를까?</strong></p>
<ul>
<li><strong>소스 코드를 직접 실행하는 것처럼 보인다.</strong></li>
<li><strong>동적 타이핑을 지원한다.</strong></li>
<li><strong>스크립트 언어의 특징을 많이 가지고 있다.</strong></li>
</ul>
<p><strong>왜 자바스크립트를 컴파일러 언어라고 부를 수 있을까?</strong></p>
<ul>
<li><strong>파싱과 컴파일 과정을 거친다.</strong></li>
<li><strong>JIT 컴파일을 통해 최적화된다.</strong></li>
<li><strong>일부 자바스크립트 엔진은 실행 전에 코드를 분석하여 오류를 사전에 감지한다.</strong></li>
</ul>
<p><strong>결론적으로 자바스크립트는 인터프리터와 컴파일러의 특징을 모두 가지고 있는 하이브리드 언어라고 할 수 있다.</strong></p>
<ul>
<li><strong>인터프리터적 특징:</strong> 소스 코드를 직접 실행하고, 동적 타이핑을 지원하며, 스크립트 언어의 특징을 가지고 있다.</li>
<li><strong>컴파일러적 특징:</strong> 파싱, 컴파일, 최적화 과정을 거치며, 정적 분석을 지원한다.</li>
</ul>
<p><strong>따라서 자바스크립트를 단순히 인터프리터 언어 또는 컴파일러 언어로 분류하기보다는, 실행 환경과 사용 목적에 따라 다르게 평가해야 한다.</strong></p>
<p><strong>핵심:</strong> 자바스크립트는 JIT 컴파일을 통해 높은 성능과 유연성을 동시에 제공하는 현대적인 프로그래밍 언어이다.</p>
<p><strong>요약:</strong> 자바스크립트는 인터프리터와 컴파일러의 장점을 모두 가지고 있는 하이브리드 언어이며, 실행 환경과 사용 목적에 따라 다르게 평가될 수 있다.</p>
<p><img src="https://i.imgur.com/Oyj683G.png" alt="">
(인터프리터, 스크립트 언어의 실행 절차)</p>
<p><img src="https://i.imgur.com/0tIkhyH.png" alt="">
(파싱 + 컴파일 + 실행)</p>
<h3 id="컴파일-단계에선-어떤-일이-일어날까">컴파일 단계에선 어떤 일이 일어날까?</h3>
<p>컴파일 단계에선 가상 머신(VM)에 전달할 이진 바이트 코드가 생성된다. 혹자는 가상 머신의 역할이 전달받은 바이트 코드를 해석하는 것이라고 이야기한다. 그런데 이런 관점에서는 자바를 비롯한 JVM 기반 언어는 컴파일이 아닌 인터프리터 언어라고 해석할수밖에 없다. 그럼 자바 등의 언어가 컴파일 언어라는 보편적인 주장에 모순이 생긴다.</p>
<p>자바와 JS는 완전히 다른 언어이지만 인터프리터와 컴파일 관점에서는 비슷한 점이 많다는 게 상당히 흥미로운 지점이다.</p>
<p>또 다른 흥미로운 점은 JS 엔진은 파싱 이후 생성된 코드를 다양한 방법으로 실행 전에 그때그떄 처리 및 최적화 한다는 점이다.
이런 JS 엔진 작동 방식으로 인해 관점에 따라 JS를 컴파일 언어 혹은 인터프리터 언어라고 할 수 있는 것이다. JS 엔진 안에서는 우리가 상상할 수 없을 정도로 복잡한 일들이 일어나고 있다.</p>
<p>지금까지 정리한 지식들을 모아 결론을 내려보아 JS로 만든 소스 코드가 실행될 때까지 어떤 절차를 거치는지 정리해보면,</p>
<ol>
<li>개발자의 손을 떠난 코드는 바벨이 트랜스파일한다. 이후 웹팩을 비롯한 번들러를 거쳐 번들링되고, 그 결과가 JS엔진에 전달된다.</li>
<li>JS 엔진은 전달받은 코드를 파싱해 추상 구문 트리로 바꿔준다.</li>
<li>이어서 인젠은 추상 구문 트리를 이진 바이트 코드로 바꾼다. 이 과정에서 JIT컴파일러가 작동하며 최적화가 함께 진행된다.</li>
<li>마지막으로 JS 가상 머신이 프로그램을 실행한다.</li>
</ol>
<p>네 단계를 그림으로 표현하면, </p>
<h3 id="js로-만든-소스-코드가-실행되는-과정">JS로 만든 소스 코드가 실행되는 과정</h3>
<p><img src="https://i.imgur.com/vdOpyE5.png" alt="">
(파싱, 컴파일 후 JS가 실행됨)</p>
<p>그렇다면 JS는 한 줄씩 실행되는 인터프리터 언어일까, 혹은 몇 단계를 거쳐 실행되는 컴파일 언어일까?</p>
<h3 id="필자의-의견">필자의 의견</h3>
<p>완전한 사실이 아니긴 하지만, YDKJSY 책의 필자는 JS가 컴파일 언어라고 생각한다고 언급했다.</p>
<blockquote>
<p>JS를 컴파일 언어라고 생각하는 게 중요한 이유는 갸벌저거 아성헌 문법을 입력하는 등의 실수를 하더라도 코드 실행 전에 정적 오류를 미리 발견할 수 있다는 특징이 있기 때문이다. 이런 JS만의 방식은 기존 스크립트 프로그램과 상당히 다른 방식으로 개발할 수 있도록 한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algorithm]브루트포스 알고리즘#2 (백준/#2231/분배합)]]></title>
            <link>https://velog.io/@do-hyun123/Algorithm%EB%B8%8C%EB%A3%A8%ED%8A%B8%ED%8F%AC%EC%8A%A4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%982-%EB%B0%B1%EC%A4%802231%EB%B6%84%EB%B0%B0%ED%95%A9</link>
            <guid>https://velog.io/@do-hyun123/Algorithm%EB%B8%8C%EB%A3%A8%ED%8A%B8%ED%8F%AC%EC%8A%A4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%982-%EB%B0%B1%EC%A4%802231%EB%B6%84%EB%B0%B0%ED%95%A9</guid>
            <pubDate>Fri, 27 Dec 2024 09:09:36 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<ul>
<li><strong>목표</strong>:<ul>
<li>주어진 자연수 <code>N</code>에 대해 <strong>가장 작은 생성자</strong>를 찾는 프로그램을 작성한다.</li>
</ul>
</li>
<li><strong>용어 정의</strong>:<ul>
<li><strong>분해합</strong>: 자연수 <code>M</code>과 <code>M</code>을 이루는 각 자리수의 합.즉, <code>분해합(M) = M + (M의 각 자리수의 합)</code>예) <code>245의 분해합 = 245 + 2 + 4 + 5 = 256</code></li>
<li><strong>생성자</strong>: 분해합이 <code>N</code>인 자연수 <code>M</code>.</li>
</ul>
</li>
<li><strong>문제의 특징</strong>:<ul>
<li>생성자가 없는 경우: 해당하는 <code>M</code>이 없다.</li>
<li>생성자가 여러 개인 경우: <strong>가장 작은 생성자</strong>를 찾아야 한다.</li>
</ul>
</li>
<li><strong>입력</strong>:<ul>
<li>자연수 <code>N</code> (1 ≤ N ≤ 1,000,000).</li>
</ul>
</li>
<li><strong>출력</strong>:<ul>
<li>자연수 <code>N</code>의 가장 작은 생성자.</li>
<li>생성자가 없으면 <code>0</code>을 출력.</li>
</ul>
</li>
<li><strong>제약</strong>:<ul>
<li><code>N</code>의 범위가 크므로, 가능한 생성자를 효율적으로 탐색해야 한다.</li>
</ul>
</li>
<li><strong>알고리즘</strong>:<ul>
<li>모든 자연수 <code>M</code>에 대해 분해합을 계산하는 것은 비효율적.</li>
<li>대신, 분해합을 통해 생성자 <code>M</code>의 범위를 좁혀서 탐색.</li>
</ul>
</li>
</ul>
<h3 id="해결한-코드">해결한 코드</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;string&gt;
#include &lt;numeric&gt;
using namespace std;

int main() {
  int n, solve = 1000001;
  cin &gt;&gt; n;
  for (int i = 0; i &lt; n; i++) {
    string numberStr = to_string(i);
    vector&lt;int&gt; v1;
    for (size_t i = 0; i &lt; numberStr.size(); i++) {
      int digit = numberStr[i] - &#39;0&#39;;
      v1.push_back(digit);
    }
    int sum = accumulate(v1.begin(), v1.end(), 0);
    if (sum + i == n &amp;&amp; sum + i &lt; solve) {
      solve = i;
      cout &lt;&lt; i;
    }
  }
  if(solve == 1000001) cout &lt;&lt; 0;
  return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/4fe98ec4-4795-44f6-ba34-bd9786df97a0/image.png" alt=""></p>
<h3 id="해결한-방법">해결한 방법</h3>
<p>주어진 자연수 <code>N</code>에 대해 <strong>가장 작은 생성자</strong>를 찾는 문제를 해결하기 위해 다음을 수행한다:</p>
<ol>
<li><p><strong>생성자 후보 탐색</strong>:</p>
<p> 모든 숫자 <code>i</code>를 <code>0</code>부터 <code>N-1</code>까지 순회하며, <code>i</code>가 <code>N</code>의 생성자인지 확인한다.</p>
</li>
<li><p><strong>분해합 계산</strong>:</p>
<p> 숫자 <code>i</code>를 이루는 각 자리수의 합(<code>자리수 합</code>)을 계산하고, 이를 <code>i</code>에 더해 <strong>분해합</strong>을 구한다.</p>
</li>
<li><p><strong>생성자 확인</strong>:</p>
<p> 계산한 분해합이 <code>N</code>과 같으면, 해당 숫자 <code>i</code>는 <code>N</code>의 생성자이다.</p>
<p> 생성자가 여러 개일 경우, 가장 작은 생성자를 기록한다.</p>
</li>
<li><p><strong>결과 출력</strong>:</p>
<p> 가장 작은 생성자를 출력하며, 생성자가 없을 경우 <code>0</code>을 출력한다.</p>
</li>
</ol>
<h3 id="개선할-부분">개선할 부분</h3>
<p>현재 코드는 모든 <code>i</code>에 대해 순회하므로 비효율적이다. 개선 방법은 다음과 같다:</p>
<ol>
<li><strong>탐색 범위 줄이기</strong>:<ul>
<li><code>i</code>의 최소값을 <code>N - (N의 최대 자리수 합)</code>로 설정.</li>
<li>예: <code>N = 256</code>일 때, 탐색 범위는 <code>256 - 54 = 202</code>부터 <code>256</code>까지.</li>
</ul>
</li>
<li><strong>벡터 제거</strong>:<ul>
<li>자리수 합은 벡터 대신, 반복문으로 바로 계산할 수 있다.</li>
</ul>
</li>
</ol>
<h3 id="개선된-코드">개선된 코드</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;
using namespace std;

int main() {
    int n;
    cin &gt;&gt; n;

    for (int i = max(1, n - 54); i &lt; n; i++) { // 탐색 범위 줄이기
        int sum = i, temp = i;
        while (temp &gt; 0) { // 자리수 합 계산
            sum += temp % 10;
            temp /= 10;
        }
        if (sum == n) {
            cout &lt;&lt; i;
            return 0;
        }
    }
    cout &lt;&lt; 0; // 생성자가 없는 경우
    return 0;
}</code></pre>
<p><a href="https://www.acmicpc.net/problem/2231">https://www.acmicpc.net/problem/2231</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] JS는 어떤 호환성을 보장하는가? 트랜스파일러 babel]]></title>
            <link>https://velog.io/@do-hyun123/JSJS%EB%8A%94-%EC%96%B4%EB%96%A4-%ED%98%B8%ED%99%98%EC%84%B1%EC%9D%84-%EB%B3%B4%EC%9E%A5%ED%95%98%EB%8A%94%EA%B0%80-%ED%8A%B8%EB%9E%9C%EC%8A%A4%ED%8C%8C%EC%9D%BC%EB%9F%AC-babel</link>
            <guid>https://velog.io/@do-hyun123/JSJS%EB%8A%94-%EC%96%B4%EB%96%A4-%ED%98%B8%ED%99%98%EC%84%B1%EC%9D%84-%EB%B3%B4%EC%9E%A5%ED%95%98%EB%8A%94%EA%B0%80-%ED%8A%B8%EB%9E%9C%EC%8A%A4%ED%8C%8C%EC%9D%BC%EB%9F%AC-babel</guid>
            <pubDate>Mon, 23 Dec 2024 08:31:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 포스트는 JS 전공도서인 YDKJSY 시리즈의 1장을 읽고 정리한 내용이다.</p>
</blockquote>
<h3 id="하위-호환성과-상위-호환성">하위 호환성과 상위 호환성</h3>
<p>JS를 지탱하는 기본 원칙 중 하나는 <strong>하위 호환성</strong>보장이다.
많은 개발자들은 이 <strong>하위 호환성</strong>과 <strong>상위 호환성</strong>이란 개념을 혼동한다.</p>
<h4 id="하위-호환성">하위 호환성</h4>
<p>하위 호환성이란, 단 한 번 이라도 유효한 JS문법이라고 인정되면 명세서가 변하더라도 절대 유효성이 깨지지 안는다는 의미이다. 예를 들어 하위 호환성 덕분에 1995년에 작성한 코드가 지금 실행해도 작동한다는걸 보장받는다.</p>
<h4 id="상위-호환성">상위 호환성</h4>
<p>상위 호환성이란, <strong>새로운 명세서에 추가된 문법으로 코드를 작성했을 때 이전 명세서를 준수하는 구형 JS엔진에서 문제가 발생하지 않아야 한다</strong>. 
많은 개발자들은 JS가 상위 호환성을 보장한다고 생각하지만 JS는 상위 호환성을 보장하지 않는다. 상위 호환성을 보장하는 기술의 예로는 HTML, CSS가 있다. 1995년에 작성된 HTML과 CSS를 어디선가 찾아냈다면 그 코드는 오늘날에 작동하지 않을 수 있다.</p>
<h3 id="트랜스파일러">트랜스파일러?</h3>
<p>JS엔 트랜스파일이라는 개념이 있다.
트랜스파일이란, JS 커뮤니티에서 만든 용어로, <code>한 형태에서 다른 형태로 소스코드를 변환해주는 것</code>을 의미한다.
이때, 변환 후 산출물은 소스코드이다. <strong>상위 호환성</strong>으로 인해 발생하는 문제 대부분은 트랜스파일러를 사용하면 해결된다. 트랜스파일러는 새로운 JS 문법을 오래된 문법으로 바꿔주며 주로 <strong>babel</strong>을 사용한다.</p>
<p>트랜스파일러가 어떻게 작동하는지의 대한 예시이다.
아래는 <strong>ES6 (ECMAScript 2015)</strong> 로, 비교적 최근 추가된 문법이다</p>
<pre><code class="language-js">if (something) {
    let x = 3;
    console.log(x);
}
else {
    let x = 4;
    console.log(x);
}</code></pre>
<p>다음은 앞선 코드를 babel이 트랜스파일한 결과이다. 프로덕션 환경에 배포할 때는 이처럼 트랜스파일러를 거친 산출물(js 파일)을 배포한다.</p>
<pre><code class="language-js">var x$0, x$1;
if(something) {
    x$0 = 3;
    console.log(x$0);
}
else {
    x$1 = 4;
    console.log(x$1);
}</code></pre>
<p>이렇게 babel을 사용해 트랜스파일한 아래 코드의 작동 결과는 원래 코드와 동일하다.</p>
<blockquote>
<p>let 키워드는 ES6에서 추가된 문법이므로 ES6 이전 문법을 지원해야 하는 환경에선 앞선 예시처럼 트랜스파일이 꼭 필요하다.</p>
</blockquote>
<h3 id="why">why?</h3>
<p>어차피 var로 트랜스파일 하는데 굳이 let을 쓸 필요가 있는가? 
최신 JS를 쓰는게 좋다고 강하게 권유하는 이유는 코드가 깔씀해지고 의도하는 바를 효과적으로 전달할 수 있기 때문이다.</p>
<p>개발자는 새로운 문법을 활용해 <strong>클린한 코드를 짜는데 집중해야한다</strong>. 상위 호환성과 관련된 문제는 <strong>babel과 같은 도구에 맡겨</strong> 오래된 JS환경에서도 별도 처리 없이 정상적으로 코드가 실행되도록 만들면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algorithm]브루트포스 알고리즘 (백준/#2798/블랙잭)]]></title>
            <link>https://velog.io/@do-hyun123/Algorithm%EB%B8%8C%EB%A3%A8%ED%8A%B8%ED%8F%AC%EC%8A%A4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B0%B1%EC%A4%802798%EB%B8%94%EB%9E%99%EC%9E%AD</link>
            <guid>https://velog.io/@do-hyun123/Algorithm%EB%B8%8C%EB%A3%A8%ED%8A%B8%ED%8F%AC%EC%8A%A4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B0%B1%EC%A4%802798%EB%B8%94%EB%9E%99%EC%9E%AD</guid>
            <pubDate>Mon, 23 Dec 2024 06:54:00 GMT</pubDate>
            <description><![CDATA[<h3 id="브루트포스-알고리즘-모든-가능성을-탐색하는-직관적인-방법">브루트포스 알고리즘: 모든 가능성을 탐색하는 직관적인 방법</h3>
<p><strong>브루트포스(Brute Force)</strong> 알고리즘은 모든 가능한 경우의 수를 일일이 확인하며 문제의 해답을 찾는 가장 직관적인 문제 해결 방식이다. 마치 바늘을 찾기 위해 건초더미를 하나씩 뒤지는 것과 같다고 할 수 있다.</p>
<h4 id="왜-브루트포스를-사용할까">왜 브루트포스를 사용할까?</h4>
<ul>
<li>단순하고 직관적이라 알고리즘 설계 및 구현이 쉽다.</li>
<li>모든 경우를 탐색하기 때문에 정확한 해답을 찾을 수 있다.</li>
<li>경우의 수가 적은 문제에서는 빠르게 해답을 찾을 수 있다.</li>
</ul>
<p>하지만 브루트포스의 한계도 명확하다.</p>
<ul>
<li>경우의 수가 많아질수록 해결 시간이 기하급수적으로 증가한다.</li>
<li>모든 경우의 수를 저장해야 하므로 많은 메모리를 필요로 한다.</li>
<li>대규모 문제에는 현실적으로 사용하기 어렵다.</li>
</ul>
<p>브루트포스 알고리즘은 크게 <strong>선형 구조</strong>와 <strong>비선형 구조</strong>로 나눌 수 있다.</p>
<ul>
<li><strong>선형 구조:</strong> 배열, 리스트 등 순차적인 자료 구조를 이용하여 문제를 해결한다. 예를 들어, 정렬되지 않은 배열에서 특정 값을 찾는 순차 탐색이 있다.</li>
<li><strong>비선형 구조:</strong> 트리, 그래프 등 비선형적인 자료 구조를 이용하여 문제를 해결한다. 예를 들어, 모든 가능한 경우의 수를 탐색하는 백트래킹, 깊이 우선 탐색(DFS), 너비 우선 탐색(BFS) 등이 있다.</li>
</ul>
<p>브루트포스 알고리즘을 사용하기 위해서는 문제의 해답이 명확하게 정의되어 있어야 하고, 모든 가능한 해답의 후보군이 유한해야 한다. 일반적으로 문제를 해결하기 위한 모든 가능한 경우의 수를 생성하고, 각 경우의 수가 문제의 조건을 만족하는지 확인한 후, 조건을 만족하는 경우의 수를 해답으로 저장하는 과정을 거친다.</p>
<p><strong>결론적으로</strong> 브루트포스 알고리즘은 문제 해결의 기본적인 접근 방식이지만, 모든 문제에 효과적인 것은 아니다. 문제의 규모와 특성에 따라 더 효율적인 알고리즘을 선택해야 한다. 브루트포스 알고리즘은 다른 알고리즘을 설계하고 이해하는 기반이 되며, 복잡한 문제를 해결하기 위한 첫걸음이 될 수 있다.</p>
<p><strong>예시:</strong></p>
<ul>
<li>정렬되지 않은 배열에서 특정 값 찾기: 배열의 모든 요소를 순차적으로 비교하여 값을 찾는다.</li>
<li>N-Queen 문제: N×N 크기의 체스판에 N개의 퀸을 서로 공격할 수 없도록 배치하는 모든 경우의 수를 찾는다.</li>
<li>암호 해독: 모든 가능한 암호 조합을 시도하여 원래의 문자열을 복원한다.</li>
</ul>
<p><strong>주의:</strong></p>
<ul>
<li>브루트포스 알고리즘은 시간과 메모리 측면에서 비효율적일 수 있으므로, 문제의 규모가 클 경우에는 다른 알고리즘을 고려해야 한다.</li>
<li>브루트포스 알고리즘은 다른 알고리즘의 기반이 되므로, 알고리즘 설계의 첫 단계로 사용될 수 있다.</li>
</ul>
<h3 id="실전문제-백준2798블랙잭">실전문제 (백준/#2798/블랙잭)</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;
using namespace std;

int main() {
    int n, m = 0;
    int sum = 0;
    int result = 0;
    cin &gt;&gt; n &gt;&gt; m;
    int arr[n];
    for(int i = 0; i &lt; n; i++) {
        cin &gt;&gt; arr[i];
    }

    for(int i = 0; i &lt; n - 2; i++) {
        for(int j = i + 1; j &lt; n - 1; j++) {
            for(int k = j + 1; k &lt; n; k++) {
                sum = arr[i] + arr[j] + arr[k];
                if(sum &gt; result &amp;&amp; sum &lt;= m) {
                    result = sum;
                }
            }
        }
    }

    cout &lt;&lt; result;

    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/do-hyun123/post/1f210df9-4542-43f3-90ea-923cde6d453f/image.png" alt=""></p>
<h3 id="문제풀이">문제풀이</h3>
<p><strong>문제 분석</strong></p>
<ul>
<li>목표: 주어진 카드 중 3장을 선택하여 합이 M을 넘지 않으면서 M에 가장 가까운 값을 찾는다.</li>
<li>입력: 카드의 개수 N, 목표값 M, 각 카드의 숫자</li>
<li>출력: 선택한 3장의 카드 합</li>
</ul>
<p><strong>해결 전략 (브루트포스)</strong></p>
<ul>
<li>모든 경우의 수 탐색: 세 개의 카드를 선택하는 모든 경우의 수를 탐색한다. (3중 for문)</li>
<li>합 계산: 선택한 세 개의 카드의 합을 계산한다.</li>
<li>조건 확인: 합이 M을 넘지 않으면서, 이전까지 계산한 합 중 가장 M에 가까운 값인지 확인한다.</li>
</ul>
<p><a href="https://www.acmicpc.net/problem/2798">https://www.acmicpc.net/problem/2798</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 많은 사람들이 모르는 javascript의 엔진, 런타임, 호출스택 #1]]></title>
            <link>https://velog.io/@do-hyun123/JS-%EB%A7%8E%EC%9D%80-%EC%82%AC%EB%9E%8C%EB%93%A4%EC%9D%B4-%EB%AA%A8%EB%A5%B4%EB%8A%94-javascript%EC%9D%98-%EC%97%94%EC%A7%84-%EB%9F%B0%ED%83%80%EC%9E%84-%ED%98%B8%EC%B6%9C%EC%8A%A4%ED%83%9D-1</link>
            <guid>https://velog.io/@do-hyun123/JS-%EB%A7%8E%EC%9D%80-%EC%82%AC%EB%9E%8C%EB%93%A4%EC%9D%B4-%EB%AA%A8%EB%A5%B4%EB%8A%94-javascript%EC%9D%98-%EC%97%94%EC%A7%84-%EB%9F%B0%ED%83%80%EC%9E%84-%ED%98%B8%EC%B6%9C%EC%8A%A4%ED%83%9D-1</guid>
            <pubDate>Sat, 16 Nov 2024 06:49:51 GMT</pubDate>
            <description><![CDATA[<hr>
<h3 id="의도">의도</h3>
<p>JavaScript를 사용한 적이 없는 웹 개발자는 없을 것이다.
그만큼 JavaScript는 웹 개발을 하기 위해 필수적인 언어이고, 이젠 웹 개발을 넘어 백엔드, 네이티브, 심지어 임베디드 장치 등 다양한 분야에서 JavaScript를 사용한다.</p>
<p>JavaScript의 내부 작동 방식을 탐구해보고, 구성요소들이 어떻게 작동하는지 정리했다.</p>
<p>JavaScript에 크게 의존하는 프로젝트일 수록, 개발자는 더욱더 좋은 소프트웨어를 만들기 위해 내부 구조를 더욱더 깊이 이해하고, 언어가 제공하는 모든 것을 최대한 사용해야 한다고 생각한다. </p>
<p>실제로 매일 JavaScript를 사용하는 사람 중에서도, 내부에서 어떤 일이 일어나는지 모르는 사람들이 많다.</p>
<h3 id="javascript-엔진">JavaScript 엔진</h3>
<p>V8엔진은 익히 들어봤을 것이다. V8엔진은 현재 Chromium기반 인터넷 사용자의 약 75% 정도가 사용한다. (Chrome, Node.js에서 사용됨)</p>
<p><img src="https://i.imgur.com/rP2uD3P.png" alt=""></p>
<p>JavaScript의 엔진은 이렇게 두 가지의 구성요소로 구성된다.</p>
<ul>
<li>Memory Heap: 메모리 할당이 발생하는 위치</li>
<li>Call Stack: 코드가 실행될 때 스택 프레임이 있는 위치</li>
</ul>
<h3 id="런타임">런타임</h3>
<p>브라우저엔 거의 모든 JavaScript 개발자가 사용한 API가 있다. ex) <code>setTimeout</code>
그러나 이런 API들은 엔진에서 제공하지 않는다.</p>
<p>좀 더 복잡한 형태로 제공되는데,</p>
<p><img src="https://i.imgur.com/DN52Oun.png" alt=""></p>
<p>물론 JavaScript는 엔진으로 동작하지만, 실제로는 훨씬 더 많은 것들이 있다.
브라우저에서 제공하는 웹 API라는 것들을 가지고 있는데, DOM, AJAX, setTimeout등이 있다.</p>
<p>그리고, 이벤트 루프와, 콜백 큐가 있다.</p>
<h3 id="call-stack호출-스택">Call Stack(호출 스택)</h3>
<p>JavaScript는 단일 스레드 프로그래밍 언어이다. 즉, 단일 호출 스택이 있다. 따라서 <strong>한 번에 한 가지 작업을 수행할 수 있다.</strong> </p>
<p><strong>호출 스택은, 프로그램이 어느 위치에 있는지 기록하는 데이터 구조이다.</strong> 함수에 들어가면 함수를 스택 맨 위에 푸쉬한다. 함수를 리턴하면 스택 맨 위에서 팝한다. 스택의 역할은 그게 전부이다.</p>
<p>다음 코드를 보면,</p>
<pre><code class="language-js">function multiply(x, y) {  
    return x * y;  
}

function printSquare(x) {  
    var s = multiply(x, x);  
    console.log(s);  
}

printSquare(5);</code></pre>
<p>엔진이 이 코드를 실행하기 시작하면, 호출 스택이 비어 있을 것이다. 그 후 단계는 다음과 같다.</p>
<p><img src="https://i.imgur.com/60BZupV.png" alt=""></p>
<p>호출 스택의 각 항목을 <strong>스택 프레임</strong>이라고 한다. </p>
<p>그리고 아래는 예외가 throw되었을 때 스택 추적이 구성되는 방식이다.</p>
<pre><code class="language-js">function foo() {  
    throw new Error(&#39;SessionStack will help you resolve crashes :)&#39;);  
}

function bar() {  
    foo();  
}

function start() {  
    bar();  
}

start();</code></pre>
<p>Chrome에서 이 코드를 실행하면 다음과 같은 스택 추적이 생성된다.</p>
<p><img src="https://i.imgur.com/1Vo71xE.png" alt=""></p>
<p>&quot;Blowing the stack&quot;이라고 불리는 <strong>스택 날리기</strong>는 최대 호출 스택 크기에 도달했을 때 발생한다.
스택 날리기는 매우 흔하게 발생하며, 특히 코드를 매우 광범위하게 테스트하지 않거나 재귀를 사용하는 경우 더욱 자주 발생한다.</p>
<pre><code class="language-js">function foo() {  
    foo();  
}

foo();</code></pre>
<p>엔진이 이 코드를 실행하면, <code>foo</code>함수를 호출하는 것으로 시작한다. 하지만 이 함수는 재귀적이며, return 없이 자체 호출을 시작한다. 따라서 실행의 모든 단계에서 함수가 호출 스택에 계속해서 추가된다.</p>
<p><img src="https://i.imgur.com/eOr7Inz.png" alt=""></p>
<p>계속 추가되고 어느 시점에서 호출 스택의 함수 호출 수가 호출 스택의 크기를 초과하는 <code>Stack Overflow</code>가 발생하고 브라우저는 다음과 같은 오류를 발생시킨다.</p>
<p><img src="https://i.imgur.com/9R1KhZe.png" alt=""></p>
<p>단일 스레드에서 코드를 실행하는 것은 매우 쉽다. 다중 스레드에서 발생하는 복잡한 시나리오(ex: 교착상태)를 처리할 필요가 없기 때문이다.</p>
<p>하지만 단일 스레드로 실행되는 것은 꽤나 제약이 따른다. JavaScript가 단일 호출 스택(Call Stack)을 가지고 있기 때문에 <strong>작업이 느려지면 어떻게 될까?</strong></p>
<h3 id="동시성-및-이벤트-루프">동시성 및 이벤트 루프</h3>
<p>처리에 엄청난 시간이 걸리는 함수 호출이 Call Stack에 있는 경우엔 어떡하지?
예를 들어, 브라우저에서 JavaScript로 복잡한 이미지 변환을 하고 싶다고 가정해보자,</p>
<p>이게 왜 문제인지 궁금할 수 있지만, 문제는 Call Stack에 실행할 함수가 있는 반면, 브라우저는 실제로 다른 일을 할 수 없다는 것이다. 
즉, 브라우저가 렌더링 할 수 없고, 다른 코드를 실행할 수 없으며, 그냥 멈춘 것이다. 앱에서 멋지고 유동적인 UI를 원한다면 큰 문제가 될 것이다.</p>
<p>그게 유일한 문제는 아니다. 브라우저가 호출 스택에서 많은 작업을 처리하기 시작하면 꽤 오랫동안 응답하지 않을 수 있다. 브라우저가 호출 스택에서 많은 작업을 처리하기 시작하면 꽤 오랫동안 응답하지 않을 수 있다. 그리고 대부분의 브라우저는 오류를 발생시키고, 웹 페이지를 종류할지 묻는다. </p>
<p><img src="https://i.imgur.com/z9bNTjk.png" alt=""></p>
<p>그렇다면 브라우저가 죽지 않게 하고 무거운 코드를 어떻게 실행할 수 있을까?
해결책은, <strong>비동기 콜백</strong>이다.</p>
<p>포스트의 시리즈를 나누기 위해
이에 대해선 다음 포스트로 설명하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] js를 쓴다면 꼭 알아야 할 원시타입과 참조타입]]></title>
            <link>https://velog.io/@do-hyun123/JS-js%EB%A5%BC-%EC%93%B4%EB%8B%A4%EB%A9%B4-%EA%BC%AD-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EC%9B%90%EC%8B%9C%ED%83%80%EC%9E%85%EA%B3%BC-%EC%B0%B8%EC%A1%B0%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@do-hyun123/JS-js%EB%A5%BC-%EC%93%B4%EB%8B%A4%EB%A9%B4-%EA%BC%AD-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EC%9B%90%EC%8B%9C%ED%83%80%EC%9E%85%EA%B3%BC-%EC%B0%B8%EC%A1%B0%ED%83%80%EC%9E%85</guid>
            <pubDate>Thu, 14 Nov 2024 08:20:14 GMT</pubDate>
            <description><![CDATA[<h3 id="원시타입">원시타입</h3>
<p>자바스크립트엔 크게 두 가지 타입으로 갈리는데, 원시타입과 객체타입(참조타입)으로 갈린다.
원시타입엔 string, number, bigint, boolean, undefined, symbol, null이 있고
원시타입은 변수를 선언하고 데이터 복사가 일어날 때 데이터를 직접 가르킨다.</p>
<p><img src="https://i.imgur.com/fcWlMNL.png" alt=""></p>
<p>때문에 우리가 변수를 재할당할 때 변수의 데이터가 바뀌는 것 처럼 보일 수 있지만 사실은 새로운 메모리 영역이 생성되고 가리키던 메모리가 바뀌는 것이다.</p>
<p>예를 들면, <code>let a = 1</code>을 선언하면 메모리에 생성된 1의 주소를 가리킨다.
그리고 <code>a = 10</code>으로 재할당을 하면 1이였던 값이 10으로 바뀌는 것이 아닌, 메모리에 10이 생성되고 a는 새롭게 10의 주소를 가리키는 것이다.</p>
<p><img src="https://i.imgur.com/pO8FF9l.png" alt=""></p>
<p>이를 고려하여 </p>
<pre><code class="language-js">let a = 100;
let b = a;
a = 50;</code></pre>
<p>이 코드를 보면, 
새로운 변수 b에 a를 통째로 복사해서 넣었기 때문에 나중에 a가 재할당 되더라도 b의 값은 바뀌지 않는다.</p>
<p><img src="https://i.imgur.com/v4ViePq.png" alt=""></p>
<h3 id="참조타입">참조타입</h3>
<p>반면에 참조타입엔 객체(object), 배열(array), 함수(function)등이 있는데 
원시타입과의 차이점은 변수의 크기가 동적으로 변하기 때문에 참조타입드의 데이터는 별도의 메모리공간(heap)에 저장되며, 변수 할당 시 데이터에 대한 주소(heap 메모리의 주소값)가 저장되기 때문에 자바스크립트 엔진이 
변수가 가지고 있는 메모리 주소를 이용해서 변수의 값에 접근한다.</p>
<p><img src="https://i.imgur.com/9D8SX4C.png" alt=""></p>
<p>위와같이 메모리엔 힙 메모리의 주소가 저장되어 있다.</p>
<pre><code class="language-js">let myArr = [];
let copyArr = myArr;

myArr.push(&quot;hello&quot;);

console.log(copyArr); // [&quot;hello&quot;]</code></pre>
<p>때문에 이렇게 원시타입으로 할당 했으면 재할당이 되겠지만 배열과 같은 참조타입으로 재할당 하면 myArr가 가리키던 힙 메모리의 주소를 그대로 참조하는 것이다.</p>
<p><img src="https://i.imgur.com/koKXr3c.png" alt=""></p>
<p>이렇게 데이터를 참조하기 때문에 참조타입이라고 불린다. 만약 이러한 참조타입의 특성을 고려하지 않으면 데이터 플로우가 예상하지 못한 방향으로 흘러갈 수 있기 때문에 주의해야 한다.</p>
]]></description>
        </item>
    </channel>
</rss>