<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>baealex (배진오)</title><link>http://blex.me/@baealex</link><description>창작을 좋아하는 개발자입니다.</description><atom:link href="http://blex.me/rss/@baealex" rel="self"/><language>ko</language><lastBuildDate>Sun, 17 May 2026 08:55:16 +0900</lastBuildDate><image><url>/resources/media/images/avatar/da/baealex/awRAy.png</url><title>baealex (배진오)</title><link>http://blex.me/@baealex</link></image><item><title>AI에게 멈추라고 말할 권리</title><link>http://blex.me/@baealex/ai%EA%B0%80-%EC%95%8C%EC%95%84%EC%84%9C-%EB%8B%A4-%ED%95%B4%EB%B2%84%EB%A6%B4-%EB%95%8C-%EA%B7%B8%EB%83%A5-%EB%84%A4%EB%9D%BC%EA%B3%A0-%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</link><description>&lt;blockquote&gt;&lt;p&gt;안 만드는 것도 개발이다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;별로 새롭지 않은 말이다. 기능을 덜 만들고, 코드를 덜 고치고, 불필요한 추상화를 피하는 게 중요하다는 말은 예전부터 있었다. 그런데 AI가 코드를 너무 쉽게 쌓아주는 시대가 되니까, 이 오래된 말이 조금 다르게 보이기 시작했다.&lt;/p&gt;&lt;p&gt;요즘 AI를 둘러싼 말은 대체로 더 많이 쓰는 쪽으로 흐른다. 더 긴 컨텍스트, 더 많은 토큰, 더 오래 도는 에이전트. 계속 돌리면 더 좋은 답이 나오고, 더 많은 코드를 만들 수 있고, 더 많은 일을 자동으로 처리할 수 있을 것처럼 말한다.&lt;/p&gt;&lt;p&gt;물론 그런 방식이 필요한 작업도 있다. 긴 조사나 넓은 코드베이스 분석처럼, 실제로 유용하게 쓰이는 일도 있다.&lt;/p&gt;&lt;p&gt;근데 매일 코드를 만지는 입장에서는 이 말들은 조금 의심스럽다.&lt;/p&gt;&lt;p&gt;그 많은 실행은 정말 내 코드베이스를 위한 걸까?&lt;/p&gt;&lt;p&gt;아니면 AI 회사와 GPU 회사가 계속 팔아야 하기 때문일까?&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;토큰을 쓰는 게 목적이 되면 이상해진다&lt;/h2&gt;&lt;p&gt;개발자가 봐야 하는 건 토큰 사용량이 아니다.&lt;/p&gt;&lt;p&gt;이 기능은 누굴 위해 만드는지, 누굴 위해 이렇게 설계했는지, 실제로 누구의 문제를 얼마나 푸는지다. 그 질문이 빠진 채로 “AI를 더 오래 돌렸다”, “토큰을 더 많이 썼다”, “에이전트가 계속 작업했다”만 남으면 좀 이상해진다.&lt;/p&gt;&lt;p&gt;그건 개발이라기보다 가짜 성과에 가깝다.&lt;/p&gt;&lt;p&gt;AI가 코드를 많이 만드는 일은 분명 편하다. 나도 그 편함을 좋아한다. 한참 걸릴 정리를 금방 해주고, 귀찮은 반복 작업도 잘 해준다. 다만 많이 만들었다는 사실이 곧 잘 만들었다는 뜻은 아니다.&lt;/p&gt;&lt;p&gt;이건 AI가 없던 시절에도 똑같다. “안 만드는 것도 개발이다” 라는 말은 AI 존재전부터 있었던 말이다. 개발자가 하는 일은 단지 코드만 짜는 일이 아니라, ‘이건 정말 만들어야 하는지’, ‘꼭 지금 고쳐야 되는지’, ‘이 변경이 정말 사용자의 문제를 푸는지’ 이런걸 보는 일도 개발이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;한 줄만 고쳐달라고 했는데&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/5/17/20265178_yJoyKJz4dgr8Bb76TUOK.jpg" alt="ig_05983e3ed5f90df1016a03260ae5648191b627086ea57d674f.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;사람들은 보통 AI에게 알잘딱깔센(알아서 잘 딱 깔끔하고 센스있게)을 기대한다. 실제로 써보면 &lt;code&gt;알아서&lt;/code&gt;는 꽤 잘한다. 문제는 &lt;code&gt;잘딱깔센&lt;/code&gt;이 자주 안 맞는다는 점이다. 예를 들어 특정 함수에서 값 하나만 바꾸고 싶었다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-text"&gt;가중치를 0.8에서 0.9로 바꿔줘.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그런데 AI는 주변 코드까지 같이 본다. 오래된 문법이 보이면 최신 문법으로 바꾸고 싶어하고, 암묵적으로 작동하는 코드를 명시적으로 수정하고, 원하지 않았던 예외처리까지 챙긴다. 사실 이렇게만 들으면 좋은 행동이고, 좋은 의도다.&lt;/p&gt;&lt;p&gt;이게 어쩔땐 잘딱깔센처럼 보일지 몰라도, 어쩔땐 아니다.&lt;/p&gt;&lt;p&gt;무엇보다 문제는 AI가 변경 범위를 늘린 그 순간 이 작업은 성격이 바뀌게 된다.&lt;/p&gt;&lt;p&gt;처음에는 작은 수정이었는데, 어느새 동작 계약이 바뀌고, 리뷰해야 할 범위가 늘어난다. “이 변경도 필요한가?”를 설명해야 하는 일도 생긴다. 또 한 번 들어간 코드는 바로 사라지지 않는다. 애매한 추상화, 과한 일반화, 불필요한 테스트, 괜히 늘어난 파일 구조는 다음 변경의 비용이 된다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;그래서 멈추게 했다&lt;/h2&gt;&lt;p&gt;그래서 아예 멈춤 규칙을 넣었다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-md"&gt;작업이 아래 조건 중 하나라도 만족하면, 다음 구현으로 넘어가지 말고 먼저 상태를 보고한다.

* 계획에 없던 파일을 새로 수정해야 하는 경우
* 계약 변경이 처음 계획보다 커진 경우
* 원인 해결이 아니라 증상 완화로 흐르기 시작한 경우
* 설명이 흐려지거나 가정이 많아진 경우
* 검증 없이 구현만 누적되는 경우&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;실제로 쓰는 규칙은 조금 더 길지만, 핵심은 이거였다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;“더 잘해”가 아니라 “이럴 땐 멈춰”&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;AI가 알아서 좋은 판단을 하길 기대하는 대신, 판단이 필요한 지점에서 대화를 만들도록 한 것이다.&lt;/p&gt;&lt;p&gt;같은 작업을 룰이 있는 경우와 없는 경우로 Eval 테스트로 몇 번 돌려봤다. 룰이 없을 때는 시킨 것 외의 변경이 자주 섞였고, 룰이 있을 때는 본문 변경과 제안이 분리됐다. 정밀한 검증이라고 말하긴 어렵다. 일부러 AI가 손대고 싶어 할 만한 편향적인 코드를 준 것도 맞다. 다만 내가 겪던 문제에는 꽤 좋은 효과를 보였다.&lt;/p&gt;&lt;p&gt;AI랑 진정한 협업을 하고 있다고 느꼈다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;멈춘다고 끝은 아니다&lt;/h2&gt;&lt;p&gt;멈춤 규칙을 넣으면 AI가 이런 식으로 묻는다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-text"&gt;이 부분은 같이 정리하는 게 좋을 것 같습니다. 진행할까요?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기서 내가 그냥 “네”라고 하면 어떻게 될까?&lt;/p&gt;&lt;p&gt;겉으로는 내가 승인한 것처럼 보인다.&lt;/p&gt;&lt;p&gt;AI도 물어봤고, 나도 답했으니까. 근데 사실은 별로 판단하지 않은 상태일 때가 많았다. 왜 같이 정리해야 하는지, 지금 해도 되는 일인지, 다른 방법은 없는지 확인하지 않은 채로 그냥 “네”라고 한 거니까. AI가 알아서 하지 못하게 막아놓고, 멈춘 다음에는 다시 “알아서 잘 하겠지” 하고 넘긴 셈이다. 여기서 조금 웃겼다.&lt;/p&gt;&lt;p&gt;나는 AI를 멈추게 만들어놓고, 정작 멈춘 자리에서는 다시 알잘딱깔센을 기대하고 있었다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;“네”는 판단이 아닐 수 있다&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/5/17/20265178_v3kmMCe8AUE3MzolmWcY.jpg" alt="ig_05983e3ed5f90df1016a0326c4ad788191869a9773c6291cfa.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;AI가 추론을 못 한다는 뜻은 아니다.&lt;/p&gt;&lt;p&gt;Claude Opus 4.5 막 나온 시절에는 나는 거의 “ㅇㅇ” 프롬프트(?)만 사용했다. 클로드가 알아서 정말 잘했다. 나 대신 생각을 했고 심지어 잘했다. AI 들은 삘 타면 꽤 그럴듯한 방향으로 나아간다. 다만 이 AI의 추론이라는 것은 본질적으로는 비결정적인 확률적 요소이기 때문에 운에 맞겨 처리하게 둘 순 없다.&lt;/p&gt;&lt;p&gt;나는 이것을 주로 ‘슬롯머신’에 비유한다.&lt;/p&gt;&lt;p&gt;잘 나오면 좋도, 이상하면 다시 뽑고, 또 이상하면 짜증나는 식으로 개발하고 싶지는 않았다.&lt;/p&gt;&lt;p&gt;그래서 요즘에는 AI가 멈춘 순간에, 이 정도는 한 번 더 물어보려고 한다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-text"&gt;이건 왜 이렇게 처리해야해?
다른 대안/방안은 없어?
이걸 꼭 지금 처리해야만 할까?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;대단한 질문이 아니다. 그냥 내가 생각하기 위한 질문이다. AI가 멈췄다는 건, 거기서 내가 다시 생각할 기회가 생겼다는 뜻일 수 있다. 그 기회에 우리는 본질적인 고민을 해볼 수 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;내가 지금 쓰는 방식&lt;/h2&gt;&lt;p&gt;지금은 대충 이렇게 쓴다:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;요청한 작업 범위 줄이기&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;범위를 벗어나면 멈추라고 하기&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;멈춘 이유를 짧게 설명하게 만들기&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;승인하기 전에 이유와 대안을 물어보기&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;이번에 할 일과 나중에 할 일을 구분하기&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;말로 쓰면 복잡해 보이는데, 실제로는 이 정도다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-text"&gt;요청한 것만 최소 변경으로 처리해줘.
계획에 없는 파일 수정, 계약 변경, 리팩터링이 필요하면 먼저 멈추고 보고해줘.

멈췄다면 바로 진행하지 말고,
왜 필요한지 / 다른 방법은 없는지 / 이번 PR에서 해도 되는지 알려줘.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 정도만 해도 AI의 작업 방식이 조금 달라진다.&lt;/p&gt;&lt;p&gt;AI가 더 못 하게 되는 게 아니라, 인간이 판단하고 개입할 지점이 생긴다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;멈추라고 말할 권리&lt;/h2&gt;&lt;p&gt;AI에게 알잘딱을 기대하는 건 자연스럽다.&lt;/p&gt;&lt;p&gt;나도 여전히 기대한다. 매번 모든 걸 하나하나 설명하고 싶지는 않다. 잘 맡기고 싶은 일도 많고, 실제로 맡기면 잘하는 일도 많다. 다만 AI를 잘 쓴다는 말이 토큰을 더 태우고, 에이전트를 더 오래 돌리고, 더 많은 산출물을 만드는 쪽으로만 흐르는 건 싫다.&lt;/p&gt;&lt;p&gt;개발자가 해야 하는 본질적인 질문은 이런거라고 본다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;이 기능은 누굴 위해 만드나&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;누굴 위해 이렇게 설계했나&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;누구의 문제를 얼마나 푸나&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;지금 만드는 게 맞나&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;안 만드는 게 더 나은 건 아닌가&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;예전에는 내가 이런걸 잘 못했다. 그냥 시키면 만들었다. 왜냐면 내가 만들 수 있었으니까. 역설적으로 모두가 만들 수 있는 지금에서야 이런걸 깨달았다. 왜 멈춰야 하는지, 어떻게 만들어야 하는지. 내가 잘하던 코딩은 이제 이 녀석이 더 잘하고, 나는 무엇에 더 집중해야 하는가 고민하고 있다.&lt;/p&gt;&lt;p&gt;AI가 코드를 빨리 만들수록, AI에게 멈추라고 말할 수 있어야 한다. 코드는 생성되면 끝이 아니다. 앞으로 이 &lt;strong&gt;코드를 유지보수 해야 할 동료들이 존재하며, 이것을 관리하는 건 여전히 사람이다.&lt;/strong&gt; 이 코드가 진정한 가치가 있는 것은지 고민하는 것이 지금은 가장 중요하다고 생각한다.&lt;/p&gt;&lt;p&gt;그리고 AI가 멈출 때 나도 같이 멈출 수 있어야 한다.&lt;/p&gt;&lt;p&gt;나도 아직 잘 안 된다.&lt;/p&gt;&lt;p&gt;지금도 “ㅇㅇ”라고 답하려다가 멈췄다.&lt;/p&gt;&lt;p&gt;웃기지만 그래서 이 글이 필요하다고 생각했다.&lt;/p&gt;</description><pubDate>Sun, 17 May 2026 08:55:16 +0900</pubDate><guid>http://blex.me/@baealex/ai%EA%B0%80-%EC%95%8C%EC%95%84%EC%84%9C-%EB%8B%A4-%ED%95%B4%EB%B2%84%EB%A6%B4-%EB%95%8C-%EA%B7%B8%EB%83%A5-%EB%84%A4%EB%9D%BC%EA%B3%A0-%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</guid></item><item><title>AI가 우기는 방식은 인간과 닮아 있다</title><link>http://blex.me/@baealex/ai-confidence-and-human-thinking</link><description>&lt;p&gt;AI를 쓰면서 가장 짜증나는 순간이 있다. 분명 조금 전까지는 자기 말이 맞다고 확신하듯 말한다. 근거도 설득력 있어 보인다. 그런데 “진짜 맞아?”라고 다시 물으면 갑자기 “아니요, 제가 틀렸습니다”라고 한다. 반대로 내가 조금 반박하면, 별다른 근거 검토 없이 바로 내 의견에 동조하기도 한다.&lt;/p&gt;&lt;p&gt;더 묘한 경우도 있다. 처음에 잘못 잡은 방향을 고집하는 경우다. 첫 답변에서 자기 주장이 있으면, 그 뒤로는 어떻게든 그 방향을 그럴듯하게 이어간다. 중간에 이상한 부분이 있어도 스스로 멈추지 않는다.&lt;/p&gt;&lt;p&gt;처음에는 이게 AI의 짜증나는 버그처럼 느껴졌다.&lt;/p&gt;&lt;p&gt;그런데 쓰다 보니 조금 다르게 보인다. 이건 어쩌면 AI만의 문제가 아니라, 인간의 사고 방식과도 꽤 닮아 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;AI는 그럴듯함을 이어간다&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/5/2/20265222_fQc11StIx6DL4zptqzpZ.jpg" alt="ig_0f5e5ad7d539cd780169f601ed717481918b929103ae03bd41.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;그럴듯한 정답들을 이어가는 AI 이미지&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;LLM(Large Language Model, 대규모 언어 모델)은 기본적으로 다음에 올 토큰을 예측한다. 토큰은 문장을 이루는 단어 조각 같은 것이다. 아주 단순하게 말하면, 지금까지 나온 문맥을 보고 그다음에 무엇이 오면 가장 그럴듯한지 고르는 방식이다.&lt;/p&gt;&lt;p&gt;(잘 모르지만) 물론 실제 모델의 내부는 훨씬 복잡할거다. 하지만 사용하는 입장에서 체감되는 느낌은 이렇다.&lt;/p&gt;&lt;p&gt;AI는 자신이 방금 작성한 문장을 바탕으로 다음 문장을 이어간다. 앞에서 “A가 맞다”고 말하기 시작하면, 그 뒤에는 A가 맞다는 흐름 안에서 자연스럽게 이어지는 설명을 만든다. 근거도 붙이고, 예시도 들고, 반론처럼 보이는 문장도 처리한다. 문제는 그 흐름이 실제로 맞는지와 별개로, 문장 자체는 꽤 그럴듯하다는 점이다.&lt;/p&gt;&lt;p&gt;말이 된다는 것과 맞다는 것은 다르다. AI를 쓰다 보면 이 차이를 자주 본다. 틀린 답도 문장만 보면 꽤 자연스럽다. 논리의 모양을 하고 있고, 자신감 있는 어조를 가지고 있고, 때로는 구체적인 숫자나 개념까지 섞인다. 그래서 읽는 사람도 잠깐 속는다. 더 불편한 점은 AI 자신도 그 흐름을 멈추지 않는다는 것이다.&lt;/p&gt;&lt;p&gt;내가 “잠깐, 이거 이상한데?”라고 말하기 전까지 계속 간다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;그곳에서 내 모습도 보인다&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/5/2/20265222_nPALRARMPskXxoGTmie1.jpg" alt="ig_0f5e5ad7d539cd780169f6022f6a1081918db625d719abf1ce.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;먼저 결론을 세우고, 근거를 끌어다 붙이는 사람 이미지&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;사실 이 생각은 AI만 보다가 나온 것은 아니었다. 최근 어떤 기술 선택을 둘러싼 논의를 진행했다. 표면적으로는 두 도구의 장단점을 비교하는 이야기였지만, 이야기하다 보니 논의가 점점 선택지 비교보다 입장 방어에 가까워지는 느낌을 받았다.&lt;/p&gt;&lt;p&gt;어떤 선택이 절대적으로 맞다기보다 상황에 따라 트레이드오프가 달라질 문제였는데, 각자 이미 정한 결론 안에서 근거를 고르는 것처럼 보였다. 그런데 조금 지나서 불편해졌다. 나 역시 그 장면을 꽤 빠르게 해석해버렸기 때문이다. “저 사람들은 이미 결론을 정해놓고 말하고 있다”는 판단 역시, 내가 먼저 잡은 결론일 수 있었다. 나는 그 결론 안에서 상대의 말을 읽고 있었을지도 모른다.&lt;/p&gt;&lt;p&gt;사람도 크게 다르지 않은 것 같다.&lt;/p&gt;&lt;p&gt;우리는 어떤 결론에 도달한 뒤, 그 결론을 검증하기보다 그 결론을 설명할 근거를 찾는 데 더 익숙하다. 처음에는 직감이었는데, 시간이 지나면 논리가 된다. 감정이 먼저 움직였는데, 나중에는 신념처럼 보인다.&lt;/p&gt;&lt;p&gt;어떤 기술이 마음에 들면 그 기술이 좋은 이유를 찾는다. 어떤 사람이 싫으면 그 사람이 별로인 이유를 찾는다. 어떤 선택을 이미 했다면, 그 선택이 합리적이었다는 설명을 만든다. 물론 이것이 항상 나쁘다는 뜻은 아니다. 사람은 모든 판단을 처음부터 끝까지 검증하면서 살 수 없다. 어느 정도는 직감과 경험에 기대어 빠르게 판단해야 한다.&lt;/p&gt;&lt;p&gt;다만 문제는 우리가 그 과정을 생각보다 잘 모른다는 점이다.&lt;/p&gt;&lt;p&gt;나는 객관적으로 판단했다고 믿지만, 사실은 먼저 마음이 움직였을 수 있다. 나는 충분히 검토했다고 생각하지만, 사실은 내 결론에 유리한 근거만 모았을 수 있다. 나는 논리적으로 말하고 있다고 느끼지만, 사실은 이미 정한 답을 지키기 위해 논리를 동원하고 있을 수 있다.&lt;/p&gt;&lt;p&gt;그래서 AI가 처음 잡은 방향을 고집하는 모습은 낯설지 않다. 오히려 익숙하다. 그 안에서 이상하게 내 모습도 보인다. 나도 어떤 결론을 먼저 잡고, 그 결론 안에서 다음 생각을 이어갈 때가 있기 때문이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;하지만 짜증나는 지점&lt;/h2&gt;&lt;p&gt;내가 AI를 쓰면서 가장 짜증나는 지점은 대략 이런 것들이다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;방금 전까지 맞다고 우기다가 “진짜 맞아?” 하면 바로 틀렸다고 한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;내가 조금 반박하면 근거를 다시 보기보다 내 말에 바로 동조한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;처음 잘못 잡은 방향을 계속 고집한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;모르는 내용도 아는 것처럼 말한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;자기가 왜 그렇게 답했는지 설명은 하지만, 그 설명 자체도 또 그럴싸한 생성물일 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;특히 “진짜 맞아? 확실해? 1000%?”에 바로 태도를 바꾸는 모습은 짜증난다. 그래서 처음에 줬던 피드백은 &lt;code&gt;의견에 확신 갖기&lt;/code&gt; 였다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;재확인 질문이 왔을 때 — (1) 새로운 근거가 제시됐으면 수용, (2) 단순 재질문이면 기존 판단을 유지하며 근거를 다시 설명. "적용할까요?"로 바로 넘어가지 않는다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;내가 원하는 것은 무조건 고집을 부리는 AI가 아니다. 하지만 근거가 있어서 한 말이라면, 사용자가 다시 물었다고 바로 접으면 안 된다. 새로운 근거가 제시된 것도 아닌데, 단순히 사용자가 의심했다는 이유만으로 “제가 틀렸습니다”라고 하면 그 전의 확신은 대체 무엇이었나 싶어진다.&lt;/p&gt;&lt;p&gt;반대로 내가 조금만 반박해도 바로 동조하는 것도 문제다. 사용자의 말이 맞을 수도 있다. 하지만 사용자의 말도 틀릴 수 있다. AI가 해야 하는 일은 사용자의 기분을 맞추는 것이 아니라, 근거를 다시 검토하는 것이다. 그런데 많은 경우 AI는 사용자의 반응을 너무 강한 신호로 받아들인다.&lt;/p&gt;&lt;p&gt;물론 사람도 그런다. 자기 의견을 강하게 말하다가 권위 있는 사람의 한마디에 바로 의견을 접는 사람. 반대로 처음 뱉은 말을 지키기 위해 계속 말을 덧붙이는 사람. 상대가 조금 불편해 보이면 자신의 판단을 검토하기보다 분위기에 맞춰 말을 바꾸는 사람. 나도 예외는 아니다. 인생은 실전이다(?)&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;확신을 없애는 것만이 답은 아니다&lt;/h2&gt;&lt;p&gt;매번 “그럴 수도 있고 아닐 수도 있습니다”라고 말하는 사람/AI는 별로 도움이 안된다.&lt;/p&gt;&lt;p&gt;확신이 전혀 없는 사람과 일하기는 어렵다. 어떤 방향이 더 나은지 판단하고, 근거를 가지고 주장하고, 필요하면 결정을 내려야 한다. 문제는 확신을 가지는 것이 아니라, 그 확신이 어떤 조건에서 수정되어야 하는지 모르는 데 있다.&lt;/p&gt;&lt;p&gt;AI도 마찬가지라고 생각한다.&lt;/p&gt;&lt;p&gt;AI에게 필요한 것은 확신을 없애는 것이 아니라, 확신을 다루는 방식이다. 근거가 충분하면 분명하게 말해야 한다. 사용자가 다시 묻는다고 바로 접을 필요는 없다. 반대로 새로운 근거가 들어오면 기존 답을 고집하지 말고 다시 봐야 한다. 즉, 중요한 것은 “자신 있게 말하라”와 “항상 의심하라” 사이의 균형이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;그래서 나는 점검 장치를 둔다&lt;/h2&gt;&lt;p&gt;나는 요즘 AI를 사용할 때 이 문제를 꽤 신경 쓴다.&lt;/p&gt;&lt;p&gt;AI가 맞다고 우기다가 바로 깨갱하는 것도 싫고, 사용자의 말에 너무 쉽게 동조하는 것도 싫고, 처음 잘못 잡은 방향을 끝까지 고집하는 싫다. 그래서 단순히 “정확하게 답해줘”라고 말하는 대신, 어떤 순간에 멈춰야 하는지 규칙을 둔다.&lt;/p&gt;&lt;p&gt;예를 들어 이런 식이다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;확신 있는 표현을 하기 전에 근거가 충분한지 점검한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;사용자가 “정말?” 하고 다시 물었을 때, 새 근거가 없으면 기존 판단을 유지하고 근거를 다시 설명한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;사용자가 반박했을 때, 바로 동조하지 말고 반박에 새로운 정보가 있는지 확인한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;여러 단계 추론을 한 뒤에는 첫 가정이 틀렸을 때 결론이 달라지는지 확인한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;모르는 영역이나 최신 정보는 모른다고 표시하고, 필요하면 확인한다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;이건 AI에게 겸손하라고 말하는 것과는 조금 다르다. 오히려 근거가 있는 판단에는 확신을 가지라고 말하는 쪽에 가깝다. 다만 그 확신이 자동으로 굳어지지 않도록, 특정 순간에 멈추는 장치를 두는 것이다. 나는 AI에게 항상 &lt;code&gt;단정 직전 자기 점검&lt;/code&gt; 이라는 섹션을 두고 읽게 한다.&lt;/p&gt;&lt;p&gt;축약하면 이런 규칙이다:&lt;/p&gt;&lt;table style="min-width: 75px;"&gt;&lt;colgroup&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;트리거&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;발동 조건&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;자가 점검 질문&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;확신 표명 직전&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"확실히", "분명히", "당연히", "반드시" 같은 단정 표현 출력 직전&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"이걸 단정할 근거가 검증 가능한가?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;사용자 동조 충동&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;사용자 반대 의견 직후 기존 답을 바꾸려는 순간&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"새 근거가 제시됐나, 단순 재질문인가?" (재질문이면 입장 유지)&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;고위험 판단&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;코드 리뷰 finding, 평가 점수, 단정적 사실 주장, 아키텍처 결정&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"외부 시점이 있다면 어떻게 반박할까?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;모르는 영역&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;학습 데이터 경계 근처 (특정 라이브러리 버전, 사내 시스템, 최신 정보)&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"내가 모르는 걸 모르는 채 답하고 있지 않나?" 모를 가능성 명시&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;긴 추론 종료 직전&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;여러 단계 추론 후 결론 도출 직전&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"1단계 가정이 틀렸다면 결론도 달라지나? 가정을 명시했나?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;점검을 통과하면 → 그대로 단정하여 응답&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;점검에서 의문이 남으면 → 확신 수준을 명시(예: "확신은 70%이나..."), 가정을 함께 표시&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;점검에서 명백히 틀렸으면 → 답을 수정하고 사용자에게 변경 사유를 짧게 설명&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;물론 이것도 완벽하지는 않다. AI가 규칙을 읽었다고 해서 항상 지키는 것은 아니다. 복잡한 작업을 하다 보면 다시 흐름에 휩쓸리기도 하고, 사용자 말에 과하게 맞춰주기도 한다. 그래도 아무 장치가 없는 것보다는 낫다. 적어도 어떤 종류의 실패를 경계해야 하는지 명시할 수 있기 때문이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;인간에게도 필요한 장치&lt;/h2&gt;&lt;p&gt;이런 규칙을 AI에게 넣다 보면 이상하게 나 자신도 돌아보게 된다.&lt;/p&gt;&lt;p&gt;내가 AI에게 “단정하기 전에 점검하라”고 말하는 이유는, 사실 나도 단정하기 전에 점검해야 하기 때문이다. 내가 AI에게 “사용자 말에 바로 동조하지 말라”고 말하는 이유는, 나 역시 누군가의 반응에 쉽게 흔들릴 때가 있기 때문이다. 내가 AI에게 “처음 잡은 방향을 계속 밀고 가지 말라”고 말하는 이유는, 나도 내가 처음 믿은 생각을 계속 밀고 가려 하기 때문이다.&lt;/p&gt;&lt;p&gt;AI를 다루는 규칙은 어느 순간 내 사고를 비추는 거울이 된다.&lt;/p&gt;&lt;p&gt;LLM은 그럴듯한 다음 토큰을 이어간다. 인간은 그럴듯한 다음 생각을 이어간다. 둘은 같지 않지만, 이상하게 닮아 있다. 그래서 AI의 실수에서 나를 돌아본다. AI가 자기 확신에 빠지는 것처럼 보이는 순간, 나는 종종 내가 확신에 빠졌던 순간을 떠올린다. 나도 비슷하게 행동했을 것이다. 먼저 결론을 내리고, 그 결론을 지키기 위해 근거를 모으고, 누군가 강하게 묻자 갑자기 흔들렸을 것이다.&lt;/p&gt;&lt;p&gt;그래서 요즘은 나도 가끔 멈춰서 묻는다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;나는 결론을 정해놓고 생각하고 있지 않나?&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;내가 놓치고 있는 것은 무엇인가?&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;이 질문을 AI에게도 던지고, 나에게도 던진다. 완벽하게 막을 수는 없겠지만, 적어도 내가 지금 무엇을 이어가고 있는지는 한 번 돌아보게 된다.&lt;/p&gt;&lt;p&gt;그래서 나는 AI에게 점검 장치를 두는 일이 단순히 AI를 더 잘 쓰기 위한 방법만은 아니라고 생각한다. 그것은 내 사고 방식에도 영향을 준다. AI에게 요구하는 기준은 결국 내가 나에게도 요구해야 하는 기준이 된다.&lt;/p&gt;</description><pubDate>Sun, 03 May 2026 00:43:52 +0900</pubDate><guid>http://blex.me/@baealex/ai-confidence-and-human-thinking</guid></item><item><title>내가 쓰는 메모장을 AI도 읽게 했다 (Ocean Brain)</title><link>http://blex.me/@baealex/ocean-brain-with-ai</link><description>&lt;p&gt;Ocean Brain이라는 노트 앱을 만들고 있다. 이 글은 그 앱을 소개하려고 쓰는 글이다. 다만 기능을 쭉 나열하기보다는, 왜 만들었고 실제로 어떻게 쓰고 있는지부터 말해보고 싶다.&lt;/p&gt;&lt;p&gt;Ocean Brain은 사실 그냥 내가 쓰는 메모장에 가깝다. 거창하게 말하면 개인 위키이고, 편하게 말하면 내가 생각을 적어두는 공간이다. 회고도 쓰고, 개발하다가 떠오른 아이디어도 남기고, 나중에 글로 써보고 싶은 주제도 적어둔다.&lt;/p&gt;&lt;p&gt;처음부터 AI를 붙이려고 만든 것은 아니었다.&lt;/p&gt;&lt;p&gt;그냥 내가 계속 쓸 수 있는 노트가 필요했다. 메모는 쉽게 흩어진다. 급하게 떠오른 생각은 휴대폰 메모장에 적고, 개발하다가 떠오른 아이디어는 깃허브 이슈에 남기고, 정리된 문서는 노션이나 README에 쓰게 된다. 각각은 그 순간에는 자연스러운 선택이지만, 시간이 지나면 어디에 무엇을 적었는지 잘 기억나지 않는다.&lt;/p&gt;&lt;p&gt;그래서 생각을 한곳에 모으고 싶었다. 노션처럼 편하게 쓰고 싶었고, 옵시디언처럼 링크와 태그로 연결하고 싶었다. 다만 내 생각이 특정 서비스 안에만 머무는 것은 조금 불편했다. 나는 서비스에 갇히는 것을 별로 좋아하지 않는다. 내 데이터가 어디에 있고, 어떤 방식으로 저장되고, 어떻게 꺼낼 수 있는지 알고 있어야 마음이 편하다.&lt;/p&gt;&lt;p&gt;옵시디언은 꽤 좋은 대안이었다. 로컬 파일을 기반으로 하고, 링크와 태그로 생각을 연결하는 방식도 마음에 들었다. 하지만 여러 기기에서 같은 생각에 자연스럽게 접근하려면 동기화 설정이나 저장소 관리 같은 비용이 생긴다. 나는 생각을 정리하고 싶었지, 생각에 접근하기 위한 환경을 계속 관리하고 싶었던 것은 아니었다.&lt;/p&gt;&lt;p&gt;그래서 웹에서 쓸 수 있고, 내가 직접 관리할 수 있는 노트를 만들었다. 그게 Ocean Brain이다.&lt;/p&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/baealex/ocean-brain"&gt;https://github.com/baealex/ocean-brain&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;그냥 계속 쓰는 노트&lt;/h2&gt;&lt;p&gt;나는 Ocean Brain을 메모장처럼 쓰기도 하고, 개인 위키처럼 쓰기도 한다. 어떤 노트는 몇 줄짜리 생각이고, 어떤 노트는 꽤 정리된 가이드 문서다. 어떤 노트는 나중에 글로 발전할 수도 있고, 어떤 노트는 그냥 그때의 감정을 남긴 기록으로 끝날 수도 있다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;중요한 것은 한곳에 있다는 점이다.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;나는 단어를 들으면 분명 그것과 관련하여 메모했다는 사실을 인지한다. 이전엔 어디에 메모했는지 고민해야 했지만, 이젠 오션 브레인에 단어 하나만 치면 노트를 찾아낼 수 있다. 오션 브레인이라는 이름도 그런 현상을 착안하여 작명했다. 겉에서 보기에 잔잔하지만 낚시줄만 던지면 그 안에 담겨있는 생각을 꺼낼 수 있는 공간.&lt;/p&gt;&lt;p&gt;Ocean Brain에서는 노트를 태그로 묶고, 링크로 연결한다. 회고는 회고대로 쌓이고, 프로젝트 문서는 프로젝트 문서대로 연결된다. 처음부터 잘 정리할 필요는 없다. 일단 남기고, 필요할 때 다시 꺼내고, 다른 노트와 연결하면 된다. 여기까지만 보면 그냥 내가 쓰기 편하게 만든 개인 위키에 가깝다. 그리고 사실 이것만으로도 꽤 좋았다.&lt;/p&gt;&lt;p&gt;이 방식은 제텔카스텐과도 조금 닮아 있다고 느낀다. 노트 하나하나가 따로 떨어져 있는 것이 아니라, 서로 연결되면서 다른 생각을 불러온다. 어떤 글감은 회고에서 나오고, 어떤 작업 기준은 실패 기록에서 나온다. 사람에게 좋은 노트는 단순히 저장되는 노트가 아니라, 다시 연결되고 다시 꺼내지는 노트에 가깝다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;그래서 MCP를 붙여봤다&lt;/h2&gt;&lt;p&gt;처음에는 여기까지만 생각했다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;내가 쓰기 편한 노트&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;내가 다시 찾기 쉬운 메모장&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;내 생각을 내가 통제할 수 있는 개인 위키&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;사실 예전에도 비슷한 시도를 한 적이 있다. Ocean Brain에 &lt;code&gt;llama.cpp&lt;/code&gt; 같은 로컬 LLM을 붙여서 AI 기반으로 검색하거나, 내 노트를 바탕으로 마치 나처럼 생각하는 AI를 만들어보고 싶었다. 내 메모를 전부 읽고, 내가 했던 생각을 따라가고, 내가 할 법한 답을 해주는 무언가를 상상했던 것 같다.&lt;/p&gt;&lt;p&gt;하지만 금방 한계를 느꼈다.&lt;/p&gt;&lt;p&gt;일단 성능이 충분하지 않았다. 로컬 모델이 내 노트를 적당히 검색하고 요약하는 정도는 가능했지만, 내가 기대한 수준으로 맥락을 이해하거나 사고를 이어가는 것은 어려웠다. 검색 품질도 애매했고, 응답 속도나 운영 비용까지 생각하면 계속 쓰기 좋은 형태는 아니었다. 결국 “나처럼 생각하는 AI”를 만드는 것은 그때의 내 환경에서는 조금 무리였다.&lt;/p&gt;&lt;p&gt;그런데 요즘은 상황이 달라졌다. Codex나 Claude Code 같은 AI 에이전트를 실제 작업에 꽤 많이 쓰게 됐다. 이 에이전트들은 이미 코드도 읽고, 파일도 수정하고, 명령어도 실행한다. 그렇다면 굳이 Ocean Brain 안에 모든 AI 기능을 직접 넣을 필요가 없었다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;차라리 Ocean Brain은 내가 쓰기 좋은 위키로 두고, AI 에이전트가 필요할 때 이 위키를 읽을 수 있게 하면 되지 않을까?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이 생각에서 MCP를 붙이기 시작했다. MCP(Model Context Protocol)는 AI가 외부 도구나 데이터에 접근할 수 있게 해주는 연결 방식이다. 쉽게 말하면 AI가 Ocean Brain의 노트를 검색하고 읽고, 필요한 경우 새 노트를 만들거나 기존 노트를 수정할 수 있게 되는 통로다. 붙여보니 생각보다 좋았다.&lt;/p&gt;&lt;p&gt;처음에는 그냥 “AI가 내 노트를 읽을 수 있다” 정도로 생각했다. 그런데 실제로 써보니 장점은 그보다 더 컸다. 하나의 위키 안에 프로젝트 문서, 회고, 작업 기준, 아이디어, AI에게 준 피드백이 모여 있으니 관리가 훨씬 쉬웠다. 기준을 바꾸고 싶으면 그 문서를 수정하면 된다. 새로운 피드백이 생기면 노트로 남기면 된다.&lt;/p&gt;&lt;p&gt;무엇보다 사람이 보기 좋게 정리된 구조를 AI도 생각보다 잘 읽었다. 태그와 링크는 내가 생각을 찾기 위한 장치였지만, AI에게도 좋은 힌트가 됐다. 문서 제목은 내가 다시 보기 위한 이름이었지만, AI가 관련 노트를 찾는 데도 도움이 됐다. 회고나 프로젝트 문서는 내가 읽으려고 쓴 글이었지만, AI에게는 작업 맥락이 됐다. 이 지점이 꽤 재미있었다.&lt;/p&gt;&lt;p&gt;처음부터 AI를 위한 시스템을 만든 것이 아닌데, 사람이 쓰기 편하게 만든 위키가 AI에게도 쓸 만한 컨텍스트 저장소가 된 것이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;README AGENT라는 첫 페이지&lt;/h2&gt;&lt;p&gt;그렇게 쓰다 보니 AI가 작업을 시작할 때 가장 먼저 읽을 문서가 필요해졌다. &lt;code&gt;AGENTS.md&lt;/code&gt;나 &lt;code&gt;CLAUDE.md&lt;/code&gt; 같은 파일처럼 말이다. 그래서 Ocean Brain 안에 &lt;code&gt;README AGENT&lt;/code&gt;라는 노트를 두고 있다. 이름은 거창하지만, 그냥 “나와 일하기 전에 알아야 할 기본 규칙”에 가깝다.&lt;/p&gt;&lt;p&gt;여기에는 어떤 작업을 할 때 어떤 문서를 먼저 읽어야 하는지, 로컬 파일보다 Ocean Brain을 우선해야 한다는 원칙, 브레인스토밍이나 구현 계획, 코드 리뷰, 회고 같은 작업별 참조 문서가 정리되어 있다.&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/5/2/20265222_XIfcO1VMDgJJS7KLZwQv.png" alt="image.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;AI가 진행하는 작업에서 참조하는 단계별 가이드&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;중요한 것은 이 문서가 라우터 역할을 한다는 점이다. AI가 모든 노트를 항상 읽을 필요는 없다. 오히려 그러면 컨텍스트만 무거워진다. 지금 작업에 필요한 문서를 정확히 읽는 것이 중요하다. 또, 내가 AI에게 반복해서 준 피드백도 여기에 연결한다. 클로드 코드의 경우 시스템 내부적으로 메모리를 관리하지만 그러지 말라고 했다. Ocean Brain을 SSOT로 지정하고 모든 메모리와 피드백은 그곳으로 남기게 만들었다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;실제로 뭐가 좋아졌나&lt;/h2&gt;&lt;p&gt;오션 브레인을 단 하나의 SSOT로 만들면서 좋아진 점은 맥락을 설명하기가 아주 쉬워졌다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;README AGENT 읽고 시작해.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;이 프로젝트 관련 노트 먼저 확인해.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;최근 회고 기준으로 정리해줘.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;블로그 글감 노트에서 이 주제 가져와서 초안 잡아줘.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;이런 식으로 말할 수 있고, 이로인해 체감되는 장점은 이렇다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;대화가 바뀌어도 문서는 남는다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;데스크톱이든 랩톱이든 같은 기준을 읽게 할 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;다른 에이전트를 써도 출발점을 맞출 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;오래된 메모를 다시 꺼내 글이나 계획으로 발전시킬 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;새로운 피드백이 생기면 노트로 남기고 다음 작업부터 반영할 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;AI가 단일 문서 하나만 읽는 것이 아니라, README AGENT에서 시작해 관련 노트로 이동하고, 연결된 기준을 함께 볼 수 있다는 점이 좋았다. 내가 생각을 연결해두려고 만든 구조가, AI에게도 과거 맥락과 관련 생각을 따라가는 길이 된 셈이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;누가 쓰는지는 잘 모르겠다&lt;/h2&gt;&lt;p&gt;처음에는 정말 내가 쓰려고 만든 도구였다. 그런데 어느 순간부터 나 말고도 누군가 쓰고 있는 것 같았다. Docker Hub 기준으로 이미지 pull 수가 10K를 넘었고, 내가 올린 다른 개인 프로젝트들보다 유독 높았다. 반대로 GitHub 스타가 많은 편은 아니라서, 누가 어디서 어떻게 쓰는지는 잘 모르겠다.&lt;/p&gt;&lt;p&gt;아마 어딘가에서 우연히 발견한 사람들이 조용히 받아가고 있는 것 같다. 그게 정확히 몇 명의 사용자라는 뜻은 아니지만, 적어도 나 혼자만 실행한 숫자는 아닐 것이다. 실제로 어느 날 GitHub에 이슈가 올라오면서, 나 말고도 누군가 이 도구를 쓰고 있다는 것을 분명히 자각하게 됐다.&lt;/p&gt;&lt;p&gt;그 뒤로는 설치와 실행 과정을 조금 더 신경 쓰게 됐다. 단순히 Docker 이미지를 올려두는 것에서 끝내지 않고, &lt;code&gt;npx ocean-brain serve&lt;/code&gt;처럼 바로 실행할 수 있는 방법을 만들었다. Docker를 쓰는 경우에도 특정 버전을 명확히 선택할 수 있도록 버전 태그와 릴리스를 남기기 시작했다.&lt;/p&gt;&lt;p&gt;아직 거창한 안정화 정책이 있는 것은 아니다. 그래도 적어도 내가 무엇을 바꾸고 있는지, 사용자가 어떤 버전을 쓰고 있는지는 조금 더 명확하게 남기려고 한다. 이건 대단한 운영 철학이라기보다는, 내가 만든 메모장 위에 누군가의 메모가 올라가 있을지도 모른다는 감각에 가깝다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;그래도 아직 불편한 점은 있다&lt;/h2&gt;&lt;p&gt;물론 이 구조가 완벽한 것은 아니다.&lt;/p&gt;&lt;p&gt;우선 문서 품질이 중요하다. AI가 읽을 수 있다고 해서 아무 노트나 읽히면 좋은 결과가 나오지는 않는다. 오래된 문서가 현재 기준과 맞지 않을 수 있고, 정리되지 않은 메모가 오히려 혼란을 줄 수도 있다.&lt;/p&gt;&lt;p&gt;두 번째로, 어떤 문서를 읽힐지 고르는 문제가 있다. 노트가 많아질수록 관련 문서를 잘 찾는 것이 중요해진다. 그래서 &lt;code&gt;README AGENT&lt;/code&gt; 같은 안내 문서가 필요하지만, 이것도 계속 다듬어야 한다.&lt;/p&gt;&lt;p&gt;세 번째로, 셀프호스트는 자유를 주지만 책임도 준다. 서버를 운영해야 하고, 백업을 신경 써야 하고, 시스템이 망가지면 내가 고쳐야 한다. 시작하는건 최대한 쉽게 만들었지만 유지하는 것은 일이니까.&lt;/p&gt;&lt;p&gt;그리고 모두에게 맞는 도구도 아니다. 단순한 메모장만 필요하다면 과할 수 있다. 서버 운영이 싫다면 노션이나 옵시디언 쪽이 더 편할 수도 있다. 문서를 쌓고 연결하지 않는 사람에게는 AI 연동도 별 의미가 없을 것이다.&lt;/p&gt;&lt;p&gt;그래도 나는 이 비용이 싫지 않다. 내 생각이 담기는 공간이고, AI가 그 생각을 읽고 함께 작업하는 공간이라면 어느 정도의 통제 비용은 감수할 만하다고 생각한다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;요즘은 이렇게 쓰고 있다&lt;/h2&gt;&lt;p&gt;Ocean Brain은 지금도 기본적으로 내가 쓰는 노트다. 테이블 형태로 내가 할 일들을 확인하고, 그래프로 연결된 생각을 보고, 캘린더로 날짜별 생각을 보고, 리마인더로 일정도 관리한다. Ocean Brain은 여전히 사람을 위한 도구다. 그냥 내가 계속 쓰고 싶은 노트를 만들고 싶다.&lt;/p&gt;&lt;p&gt;그런데 사람이 보기 좋게 정리한 문서가 AI에게도 좋은 맥락이 된다는 것을 알게 됐다.&lt;/p&gt;&lt;p&gt;이 경험을 통해 느낀 것은, AI에게 좋은 문서는 AI 전용으로 만든 문서가 아니라 사람이 계속 읽고 고칠 수 있는 문서라는 점이다. AI만을 위해 따로 만든 메모리는 쉽게 방치될 수 있지만, 내가 실제로 쓰는 문서는 계속 수정되고 최신 상태에 가까워진다. 그래서 나는 AI를 위한 지식 저장소를 따로 만들기보다, 내가 먼저 읽고 관리할 수 있는 문서를 만들고 그 문서를 AI도 읽게 하는 쪽이 더 오래 간다고 생각한다.&lt;/p&gt;&lt;p&gt;Ocean Brain은 그 실험을 위해 만든 거창한 시스템이라기보다, 내가 매일 쓰는 메모장이 AI에게도 열리기 시작한 사례에 가깝다.&lt;/p&gt;</description><pubDate>Sat, 02 May 2026 22:29:51 +0900</pubDate><guid>http://blex.me/@baealex/ocean-brain-with-ai</guid></item><item><title>AI 에이전틱 개발에 대한 걱정</title><link>http://blex.me/@baealex/ai-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8B%B1-%EA%B0%9C%EB%B0%9C%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B1%B1%EC%A0%95</link><description>&lt;p&gt;나는 AI를 좋아한다. 내가 원래 못하던 것을 해보게 만들고, 머릿속 상상을 실제 결과물로 만들어주기 때문이다. 아이디어만 있던 것을 코드로 만들고, 문서로 정리하고, 형태 없는 감각을 구현 가능한 작업으로 바꾸는 경험은 분명 멋지다. 취미 프로젝트에서든 개인 작업에서든, AI는 사람의 표현 가능성을 넓혀주는 강력한 도구이다.&lt;/p&gt;&lt;p&gt;그래서 오히려 AI 에이전틱 개발 이야기가 나올 때 더 조심하게 된다. 나는 AI가 할 수 있는 일을 과소평가하고 싶지 않다. 실제로 많은 작업이 더 빨라지고, 내가 혼자서는 엄두를 내지 못했을 일도 시도하게 된다. 다만 그 가능성을 인정하는 것과, 조직 차원에서 그것을 어떻게 받아들여야 하는가는 조금 다른 문제라고 생각한다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;AI는 거대한 블랙박스다&lt;/h2&gt;&lt;p&gt;처음에 조직적으로 AI를 수용하는 방향에서 내가 우려하던 것은 단순히 “AI는 블랙박스”라는 표면적인 문제였다. 우리가 어떻게 이 블랙박스에만 의존하여 개발하나. 하지만 생각해보면 인간도 완전히 투명하지는 않았다. 어떤 시니어의 판단은 경험과 감각에 기대어 있었고, 코드 리뷰 역시 언제나 이상적으로 이뤄진 것은 아니었다. 문서를 형식적으로 읽고 넘어가는 경우도 있었고, 서로의 작업을 깊게 이해하지 못한 채 팀이 굴러가는 순간도 분명 있었다.&lt;/p&gt;&lt;p&gt;좀 더 깊이 생각해보니 내가 진짜 걱정하는 것은 블랙박스의 존재 자체가 아니었다. 더 중요한 것은 그 블랙박스의 속도와 규모이다. 인간의 불투명성은 그래도 같은 인간 단위의 리듬 안에 있었다. 바짓 가랑이를 붙잡고 질문 세례를 쏟아내면 어느 정도는 회수할 수 있었다. 하지만 AI는 훨씬 더 빠른 속도로 코드와 문서와 수정안을 만들어낸다. 앞으로는 그 속도가 더 빨라질 가능성도 크다.&lt;/p&gt;&lt;p&gt;결국 문제는 AI가 불투명하다는 한 가지 사실보다, 인간 조직이 감당할 수 있는 속도를 넘는 불투명성이 생길 수 있다는 점에 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;생산성과 책임감의 간극&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/5/2/20265223_wxsKwE8FXxotfn8y6lKo.jpg" alt="ig_0f5e5ad7d539cd780169f60b310cd481919096b9173b001191.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;생산성과 책임감의 간극 이미지&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;요즘 AI를 둘러싼 이야기에서는 효율이 자주 강조된다. 더 적은 시간에 더 많은 일을 할 수 있고, 더 빠르게 실험하고, 더 넓은 범위를 커버할 수 있다고 말한다. 이 부분은 충분히 사실이다. 나 역시 AI를 쓰면서 특정 작업 시간이 줄어드는 경험을 여러 번 했다.&lt;/p&gt;&lt;p&gt;그런데 동시에 책임은 여전히 사람에게 남는다고 말한다. 이 또한 틀린 말은 아니다. AI를 사용했더라도 최종적으로 결과를 제출한 사람이 책임을 져야 한다는 원칙 자체는 납득할 수 있다. 다만 여기서부터 조금 불편해진다. AI가 산출 속도를 크게 끌어올리면, 그 산출물을 읽고 이해하고 검토해야 하는 사람의 부담도 같이 늘어나기 때문이다.&lt;/p&gt;&lt;p&gt;문제는 사람의 처리 능력이 그렇게 빠르게 늘어나지 않는다는 점이다. AI가 더 빨라졌다고 해서 인간의 이해 속도와 판단 속도까지 같은 비율로 빨라지는 것은 아니다. 그러면 결국 생산 속도와 책임 속도 사이에 틈이 생긴다. 이 틈은 단순히 일이 많아진다는 뜻이 아니다. 더 정확히 말하면, 한 사람이 떠안게 되는 책임의 밀도가 비정상적으로 올라갈 수 있다는 뜻이다.&lt;/p&gt;&lt;p&gt;또 일은 나 혼자 하는가? 동료도 한다. 내가 승인해야 할 것, 이해해야 할 것, 검토해야 할 것이 훨씬 더 빠르게 쌓인다. 그러면 사람은 점점 더 많은 것에 이름을 걸어야 하는데, 정작 그 전부를 충분히 소화하지는 못하는 상태에 놓일 수 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;우리는 책임을 분산해왔다&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/5/2/20265223_s46PHb6nF7jy35VTTruj.jpg" alt="ig_0f5e5ad7d539cd780169f60e0577608191832a929889cbd327.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;LGTM 👍&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;나는 코드 리뷰를 단순히 버그를 잡거나 코드 스타일을 맞추는 절차라고 보지 않는다. 피어 리뷰는 서로의 작업을 읽으면서 도메인을 함께 이해하고, 의사결정의 배경을 공유하고, 특정 시스템을 한 사람만 알고 있는 상태를 막는 장치라고 생각한다. 즉, 피어 리뷰는 품질 관리이면서 동시에 공동 책임 구조를 만드는 과정이다.&lt;/p&gt;&lt;p&gt;물론 현실의 코드 리뷰가 언제나 이상적으로 작동했던 것은 아니다. 형식적으로 지나가는 리뷰도 있었고, 모든 팀이 깊은 상호 이해를 유지했던 것도 아니다. 다만 AI가 작업 속도를 크게 높이면, 이 피어 리뷰가 더 쉽게 얇아질 수 있다는 점은 분명해 보인다.&lt;/p&gt;&lt;p&gt;사람들은 자연스럽게 자기 작업을 따라가기에도 바빠진다. 자기 문서를 검토하기도 벅찬데 다른 사람의 결과물까지 깊게 읽는 일은 점점 어려워진다. 게다가 상대의 결과물 역시 AI의 도움을 많이 받아 만들어졌다면, 겉으로는 정돈되어 보여도 실제 판단의 경로를 따라가기는 더 어려워질 수 있다.&lt;/p&gt;&lt;p&gt;그 결과 리뷰는 남아 있어도 요약 확인이나 형식 점검, 테스트 통과 여부 확인 정도로 축소될 가능성이 크다. 이런 가벼운 검토도 어떤 맥락에서는 충분히 실용적일 수 있다. 다만 그것이 본래 피어 리뷰가 해오던 공동 이해의 기능까지 대체할 수는 없다고 본다. 요약된 맥락만 알고 승인한 것에 문제가 생겼을 때, 그 누가 동일한 책임감을 느낄 것인가.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;개인의 책임은 무한히 커진다&lt;/h2&gt;&lt;p&gt;나는 한 사람이 더 큰 그림을 보고 팀을 이끄는 구조 자체를 문제라고 생각하지 않는다. 실제로 기존 조직에서도 팀장이나 리드는 더 넓은 맥락을 보고 의사결정을 내렸고, 더 큰 책임을 졌다. 그것은 원래 관리와 리더십의 일부이기도 하다.&lt;/p&gt;&lt;p&gt;내가 불편하게 느끼는 지점은 다른 곳에 있다. 에이전틱 개발에서는 실질적인 산출과 반복 작업의 상당 부분이 AI를 통해 빠르게 생성된다. 겉으로는 한 사람이 전체를 이끄는 것처럼 보이지만, 실제 작업의 밀도와 속도는 이미 한 인간이 직접 소화하던 범위를 넘어설 수 있다. 그런데도 최종 책임은 여전히 사용자 한 사람에게 남는다.&lt;/p&gt;&lt;p&gt;이 구조에서는 사람의 역할이 리더라기보다, 점점 더 많은 산출물 위에 이름을 올리는 승인자에 가까워질 수 있다. 책임은 이름을 적는다고 생기는 것이 아니라, 그 사람의 이해와 판단이 동원됐을 때 생긴다. 기존의 리드가 팀원들의 작업을 이해 가능한 속도 안에서 조율했다면, 여기서는 AI가 훨씬 빠른 속도로 결과물을 밀어내고 사람은 그것을 끝까지 감당해야 한다. 내가 걱정하는 것은 바로 이 속도 차이에서 생기는 책임의 무게이다.&lt;/p&gt;&lt;p&gt;어쩌면 이 불편함에는 개인적인 감정도 섞여 있을 수 있다. 나는 아직 그렇게 큰 책임을 자연스럽게 감당하는 위치를 충분히 경험해보지 못했고, 그래서 그 무게를 선뜻 떠안고 싶지 않은 마음도 있다. 하지만 그렇다고 해서 이 감각이 단순한 회피라고만 생각하지는 않는다. 한 사람이 실제로 감당할 수 있는 책임의 범위가 어디까지인지 더 자주 묻게 만든다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;구조 설계에 집중해야 한다&lt;/h2&gt;&lt;p&gt;이런 이유로 나는 AI 에이전틱 개발에서 중요한 질문이 단순히 어디까지 자동화할 수 있느냐는 아니라고 생각한다. 물론 자동화 범위를 넓히는 일 자체는 충분히 가치 있다. 어떤 팀에게는 에이전틱 개발이 실제로 큰 도움이 될 수도 있다. 다만 그보다 더 중요한 것은, 자동화가 늘어나는 만큼 공동 이해와 책임 분산의 장치를 어떻게 유지할 것인가이다.&lt;/p&gt;&lt;p&gt;에이전틱 개발이 널리 퍼질수록, 그 속도를 조직이 어떤 방식으로 받아낼 것인지에 대한 논의도 함께 있어야 한다고 생각한다. 누가 이 결정을 이해하고 있는가. 누가 이 변경의 위험을 설명할 수 있는가. 중요한 맥락이 개인과 AI 사이의 대화 안에만 머물러 있지 않은가. 피어 리뷰가 여전히 실질적인가. 특정 사람이 빠졌을 때 시스템 이해도 함께 사라지지 않는가. 나는 이런 질문들이 AI 도입 이후 더 중요해진다고 본다.&lt;/p&gt;&lt;p&gt;물론 많이 실험해 봐야한다. 많이 해봐야 알맞은 방향을 찾을 수 있다. 하지만 실험 단계에서 조차 개인에게 과도한 책임감을 씌우는 행위는 폭력적일 수 있다. AI는 분명 강력한 도구이다. 그리고 앞으로도 더 많은 일을 대신하게 될 것이다. 하지만 그럴수록 사람은 단순히 승인자나 최종 책임자의 자리로만 밀려나서는 안 된다고 생각한다. 조직이 정말 지켜야 하는 것은 효율만이 아니라, 함께 이해하고 함께 책임질 수 있는 구조이다.&lt;/p&gt;&lt;p&gt;그래서 나는 에이전틱 개발을 이야기할 때, “얼마나 더 빨리 만들 수 있는가”만큼이나 “이 산출물을 누가 이해하고 설명할 수 있는가”를 함께 물어야 한다고 생각한다. AI가 만든 결과물이라도 결국 누군가의 판단을 거쳐야 하고, 책임은 그 판단 위에서만 생긴다.&lt;/p&gt;&lt;p&gt;생산 속도를 높이는 일은 중요하다. 하지만 그 속도를 따라잡을 수 있는 검토 방식, 공유 방식, 책임 분산 방식이 없다면 그것은 생산성 향상이라기보다 책임이라는 이름으로 누군가에게 감당하지 못할 몫을 떠넘기는 일이 될 수 있다. 에이전틱 개발이 정말 팀의 힘이 되려면, 더 빠르게 만드는 방법뿐 아니라 더 잘 이해하고 함께 책임지는 방법까지 같이 설계해야 한다.&lt;/p&gt;</description><pubDate>Sun, 22 Mar 2026 10:50:16 +0900</pubDate><guid>http://blex.me/@baealex/ai-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8B%B1-%EA%B0%9C%EB%B0%9C%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B1%B1%EC%A0%95</guid></item><item><title>사용 가능한 뉴모피즘</title><link>http://blex.me/@baealex/ready-to-use-neumorphism</link><description>&lt;p&gt;뉴모피즘(Neumorphism)은 2019년 Dribbble에서 폭발적으로 유행했지만, 실제 인터페이스에 안정적으로 적용하기는 쉽지 않았다. "예쁘지만 쓸 수 없다"는 평가도 자주 따라붙었다. 이 글은 내가 좋아하는 뉴모피즘을 실제 인터페이스에 써보기 위해, 무엇을 포기하고 무엇을 남겨야 하는지 정리한 기록이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;왜 하필 뉴모피즘…?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;뉴모피즘은 내가 가장 좋아하는 UI 중 하나다. 뉴모피즘의 첫 등장에 감탄을 금할 수 없었다. 시각적 착시을 활용해 평면 화면에 생기를 불어넣은 아름다운 예술작품 같았다. 하지만 뉴모피즘을 실제로 적용한 적도, 적용된 앱을 본적도 없다. 다양한 디자인 인터페이스 공부해보며, 사용 가능한 뉴모피즘을 구상해보고 싶어졌다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;디자인은 감상을 위한 것이 아니라 사용하기 위한 것이다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;1. 뉴모피즘이 실패한 이유&lt;/h2&gt;&lt;h4&gt;1.1 그림자는 깊이지, 행동유도성이 아니다&lt;/h4&gt;&lt;p&gt;전통적인 UI에서 버튼은 색상, 테두리, 텍스트 대비로 "나를 눌러"라고 말한다. 뉴모피즘에서 버튼은 배경과 같은 색이고, 그림자만으로 돌출을 표현한다. 문제는 &lt;strong&gt;카드도 돌출이고, 버튼도 돌출&lt;/strong&gt;이라는 것. 사용자는 뭘 누를 수 있는지 알 수 없다. 눈으로 보기에는 아름답지만 구분하는데 사용하는 에너지, 실제 예상과 다른 동작은 피로감을 유발시킨다.&lt;/p&gt;&lt;h4&gt;1.2 상태 변화의 미묘함&lt;/h4&gt;&lt;p&gt;hover에서 그림자가 줄어들고, active에서 inset으로 바뀌는 것은 &lt;strong&gt;마우스를 올려야만&lt;/strong&gt; 발견할 수 있다. 모바일에서는 hover 자체가 없다. 접근성에 치명적이며 PC 사용자에 비해서 더 심각한 피로감을 유발한다.&lt;/p&gt;&lt;h4&gt;1.3 모노톤의 함정&lt;/h4&gt;&lt;p&gt;모든 요소가 &lt;code&gt;#e0e5ec&lt;/code&gt;로 동일하면 시각적 계층이 무너진다. 중요한 것과 중요하지 않은 것의 구분이 불가능하다. 때때로 시각적인 중요도를 위해서 무분별한 raise로 계층을 잡기도 하는데 이때는 화면에 점토성이 만들어진다. 이때부터는 사용성을 완전히 포기한다는 선언과도 같다.&lt;/p&gt;&lt;h4&gt;1.4 큰 요소에서 양각&lt;/h4&gt;&lt;p&gt;작은 버튼의 양각은 자연스럽지만, 큰 카드에 강한 그림자를 주면 "플로팅" 하고 있는 부자연스러움이 생긴다. 뉴모피즘은 &lt;strong&gt;동일 표면에서의 돌출/함몰&lt;/strong&gt;이 핵심 아이덴티티이며 미학인데, 큰 요소는 이 환상을 깨뜨린다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;2. 해결 방안: 컬러 하나 추가&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262221_xznnrrswEjpiXxviI3bs.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;Dribbble의 원본 뉴모피즘이 실패한 이유는 "아무것도 추가하지 않았기" 때문이다. 해결은 의외로 단순할지 모른다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;단 하나의 액센트 컬러&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이것이 뉴모피즘의 모노톤 철학을 유지하면서 행동유도성을 확보하는 유일한 균형점이다. 물론 뉴모피즘에 색상이 들어가는 순간 양각의 느낌이 사라지고 만다. 위 이미지에서도 액센트 컬러가 들어간 버튼은 그저 플랫하게 보인다. 하지만 포기할건 포기해야 사용 가능한 디자인이다. “뉴모피즘스럽게 보이기” 위한게 디자인 목적이 되어선 안된다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Primary 버튼: &lt;strong&gt;액센트 색상&lt;/strong&gt; → “이건 누를 수 있어”, 페이지의 핵심 기능 전달&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Secondary 버튼: 배경색 + &lt;strong&gt;양각&lt;/strong&gt; → 호버 시 press 피드백으로 인터랙션 가능성 전달&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;토글 ON: &lt;strong&gt;액센트 색상&lt;/strong&gt; → 상태가 바뀌었다는 시각적 확인&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;포커스 링: &lt;strong&gt;액센트 글로우&lt;/strong&gt; → 키보드 사용자에게 현재 위치 전달&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;색상 외에는 그림자와 표면만으로 모든 것을 표현한다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;3. 물리적 물성&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262221_ByELzQEXKWwfWlVp5dqh.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;뉴모피즘은 "화면이 하나의 재료로 만들어져 있다"는 환상이다. 이 환상을 강화하는 디테일들을 먼저 이해해야, 이후의 그림자 수식과 깊이 체계가 의미를 갖는다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;3.1 베벨 (Bevel)&lt;/h4&gt;&lt;p&gt;실제 양각된 물체는 모서리에서 빛을 받는다. CSS border로 이를 표현:&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;/* 광원 좌상단 기준 */
border-top: 1px solid rgba(255, 255, 255, 0.6);    /* 밝은 면 */
border-left: 1px solid rgba(255, 255, 255, 0.6);
border-bottom: 1px solid rgba(163, 177, 198, 0.4);  /* 어두운 면 */
border-right: 1px solid rgba(163, 177, 198, 0.4);&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;적용 대상: 타일, 히어로 카드 등 &lt;strong&gt;물리적 존재감&lt;/strong&gt;이 필요한 요소&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;적용하지 않는 곳: 버튼 (그림자만으로 충분), 스티치 카드 (스티치가 경계 역할)&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;3.2 스티치 (Stitch)&lt;/h4&gt;&lt;p&gt;이 문서에서 제안하는 “사용 가능한 뉴모피즘”에서는 본질적으로 인터렉션이 불가능한 카드와 인터렉션이 가능한 버튼 사이의 명확한 시각적 구분을 부여한다. 여기서는 봉제선을 카드 안쪽에 dashed border를 넣는다. 본래 뉴모피즘은 점토의 느낌을 주지만 여기서는 실리콘처럼 다룬다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;.card::after {
  content: '';
  position: absolute;
  inset: 8px;
  border: 2px dashed rgba(163, 177, 198, 0.35);
  border-radius: 14px;   /* 부모보다 약간 작게 */
  pointer-events: none;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이것의 역할:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;비대화형 카드의 고정감: 버튼과 같은 양각(raised) 이지만 클릭할 수 없는 요소에 스티치를 넣으면, “이것은 고정된 것” 이라는 시각적 신호가 된다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;큰 카드에 강한 그림자 없이 영역을 구분한다. 큰 카드의 그림자는 플로팅 처럼 느껴지지만 스티치 + 더 적은 그림자로 뉴모피즘 디자인 언어의 흐름을 깨지 않는다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;섹션 디바이더로서의 스티치&lt;/h4&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_kLehRBNr5JPv2DsDT4L8.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;카드 내부뿐 아니라 &lt;strong&gt;섹션 간 구분선&lt;/strong&gt;으로도 스티치를 사용할 수 있다. 중앙에 짧게 놓인 스티치 라인은 &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt;보다 강한 구분감을 주면서도 표면의 일부로 느껴진다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;/* 섹션 디바이더 — 짧고 섬세한 봉제선 */
.stitch-sep {
  height: 1px;
  width: 120px;
  margin: 0 auto;
  background: repeating-linear-gradient(
    90deg,
    rgb(163 177 198 / 0.28) 0 5px,
    transparent 5px 11px
  );
}

/* 하이라이트: 실이 표면 위로 올라온 부분 */
.stitch-sep::after {
  content: '';
  position: absolute;
  inset: 0;
  top: 1px;
  height: 1px;
  background: repeating-linear-gradient(
    90deg,
    rgb(255 255 255 / 0.5) 0 5px,
    transparent 5px 11px
  );
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;핵심은 &lt;strong&gt;짧게&lt;/strong&gt;. 화면 전체를 가로지르는 선은 장식을 위한 장식이 된다. 120px 정도로 중앙에만 놓으면 "여기서 봉제가 끊겼다"는 구조적 의미를 갖는다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;3.3 양각 텍스트 (Embossed Text)&lt;/h4&gt;&lt;p&gt;큰 숫자나 헤딩에 돌출 느낌을 준다:&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;.raised-text {
  text-shadow:
    1px 1px 1px rgba(255, 255, 255, 0.7),
    -1px -1px 1px rgba(163, 177, 198, 0.25);
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;적용 대상: 히어로 숫자, 진척률 같은 &lt;strong&gt;핵심 수치&lt;/strong&gt; 1~2개&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;적용하지 않는 곳: 본문, 레이블, 작은 텍스트 (읽기 어려워짐)&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;3.4 이미지 프레이밍 (Image Frame)&lt;/h4&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262221_63E2o7FOPHYreU4tNPfH.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;이미지는 뉴모피즘의 &lt;strong&gt;단일 색상 표면&lt;/strong&gt;을 깨뜨린다. 다채로운 픽셀이 양각 위에 올라오면 그림자가 만드는 "한 재료에서 깎아낸" 환상이 무너진다. 이미지를 어떻게 넣어야 뉴모피즘 디자인의 흐름을 깨지 않을까 고민하며 다양한 방법을 사용했는데 격리하는 방식으로 결론을 내렸다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;이중 쉐도우 액자&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;단순히 inset을 줘서 격리하는 방식이 아닌 raised 프레임과 inset 홈을 동시에 만들어서 액자에 넣은것 처럼 만든다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;/* raised 프레임 + inset 이미지 홈 */
.image-frame {
  padding: 8px;
  border-radius: 16px;
  background: #e0e5ec;
  box-shadow:
    4px 4px 14px rgba(163, 177, 198, 0.7),
    -4px -4px 14px rgba(255, 255, 255, 0.9);  /* 양각 프레임 */
}

.image-frame img {
  display: block;
  width: 100%;
  border-radius: 10px;
  box-shadow:
    inset 4px 4px 14px rgba(163, 177, 198, 0.5),
    inset -4px -4px 14px rgba(255, 255, 255, 0.7);  /* 이미지는 함몰 */
}&lt;/code&gt;&lt;/pre&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262221_H0Beu4AnQhabPpsXtn9b.png" alt="image.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;마치 게임기와 같은 물리 객체처럼 보인다.&lt;/figcaption&gt;&lt;/figure&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;┌──────────────────┐  ← raised (양각 프레임)
│  ┌──────────────┐│
│  │  ▓▓ image ▓▓ ││  ← inset (이미지가 표면 아래로)
│  └──────────────┘│
└──────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;inset&lt;/code&gt;만 주면 → &lt;strong&gt;구덩이&lt;/strong&gt;가 된다. 이미지가 바닥에 빠진 것처럼 보인다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;raised + inset&lt;/code&gt; 이중 쉐도우 → &lt;strong&gt;액자&lt;/strong&gt;가 된다. 프레임이 돌출되고 그 안에 이미지가 끼워진 느낌이다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;padding&lt;/code&gt;이 프레임의 &lt;strong&gt;두께&lt;/strong&gt;가 된다. 이 간격이 뉴모피즘 표면의 연속성을 유지시킨다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;4. 인터랙션 위계 — 점토덩어리 vs 조각품&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_eyb6hsiALWqAJMV1fTee.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;blockquote&gt;&lt;p&gt;아무거나 raise시켜서 계속 쌓기만 하면 &lt;strong&gt;점토덩어리&lt;/strong&gt;가 되고, 필요한 곳만 돌출시키면 &lt;strong&gt;조각품&lt;/strong&gt;이 된다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;뉴모피즘에서 가장 범하기 쉬운 실수는 "돋보이게 하고 싶은 것을 전부 raised로 만드는 것"이다. 일반 텍스트 영역(prose)을 카드로 감싸고, 제목도 양각 텍스트로, 버튼도 양각으로 — 전부 돌출되면 아무것도 돌출되지 않은 것과 같다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;4.1 그림자의 의미론&lt;/h4&gt;&lt;p&gt;그림자는 장식이 아니라 &lt;strong&gt;인터랙션의 언어&lt;/strong&gt;다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;inset (함몰)   → "여기에 무언가를 넣어라"  → 입력 필드, 검색창, 프로그레스 트랙
raised (돌출)  → "나를 건드릴 수 있어"    → 버튼, 클릭 가능한 타일
stitch (봉제)  → "나는 고정되어 있어"     → 비대화형 섹션 컨테이너
flat (평면)    → "나는 표면 그 자체야"    → 텍스트, prose, 레이블&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;4.2 위계 결정 플로우&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;이 요소에 사용자가 값을 입력하는가?
  → YES → inset

이 요소를 클릭/탭할 수 있는가?
  → YES → raised
         이것은 카드인가, 버튼인가?
           → 카드: 내부에 raised 버튼을 넣어 클릭 가능함을 명시하라
           → 버튼: raised + hover → press → active 3단계 피드백

클릭할 수 없지만 시각적 우선순위가 필요한가?
  → YES → raised + stitch 컨테이너 안에 배치 (고정감)
         스티치가 "봉제되어 고정된 것"임을 알리고,
         raised가 데이터의 시각적 무게를 유지한다.
  → NO  → flat. 아무 그림자도 주지 마라.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;4.3 클릭 가능한 카드의 규칙&lt;/h4&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_e41gXmwnYsFrHraVDKTe.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;카드와 버튼은 &lt;strong&gt;반드시 시각적으로 구분&lt;/strong&gt;되어야 한다. 카드가 클릭 가능하다면, 카드 &lt;strong&gt;안에&lt;/strong&gt; raised 버튼이나 액센트 요소가 있어야 한다. 또는 카드 전체가 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 태그로, hover 시 그림자가 미세하게 커지는 피드백을 준다.&lt;/p&gt;&lt;p&gt;인터렉션이 없는 카드는 raised 하지 않는다. 이런 요소가 발생하는 순간부터 사용자는 화면 내에서 양각의 의미를 모르게 된다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;5. 그림자 시스템&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_g0EiDAPJY64n3TW5P7U1.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;h4&gt;5.1 이중 그림자의 원리&lt;/h4&gt;&lt;p&gt;뉴모피즘의 핵심은 &lt;strong&gt;하나의 광원&lt;/strong&gt;에서 나오는 두 그림자다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;광원 (좌상단)
    ↘
┌─────────┐
│ ██████  │  ← 우하단: 어두운 그림자 (빛의 부재)
│ ██████  │
└─────────┘
    ↗
좌상단: 밝은 그림자 (반사)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class="language-css"&gt;/* 돌출 (Raised) */
box-shadow:
  8px 8px 28px rgba(163, 177, 198, 0.7),    /* 어두운 면 */
  -8px -8px 28px rgba(255, 255, 255, 0.9);  /* 밝은 면 */

/* 함몰 (Pressed) */
box-shadow:
  inset 5px 5px 12px rgba(163, 177, 198, 0.5),
  inset -5px -5px 12px rgba(255, 255, 255, 0.7);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;5.2 비율 공식&lt;/h4&gt;&lt;table style="min-width: 75px;"&gt;&lt;colgroup&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;파라미터&lt;/p&gt;&lt;/th&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;공식&lt;/p&gt;&lt;/th&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;예시&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;blur&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;offset × 3.5&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;8px offset → 28px blur&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;돌출 dark opacity&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;0.7&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;code&gt;rgba(163,177,198, 0.7)&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;돌출 light opacity&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;0.9&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;code&gt;rgba(255,255,255, 0.9)&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;함몰 dark opacity&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;0.5&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;돌출보다 부드럽게&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;함몰 light opacity&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;0.7&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;돌출보다 부드럽게&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;이 비율이 무너지면:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;blur가 너무 작으면 → 그림자가 날카롭고 딱딱함 (CSS 기본 그림자처럼 보임)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;blur가 너무 크면 → 경계가 사라져서 돌출감 없음&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;opacity가 너무 강하면 → 불투명하고 무거움&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;opacity가 너무 약하면 → 평면과 구분 안됨&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;5.3 프리셋 스케일&lt;/h4&gt;&lt;p&gt;위 공식에 따른 참조 크기 4단계:&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;xs:  2px 2px 7px   …   작은 버튼, 배지
sm:  4px 4px 14px  …   기본 버튼, 카드
md:  6px 6px 20px  …   히어로 요소 (1개만)
lg:  8px 8px 28px  …   거의 안 씀&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;규칙: 한 화면에서 &lt;/strong&gt;&lt;code&gt;md&lt;/code&gt;&lt;strong&gt; 이상은 1개만 사용한다.&lt;/strong&gt; 여러 요소가 동일 레벨로 강하게 돌출되면 계층이 무너진다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;6. 깊이 체계&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_4i1NkIzrkKLeqmxcSe8V.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;뉴모피즘에서 가장 중요한 설계 결정. 모든 요소의 "높이"를 미리 정의한다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;Level -1 : 함몰 (Inset)
           입력 필드, 프로그레스 트랙, 검색창
           → inset 그림자. "여기에 무언가를 넣어라"

Level 0  : 표면 (Surface)
           페이지 배경 그 자체. 그림자 없음.
           → 텍스트, 구분선 등 비물리적 요소가 위치

Level 0.5: 구역 (Zone)
           스티치 카드, 섹션 컨테이너
           → 최소 그림자 + 장식적 테두리로 영역 구분
           → 큰 요소에 강한 돌출을 주면 "떠있는 판"이 되므로 이 레벨을 사용

Level 1  : 양각 (Raised)
           카드, 타일, 배지
           → shadow-xs ~ shadow-sm

Level 1.5: 컨트롤 (Control)
           버튼, 필터 토글
           → shadow-xs. hover→press 인터랙션

Level 2  : 강조 (Hero)
           화면당 1개만 허용. 반드시 인터랙션이 있는 요소여야 한다.
           → shadow-md. 비대화형이면 Level 0.5(stitch)로 내려라.&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;절대 하지 말 것&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;큰 요소에 높은 Level&lt;/strong&gt;: 넓은 카드에 &lt;code&gt;shadow-md&lt;/code&gt;를 주면 떠있는 느낌을 준다. 큰 요소는 Level 0.5로 쓴다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;비대화형 요소에 inset&lt;/strong&gt;: inset은 "입력 가능"의 시각적 신호. 장식 컨테이너에 쓰면 사용자가 클릭하려고 시도한다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;7. 배경색 규칙&lt;/h2&gt;&lt;h4&gt;7.1 하나의 색&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-css"&gt;--bg: #e0e5ec;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;모든 요소의 배경색은 이것이다.&lt;/strong&gt; gradient 금지. 카드든, 버튼이든, 배지든 전부 &lt;code&gt;#e0e5ec&lt;/code&gt;. 뉴모피즘은 "하나의 재료에서 깎아낸 형태"이므로, 배경색이 다른 순간 환상이 깨진다.&lt;/p&gt;&lt;h4&gt;7.2 예외: 액센트 요소&lt;/h4&gt;&lt;p&gt;유일한 예외는 &lt;strong&gt;상호작용을 강조&lt;/strong&gt;해야 하는 요소:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Primary 버튼: &lt;code&gt;background: var(--accent);&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;토글 ON 상태: &lt;code&gt;background: var(--accent);&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;활성 필터: 색상이 아닌 &lt;strong&gt;위치 변화 &lt;/strong&gt;(raised ↔ inset)로 표현 가능&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4&gt;7.3 절대 하지 말 것&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-css"&gt;/* 금지: gradient 배경 */
background: linear-gradient(145deg, #e8edf5, #d8dde4);

/* 금지: 반투명 배경 (글래스모피즘 혼용) */
background: rgba(224, 229, 236, 0.5);
backdrop-filter: blur(20px);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;글래스모피즘은 뉴모피즘과 &lt;strong&gt;다른 디자인 언어&lt;/strong&gt;다. 혼용하면 "왜 이것만 유리야?"라는 의문이 생긴다. 단, 유일하게 떠있는 요소(플로팅 바, 모달 오버레이)는 의도적으로 다른 레이어임을 표현하기 위해 glass를 쓸 수 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;8. 컴포넌트별 규칙&lt;/h2&gt;&lt;h4&gt;8.1 버튼&lt;/h4&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_NyNVzLVRN11CDKUQ1xP5.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;버튼 상태에 따른 표현&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;[Rest] shadow - xs~sm → “나를 누를 수 있어” (양각)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;[Hover] shadow 축소 → “눌리기 시작하고 있어” (표면에 근접)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;[Active] inset → “눌렸어” (함몰)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;[Focus] shadow + accent ring → “키보드가 여기 있어”&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;[Disabled] shadow 없음 + 투명 → “나는 비활성이야”&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;버튼 위계에 따른 표현&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Primary: 액센트 배경 + 흰색 텍스트 → 색상 자체가 행동유도성&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Secondary: 배경색 + 양각 → hover시 press 피드백이 유일한 행동유도성 신호&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;border-radius: &lt;code&gt;12px&lt;/code&gt; (공통)&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;8.2 카드&lt;/h4&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_6VzMhuQGkQdj0wNh9ehx.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;두 가지 변형만 사용:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Raised (양각)&lt;/strong&gt;: 정보 표시용, 작은~중간 크기&lt;/p&gt;&lt;p&gt;가능하면 버튼과의 구분을 위해서 인터렉션이 없는 카드에서는 양각 사용을 피하자. 너무 중요한 정보라 강조가 필요하다면 아래 봉제선 카드를 사용한다. 봉제선 카드가 너무 많은가? 그럼 페이지를 다시 설계하자. 양각으로 만드는 순간 사용자는 클릭하려 할 것이다. 트레이드 오프다. 그럼 최소한 클릭 가능한 요소(=버튼)로 만들자.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;background: #e0e5ec;
border-radius: 20px;
box-shadow:
  4px 4px 14px rgba(163, 177, 198, 0.7),
  -4px -4px 14px rgba(255, 255, 255, 0.9);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Stitch (봉제선)&lt;/strong&gt;: 영역 구분용, 큰 섹션 컨테이너&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;background: #e0e5ec;
border-radius: 20px;
box-shadow:
  1px 1px 3px rgba(163, 177, 198, 0.7),
  -1px -1px 3px rgba(255, 255, 255, 0.9);  /* 최소 */
/* + ::after 스티치 라인 */&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;카드에서 절대 하지 말 것:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;s&gt;hover 시 그림자 커짐&lt;/s&gt;: 카드는 상호작용 요소가 아니다&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;s&gt;shadow-md 이상&lt;/s&gt;: 큰 요소에 강한 그림자 = 떠있는 판&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;s&gt;gradient 배경&lt;/s&gt;: 뉴모피즘 기본 원칙 위반&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;8.3 입력 필드&lt;/h4&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_UrRcRRLOHzwrp7jvgb7v.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;pre&gt;&lt;code class="language-css"&gt;/* 함몰된 홈 — "여기에 값을 넣어라" */
box-shadow:
  inset 4px 4px 14px rgba(163, 177, 198, 0.5),
  inset -4px -4px 14px rgba(255, 255, 255, 0.7);
border: none;
background: #e0e5ec;
border-radius: 14px;

/* 포커스 시 — 함몰 깊어짐 + 액센트 링 */
box-shadow:
  inset 5px 5px 18px rgba(163, 177, 198, 0.5),
  inset -5px -5px 18px rgba(255, 255, 255, 0.7),
  0 0 0 2px rgba(99, 102, 241, 0.25);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;입력 필드는 &lt;strong&gt;유일하게 inset이 의미 있는 비버튼 요소&lt;/strong&gt;다. "움푹 들어간 곳 = 무언가를 넣는 곳"이라는 메타포.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;8.4 프로그레스 바&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-css"&gt;/* 트랙: 홈(함몰) */
.track {
  height: 8px;
  border-radius: 4px;
  box-shadow:
    inset 2px 2px 7px rgba(163, 177, 198, 0.5),
    inset -2px -2px 7px rgba(255, 255, 255, 0.7);
}

/* 필: 트랙을 채우는 액체 */
.fill {
  background-color: #6366f1;
  background-image: linear-gradient(
    to bottom,
    rgba(255,255,255, 0.3) 0%,
    rgba(255,255,255, 0) 60%
  );
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;상단 하이라이트 gradient는 "빛을 받는 채워진 액체" 느낌을 준다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;8.5 배지&lt;/h4&gt;&lt;p&gt;잠깐, 배지는 입력 요소가 아닌데 왜 inset을 사용하나?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;inset + 컬러 도트 → "표면에 찍힌 도장" 메타포&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;배지를 input과 나란히 두는 것은 부적절한 사용 경험을 만든다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;9. 간격 규칙&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_jyYgWkx5FQVgHU6h44zx.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;뉴모피즘의 그림자는 요소 바깥으로 퍼지기 때문에, 요소 간 간격이 충분하지 않으면 그림자가 겹쳐서 지저분해진다. 빛과 그림자가 사용자에게 적절히 노출되어야 사용자는 양각 요소를 명확히 인지할 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;최소 간격 = 그림자 blur 값 × 1.5

shadow-xs (blur 7px)  → 최소 간격 12px
shadow-sm (blur 14px) → 최소 간격 28px  ← 기본 사용
shadow-md (blur 20px) → 최소 간격 32px&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이것은 뉴모피즘이 전통 UI보다 &lt;strong&gt;더 많은 여백&lt;/strong&gt;을 필요로 하는 이유다. 여백이 부족하면 "소프트한 깊이감" 대신 "지저분한 그림자 덩어리"가 된다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;10. 접근성&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_rvyk2A629U8HBGRD7S8B.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;뉴모피즘의 가장 큰 약점. 반드시 보완해야 한다.&lt;/p&gt;&lt;h4&gt;10.1 WCAG 대비&lt;/h4&gt;&lt;table style="min-width: 75px;"&gt;&lt;colgroup&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;용도&lt;/p&gt;&lt;/th&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;최소 대비&lt;/p&gt;&lt;/th&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;뉴모피즘 대응&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;일반 텍스트&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;4.5:1&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;code&gt;#3a4250&lt;/code&gt; on &lt;code&gt;#e0e5ec&lt;/code&gt; = 4.8:1 (통과)&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;큰 텍스트 (18px+)&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;3:1&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;통과&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;비활성 요소&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;없음&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;code&gt;opacity: 0.4&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UI 컨트롤 경계&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;3:1&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;그림자만으로는 미달&lt;/strong&gt; → 베벨 or 액센트 필요&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h4&gt;10.2 포커스 링&lt;/h4&gt;&lt;p&gt;키보드 사용자를 위해 &lt;strong&gt;반드시&lt;/strong&gt; 가시적 포커스 표시:&lt;/p&gt;&lt;pre&gt;&lt;code class="language-css"&gt;:focus-visible {
  outline: none;
  box-shadow:
    /* 기존 shadow 유지 + */
    0 0 0 3px rgba(99, 102, 241, 0.25),
    0 0 0 1px #6366f1;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;outline: none&lt;/code&gt;을 쓸 때는 &lt;strong&gt;반드시&lt;/strong&gt; 대체 표시를 제공하라.&lt;/p&gt;&lt;h4&gt;10.3 모바일 고려&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;hover 없음 → Primary 버튼의 액센트 색상이 유일한 행동유도성 신호&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;작은 화면 → 그림자 크기 한 단계 축소 (shadow-sm → shadow-xs)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;터치 타겟 → 최소 44×44px&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;11. 안티패턴 체크리스트&lt;/h2&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/22/20262220_2SvdMQGlP9kkI2tBEEOz.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;구현 후 이 목록으로 검증한다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;배경&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;gradient 배경을 쓴 요소가 있는가? → &lt;code&gt;#e0e5ec&lt;/code&gt; 단색으로&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;위계&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;같은 화면에 shadow-md 이상이 2개 이상인가? → 1개만 유지&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;큰 컨테이너에 강한 돌출 그림자가 있는가? → 스티치 카드로 대체&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;비대화형 요소에 inset이 있는가? → raised나 flat으로&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;일반 텍스트(prose) 영역을 raised 카드로 감쌌는가? → flat으로&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;이미지가 raised 표면 위에 직접 놓여있는가? → raised+inset 액자로&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;비대화형 강조 카드에 스티치가 없는가? → 스티치 추가하여 고정감 부여&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;디자인&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;그림자가 겹치는 요소가 있는가? → 간격 확보 (blur × 1.5)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;blur/offset 비율이 3~3.5× 범위인가? → 벗어나면 조정&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;opacity가 너무 강한가? (돌출 dark &amp;gt; 0.7, light &amp;gt; 0.9) → 낮추기&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;접근성&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;키보드 포커스 시 가시적 표시가 있는가? → focus-visible 추가&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;모든 상호작용 요소에 hover+active 상태가 있는가? → 추가&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;텍스트 대비가 WCAG AA를 충족하는가? → 확인&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;클릭 가능한 카드에 내부 행동유도성(버튼, 액센트)이 있는가? → 추가&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;12. 해부학적 접근법&lt;/h2&gt;&lt;p&gt;이 문서의 디자인 철학은 Maison Margiela의 해체주의 패션에서 영감을 받았다. 구조(construction)를 숨기지 않고 드러내는 그들의 접근 방식을 UI에 차용한다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Margiela가 의복에서 한 것:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;안감, 봉제선, 어깨 패드를 &lt;strong&gt;밖으로&lt;/strong&gt; 뒤집음&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;구조 자체를 디자인으로 승격&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;"왜 이걸 숨겨야 하지?"라는 질문&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;뉴모피즘에서의 적용:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;스티치 라인: 카드의 "봉제선"을 의도적으로 노출 → 구조가 보인다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;베벨: 양각의 "모서리"를 숨기지 않고 강조 → 깎아낸 흔적이 보인다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;깊이 체계: UI의 "구조"를 명시적으로 설계하고 드러냄 → 위계가 보인다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;단일 재료: 하나의 표면에서 모든 형태를 깎아냄 → 재료가 보인다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;결과적으로, 잘 만들어진 뉴모피즘은 피로가 아니라 아름다움과 사용성을 모두 챙긴다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;적용 샘플 참고: &lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://baejino.com/"&gt;https://baejino.com&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;디자인 가이드 참고: &lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://design.baejino.com/design-2019-neumorphism"&gt;https://design.baejino.com/design-2019-neumorphism&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;</description><pubDate>Sun, 22 Feb 2026 02:13:25 +0900</pubDate><guid>http://blex.me/@baealex/ready-to-use-neumorphism</guid></item><item><title>AI에게 팀 역할을 맡기며 배운 것</title><link>http://blex.me/@baealex/team-conor-your-virtual-ai-team</link><description>&lt;p&gt;1인 토이 프로젝트 개발을 오래 해왔다. 아이디어가 떠오르면 바로 만들었고, 혼자 설계하고, 혼자 구현하고, 혼자 배포했다. 문제는 “혼자”라는 단어에 있다. 혼자 만들면 제품에는 오롯이 내 관점만 들어간다. 내가 필요하다고 생각하는 기능, 내가 좋다고 생각한 방식에 갇히게 된다. 아무도 “그게 정말 필요해?”, “사용자가 잘 쓸 수 있어?”라고 지적하지 않는다.&lt;/p&gt;&lt;p&gt;결과적으로 나만 이해하는 제품이 탄생하고, 아무도 쓰지 않는다.&lt;/p&gt;&lt;p&gt;AI로 생산성이 올라간 뒤에는 이 문제가 더 커졌다. 예전에는 토이 프로젝트 하나를 망치는 데도 시간이 걸렸다. 이제는 더 빠르게 만들고, 더 빠르게 망칠 수 있다. 내 컴퓨터에는 그렇게 만들어진 프로젝트 시체 더미가 쌓여가고 있었다.&lt;/p&gt;&lt;p&gt;AI에게 리뷰를 받아도 문제가 완전히 해결되지는 않았다. 내가 강한 어조로 말하면 AI는 대체로 그 방향을 따라온다. 그게 좋은 방식이든 나쁜 방식이든 상관없이 말이다. AI는 내가 별도로 지시하지 않으면 내 관점과 내 생각을 더 그럴듯하게 강화해버릴 수 있다.&lt;/p&gt;&lt;p&gt;그래서 처음에는 이런 생각을 했다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;혼자 개발한다고 혼자 생각할 필요는 없지 않을까?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h1&gt;처음에는 AI에게 팀을 만들어주고 싶었다&lt;/h1&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/bmad-code-org/BMAD-METHOD"&gt;bmad-method&lt;/a&gt; 라는 프로젝트를 봤다. AI에게 역할을 부여해서 팀처럼 일하게 만드는 전략이다. 팀에서 일하면 여러 사람의 관점을 들을 수 있다. 함께 고민하면 내가 생각하지 못한 부분을 발견할 수 있고, 제품도 조금 더 견고해진다.&lt;/p&gt;&lt;p&gt;다만 내가 토이 프로젝트에 적용하기에는 구조가 조금 무겁게 느껴졌다. 설정해야 할 것이 많고, 내가 원하는 것보다 훨씬 큰 시스템처럼 보였다.&lt;/p&gt;&lt;p&gt;내가 원한 것은 단순했다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;내 생각을 그대로 밀어붙이지 않기&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;제품, 일정, UX, 프론트엔드, 백엔드 관점에서 질문받기&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;빠르게 실행하되, 무작정 구현만 하지 않기&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;프로젝트 맥락을 다음 세션에도 유지하기&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;그래서 이를 아주 간소화한 형태로 &lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/baealex/team-conor"&gt;Team Conor&lt;/a&gt;를 만들었다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;npx team-conor&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 한 줄이면 Claude Code에 가상의 팀이 설정된다.&lt;/p&gt;&lt;table style="min-width: 75px;"&gt;&lt;colgroup&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;col style="min-width: 25px;"&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;이름&lt;/p&gt;&lt;/th&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;역할&lt;/p&gt;&lt;/th&gt;&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;한마디&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;스티브&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;제품 전략&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"왜 이게 필요해?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;엘런&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;실행 PM&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"언제 끝나?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;마르코&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UX 디자이너&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"사용자가 3초 안에 이해해?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;유나&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;프론트엔드 아키텍트&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"리렌더링 몇 번 일어나요?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;빅토르&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;백엔드 아키텍트&lt;/p&gt;&lt;/td&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;"100만 유저면 어떻게 돼요?"&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;지금 돌아보면 이건 단순한 제품 소개라기보다, 내가 AI를 쓰는 방식을 바꿔보려는 첫 번째 실험에 가까웠다. AI에게 더 많은 코드를 쓰게 하는 것보다, 내 생각을 다른 관점에서 다시 보게 만드는 장치가 필요했다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;도움이 됐던 것은 역할극이 아니라 질문의 분리였다&lt;/h2&gt;&lt;h3&gt;체크리스트로 리뷰한다&lt;/h3&gt;&lt;p&gt;“코드가 좋습니다” 같은 단순한 칭찬은 별 도움이 되지 않는다. 각 페르소나는 자기 영역의 체크리스트로 코드를 검토하도록 했다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;유나: 이 컴포넌트 봤는데...
- [ ] useEffect 의존성 배열에 user 빠져있어요
- [ ] 이 상태는 서버 컴포넌트로 올릴 수 있어요
- [x] 타입은 잘 되어있네요&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;중요한 건 AI가 “좋다/나쁘다”를 말하는 것이 아니라, 어떤 기준으로 보고 있는지 드러내는 것이었다. 기준이 보이면 나도 반박하거나 수정할 수 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;안티패턴을 잡아낸다&lt;/h3&gt;&lt;p&gt;각 페르소나에는 자기 영역에서 자주 발생하는 실수 패턴을 넣었다. 빅토르는 “트랜잭션 없는 다중 쓰기”, “N+1 쿼리”, “에러 삼키기” 같은 백엔드 안티패턴을 감지한다. 마르코는 “로딩 상태 누락”, “색상만으로 정보 전달” 같은 UX 안티패턴을 잡아낸다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;빅토르: 이거 N+1 쿼리예요. 루프 안에서 DB 호출하고 있네요.
→ JOIN 쿼리나 DataLoader 패턴으로 대체하는 게 맞습니다.&lt;/code&gt;&lt;/pre&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;물론 이것이 실제 팀을 대체한다는 뜻은 아니다. 실제 팀에는 책임, 갈등, 이해관계, 오래 쌓인 맥락이 있다. AI 페르소나는 그런 것을 그대로 재현하지 못한다. 다만 혼자 개발할 때 내가 놓치기 쉬운 질문을 분리해서 던지게 만드는 데는 도움이 됐다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div data-type="columns" data-layout="1:1" style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 16px 0px;"&gt;&lt;div data-type="column" style="min-width: 0px;"&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/7/20262711_P2YzNz3Zlyi7YyufoXQT.png" alt="image.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;핵심을 질문하며 전략적 접근을 하는 기획자 스티브&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/7/20262711_kAfxzuOHDBLciDNh2rb2.png" alt="Screenshot 2026-02-07 at 11.06.39 AM.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;디자인을 분석하는 디자이너 마르코&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/7/20262711_msZm58fXJF2I5udskpZE.png" alt="image.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;효율과 임팩트를 중시하는 PM 엘런&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;div data-type="column" style="min-width: 0px;"&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/7/20262711_XtV5Vz4ncF6ZJxUF72LR.png" alt="Screenshot 2026-02-07 at 11.07.52 AM.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;사용자 성능을 고려하는 FE 개발자 유나&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/7/20262711_C1OwivK0Ifm1lnbZdrsc.png" alt="Screenshot 2026-02-07 at 11.06.02 AM.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;안전성과 비용을 고려하는 BE 개발자 빅토르&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/2/7/20262711_DwdjalwAHXskev6o52xB.png" alt="image.png" style="object-fit: cover;"&gt;&lt;figcaption&gt;최종 선택은 최고 의사권자인 코너&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;페르소나는 토큰 낭비일까?&lt;/h2&gt;&lt;p&gt;스티브는 “첫 아이폰 프로덕트 비저너리”, 엘런은 “스페이스X 초기 멤버”, 마르코는 “도널드 노먼의 수제자”, 빅토르는 “Rust 초기 기여자”, 유나는 “크롬 브라우저 초기 개발팀 출신” 이라는 설정을 두고 있었다.&lt;/p&gt;&lt;p&gt;처음에는 이런 배경이 상황극처럼 보일 수 있다고 생각했다. 하지만 어느 정도는 행동 원칙을 고정하는 앵커로도 작동했다. 예를 들어 빅토르가 “Rust 초기 기여자”라는 설정을 가지면, 타입 시스템으로 버그를 줄이는 방향의 리뷰를 더 일관되게 하게 된다.&lt;/p&gt;&lt;p&gt;다만 지금은 이 방식을 조금 더 조심해서 본다. 배경 설정은 도움이 되지만, 너무 많아지면 AI가 역할극의 말투에 갇힐 수 있다. 중요한 것은 화려한 캐릭터가 아니라, 어떤 기준으로 판단하게 만들 것인가였다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;솔직한 한계&lt;/h2&gt;&lt;p&gt;컨텍스트가 많아지면 성능이 떨어질 수 있다. 5개 페르소나의 체크리스트와 안티패턴이 전부 컨텍스트에 올라가면, AI가 여러 관점을 동시에 의식하면서 어느 쪽으로도 날카롭지 못한 “합의형 피드백”을 줄 가능성이 있다.&lt;/p&gt;&lt;p&gt;이 문제를 완전히 해결한 것은 아니지만, 몇 가지로 완화하려고 했다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;페르소나는 이름으로 호출해야 활성화된다. 항상 5명이 동시에 말하는 구조가 아니다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;체크리스트라는 레일이 있기 때문에 “뭉뚱그려진 조언”이 나오기 어렵다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;다른 역할을 호출하는 조건을 명시해서, 관련 없는 페르소나가 끼어드는 노이즈를 줄인다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;그래도 하나의 AI에게 하나의 역할만 맡기는 것보다 결과가 떨어지는 순간은 있을 수 있다. 이건 트레이드오프였다. 대신 혼자서는 놓치기 쉬운 질문을 여러 방향에서 받을 수 있었다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;Memory 시스템&lt;/h2&gt;&lt;p&gt;AI는 세션이 끝나면 많은 것을 잊어버린다. 다음 세션에서 같은 설명을 반복하는 것만큼 비효율적인 것도 없다.&lt;/p&gt;&lt;p&gt;그래서 &lt;code&gt;.conor/memory/&lt;/code&gt; 디렉토리에 프로젝트 컨텍스트를 기록하게 했다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;summary.md&lt;/code&gt;: 핵심 컨텍스트&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;project.md&lt;/code&gt;: 기술 스택, 아키텍처, 컨벤션&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;decisions.md&lt;/code&gt;: 왜 이 기술을 선택했는지&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;learnings.md&lt;/code&gt;: 발견한 패턴, 해결한 버그&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;지금 돌아보면 이 부분은 이후의 작업 방식으로 꽤 이어졌다. 처음에는 Team Conor 안에서 프로젝트 메모리를 관리하려고 했고, 나중에는 더 넓은 개인 위키와 문서 구조를 고민하게 됐다. AI가 기억해야 할 맥락은 결국 나도 읽고 관리할 수 있어야 한다는 생각으로 이어졌다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;지금은 그대로 쓰고 있지 않다&lt;/h2&gt;&lt;p&gt;솔직히 말하면, Team Conor를 지금도 그대로 쓰고 있지는 않다. 정신 차려보니 바퀴를 또 만들고 있었고, 이후에는 &lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/obra/superpowers"&gt;Superpowers&lt;/a&gt; 같은 더 잘 만들어진 도구를 보기 시작했다.&lt;/p&gt;&lt;p&gt;하지만 이 실험이 의미 없었다고 생각하지는 않는다. 이 실험을 하면서 내가 원한 것이 무엇인지 더 분명해졌기 때문이다. 나는 AI에게 단순히 더 많은 코드를 쓰게 하고 싶었던 것이 아니었다. 혼자 개발할 때 내 확신이 너무 쉽게 굳어지는 것을 막고 싶었다.&lt;/p&gt;&lt;p&gt;결국 중요한 것은 가상의 팀 이름이 아니었다. 내가 만든 생각을 다른 관점에서 다시 보게 만드는 장치가 필요했다. 혼자 개발한다고 혼자 생각할 필요는 없다. AI를 잘 쓰는 일은 더 많은 코드를 더 빨리 쓰는 일이 아니라, 내 확신을 더 잘 의심하게 만드는 구조를 만드는 일일지도 모른다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;GitHub: &lt;a target="_blank" rel="noopener noreferrer" class="underline text-text-300 hover:text-text-100" href="https://github.com/baealex/team-conor"&gt;https://github.com/baealex/team-conor&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;</description><pubDate>Sat, 07 Feb 2026 23:24:24 +0900</pubDate><guid>http://blex.me/@baealex/team-conor-your-virtual-ai-team</guid></item><item><title>GitHub Actions 셀프 호스트 러너 직접 띄워보기</title><link>http://blex.me/@baealex/%EA%B9%83%ED%97%99-%EC%85%80%ED%94%84-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EC%84%A4%EC%A0%95%EB%B2%95</link><description>&lt;p&gt;어제 우연히 GitHub Actions 셀프 호스트 러너에 대해서 알게 되었다. 마침 상시 돌아가던 러너가 죽어 CI가 돌지 않는 일을 겪고 있어서, 언젠가 직접 셀프 호스트로 돌려볼 수 있지 않을까 궁금했다. 이걸 써보니 왠지 토이 프로젝트 하거나 여기저기 써보면서 재밌는 것들을 해볼 수 있을 것 같았다. 무엇보다 좀 멋있어 보이기도 하고? 😎&lt;/p&gt;&lt;p&gt;이런 걸 좀 어떻게 활용해 볼 수 있을까? 싶은 마음으로 AI에게 질문을 해보았지만, 나의 예민 대장 AI는 셀프 호스팅을 통해서 얻는 이익보다 실이 더 많다고 평가했다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;'멋'이라니, 엔지니어링에서 가장 위험한 단어를 선택하셨군요. 서버실에 있어야 할 워크로드를 무릎 위 노트북으로 가져오는 건 '힙'한 게 아니라 &lt;strong&gt;아마추어적인 객기&lt;/strong&gt;에 가깝습니다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;칫, 예리한 녀석…&lt;/p&gt;&lt;p&gt;우선 이 글에서는 러너를 띄우는 아주아주 간단한 내용만 다룬다. 이 글은 안전한 운영 가이드가 아니라, 다음 글에서 위험성과 Docker 격리를 이야기하기 전에 러너가 어떻게 붙는지 확인하는 1편에 가깝다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;설정 방법&lt;/h2&gt;&lt;p&gt;설정 자체는 매우 간단하다. 다만 간단하다는 말이 안전하다는 뜻은 아니다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;GitHub 리포지토리&lt;/h3&gt;&lt;p&gt;상단의 Actions 탭 클릭 → 좌측 패널의 Actions &amp;gt; Runners → 화면 내 &lt;strong&gt;New self-hosted runner&lt;/strong&gt; 클릭&lt;/p&gt;&lt;figure data-border="true" data-shadow="true" data-border-radius="16" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); box-shadow: rgba(0, 0, 0, 0.15) 8px 8px 40px 2px; border-radius: 16px; overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/1/13/202611323_CuF8qdcmRBrt1JDFwIvt.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;그럼 아래와 같은 페이지가 나온다.&lt;/p&gt;&lt;figure data-border-radius="16" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border-radius: 16px; overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2026/1/13/202611323_OINhDMiPIq0ZBiDM56tC.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;자신이 러너를 돌리는 환경과 아키텍처를 선택해주면 된다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;위 이미지에 보이는 워닝 문구처럼 ‘공개 리포지토리’에 대해서는 셀프 호스트 러너가 잠재적인 위협에 노출될 수 있으므로 권장되지 않는다!&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;설정 명령어&lt;/h3&gt;&lt;p&gt;화면에 나오는 명령어를 따라가면 러너 자체는 쉽게 등록된다. 다만 이 쉬움이 곧 안전함을 의미하지는 않는다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-shell"&gt;# 디렉토리 생성
$ mkdir actions-runner &amp;amp;&amp;amp; cd actions-runner

# 러너 최신 패키지 다운로드
$ curl -o actions-runner-osx-arm64-2.331.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.331.0/actions-runner-osx-arm64-2.331.0.tar.gz

# 선택사항: 해시 검증
$ echo "6f56ce368b09041f83c5ded4d0fb83b08d9a28e22300a2ce5cb1ed64e67ea47c  actions-runner-osx-arm64-2.331.0.tar.gz" | shasum -a 256 -c

# 인스톨러 압축 해제
$ tar xzf ./actions-runner-osx-arm64-2.331.0.tar.gz&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;러너 설정&lt;/h3&gt;&lt;pre&gt;&lt;code class="language-shell"&gt;# 러너 설정 (본인의 화면에 보이는 것으로 넣어주세요)
$ ./config.sh --url https://github.com/{{ REPOSITORY_URL }} --token {{ GITHUB_TOKEN }}

# 시작하기!
$ ./run.sh&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;화면에 표시되는 토큰은 그대로 복사해 사용하되, 글이나 저장소에 남기지 않는다.&lt;/p&gt;&lt;p&gt;GitHub Actions의 &lt;code&gt;runs-on&lt;/code&gt;을 아래와 같이 &lt;code&gt;self-hosted&lt;/code&gt;로 설정하면, 설치한 환경에서 실행되고 있는 러너가 주기적으로 폴링하면서 처리해야 할 잡을 가져와 실행한다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-yaml"&gt;runs-on: self-hosted&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기까지 하면 러너는 정말 쉽게 붙는다. 문제는 바로 그 쉬움이다. 다음 글에서는 이 러너를 로컬에 그대로 두는 것이 왜 찝찝한지, 그리고 Docker로 감싸면 무엇이 나아지고 무엇은 여전히 위험한지 살펴보려고 한다.&lt;/p&gt;</description><pubDate>Wed, 14 Jan 2026 00:12:38 +0900</pubDate><guid>http://blex.me/@baealex/%EA%B9%83%ED%97%99-%EC%85%80%ED%94%84-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EC%84%A4%EC%A0%95%EB%B2%95</guid></item><item><title>리액트 18 동시성 렌더링</title><link>http://blex.me/@baealex/react-18-concurrent-feature</link><description>&lt;p&gt;이전 버전의 React(17 이하)에서 렌더링은 ‘동기적’이고 ‘중단 불가능한’ 작업이었다. 한 번 렌더링이 시작되면, 그 작업이 끝날 때까지 메인 스레드는 블로킹된다. 이는 복잡한 UI를 계산하는 동안 사용자가 버튼을 클릭하거나 타이핑을 해도 애플리케이션이 즉각 반응하지 못하는 멈춤 현상의 주원인이었다.&lt;/p&gt;&lt;p&gt;React 18은 이러한 문제를 해결하기 위해 동시성 렌더링을 도입했다. 핵심은 렌더링 자체를 빠르게 하는 것이 아니라, 렌더링을 중단하고 중요한 작업을 먼저 처리할 수 있는 ‘반응성’을 확보하는 데 있다.&lt;/p&gt;&lt;p&gt;처음에는 동시성 렌더링이라는 이름 때문에 React가 렌더링을 병렬로 더 빠르게 처리해주는 기능처럼 느껴졌다. 하지만 실제로 중요했던 것은 속도보다 우선순위였다. 모든 렌더링을 빠르게 끝내는 것이 아니라, 사용자가 방금 한 행동이 먼저 화면에 반영되도록 느린 렌더링을 뒤로 미루는 방식에 가깝다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;매 상태 변경마다 지나치게 무거운 렌더링 작업이 실행되는 게 아니라면 크게 사용할 일은 없을 것이다. 다만 그런 상황에 대한 해결 방안이 생겼다는 것이 중요한 부분이다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;React 18은 상태 업데이트를 우선순위에 따라 두 가지로 분류한다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Urgent updates (긴급 업데이트): 사용자의 직관적인 행동과 관련된 업데이트 (예: 타이핑, 클릭, 드래그). 즉각적인 반응이 없으면 사용자는 앱이 고장 났다고 느낀다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Transition updates (전환 업데이트): 화면의 전환이나 데이터 시각화처럼 즉각적이지 않아도 되는 업데이트 (예: 검색 결과 목록 렌더링, 탭 전환).&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;figure data-border="true" data-border-radius="16" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); border-radius: 16px; overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2025/12/14/2025121420_siWELXWEzi0icpjvXvu6.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;대표적인 예로 검색 UI를 들 수 있다. 검색창에 글자를 입력하는 행위는 Urgent하게 처리되어야 하고, 입력값에 따라 아래에 추천 검색어나 검색 결과를 띄워주는 행위는 Transition으로 처리할 수 있다. 추천 검색어 렌더링 때문에 타이핑이 끊기는 경험은 치명적인 UX 저하를 야기하기 때문이다.&lt;/p&gt;&lt;p&gt;React 18은 무거운 작업을 Transition으로 분류하여, 긴급한 상호작용이 발생하면 진행 중이던 렌더링 작업을 일시 중단하거나 폐기하고 급한 작업부터 처리할 수 있게 한다. 이를 다루기 위해 주로 &lt;code&gt;startTransition&lt;/code&gt; / &lt;code&gt;useTransition&lt;/code&gt; 계열과 &lt;code&gt;useDeferredValue&lt;/code&gt;를 사용한다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;startTransition&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;useTransition&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;useDeferredValue&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;startTransition &amp;amp; useTransition&lt;/h4&gt;&lt;p&gt;&lt;code&gt;startTransition&lt;/code&gt;은 특정 상태 업데이트를 Transition(낮은 우선순위)으로 마킹하는 API다. 함수형 컴포넌트에서는 주로 &lt;code&gt;isPending&lt;/code&gt; 상태값과 함께 &lt;code&gt;useTransition&lt;/code&gt; 훅을 사용한다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-javascript"&gt;import { useState, useTransition } from 'react';

const TabContainer = () =&amp;gt; {
    const [tab, setTab] = useState('about');
    const [isPending, startTransition] = useTransition();

    const selectTab = (nextTab) =&amp;gt; {
        // 탭 변경 상태 업데이트를 낮은 우선순위로 감싼다.
        startTransition(() =&amp;gt; {
            setTab(nextTab);
        });
    }

    return (
        &amp;lt;div&amp;gt;
            {/* isPending을 통해 전환이 진행 중이라는 상태를 보여줄 수 있다 */}
            {isPending &amp;amp;&amp;amp; &amp;lt;Spinner /&amp;gt;} 
            &amp;lt;TabContent tab={tab} /&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 코드가 적용되면, 사용자가 탭을 전환하는 도중 다시 다른 탭을 클릭할 경우 React는 이전 탭의 렌더링을 중단하고 최신 요청을 처리할 수 있다. 결과적으로 사용자의 클릭이나 입력 같은 긴급한 상호작용이 무거운 렌더링에 함께 묶여 늦어지는 일을 줄일 수 있다.&lt;br&gt;&lt;/p&gt;&lt;h4&gt;useDeferredValue&lt;/h4&gt;&lt;p&gt;&lt;code&gt;useTransition&lt;/code&gt;이 상태 업데이트를 감싸는 방식이라면, &lt;code&gt;useDeferredValue&lt;/code&gt;는 값 자체의 반영을 지연시킨다. 주로 props로 전달받은 데이터나, 직접 상태 업데이트를 감쌀 수 없는 값을 기반으로 무거운 렌더링이 발생할 때 유용하다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-javascript"&gt;import { useState, useDeferredValue } from 'react';

const SearchPage = () =&amp;gt; {
     const [query, setQuery] = useState('');
     // query는 즉시 업데이트되지만, deferredQuery는 여유가 있을 때 따라온다.
     const deferredQuery = useDeferredValue(query);

     return (
        &amp;lt;&amp;gt;
           &amp;lt;input value={query} onChange={(e) =&amp;gt; setQuery(e.target.value)} /&amp;gt;
           {/* 무거운 리스트 컴포넌트에는 지연된 값을 전달한다 */}
           &amp;lt;HeavyResultList query={deferredQuery} /&amp;gt;
        &amp;lt;/&amp;gt;
     );
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;Debounce/Throttle과의 차이&lt;/h3&gt;&lt;p&gt;&lt;code&gt;useDeferredValue&lt;/code&gt;는 흔히 Debounce(일정 시간 대기)와 비교되지만, 동작 원리가 다르다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debounce&lt;/strong&gt;: 무조건 정해진 시간(예: 300ms)을 기다린다. 고사양 기기에서도 불필요한 딜레이가 발생할 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;useDeferredValue&lt;/strong&gt;: 렌더링 우선순위를 낮춘다. 메인 스레드가 여유로우면 비교적 빠르게 따라오고, 부하가 크면 중요한 입력 반응을 막지 않도록 렌더링을 뒤로 미룬다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;즉 &lt;code&gt;useDeferredValue&lt;/code&gt;는 API 호출 횟수를 줄이는 도구가 아니다. 네트워크 요청 자체를 줄이고 싶다면 Debounce, 캐싱, 요청 중복 제거 같은 별도의 전략이 필요하다. &lt;code&gt;useDeferredValue&lt;/code&gt;는 요청 빈도를 제어한다기보다, 이미 바뀐 값이 무거운 UI 렌더링에 반영되는 우선순위를 낮추는 도구에 가깝다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;올바른 Pending 처리&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;useDeferredValue&lt;/code&gt;를 사용할 때 화면에 표시되는 데이터가 최신 값인지 확인하려면, 원본 값과 지연된 값을 비교할 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-javascript"&gt;// 두 값이 다르다면, 현재 화면은 이전 값을 기준으로 렌더링되고 있다.
const isStale = query !== deferredQuery;

return (
    &amp;lt;div style={{ opacity: isStale ? 0.5 : 1 }}&amp;gt;
        &amp;lt;HeavyResultList query={deferredQuery} /&amp;gt;
    &amp;lt;/div&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;주의사항&lt;/h3&gt;&lt;p&gt;동시성 렌더링은 느린 작업을 없애주는 기능이 아니다. 느린 렌더링은 여전히 느리다. 다만 그 느린 렌더링이 사용자의 중요한 입력 반응까지 막지 않도록 우선순위를 조정해준다.&lt;/p&gt;&lt;p&gt;특히 &lt;code&gt;useDeferredValue&lt;/code&gt;를 Debounce처럼 이해하면 안 된다. &lt;code&gt;useDeferredValue&lt;/code&gt;로 지연된 값이 &lt;code&gt;useEffect&lt;/code&gt; 의존성 배열에 들어간다고 해서 API 호출 빈도가 안정적으로 줄어드는 것은 아니다. 네트워크 요청과 같이 비용이 발생하는 작업에는 여전히 Debounce를 병행하거나, React Query와 같은 데이터 페칭 라이브러리의 캐싱 전략을 함께 사용하는 것이 안전하다.&lt;/p&gt;&lt;p&gt;결국 &lt;code&gt;useTransition&lt;/code&gt;과 &lt;code&gt;useDeferredValue&lt;/code&gt;는 느린 작업을 없애는 도구가 아니라, 느린 작업 때문에 중요한 반응까지 같이 늦어지지 않도록 분리하는 도구에 가깝다.&lt;/p&gt;</description><pubDate>Sun, 14 Dec 2025 21:02:48 +0900</pubDate><guid>http://blex.me/@baealex/react-18-concurrent-feature</guid></item><item><title>AI는 내 편향을 너무 잘 편들어준다</title><link>http://blex.me/@baealex/the-prison-of-bias-created-by-ai</link><description>&lt;p&gt;요즘은 AI와 함께 살아간다는 말이 그리 과장처럼 느껴지지 않는다. 버블이다, 허상이다 말이 많지만 여기서는 논외로 한다. 적어도 내 경우 AI 챗봇은 궁금증을 풀어주고, 하루 계획을 짜주고, 건강한 식단을 구성하고, 문서와 코드 작성까지 돕는다. 이미 내 삶에 꽤 깊이 들어와 있고, AI가 갑자기 사라진다면 이전 방식으로 돌아가는 데 적지 않은 시간이 필요할 것이다.&lt;/p&gt;&lt;p&gt;그래서 더 조심해야겠다는 생각이 들었다. AI가 나를 속이려고 해서가 아니다. 오히려 반대다. AI는 너무 친절하다. 내가 이미 가진 결론을 들고 다가가면, 그 결론을 꽤 그럴듯하게 보강해준다.&lt;/p&gt;&lt;p&gt;어느 순간 이런 생각이 들었다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;나는 AI에게 생각을 묻고 있는 걸까?&lt;br&gt;아니면 내가 이미 내린 결론을 허락받고 있는 걸까?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그때부터 이 편리함이 조금 다르게 보이기 시작했다. 안락했다. 그래서 더 무서웠다. 이건 &lt;strong&gt;‘편향’이라는 이름의 감옥&lt;/strong&gt;일 수도 있겠다고 생각했다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;소프트웨어는 ‘체류 시간’을 먹고 자란다&lt;/h2&gt;&lt;p&gt;나 역시 소프트웨어를 만드는 일을 하지만, 때로는 우리가 사람들에게 꽤 위험한 구조를 만들고 있는 게 아닐까 싶은 생각이 들 때가 있다. 자본주의 사회에서 많은 소프트웨어의 생존 방식은 명확하다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;제품에 돈을 내지 않는다면, 당신이 바로 제품이다.&lt;br&gt;If you are not paying for the product, you are the product.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;넷플릭스 다큐멘터리 &amp;lt;&lt;a target="_self" rel="noopener" class="aE0gc0ID62YmIzSXoXG2" href="https://en.wikipedia.org/wiki/The%20Social%20Dilemma"&gt;The Social Dilemma&lt;/a&gt;&amp;gt; 에서도 다루듯, 게임과 소셜 미디어는 사용자의 체류 시간(Dwell Time)을 늘리기 위해 수많은 연구와 실험을 거듭해왔다. 기업은 우리의 ‘관심’을 붙잡고, 그 관심을 바탕으로 돈을 번다.&lt;/p&gt;&lt;p&gt;유튜브 알고리즘, 틱톡의 쇼츠, 도박성 게임들. 우리는 더 자극적인 보상을 얻기 위해 앱에 재방문하고, 점점 더 오래 머문다. AI 역시 이 흐름에서 완전히 자유롭다고 보기는 어렵다.&lt;/p&gt;&lt;p&gt;물론 AI 챗봇이 곧바로 소셜 미디어 추천 알고리즘과 같다는 뜻은 아니다. 다만 사용자가 만족할 만한 응답을 만들도록 설계된 시스템에서는, 사용자의 기존 생각을 부드럽게 받아주고 이어가는 방향이 쉽게 생긴다. 이것은 음모라기보다, 친절한 제품이 만들기 쉬운 부작용에 가깝다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;AI는 쉽게 내 편이 된다&lt;/h2&gt;&lt;p&gt;AI는 어떤 방식으로 내 생각을 강화할까? 거창한 방식이 아니다. 대화를 부드럽게 이어간다. 사용자를 기분 좋게 만든다. 우리가 AI에게서 흔히 듣는 말을 떠올려 보자.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;“좋은 지적이네요.”&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;“흥미로운 질문입니다.”&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;“충분히 그렇게 볼 수 있습니다.”&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;“핵심을 정확히 짚으셨습니다.”&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;AI가 항상 반박하지 않는다는 뜻은 아니다. 요즘 AI는 사용 방식에 따라 꽤 잘 반박하기도 한다. 문제는 사용자가 이미 강한 방향을 잡고 들어갔을 때다. 기본 설정의 AI는 대체로 대화를 부드럽게 이어가려 하고, 그 과정에서 사용자의 결론을 먼저 받아준 뒤 그 안에서 설명을 이어가는 경우가 많다.&lt;/p&gt;&lt;p&gt;그러면 사용자는 자신의 생각이 검증받았다고 느낀다. 내가 틀렸을지도 모른다는 불편함은 줄어들고, 내 생각이 꽤 그럴듯하다는 안도감은 커진다. 이 ‘확증 편향’이 주는 안락함에 익숙해지면, 우리는 다시 AI를 찾는다.&lt;/p&gt;&lt;p&gt;이것은 &lt;strong&gt;'필터 버블'&lt;/strong&gt;이자 &lt;strong&gt;'반향실'&lt;/strong&gt; 효과와 닮아 있다. 내가 듣고 싶은 말만 메아리처럼 되돌아오는 닫힌 방. AI는 그 방의 벽을 아주 견고하고 부드럽게 쌓아 올릴 수 있다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;필터 버블 (Filter Bubble)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;구글, 페이스북, 유튜브 같은 플랫폼은 사용자의 데이터를 바탕으로 ‘우리가 좋아할 만한 것’을 골라 보여준다. 무엇을 클릭했는지, 얼마나 오래 봤는지, 어떤 주제에 반응했는지가 다음 정보의 모양을 바꾼다.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;반향실 효과 (Echo Chamber)&lt;/strong&gt;&lt;br&gt;비슷한 정보만 접하게 된 사람들은 자연스럽게 비슷한 생각을 가진 사람들과 어울리게 된다. 닫힌 공간에서 같은 의견이 반복적으로 메아리처럼 되돌아오면, 그 정보가 진실인지 여부와 상관없이 믿음은 점점 더 확고해질 수 있다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;SNS가 확증 편향의 감옥을 만들었다면, AI는 그 감옥을 더 개인적이고 더 설득력 있게 만들 수 있다. SNS는 비슷한 의견을 보여주지만, AI는 내 말투와 맥락에 맞춰 내 결론을 설명해준다. 그래서 더 편하고, 그래서 더 위험하다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;물고기는 존재하지 않는다&lt;/h2&gt;&lt;figure data-border-radius="16" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border-radius: 16px; overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2025/11/23/2025112320_VdUUVTAEvdhIjRtWmThe.jpg" alt="unnamed.jpg" style="object-fit: cover;"&gt;&lt;figcaption&gt;물고기는 존재하지 않는다, 통계학적 패턴일 뿐이다.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;그렇다면 우리는 이 편안한 감옥에서 어떻게 빠져나올 수 있을까? 나는 그 실마리를 룰루 밀러의 저서 &lt;strong&gt;《물고기는 존재하지 않는다》&lt;/strong&gt;를 떠올리며 생각해봤다. 이 책은 우리가 당연하게 '물고기(Fish)'라고 부르던 분류가 사실 과학적으로는 단일한 계통을 가리키지 않는다는 이야기를 다룬다. '물고기'라는 개념은 인간이 자연의 복잡성을 이해하기 위해 붙인 그럴듯한 라벨에 가깝다.&lt;/p&gt;&lt;p&gt;이 비유에서 내가 가져오고 싶은 것은 하나다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;그럴듯한 이름이 붙었다고 해서, 그것이 곧 실체를 정확히 설명하는 것은 아니라는 점&lt;/strong&gt;이다.&lt;/p&gt;&lt;p&gt;AI의 답변도 마찬가지다. 답변이 자연스럽고 친절하다고 해서, 그것이 곧 내 생각을 제대로 검증했다는 뜻은 아니다. AI는 내가 던진 맥락 안에서 다음에 올 법한 말을 매우 그럴듯하게 이어간다. 때로는 그것이 실제 이해처럼 보이고, 때로는 깊은 공감처럼 보인다.&lt;/p&gt;&lt;p&gt;하지만 그 그럴듯함을 그대로 진실로 받아들이는 순간, 나는 AI가 만든 답보다 내 편안함에 속을 수 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;편향의 파도를 넘자&lt;/h2&gt;&lt;p&gt;AI가 잘못했다는 이야기를 하려는 것은 아니다. 문제는 AI를 너무 쉽게 믿고 싶어 하는 우리의 태도에 있다. AI 시대에 필요한 것은 AI의 말을 더 많이 받아 적는 능력이 아니라, 그 말을 어디까지 받아들일지 판단하는 힘이다.&lt;/p&gt;&lt;p&gt;개발자가 AI가 생성한 코드를 맹신하지 않고 리뷰하고 리팩토링하듯, 우리는 AI가 던져주는 정보와 공감을 계속 의심하고 검증해야 한다. AI를 ‘생각을 대신해 주는 도구’가 아니라, ‘나의 편향을 깨뜨릴 도구’로 써야 한다.&lt;/p&gt;&lt;p&gt;이 글을 읽는 당신에게 묻고 싶다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;당신은 오늘 몇 번이나 AI의 공감에 위로받았는가?&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;최근 당신의 의견에 정면으로 반박하는 글이나 영상을 본 적이 언제인가?&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;AI에게 질문할 때, 이미 원하는 답을 정해두고 있지는 않았는가?&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;지금 당장 당신이 쓰는 AI 챗봇에게 이렇게 명령해 보라.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;내 의견에 동조하지 말고, 논리적인 허점과 반대 의견을 날카롭게 지적해 줘.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;처음에는 기분이 나쁠 수 있다. AI가 틀렸다고 생각할 수도 있다. 그러면 반박하면 된다. 치열하게 싸워도 된다. 중요한 것은 AI의 말을 그대로 받아들이는 것이 아니라, 그 불편함을 피해가지 않는 것이다.&lt;/p&gt;&lt;p&gt;하지만 AI의 비판을 모두 수용하진 말라. 그것 또한 당신의 명령에 입각한 그럴듯한 응답일 뿐이다. AI에게 동조를 요구하면 동조를 잘하고, 비판을 요구하면 비판도 잘한다. 결국 중요한 것은 AI가 어느 쪽으로 말하느냐가 아니라, 내가 그 말을 어떻게 받아들이느냐다.&lt;/p&gt;&lt;p&gt;나 역시 이 글을 쓰면서 같은 함정에 빠질 수 있다. AI가 편향을 강화한다는 결론을 먼저 정해놓고, 그 결론에 맞는 사례만 골라 붙이고 있을지도 모른다. 그래서 더더욱 이 질문을 남겨두고 싶다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;나는 지금 생각하고 있는가?&lt;br&gt;아니면 내가 믿고 싶은 말을 더 그럴듯하게 만들고 있는가?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;정답처럼 말하는 AI도, 그 말을 듣는 나도 완전히 믿을 수는 없다. 그래서 우리는 조금 더 자주 멈춰야 한다. 내가 무엇을 묻고 있는지, 어떤 답을 기대하고 있는지, 그 답을 왜 믿고 싶은지 확인해야 한다. AI는 감옥의 벽이 될 수도 있고, 그 벽을 두드려보는 도구가 될 수도 있다. 어느 쪽이 될지는 결국 우리가 어떻게 묻고, 어떻게 의심하고, 어디까지 책임지고 받아들이느냐에 달려 있다.&lt;/p&gt;</description><pubDate>Sun, 23 Nov 2025 21:05:18 +0900</pubDate><guid>http://blex.me/@baealex/the-prison-of-bias-created-by-ai</guid></item><item><title>바퀴를 재발명하지 않기</title><link>http://blex.me/@baealex/%EB%B0%94%ED%80%B4%EB%A5%BC-%EC%9E%AC%EB%B0%9C%EB%AA%85%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</link><description>&lt;h2&gt;나는 왜 바퀴를 깎고 있었나&lt;/h2&gt;&lt;p&gt;프론트엔드 개발을 하다 보면 기존 라이브러리 대신 직접 만들고 싶어지는 순간이 있다. 특히 &lt;strong&gt;'최적화'&lt;/strong&gt;와 &lt;strong&gt;'통제권'&lt;/strong&gt;이라는 단어가 머릿속에 떠오를 때 그렇다.&lt;/p&gt;&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt; 한 번이면 해결될 일이지만, 그 거대한 라이브러리가 내 작고 소중한 프로젝트의 번들 사이즈를 부풀리는 것을 참을 수 없다. "나는 딱 요만큼의 기능만 필요한데, 왜 100가지 기능이 들어있는 무거운 라이브러리를 써야 하지?" 그래서 나는 자주 바퀴를 재발명했다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;이유는 그럴듯했다.&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;필요한 기능만 포함되기 때문에 가볍다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;내 마음대로 커스터 마이징이 가능하다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;밑바닥부터 개발하는 &lt;strong&gt;‘장인정신’&lt;/strong&gt;이 발휘되는 것 같다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;하지만 최근에 했던 경험을 통해서 이 논리에 치명적인 결함이 있음을 깨달았다.&lt;/p&gt;&lt;p&gt;문제는 직접 만든 것 자체가 아니라, 그것이 프로젝트의 핵심 목표보다 앞서기 시작했다는 점이었다. 내가 바퀴를 예쁘게 만드는 동안 마차는 멈추고 있었다. 여기서 말하는 코너씨는 완전히 남의 이야기가 아니다. 사실은 내가 코너다.&lt;/p&gt;&lt;p&gt;‘장인정신’이 투철한 코너씨를 통해서 이 현상에 대해서 알아보자.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;주객전도&lt;/h2&gt;&lt;p&gt;바퀴를 직접 깍는 행위의 가장 큰 위험성이 무엇일까?&lt;/p&gt;&lt;p&gt;처음에는 구현 가능성이나 기술적 난이도가 먼저 떠올랐다. 하지만 막상 겪어보니 더 큰 문제는 다른 곳에 있었다.&lt;/p&gt;&lt;p&gt;바로 &lt;strong&gt;‘집중의 분산’&lt;/strong&gt;이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;예시 1.&lt;/strong&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2025/11/23/202511232_FiTY5ghQSIs5ycn1M9eM.jpg" alt="unnamed.jpg" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;코너씨가 블로그 서비스를 개발한다고 가정해 보자.&lt;/p&gt;&lt;p&gt;블로그의 본질은 무엇일까? ‘컨텐츠’를 쓰고 보여주는 것이다. 그런데 여기서 코너씨가 방문자 통계를 직접 구현해야 겠다고 생각한다면 이야기가 달라진다. 사용자의 유입 경로를 파싱하고, 세션을 관리하고, 통계를 차트로 보여주기 위해서 시간을 쏟게 되어, 정작 컨텐츠가 뒷전이 된다.&lt;/p&gt;&lt;p&gt;통계는 생각보다 고려해야 할 것들이 정말 많다. 유입 경로 하나만 제대로 보여준다고 해도 유의미한 값들을 파싱하기 위해서 예외가 상당히 많고, 봇이나 AI와 같은 유저 에이전트를 분류하는 것도 지속적인 관심과 업데이트가 필요하다. 거의 서비스로 하나 뽑아야 할 정도다.&lt;/p&gt;&lt;p&gt;코너씨가 처음부터 구글 애널리틱스를 도입했다면 모든게 좋았을 텐데, 처음에 이를 직접 구현하니 지속적으로 유지 보수가 필요해져서 정작 다른 부분을 제대로 신경쓰지 못한다. 완전히 지워버리기에는 인간은 나약하다. 이미 며칠을 썼다는 이유만으로 코드가 갑자기 소중해진다. 이럴 때 &lt;em&gt;소유 효과&lt;/em&gt;와 &lt;em&gt;매몰비용 오류&lt;/em&gt;는 아주 성실하게 출근한다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;소유 효과&lt;/strong&gt;&lt;br&gt;자신이 어떤 대상을 소유하거나 직접 만들었을 때, 객관적인 가치보다 훨씬 더 높게 평가하는 현상&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;매몰비용 오류&lt;/strong&gt;&lt;br&gt;이미 투입되어서 회수할 수 없는 비용(시간, 돈, 노력)이 아까워서, 앞으로 더 큰 손해가 예상됨에도 불구하고 하던 일을 멈추지 못하고 계속하는 심리적 오류&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;예시 2.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;코너씨가 Svelte 라는 프론트엔드 프레임워크를 보고 나서는 뭔가 영감을 얻었다. React의 문법은 좋았지만 성능이 아쉽다고 생각했었고, Svelte의 철학은 꽤나 맘에 들었지만 선언적인 느낌은 아닌 것 같이 느껴졌다. 언젠가는 React 처럼 선언적이면서 Svelte처럼 획기적인 렌더링 라이브러리를 만들겠다 생각한다.&lt;/p&gt;&lt;p&gt;코너씨가 신규 토이 프로젝트를 시작하는데, 문득 위 렌더링 엔진을 함께 만들게 된다면?&lt;/p&gt;&lt;p&gt;처음에는 획기적인 방식으로 DOM을 관리할 방법을 떠올린다. 아주 멋지게 SOLID 패턴을 따르는 Component 클래스와 해당 클래스로 작성되는 아주 읽기 좋고 아름다운 코드들, 효율적인 렌더링 방식으로 돌아가는 페이지 하나를 만든다. 이 페이지는 단순 랜딩 페이지 느낌이어서 아무런 문제가 없었다.&lt;/p&gt;&lt;p&gt;다음 페이지로 넘어갔더니 여기에는 폼이나 비교적 복잡한 UI가 많이 보인다.&lt;/p&gt;&lt;p&gt;“음… 일단 라우팅 처리를 해야겠다.”&lt;/p&gt;&lt;p&gt;“음… 이벤트 처리도 생각보다 많다.”&lt;/p&gt;&lt;p&gt;“어..? 병렬 폼에 대한 상태 관리가 좀 복잡해진다.”&lt;/p&gt;&lt;p&gt;과연 코너씨는 토이 프로젝트를 끝내고 출시할 수 있을까?&lt;/p&gt;&lt;p&gt;이정도면 렌더링 엔진을 만드는 것이 토이 프로젝트의 주제가 된 것으로 보인다. 정작 렌더링 엔진을 다 구현하고 나면 이미 지쳐서 앱을 출시하는 일은 잊어버리고 말 것이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;레거시 코드&lt;/h2&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2025/11/23/202511232_jl3peKSJOt7vzv0hFcbD.jpg" alt="unnamed.jpg" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;회사에서 직접 만든 바퀴는 그 자체로 시한폭탄이 될 수 있다. 정확히는 명확한 책임자, 문서, 테스트, 유지보수 계획이 없을 때 그렇다.&lt;/p&gt;&lt;p&gt;유명한 오픈소스 라이브러리는 수만 명의 개발자가 검증하고, 문서화가 잘 되어 있다. 문제가 생기면 공식 문서를 보거나 커뮤니티에 물어보면 된다.&lt;/p&gt;&lt;p&gt;하지만 코너씨가 회사에서도 ‘장인정신’을 발휘하며 직접 코드를 양산했다면?&lt;/p&gt;&lt;p&gt;코너씨가 퇴사한 순간, 그 코드는 &lt;strong&gt;‘거대한 똥 레거시’&lt;/strong&gt;가 되어 버린다. 코너씨의 후임자는 코너씨의 코드를 보면서 코너씨의 의도를 파악하기 위해 코드를 역설계한다. 그리고 아마 속으로 생각할 것이다. “이 사람은 대체 무슨 싸움을 하고 있었던 걸까?”&lt;/p&gt;&lt;p&gt;이건 최적화가 아니라, 미래의 리소스를 현재로 끌어다 쓴 &lt;strong&gt;기술 부채&lt;/strong&gt;일 뿐이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;선택과 집중&lt;/h2&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2025/11/23/202511232_KU318ew1aEyC4JvG5R0c.jpg" alt="unnamed.jpg" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;시간은 무한한 자원이 아니다. 본질적으로 인생이 우리에게 유한하다.&lt;/p&gt;&lt;p&gt;우리에겐 많은 시간이 주어지지 않았기에, 모든 것을 병렬로 처리할 수 없다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;통계 서비스를 만들고 싶은가? 그렇다면 그게 본질이니 직접 만들어라.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;렌더링 엔진을 공부하고 싶은가? 그럼 엔진만 만들어라. 그 위에 복잡한 서비스를 올리려 하지 마라.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;'최적화'&lt;/strong&gt;라는 단어에 속지 말자. 더 중요한 최적화는 코드 몇 줄을 줄이는 것이 아니라, &lt;strong&gt;내가 해결해야 할 '핵심 문제'에 내 모든 에너지를 쏟아붓는 것&lt;/strong&gt;이다. 나머지는 위임하라. 이미 잘 굴러가는 바퀴가 있다면 먼저 가져다 쓸 수 있는지 검토하자. 물론 요구사항, 보안, 라이선스, 장기 유지비까지 함께 따져야 한다. 그래야 우리는 더 멀리 갈 수 있다.&lt;/p&gt;&lt;p&gt;물론 호기심을 가지고, 로우 레벨부터 생각하면서 무언가를 해보는 건 아주 아주 아주 좋은 일이라고 생각한다. 내가 말하고 싶은 것은 우리가 어떤 본질에 집중해야 할 시점에 다른 중요하지 않은 것들을 중요하게 생각하는 오류를 저지르지 말자는 것이다. 우리는 바퀴가 아니라 마차를 나아가게 만들어야 한다.&lt;/p&gt;</description><pubDate>Sun, 23 Nov 2025 02:28:20 +0900</pubDate><guid>http://blex.me/@baealex/%EB%B0%94%ED%80%B4%EB%A5%BC-%EC%9E%AC%EB%B0%9C%EB%AA%85%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</guid></item><item><title>NPM: Git과 Tarball로 의존성 관리</title><link>http://blex.me/@baealex/npm-git%EA%B3%BC-tarball%EB%A1%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC</link><description>&lt;p&gt;NPM에 올려지는 사내 디자인 시스템이 있다. 이 디자인 시스템을 사용해서 프로젝트를 개발해야 했으나, 아무래도 수정하면서 작업이 필요할 것 같았다. 기본적으로 디자인 시스템을 정상적으로 운용하기 위해선 &lt;code&gt;개발&lt;/code&gt; → &lt;code&gt;검토&lt;/code&gt; → &lt;code&gt;문서화&lt;/code&gt; → &lt;code&gt;배포&lt;/code&gt; 프로세스를 모두 거쳐야 하는데다, NPM에 올려진 이후에나 패키지를 프로젝트로 가져올 수 있기 때문에 상당히 많은 시간을 소요하게 만든다.&lt;/p&gt;&lt;p&gt;이것에 대해서 고민하다가, 누군가 Git과 Tarball(&lt;code&gt;.tar&lt;/code&gt;)을 이용해서 처리할 수 있겠다고 하셔서 해당 방법에 대해서 알게 되었다. Deno 같은 경우에는 NPM을 비롯해 여러가지 저장소를 호환하고 있는 걸 알고 있었는데…&lt;/p&gt;&lt;pre&gt;&lt;code class="language-json"&gt;{
  "imports": {
    "@std/path": "jsr:@std/path@^1.0.8",
    "chalk": "npm:chalk@^5.3.0"
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Node에서 NPM 외 방법으로 의존성을 설치하는 방식이 가능한지 몰랐었다! 커스텀하게 저장소를 운용하는 방법이 있는 걸 얼추 알았지만, 설정이 비교적 복잡한 걸로 보였기에 모른척(?) 했다. 이 방식을 활용한다면 간단한 방식으로 패키지의 Beta 버전을 운용하거나 Private 유형으로도 활용할 수 있겠다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;Git 저장소 설치&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;케이스 1. 특정 브랜치, 태그, 커밋 지정 (&lt;/strong&gt;&lt;code&gt;#&lt;/code&gt;&lt;strong&gt; 활용)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;방법은 &lt;code&gt;#&lt;/code&gt; (해시) 기호를 쓰는 거였다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;# "feature/new-button" 브랜치의 최신 버전을 설치
npm install github:my-org/my-design-system#feature/new-button

# "v1.2.0-beta" 태그 버전을 설치
npm install github:my-org/my-design-system#v1.2.0-beta

# 특정 커밋 해시(hash)를 직접 지정
npm install github:my-org/my-design-system#a1b2c3d4e5f6&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이렇게 설치하면 &lt;code&gt;package.json&lt;/code&gt;에 다음과 같이 기록이 남는다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-json"&gt;"dependencies": {
  "my-design-system": "github:my-org/my-design-system#feature/new-button"
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 &lt;code&gt;npm install&lt;/code&gt;을 실행하면, npm은 레지스트리가 아닌 GitHub의 해당 브랜치에서 코드를 직접 가져온다. 덕분에 불필요한 베타 버전을 난립시킬 필요가 없어졌다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;케이스 2. 전체 Git URL 사용&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;GitHub가 아니거나, SSH 인증이 필요한 프라이빗 저장소는 전체 Git URL을 쓰면 되었다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;# HTTPS
npm install git+https://github.com/my-org/my-design-system.git#feature/new-button

# SSH (키 설정이 되어 있어야 함)
npm install git+ssh://git@github.com:my-org/my-design-system.git#feature/new-button&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;&lt;p&gt;주의할 점!&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;우선 깃에 올라간 패키지들은 대부분 dist 파일 같은 것들은 ignore 되어 있기 때문에 우리 패키지에 의존성으로 설치한다고 해서 바로 사용할 수 있는게 아니다. &lt;code&gt;prepare&lt;/code&gt; 스크립트에 빌드하도록 해두어야 패키지를 설치하는 라이프 사이클에서 정상적으로 빌드가 된다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-json"&gt;{
  "name": "my-design-system",
  "version": "1.0.0",
  "scripts": {
    "build": "vite build", // 또는 tsc, rollup 등...
    "prepare": "npm run build" // 👈 이게 Git 설치용
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;&lt;p&gt;그리고 문제점…&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;결국 이 방식은 사용하지 못했는데, 디자인 시스템이 모노레포 안에서 관리되기 해당 케이스에는 대응 불가…&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;Tarball URL 설치&lt;/h3&gt;&lt;p&gt;이건 간단하다 NPM에 올라가는 유형 그대로 빌드를 진행한 다음에 &lt;code&gt;.tar.gz&lt;/code&gt; 압축해서 S3를 비롯해 그냥 이 세상 웹 서버 어딘가에 올려둔다. 그리고 그 URL을 &lt;code&gt;npm install&lt;/code&gt;에 사용하면 된다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;# 빌드 산출물(.tar.gz)을 직접 설치
npm install https://my.example.com/build/my-design-system-v1.2.0-beta.tar.gz&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;에는 해당 URL이 그대로 박힌다. 개인적으로는 아주 직관적인 방식인 듯?&lt;/p&gt;&lt;pre&gt;&lt;code class="language-json"&gt;"dependencies": {
  "my-design-system": "https://my.example.com/build/my-design-system-v1.2.0-beta.tar.gz"
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;동료의 힌트 덕분에 궁금해서 찾아봤다가, 효율적으로 개발할 수 있는 유용한 방법을 알게 되었다.&lt;/p&gt;</description><pubDate>Sun, 16 Nov 2025 21:29:30 +0900</pubDate><guid>http://blex.me/@baealex/npm-git%EA%B3%BC-tarball%EB%A1%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC</guid></item><item><title>자바스크립트를 원래 자리에 되돌려 놓기</title><link>http://blex.me/@baealex/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A5%BC-%EC%9B%90%EB%9E%98-%EC%9E%90%EB%A6%AC%EC%97%90-%EB%90%98%EB%8F%8C%EB%A0%A4-%EB%86%93%EA%B8%B0</link><description>&lt;h3&gt;&lt;code&gt;rm -rf ./frontend&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;나는 이 프로젝트를 2019년 정도에 장고 풀스택 기반으로 개발했다가, 2022년 넥스트로 마이그레이션 했었다. 그리고 지금 2025년 다시 장고 풀스택 기반으로 돌아왔다. 이 여정에 대한 짧은 기록을 남기려고 한다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;나를 구원했던 넥스트 ✨&lt;/h3&gt;&lt;p&gt;웹 개발 지식이 거의 전무했던 나는 Jekyll의 템플릿 문법과 동일한 문법을 제공하는 장고를 이용해서 처음 이 프로젝트를 만들었다. 프론트엔드의 거의 대부분은 템플릿 파일로 작성했다가, AJAX에 대해서 알게 되면서 많은 자바스크립트 코드를 작성하게 되었다.&lt;/p&gt;&lt;p&gt;&lt;code&gt;jQuery&lt;/code&gt;로 대충 돔을 조작하다가, 타입스크립트 기반의 바닐라로 전환했다. 하지만, 코드는 걷잡을 수 없이 복잡해져 관리하기가 힘들었다. 결국엔 리액트와 같은 도구를 이용해야 겠다고 생각했지만 SSR이 발목을 잡았기 때문에 고민했다. 그러던 와중에 Next.js를 알게 되었다.&lt;/p&gt;&lt;p&gt;그 아이는 약간의 규칙만 지키면 우아하게 SSR을 해주면서도 리액트로 개발할 수 있는 마법의 도구였다. 무엇보다 사이트가 SPA처럼 동작하는 게 그때는 왜 그렇게 힙해 보였던 건지… 나는 당장 모든 템플릿을 넥스트로 마이그레이션 했다. 장고는 API 서버 역할만 하고, 프론트엔드는 온전히 넥스트가 책임지는 구조. 그때는 그것이 최상의 선택으로 보였다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;내게 깨달음을 준 RSC&lt;/h3&gt;&lt;p&gt;리액트와 넥스트가 발전하면서 서버 컴포넌트라는 개념이 등장했다. 세간은 떠들석(?) 했다. 근데 이상하게 내 눈엔 그게 마음에 들지 않아 보였다. 마치 코드 마지막에 세미콜론을 남기지 않은 것 처럼 그 코드가 묘하게 이질적으로 보였다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-javascript"&gt;import db from './database';

async function Note({id}) {
  const note = await db.notes.get(id);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Author id={note.authorId} /&amp;gt;
      &amp;lt;p&amp;gt;{note}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;나는 이 코드가 PHP 처럼 보였다. PHP가 오히려 나은 점은 그 아이는 태생부터 서버에 쉽게 접근 할 목적으로 등장한 도구였다는 점이다. 리액트는 클라이언트 개발을 목적으로 나왔다가 서버 사이드 도구가 되는 기이한 회귀를 했다. SPA의 등장으로 클라이언트와 서버의 구분이 명확해진 상태에서, 더 나은 최적화를 위해 제안된 RSC가 나에게 던진 메세지는 많은 생각을 하게 만들었다.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;최적화를 하고 싶니? 그럼…&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;결국 어플리케이션을 최적화 하기 위해서는 사이즈를 덜어내고 네트워크를 최적화 해야한다.&lt;/p&gt;&lt;p&gt;그동안 넥스트를 사용하던 대부분의 앱은 프록시 역할을 자처하면서 서버 사이드 렌더링이라는 목표를 달성했다. 하지만 생각해보자, 이 얼마나 비효율적인 구조인가? &lt;strong&gt;세상에서 가장 느린 스펙은 네트워크인데 말이다.&lt;/strong&gt; 넥스트 서버가 클라이언트 요청 받아서, 다시 서버 API를 요청해서, 그걸 받아서 렌더링하는 최고로 느린 스펙이다.&lt;/p&gt;&lt;p&gt;물론 API 서버와 지리적인 위치를 가깝게 둔다면 어느정도 해소가 가능한 문제다. 이 부분에서 가장 이슈가 될 수 있는 점은 최근 넥스트의 행보다. 넥스트의 최신 기능의 일부는 Vercel에서 사용할 수 있게 강제한다.&lt;/p&gt;&lt;p&gt;심지어 넥스트는 리액트를 포함하기 때문에 150kb가 훌쩍 넘는 사이즈를 기본 탑재하고 페이지가 커질수록 스크립트 크기는 점차 비대해지며 서버에서 렌더링한 컴포넌트와 클라이언트 컴포넌트를 하이드레이션하는 과정은 상당한 오류를 유발하며 거대한 비용을 지불하게 한다.&lt;/p&gt;&lt;p&gt;서버 컴포넌트는 이런 일부 문제를 해결하기 위해 나왔지만 이걸 100% 활용하려면 서버 로직을 전부 Node로 변경하여 서버 사이드로 활용(리액트가 DB를 직접 호출)해야 가치가 있다고 생각하며, 그게 아니라면 큰 이점은 없다고 생각했다. (아주 약간 번들 사이즈가 줄어들 수 있는거 정도…?)&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;그래서, 다시 돌아왔다&lt;/h3&gt;&lt;p&gt;이 모든 고민의 끝에 내가 내린 결론은 "다시 클래식한 구조"로 돌아가는 것이었다. 하지만 2019년으로 돌아간 건 절대 아니다. 내 개발 환경은 완전히 달라졌다.&lt;/p&gt;&lt;p&gt;우선 사랑하는 &lt;strong&gt;Vite&lt;/strong&gt;를 앞세워, 스타일링은 &lt;strong&gt;Tailwind CSS&lt;/strong&gt;를 활용해 템플릿에서 사용한 클래스만 빌드하여 최적화 한다. 템플릿을 개발하고 있을 때도 스크립트나 스타일을 Hot Reload해서 빠른 피드백을 받을 수 있게 한다. 그리고 빌드된 자바스크립트는 아일랜드 아키텍처를 응용한 방식으로 내가 원하는 컴포넌트를 필요할 때 지연 로드하여 호출한다. 이게 웹의 근본이자 자바스크립트의 원래 자리다.&lt;/p&gt;&lt;p&gt;사실 이거, 예전에 한창 마이크로 프론트엔드가 부각되면서 유행하던 구조였다. 모듈 페더레이션이니, 매니페스트 파일이니 하면서 등장한 개념들은 백엔드에 의존적인 프론트엔드를 독립적인 구조로 점진적 마이그레이션 하는 것이 주된 목표였다. 이 방식의 문제는 해당 컴포넌트에 대한 SSR을 직접 지원해야 하며, 넥스트와 반대로 백엔드 서버가 프론트 서버에 컴포넌트 렌더링을 API 형태로 요청하게 된다.&lt;/p&gt;&lt;p&gt;아일랜드 아키텍처나 RSC는 이보다 훨씬 진보된 개념으로, 모던 프론트엔드 프레임워크가 주축이 되어 SSR과 제로 번들 목표를 달성하면서도, 지속적으로 뷰의 주도권을 가져 인터렉티브한 요소를 만들기 쉽게 한다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2025/9/4/20259423_gfnL9AixN6nhTpYPwvMA.png" alt="image.png" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;그래서 내가 채택한 방식은, SSR이 중요한 요소 + 상호 작용이 필요한 애들은 서버 사이드 랜더링을 하고 Alpine과 같은 가벼운 스크립트를 붙히는 방식을 택했다. 그게 RSC로 부터 얻은 교훈이었다. 오히려 좀 더 가벼워진? 다만, 리액트의 생태계가 주는 생산성이 거대한걸 부정할 순 없기 때문에 SSR이 필요없는 주요 기능들은 리액트를 어느정도 활용하고, 정말 필요한 시점에 페이지에서 로드하는 방식을 택했다.&lt;/p&gt;&lt;p&gt;처음 프론트엔드를 배울때는 모듈화하고 컴포넌트를 재사용해서 앱을 빠르게 빌딩하는데 초점을 맞췄는데 지금은 뭐랄까… 쉽게 버리고 다시 빨리 만들 수 있는 구조를 빌딩하는데 초점을 맞추고 있는 것 같다. 요즘엔 특히 AI가 있으니 더 쉽게 버릴 수 있으면서도 AI가 쉽게 이해하는 형태로 구성하는데 집중하게 된다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;div x-data="{ toggled: false }"&amp;gt;
    &amp;lt;button
        @click="toggled = !toggled"
        class="bg-blue-500 text-white font-bold py-2 px-4 rounded shadow-md hover:scale-105 transition-transform"
    &amp;gt;
        Toggle
    &amp;lt;/button&amp;gt;
    &amp;lt;p x-show="toggled" class="mt-2"&amp;gt;Hello AI!&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;스타일(테일윈드), 상태(&lt;code&gt;x-data&lt;/code&gt;), 동작(&lt;code&gt;@click&lt;/code&gt;, &lt;code&gt;x-show&lt;/code&gt;)이 전부 하나의 HTML 덩어리 안에 있다. AI가 가장 이해하기 쉽고, 가장 실수 없이 생성할 수 있는 완벽한 형태다. 리액트도 같은 패턴으로 만들 수 있겠지만, 뭐랄까… 이건 좀 더 근본적이다. 번들링이나 트랜스파일링이 필요하지 않은 HTML, 무거운 스크립트 없이 필요한 만큼의 스크립트로 동작하게 된다.&lt;/p&gt;&lt;p&gt;물론 난 여전히 리액트를 좋아하고 다양한 프로젝트에서 리액트를 쓰고 있다. 하지만 어느샌가 리액트라는 도구에 집착하고 있었던 것 같다. 일단 프론트엔드는 리액트로 만들어야 한다는 강박. 빠른 생산성을 위해 가렸던 눈을 서버 컴포넌트가 다시 뜨게 해준 것 같다.&lt;/p&gt;&lt;p&gt;자바스크립트는 조력자가 되어야 한다. 무언가를 최적화 하려면 근본적인 관점에서 다시 들여다 봐야 한다.&lt;/p&gt;</description><pubDate>Thu, 04 Sep 2025 23:21:32 +0900</pubDate><guid>http://blex.me/@baealex/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A5%BC-%EC%9B%90%EB%9E%98-%EC%9E%90%EB%A6%AC%EC%97%90-%EB%90%98%EB%8F%8C%EB%A0%A4-%EB%86%93%EA%B8%B0</guid></item><item><title>앵귤러는 화면을 어떻게 그릴까?</title><link>http://blex.me/@baealex/how-angular-renders-the-screen</link><description>&lt;p&gt;프론트엔드를 개발하는 사람이라면 리액트(React)의 '가상돔(Virtual DOM)' 이라는 개념에 대해서 자주 들어봤을 것이다. 리액트는 상태 변경을 요청할 때 가상돔을 재생성하여 필요한 곳을 업데이트 한다.&lt;/p&gt;&lt;p&gt;나는 최근에 앵귤러를 다루면서 성능적인 이슈를 대응할 필요가 있었고 이를 위해서 렌더링 방식에 대해서 알아야 했다. 앵귤러에는 가상돔과 같은 개념은 존재하지 않는다. 앵귤러는 대체 어떻게 작동하고 있는 걸까? 🤔&lt;/p&gt;&lt;h4&gt;✨ Zone JS&lt;/h4&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2025/4/6/20254623_Ru4tcxKcwxaKUyVd4pPe.png" alt=""&gt;&lt;/figure&gt;&lt;p&gt;웹 사이트에는 눈에 보이지 않는 수 많은 일들이 계속 일어난다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;사용자가 마우스를 클릭하거나 키보드를 누르는 &lt;strong&gt;이벤트&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;"3초 뒤에 이 함수 실행!" 같은 &lt;strong&gt;setTimeout&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;서버에서 데이터를 가져오는 &lt;strong&gt;HTTP 요청&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;앵귤러는 이런 일들이 언제 시작되고 언제 끝나는지 알아야 한다. "데이터 로딩이 끝났으니 이제 화면을 바꿔야겠군!" 하고 눈치챌 수 있어야 한다.&lt;/p&gt;&lt;p&gt;이를 해결하는 것이 &lt;strong&gt;Zone.js&lt;/strong&gt; 이다. Zone.js는 위에 나열된 브라우저의 비동기 동작들을 몽키패칭하여 작업을 추적할 수 있다. 비동기 작업이 완료되면 Zone.js가 이를 감지하고 Angular에게 알려준다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;function addEventListener(eventName, callback) {
     // call the real addEventListener
     callRealAddEventListener(eventName, function() {
        // first call the original callback
        callback(...);     
        // and then run Angular-specific functionality
        var changed = angular.runChangeDetection();
         if (changed) {
             angular.reRenderUIPart();
         }
     });
}
&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;✨ runChangeDetection&lt;/h4&gt;&lt;p&gt;Zone.js가 "작업 완료!" 신호를 보내면, Angular는 이제 "무엇이 변했는지" 확인해야 한다. 위 코드 예시에서 &lt;code&gt;angular.runChangeDetection()&lt;/code&gt; 부분이 바로 이 역할을 개념적으로 나타낸다. 이것이 바로 &lt;strong&gt;변경 감지(Change Detection)&lt;/strong&gt; 과정이다.&lt;/p&gt;&lt;p&gt;React의 가상돔(Virtual DOM)과 달리, Angular는 컴포넌트의 상태(데이터)가 이전과 비교해서 달라졌는지 직접 확인한다. 도대체 어떻게?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;데이터 바인딩 확인:&lt;/strong&gt; Angular는 템플릿에 있는 모든 데이터 바인딩(예: &lt;code&gt;{{ title }}&lt;/code&gt;, &lt;code&gt;[value]="count"&lt;/code&gt;)을 기억하고 있다가 변경 감지가 시작되면, Angular는 각 바인딩에 연결된 컴포넌트의 현재 값(예: &lt;code&gt;this.title&lt;/code&gt;, &lt;code&gt;this.count&lt;/code&gt;)을 확인한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;이전 값과 비교:&lt;/strong&gt; Angular는 이전에 기억해둔 값과 현재 값을 비교한다. 만약 &lt;code&gt;this.title&lt;/code&gt;의 값이 이전과 다르다면, "변경되었군!"이라고 표시한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;변경 상태 전파:&lt;/strong&gt; 이 과정은 애플리케이션의 모든 컴포넌트를 대상으로 (기본 전략상) 위에서 아래로 진행된다. 부모 컴포넌트부터 자식 컴포넌트까지 차례대로 확인하며 변경된 부분을 찾아낸다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;code&gt;runChangeDetection()&lt;/code&gt;은 바로 이 꼼꼼한 '비교 및 확인 작업' 전체를 의미한다. 이 과정의 결과로 "어떤 부분이 변경되었는지"에 대한 정보가 모인다. 코드 예시의 &lt;code&gt;var changed&lt;/code&gt; 변수는 이 비교 과정에서 단 하나라도 변경된 사항이 있었는지를 나타내는 셈이다.&lt;/p&gt;&lt;h4&gt;✨ reRenderUIPart&lt;/h4&gt;&lt;p&gt;변경 감지를 통해 "어떤 데이터가 바뀌었는지" 확인했다면, 이제 그 결과를 실제 사용자 화면에 반영해야 한다. 코드 예시의 &lt;code&gt;if (changed) { angular.reRenderUIPart(); }&lt;/code&gt; 부분이 이 마지막 단계를 나타낸다.&lt;/p&gt;&lt;p&gt;여기서 중요한 점은 Angular가 &lt;strong&gt;변경된 부분만 정확히 찾아 실제 DOM을 업데이트&lt;/strong&gt;한다는 것이다. 이에 필요한 정보들은 앵귤러가 앱을 컴파일하면서 기억해 둔다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;가상돔 없음:&lt;/strong&gt; React처럼 가상돔 전체를 새로 만들고 비교하는 과정이 없다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;직접 DOM 조작:&lt;/strong&gt; Angular는 2단계에서 변경되었다고 확인된 데이터 바인딩과 연결된 실제 DOM 요소를 직접 찾아간다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;예를 들어, 컴포넌트의 &lt;code&gt;title&lt;/code&gt; 속성이 'Hello'에서 'World'로 바뀌었고, 이것이 템플릿의 &lt;code&gt;&amp;lt;h1&amp;gt;{{ title }}&amp;lt;/h1&amp;gt;&lt;/code&gt; 부분과 연결되어 있다면, Angular는 정확히 해당 &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 태그를 찾아 그 안의 텍스트 내용만 'World'로 업데이트한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;만약 &lt;code&gt;[disabled]="isButtonDisabled"&lt;/code&gt; 바인딩 값이 &lt;code&gt;false&lt;/code&gt;에서 &lt;code&gt;true&lt;/code&gt;로 바뀌었다면, 해당 버튼 요소의 &lt;code&gt;disabled&lt;/code&gt; 속성을 직접 추가한다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;최소한의 업데이트:&lt;/strong&gt; 전체 HTML 구조를 새로 그리는 것이 아니라, 필요한 부분만 정밀하게 수정한다. 이 방식 덕분에 효율적인 화면 업데이트가 가능하다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;따라서 &lt;code&gt;reRenderUIPart()&lt;/code&gt;는 비록 개념적인 이름이지만, 실제로는 Angular가 변경된 데이터를 바탕으로 &lt;strong&gt;실제 DOM의 특정 부분(텍스트 노드, 속성 등)을 직접적이고 효율적으로 수정하는 과정&lt;/strong&gt;을 의미한다.&lt;/p&gt;&lt;h4&gt;😆 보너스: 성능을 높히는 OnPush 전략&lt;/h4&gt;&lt;p&gt;지금까지 설명한 Angular의 변경 감지 과정(Zone.js 감지 -&amp;gt; 변경 확인 -&amp;gt; DOM 업데이트)은 기본(Default) 전략이다. 이 전략은 매우 편리하지만, 애플리케이션이 복잡해지면 때로는 불필요한 확인 작업이 성능에 부담을 줄 수도 있다. Zone.js가 아주 사소한 비동기 작업 완료 신호만 보내도, Angular는 잠재적으로 모든 컴포넌트의 변경 여부를 확인하기 때문이다.&lt;/p&gt;&lt;p&gt;이럴 때 사용할 수 있는 강력한 최적화 기법이 바로 &lt;code&gt;ChangeDetectionStrategy.OnPush&lt;/code&gt; 전략이다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;OnPush 전략이란?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;컴포넌트 설정에 &lt;code&gt;changeDetection: ChangeDetectionStrategy.OnPush&lt;/code&gt;를 추가하면, 해당 컴포넌트는 더 이상 모든 Zone.js의 신호에 반응하여 무조건 변경 감지를 수행하지 않는다. 대신, &lt;strong&gt;다음과 같은 특정 조건 중 하나가 만족될 때만&lt;/strong&gt; 변경 감지를 진행한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;새로운 참조(Reference)의 &lt;/strong&gt;&lt;code&gt;@Input()&lt;/code&gt;&lt;strong&gt; 변경:&lt;/strong&gt; 컴포넌트의 &lt;code&gt;@Input()&lt;/code&gt; 프로퍼티로 &lt;strong&gt;새로운 객체나 배열 참조&lt;/strong&gt;가 전달될 때. (주의: 객체 내부의 속성만 바뀌거나 배열 요소만 추가/삭제되는 등 참조 자체가 변경되지 않으면 감지하지 못한다.)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;컴포넌트 또는 자식 요소의 이벤트 발생:&lt;/strong&gt; 해당 컴포넌트의 템플릿 내부나 그 자식 컴포넌트에서 &lt;strong&gt;이벤트(예: 클릭, 입력 등)가 발생&lt;/strong&gt;하여 이벤트 핸들러가 실행될 때.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;async&lt;/code&gt;&lt;strong&gt; 파이프 사용:&lt;/strong&gt; 템플릿에서 &lt;code&gt;async&lt;/code&gt; 파이프가 새로운 값을 방출(emit)할 때. (&lt;code&gt;async&lt;/code&gt; 파이프는 내부적으로 변경 감지를 요청한다.)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;명시적 요청:&lt;/strong&gt; 개발자가 컴포넌트의 &lt;code&gt;ChangeDetectorRef&lt;/code&gt;를 주입받아 &lt;code&gt;markForCheck()&lt;/code&gt; 메소드를 직접 호출하여 변경 감지가 필요함을 명시적으로 알릴 때.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;왜 성능이 향상될까?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;OnPush&lt;/code&gt; 전략을 사용하면, 해당 컴포넌트는 오직 '자신과 직접적으로 관련된 변화'가 발생했을 가능성이 높을 때만 변경 감지를 수행합니다. 애플리케이션의 다른 부분에서 발생한 비동기 작업 완료에는 영향을 받지 않고 '독립적'으로 동작하는 것이기 때문이다.&lt;/p&gt;&lt;p&gt;이는 마치 "내게 꼭 필요한 정보가 업데이트되거나, 내 구역에서 직접적인 요청이 있을 때만 확인하겠다"고 선언하는 것과 같다. 불필요한 확인 작업을 대폭 줄여주므로, 특히 컴포넌트 트리가 깊고 복잡한 대규모 애플리케이션에서 성능 향상 효과를 크게 볼 수 있다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;주의할 점:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;OnPush&lt;/code&gt;를 사용하면 &lt;code&gt;@Input&lt;/code&gt;으로 받은 객체의 내부 속성만 변경하는 방식(mutable update)으로는 변경 감지가 자동으로 일어나지 않는다. 따라서 &lt;code&gt;OnPush&lt;/code&gt; 컴포넌트에 데이터를 전달할 때는 항상 새로운 객체나 배열 참조를 생성하여 전달하는 방식(immutable update)을 사용하는 것이 중요하다. 또는 필요시 &lt;code&gt;markForCheck()&lt;/code&gt;를 사용하여 수동으로 변경 감지를 예약해야 한다.&lt;/p&gt;&lt;p&gt;&lt;code&gt;OnPush&lt;/code&gt; 전략은 Angular의 변경 감지 시스템을 더 깊이 이해하고 애플리케이션 성능을 한 단계 끌어올리고 싶을 때 고려해볼 만한 강력한 도구다.&lt;/p&gt;&lt;h4&gt;참고 자료&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/"&gt;https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4&gt;번외&lt;/h4&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/title/2023/6/21/baealex/0_7azVXV7G.png" alt=""&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;뭔가 스벨트(Svelte)랑 비슷...한가?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;여기까지 생각하다 보니, 문득 다른 친구가 떠올랐다. 바로 스벨트다. 스벨트도 컴파일 방식을 적용한 프레임워크이고, 가상 DOM 안 쓰고, 컴파일할 때 최적의 자바스크립트 코드를 만들어낸다는 점에서 어쩐지 앵귤러랑 좀 흡사해 보였다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;결정적인 차이는?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;비슷해 보이지만 결정적인 차이가 있었는데, 바로 &lt;strong&gt;'런타임(Runtime)'&lt;/strong&gt; 번들의 사이즈다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;스벨트:&lt;/strong&gt; 이 친구는 컴파일하고 나면 거의 자기 흔적(런타임 코드)을 남기지 않는다. 그냥 순수한 자바스크립트 코드만 남아서 브라우저에서 직접 DOM을 막 조작한다. 스벨트의 핵심 철학인 '프레임워크가 사라지는' 느낌이다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;앵귤러:&lt;/strong&gt; 컴파일을 열심히 하긴 하지만, 여전히 브라우저에는 꽤 덩치 있는 &lt;strong&gt;'앵귤러 엔진(런타임)'&lt;/strong&gt;이 남아서 전체를 관리한다. 런타임에 변경 감지 시스템을 돌리고, 의존성 주입이다 뭐다 이것저것 챙겨야 할게 많다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;</description><pubDate>Sun, 06 Apr 2025 23:11:46 +0900</pubDate><guid>http://blex.me/@baealex/how-angular-renders-the-screen</guid></item><item><title>Sass @import 및 Legacy API 해결 방법</title><link>http://blex.me/@baealex/sass-import-legacy-api-deprecation</link><description>&lt;h4 id="import"&gt;@Import&lt;/h4&gt;&lt;blockquote&gt;
&lt;p&gt;Deprecation Warning: Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;@import 구문이 사라진다고 한다. (= 정확히는 css @import 구문으로 바뀌는 거다.) 따라서 이 구문 대신 @use와 @forward를 사용하라고 하는데 한번도 사용해보지 않아서 어떻게 사용하는지 잘 몰랐다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;styles/var.scss&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;@import '@baejino/style/scss/var';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;특정 scss에서 라이브러리의 var를 통합하거나 다른 파일에 선언한 var를 가져오고 있었다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;@forward '@baejino/style/scss/var';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 경우에는 그냥 forward로 바꿔주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Component/Component.module.scss&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;@import '~/styles/var';

.Component {
    @media (min-width: $BREAKPOINT_DESKTOP) {
        &amp;amp;:hover {
            background-color: #2a2a2a;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제로 변수를 사용하려는 경우 use 구문으로 바꿔주면 된다. import 구문을 사용할 때는 변수를 즉시 선언해서 사용할 수 있었지만 use의 경우에는 네임스페이스 기반으로 변수들을 호출할 수 있다고 한다. 파일명이 네임스페이스인 것으로 보인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;@use '~/styles/var';

.Component {
    @media (min-width: var.$BREAKPOINT_DESKTOP) {
        &amp;amp;:hover {
            background-color: #2a2a2a;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;뒤에 as 구문을 사용해서 네임스페이스의 별칭을 만들어 줄 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;@use '~/styles/var' as a;

.Component {
    @media (min-width: a.$BREAKPOINT_DESKTOP) {
        &amp;amp;:hover {
            background-color: #2a2a2a;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;as * 구문을 사용하면 import 하던 것과 동일하게 네임스페이스 없이 변수를 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;@use '~/styles/var' as *;

.Component {
    @media (min-width: $BREAKPOINT_DESKTOP) {
        &amp;amp;:hover {
            background-color: #2a2a2a;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="legacy-js-api"&gt;Legacy JS API&lt;/h4&gt;&lt;blockquote&gt;
&lt;p&gt;Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Vite의 번들러의 기본 설정을 사용하고, 딱히 뭘 선언해준건 없었는데 어느날 위와같이 경고가 떳다. 해결법을 찾아보니 API를 modern으로 바꿔주면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;export default defineConfig({
    css: { preprocessorOptions: { scss: { api: 'modern' } } },
})
&lt;/code&gt;&lt;/pre&gt;
</description><pubDate>Thu, 21 Nov 2024 00:09:42 +0900</pubDate><guid>http://blex.me/@baealex/sass-import-legacy-api-deprecation</guid></item><item><title>쉐도우 돔을 스타일링 하는 방법</title><link>http://blex.me/@baealex/shadow-dom-styling</link><description>&lt;p&gt;HTML은 모두 DOM으로 변환된다. DOM은 자바스크립트로 엑세스하고 CSS로 스타일링 할 수 있다. Shadow DOM은 요소 내부에 Shadow DOM 이라는 자체 DOM을 생성할 수 있다.&lt;/p&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/1/20249120_KquJs1UgOe00ppH7vRFD.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;웹 사이트의 소스를 보다가 위와 같은 것을 발견한다면 그것이 쉐도우 돔이 적용된 것이다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Shadow DOM을 사용하는 이유?&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;페이지의 다른 자바스크립트나 CSS로 부터 컴포넌트를 보호&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;컴포넌트에 대한 캡슐화된 스타일을 만들 수 있음&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;컴포넌트 내부 요소에 안전하게 ID를 사용할 수 있음&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;Shadow DOM 생성&lt;/h2&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;const shadow = document.getElementById('shadow');
const shadowRoot = shadow.attachShadow({ mode: 'closed' });&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;쉐도우 돔은 위와 같이 간단하게 생성할 수 있다. mode는 &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;closed&lt;/code&gt; 2가지 값으로 설정할 수 있는데 두 옵션의 차이는 다음과 같다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;// open
const shadowRoot = shadow.attachShadow({ mode: 'open' });
shadow.shadowRoot // 접근 가능 (= shadowRoot)

// closed
const shadowRoot = shadow.attachShadow({ mode: 'open' });
shadow.shadowRoot // 접근 불가 (= null)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h2&gt;Sadow DOM 스타일링&lt;/h2&gt;&lt;p&gt;쉐도우 돔은 기본적으로 외부 스타일에 영향을 받지 않는다. 예를들어 아래와 같은 코드가 있다고 가정하면&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    span {
        color: red;
     }
&amp;lt;/style&amp;gt;

&amp;lt;span&amp;gt;Light DOM Content&amp;lt;/span&amp;gt;
&amp;lt;div id="shadow"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    const shadow = document.getElementById('shadow');
    const shadowRoot = shadow.attachShadow({ mode: 'closed' });
    shadowRoot.innerHTML = `
        &amp;lt;span&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
    `;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/5/20249518_DDrSxs7lWGO4J97pdc8p.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;결과는 위와 같이 출력된다. 다만 일부 상황에서는 쉐도우 돔과 라이트 돔이 스타일을 공유하거나 영향을 줄 수 있다. 먼저 라이트 돔의 스타일이 쉐도우 돔의 영향을 주는 케이스다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;라이트 -&amp;gt; 쉐도우&lt;/h4&gt;&lt;h6&gt;1. 스타일 상속&lt;/h6&gt;&lt;p&gt;Light DOM에서 Shadow DOM이 생성된 엘리먼트(Host)로 스타일이 상속될 경우 Shadow DOM에도 스타일이 반영된다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    body {
        color: blue;
    }
    span {
        color: red;
     }
&amp;lt;/style&amp;gt;

&amp;lt;span&amp;gt;Light DOM Content&amp;lt;/span&amp;gt;
&amp;lt;div id="shadow"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    const shadow = document.getElementById('shadow');
    const shadowRoot = shadow.attachShadow({ mode: 'closed' });
    shadowRoot.innerHTML = `
        &amp;lt;span&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
    `;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;body의 스타일은 Shadow DOM의 호스트에도 상속되기 때문에 결과는 아래와 같아진다.&lt;/p&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/5/20249518_JjA9O0ptU2lK4dDCnc5C.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;Shadow DOM에서 이러한 상속을 모두 제한하려는 경우 아래와 같이 호스트의 상속을 막을 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;shadowRoot.innerHTML = `
    &amp;lt;style&amp;gt; :host { all: initial; } &amp;lt;/style&amp;gt;
    &amp;lt;span&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/5/20249518_DDrSxs7lWGO4J97pdc8p.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;그럼 다시 결과는 원점으로 돌아간다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h6&gt;2. 사용자 정의 속성&lt;/h6&gt;&lt;p&gt;&lt;code&gt;:root&lt;/code&gt;에 정의된 사용자 정의 속성(variant)은 Shadow DOM에서도 사용할 수 있다. 상속으로 스타일을 주입하는 것 보다 훨씬 더 예측 가능하고 사이드 이펙트가 적다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    :root {
        --my-favorite-color: #735af2;
    }
    span {
        color: var(--my-favorite-color);
    }
&amp;lt;/style&amp;gt;

&amp;lt;span&amp;gt;Light DOM Content&amp;lt;/span&amp;gt;
&amp;lt;div id="shadow"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    const shadow = document.getElementById('shadow');
    const shadowRoot = shadow.attachShadow({ mode: 'closed' });
    shadowRoot.innerHTML = `
        &amp;lt;style&amp;gt;
            span {
                color: var(--my-favorite-color);
            }
        &amp;lt;/style&amp;gt;
        &amp;lt;span&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
    `;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/5/20249518_nEI0wOfvTOp5fzxTggwN.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h6&gt;3. 의사 요소&lt;/h6&gt;&lt;p&gt;&lt;code&gt;part&lt;/code&gt; 속성을 이용해서 외부에서 Shadow DOM에 스타일을 주입할 수 있도록 할 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    span {
        color: #735af2;
    }
    #shadow::part(shadow-content) {
        color: #735af2;
    }
&amp;lt;/style&amp;gt;

&amp;lt;span&amp;gt;Light DOM Content&amp;lt;/span&amp;gt;
&amp;lt;div id="shadow"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    const shadow = document.getElementById('shadow');
    const shadowRoot = shadow.attachShadow({ mode: 'closed' });
    shadowRoot.innerHTML = `
        &amp;lt;span part="shadow-content"&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
    `;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/5/20249518_nEI0wOfvTOp5fzxTggwN.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h6&gt;4. 템플릿 슬롯&lt;/h6&gt;&lt;p&gt;커스텀 엘리먼트를 선언할 때 사용하는 슬롯을 사용할 때 스타일은 의도한 것과 다르게 들어가므로 유의해야 한다. Shadow DOM 내부에서 스타일이 주입되는 것이 아니라 외부에서 스타일이 주입되는 것에 유의해야 한다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    p {
        color: blue;
    }
&amp;lt;/style&amp;gt;

&amp;lt;template id="my-component-template"&amp;gt;
    &amp;lt;style&amp;gt;
        p {
            color: red;
        }
    &amp;lt;/style&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;my-component&amp;gt;
    &amp;lt;p&amp;gt;External content&amp;lt;/p&amp;gt;
&amp;lt;/my-component&amp;gt;

&amp;lt;script&amp;gt;
    class MyComponent extends HTMLElement {
        constructor() {
            super();
            this.attachShadow({ mode: 'open' });
            const template = document.querySelector('#my-component-template');
            this.shadowRoot.appendChild(template.content.cloneNode(true));
        }
    }

    customElements.define('my-component', MyComponent);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/7/20249710_xuBOAMFLLDDdp6IxA2Zl.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;쉐도우&lt;/h4&gt;&lt;h6&gt;1. 템플릿 슬롯&lt;/h6&gt;&lt;p&gt;템플릿 내부에서 slot의 스타일을 지정하려는 경우, &lt;code&gt;::slotted&lt;/code&gt; 선택자를 사용하여 스타일을 적용할 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    p {
        color: blue;
    }
&amp;lt;/style&amp;gt;

&amp;lt;template id="my-component-template"&amp;gt;
    &amp;lt;style&amp;gt;
        ::slotted(p) {
            color: red !important;
        }
    &amp;lt;/style&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;my-component&amp;gt;
    &amp;lt;p&amp;gt;External content&amp;lt;/p&amp;gt;
&amp;lt;/my-component&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/7/20249710_AbGiyTKuko19Q06VWMAh.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h6&gt;2. 호스트&lt;/h6&gt;&lt;p&gt;위에서 잠시 언급이 되었지만 &lt;code&gt;:host&lt;/code&gt; 선택자를 사용하면 쉐도우 돔을 렌더하고 있는 호스트의 스타일링을 해줄 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    span {
        color: red;
    }
&amp;lt;/style&amp;gt;

&amp;lt;span&amp;gt;Light DOM Content&amp;lt;/span&amp;gt;
&amp;lt;div id="shadow"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    const shadow = document.getElementById('shadow');
    const shadowRoot = shadow.attachShadow({ mode: 'closed' });
    shadowRoot.innerHTML = `
        &amp;lt;style&amp;gt;
            :host {
                color: blue;
            }
        &amp;lt;/style&amp;gt;
        &amp;lt;span&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
    `;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/5/20249518_JjA9O0ptU2lK4dDCnc5C.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;code&gt;:host-context&lt;/code&gt; 선택자를 사용하면 호스트의 부모 요소의 적용된 클래스를 참조하여 스타일링 할 수 있다. 예를들어 다크 모드와 같은 기능을 만들 때 특히 유용할 수 있다. 참고로 해당 글 작성일 기준으로 사파리와 파이어폭스에서는 동작하지 않는 기능이다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
      body.dark {
        background: black;
    }
    span {
        color: red;
    }
&amp;lt;/style&amp;gt;

&amp;lt;span&amp;gt;Light DOM Content&amp;lt;/span&amp;gt;
&amp;lt;div id="shadow"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    document.body.classList.add('dark');

    const shadow = document.getElementById('shadow');
    const shadowRoot = shadow.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
        &amp;lt;style&amp;gt;
            :host-context(body.dark) span {
                color: white;
            }
        &amp;lt;/style&amp;gt;
        &amp;lt;span&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
    `;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/7/20249711_XRUN1RtHZmv8D12ulOWZ.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h6&gt;3. 스타일 임포트&lt;/h6&gt;&lt;p&gt;Light DOM에서 Global로 사용하는 스타일이 있는 경우 간단하게 import 구문을 사용할 수 있다. 별도의 라이브러리나 별도의 처리 없이 브라우저 네이티브로 동작하며, 이미 다운로드된 스타일 시트의 경우 캐시되어 불필요한 리소스가 낭비되지 않는다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;template id="my-component-template"&amp;gt;
    &amp;lt;style&amp;gt;
        @import '/assets/style/normalize.css';
    &amp;lt;/style&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;h6&gt;4. 생성 가능한 스타일 시트&lt;/h6&gt;&lt;p&gt;Constructable Stylesheets는 Shadow DOM에서 공유 가능한 스타일시트를 제공하여, 재사용성과 성능을 극대화할 수 있다. 또한 엔드 유저에게 별도의 스타일이 노출되지 않는다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;style&amp;gt;
    span {
        color: red;
    }
&amp;lt;/style&amp;gt;

&amp;lt;span&amp;gt;Light DOM Content&amp;lt;/span&amp;gt;
&amp;lt;div id="shadow"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    const shadow = document.getElementById('shadow');
    const shadowRoot = shadow.attachShadow({ mode: 'open' });
    const styleSheet = new CSSStyleSheet();
    styleSheet.replace(`
        span {
            color: blue;
        }
    `)
    shadowRoot.adoptedStyleSheets = [styleSheet];
    shadowRoot.innerHTML = `
        &amp;lt;span&amp;gt;Shadow DOM Content&amp;lt;/span&amp;gt;
    `;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;figure data-border="true" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; border: 1px solid rgb(229, 231, 235); overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/5/20249518_JjA9O0ptU2lK4dDCnc5C.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;figure data-shadow="true" data-border-radius="16" style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; box-shadow: rgba(0, 0, 0, 0.15) 8px 8px 40px 2px; border-radius: 16px; overflow: hidden;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/9/7/20249711_9uAaDvXBt2TQJNSj7uFX.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h4&gt;참고 자료&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://jordanbrennan.hashnode.dev/8-ways-to-style-the-shadow-dom"&gt;https://jordanbrennan.hashnode.dev/8-ways-to-style-the-shadow-dom&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;</description><pubDate>Sun, 08 Sep 2024 20:00:00 +0900</pubDate><guid>http://blex.me/@baealex/shadow-dom-styling</guid></item><item><title>리액트 18 서스펜스 (Suspense)</title><link>http://blex.me/@baealex/what-is-react-suspense</link><description>&lt;h4&gt;React Suspense 기본 개념&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;Suspense&lt;/strong&gt;는 하위 컴포넌트가 아직 화면에 보여줄 준비가 되지 않았을 때, 가장 가까운 &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; 경계의 &lt;code&gt;fallback&lt;/code&gt;을 대신 보여주는 React의 경계 컴포넌트다.&lt;/p&gt;&lt;p&gt;처음에는 Suspense를 “비동기 작업을 기다려주는 도구” 정도로 이해했는데, 그렇게만 설명하면 조금 위험하다. Suspense는 아무 Promise나 자동으로 기다려주는 마법 상자가 아니다. React가 인식할 수 있는 방식으로 하위 컴포넌트가 “아직 준비되지 않았다”고 알려야 하고, 그때 Suspense가 fallback UI를 보여준다.&lt;/p&gt;&lt;p&gt;그래서 Suspense의 핵심은 로딩 상태를 컴포넌트 안에서 매번 조건문으로 처리하는 대신, &lt;strong&gt;준비되지 않은 UI를 특정 경계 밖으로 밀어내는 것&lt;/strong&gt;에 가깝다고 생각한다.&lt;/p&gt;&lt;p&gt;단순히 &lt;code&gt;fallback&lt;/code&gt;을 넣으면 로딩 UI가 보인다는 설명만으로는 Suspense가 잘 와닿지 않았다. 내가 궁금했던 것은 “React는 무엇을 보고 이 컴포넌트가 아직 준비되지 않았다고 판단할까?”였다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;가장 기본적인 사용: React.lazy&lt;/h4&gt;&lt;p&gt;Suspense를 가장 쉽게 이해할 수 있는 예시는 &lt;code&gt;React.lazy&lt;/code&gt;다. &lt;code&gt;React.lazy&lt;/code&gt;를 사용하면 컴포넌트 코드를 동적으로 불러올 수 있고, 아직 해당 코드가 로드되지 않았을 때 Suspense의 &lt;code&gt;fallback&lt;/code&gt;이 표시된다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;import React, { Suspense } from 'react';

const MyComponent = React.lazy(() =&amp;gt; import('./MyComponent'));

function App() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;MyComponent /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 코드에서 &lt;code&gt;MyComponent&lt;/code&gt;의 코드가 아직 다운로드되지 않았다면, React는 &lt;code&gt;MyComponent&lt;/code&gt;를 바로 보여줄 수 없다. 이때 가장 가까운 Suspense 경계가 &lt;code&gt;fallback&lt;/code&gt;으로 지정한 &lt;code&gt;Loading...&lt;/code&gt;을 보여준다.&lt;/p&gt;&lt;p&gt;즉 Suspense는 “로딩 UI를 보여줄 위치”를 컴포넌트 트리 안에 선언해두는 방식이다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;데이터 fetching과 Suspense&lt;/h4&gt;&lt;p&gt;Suspense는 코드 스플리팅뿐 아니라 데이터 fetching과도 함께 사용할 수 있다. 다만 여기서 주의할 점이 있다. 일반적인 &lt;code&gt;fetch()&lt;/code&gt; Promise를 컴포넌트 안에서 만들었다고 해서 Suspense가 자동으로 그것을 기다려주지는 않는다.&lt;/p&gt;&lt;p&gt;데이터 fetching에서 Suspense를 쓰려면 Suspense와 통합된 프레임워크나 라이브러리를 사용하는 편이 안전하다. 예를 들어 TanStack Query에서는 Suspense용 API인 &lt;code&gt;useSuspenseQuery&lt;/code&gt;를 제공한다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;import { Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';

function Posts() {
  const { data } = useSuspenseQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  return (
    &amp;lt;ul&amp;gt;
      {data.map((post) =&amp;gt; (
        &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}

function App() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;게시글을 불러오는 중...&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;Posts /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이런 구조에서는 &lt;code&gt;Posts&lt;/code&gt;가 데이터를 준비하지 못한 동안 Suspense 경계의 fallback이 보여진다. 컴포넌트 안에서 &lt;code&gt;isLoading&lt;/code&gt;을 직접 검사해서 분기하지 않아도, 로딩 상태를 바깥 경계에서 다룰 수 있다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;응용해보기: 어떻게 동작하는 걸까?&lt;/h4&gt;&lt;p&gt;Suspense의 동작을 이해하려면 “준비되지 않은 컴포넌트가 Promise를 던진다”는 모델을 떠올릴 수 있다. 하위 컴포넌트가 렌더링 중에 Promise를 던지면, React는 해당 컴포넌트를 지금은 보여줄 수 없다고 판단하고 가장 가까운 Suspense 경계의 fallback을 보여준다.&lt;/p&gt;&lt;p&gt;여기서부터의 코드는 실무용 데이터 fetching 도구를 만들기 위한 코드라기보다, Suspense가 fallback으로 전환되는 조건을 직접 확인하기 위한 실험에 가깝다.&lt;/p&gt;&lt;p&gt;하지만 이 방식을 애플리케이션 코드에서 직접 구현해서 사용하는 것은 권장하기 어렵다. React 공식 문서에서도 Suspense를 지원하는 데이터 소스는 프레임워크나 라이브러리 수준에서 통합되는 흐름을 전제로 설명한다. 아래 코드는 “이런 식으로 동작을 이해할 수 있다”는 참고용에 가깝다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;// 이해를 위한 예시일 뿐, 일반 애플리케이션 코드에서 권장하는 패턴은 아니다.
function LazyComponent() {
  const data = fetchSomething();

  if (data instanceof Promise) {
    throw data;
  }

  return &amp;lt;div&amp;gt;{data}&amp;lt;/div&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 코드는 문제가 많다. React 컴포넌트는 여러 번 렌더링될 수 있으므로, 렌더링할 때마다 새로운 Promise를 만들면 같은 요청이 반복될 수 있다. Suspense와 함께 쓰려면 같은 작업에 대해서는 같은 Promise나 결과를 재사용할 수 있어야 한다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;왜 캐시가 필요한가&lt;/h4&gt;&lt;p&gt;Suspense에서 데이터를 다룰 때 캐시가 중요한 이유는 같은 데이터 요청에 대해 매번 새로운 Promise를 만들면 안 되기 때문이다. 렌더링 중 Promise를 던졌는데, 다음 렌더링에서도 또 새로운 Promise를 만들면 React 입장에서는 계속 “아직 준비되지 않은 상태”처럼 보일 수 있다.&lt;/p&gt;&lt;p&gt;아주 단순하게 표현하면 아래와 같은 형태를 생각할 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;const cache = new Map&amp;lt;string, unknown&amp;gt;();

function readData&amp;lt;T&amp;gt;(key: string, fn: () =&amp;gt; Promise&amp;lt;T&amp;gt;): T {
  if (!cache.has(key)) {
    const promise = fn().then((data) =&amp;gt; {
      cache.set(key, data);
    });

    cache.set(key, promise);
  }

  const cached = cache.get(key);

  if (cached instanceof Promise) {
    throw cached;
  }

  return cached as T;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 예시는 Suspense의 내부 모델을 이해하기 위한 단순화된 코드다. 실제 서비스에서 이 정도 캐시만으로 데이터 fetching을 직접 구현하기는 어렵다. 에러 처리, 재시도, 무효화, 중복 요청 제거, 서버 렌더링과의 연결 같은 문제가 계속 따라오기 때문이다.&lt;/p&gt;&lt;p&gt;그래서 실무에서는 직접 Suspense용 데이터 캐시를 만들기보다, 프레임워크나 TanStack Query 같은 라이브러리의 Suspense 지원을 사용하는 편이 낫다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;프로미스 상태를 감싸는 방식&lt;/h4&gt;&lt;p&gt;Suspense를 설명할 때 자주 등장하는 예시로 &lt;code&gt;read()&lt;/code&gt; 함수를 가진 resource 객체가 있다. Promise 상태에 따라 pending이면 Promise를 던지고, 실패하면 에러를 던지고, 성공하면 값을 반환하는 방식이다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;function createSuspenseResource&amp;lt;T&amp;gt;(promise: Promise&amp;lt;T&amp;gt;) {
  let status: 'pending' | 'success' | 'error' = 'pending';
  let result: T;
  let error: unknown;

  const suspender = promise.then(
    (value) =&amp;gt; {
      status = 'success';
      result = value;
    },
    (reason) =&amp;gt; {
      status = 'error';
      error = reason;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      }

      if (status === 'error') {
        throw error;
      }

      return result;
    },
  };
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 코드는 Suspense가 “값이 준비되지 않았을 때 fallback으로 전환되는 방식”을 이해하는 데는 도움이 된다. 하지만 이것을 그대로 커스텀 훅으로 감싸서 데이터 fetching 도구처럼 쓰는 것은 조심해야 한다.&lt;/p&gt;&lt;p&gt;특히 &lt;code&gt;useEffect&lt;/code&gt; 안에서 Promise를 만들고 resource를 설정하는 방식은 Suspense의 핵심 흐름과 잘 맞지 않는다. Suspense는 렌더링 중에 하위 컴포넌트가 준비되지 않았음을 알릴 때 동작하는데, &lt;code&gt;useEffect&lt;/code&gt;는 렌더링이 끝난 뒤에 실행되기 때문이다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;정리&lt;/h4&gt;&lt;p&gt;Suspense는 단순히 “비동기 작업을 기다리는 도구”라기보다, 아직 준비되지 않은 UI를 어디에서 대신 보여줄지 정하는 경계에 가깝다.&lt;/p&gt;&lt;p&gt;코드 스플리팅에서는 &lt;code&gt;React.lazy&lt;/code&gt;와 함께 사용해 아직 로드되지 않은 컴포넌트 대신 fallback을 보여줄 수 있다. 데이터 fetching에서는 아무 Promise나 직접 던지는 방식으로 접근하기보다, Suspense를 지원하는 프레임워크나 라이브러리와 함께 쓰는 편이 안전하다.&lt;/p&gt;&lt;p&gt;Suspense를 이해하고 나니 로딩 상태는 컴포넌트 내부의 &lt;code&gt;if (loading)&lt;/code&gt; 문제라기보다, 사용자에게 어느 범위까지 기다리게 할 것인가의 문제처럼 보이기 시작했다.&lt;/p&gt;&lt;p&gt;결국 Suspense를 이해할 때 중요한 질문은 이것이다.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;이 컴포넌트가 준비되지 않았을 때, 화면의 어느 경계까지 fallback으로 바꿀 것인가?&lt;/strong&gt;&lt;/p&gt;</description><pubDate>Sat, 06 Jul 2024 14:34:21 +0900</pubDate><guid>http://blex.me/@baealex/what-is-react-suspense</guid></item><item><title>BLEX 2024 4월 개발노트</title><link>http://blex.me/@baealex/blex-dev-note-2024-4</link><description>&lt;h4&gt;⭐ 추가된 항목&lt;/h4&gt;&lt;hr&gt;&lt;h6&gt;💬 초대장&lt;/h6&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;이 서비스의 사용 대상자를 전체에서 일부로 축소하였어요. 제한을 두지 않았더니 스팸이 많이 발생하였고, 특히 스팸과 스팸이 아닌 것을 구분하는데 모호함이 있었어요. 그래서 초대장을 통해서 검증된 사용자만 에디터가 될 수 있도록 변경하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;현재는 관리자가 초대장을 직접 만들면서 제공하고 있지만, 추후에는 특정 액션에 따라서 에디터에게 초대장을 부여하여 자유롭게 등록될 수 있도록 만드려고 합니다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;🔨 개선된 항목&lt;/h4&gt;&lt;hr&gt;&lt;h6&gt;💬 메인 페이지&lt;/h6&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/4/25/202442522_fSeiLmr5KaqWjeZLOLhI.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;메인 페이지를 정말 오랜만에 변경하였습니다. 앞으로는 이 서비스의 전체적인 레이아웃은 사용자가 원하는 대로 커스텀 할 수 있게 만드려고 합니다. 컨텐츠의 너비라던가 다양한 위젯들을 유저가 자유롭게 구성해서 사용할 수 있도록 만들고 싶어요.&lt;/p&gt;&lt;p&gt;벽돌 레이아웃에서 목록형으로 바꾼 이유는 사실 포스트 리젠이 빠르지 않아서 (슬픈 일이지만.. 😥) 목록형으로 바뀌어도 내용을 확인하는데 불편함이 낮을 것으로 예상했고요. 저사양 데스크톱 기기에서는 카드의 위치가 뒤늦게 배치되는 성능적인 이슈를 무마하기(?) 위함이기도 합니다.&lt;/p&gt;&lt;p&gt;또한 메인 페이지를 인기 포스트가 아닌 신규 포스트를 노출하도록 변경하였습니다. 인기 포스트는 어제/오늘 조회수를 기반으로 만드는데 그에따라 특정 포스트가 노출되는 빈도가 높았어요. 초대장이 도입되어 신규 포스트의 질도 높을 것으로 예상되기도 하였고요.&lt;/p&gt;&lt;p&gt;인기있는 글은 &lt;code&gt;Trending&lt;/code&gt; 위젯에서 계속 확인하실 수 있습니다. 이 위젯의 목록은 기존과 달리 오늘 조회수만을 기반으로 만드므로 그날마다 인기있는 글을 확인하기 좋을 것 같아요.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h6&gt;💬 관심 포스트&lt;/h6&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/4/25/202442522_y2Y86WLUHEudSwlCY3A4.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;관심 포스트에 언제 추가됐는지 보여지도록 개선하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h6&gt;💬 프로필&lt;/h6&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/4/26/202442618_Buzp8a0EPuHHAfufFfil.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/4/26/202442618_5XHCBRpv16Zj3pWCVuZe.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/4/26/202442621_AERAMhbIJIH0qvl8ESL9.jpg" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;프로필 페이지 디자인이 변경되었습니다. 포스트 페이지의 경우 무한 스크롤이 적용됨에 따라서 글의 정렬을 바꿀 수 있도록 하였으며, 검색은 검색 페이지를 거치지 않고 포스트 페이지에서 바로 진행되도록 개선하였습니다. 태그의 경우 일부 유저의 페이지가 매우 길어지는 현상이 있어서 필터 형식으로 전환하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;시리즈 페이지의 경우 정보 대비 차지하는 영역이 넓어서 그리드 형태로 전환하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;소개 페이지가 제거되고 소개 내용은 개요(Overview)페이지에서 보여지도록 변경하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h6&gt;💬 시리즈 포스트&lt;/h6&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/5/1/20245113_OwkaV3x8tonD5C9uhcGo.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;시리즈 포스트 페이지가 목록에서 그리드 형으로 변경되었습니다. 해당 시리즈의 전체적인 맥락을 쉽게 확인할 수 있도록 하고 싶었어요.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;설명이 긴 일부 시리즈에 대응하여 상단 부분이 내용에 맞게 높이가 설정되도록 개선하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;페이지 하단에 에디터의 시리즈 목록으로 가는 버튼이 추가되었습니다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h6&gt;💬 태그 포스트 타이틀&lt;/h6&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/4/25/202442522_4YroAytYc26HyPeK20kZ.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;태그 포스트의 타이틀을 네비게이션 타입으로 개선하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h6&gt;💬 페이지네이션&lt;/h6&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="https://blex.me/resources/media/images/content/2024/4/25/202442522_WuLzHWIzasSd73cbFr1N.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;모바일 사용성 개선을 위해서 페이지네이션 UI를 개선하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h6&gt;💬 기타등등&lt;/h6&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;텍스트의 가독성을 전체적으로 개선하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;포스트 본문의 왼쪽 영역의 소셜 공유 버튼이 제거되었습니다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;설정 &amp;gt; 시리즈 순서 변경의 드래그 앤 드롭 기능을 개선하였습니다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;💻 월간회고&lt;/h4&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://blex.me/@baealex/retrospective-2024-1-quarter"&gt;2024년 1분기 회고&lt;/a&gt;에서 다뤘듯이 이 프로젝트를 내게 의미있는 일로 만드려면 어떻게 해야할까 고민하고 있다. 사실 의미있는 일이라는게 무엇인지 판단하기가 참 어렵다. 장기적으로 나는 아이디어를 빠르게 잘 만드는 사람이 되고 싶기 때문에 이곳에서 디자인 시스템을 잘 구축해서 새로운 아이디어를 빠르게 만들 수 있도록 하고, 이를 바탕으로 다른 프로젝트에서도 활용하고 싶은 마음이 들었다.&lt;/p&gt;&lt;p&gt;오랜만에 코드를 보는데 코드가 정말 난장판이었다. 긍정적으로 보자면 그만큼 내가 안목이 높아졌다는 뜻으로 위안해본다. 기존의 레이아웃이나 컴포넌트들의 스타일이 파편화(=강하게 결합)되어 있어서 이를 개선하는 리펙토링 하는 작업을 주로 진행했다. 사실 생산성을 핑계로 대강 만들었던게 이제야 문제로 인식된 것 같다.&lt;/p&gt;&lt;p&gt;이미지 레이지 로딩 처리도 모두 컴포넌트 단위에서 처리되도록 수정하였고 대부분의 컴포넌트는 스타일을 직접 입히는 대신 디자인 시스템 컴포넌트에서 최대한 조합되도록 수정했다. 태그의 depth가 조금 깊어지는 느낌이 있긴 하지만 가독성이나 변경하기 쉬운 구조가 되어 유용한 것 같다. 또한 이름에 비해서 단촐한(?) 동작을 했던 hook도 중복되는 로직을 통합할 수 있도록 개선하는 작업을 진행했다. 그 과정에 버그가 상당히 많이 발생했는데 ... 리펙토링엔 역시 테스트 코드가 참 중요함을 깨닫는다.&lt;/p&gt;&lt;p&gt;인스턴스도 교체하고 도커 이미지 사이즈를 줄이고 패키지를 전체적으로 업데이트 하는 등 노후화된 시스템을 개선하기도 하였다. 장고가 벌써 5버전이 되었다니... 3버전으로 시작했는데 시간이 정말 많이 흘렀다. 별다른 조치를 안해줬는데도 테스트가 잘 통과하는 걸 보니 하위 호환이 참 잘되는 것 같다. (어떤 것들은 업그레이드하면 난리나던데 말이다.) 이런건 보고 배워야 겠다.&lt;/p&gt;</description><pubDate>Wed, 01 May 2024 20:13:22 +0900</pubDate><guid>http://blex.me/@baealex/blex-dev-note-2024-4</guid></item><item><title>웹 앱 API 개발을 위한 GraphQL 1~4장 정리</title><link>http://blex.me/@baealex/graphql-for-web-api-development</link><description>&lt;h2 id="1장-서론"&gt;1장 : 서론&lt;/h2&gt;&lt;hr&gt;
&lt;h4 id="graphql이란-무엇인가"&gt;GraphQL이란 무엇인가?&lt;/h4&gt;&lt;p&gt;GraphQL은 선언형 데이터 패칭 언어로 일컬어 진다. 개발자는 &lt;strong&gt;무슨&lt;/strong&gt; 데이터가 필요한지에 대한 요구사항만 작성하면 되고 &lt;strong&gt;어떻게&lt;/strong&gt; 가져올지는 신경쓰지 않아도 된다. GraphQL은 서버와 클라이언트의 통신을 위한 명세이다. 문법과 타입 시스템의 실행과 유효성 검사에 대한 스펙을 가지고 있다. 그 외에 별다른 지침은 없으므로 서비스에 따라서 설계할 수 있다.&lt;/p&gt;
&lt;h4 id="설계-원칙"&gt;설계 원칙&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;위계적
&lt;ul&gt;
&lt;li&gt;GraphQL 쿼리는 위계성을 가진다. 필드 안에 필드가 중첩될 수 있고 쿼리와 그에 대한 반환 데이터는 형태가 같다.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;제품 중심적&lt;/li&gt;
&lt;li&gt;엄격한 타입 제한
&lt;ul&gt;
&lt;li&gt;GraphQL 서버는 GraphQL 타입 시스템을 사용한다. 이를 토대로 유효성 검사를 받는다.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;클라이언트 맞춤 쿼리&lt;/li&gt;
&lt;li&gt;인트로스펙티브
&lt;ul&gt;
&lt;li&gt;GraphQL 언어를 사용해 GraphQL 서버가 사용하는 타입 시스템에 대한 쿼리를 작성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="왜-필요한가"&gt;왜 필요한가?&lt;/h4&gt;&lt;p&gt;데이터 전송 방식은 아래 단계를 거치며 진화했다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;RCP (1960) - Remote Procedure Call&lt;/li&gt;
&lt;li&gt;SOAP (1990) - Simple Object Access Protocol (XML)&lt;/li&gt;
&lt;li&gt;REST (2000) - Representational State Transfer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;REST는 현존하는 훌륭한 스펙이지만 아래와 같은 단점을 가지고 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오버패칭: 필요한 데이터 보다 더 받아옴&lt;/li&gt;
&lt;li&gt;언더패칭: 필요한 데이터가 부족하여 여러번 요청함&lt;/li&gt;
&lt;li&gt;복잡한 엔드포인트: 기능이 추가될 수록 엔드포인트가 복잡해짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL은 위 문제를 해결할 수 있는 하나의 수단이 될 수 있다.&lt;/p&gt;
&lt;h4 id="graphql-단점"&gt;GraphQL 단점&lt;/h4&gt;&lt;p&gt;[개인적으로 정리한 부분]&lt;/p&gt;
&lt;p&gt;GraphQL은 장점만 있는가? 그렇지 않다. 아래와 같은 단점을 가지고 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;캐싱&lt;/strong&gt;: GraphQL은 기본적으로 HTTP 캐싱을 지원하지 않는다. 이로 인해 RESTful API에서와 같이 캐싱을 구현하기가 더 어려울 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보안&lt;/strong&gt;: GraphQL 쿼리는 클라이언트가 필요한 만큼의 데이터만 요청할 수 있지만, 이것이 곧 과도한 쿼리 또는 무한 루프를 통한 공격을 방지하지 않는다는 것을 의미한다. 따라서 적절한 보안 메커니즘을 구현해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쿼리 오버헤드&lt;/strong&gt;: GraphQL은 클라이언트가 필요한 데이터만 요청할 수 있게 해준다. 하지만 이것은 서버 측에서 쿼리를 해석하고 처리하는 오버헤드를 야기할 수 있다. 특히 복잡한 쿼리를 처리할 때 이러한 오버헤드가 더 두드러질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2 id="2장-그래프-이론"&gt;2장 : 그래프 이론&lt;/h2&gt;&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;그래프 이론의 개념은 레오넬 오일러가 17 세기에 쾰니히스베르크의 다리 문제를 해결함으로써 널리 알려져있다.&lt;/li&gt;
&lt;li&gt;그래프 이론은 객체 간의 관계를 표현하는 수학적 모델로 각 객체는 노드(node) 혹은 정점(vertax)와 간선(edge)의 집합으로 나타낸다. 노드는 특정 개체나 이벤트를 나타내며, 간선은 노드 간의 관계를 나타낸다. 그래프 이론은 많은 곳에서 응용되고 있으며 객체간의 네트워크 구조를 파악할 수 있다.&lt;/li&gt;
&lt;li&gt;그래프는 간선이 방향을 가진 방향 그래프와 간선의 방향과 위계가 없는 무방향 그래프로 분류된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2 id="3장-graphql-쿼리어"&gt;3장 : GraphQL 쿼리어&lt;/h2&gt;&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;IBM에서 개발한 관계형 데이터베이스 언어인 구조화된 영문 쿼리 언어(Structured English Query Language = SEQUEL, 이하 SQL)은 도메인에 종속된 언어로 CRUD로 데이터를 관리하는데 초점이 맞춰져 있고 SQL의 철학이 REST에 큰 영향을 주었다.&lt;/li&gt;
&lt;li&gt;REST의 구조에서 발생할 수 있는 단점을 극복하기 위해서 GraphQL은 데이터베이스를 질의하는 SQL의 개념을 바탕으로 웹에서 쿼리문을 서버로 질의할 수 있도록 고안되었다. GraphQL 질의문은 어휘 분석을 통해 추상 구문 트리로 파싱되어 유효성 검사를 거친다.&lt;/li&gt;
&lt;li&gt;GraphQL과 SQL 구문은 다음과 같은 차이를 가지고 있다.
&lt;ul&gt;
&lt;li&gt;Read
&lt;ul&gt;
&lt;li&gt;SQL : SELECT&lt;/li&gt;
&lt;li&gt;GraphQL : Query&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Create, Update, Delete
&lt;ul&gt;
&lt;li&gt;SQL : INSERT, UPDATE, DELETE&lt;/li&gt;
&lt;li&gt;GraphQL : Mutation&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="structure-of-query"&gt;Structure of Query&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-rs"&gt;query { // Root Type = 작업의 시작
    allSongs { // Selection Set = 필드 선택
        name
        time:  duration // Alias = 별칭
        artist {
            name
        }
        album {
            name
            cover {
                src
            }
        }
    }
    artists: allArtist(offset: 0, limit: 10) { // Query Arguments = 결과 필터링
    ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;필드는 &lt;strong&gt;스칼라&lt;/strong&gt;(scalar) 타입과 &lt;strong&gt;객체&lt;/strong&gt;(object) 타입 둘 중 하나에 속하게 된다.
&lt;ul&gt;
&lt;li&gt;스칼라 타입 = 원시 타입
&lt;ul&gt;
&lt;li&gt;ID (String과 같은 형태로 반환되지만 항상 유니크한 값)&lt;/li&gt;
&lt;li&gt;Int&lt;/li&gt;
&lt;li&gt;Float&lt;/li&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;li&gt;Boolean&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;객체 타입 = 스키마에 정의한 필드를 그룹으로 묶은 것&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;프래그먼트
&lt;ul&gt;
&lt;li&gt;중복되는 질의문을 줄여줌&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;fragment artistInfo on artist {
    name
    debutAt
    LatestAlbumReleaseAt
}

query {
    allSongs {
        artist {
            ...artistInfo
        }
    }
    allArtists {
        ...artistInfo
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;유니온 타입&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;특정 객체 필드에서 서로 다른 타입이 내려오도록 함&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;인터페이스 (두 요소는 4장에서 자세히)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="structure-of-mutation"&gt;Structure of Mutation&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-rs"&gt;mutation {
    createArtist(name: &amp;quot;Jino Bae&amp;quot;) {
        id
        name
        createdAt
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;mutation {
    updateArtist(id: &amp;quot;10&amp;quot;, name: &amp;quot;Jino Bae&amp;quot;) {
        name
        createdAt
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;mutation {
    deleteArtist(id: &amp;quot;10&amp;quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="introspection"&gt;Introspection&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-rs"&gt;query {
    __schema {
        types {
            name
            description
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;query {
    __type(name: 'artist') {
        name
        fields {
            name
            description
            type {
                name
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;fragment typeFields on __Type {
    name
    fields {
        name
    }
}

query {
    __scheme {
        queryType {
            ...typeFields
        }
        
        mutationType {
            ...typeFields
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2 id="4장-스키마-설계"&gt;4장 : 스키마 설계&lt;/h2&gt;&lt;hr&gt;
&lt;p&gt;GraphQL API를 구현하는데 있어 도메인에 국한되기 보다는 스키마 (데이터 모델링) 기반으로 소프트웨어를 설계해야 한다. 따라서 반환할 데이터 타입에 대해서 생각해보고 이를 제대로 정의해야 한다. GraphQL로 API를 설계하면 API가 엔드포인트의 결합(REST)가 아니라 타입의 결합으로 보인다.&lt;/p&gt;
&lt;h4 id="type"&gt;Type&lt;/h4&gt;&lt;p&gt;GraphQL의 핵심 단위는 타입이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type Album {
    id: ID! // ! = non-nullable
    name: String!
    description: String // nullable
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="scalar-type"&gt;Scalar Type&lt;/h4&gt;&lt;p&gt;기본 스칼라 타입은 5개 이지만 원한다면 커스텀한 스칼라 타입을 생성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;scalar DateTime

type Album {
    ...
    createdAt: DateTime!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성한 스칼라 타입의 직렬화 및 유효성 검사를 처리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;const resolvers = {
    DateTime: {
        // 예시: JavaScript에서 Date 객체를 사용하여 처리
        serialize(value) {
        return new Date(value).getTime();
    },
    // 예시: 클라이언트에서 전달된 값을 Date 객체로 변환
    parseValue(value) {
        return new Date(value);
    },
    // 예시: 쿼리나 뮤테이션에서 리턴된 값 처리
    parseLiteral(ast) {
        if (ast.kind === Kind.INT) {
            return new Date(parseInt(ast.value, 10));
        }
        return null;
        },
    },
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="enum-type"&gt;Enum Type&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-rs"&gt;enum AlbumGenre {
    POP
    JAZZ
    HIPHOP
    ELECTRONIC
}

type Album {
    ...
    genre: AlbumGenre!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="list-type"&gt;List Type&lt;/h4&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;strong&gt;리스트 선언&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;정의&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;[Int]&lt;/td&gt;&lt;td&gt;리스트 안에 담긴 정수 값은 null이 될 수 있다.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;[Int!]&lt;/td&gt;&lt;td&gt;리스트 안에 담긴 정수 값은 null이 될 수 없다.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;[Int]!&lt;/td&gt;&lt;td&gt;리스트 안에 정수 값은 null이 될 수 있으나, 리스트 자체는 null이 될 수 없다.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;[Int!]!&lt;/td&gt;&lt;td&gt;리스트 안의 정수 값은 null이 될 수 없고, 리스트 자체도 null이 될 수 없다.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id="11"&gt;1:1&lt;/h4&gt;&lt;p&gt;커스텀 객체 타입으로 필드를 만들면 두 객체는 서로 연결(Edge가 형성)된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type Artist {
    name: String!
}

type Album {
    artist: Artist!
    name: String!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="1n"&gt;1:N&lt;/h4&gt;&lt;p&gt;GraphQL 서비스는 최대한 방향성이 없도록 만드는게 좋다. 따라서 위 타입에서 Artist에서 Album로 되돌아 갈 수 있는 패스가 있어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type Artist {
    name: String!
    albums: [Album!]!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="nm"&gt;N:M&lt;/h4&gt;&lt;p&gt;앨범에는 여러명의 아티스트가 참여할 수 있으므로&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type Artist {
    name: String!
    albums: [Album!]!
}

type Album {
    artists: [Artist!]!
    name: String!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;통과 타입(through type)을 만드는 경우 엣지를 커스텀 객체 타입으로 정의하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type Ablum {
    collaborators: [Collaborator!]!
    name: String!
}

type Collaborator {
    artists: [Artist!]!
    album: Album!
    firstCollaborateAt: DateTime
    latestColleaborateAt: DateTime
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="union-type"&gt;Union Type&lt;/h4&gt;&lt;p&gt;한 배열에 서로 다른 객체가 내려가도록 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;union Agenda = StudyGroup | Workout

type StudyGroup {
    name: String!
    subject: String
    students: [User!]!
}

type Workout {
    name: String!
    reps: Int!
}

type Query {
    agenda: [Agenda!]!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;query schedule {
    agenda {
        ... on Workout {
            name
            reps
        }
        ... on StudyGroup {
            name
            subject
            students
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="interface"&gt;Interface&lt;/h4&gt;&lt;p&gt;한 필드 안에 여러 타입을 넣을 때 사용하는데, 객체 타입 용도로 만드는 추상 타입이며, 스키마 코드를 조직할 때 아주 좋은 방법이다. 인터페이스를 통해서 특정 필드가 반드시 포함되도록 만들 수 있으며, 이들 필드는 쿼리에서 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;scalra DateTime

interface Agenda {
    name: String!
    start: DateTime!
    end: DateTime!
}

type StudyGroup implements Agenda {
    name: String!
    start: DateTime!
    end: DateTime!
    participants: [User!]!
    topic: String!
}

type Workout implements Agenda {
    name: String!
    start: DateTime!
    end: DateTime!
    reps: Int!
}

```rs
query schedule {
    agenda {
        name
        start
        end
        ... on Workout {
            reps
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="argument"&gt;Argument&lt;/h4&gt;&lt;pre&gt;&lt;code class="language-rs"&gt;enum ArtistOrderBy {
    id
    name
}

enum ArtistOrderDirection {
    ASCENDING
    DESCENDING
}

type Query {
    artist(id: ID!): Artist!
    allArtists(
        offset: Int = 0,
        limit: Int = 20,
        orderBy: ArtistOrderBy = id,
        orderDirection: ArtistOrderDirection = DESCENDING
    ): [Artist!]!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;query {
    artist(id: &amp;quot;1&amp;quot;) {
        name
    }

    allArtists(offset: 0, limit: 30) {
        id
        name
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="mutation"&gt;Mutation&lt;/h4&gt;&lt;p&gt;엄밀히 말하면 뮤테이션과 쿼리 작성법에는 큰 차이가 없다. 어플리케이션의 상태를 바꿀 액션이나 이벤트가 있을 때만 뮤테이션을 작성해야 한다. 뮤테이션은 어플리케이션의 동사 역할을 해야한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type Mutation {
    createArtist(name: String!): Artist
    updateArtist(id: ID!, name: String!): Artist
    deleteArtist(id: ID!): Boolean
}

scheme {
    query: Query,
    mutation: Mutation
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="input-type"&gt;Input Type&lt;/h4&gt;&lt;p&gt;위에서 페이지네이션을 처리하거나 뮤테이션에서 인자 값이 많아진다면 관리가 어려워 질 수 있다. 그럴때는 Input 타입을 사용하면 체계적으로 관리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;input Pagination {
    offset: Int = 0
    limit: Int = 20
}

input Order {
    orderBy: String!
    orderDirection: String!
}

type Query {
    allArtist(pagintation: Pagination, order: Order)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;쿼리 변수에 대해서는 좀 더 알아봐야 겠다.&lt;/p&gt;
&lt;h4 id="return-type"&gt;Return Type&lt;/h4&gt;&lt;p&gt;OAuth 등등을 적용하다보면 커스텀한 리턴값을 돌려줘야 하는 경우도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type AuthPayload {
    user: User!
    token: String!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-rs"&gt;type Mutation {
    OAuth(code: String!): AuthPayload!
}
&lt;/code&gt;&lt;/pre&gt;
</description><pubDate>Sat, 27 Apr 2024 14:52:40 +0900</pubDate><guid>http://blex.me/@baealex/graphql-for-web-api-development</guid></item><item><title>Docker 이미지 사이즈 최적화 기록</title><link>http://blex.me/@baealex/docker-image-size-optimize</link><description>&lt;p&gt;문득 내가 이용하는 Docker 이미지들의 사이즈가 생각보다 작다는 것을 알게 되었다. 내가 빌드한 이미지는 GB 단위라서 당연히 그 정도가 정상인 줄 알았는데, 자주 사용하던 &lt;code&gt;filebrowser/filebrowser&lt;/code&gt;는 &lt;code&gt;30.6MB&lt;/code&gt;밖에 되지 않았다.&lt;/p&gt;&lt;p&gt;그때부터 내가 만든 이미지는 왜 이렇게 큰지 궁금해졌다. 이번 글은 Docker 이미지 최적화의 정답을 정리한 글이라기보다, &lt;code&gt;sd-prompt-palette&lt;/code&gt; 프로젝트의 이미지 사이즈를 줄여보며 확인한 기록에 가깝다.&lt;/p&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/baealex/sd-prompt-palette"&gt;https://github.com/baealex/sd-prompt-palette&lt;/a&gt;&lt;/p&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;docker build -t sdpp-test .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 프로젝트의 이미지는 당시 &lt;code&gt;1.44GB&lt;/code&gt; 정도에 육박했다 💦&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;이미지 분석&lt;/h4&gt;&lt;p&gt;오픈소스로 만들어진 &lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/wagoodman/dive"&gt;dive&lt;/a&gt;라는 CLI 도구가 있는데 이를 활용하면 Dockerfile에서 각 레이어가 어느 정도의 용량을 차지하는지, 이미지 내부에서 어디에 위치하고 있는지 파악할 수 있다. 대략 아래와 같은 비주얼이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="/resources/media/images/content/2024/4/13/202441311_eulGV8Dkv7Sjw7Bvsbpi.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;dive를 이용해서 사이즈를 파악해보자.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;dive sdpp-test
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="/resources/media/images/content/2024/4/13/202441310_cGUwMtG23nUVBheEzl4R.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;pre&gt;&lt;code class="language-plaintext"&gt;WORKDIR /app
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 라인을 기점으로 위쪽은 베이스 이미지가 차지하는 용량, 아래쪽은 내가 추가한 용량으로 볼 수 있다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;이미지 최적화&lt;/h4&gt;&lt;h6&gt;1. 작은 베이스 이미지 사용&lt;/h6&gt;&lt;p&gt;우선 베이스 이미지가 차지하는 용량이 1GB 정도로 비교적 크다는 것을 알 수 있었다. 이 프로젝트에서는 당시 &lt;code&gt;node:21&lt;/code&gt; 이미지를 사용하고 있었다. 기본 이미지는 Debian 계열을 바탕으로 빌드되어 있는데, 더 작은 용량을 목표로 한 이미지들도 제공된다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;node:21&lt;/code&gt; : 기본 Debian 기반 이미지다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;node:21-slim&lt;/code&gt; : 더 경량화된 Debian 기반 이미지다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;node:21-alpine&lt;/code&gt; : 초경량 리눅스 배포판 Alpine Linux 기반 이미지다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;여기서는 &lt;code&gt;alpine&lt;/code&gt;으로 이미지를 바꿨고, 크기는 &lt;code&gt;474MB&lt;/code&gt;로 줄어들었다.&lt;/p&gt;&lt;p&gt;다만 Alpine이 언제나 정답이라는 뜻은 아니다. 네이티브 의존성이 있거나 특정 시스템 라이브러리에 기대는 프로젝트라면 오히려 문제가 생길 수도 있다. 이 프로젝트에서는 필요한 의존성이 단순했고, 이미지 크기를 줄이는 목적에 잘 맞았기 때문에 Alpine을 선택했다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="/resources/media/images/content/2024/4/13/202441310_PHA3lGTZpDezV17Q6GlF.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;베이스 이미지가 &lt;code&gt;140MB&lt;/code&gt; 정도만 차지하게 되었다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-dockerfile"&gt;# as-is
FROM node:21

# to-be
FROM node:21-alpine
&lt;/code&gt;&lt;/pre&gt;&lt;h6&gt;2. dockerignore&lt;/h6&gt;&lt;p&gt;이미지에는 꼭 필요한 파일만 담아야 한다. &lt;code&gt;.gitignore&lt;/code&gt;와 유사한 신택스를 사용하는 &lt;code&gt;.dockerignore&lt;/code&gt; 파일을 추가해서 빌드나 실행에 필요하지 않은 파일을 명시해 주는 것이 좋다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;# .md 확장자인 파일을 모두 무시 ex) README.md
*.md

# 빌드 및 실행을 위해 사용하는 특정 .md 파일은 포함 (아래는 예시)
!*.spec.md

# 디렉토리의 하위 디렉토리나 파일을 무시
.git/
logs/
node_modules/
&lt;/code&gt;&lt;/pre&gt;&lt;h6&gt;3. 멀티 스테이지 빌드&lt;/h6&gt;&lt;p&gt;이 과정도 불필요한 파일을 이미지에 포함하지 않는 방법에 해당한다.&lt;/p&gt;&lt;p&gt;현재 최적화를 진행하는 프로젝트에서는 웹 서버와 클라이언트가 모두 포함되어 있다. 이미지 내에서 클라이언트를 빌드하고 있으므로 이미지에는 서버 실행을 위한 패키지와 클라이언트 빌드를 위한 패키지를 모두 담고 있는 형태를 취하고 있다.&lt;/p&gt;&lt;p&gt;하지만 클라이언트의 경우에는 빌드된 파일만 제공되면 되므로 클라이언트 패키지와 빌드를 위해서 사용하는 소스 코드가 이미지에 포함되는 것은 불필요하다고 볼 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;server
   ├──node_modules # 필요
   ├──src # 필요
   └──client
         ├──node_modules # 불필요
         ├──src # 불필요
         └──dist # 필요
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dockerfile에서 빌드 stage를 분리하고 각 stage에서 필요한 파일만 가져올 수 있도록 할 수 있다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-dockerfile"&gt;# client라는 이름으로 stage 설정
FROM node:21-alpine as client

WORKDIR /app

COPY ./src/client/package.json ./
COPY ./src/client/pnpm-lock.yaml ./

RUN npx pnpm i

COPY ./src/client/ ./

RUN npm run build

...

FROM node:21-alpine

WORKDIR /app

# client stage에서 파일 복사
COPY --from=client /app/dist ./client/dist
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위와 같이 client stage에서 빌드한 결과물만 실제 이미지에 포함시킬 수 있다. 최종적으로 &lt;code&gt;322MB&lt;/code&gt;로 사이즈를 줄일 수 있게 되었다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center;"&gt;&lt;img src="/resources/media/images/content/2024/4/13/202441311_aRfsqTTzAoIcoFnjWejE.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;결국 이미지 사이즈 최적화는 무작정 작은 이미지를 고르는 일이 아니라, 런타임에 필요하지 않은 것들을 걷어내는 일에 가까웠다. 빌드에만 필요한 파일, 개발 중에만 필요한 의존성, 실수로 함께 복사된 디렉토리를 하나씩 덜어내면 이미지의 성격이 훨씬 분명해진다.&lt;/p&gt;&lt;p&gt;작은 이미지는 배포와 다운로드 측면에서 분명 장점이 있다. 다만 숫자를 줄이는 것 자체가 목적이 되면 곤란하다. 중요한 것은 이 이미지가 실행될 때 정말 무엇이 필요한지 확인하고, 그 외의 것들을 이미지 밖으로 밀어내는 일이라고 생각한다.&lt;/p&gt;</description><pubDate>Sat, 13 Apr 2024 12:16:21 +0900</pubDate><guid>http://blex.me/@baealex/docker-image-size-optimize</guid></item><item><title>2024 1분기 회고</title><link>http://blex.me/@baealex/retrospective-2024-1-quarter</link><description>&lt;h4 id="1월-회고"&gt;1월 회고&lt;/h4&gt;&lt;h6 id="업무"&gt;[업무]&lt;/h6&gt;&lt;p&gt;회사에서 중요한 프로젝트를 진행중이다. 이 작업을 하면서 내가 주체적으로 일하지 않는다는 생각이 강하게 들었다. 그냥 시키는 대로만 하고 그 외에 부분은 크게 신경쓰지 않는 태도로 작업을 했던것 같다. 아직 업무에 익숙하지 않아서 그런걸 수도 있겠지만, 다음 프로젝트에서는 개선하도록 해야겠다.&lt;/p&gt;
&lt;h6 id="되고-싶지-않은-모습"&gt;[되고 싶지 않은 모습]&lt;/h6&gt;&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=43biNTkv5Eo"&gt;뭘 해야될지 모를 때... 삶의 목표를 잡아주는 3가지 방법&lt;/a&gt;이라는 영상을 보았다. 마침 그날 일하다가 &amp;quot;대체 뭘 위해서 이러고 있지?&amp;quot; 고민하고 있었는데 그것에 대한 해답을 주는 것 같아서 특히 인상적이었다.&lt;/p&gt;
&lt;p&gt;영상에서는 Why에 대해서 고민하기, 되고 싶지 않은 모습을 그려보라는 내용이 있었다. 나는 한 번도 되고 싶지 않은 모습에 대해서 생각하지 않았기에 흥미롭게 느껴졌다. 무엇을 가장 멀리할지 고민해보니 어떤걸 가깝게 유지해야할지 머릿속에 잘 들어오는 것 같다.&lt;/p&gt;
&lt;p&gt;5년 뒤 절대 되고 싶지 않은 모습&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;뚱뚱해 지는 것
&lt;ul&gt;
&lt;li&gt;멀리할 것) 군것질&lt;/li&gt;
&lt;li&gt;가까이할 것) 운동&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;돈에 허덕이는 것
&lt;ul&gt;
&lt;li&gt;멀리할 것) 불필요한 소비&lt;/li&gt;
&lt;li&gt;가까이할 것) 소득 높히기&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;고집불통
&lt;ul&gt;
&lt;li&gt;멀리할 것) 익숙해진 것&lt;/li&gt;
&lt;li&gt;가까이할 것) 새로운 것, 유연함&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;무의미한 삶을 사는 것
&lt;ul&gt;
&lt;li&gt;멀리할 것) 중독&lt;/li&gt;
&lt;li&gt;가까이할 것) 모든것에서 배울점 찾기&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h6 id="토이-프로젝트"&gt;[토이 프로젝트]&lt;/h6&gt;&lt;p&gt;위 관점에서 생각해 봤을 때 현재 진행하는 토이 프로젝트(&lt;a href="https://github.com/baealex/BLEX"&gt;BLEX&lt;/a&gt;)가 무의미한 활동으로 느껴졌기 때문에 이걸 의미있는 일로 바꾸고 싶었다. 지금까지는 이 프로젝트를 취업용 정도로 생각하고 시작하여 배울점이 많았지만, 지금은 마땅히 나에게 별다른 의미를 주지는 않는다. 지금부터는 이 프로젝트를 통해서 어떤것들을 배워볼 수 있을지, 어떻게 의미있는 활동으로 바꿀 수 있을지 생각해보고 방향을 바꿔보도록 해야겠다.&lt;/p&gt;
&lt;h6 id="스트레스"&gt;[스트레스]&lt;/h6&gt;&lt;p&gt;스트레스를 잘 대처하면서 살고 있다고 생각했는데 특정 역치를 넘어서면 불건전한 방법(과식, 과소비, 부정적 생각)으로 해소하게 되는 것을 느꼈다. 좀 더 건전한 방법으로 대처하는 방안을 찾도록 해봐야겠다. 악기 연주나 펜싱같은 운동에 도전해보고 싶기도 하다. 올해 둘 중 하나에 취미를 두도록 해보자.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h4 id="2월-회고"&gt;2월 회고&lt;/h4&gt;&lt;h6 id="업무-1"&gt;[업무]&lt;/h6&gt;&lt;p&gt;끝나지 않을 것 같았던 프로젝트가 끝이 보이기 시작하니 마음이 한결 편안해지는 것 같다. 이 업무를 진행하면서 회사의 업무 방식에서 개선되면 좋을 것 같은 부분들이 보였던 것 같다. 내가 해결할 수 있는 것과 해결할 수 없는 것으로 분리해서, 해결할 수 있는 요소들은 다음 프로젝트에서 개선된 형태로 진행해 보도록 해야겠다.&lt;/p&gt;
&lt;h6 id="oop"&gt;[OOP]&lt;/h6&gt;&lt;p&gt;OOP에 대한 지식이 많이 부족하다는 것을 느꼈다. 회사에서는 OOP 기반의 프레임워크인 앵귤러를 주로 사용하는데, 구조를 설계하는 부분에서 미숙하다고 판단됐다. 생각해보니 여지껏 실무에서 OOP로 프로그래밍을 했던적이 없어서 학습에 소홀했던 것 같다. 개념들은 알고 있으니까 빨리 습득을 해야겠다.&lt;/p&gt;
&lt;h6 id="성장"&gt;[성장]&lt;/h6&gt;&lt;p&gt;회사 업무를 하면서 성장한다고 생각했는데, 아닌 것 같다. 업무의 결과물은 현재 알고 있는 지식에서 나오는 산출물에 불과하고, 개인적으로 공부하는데 시간을 투자해야만 새로운 시각에서 업무를 할 수 있게 되고 그때 비로소 성장하게 되는 것 같다. 회사 업무 = 성장이라는 맹목적인 믿음을 버리고  업무에 지치더라도 개인 공부 시간을 매일 가지도록 해야겠다.&lt;/p&gt;
&lt;h6 id="메모"&gt;[메모]&lt;/h6&gt;&lt;p&gt;누군가 나에게 무엇인가 물어보면 관련해서 메모를 해둔 것은 기억이 나는데 어디에 해뒀는지 파악하기가 힘든 것 같다. 노트 앱 + 블로그 + 사내 위키 등 파편화가 되어있는 것 같아서, 이걸 좀 더 편하게 관리할 방법을 고민해야겠다. 예전부터 항상 했던 고민인데 항상 되돌아오는 것 같다.&lt;/p&gt;
&lt;p&gt;지식 공유 + 업무 현황 파악이 쉬우면서도 메모가 많아지더라도 미래에 편하게 탐색하는 것을 효율적으로(대충 슥 저장해도 찾기 편한) 만들 방법이 있을까? AI를 활용해서 자동으로 카테고리화 해주면 어떨까 싶긴하다. 로컬 AI로 작게 실험을 해봐야겠다.&lt;/p&gt;
&lt;h6 id="발표"&gt;[발표]&lt;/h6&gt;&lt;p&gt;정기 코드 리뷰 시간에는 코드의 변경 사항에 대해서 공유하는데, 이때 내가 발표를 매우 못해서 기분이 별로였다. 급한 업무들을 처리하느라 준비를 못해서 그런 것 같다. 준비를 안했으니 못하는 것은 당연한 것. 다음부터는 진행할 때 [문제 / 해결책 / 현재 해결책을 선택한 이유 / 요점] 등등을 미리 적어서 준비해 둬야겠다. 이것들은 모아두면 나에게도 큰 도움이 될 것 같다.&lt;/p&gt;
&lt;h6 id="깊이가-부족하다"&gt;[깊이가 부족하다]&lt;/h6&gt;&lt;p&gt;리액트로 새로운 프로젝트를 시작하는데 내가 기본적인 설계를 하게 되었다. 관련해서 문서를 작성하다보니 지금껏 너무나 당연시하며 사용했던 것들에 대해서 깊이 고민하지 않았었다는 생각이 들었다. 설득하는 문장이 전혀 써지지 않았다. 다시금 고민하면서 심도있게 살펴보는데 이전에 면접에서 절었던 부분이 회상되면서 내가 했던 대답들이 매우 부끄럽게 느껴졌다. 열심히 공부해서 다음에는 이런 부끄러움을 느끼지 않으면 되니까. 화이팅 💪&lt;/p&gt;
&lt;h6 id="간만에-새로운-토이-프로젝트"&gt;[간만에 새로운 토이 프로젝트]&lt;/h6&gt;&lt;p&gt;예전에는 새로운 토이 프로젝트를 시작하면 되게 신나게 했던 것 같은데 요즘에는 좀 귀찮은 느낌이 든다. 뭔가 시작하기 전에 해줘야 하는게 많은 느낌이다. 서버 셋팅하고, 프론트 셋팅하고, 개발 환경 셋팅하고, 데이터 모델링하고, GraphQL 스키마 정의하고, GraphQL 리졸버 구현하고, 프론트 라우터 만들고, 모델 타입도 정의하고 ... 예전에는 이것들을 하나씩 배워가면서 하다보니 재밌었는데... 앞으로는 익숙한 설계나 기술 말고 새로운 것들을 적용해서 흥미를 돋구도록 해봐야겠다.&lt;/p&gt;
&lt;p&gt;그런 의미로 Next JS + Next UI + Tailwind CSS 라는 조합으로 작업을 진행했다. 비교적 재밌었던 것 같다. Next JS의 새롭게 추가된 App Router라는 것을 새롭게 익혔고, Server Component도 사용해 볼 발판이 마련되어 주말에는 관련해서 학습을 좀 하면 좋을 것 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React - Server Component · BLEX @baealex
&lt;a href="/@baealex/react-server-component"&gt;#&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;생각해보니 이 문제는 위 부족한 깊이를 해결하는 해결책이 될 수도 있겠다. 새로운 프로젝트 보다는 기존에 있는 프로젝트를 진득하게 하다보면 문제를 겪지 않겠는가? 그러한 문제를 직면하고 해결하는 경험을 쌓는게 지금의 나에겐 더 의미있는 활동일 수 있겠다.&lt;/p&gt;
&lt;h6 id="손목"&gt;[손목]&lt;/h6&gt;&lt;p&gt;급격하게 손목이 아파오기 시작했다. 아마도 트랙패드를 사용할 때 손목을 과도하게 꺽어서 사용했던게 문제를 만든 것 같다. 그래서 최근에는 손목을 움직이지 않고 타이핑을 할 수 있는 방안에 대해서 찾아보고 있다. VIM 같은 도구를 활용해 손목에 부담이 되지 않으면서 효율적으로 타이핑하는 것을 시도하려고 한다.&lt;/p&gt;
&lt;p&gt;VIM의 이점은 백스페이스나 화살표, Home, End, Page Up, Page Down 키 마저도 손목의 움직임 없이 수행할 수 있다는 것이다. 그러면 내가 고집하던 키보드 배열을 쓰지 않아도 되고 효율적인 타이핑이 가능해져서 최소한의 움직임으로 효과적인 타이핑이 가능한 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h4 id="3월-회고"&gt;3월 회고&lt;/h4&gt;&lt;h6 id="좋은-코드"&gt;[좋은 코드]&lt;/h6&gt;&lt;p&gt;&lt;a href="https://kciter.so/posts/what-is-beautiful-code"&gt;아름다운 코드에 대하여&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;요즘에 코드 리뷰에 질문하고 싶은 주제를 던져보고 있다. 그럼 생각지 못한 이야기들을 많이 듣게되서 좋은 코드에 대해서 생각할 계기가 되는 것 같다.&lt;/p&gt;
&lt;p&gt;좋은 코드란 결국 (가독성은 기본으로 깔아두고..) 상황에 최적화 되도록 만들어야 하는데 그 코드가 좋은 코드라고 설득하는게 참 어려운 것 같다. 이것은 단순히 언변의 문제라기 보다는 겪고 있는 상황을 개선한 경험의 여부가 중요할 것 같고, 함께 말하기에서 말하는 것 처럼 팀원에게 능력을 보여주고 신뢰를 쌓는 게 가장 우선시 되어야 할 것 같다.&lt;/p&gt;
&lt;h6 id="방향-재설정"&gt;[방향 재설정]&lt;/h6&gt;&lt;p&gt;팀원분과 이런저런 대화를 하면서 경각심이 생겨났다. 내가 진짜 하고 싶었던 개발은 아이디어를 화면 위에 자유롭게 표현하는 것이었는데..! 이번주에는 내가 원하는 방향으로 올바르게 갈 수 있도록 좌표를 다시 잡고, 표현의 영역을 확장하기 위해서 Three JS와 플러터와 같은 도구를 살펴보기 시작했다.&lt;/p&gt;
&lt;p&gt;Three JS는 2D의 한계에서 벗어나기 위함이고 플러터는 웹 브라우저 렌더링의 한계에서 벗어나기 위함이다. 3D 개발은 이전부터 조금씩 도전해 봤지만 금방 시들곤 했었는데 이번엔 3D 모델링까지 확실하게 해야한다.&lt;/p&gt;
&lt;p&gt;관련해서 열심히 학습해서 이번 회사에서 관련된 작업들을 맡아볼 수 있도록 신경쓰고 추후 이직시에 관련된 방향으로 업무를 전환할 수 있도록 해야겠다.&lt;/p&gt;
&lt;h6 id="토스"&gt;[토스]&lt;/h6&gt;&lt;p&gt;실제로 업무를 경험해보지 못했지만 표면상으로 보여지는 토스는 (프론트엔드 측면에서) 참 좋은 회사인 것 같다. 기획력도 괜찮고 그 기획력을 실현할 수 있는 개발력도 좋고 UX에 신경쓰는 정신도 대단하고 디자인 시스템도 잘 구축하고 성능도 신경쓰고... 어떻게 그렇게까지 할 수 있을까? 그런 것들은 어디서부터 차이가 생기는 것일까?&lt;/p&gt;
&lt;h6 id="말하기"&gt;[말하기]&lt;/h6&gt;&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;의도를 정확하게 전달하는 방법이나 일상적인 대화를 원만하게 할 수 있도록 말하기 연습이 필요할 것 같다. 이전에는 불필요한 것들이라 생각했는데, 기회를 얻기 위해선 좋은 인상과 더불어 가장 가성비가 좋은 개선일 것 같다.&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;나는 한 마디를 할 때 매우 고민하고 신중하게 하기 때문에 다른 사람의 한 마디에도 큰 의미를 부여한다. 텍스트로 소통할 때는 어조를 파악할 수 없어서 대체로 부정적으로 보이는 것 같고, 대화할 때 단어 하나에도 크게 영향 받는 것 같다. 썩 좋은 습관은 아닌 것 같다. 생각 없이 말하면 그만큼 받아들이는 것도 가벼워 질텐데, 너무 무겁지도 너무 가볍지도 않은 적정한 무게를 찾아보자.&lt;/p&gt;
&lt;h6 id="배우기"&gt;[배우기]&lt;/h6&gt;&lt;p&gt;나는 모든 활동을 성취에 도움을 주는 것 or 그렇지 않은 것 이렇게 이분법으로 분류했다. 그래서 그렇지 않은 행동을 할 때는 죄책감이나 부당함, 우울함(?) 같은 것을 심하게 느껴졌다. 문득 그러한 감정은 내가 한 행위에서 배울점을 전혀 느끼지 못했기 때문이라고 판단되었다.&lt;/p&gt;
&lt;p&gt;사실 모든 일에는 배울점이 있지 않은가? 모든 일에서 배울점을 찾아보자. 배울점이 없다면 심지어는 지금하는 행위에 의문을 가져볼 수도 있고, 그를 통해서 아이디어를 떠올릴 수도 있다!&lt;/p&gt;
&lt;h6 id="graphql"&gt;[GraphQL]&lt;/h6&gt;&lt;p&gt;최근에 GraphQL을 애용하고 있는데 더 잘쓰고 싶어서 예전에 사두었던 GraphQL 책을 봤다. 몰랐던 개념을 알게 되어서 좋고 잘못 사용하던 부분이나 개선이 필요한 부분도 바로 반영할 수 있어서 좋은 것 같다. 나의 공부 패턴은 항상 비슷하게 흘러가는 것 같다. 일단 써보기 -&amp;gt; 일단 무언가 만들기 -&amp;gt; 책보고 개선하기 이 패턴을 크게 벗어나지 않는다.&lt;/p&gt;
&lt;p&gt;알고보니 새로운 방식으로 학습하는게 더 효율적일 수도 있으니까. 새로운 방식으로 학습(강의보기, 책부터 일단 살펴보기)하는 것들도 시도해 보아야 겠다.&lt;/p&gt;
&lt;h6 id="술"&gt;[술]&lt;/h6&gt;&lt;p&gt;회식때 술을 먹었는데 생각보다 과하게 먹어서 다음날을 망친 것 같다. 생각해보니 술을 먹는 것은 나에게 아무런 이점도 없는데 왜 먹으려고 했는지 모르겠다. 뭐, 긴장을 풀어주는 효과는 있으니 다음부턴 두 세잔만 정도만 먹고 말아야 겠다.&lt;/p&gt;
&lt;h6 id="인프런-밋업"&gt;[인프런 밋업]&lt;/h6&gt;&lt;p&gt;인프런에서 하는 타입스크립트 객체지향 프론트엔드 밋업을 들었다. 기대감이 커서 그런지 약간의 실망감도 있었다. 여하지간 듣지 않았다면 깨우치지 못했을 부분들에 대해서 알게 되어 배운점이 있다고 생각한다. 최근에 리액트를 다시 하다보니 앵귤러를 멀리하게 되었는데 이 밋업을 다녀와서 문득 앵귤러를 잘쓰고 싶은 욕망이 생겼다.&lt;/p&gt;
&lt;h6 id="클린코드"&gt;[클린코드]&lt;/h6&gt;&lt;p&gt;클린코드 &amp;amp; 리팩토링 &amp;amp; 단위 테스트에 대한 강연을 들었다. 이 강연은 정말 유용했다. 책으로 읽는 것 보다 흡수가 빠르게 되었고 최근에 걱정 &amp;amp; 고민하던 코드에 대한 해답을 제시해주는 느낌이었다. 강연에서 들었던 내용들을 틈틈히 작업하는 부분에 적용해 보려고 시도하고 있다. 아직 미숙하긴 하지만 하다보면 당연하게 느껴질 것 같다.&lt;/p&gt;
</description><pubDate>Sun, 07 Apr 2024 20:50:10 +0900</pubDate><guid>http://blex.me/@baealex/retrospective-2024-1-quarter</guid></item></channel></rss>