<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>chaesunbak 님의 블로그</title>
    <link>https://chaesunbak.tistory.com/</link>
    <description>어떻게 하면 더 편하게 살 수 있을까</description>
    <language>ko</language>
    <pubDate>Sun, 31 May 2026 00:00:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>chaesunbak</managingEditor>
    <image>
      <title>chaesunbak 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/7682816/attach/ece2244cd9c542a3be2a42c820570684</url>
      <link>https://chaesunbak.tistory.com</link>
    </image>
    <item>
      <title>블로그 이사합니다.</title>
      <link>https://chaesunbak.tistory.com/pages/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%82%AC%ED%95%A9%EB%8B%88%EB%8B%A4-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.chaesunbak.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.chaesunbak.com/&lt;/a&gt;&lt;/p&gt;</description>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/pages/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%82%AC%ED%95%A9%EB%8B%88%EB%8B%A4-1</guid>
      <pubDate>Tue, 28 Apr 2026 22:13:27 +0900</pubDate>
    </item>
    <item>
      <title>블로그 이사합니다.</title>
      <link>https://chaesunbak.tistory.com/notice/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.chaesunbak.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.chaesunbak.com/&lt;/a&gt;&lt;/p&gt;</description>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/notice/46</guid>
      <pubDate>Tue, 28 Apr 2026 22:12:50 +0900</pubDate>
    </item>
    <item>
      <title>딸깍으로 내 서비스 UX 라이팅 개선하기</title>
      <link>https://chaesunbak.tistory.com/45</link>
      <description>&lt;h2&gt;배경&lt;/h2&gt;
&lt;p&gt;현재 운영중인 학점은행제 학점계산기 서비스에는비로그인 상태에서도 플랜을 쉽게 공유하고 저장할 있도록 플랜을 URL 저장하는 기능이 있다.&lt;/p&gt;
&lt;p&gt;연결해둔 채널톡으로 해당 기능과 관련한 CS가 접수되었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Q : 저장했는데 북마크는 어디있나요?
A : 현재 주소를 브라우저의 북마크에 추가하시면 플랜을 다시 확인하실 수 있습니다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/moZ4w/dJMb99MPala/6dUWN8rKyb3YUAaKnwEdTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/moZ4w/dJMb99MPala/6dUWN8rKyb3YUAaKnwEdTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/moZ4w/dJMb99MPala/6dUWN8rKyb3YUAaKnwEdTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmoZ4w%2FdJMb99MPala%2F6dUWN8rKyb3YUAaKnwEdTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1162&quot; height=&quot;552&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개선하기&lt;/h2&gt;
&lt;p&gt;해당 기능을 개선할 필요를 느꼈다.&lt;/p&gt;
&lt;p&gt;우선 UX 라이팅을 개선하기로 했다. 기존의 메세지 ‘저장되었습니다, 저장할 수 있습니다’는 지금 저장이 된건지 아닌지, 추가적인 액션이 필요한지 아닌지 헷갈렸다. 또한, 북마크 기능이라는 것이 명확하지 않았다. 서비스 자체의 북마크 기능인이 브라우저의 북마크 기능인지 알 수 없다. 따라서 메세지를 개선해주었다.&lt;/p&gt;
&lt;p&gt;또한, 백문이 불여일견이므로, 시작적인 피드백을 추가로 제공하기로 했다. 여기서 고민한 점은. 데스크탑, 모바일 마다 북마크 방법이 다르고, 사용자하는 브라우저마다 북마크 방법이 다르다는 것이었다. &lt;code&gt;navigator.userAgent&lt;/code&gt; 속성을 확인해서 구체적인 브라우저 종류를 확인할 수 있지만 이는 너무 과하다고 생각했다.적절히 타협하여 데스크탑, 모바일에서 각각 추상화된 북마크 애니메이션을 보여주기로 했다.&lt;/p&gt;
&lt;p&gt;이때 webp나 avif같은 가벼운 포맷의 이미지로 애니메이션을 보여줄지, 리액트 컴포넌트를 만들어서 보여줄지 고민했다. 후자가 더 간편하다고 생각해서 후자를 선택해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TpDAR/dJMcacW6gBP/vEQElsBQACAjuZf1lajP1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TpDAR/dJMcacW6gBP/vEQElsBQACAjuZf1lajP1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TpDAR/dJMcacW6gBP/vEQElsBQACAjuZf1lajP1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTpDAR%2FdJMcacW6gBP%2FvEQElsBQACAjuZf1lajP1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1148&quot; height=&quot;776&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;데스크탑과 모바일에서 다른 컴포넌트를 보여주기 위해서는 &lt;code&gt;&amp;lt;Responsive/&amp;gt;&lt;/code&gt; 유틸리티 컴포넌트를 만들고 사용해주었다.&lt;/p&gt;
&lt;h2&gt;가속화하기&lt;/h2&gt;
&lt;p&gt;내 서비스의 모든 문구를 점검할 필요를 느꼈다. 우선 메세지들을 한곳에서 관리하기 쉽도록 컴포넌트에 하드코딩 되어있던 메세지들을 상수로 분리해줬다. &lt;code&gt;features/(feature)/constant/messages.ts&lt;/code&gt;처럼 기능별로 묶어주었다. 모든 텍스트를 분리하기보다는 우선 토스트나, 다이얼로그로 사용자에게 피드백을 주는 경우를 분리해주었다.&lt;/p&gt;
&lt;p&gt;다음 UX 라이팅 베스트 프랙티스들을 학습해서 ux-writer &lt;a href=&quot;https://platform.claude.com/docs/ko/agents-and-tools/agent-skills/overview&quot;&gt;SKILL&lt;/a&gt;을 만들어준다. 그 다음 해당 스킬을 추가해주면 &amp;#39;UX 라이팅 개선해줘&amp;#39;와 같은 작업을 내렸을때 에이전트가 해당 스킬이 필요하다고 판단하면 해당 스킬을 읽고 작업에 참고한다.&lt;/p&gt;
&lt;p&gt;이를 통해 매우 간편하게 기존에 통일되지 않았던 어투를 통일하고 불친절한 표현을 찾아 개선할 수 있었다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>Skills</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/45</guid>
      <comments>https://chaesunbak.tistory.com/45#entry45comment</comments>
      <pubDate>Mon, 20 Apr 2026 09:01:54 +0900</pubDate>
    </item>
    <item>
      <title>AWS Certified Developer - Associate(DVA-C02) 취득 후기</title>
      <link>https://chaesunbak.tistory.com/44</link>
      <description>&lt;p&gt;작년 4월에 AWS Certified Cloud Practitioner 취득하고 올해 4월에 AWS Certified Developer - Associate 취득했다. 시험 준비하면서 얻은 팁들을 정리하고 공유한다.&lt;/p&gt;
&lt;h2&gt;시험정보&lt;/h2&gt;
&lt;p&gt;시험 응시료 할인 바우처, 학습자료 등 AWS 자격증 관련 최신정보는 가장 활발한 커뮤니티인 &lt;a href=&quot;https://www.reddit.com/r/AWSCertifications/&quot;&gt;레딧&lt;/a&gt;이 가장 좋은 것 같다.&lt;/p&gt;
&lt;h2&gt;학습방법&lt;/h2&gt;
&lt;h3&gt;공식 가이드&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/certification/certified-developer-associate/&quot;&gt;공식 가이드&lt;/a&gt;를 먼저 읽어보면 좋을 것 같다. &lt;/p&gt;
&lt;h3&gt;AWS Skill Builder&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://skillbuilder.aws/category/exam-prep/developer-associate-DVA-C02&quot;&gt;스킬 빌더&lt;/a&gt;에서 샘플 문제 조금과 강의 조금을 무료로 제공해준다. 구독 옵션도 존재하지만 무료 강의로 충분한 것 같다.&lt;/p&gt;
&lt;h3&gt;덤프&lt;/h3&gt;
&lt;p&gt;기존에 공부했던 덤프 사이트에 광고가 많이 뜨고 불편해서 이번에는 유튜브로 덤프를 풀었다. 시험명을 검색하면 바로 나오는 것 같다.&lt;/p&gt;
&lt;p&gt;가장 쉽고 빠르게 시험에 합격하는 방법은 덤프를 많이 푸는 것 같다. 해설을 꼼꼼히 읽어보고 모르거나 헷갈리는 부분은 공식문서를 찾아보거나 AI에게 물어보면서 정리하면 좋을 것 같다.&lt;/p&gt;
&lt;h2&gt;시험 팁&lt;/h2&gt;
&lt;p&gt;신분증과 영어이름이 포함된 체크카드 또는 신용카드가 필요하다.&lt;/p&gt;
&lt;p&gt;오프라인으로 응시했다. 시험장이나 상황마다 다를 것 같은데 나는 예약한 시간보다 30분 먼저 도착했는데 일찍 시험을 시작했다. 오프라인이면 중간에 화장실도 갈 수 있는 것 같다.&lt;/p&gt;
&lt;p&gt;객관식 시험인 만큼 모든 선지를 꼼꼼히 읽어보면서 소거법으로 풀면 좋을 것 같다.&lt;/p&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;사실 AWS 자격증은 실제로는 없는거고, 아마존이 교모하게 사람들을 가스라이팅해서 본인들 서비스 설명 + 홍보를 자격증이라고 속이면서 돈을 벌고 있는거라고 생각한 적이 많다. 응시료 말고 더 가치있게 쓸 수 있는 방법이 많은 것 같다. 그치만 굳이 장점을 찾아 이야기해보자면, AWS의 다양한 서비스들을 체계적으로 여러 관점(특히 보안영역을 포함해서)에서 시험이라는 익숙한 형태로 공부할 수 있는 것이 장점인 것 같다. 평범한 사이드프로젝트에서는 EC2, RDS, S3같은 기본적인 서비스만 사용하게 되는것 같은데, 엔터프라이즈급 요구사항을 SQS, SNS, Lambda Function, API Gateway, Elastic Cache 같은 서비스를 사용해서 간단하면서도 확장가능하게 구축하는 방법을 익힐 수 있는 기회인 것 같기도 하다. 대 AI 시대에 단순 지식을 평가하는 시험이 무슨 의미가 있는가 싶기도 하지만 이런이런게 있다고 익혀두면 AI로 그걸 구체화하고 실현하는 것이 쉬워져서 완전 의미가 없지는 않은 것 같다.&lt;/p&gt;</description>
      <category>후기</category>
      <category>AWS</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/44</guid>
      <comments>https://chaesunbak.tistory.com/44#entry44comment</comments>
      <pubDate>Mon, 20 Apr 2026 09:00:54 +0900</pubDate>
    </item>
    <item>
      <title>사파리 Clipboard API NotAllowedError</title>
      <link>https://chaesunbak.tistory.com/43</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 브라우저(Safari)에서만 Clipboard API 사용시 NotAllowedError가 발생하는 이슈가 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제의 코드&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt; async function handleShare(text){
    // 로깅용 fetch
    fetch(...)

    // 클립보드 복사
    await navigator.clipboard.writeText(text);
    // ❌ : NotAllowedError!
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Safari에서만 NotAllowedError가 발생한 이유&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The Clipboard API allows users to programmatically read and write text and other kinds of data to and from the system clipboard in &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts&quot;&gt;secure contexts&lt;/a&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;For writing to the clipboard the specification expects that the page has been granted the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API&quot;&gt;Permissions API&lt;/a&gt; &lt;code&gt;clipboard-write&lt;/code&gt; permission, and the browser may also require &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/User_activation&quot;&gt;transient user activation&lt;/a&gt;. Browsers may place additional restrictions over use of the methods to access the clipboard.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API&quot;&gt;mdn : Clipboard API&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Safari에서 Clipboard API를 사용하려면 클릭 이벤트 등 일시적 사용자 활성화가 필요하다. &lt;b&gt;클릭 사용자 이벤트와 Clipboard API 호출 사이에 다른 작업이 끼어들면서 NotAllowedError가 발생한 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 이벤트와 Clipboard API를 최대한 가깝게 유지한다. 중간에 fetch같은 작업을 끼우지 않는다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt; async function handleShare(text){
    // 클립보드 복사
    await navigator.clipboard.writeText(text);
    // ✅ OK

    // 로깅용 fetch
    fetch(...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 처리를 보다 친절하게 해보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt; async function handleShare(text){
    try {
        await navigator.clipboard.writeText(text);
    } catch(e){
        // 사용자에게 피드백을 주세요(그렇지 않으면 사용자는 '반응이없네?' 라고 느낍니다.)
        toast.error(&quot;복사에 실패했어요.&quot;)

        // 대체 수단을 제공하세요
        showDialog(`다음 텍스트를 복사하세요 ${text}`)

        // 로깅만 하지 말아줘잉
        logError(e)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;교훈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 API를 사용할 때는 해당 API가 어떤 에러를 던질 수 있는지 꼭 확인해야한다. mdn 정독 필수.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 버그는 공유 기능 구현 시점이 아닌, 이후 로깅 기능을 추가하는 과정에서 발생했다. (왜 저기다 추가했지...?) E2E 테스트 자동화를 통해 크로스 브라우징 이슈를 검증하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@hailieejkim/safari-clipboard-errors-notallowederror-and-typeerror-fixes-e9c23becd856&quot;&gt;Safari Clipboard Errors: NotAllowedError and TypeError Fixes&lt;/a&gt;&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <category>clipboard api</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/43</guid>
      <comments>https://chaesunbak.tistory.com/43#entry43comment</comments>
      <pubDate>Thu, 9 Apr 2026 09:00:44 +0900</pubDate>
    </item>
    <item>
      <title>NCP Object Storage CORS 에러 해결법</title>
      <link>https://chaesunbak.tistory.com/42</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용성 테스트 플랫폼 UTMate 개발 중 1MB 정도 크기의 JSON 로그 파일을 NCP Object Storage에 저장하고, 이를 프론트엔드에서 가져오려고 했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;async function getLog(url){
  cosnt result = await fetch(url);
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;CORS ERROR&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 짜잔, CORS 에러가 발생했다. CORS 에러는 잊을만 하면 등장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(나야 CORS)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쯤 되면 절대 당황하지 않고 침착하다. &lt;b&gt;CORS 에러는 브라우저의 오리진과 리소스 오리진이 달라서 발생하는 이슈이므로, 리소스 서버의 응답에 CORS 관련 헤더를 추가해주면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 뭔가 이상하다. 외부 API를 쓰는 것도 아니고 스토리지 서비스를 쓸때 CORS에러가 발생하기도 하던가? 스토리지 서비스를 쓰다가 CORS 에러를 마주했다는 사례를 본적이 없다. 다들 이미지나 비디오를 스토리지 서비스에 저장하고 그냥 잘 불러오지 않나?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리소스를 어떻게 사용하냐에 따라 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 보안의 기본 원칙인 동일 출처 정책(Same-Origin Policy, SOP)은 기본적으로 &lt;code&gt;&amp;lt;img &amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 같은 태그로 다른 출처의 리소소를 내 웹사이트 임베딩하는 것을 허용한다. 따라서, &lt;code&gt;&amp;lt;img &amp;gt;&lt;/code&gt; 태그로 외부 이미지 링크를 가져와 보여주려고 하면 문제없이 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;&lt;code&gt;fetch&lt;/code&gt; 를 통해서 스크립트로 리소스 읽기를 시도하는 경우 더 엄격한 보안정책이 적용된다.&lt;/b&gt; 이 경우 Preflight(OPTIONS) 요청을 통해 다른 출처가 해당 리소스 읽기를 허용하고 있는지를 확인하고, 다른 출처가 헤더를 통해 리소스 읽기를 허용하고 있지 않는 경우 이를 차단시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 CORS 에러는 브라우저가 리소스를 그리거나 실행할때는 발생하지 않으나, 리소스를 읽으려고 할 때(자바스크립트 변수에 담을 때) 발생하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 방법은 두가지가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프록시 패턴 도입하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 스토리지를 직접 호출하는 대신, 우리가 제어할 수 있는 백엔드 서버를 거쳐서 데이터를 가져오는 방식이다. 우리가 제어하는 백엔드 서버는 &lt;code&gt;same-origin&lt;/code&gt;이거나 CORS 관련 헤더를 우리가 직접 추가할 수 있으므로 CORS 에러 해결이 가능하다. 다만, API 서버 부하가 걸리므로 이 방법은 특별한 보안 요구사항이 있지 않은 경우에는 불필요한 방법이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 스토리지 서비스에 CORS 설정 추가하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 정석적인 방법이다. 스토리지 서비스 설정에 CORS 설정을 변경하면 CORS 관련 헤더가 추가된다. 우리 프로젝트에서는 해당 방법을 선택했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NCP CORS 설정 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 콘솔에 들어가서 허용 오리진에 프로젝트의 도메인을 추가하면 된다... 그런데 NCP Object Storage 서비스는 AWS S3와 달리 CORS 정책 설정 GUI를 지원하지 않는다. (2026년 1월 기준)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://api.ncloud-docs.com/docs/storage-objectstorage-putbucketacl&quot;&gt;NCP Object Storage 공식문서&lt;/a&gt;에 따르면 정해진 경로로 정해진 형식으로 HTTP 요청을 날리는 방식으로 CORS 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 예시&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;PUT /{bucket-name}?acl= HTTP/1.1
Host: kr.object.ncloudstorage.com
X-amz-date: {Timestamp}
Authorization: {authorization-string}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot; data-ke-language=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;AccessControlPolicy xmlns=&quot;http://s3.amazonaws.com/doc/2006-03-01/&quot;&amp;gt;
  &amp;lt;Owner&amp;gt;
    &amp;lt;ID&amp;gt;{owner-user-id}&amp;lt;/ID&amp;gt;
    &amp;lt;DisplayName&amp;gt;{owner-user-id}&amp;lt;/DisplayName&amp;gt;
  &amp;lt;/Owner&amp;gt;
  &amp;lt;AccessControlList&amp;gt;
    &amp;lt;Grant&amp;gt;
      &amp;lt;Grantee xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:type=&quot;CanonicalUser&quot;&amp;gt;
        &amp;lt;ID&amp;gt;{first-grantee-user-id}&amp;lt;/ID&amp;gt;
        &amp;lt;DisplayName&amp;gt;{first-grantee-user-id}&amp;lt;/DisplayName&amp;gt;
      &amp;lt;/Grantee&amp;gt;
      &amp;lt;Permission&amp;gt;READ_ACP&amp;lt;/Permission&amp;gt;
    &amp;lt;/Grant&amp;gt;
    &amp;lt;Grant&amp;gt;
      &amp;lt;Grantee xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:type=&quot;CanonicalUser&quot;&amp;gt;
        &amp;lt;ID&amp;gt;{second-grantee-user-id}&amp;lt;/ID&amp;gt;
        &amp;lt;DisplayName&amp;gt;{second-grantee-user-id}&amp;lt;/DisplayName&amp;gt;
      &amp;lt;/Grantee&amp;gt;
      &amp;lt;Permission&amp;gt;FULL_CONTROL&amp;lt;/Permission&amp;gt;
    &amp;lt;/Grant&amp;gt;
  &amp;lt;/AccessControlList&amp;gt;
&amp;lt;/AccessControlPolicy&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS SDK 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 형식에 맞춰 요청을 보내려면 보안을 위해 시그니쳐 등 각종 항목을 추가해 보내한다. 따라서, curl을 사용하거나 자바스크립트로 직접 구현하려고 한다면 무척 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 때 AWS SDK를 사용하면 보다 간편하게 해결할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NCP의 Object Storage는 AWS S3와 호환되는 API 구조를 가지고 있다. 따라서, 별도의 NCP 전용 라이브러리 없이 &lt;code&gt;aws-sdk&lt;/code&gt;와 같은 도구를 그대로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;aws-sdk&lt;/code&gt; 사용방법은 다음 예시들을 참고할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료 1 : &lt;a href=&quot;https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/command/PutBucketCorsCommand/&quot;&gt;S3::PutBucketCorsCommand&lt;/a&gt;&lt;br /&gt;참고자료 2 : &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/code-library/latest/ug/s3_example_s3_PutBucketCors_section.html&quot;&gt;AWS SDK 또는 CLI와 PutBucketCors 함께 사용&lt;/a&gt;&lt;/p&gt;</description>
      <category>위키</category>
      <category>AWS</category>
      <category>cors</category>
      <category>ncp</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/42</guid>
      <comments>https://chaesunbak.tistory.com/42#entry42comment</comments>
      <pubDate>Tue, 17 Mar 2026 09:00:50 +0900</pubDate>
    </item>
    <item>
      <title>사용자 경험을 데이터로 증명하세요 : UTMate 회고</title>
      <link>https://chaesunbak.tistory.com/39</link>
      <description>&lt;p&gt;부스트캠프 웹·모바일 10기 그룹프로젝트를 진행하며 매주 회고를 남겨보았다. 지루하고 현학적인 기술 이야기보다 의사결정과정에 있어서 내가 어떤 생각을 했고 무엇을 느꼈는지를 중점으로 정리해 봤다.&lt;/p&gt;
&lt;h2&gt;1주차 회고 : 주제 선정과 기획&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사용성 테스트 플랫폼 만들기.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2144&quot; data-origin-height=&quot;1248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cilaFu/dJMcahpOhuI/JoqlkSzKey8Y7OUcEduIkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cilaFu/dJMcahpOhuI/JoqlkSzKey8Y7OUcEduIkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cilaFu/dJMcahpOhuI/JoqlkSzKey8Y7OUcEduIkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcilaFu%2FdJMcahpOhuI%2FJoqlkSzKey8Y7OUcEduIkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2144&quot; height=&quot;1248&quot; data-origin-width=&quot;2144&quot; data-origin-height=&quot;1248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;아이디어 회의&lt;/h3&gt;
&lt;p&gt;새로운 사람들과 만남에 무척 긴장했었다. 그러나 곧 바보같은 걱정을 했다고 느꼈다. 부캠에는 다 잘하고 열심히 하는 사람밖에 없다는 걸 까먹었다.&lt;/p&gt;
&lt;p&gt;평소에 나는 프로젝트 아이디어가 떠오르면 잘 쌓아놨던 것 같은데, 이번에 내가 낸 아이디어는 내가 봐도 좀 구렸다. 모두가 다 좋아해야 한다는 생각에 자체검열을 하다보니 무난하고 구린 것만 제안하게 된 것 같다.&lt;/p&gt;
&lt;p&gt;그러다 보니 나는 내 기준 팀플 블랙리스트를 비토하는 데만 애썼던 것 같다. 좋은 쪽으로 에너지를 발산하지 못한 것이 아쉽다. 하지만 사실 좋은 아이디어라는 건 그냥 아이디어를 모은다고 얻어지는게 아닌 것 같기도 하다. 좋은 아이디어는 듣자마자(혹은 떠올리자마자) “이거다!”라고 알 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;내 기준 팀 프로젝트로 해서는 안되는 것
1. 커뮤니티 &amp;amp; SNS : 실제 사용자를 모으기 어렵다. 해결하고자 하는 문제가 명확하지 않다.
2. 게임 : 게임성이 모든 걸 좌우하게 된다. 그리고 게임성이라는 건 굉장히 미묘하다.
3. AI가 메인인 서비스 : 서비스의 품질을 보장할 수 없다. AI는 보조적인 수단으로 사용하거나 매우 잘 써야한다.
4. 협업툴 : 뻔하다.
(근데 이걸 빼고 나면 뭐가 남지...?)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그런데 이번 프로젝트에서는 정말 운이 좋게 사용성 테스트(UT) 솔루션이라는 좋은 주제를 만날 수 있었다. 듣자마다 “이거다!” 했다. 이 주제가 왜 좋은 프로젝트 주제인지를 간략하게 설명하면 다음과 같다&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 있어빌리티한 주제 : 사용성이라는 있어보이는 주제. 나는 사용성 테스트가 뭔지 이때 처음 알았다..
2. 명확한 문제의식 : UT의 허들을 낮추자!
3. 적절한 레퍼런스 : 참고할만한 해외 서비스들이 있다. 없는 걸 만드는 건 난이도가 10배다.
4. 도전적인 기술적 난이도 : SDK 개발 + 로그 분석... 이런걸 언제 해보겠어!
5. 잠재력 : 분석기능을 고도화할 수도 있고 테스터 매칭 등의 기능으로 확장할 수 도 있다.&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;방향&lt;/h3&gt;
&lt;p&gt;1주차에는, 무엇을 개발할지 뿐만 아니라 어떻게 개발할 지도 이야기할 수 있었다. 그리고 다음과 같은 팀 목표를 설정할 수 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;팀 목표
1. 사용자/서비스 관점에서 의사결정하기
2. 팀 차원에서 신중한 의사결정하기
3. 데이터에 근거해서 의사결정하기&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 여기에 나는 개인적인 목표를 하나 더했다. ‘서비스 개선을 위해서라면 뭐든지 해보기’. 이러한 목표에 근거해서, 기획서를 완성하기 이전에 간단한 설문조사로 데이터를 수집해보면 좋겠다고 생각했다. 곧바로 사용성 테스트 경험을 묻는 설문을 만들었다. ‘만들면 쓰실건가요?’같은 확실하지 않은 질문보다, 사용성 테스트와 관련한 경험 중심으로 객관적인 질문을 구성하려 노력했다. 현직자분들을 포함하여 약 40명 정도가 설문에 응답했고, 팀 기획 방향을 확실히 하는데 도움이 되는 유의미한 데이터를 얻을 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bssvcq/dJMcahwAGp8/Hbtt4mRvNUL69cl634EWrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bssvcq/dJMcahwAGp8/Hbtt4mRvNUL69cl634EWrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bssvcq/dJMcahwAGp8/Hbtt4mRvNUL69cl634EWrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbssvcq%2FdJMcahwAGp8%2FHbtt4mRvNUL69cl634EWrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;462&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;2주차 회고 : MVP 개발&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;다른 모든 것을 생략하고 기술적으로 난이도가 가장 어려운 부분에 집중했다. UT가 진행되는 동안 SDK로 로그를 수집될 수 있는지를 검증했다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;회의&lt;/h3&gt;
&lt;p&gt;팀원 모두가 내향적인 성격이여서, 회의를 주도하는 사람이 없었다. 회의중 긴 정적이 이어질 때도 종종 있었다. 회의가 매끄럽지 않다는 팀 피드백이 있었고, 이를 반영하여, 필요한 경우에만 회의를 갖고, 회의 전에는 이야기할 내용을 미리 준비하자는 액션을 도출했다.&lt;/p&gt;
&lt;p&gt;이때 정해진 회의방식이 이후 팀 스타일로 굳어졌다. 데일리 스크럼을 매일 오전 10시부터 12시까지 했다. 데일리 스크럼에서는 전날 작업 내용을 공유하고 새로 제안할 것을 공유하고, 다음 작업을 할당했다. 추가적으로, 새 기능을 설계하고 태스크를 정의하는 월요일에는 오후에도 회의했고, 데모 전날인 목요일에는 배포를 진행하기 위해 오후 5시에 모였다.&lt;/p&gt;
&lt;h3&gt;위기&lt;/h3&gt;
&lt;p&gt;팀원 한분이 개인적인 이유(Positive)로 퇴소했다. 처음 아이디어를 제안했기도 하고, 리더십이 있는 분이여서 매우 아쉬웠다. 시니어 피드백에서는 서비스 볼륨이 너무 커 우선순위를 잘 정해야할 것 같다는 피드백을 받았다. 안그래도 인력이 줄어들었는데, 매우 불안했다.&lt;/p&gt;
&lt;p&gt;하지만 이는 우리 팀이 각성하는 계기가 되었다. 빠른 개발이라는 공동의 목표를 갖게 되었으며, 기능별 개발 우선순위를 동기화할 수 있었다. 결과적으로 2주차 목표인 MVP 개발을 성공적으로 완료할 수 있었다.&lt;/p&gt;
&lt;h2&gt;3주차 회고 : 인증과 테스트 생성 기능&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;기능을 유저 플로우 순으로 구현하기로 했다. 따라서 3주차에는 회원가입하고 테스트를 생성하는 기능을 구현했다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;초능력&lt;/h3&gt;
&lt;p&gt;우리 팀은 프론트 2명, 백엔드 1명이었는데, 백엔드 한분이 혼자 한주만에 CI/CD, 인증 API, 테스트 CRUD API를 해냈다. 초능력을 쓴 것 같다고 생각했다. 적어도 2명 분량을 해냈다고 느꼈다. 물론 일부 코드는 이전 프로젝트에서 작업 것을 가져왔기 때문에 가능한 거기도 하지만, 모두 작업 퀄리티가 높아서 놀랐다. 항상 느끼는 거지만 ‘자바 스프링을 하다왔다’이거는 항상 신뢰가 가는 것 같다.&lt;/p&gt;
&lt;h3&gt;분업&lt;/h3&gt;
&lt;p&gt;빠른 개발을 위해 분업할 필요가 있었다. 풀스택과정이라는 취지에 어긋날 수 도 있다고 느꼈지만, 목표를 완수하려면 어쩔수 없었던것 같다. 나는 프론트 작업을 주로하게 되었다.&lt;/p&gt;
&lt;h3&gt;API 명세&lt;/h3&gt;
&lt;p&gt;API 명세 설계에서 의견 불일치가 있기도 했다. 나는 내 생각에 조금 더 RESTful한 API 명세를 원했는데, 모듈간의 의존성 문제를 이유로 거절 당했다. 조금 더 문서로 정리해서 다시 공유할 수 도 있겠겠지만, 빠른 개발과 분업이 필요하다고 생각했기 때문에, 이런 부분에 시간을 쓰기보다는 내가 맡아서 해야할 일에 집중하기로 했다. 일단 돌아가는 뭔가를 만드는데 집중하기로 했다.&lt;/p&gt;
&lt;h3&gt;팀 스타일&lt;/h3&gt;
&lt;p&gt;작업을 진행하면서 내가 파악한 우리 팀의 스타일은 느긋하면서도 급하다는 것이다. A기능과 B기능이 있을 때 우리는 “둘 다 하죠”라고 이야기하면서도, 한 주의 작업은 욕심내지 않고 목요일 5시에 마무리했다. 매주 금요일의 팀 회고는 후딱 5분만에 끝마칠 때도 있지만, 모두 같은 방향으로 피드백과 액션을 도출했으며, 도출된 액션은 무조건 반영했다.&lt;/p&gt;
&lt;h2&gt;4주차 회고 : 테스트 참여&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이어서, 테스트 참여 기능과 테스트 결과 기능을 작업했다. 결과 기능이 지연되어 다음 주로 밀렸다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;1094&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQhJ0u/dJMcacB2hK2/oTVMEnDEApCpOA4VN35drK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQhJ0u/dJMcacB2hK2/oTVMEnDEApCpOA4VN35drK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQhJ0u/dJMcacB2hK2/oTVMEnDEApCpOA4VN35drK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQhJ0u%2FdJMcacB2hK2%2FoTVMEnDEApCpOA4VN35drK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1356&quot; height=&quot;1094&quot; data-origin-width=&quot;1356&quot; data-origin-height=&quot;1094&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;회고&lt;/h3&gt;
&lt;p&gt;페이지 시안이나 구체적인 API 명세 없이 작업하는 것을 지양하자는 팀 피드백이 있었다. 지난주에는 구체적인 와이어프레임 없이 작업했다. 따라서, ‘다른 서비스 참고해서 알아서 센스있게 개발해주세요.’의 방식으로 개발이 진행되었다. API 명세도 구체적으로 정의되지 않아서, ‘일단 구현되면 거기에 맞출게요’와 같은 방식으로 개발했다. 빠른 개발이 가능하다는 장점이 있었지만 그 이외 모든 것이 단점이었다. 특히 작업의 퀄리티가 개인의 실력과 경험에 의해서 크게 결정되고, 팀 시너지를 얻을 수 없는 것이 가장 큰 문제였다&lt;/p&gt;
&lt;p&gt;따라서, 이번주에는 이러한 피드백을 반영하여 월요일 설계 시간에, 어떤 화면에서 어떤 API를 호출하고, 요청과 응답형식이 어떻게 되고, 예외 상황 등은 무엇이 있는지를 다 같이 그림으로 정리하여 모두 공유한다음 작업을 시작했다. 이렇게 정의한다고 설계시간이 크게 늘어나지도 않았고, 오히려 코드를 수정할 일이 적어져서 한주 작업이 더 원활하게 진행되었다. 여러 예외 상황에 대해서 더 많이 고민 해 볼 수 있는 것은 덤이었다. 매주 회고를 잘하는 것이 우리 팀의 장점이라고 느꼈다.&lt;/p&gt;
&lt;h3&gt;의사결정&lt;/h3&gt;
&lt;p&gt;테스트 참여중 이탈한 경우 테스트를 이어할 수 있는 기능과 테스트를 이탈하는 경우와 테스트를 진행 중인 경우를 구분하는 기능을 구현하느라 생각했던 것보다 테스트 참여 기능 개발이 지연되었다. 부가적인 기능은 나중에 구현해도 좋았을 것 같다는 아쉬움이 들어 이를 팀 회고 시간에 공유했다. 기능 우선 순위와 예상 작업시간을 복합적으로 고려해서 태스크를 분배하자는 액션을 도출했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1090&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJrozy/dJMcah4pzDz/RwuFYzaSCPK7nqntqFH96k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJrozy/dJMcah4pzDz/RwuFYzaSCPK7nqntqFH96k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJrozy/dJMcah4pzDz/RwuFYzaSCPK7nqntqFH96k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJrozy%2FdJMcah4pzDz%2FRwuFYzaSCPK7nqntqFH96k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1090&quot; height=&quot;1022&quot; data-origin-width=&quot;1090&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;에셋&lt;/h3&gt;
&lt;p&gt;항상, 진짜같은 웹사이트와 그렇지 않은 웹사이트를 구분하는 것은 에셋이라고 느꼈어서, 이번 프로젝트에서는 에셋을 많이 사용해서 실제 서비스와 유사하게 만들고 싶었다. 그리고 이런부분이 대AI시대에 AI를 적극 활용할 수 있는 부분이라고 느꼈다. 따라서, 서비스 소개에 사용할 캐릭터를 AI로 생성해보았다. 항상 만족스러운 응답이 나온것은 아니지만 그중 가장 맘에 드는 것을 선택해서 사용했다. 나름 만족스러운 결과물이 나온 것 같다.&lt;/p&gt;
&lt;p&gt;심심할때마다 이미지 생성 요청을 한번씩 해보는 것도 좋은 것 같다. 내가 AI 생성 이미지를 공유한 뒤로, 팀원들도 AI를 활용해서 비슷한 이미지를 만들어 오기도 했다. 이 과정에서, 이미지 각도 조정, 배경색깔 변경 등은 내가 직접할 수 있도록 사용법을 익혀둘 필요를 느꼈다.&lt;/p&gt;
&lt;p&gt;AI 생성 컨텐츠 이외에도 여러 무료 에셋들을 찾아보고, 그 중 마음에 드는 것을 우리 서비스에 적용하기도 했다. 랜딩페이지 등 다양한 부분에 더 많은 에셋을 써보지 못해 아쉽긴 하지만, 첫 시도여서 만족스럽다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qnRed/dJMcafMjxbd/ebTabscOny62AMMPs7rUG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qnRed/dJMcafMjxbd/ebTabscOny62AMMPs7rUG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qnRed/dJMcafMjxbd/ebTabscOny62AMMPs7rUG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqnRed%2FdJMcafMjxbd%2FebTabscOny62AMMPs7rUG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;546&quot; height=&quot;854&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;5주차 회고 : 테스트 결과&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;테스트 결과를 확인할 수 있는 기능을 작업했다. 기존 결과페이지 레이아웃이 밋밋하고 체계적이지 않은 것 같아서 새 레이아웃을 제안했고 이게 받아들여졌다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;마이그레이션&lt;/h3&gt;
&lt;p&gt;5주차 배포에서는 메인 디비를 날렸다. 마이그레이션 과정에서 문제가 생겨서 그냥 날리기로 결정했다. 백엔드 팀원이 친절하게 마이그레이션 가이드 문서도 작성해주고, 직접 시연(+ 강의)까지 해주었는데도 사실 나는 마이그레이션이 뭔지 아직도 이해를 못했다. 이해 못하는 개념이 있으면 꼭 따로 공부를 할 필요를 느꼈다.&lt;/p&gt;
&lt;h3&gt;개발 환경 구축&lt;/h3&gt;
&lt;p&gt;로컬에서 테스트하기 좋은 환경을 구축해야한다고 생각했다. 우리 서비스 개발에 페인포인트를 확인할 수 있었다. 우리 서비스의 기능을 로컬에서 테스트하려면, 클라이언트, 테스트 대상 사이트에 삽입할 스크립트, 테스트 대상 사이트, API 서버가 갖춰져야했다. 로컬 개발 환경에서는 이것들을 수동으로 작업해줘야하는 부분들이 많았고, 일일히 그렇게 하고 있었다. 그 과정이 매우 귀찮았기 때문에 나는 완벽히 동작을 테스트해보기 보다는 대충 이정도면 대충 돌아가겠지 하고 PR을 날린 적이 많다. 편리한 로컬 개발환경이 구축되지 않은 점이, 코드 퀄리티를 낮추며 자잘한 버그를 놓치는 원인이 된다고 생각했다. 개발 초기에 이런 점을 고려하지 못한 점이 아쉽다.&lt;/p&gt;
&lt;p&gt;로컬에서 테스트하기 귀찮다 보니, 일단 배포해보고 프로덕션 환경에서 QA를 자주하게 된것 같다. API 모킹을 더 실제와 유사하게 구현하거나, 더미 데이터를 DB 벌크로 삽입하여 성능을 확인할 필요를 느꼈다. 테스트 서버를 따로 두는 것도 방법이라고 생각했다. 개발하기 좋은 환경을 구축하는 것이 팀 개발 효율과 퀄리티를 높일수 있는 일임을 배웠다. 아키텍쳐를 설계할 때 로컬에선 어떻게 동작을 검증하고 테스트할 것인가 계획을 포함할 필요를 느꼈다.&lt;/p&gt;
&lt;h3&gt;작업 방식&lt;/h3&gt;
&lt;p&gt;여태껏 프론트와 백엔드를 나눠서 주로 작업해왔는데. 이번주의 몇몇 작업들은 풀스택으로 작업했다. 작은 기능을 추가하거나, API 응답 형식을 변경하고 이에 맞게 디스플레이를 변경하는 것은 혼자 엔드투엔드로 작업했을 때 더 효율적이라고 판단했다. 풀스택과정 취지를 살리면서도 팀 목표 달성을 위한 유연한 판단을 한 것 같아서 매우 만족스럽다.&lt;/p&gt;
&lt;h3&gt;UT로 UT하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xcKMR/dJMcafldGdb/88NUSkjkL6VrcixkbVe6YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xcKMR/dJMcafldGdb/88NUSkjkL6VrcixkbVe6YK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xcKMR/dJMcafldGdb/88NUSkjkL6VrcixkbVe6YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxcKMR%2FdJMcafldGdb%2F88NUSkjkL6VrcixkbVe6YK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;582&quot; height=&quot;322&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;작은 버그들과 예외처리가 부족한 부분이 있었지만, 테스트 생성 → 참여→ 결과확인 기능 구현이 완료되어 이번 주 데모에서는 우리 서비스를 우리 서비스로 UT하는 데모 방식을 도입했다. 사용성 테스트 플랫폼의 사용성을 사용성 테스트 플랫폼으로 테스트하는 약간 어지러운 방식이었는데, 사용자가 직접 서비스를 체험해보면서 기능을 보여줄 수 있으면서도, 우리는 사용성 개선에 필요한 데이터를 수집할 수 있다는 두마리 토끼 를 모두 잡을 수 있는 방식이었다.&lt;/p&gt;
&lt;p&gt;사실 프로젝트 5주차가 되니 개발동기가 많이 떨어졌었다.그런데 사용자가 우리 서비스를 사용하는 모습을 보면서 완전히 재충전 되었다. 다른 사람이 내가 만든 서비스를 사용하는 걸 보는게 큰 동기부여가 된다는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;h2&gt;6주차 : 테스트 탐색&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;테스트 결과 기능을 고도화하고 테스트 매칭기능을 어설프게 구현했다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;피그마&lt;/h3&gt;
&lt;p&gt;사실 나는 CSS 노가다가 재밌고 잘할 자신있다. 머릿속에 내가 원하는 디자인을 상상하고 그걸 CSS로 구현하는 걸 나름 좋아하고 잘한다고 생각한다. 빈 화면에서 뚝딱 그럴듯한 결과물을 내놓는 걸 능력이라고 생각하기도 했다. 하지만, 이번 프로젝트를 하면서 피그마같은 디자인 툴의 필요성을 느꼈다. 간단한 툴로 그린 와이어프레임만 있고 구체적인 디자인이 없다보니, 프론트엔드 작업의 인수기준을 정하기가 어려워졌다. AI 냄새가 많이 나거나 조금 투박한 디자인으로 작업이 되었을 때, 해당 작업이 완성된 것인지 아닌지 판단하기 어려웠다. 팀원의 작업물에 내가 디자인을 수정하는것이 효율적인 분업일 수도 있지만, 다른 팀원의 기능 개발을 위축시키거나 기회를 뺏는 것이라는 걱정도 들었다. 또한 스타일 개선과 다른 기능 개발의 우선순위를 비교하기도 어려웠다.&lt;/p&gt;
&lt;p&gt;이번 프로젝트는 대AI시대에 맞게 기존 개발자의 영역에서 벗어나 기획자, 마케터와 같은 다양한 역할을 수행해보자고 생각했었는데, 디자이너의 역할은 미처 생각해보지 못한 것 같다. 물론 인터미션 때 피그마 작업을 하는게 아니라면 일정상 피그마 작업을 하는게 물리적으로 불가능 했을 것 같긴하다.&lt;/p&gt;
&lt;h3&gt;백엔드 차이&lt;/h3&gt;
&lt;p&gt;이번엔 백엔드 코드를 많이 짜지 않고, 주로 보기만 했다. 이미 틀이 잡혀있는 코드를 조금 수정하기만 했다.&lt;/p&gt;
&lt;p&gt;코드를 많이 보면서 내 코드와 차이를 많이 느끼기도 했다. 계층 분리, 모듈 분리, 어떤 로직을 어디에 둘 것인가 차이가 많이 있었다. 좋은 코드라는 게 프로그래밍 경험에 따라 다르게 느껴질 수 있다고 생각했는데, 이번에 많이 느꼈다.&lt;/p&gt;
&lt;p&gt;나는 기존에는 쿼리가 단순하고 눈에 잘 보이는 백엔드 코드를 선호했다는 사실을 알 수 있었다. 복잡한 쿼리와 비즈니스 로직을 추상화하여 이해하고 생각할 수 있는 능력을 길러야겠다고 느꼈다. 두 스타일이 어떻게 다르고, 어떤 장단점이 있는지 명확히 설명할 수 있어야 한다고 느꼈다.&lt;/p&gt;
&lt;h3&gt;완성도&lt;/h3&gt;
&lt;p&gt;6주차에 오픈 베타를 출시했다. 예외처리, 잔버그, 불편한 사용성 등 아쉬운 부분이 많은데 출시하게 되어서 매우 아쉬웠다. 기획 단계에서 목표했던 오픈베타 출시보다 2주가 딜레이 되면서, 매 데모가 스트레스였다. (부캠 끝나고가 막막한 것은 덤이었다.) 실제 사용자로부터 데이터를 얻으면서 ‘우리 서비스가 사용성을 찾고 개선하는데 도움이 될 수 있다’는핵심가설을 검증하는 부분을 경험하지 못한 부분이 아쉬웠다.&lt;/p&gt;
&lt;h2&gt;7주차 : 리팩토링&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;리팩토링보다는 버그수정과 문서화에 집중하기로 했다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;테스트코드&lt;/h3&gt;
&lt;p&gt;SDK 핵심 로직을 테스트 코드로 검증할 수 있으면 좋겠다고 생각했다. SDK를 테스트해본적은 없어서 어떻게 테스트해야할지는 잘 몰랐지만, 적절한 방법을 찾아야 한다고 생각했다. 우선 테스트하기 좋은 구조로 리팩토링해보자는 의견도 제시했다. 결과적으로는 통합테스트를 작성하여 핵심로직을 검증했다. 다만 이부분도 약간 아쉬운게 브라우저를 띄워서 진행하는 E2E 테스트를 해봤으면 어땠을까 하는 생각도 있다.&lt;/p&gt;
&lt;h3&gt;문서화 부채&lt;/h3&gt;
&lt;p&gt;이번주는 과감하게 월·화에만 코드작업을 하고 수·목에는 문서화와 최종발표준비에 힘쓰기로 했다. 과감한 결정으로 보다 온전한 정신으로 프로젝트를 돌아보며 발표를 준비할 수 있어서 좋았다. 다만 테스트코드, 리팩토링을 그만큼 못하게된 점이 아쉽다. 문서화도 다 끝내지못했다. 위키의 기술문서들은 결국 수료 이후에 추가 작업하기로 했다. 문서화 부채라는게 존재할 수 있다고 느꼈다. 기억이 휘발되기전에 미리 기록해두는게 중요한 것 같다.&lt;/p&gt;
&lt;h2&gt;최종회고&lt;/h2&gt;
&lt;p&gt;이번 프로젝트는 의미있다. 일단 좋은 주제로 열정있고 실력있는 팀원들과 함께 할 수 있는 기회는 흔치않다. 솔직히 다음 프로젝트를 하더라도 이거보다 잘 할 수 있을까라는 걱정이 든다.&lt;/p&gt;
&lt;p&gt;다만, 이번 프로젝트에서 가장 어려웠던 문제가 무엇인가요?라고 물어본다면, 바로 대답하기 어려운것 같다. 팀원 모두 의외로 SDK개발과 로그 분석이 쉬웠다고 회고했다. 물론 이는 아직 우리가 개발과정에서 어떤 시행착오를 겪었는지 제대로 정리해보지 않아서 그런 것일 수도 있지만, 실제로 해당 문제에 대해서 깊이 파고들지 않아서 그럴 수도 있는거라고도 생각한다. 개발과정을 더 정리해보고, 로그 분석을 고도화한다면 어떻게 할 수 있을지 정리해볼 필요를 느꼈다.&lt;/p&gt;
&lt;p&gt;최종 발표를 마치고, 피드백을 종합해보고 두 가지 부분에서 개선점을 확인할 수 있었다. 하나는, 프라이버시(보안)과 관련한 부분이고, 두가지는, 대규모 로그 분석과 관련한 부분이다. 기획단계에서 이런 부분에서 목표를 정하고 도전해봤다면 좋았을 것 같다는 아쉬움이 든다. 이 부분과 관련해서 현재 아키텍쳐에서는 어떠한 한계점이 있고 아키텍쳐를 어떤 식으로 개선할 수 있는지는 다른 글에서 정리해보고자 한다.&lt;/p&gt;
&lt;h2&gt;팁&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;주제를 잘 생각해놓으면 좋다. 프로젝트는 주제빨이 크다. 해결하고자 하는 문제가 명확하면서 난이도 있어야 한다.&lt;/li&gt;
&lt;li&gt;기술을 먼저 정해놓고 주제를 정하는 것보다 머리를 최대한 굴려서, 문제해결에 적절한 기술을 찾아 도전하고 고도화해보는 것이 좋은 것 같다.&lt;/li&gt;
&lt;li&gt;프로젝트 종료 이후 계획을 기획 단계에서 팀원들과 미리 의논해봐도 좋을 것 같다. 다만 매일 8시간++ 갈아넣으면서 개발하는 것이 아니므로 다른 방식의 개발을 고려해야봐야 할 것 같다.&lt;/li&gt;
&lt;li&gt;프로젝트 종료 이후에도 서비스를 운영하고 싶다면 크레딧 지출 관리를 하는게 좋아보인다. 최대 90만원 6개월 크레딧이므로. 한달에 15만원씩 사용한다면 6개월을 띄워놓을 수 있다. (홈서버가 있는 팀원이 있으면 해결되긴한다. 다만 홈서버로 전환하려면 아키텍쳐 변경이 필요한 부분이 있는 것 같다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;부록&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/boostcampwm2025/web16-UTMate&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>부스트캠프</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/39</guid>
      <comments>https://chaesunbak.tistory.com/39#entry39comment</comments>
      <pubDate>Thu, 12 Feb 2026 09:00:55 +0900</pubDate>
    </item>
    <item>
      <title>타입스크립트 개발자들이 enum을 싫어하는 이유</title>
      <link>https://chaesunbak.tistory.com/38</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbkL33/dJMcagRQ4tU/IEwVZlK71Ra8ogA1sxHUL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbkL33/dJMcagRQ4tU/IEwVZlK71Ra8ogA1sxHUL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbkL33/dJMcagRQ4tU/IEwVZlK71Ra8ogA1sxHUL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbkL33%2FdJMcagRQ4tU%2FIEwVZlK71Ra8ogA1sxHUL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소처럼 코드 리뷰를 하던 중, 한 타입 전용 패키지가 &lt;code&gt;devDependencies&lt;/code&gt;(개발 의존성)가 아닌 &lt;code&gt;dependencies&lt;/code&gt;(일반 의존성)에 추가된 것을 발견했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 타입 패키지를 개발 의존성으로 변경하면 좋을 것 같아요!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 제안은 그럴듯해 보였고, 팀원도 흔쾌히 수용했다. 이때까지만 해도 내가 무슨 일을 저질렀는지 몰랐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 수정 후, 평화롭던 빌드 파이프라인에서 에러가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Error: Cannot find module 'types/my-package' or its corresponding type declarations.
...
FAILED: build project&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당황스러웠다. 분명 타입 패키지인데 왜 빌드 타임에 모듈을 찾을 수 없다고 나오는 걸까? 다시 해당 패키지를 개발 의존성에서 일반 의존성으로 변경하니 빌드에 성공했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;의문점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 이상해서 프로젝트 레포지터리를 둘러봤고, 재밌는 사실을 발견할 수 있었다. 어떤 패키지에서는 해당 패키지를 개발 의존성으로, 어떤 곳에서는 일반 의존성으로 가져가고 있었다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; data-ke-language=&quot;json&quot;&gt;&lt;code&gt;//apps/client/package.json
{
  &quot;name&quot;: &quot;client&quot;,
  &quot;devDependencies&quot;: {
    &quot;@types/package&quot;: &quot;2.0.0&quot;,
  },
}
//apps/server/package.json
{
  &quot;name&quot;: &quot;server&quot;,
  &quot;dependencies&quot;: {
    &quot;@types/package&quot;: &quot;2.0.0&quot;,
  },
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;어쩔 땐 되고, 어쩔 땐 안 되는 이유가 뭘까?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { IamEnum } from '@types/package';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범인은 바로 &lt;code&gt;enum&lt;/code&gt;이었다. 문제가 된 서버 패키지에서는 타입 패키지에서 &lt;code&gt;enum&lt;/code&gt;을 가져와 타입이 아니라 값으로 사용하고 있었기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;enum이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.typescriptlang.org/ko/docs/handbook/enums.html&quot;&gt;&lt;code&gt;enum&lt;/code&gt;&lt;/a&gt;은 열거형 변수로, 정해진 범위 안의 값들을 이름으로 관리할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;export enum Direction {
  Up = 'UP',
  Down = 'DOWN'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트의 대부분 기능(&lt;code&gt;Interface&lt;/code&gt;, &lt;code&gt;Type&lt;/code&gt;)은 컴파일 과정에서 사라지지만(타입 소거), &lt;code&gt;enum&lt;/code&gt;은 다르다. 타입으로도 쓸 수 있지만. 실제로 메모리에 올라가는 객체(값)로도 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제의 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &lt;code&gt;devDependencies&lt;/code&gt;가 아닌 &lt;code&gt;dependencies&lt;/code&gt;여야 했을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;런타임에 코드가 존재하는가?&quot;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;타입(Type, Interface):&lt;/b&gt; 컴파일 단계에서 사라짐. 빌드할 때만 정보가 필요하므로 &lt;code&gt;devDependencies&lt;/code&gt;에 있어도 무방함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;값(Enum, Class):&lt;/b&gt; 컴파일 후에도 자바스크립트 객체로 남아 런타임에 실행됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;enum&lt;/code&gt;을 타입이 아니라 값으로 호출하는 순간, 이 코드는 실행 시점에 해당 패키지의 실제 로직을 참조해야 한다.&lt;/b&gt; 하지만 &lt;code&gt;devDependencies&lt;/code&gt;는 보통 배포나 런타임 환경에서 제외되기 때문에, 빌드 과정에서 참조할 실제 소스 코드를 찾지 못해 에러가 발생하게 된 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;enum의 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입과 값의 경계가 모호하다는 점 뿐만 아니라 enum은 태생적으로 몇가지 이슈가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트리 쉐이킹 이슈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 타입스크립트 코드는 사용하지 않으면 빌드 시 삭제되는 트리쉐이킹이 적용되지만, &lt;code&gt;enum&lt;/code&gt;은 자바스크립트의 즉시 실행 함수(IIFE) 형태로 변환된다. 그런데 &lt;b&gt;Rollup과 같은 번들러는 IIFE를 '사용하지 않는 코드'라고 판단할 수 없어서 트리 쉐이킹되지 않는다.&lt;/b&gt; 결국 &lt;code&gt;enum&lt;/code&gt;을 &lt;code&gt;import&lt;/code&gt;하고 실제로는 사용하지 않더라도 최종 번들에 포함되게 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;export var Direction;
(function (Direction) {
    Direction[&quot;Up&quot;] = &quot;UP&quot;;
    Direction[&quot;Down&quot;] = &quot;DOWN&quot;;
})(Direction || (Direction = {}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking&quot;&gt;&lt;b&gt;참고자료 : TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다.&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;유연성 부족&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문자열 열거형(String enums)은 값이 같더라도 호환되지 않는다. 이는 유연성을 크게 떨어뜨린다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;enum Color {
  Red = 'RED'
}

// 값은 똑같은 'RED'이지만, 타입 에러가 발생한다.
const myColor: Color = 'RED'; 
// Error: Type '&quot;RED&quot;' is not assignable to type 'Color'.&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Enum의 대안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 의존성 혼란과 런타임 오염 때문에 많은 타입스크립트 개발자들은 &lt;code&gt;enum&lt;/code&gt; 사용을 피한다. &lt;code&gt;enum&lt;/code&gt; 대신 &lt;code&gt;const enum&lt;/code&gt;을, &lt;code&gt;const enum&lt;/code&gt; 대신 &lt;code&gt;as const&lt;/code&gt;를 사용할 것이 권장된다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Pascalcase&amp;lt;S extends string&amp;gt; = Capitalize&amp;lt;Lowercase&amp;lt;S&amp;gt;&amp;gt;;

type Direction = 'UP' | 'DOWN';

export const DIRECTION = {
  Up: 'UP',
  Down: 'DOWN',
} as const satisfies Record&amp;lt;Pascalcase&amp;lt;Direction&amp;gt;, Direction&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Husky 기반의 Pre-push 빌드 자동화 부재로 인해 결함을 조기에 포착하지 못한 점이 아쉽다. 이번 경험으로 자동화 검증 단계의 중요성을 다시 한번 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;enum&lt;/code&gt;을 선호하지 않는다라는 사실만 어렴풋이 알고 잇었는데, 이를 체험해 볼 수 있었다. 값과 타입의 혼돈을 피하기 위해 &lt;code&gt;enum&lt;/code&gt;을 지양하는 것이 좋아보인다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yceffort.kr/2022/03/typescript-use-union-types-instead-enum&quot;&gt;내가 타입스크립트에서 Enum을 잘 쓰지 않는 이유&lt;/a&gt;&lt;/p&gt;</description>
      <category>위키</category>
      <category>enum</category>
      <category>TypeScript</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/38</guid>
      <comments>https://chaesunbak.tistory.com/38#entry38comment</comments>
      <pubDate>Mon, 26 Jan 2026 09:00:36 +0900</pubDate>
    </item>
    <item>
      <title>터미널에서 ls 명령어를 치면 무슨 일이 일어날까?</title>
      <link>https://chaesunbak.tistory.com/36</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mp9mO/dJMcagRHOeR/rlTj0k701wpkAdTlsMW1m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mp9mO/dJMcagRHOeR/rlTj0k701wpkAdTlsMW1m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mp9mO/dJMcagRHOeR/rlTj0k701wpkAdTlsMW1m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMp9mO%2FdJMcagRHOeR%2FrlTj0k701wpkAdTlsMW1m1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;920&quot; height=&quot;512&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ls&lt;/code&gt; 명령어는 파일 및 디렉터리 목록을 보여주는(list) 기본 리눅스 명령어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;입력&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;ls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;출력&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;bin   dev  home  media  opt   root  sbin  sys  usr
boot  etc  lib   mnt    proc  run   srv   tmp  var&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 터미널에 &lt;code&gt;ls&lt;/code&gt; 명령어를 칠 때 내부적으로는 무슨 일어날까? 이번에는 운영체제(OS)의 관점에서 그 여정을 따라가 보며 OS의 핵심 원리들을 정리해보고자 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로그램과 프로세스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 터미널에 &lt;code&gt;ls&lt;/code&gt;를 입력하기 전, &lt;code&gt;ls&lt;/code&gt;는 디스크의 특정 위치(&lt;code&gt;/usr/bin/ls&lt;/code&gt;)에 저장되어 있는 &lt;b&gt;프로그램&lt;/b&gt;(Program)으로 존재한다. 정확히는 C 언어로 작성되어 컴파일된 바이너리 형식의 파일이다. 프로그램이란 어떤 작업을 위해 실행될 수 있는 파일을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔터키를 입력하는 순간, 이 정적인 파일은 메모리에 올라가 실행 엔진이 돌아가는 &lt;b&gt;프로세스&lt;/b&gt;(Process)로 변신한다. 프로세스란 실행 중인 프로그램을 의미하며, CPU에 의해 실행되는 동적인 상태의 작업 단위이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제는 프로세스를 트리 구조의 계층구조로 관리한다. 한 프로세스가 다른 프로세스를 생성하면, 두 프로세스는 부모-자식 관계를 맺는다. 따라서 &lt;code&gt;ls&lt;/code&gt; 명령어를 입력하면, 쉘은 부모 프로세스, &lt;code&gt;ls&lt;/code&gt;는 자식 프로세스가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 프로세스는 생애주기에 따라 생성, 준비, 실행, 대기, 종료라는 상태를 갖는다. &lt;code&gt;ls&lt;/code&gt;는 실행되어 결과를 출력하고 나면 즉시 종료 상태로 전이되며 OS에 의해 자원이 회수된다. 이러한 상태 전이를 프로세스 상태 전이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제는 프로세스 스케쥴링을 통해 여러 프로세스를 번갈아 실행하면서, CPU의 코어 갯수보다 많은 프로세스가 마치 동시에 동작하는 것처럼 동시성을 구현할 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 공간&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ls&lt;/code&gt;는 주어진 경로의 파일 및 디렉터리 목록를 출력하기 위해, 파일 등의 자원에 접근할 필요가 있다. 하지만, 운영체제는 안정성, 보안, 효율성을 위해 프로세스가 하드웨어나 민감한 시스템 자원에 직접 접근하는 것을 막는다. 이러한 목적으로, 운영체제는 &lt;code&gt;ls&lt;/code&gt;와 같은사용자 프로그램과 운영체제의 핵심인 커널을 각각 사용자 공간과 커널 공간으로 격리하여 실행한다. 오직 커널만이 CPU, 메모리, 디스크 등 모든 하드웨어 자원을 제어할 수 있는 권한을 가지므로, &lt;code&gt;ls&lt;/code&gt; 프로세스는 직접 시스템 자원에 접근하는 대신 커널에게 요청해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커널과 시스템 콜&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널은 운영체제의 심장부로, 응용 프로그램들이 자원을 안전하게 사용할 수 있게 지휘한다. 이때 응용 프로그램이 커널에게 서비스를 요청하는 유일한 통로가 바로 시스템 콜(System Call)이다. 쉽게말해, 시스템 콜은 사용자 공간의 프로세스가 커널의 기능을 사용하기 위해 호출하는 인터페이스다. 리눅스의 시스템 콜 목록은 &lt;a href=&quot;https://thevivekpandey.github.io/posts/2017-09-25-linux-system-calls.html&quot;&gt;여기&lt;/a&gt;서 확인해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ls&lt;/code&gt; 프로세스는 크게 다음과 같은 시스템 콜을 호출하여 작업을 수행한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;execve()&lt;/code&gt;: 쉘(bash, zsh 등)이 &lt;code&gt;ls&lt;/code&gt;라는 실행 파일을 메모리에 올리고 실행.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openat()&lt;/code&gt;: 현재 디렉토리(&lt;code&gt;.&lt;/code&gt;) 열기. (리눅스에서는 디렉토리도 하나의 '파일'로 취급된다.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getdents64()&lt;/code&gt; : 디렉터리 엔트리(Directory Entries)를 가져오기. 디렉터리 내부의 파일 이름, inode 번호 등을 커널로부터 받아오는 가장 핵심적인 단계.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write()&lt;/code&gt;: 커널이 가져다준 데이터를 가공하여 터미널(표준 출력)에 화면을 그림.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VFS (Virtual File System)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 여러 종류의 파일 시스템을 사용한다. 내장 하드에서는 &lt;code&gt;ext4&lt;/code&gt;, USB에서는 &lt;code&gt;FAT32&lt;/code&gt;, 네트워크에서는 &lt;code&gt;NFS&lt;/code&gt;를 사용한다. 각 파일 시스템은 데이터를 저장하고 관리하는 방식(레이아웃)이 모두 다르다. 하지만, 가상 파일 시스템(VFS)은 프로세스가 동일한 인터페이스로(&lt;code&gt;getdents64&lt;/code&gt;) 여러 파일 시스템을 사용할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;외장 USB(FAT32)에서 &lt;code&gt;ls&lt;/code&gt;를 치는 경우의 흐름:&lt;/i&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;ls&lt;/code&gt;는 파일 시스템 종류와 상관없이 동일하게 &lt;code&gt;getdents64&lt;/code&gt; 시스템 콜을 보낸다.&lt;/li&gt;
&lt;li&gt;VFS는 해당 경로가 어느 파일 시스템에 마운트되어 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;VFS는 내부의 함수 포인터를 사용하여, 타겟이 내장 하드라면 &lt;code&gt;ext4_readdir&lt;/code&gt;를, USB라면 &lt;code&gt;fat_readdir&lt;/code&gt;를 매핑하여 호출한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;strace&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;strace&lt;/code&gt;는 프로세스가 실행되는 동안 호출하는 모든 시스템 콜을 추적(trace)하는 리눅스 명령어다.&lt;code&gt;strace ls&lt;/code&gt;를 실행해 보면 &lt;code&gt;ls&lt;/code&gt; 프로세스가 주어진 작업을 수행하는 흐름을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;입력&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;&lt;code&gt;strace ls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;출력&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;&lt;code&gt; root@f2b0ddc7ca37:/# strace ls
execve(&quot;/usr/bin/ls&quot;, [&quot;ls&quot;], 0xffffd5898c80 /* 8 vars */) = 0
brk(NULL)                               = 0xaaab22737000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff8b1f5000
faccessat(AT_FDCWD, &quot;/etc/ld.so.preload&quot;, R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, &quot;/etc/ld.so.cache&quot;, O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=5199, ...}) = 0
mmap(NULL, 5199, PROT_READ, MAP_IVATE, 3, 0) = 0xffff8b1f3000
close(3)                                = 0
openat(AT_FDCWD, &quot;/lib/aarch64-linux-gnu/libselinux.so.1&quot;, O_RDONLY|O_CLOEXEC) = 3
read(3, &quot;\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\0\0\0\0\0\0\0\0&quot;..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=198800, ...}) = 0
mmap(NULL, 337472, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xffff8b169000
mmap(0xffff8b170000, 271936, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xffff8b170000
munmap(0xffff8b169000, 28672)           = 0
munmap(0xffff8b1b3000, 34368)           = 0
mprotect(0xffff8b19c000, 77824, PROT_NONE) = 0
mmap(0xffff8b1af000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2f000) = 0xffff8b1af000
mmap(0xffff8b1b1000, 5696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff8b1b1000
close(3)                                = 0
openat(AT_FDCWD, &quot;/lib/aarch64-linux-gnu/libc.so.6&quot;, O_RDONLY|O_CLOEXEC) = 3
read(3, &quot;\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\360\206\2\0\0\0\0\0&quot;..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1722920, ...}) = 0
mmap(NULL, 1892240, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xffff8afa2000
mmap(0xffff8afb0000, 1826704, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xffff8afb0000
munmap(0xffff8afa2000, 57344)           = 0
munmap(0xffff8b16e000, 8080)            = 0
mprotect(0xffff8b149000, 81920, PROT_NONE) = 0
mmap(0xffff8b15d000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0xffff8b15d000
mmap(0xffff8b162000, 49040, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff8b162000
close(3)                                = 0
openat(AT_FDCWD, &quot;/lib/aarch64-linux-gnu/libpcre2-8.so.0&quot;, O_RDONLY|O_CLOEXEC) = 3
read(3, &quot;\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\0\0\0\0\0\0\0\0&quot;..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=592328, ...}) = 0
mmap(NULL, 721536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xffff8aeff000
mmap(0xffff8af00000, 656000, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xffff8af00000
munmap(0xffff8aeff000, 4096)            = 0
munmap(0xffff8afa1000, 57984)           = 0
mprotect(0xffff8af88000, 94208, PROT_NONE) = 0
mmap(0xffff8af9f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x8f000) = 0xffff8af9f000
close(3)                                = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff8b1f1000
set_tid_address(0xffff8b1f10f0)         = 163
set_robust_list(0xffff8b1f1100, 24)     = 0
rseq(0xffff8b1f1740, 0x20, 0, 0xd428bc00) = 0
mprotect(0xffff8b15d000, 12288, PROT_READ) = 0
mprotect(0xffff8af9f000, 4096, PROT_READ) = 0
mprotect(0xffff8b1af000, 4096, PROT_READ) = 0
mprotect(0xaaaae4afe000, 8192, PROT_READ) = 0
mprotect(0xffff8b1fa000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0xffff8b1f3000, 5199)            = 0
statfs(&quot;/sys/fs/selinux&quot;, 0xffffeb7a0260) = -1 ENOENT (No such file or directory)
statfs(&quot;/selinux&quot;, 0xffffeb7a0260)      = -1 ENOENT (No such file or directory)
getrandom(&quot;\x6f\x51\x87\x54\xb1\x2b\xb8\xfc&quot;, 8, GRND_NONBLOCK) = 8
brk(NULL)                               = 0xaaab22737000
brk(0xaaab22758000)                     = 0xaaab22758000
openat(AT_FDCWD, &quot;/proc/filesystems&quot;, O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(3, &quot;nodev\tsysfs\nnodev\ttmpfs\nnodev\tbd&quot;..., 1024) = 577
read(3, &quot;&quot;, 1024)                       = 0
close(3)                                = 0
faccessat(AT_FDCWD, &quot;/etc/selinux/config&quot;, F_OK) = -1 ENOENT (No such file or directory)
ioctl(1, TCGETS, {c_iflag=ICRNL|IXON, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0}) = 0
openat(AT_FDCWD, &quot;.&quot;, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, 0xaaab2273c600 /* 21 entries */, 32768) = 520
getdents64(3, 0xaaab2273c600 /* 0 entries */, 32768) = 0
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
write(1, &quot;bin   dev  home  media\topt   roo&quot;..., 50bin   dev  home  media    opt   root  sbin  sys  usr
) = 50
write(1, &quot;boot  etc  lib\t mnt\tproc  run   &quot;..., 47boot  etc  lib     mnt    proc  run   srv   tmp  var
) = 47
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시스템 콜 딥다이브&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ls&lt;/code&gt; 프로세스가 호출한 전체 시스템콜 중 주요 시스템 콜들을 더 자세히 알아보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로세스 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;&lt;code&gt;execve(&quot;/usr/bin/ls&quot;, [&quot;ls&quot;], 0xffffd5898c80) = 0&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt;는 현재 실행 중인 쉘(Shell) 프로세스의 복사본을 만들어 자식 프로세스를 생성하는 시스템 콜이다. 생성된 자식 프로세스는 &lt;code&gt;execve()&lt;/code&gt;를 호출하여 자신의 메모리 공간을 &lt;code&gt;ls&lt;/code&gt; 프로그램의 바이너리로 완전히 교체한다. 쉘은 이 과정을 통해 명령어를 새로운 독립적인 작업 단위로 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;런타임 환경 준비&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;&lt;code&gt;openat(AT_FDCWD, &quot;/etc/ld.so.cache&quot;, O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, &quot;/lib/aarch64-linux-gnu/libselinux.so.1&quot;, ...) = 3
openat(AT_FDCWD, &quot;/lib/aarch64-linux-gnu/libc.so.6&quot;, ...) = 3
...
mmap(NULL, 337472, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xffff8b169000&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;openat()&lt;/code&gt;을 사용하여 프로그램 실행에 필수적인 표준 라이브러리(&lt;code&gt;libc.so&lt;/code&gt; 등) 파일을 찾아 연다. 그 후 &lt;code&gt;mmap()&lt;/code&gt; 시스템 콜을 통해 해당 라이브러리 파일의 내용을 프로세스의 가상 메모리 주소 공간에 직접 매핑한다. 이 과정을 거쳐 &lt;code&gt;ls&lt;/code&gt;는 별도의 복사 과정 없이 메모리상의 라이브러리 코드를 즉시 호출할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;하드웨어 환경 파악&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ioctl(1, TCGETS, ...) = 0
ioctl(1, TIOCGWINSZ, {ws_row=24, ws_col=80, ...}) = 0&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ioctl()&lt;/code&gt; 시스템 콜을 사용하여 현재 출력이 나갈 터미널(TTY)의 상태를 확인한다. 특히 터미널의 가로 폭(Window Size) 정보를 가져와 파일 목록을 몇 개의 열로 배치할지 결정한다. 출력 대상이 터미널인지 파일인지에 따라 &lt;code&gt;ls&lt;/code&gt;가 결과 포맷을 다르게 구성하는 기준이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;목록 조회&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;&lt;code&gt;openat(AT_FDCWD, &quot;.&quot;, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
getdents64(3, /* 21 entries */, 32768) = 520&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;openat()&lt;/code&gt;으로 현재 디렉터리(&lt;code&gt;.&lt;/code&gt;)를 열어 파일 디스크립터(번호표)를 획득한다. 핵심인 &lt;code&gt;getdents64()&lt;/code&gt; 시스템 콜을 통해 커널로부터 디렉터리 내의 모든 파일 이름과 Inode 번호 묶음을 한꺼번에 받아온다. 이때 VFS는 실제 장치가 무엇인지 판단하여 하드웨어의 데이터를 읽어온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;5&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;출력과 프로세스 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;&gt;&lt;code&gt;write(1, &quot;bin   dev  home  media\topt   roo&quot;..., 50) = 50
write(1, &quot;boot  etc  lib\t mnt\tproc  run   &quot;..., 47) = 47
exit_group(0)                           = ?&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;write()&lt;/code&gt; 시스템 콜을 사용하여 가공된 텍스트 데이터를 1번 파일 디스크립터(표준 출력)로 보낸다. 터미널은 이 데이터를 받아 화면에 글자를 렌더링하여 사용자에게 보여준다. 모든 작업이 끝나면 &lt;code&gt;exit_group()&lt;/code&gt;을 호출해 프로세스가 사용하던 메모리와 자원을 OS에 모두 반납하고 완전히 사라진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여기서 다루지 않은 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스와 시스템콜 개념에 집중하여 설명하다보니, 몇몇 개념은 언급만 하거나 자세히 다루지 못했다. 이후에 아래 개념들을 더 학습해보고 위 흐름과 연결지어보면 좋을 것 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리눅스의 구성 요소&lt;/li&gt;
&lt;li&gt;터미널(Terminal)과 쉘(Shell)&lt;/li&gt;
&lt;li&gt;Inode&lt;/li&gt;
&lt;li&gt;정적 라이브러리와 동적 라이브러리&lt;/li&gt;
&lt;li&gt;TTY&lt;/li&gt;
&lt;li&gt;IPC와 파이프&lt;/li&gt;
&lt;li&gt;파일 디스크립터(File Descriptor)&lt;/li&gt;
&lt;li&gt;덴트리 캐시(Dentry Cache)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>분석</category>
      <category>Linux</category>
      <category>OS</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/36</guid>
      <comments>https://chaesunbak.tistory.com/36#entry36comment</comments>
      <pubDate>Sun, 4 Jan 2026 13:00:59 +0900</pubDate>
    </item>
    <item>
      <title>2025 회고 : The Best Programmer I Know</title>
      <link>https://chaesunbak.tistory.com/34</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGfOZE/dJMcaaqgAFk/zp5780RtGE3D6FMRm7ymI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGfOZE/dJMcaaqgAFk/zp5780RtGE3D6FMRm7ymI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGfOZE/dJMcaaqgAFk/zp5780RtGE3D6FMRm7ymI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGfOZE%2FdJMcaaqgAFk%2Fzp5780RtGE3D6FMRm7ymI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;640&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://endler.dev/2025/best-programmers/&quot;&gt;The Best Programmers I Know&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;오픈소스 관리자이자 Rust 컨설팅 회사 corrode의 설립자 &lt;a href=&quot;https://endler.dev/about/&quot;&gt;Matthias Endler&lt;/a&gt;는 2025년 4월, 자신이 관찰한 최고의 프로그래머들의 특징을 공유하며 프로그래밍 입문자에게 조언을 주는 &amp;lt;&lt;strong&gt;The Best Programmers I Know&lt;/strong&gt;&amp;gt;을 블로그에 게시했다. 이후 이 글은 레딧과 해커뉴스같은 개발자 커뮤니티에 공유되며 많은 주목을 받았다.&lt;/p&gt;
&lt;h3&gt;2025년 회고하기&lt;/h3&gt;
&lt;p&gt;나 또한 위 글을 읽고 많이 공감하며 여러 인사이트를 얻을 수 있었다. 사실, 이 글을 처음 읽은 것은 올해 5~6월 쯤이이었는데, 이를 바탕으로 늦은 2024년 회고를 해보면 좋겠다고 생각했다. 하지만, 이런저런 이유로 늦어졌다. 저자는 위 내용이 &lt;strong&gt;체크리스트가 아님&lt;/strong&gt;을 명확히 했지만. 이를 체크리스트 삼아 나의 2025년 프로그래밍 경험을 회고해보려 한다.&lt;/p&gt;
&lt;h3&gt;참고 자료(Reference)를 읽으세요&lt;/h3&gt;
&lt;p&gt;곧바로 아파치 웹서버 문서, 파이썬 표준 라이브러리, 그리그 TOML 명세를 살펴봤다. 리액트 공식문서가 매우 친절함을 다시 한번 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;프론트엔드 개발자로서 내가 사용하는 기술의 공식문서를 꾸준히 읽고 숙지할 필요를 느꼈다. 간단한 내용도 놓치고 있을 때가 종종 있다고 느꼈다.&lt;/p&gt;
&lt;p&gt;매일 한 챕터 읽기, API 사용할 때마다 읽기 같이 여러 방법을 시도해봤지만, 아직 부족한 것 같다. 특정 방법을 찾기보다는 무식하게 끼고 사는게 좋을 것 같다.&lt;/p&gt;
&lt;p&gt;전문가로 인정받기 위해, 공식문서 어디에 무슨 내용이 있는지 기억할정도로 이를 숙지해야겠다.&lt;/p&gt;
&lt;h3&gt;자신의 도구를 아주 잘 아세요&lt;/h3&gt;
&lt;p&gt;해당 단락을 읽고, 이런 저런 도구를 써봤다고 나열하기보다 한 도구를 깊게 써 봐야겠다고 생각했다. 특히, 여러 설정 옵션들을 읽고 그 의미를 설정할 수 있을 정도로 한 도구를 잘 알 필요를 느꼈다.&lt;/p&gt;
&lt;p&gt;이를 의식해서, 새로운 도구를 사용하기보다는 기존에 사용하던 도구를 의도적으로 더 많이 사용해봤다. 따로, 해당 도구를 학습하려고 하지 않아도, 쓰면서 하나씩 보이게되는것 같다.&lt;/p&gt;
&lt;p&gt;한 도구의 전체 기능이 어느 정도이고 내가 그 중 얼마나 숙지하고 있는지 확인해볼 필요를 느꼈다. 고지식하게 한 도구를 고수하는 것처럼 보일정도로 한 도구를 매우 잘 알고 싶다. 물론, 이를 바탕으로 문제해결에 적절한 도구를 선택하는 유연한 사고를 기르고 싶다.&lt;/p&gt;
&lt;h3&gt;에러 메시지를 읽으세요&lt;/h3&gt;
&lt;p&gt;매번 에러 메시지를 그대로 LLM에 복사/붙혀넣기하던 나를 반성하게 되었다.&lt;/p&gt;
&lt;p&gt;의식적으로, 에러 메세지를 읽으려 했다. 그 결과 대부분의 에러 메세지는 매우 친절하며, 때로는 이를 해결할 수 있는 단서를 제공함을 알 수 있었다. 에러 메시지를 그대로 검색하는 것이 LLM보다 더 유용한 결과를 주기도 했다.&lt;/p&gt;
&lt;p&gt;물론 에러 메시지를 LLM에게 맡기는게 시간효율적일 수도 있지만, 에러 메시지를 직접 읽었을 때에 장점도 체감할 수 있었다. 내가 어떤 문제를 해결하느라 시간을 소모했는지, 이를 어떻게 해결했는지를 더 명확하게 기억하고 설명할 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;앞으로도, 에러 메세지를 읽는 습관을 유지하고 싶다.&lt;/p&gt;
&lt;h3&gt;문제를 분해하세요&lt;/h3&gt;
&lt;p&gt;기본이지만 항상 어려운 것 같다. 사이드프로젝트를 진행하면서, 한번에 많은 걸 해결하려다 보니 길을 잃을 때가 종종 있었다.&lt;/p&gt;
&lt;p&gt;올해는 익숙하지 않던 CS관련 문제를 해결하다보니, 직관적인 접근을 시도할 때 더 많은 한계를 느꼈다. 이 때, 다이어그램을 그려보는 것이 그나마 도움이 되었다.&lt;/p&gt;
&lt;p&gt;앞으로는 그림을 더 많이 그려보며, 문제를 분해하고 구조화하고 싶다.&lt;/p&gt;
&lt;h3&gt;손 더럽히는 것을 두려워하지 마세요&lt;/h3&gt;
&lt;p&gt;관련된 프로그래밍 경험이 없는 것 같다.&lt;/p&gt;
&lt;p&gt;어떤 코드든(익숙하지 않은 언어든, 분야든) 피하지 말고 많이 읽고 다뤄보자.&lt;/p&gt;
&lt;h3&gt;항상 다른 사람을 도우세요&lt;/h3&gt;
&lt;p&gt;올해 여러 사람들과 동료학습을 진행하면서, 나의 영향력에 대해서 고민해 볼 수 있었다.&lt;/p&gt;
&lt;p&gt;여태껏, ‘나는 다른 프로그래머를을 돕는 프로그래머인가? 도우려고 한적은 있는가?’ 이런 고민 없이 그냥 달려온 것 같다.&lt;br&gt;개발자가 된다는 것을, 그냥 &amp;#39;나&amp;#39;라는 사람이 &amp;#39;개발자&amp;#39;라는 옷을 입는 것으로 착각했다. 다른 사람에게 내가 어떤 개발자이고 싶은지를 더 고민해봐야 할 것 같다.&lt;/p&gt;
&lt;p&gt;나의 영향력이 항상 같지는 않다는 것도 느낄 수 있었다. ‘나는 그냥 항상 나’라고 생각했지만, 주변 환경에 따라, 내 영향력이 바뀐다는 것도 확인할 수 있었다. 때로는, 다른 사람을 돕기도 했지만, 때로는 그러지 못했다.&lt;/p&gt;
&lt;p&gt;이 과정에서, 내가 &amp;#39;개발자되기&amp;#39;라는 그림을 잘못 그리고 있었다는 것을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;내가 어떤 영향력을 미치고 싶은가를 기억하면서 이를 이루기 위해 노력해야하는 것 같다. 내가 어떤 도움을 받았는지를 기억하고 이를 되돌려주려 노력하자. 내가 어떤 강점을 지니고 어떤 약점을 지녔는지 기억하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2798&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vV3nG/dJMcaiovyli/GdDkvnWkhplvg2aghzwQSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vV3nG/dJMcaiovyli/GdDkvnWkhplvg2aghzwQSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vV3nG/dJMcaiovyli/GdDkvnWkhplvg2aghzwQSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvV3nG%2FdJMcaiovyli%2FGdDkvnWkhplvg2aghzwQSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2798&quot; height=&quot;1108&quot; data-origin-width=&quot;2798&quot; data-origin-height=&quot;1108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;글을 쓰세요&lt;/h3&gt;
&lt;p&gt;올해 초부터 블로그에 글을 쓰고 있다. 올해는 나름 만족스러운 글쓰기 활동을 경험한 것 같다.&lt;/p&gt;
&lt;p&gt;대 AI시대에 대응하기 위해, ‘나’만 쓸 수 있는 글을 쓰려고 노력했다. 내가 개발하면서 얻은 관점을 공유하거나 실제 프로젝트에 얻은 데이터와 노하우를 공유하려 노력했다. 꼭 기술관련 글일 필요도 없는 것 같아서, 다른 주제를 시도해 보기도 했다.&lt;/p&gt;
&lt;p&gt;하지만, 항상 만족스러웠던 것은 아니다. 한번은, 숙제처럼 의무감에 글을 쓰고, ‘이런 주제로 포스팅하면 좋을것 같은데’라며 내가 잘 알지도 못하는 내용을 급급히 쓰는 것 같아 작성한 내용을 모두 지워버리기도 했다. 물론, 그 과정에서 학습하는 것이 있기 때문에 나쁘지만은 않겠지만, &amp;#39;나&amp;#39;만 쓸 수 있는 글을 쓰겠다는 처음의 취지에서 벗어난 것 같았다.&lt;/p&gt;
&lt;p&gt;앞으로도 꾸준히 글쓰기 활동을 이어가면 좋겠다.&lt;/p&gt;
&lt;h3&gt;결코 배우는 것을 멈추지 마세요&lt;/h3&gt;
&lt;p&gt;올해는 새로운 것들을 많이 학습할 수 있는 기회가 있어서, 잠시 내 입맛은 접어두고, 내게 주어지는 것들에 몰입하며 학습하려고 했다. 결과적으로, Nest.js, MySQL, 클라우드, 인프라, CS 등 평소 친하지 않던 분야에서 만족스럽게 학습한 것 같다.&lt;/p&gt;
&lt;p&gt;이 과정에서 구체적인 학습 목표를 설정할 필요를 느꼈다. ‘DB 마스터하기’처럼 추상적인 목표보다 ‘EXPLAIN 명령어로 쿼리 실행계획 확인하고 쿼리속도 개선하기’같이 구체적이고 작은 단위의 목표를 정하고 실행하는 것이 도움이 되었다.&lt;/p&gt;
&lt;p&gt;다만, 학습에 조급할 필요는 없는 것 같다. 약간 경우가 다르기도 하지만, 공부할 시간도 없이 무리하기 자격증 시험을 신청하고 포기한적도 있었다.(내 돈!) 불안감에 계획없이 행동한 것 같아 아쉽다. 내년에는 데이터분석과 클라우드 관련 분야의 자격증을 잘 계획해서 도전했으면 한다.&lt;/p&gt;
&lt;p&gt;프론트엔드 분야에서는 무지함의 봉우리의 꼭대기에 올라왔다 떨어지는 것을 반복 경험한 것 같다. ‘이제는 좀 경지에 오른것 같은데?’라는 자신감에 어깨가 가벼워졌다가도, 금세 부족한 부분을 확인하고 무너지기도 했다. 이게 내가 성장하고 있다는 증거기도 하겠지만, 너무 자기자신을 과대평가하거나 평가절하할 필요는 없는 것 같다.&lt;/p&gt;
&lt;h3&gt;지위는 중요하지 않습니다&lt;/h3&gt;
&lt;p&gt;관련된 프로그래밍 경험이 없는 것 같다.&lt;/p&gt;
&lt;h3&gt;평판을 쌓으세요&lt;/h3&gt;
&lt;p&gt;올해는 오픈소스에 기여해보고 싶은 목표가 있었고, 번역 기여와 타입 기여부터 시작할 수 있었다. 운이 좋게 내가 사용하던 프로젝트의 간단한 이슈를 해결하는 기여도 해볼 수 있었다.&lt;/p&gt;
&lt;p&gt;앞으로는 더 많은 사람들이 사용하는 저명한 프로젝트에 기여해보며 평판을 쌓고 싶다. 이를 위해서는, 한 프로젝트를 자주 사용해보며, 잘 알고 좋아애야하는 것 같다. 한 프로젝트를 정해서, 꾸준히 들여다 볼 필요가 있는 것 같다.&lt;/p&gt;
&lt;h3&gt;인내심을 가지세요&lt;/h3&gt;
&lt;p&gt;올해 인내심을 가지고 꾸준히 공부한 부분도 있지만, 코테 준비에서 인내심이 부족했던 것 같다. 막히는 부분이 있을 때마다 멀리하게 된 것 같다. 인내심을 가지고 코테를 준비해보자.&lt;/p&gt;
&lt;h3&gt;절대 컴퓨터를 탓하지 마세요&lt;/h3&gt;
&lt;p&gt;관련된 프로그래밍 경험이 없는 것 같다.&lt;/p&gt;
&lt;h3&gt;&amp;quot;모르겠습니다&amp;quot;라고 말하는 것을 두려워하지 마세요&lt;/h3&gt;
&lt;p&gt;흔치 않게 찾아온 면접을 준비하면서, 단 한가지 명심했던 것은 ‘모르는건 모른다’고 말하자였다. 그런데 정말 기묘하게도 매번 내가 단 한번도 생각해보지 않은 것만 물어보는 것 같아 무척 당황했다. 결국 말미에는 찍어서라도 맞추려고 해서 망쳐버린 것 같다.&lt;/p&gt;
&lt;p&gt;기본기가 부족했기도 했지만, 그 동안 내가 아는것과 모르는 것의 경계를 확실히 하지 않았다는 것도 느낄 수 있었다. 앞으로, 면접이건 아니건 내가 어디까지 알고 있는지, 무엇을 더 알고싶은지를 말하는 것을 두려워하지 않아야겠다.&lt;/p&gt;
&lt;h3&gt;추측하지 마세요&lt;/h3&gt;
&lt;p&gt;단순히 내가 직접 간단하게 실험해서 확인 할 수 있는 일도, LLM에게 물어보게 되는 것 같다. 이때 생성된 답변을 신뢰해도 되는지 안해도 되는지 판단하기 어려울 때면, 많은 추측을 하게 되는 것 같다.&lt;/p&gt;
&lt;p&gt;추측을 줄이고 관측을 많이 하도록 해보자.&lt;/p&gt;
&lt;h3&gt;단순하게 유지하세요&lt;/h3&gt;
&lt;p&gt;올해는 사이드프로젝트를 유지보수하는 경험도 해봤고, 다른 사람이 작성한 코드를 리팩토링하는 경험도 해봤다. 그 과정에서, 단순한 코드가 이해하기도 쉽고 유지보수하기도 쉬움을 체험할 수 있었다.&lt;/p&gt;
&lt;p&gt;단순하게 유지하기 위해서는 핵심만 남길 수 있어야 하는데 이는 내가 작성한 코드을 완전한 이해하기를 필요로하는 것 같다. 또한 검증되고 유명한 라이브러리를 사용하여 내 코드를 단순화하는 것도, 어떤 라이브러리들이 있는지, 해당 라이브러리가 어떻게 돌아가는건지를 이해해야 잘 할 수 있는 것 같다.&lt;/p&gt;
&lt;p&gt;단순하게 유지한다는 목표를 담아두고 실천해보고 싶다.&lt;/p&gt;</description>
      <category>회고</category>
      <author>chaesunbak</author>
      <guid isPermaLink="true">https://chaesunbak.tistory.com/34</guid>
      <comments>https://chaesunbak.tistory.com/34#entry34comment</comments>
      <pubDate>Tue, 9 Dec 2025 09:00:12 +0900</pubDate>
    </item>
  </channel>
</rss>