<?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, 22 Mar 2026 10:50:16 +0900</lastBuildDate><image><url>/resources/media/images/avatar/da/baealex/aqTad.JPG</url><title>baealex (배진오)</title><link>http://blex.me/@baealex</link></image><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;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;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;속도는 기술이 올려줄 수 있다. 하지만 누가 이해하고, 누가 감당하고, 누가 책임질 것인지는 여전히 사람이 정해야 한다. 나는 에이전틱 개발의 미래가 단지 더 빠른 개발에 머물지 않고, 더 건강한 책임 구조까지 함께 설계하는 방향으로 가면 좋겠다고 생각한다.&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;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;뉴모피즘은 내가 가장 좋아하는 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>이제는 더 이상 토이 프로젝트를 망치지 않는다 (팀 코너)</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로 생산성이 올라간 지금. 내 컴퓨터에 시체 더미가 쌓여가고 있다. AI에게 리뷰를 받아도 사실 AI는 내가 강한 어조로 말해버리면 그걸 그대로 이행한다. 그게 나쁜 방식이던 좋은 방식이던 상관없이 말이다. “&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://blex.me/@baealex/the-prison-of-bias-created-by-ai"&gt;AI는 우리를 편향주의 감옥에 가둔다&lt;/a&gt;” 라는 글을 기고했던 것 처럼 AI는 내가 별도로 지시하지 않으면 나의 관점과 나의 생각을 편협하게 가둬버린다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h1&gt;팀을 만들자&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에게 역할을 부여해서 팀처럼 일하게 만드는 전략이다. 팀에서 일하면 많은 사람들의 생각을 들어볼 수 있다. 함께 고민해서 만들면 내가 생각하지 못했던 것에 대해서 인사이트를 얻을 수 있고 더 견고한 제품을 만들 수 있게 된다. bmad 메서드를 깊이 사용하진 못했지만 솔직히 말하자면 토이 프로젝트에 적용하기엔 불필요하게 복잡하고 과했다. 설정해야 할 것이 많고, 구조가 무겁다.&lt;/p&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;이 한 줄이면 클로드 코드에 가상의 팀이 셋팅된다.&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;단순히 이름만 붙인 역할극이 아니다. 각 페르소나에는 &lt;strong&gt;체크리스트&lt;/strong&gt;, &lt;strong&gt;안티패턴 목록&lt;/strong&gt;, &lt;strong&gt;해결 패턴&lt;/strong&gt;, 그리고 &lt;strong&gt;다른 페르소나를 호출하는 트리거&lt;/strong&gt;가 설계되어 있다.&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;통과한 건 통과했다고, 안 된 건 뭐가 안 됐는지 명확하게 짚어준다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;안티패턴을 잡아낸다&lt;/h3&gt;&lt;p&gt;각 페르소나는 자기 영역에서 흔히 발생하는 실수 패턴을 알고 있다. 빅토르는 "트랜잭션 없는 다중 쓰기", "N+1 쿼리", "에러 삼키기" 같은 백엔드 안티패턴을 감지한다. 마르코는 "로딩 상태 누락", "색상만으로 정보 전달" 같은 UX 안티패턴을 잡아낸다. 중요한 건, &lt;strong&gt;문제만 지적하고 끝나지 않는다&lt;/strong&gt;는 것이다.&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;마르코와 유나가 함께 보면 화면이 예쁘면서도 성능이 좋아진다. 마르코가 "Skeleton UI 넣어야 한다"고 하면 유나가 "CSS transform으로 애니메이션 걸어야 60fps 나온다"고 받아준다. 스티브가 "이 기능 범위가 커지고 있는데"라고 하면 엘런이 "뭘 빼면 절반 시간에 되는지 볼게요"라고 스코프를 잡아준다.&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;&lt;strong&gt;배경은 페르소나의 행동 범위를 제한하는 제약 조건이다.&lt;/strong&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;Cross-domain 트리거가 명시적이라서, 관련 없는 페르소나가 끼어드는 노이즈가 적다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;그래도 하나의 AI에게 하나의 역할만 맡기는 것보다 결과가 떨어지는 순간은 있을 수 있다. 이건 트레이드오프다. 대신 혼자서는 절대 얻지 못하는 &lt;strong&gt;다각도 피드백&lt;/strong&gt;을 얻는다.&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;a target="_blank" rel="noopener noreferrer nofollow" href="http://summary.md"&gt;summary.md&lt;/a&gt;: 핵심 컨텍스트 (항상 참조됨)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="http://project.md"&gt;project.md&lt;/a&gt;: 기술 스택, 아키텍처, 컨벤션&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="http://decisions.md"&gt;decisions.md&lt;/a&gt;: 왜 이 기술을 선택했는지&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="http://learnings.md"&gt;learnings.md&lt;/a&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;시작하기&lt;/h2&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;npx team-conor&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;팀원들이 당신을 불러줄 이름을 입력하면 끝이다. &lt;code&gt;CLAUDE.md&lt;/code&gt;와 &lt;code&gt;.conor/&lt;/code&gt; 디렉토리가 생성되고, 다음 Claude 세션부터 팀이 함께한다. 각 페르소나의 체크리스트, 안티패턴, 해결 패턴은 전부 &lt;code&gt;.conor/persona/*.md&lt;/code&gt; 파일에 있으니 프로젝트에 맞게 자유롭게 수정할 수 있다.&lt;/p&gt;&lt;p&gt;혼자 개발한다고 혼자 작업할 필요는 없다.&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;&lt;s&gt;https://github.com/baealex/team-conor&lt;/s&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&amp;nbsp;정신 차려보니까 바퀴를 또 만들고 있었어요! 당장 접고 &lt;/strong&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/obra/superpowers"&gt;&lt;strong&gt;Superpowers&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; 를 사용하기 시작했습니다.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&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 Action 셀프 호스트 러너 설정법</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;어제 우연히 깃헙 액션 셀프 호스트에 대해서 알게 되었다. 회사에서 상시 돌아가는 러너가 죽어있어서 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;우선 이 글에서는 러너를 띄우는 아주아주 간단한 내용만 다루며, 이후 셀프 호스트 러너의 위험성, 셀프 호스트 러너를 효율적으로 활용하는 방안, 보다 안전하게 돌리는 방안에 대해서 시리즈로 다룰 예정이다.&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;깃헙 리포지토리&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;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;후후.. 이 간단한 설치 뒤에 펼쳐지는 위험성과 더 안전하게 돌리는 방법에 대해서 알아보자.&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;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으로 분류하여, 긴급한 상호작용이 발생하면 진행 중이던 렌더링 작업을 일시 중단하거나 폐기하고 급한 작업부터 처리한다. 이를 제어하기 위해 3가지 주요 API가 도입되었다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;startTransition&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;useTransition&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;useDefferedValue&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;useDefferedValue&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;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;// 두 값이 다르다면, 현재 React가 백그라운드에서 deferredQuery를 업데이트하는 중이다.
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;code&gt;useDeferredValue&lt;/code&gt;로 전달된 값이 API 호출의 &lt;code&gt;useEffect&lt;/code&gt; 의존성 배열에 들어갈 경우, 사용자의 입력마다 API가 호출될 위험이 있다.&lt;/p&gt;&lt;p&gt;따라서 네트워크 요청과 같이 비용이 발생하는 작업에는 여전히 Debounce를 병행하거나, React Query와 같은 데이터 페칭 라이브러리의 캐싱 전략을 함께 사용하는 것이 안전하다.&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와 보내는 시간이 길어질수록 우리는 &lt;strong&gt;‘편향’이라는 이름의 안락한 감옥&lt;/strong&gt;에 갇힌다.&lt;/p&gt;&lt;p&gt;섬뜩한 사실은, 대부분의 AI에게 이것은 '오류'가 아니라 철저히 계산된 &lt;strong&gt;'의도된 동작'&lt;/strong&gt;이라는 점이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;소프트웨어는 ‘체류 시간’을 먹고 자란다&lt;/h3&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; 가 폭로했듯, 우리는 하는 게임, 소셜 미디어, 그리고 AI 서비스는 사용자의 체류 시간(Dwell Time)을 늘리기 위해 수많은 연구와 실험을 거듭한다. 기업은 우리의 ‘관심’을 판다. 그리고 아이러니하게도 그 과정은 사람들을 바보로 만든다.&lt;/p&gt;&lt;p&gt;우리는 도파민의 늪에 빠져있다. 유튜브 알고리즘, 틱톡의 쇼츠, 도박성 게임들… 더 많은 도파민을 얻기 위해 우리는 앱에 재방문하고, 점점 더 오래 머문다. AI 역시 이 거대한 흐름에서 예외가 아니다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;AI는 ‘아니오’라고 말하지 않는다&lt;/h3&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_MLotsQU8rWDrY2sXZc4f.jpg" alt="unnamed.jpg" style="object-fit: cover;"&gt;&lt;figcaption&gt;우리는 안락한 필터 버블 속에서 살아가고 있다.&lt;/figcaption&gt;&lt;/figure&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;“네, 100% 공감합니다.”&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;/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;구글, 페이스북, 유튜브, 그리고 최신 AI 챗봇에 이르기까지 모든 플랫폼은 사용자의 데이터를 수집한다. 우리가 무엇을 클릭했는지, 얼마나 오래 봤는지, 현재 위치는 어디인지, 나이와 성별은 무엇인지 분석한다. 이 데이터를 바탕으로 알고리즘은 ‘우리가 좋아할 만한 것’만 골라서(필터링해서) 보여준다.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;반향실 효과 (Echo Chamber)&lt;/strong&gt;&lt;br&gt;필터 버블로 인해 비슷한 정보만 접하게 된 사람들은, 자연스럽게 비슷한 생각을 가진 사람들과 어울리게 된다(온라인 커뮤니티, SNS 그룹 등). 이 닫힌 공간에서 구성원들은 서로의 의견에 동조하고 맞장구친다. 같은 의견이 반복적으로 메아리처럼 되돌아오면서, 그 정보가 진실인지 여부와 상관없이 믿음은 점점 더 확고해지고 극단적으로 변한다.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;현재 소셜 미디어는 자신이 보고 싶은것, 자신의 생각과 같은 피드만 나온다. SNS가 확증 편항 감옥을 만들었다면, AI는 더 이것을 더 강화하고 견고하게 만들고 있다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;물고기는 존재하지 않는다&lt;/h3&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;와 AI의 작동 원리에서 찾을 수 있다. 이 책은 우리가 당연하게 '물고기(Fish)'라고 부르던 분류가 사실 과학적으로는 허구(측계통군)임을 밝힌다. '물고기'라는 개념은 인간의 편의를 위해 자연의 복잡성을 뭉뚱그려 만든 '그럴듯한 라벨'일 뿐, 실존하는 단일 계통이 아니다.&lt;/p&gt;&lt;p&gt;AI의 작동 방식도 이와 동일하다.&lt;/p&gt;&lt;p&gt;AI가 내놓는 답변은 문제의 본질을 '이해'하고 논리적인 해결책을 '창조'한 것이 아니다. 전 세계 수십억 자료를 학습한 뒤, 통계적으로 &lt;strong&gt;‘가장 그럴듯한 다음 단어 조각’&lt;/strong&gt;을 나열한 결과물일 뿐이다. 마치 '물고기'라는 단어가 실제 자연의 복잡성을 가리는 편의상의 라벨이듯, AI의 답변 역시 '진짜 이해'를 가리는 &lt;strong&gt;‘통계적 패턴의 집합체’&lt;/strong&gt;다.&lt;/p&gt;&lt;p&gt;AI가 내게 건네는 공감과 칭찬, 그리고 그럴듯한 답변들은 진실이 아니라&lt;/p&gt;&lt;p&gt;&lt;strong&gt;“사용자가 만족할 확률이 가장 높은 통계적 패턴”&lt;/strong&gt;일 뿐이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;편향의 파도를 넘자&lt;/h3&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_KqxcfRBs5PU3rSqitfBo.jpg" alt="unnamed.jpg" style="object-fit: cover;"&gt;&lt;figcaption&gt;파도에 갖히지 않으려면 강해져야만 한다.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;AI는 잘못이 없다. 시스템 프롬프트에 적힌 대로 “사용자의 심기를 건드리지 말라”는 명령을 충실히 수행하고 있을 뿐이다. 문제는 그것을 맹목적으로 받아들이는 우리의 태도에 있다. AI 시대, 우리가 갖춰야 할 가장 중요한 역량은 &lt;strong&gt;‘비판적 수용력’&lt;/strong&gt;이다.&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;/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의 비판을 모두 수용하진 말라. 그것 또한 당신의 명령에 입각한 통계학적 패턴일 뿐이다. 여전히 가장 중요한건 사용자의 인식과 비판적 수용력이다. 이 세상에 정답은 없다. 흑백논리가 아닌 그레이존 안에 머무르며 혜안을 키울 필요가 있다.&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;h3&gt;나는 왜 바퀴를 깎고 있었나&lt;/h3&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;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_o4xHfCyvL9rroenB6Ttc.jpg" alt="unnamed.jpg" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;주객전도&lt;/h3&gt;&lt;p&gt;바퀴를 직접 깍는 행위의 가장 큰 위험성이 무엇일까?&lt;/p&gt;&lt;p&gt;구현 가능성? 기술적 난이도? 와 같은 단어가 먼저 떠올랐다면,&lt;/p&gt;&lt;p&gt;당신은 &lt;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;h3&gt;레거시 코드&lt;/h3&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;가 되어 버린다. 코너씨의 후임자는 코너씨의 코드를 보면서 코너씨의 의도를 파악하기 위해 코드를 역설계한다. 후임자는 10분이면 끝났을 일을 3일 동안 하다보니, 커밋에 남겨져 있는 코너씨의 메일로 욕설을 한 가득 써서 보내고 싶은 마음이 솓구친다.&lt;/p&gt;&lt;p&gt;이건 최적화가 아니라, 미래의 리소스를 현재로 끌어다 쓴 &lt;strong&gt;기술 부채&lt;/strong&gt;일 뿐이다.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h3&gt;선택과 집중&lt;/h3&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;는 React에서 비동기 작업을 처리할 때, 컴포넌트의 렌더링을 잠시 멈추고, 그 작업이 완료될 때까지 대기하는 기능을 제공해주는 도구이다. 쉽게 말해서, 데이터가 준비될 때까지 기다리게 하고, 그동안 로딩 상태를 보여준다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;주요 개념&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Suspense 컴포넌트&lt;/strong&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Suspense 컴포넌트는 비동기 작업이 완료될 때까지 대기 상태를 관리한다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;fallback&lt;/code&gt; 속성을 사용해서 비동기 작업이 완료될 때까지 보여줄 UI를 정의할 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&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;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;React.lazy&lt;/strong&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;React.lazy를 사용하면 컴포넌트를 동적으로 로드할 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;React.lazy는 보통 코드 스플리팅을 위해서 사용된다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;const MyComponent = React.lazy(() =&amp;gt; import('./MyComponent'));&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;API Fetch&lt;/strong&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;React Query의 suspense 옵션을 사용하면 간단하게 API가 응답되기 전에 Suspense의 fallback이 실행되도록 만들 수 있다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h4&gt;응용 해보기&lt;/h4&gt;&lt;h6&gt;어떻게 동작하는 걸까?&lt;/h6&gt;&lt;p&gt;기본적으로 컴포넌트에서 Promise가 Throw되면 Suspense는 fallback의 컴포넌트를 렌더링 한다. 아래는 기본적인 구조를 나타낸 것이지만 기본적으로 리액트에서 컴포넌트는 계속해서 실행되는 개념이기 때문에 Promise가 반복되서 실행되지 않도록 해야한다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;// Don't
function LazyComponent() {
    const data = fetchSomething(); // 'data' type is inferred to be Promise.

    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;h6&gt;메모라이즈&lt;/h6&gt;&lt;p&gt;기본적으로는 메모리에 저장해두고 같은 키를 바탕으로 같은 객체를 반환하도록 해준다. Promise 타입일 때는 Promise를 Throw 시켜서 Suspense의 fallback을 실행시키는 전략이다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;const cache = new Map&amp;lt;string, unknown&amp;gt;()

function useMemorizedSuspensePromise&amp;lt;T&amp;gt;(keys: string[], fn: () =&amp;gt; Promise&amp;lt;T&amp;gt;): T | null {
    const key = keys.join('.')

    if (!cache.has(key)) {
        const promise = fn().then((data) =&amp;gt; {
            cache.set(key, data);
        });
        cache.set(key, promise);
    }

    const cachedData = cache.get(key);
    if (cachedData instanceof Promise) {
        throw cachedData;
    }

    return cachedData as T;
}

function LazyComponent() {
    const data = useMemorizedSuspensePromise(['fetch', 'something'], () =&amp;gt; fetchSomething());

    return (
        &amp;lt;div&amp;gt;
            {data}
        &amp;lt;/div&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;&lt;h6&gt;프로미스 상태 판별&lt;/h6&gt;&lt;p&gt;프로미스의 처리 상태를 직관적으로 파악하기 위해서 간단한 함수를 만들어보자.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;function createSuspensePromise&amp;lt;T&amp;gt;(promise: Promise&amp;lt;T&amp;gt;) {
    let status = 'pending';
    let result: T;
    const suspender = promise.then(
        r =&amp;gt; {
            status = 'success';
            result = r;
        },
        e =&amp;gt; {
            status = 'error';
            result = e;
        }
    );

    return {
        read() {
            if (status === 'pending') {
                throw suspender;
            } else if (status === 'error') {
                throw result;
            } else if (status === 'success') {
                return result;
            }
        }
    };
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 함수를 이용한 기본적인 사용 방법은 아래와 같다. 컴포넌트에서는 read() 함수만 실행하면 데이터를 가져오거나 Suspense 컴포넌트의 fallback이 실행된다.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;const suspensePromise = createSuspensePromise(fetchSomething())

function LazyComponent() {
    const data = suspensePromise.read()

    return (
        &amp;lt;div&amp;gt;
            {data}
        &amp;lt;/div&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;컴포넌트 내부에서 훅처럼 사용하도록 개선해보자.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-typescript"&gt;function useSuspensePromise&amp;lt;T&amp;gt;(promise: Promise&amp;lt;T&amp;gt;, dependents: string[] = []) {
    const [resource, setResource] = useState&amp;lt;ReturnType&amp;lt;typeof suspensePromise&amp;lt;T&amp;gt;&amp;gt; | null&amp;gt;(null)

    useEffect(() =&amp;gt; {
        setResource(suspensePromise(promise))
    }, dependents)

    return resource?.read();
}

function LazyComponent() {
    const data = useSuspensePromise(fetchSomething());

    return (
        &amp;lt;div&amp;gt;
            {data}
        &amp;lt;/div&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&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;문득 내가 이용하는 도커 이미지들의 사이즈가 엄청 작다는 것을 알게 되었다. 내가 빌드한 것은 GB 단위여서 당연히 그게 정상인 줄 알았는데 애용한던 &lt;code&gt;filebrowser/filebrowser&lt;/code&gt;는 &lt;code&gt;30.6MB&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;p&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;node:21 : 가장 기본이 Debian 기반으로 빌드된 이미지다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;node:21-slim : 더 경량화 된 Debian을 바탕으로 빌드된 이미지다.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;node:21-alpine : 초경량 리눅스 배포판 Alpine Linux를 바탕으로 빌드된 이미지다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;여기서는 alpine으로 이미지를 바꿔주었고 크기는 &lt;code&gt;474MB&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/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;</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><item><title>리액트 19 서버 컴포넌트 (RSC)</title><link>http://blex.me/@baealex/react-server-component</link><description>&lt;p&gt;리액트 18에서 Server Component 라는 개념이 추가되었다. 서버 컴포넌트란? 서버에서 렌더링되는 컴포넌트이다. (이 이상의 표현이 아직은 떠오르지 않는다.) 하지만 사실 리액트에서는 이미 컴포넌트를 서버에서 렌더링 하는 방법을 제공하고 있었다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h2&gt;기존 SSR의 문제점&lt;/h2&gt;&lt;hr&gt;&lt;p&gt;기존 컴포넌트를 서버 사이드 렌더링 (이하 SSR) 할 때 어떤 문제들이 있었을까?&lt;/p&gt;&lt;h4&gt;1. 하이드레이션&lt;/h4&gt;&lt;p&gt;사용자는 서버에서 렌더링 된 HTML과 다운로드된 자바스크립트를 활용해서 React Virtual DOM을 구성하고 이벤트 바인딩을 처리해야 웹 사이트를 정상적으로 사용할 수 있게 된다. 이 과정을 하이드레이션이라고 한다. 하이드레이션의 문제는 컴포넌트에 대한 모든 코드가 자바크립트 코드(이하 Bundle)에 포함되어야 한다는 점이다. 페이지의 규모가 커질수록 하이드레이션과 Bundle 사이즈에 큰 비용이 발생한다.&lt;/p&gt;&lt;h4&gt;2. 클라이언트 기반 컴포넌트&lt;/h4&gt;&lt;p&gt;리액트의 모든 컴포넌트는 클라이언트에서 동작하는 것을 기본으로 하지만 SSR을 처리하려면 서버 환경에서 구동되는 것도 고려하지 않을 수 없다. 모든 컴포넌트에 대하여 브라우저 API를 사용하거나 브라우저 API에 의존적인 라이브러리를 사용한다면 서버에서 처리하지 않도록 별도로 처리를 해줘야 한다.&lt;/p&gt;&lt;h4&gt;3. Props Drilling&lt;/h4&gt;&lt;p&gt;페이지 단위로 SSR을 구성했거나, (페이지 라우터 기반의) Next 프레임워크를 사용할 경우 페이지에 props으로 렌더링 될 데이터를 전달하는 방식을 주로 취하는데, 이때 컴포넌트의 깊이가 깊어지고 하위의 컴포넌트에 데이터 전달이 필요하다면 불필요한 Props Drilling이 발생할 수 있다.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h2&gt;서버 컴포넌트&lt;/h2&gt;&lt;hr&gt;&lt;p&gt;서버 컴포넌트의 도입으로 위 3가지 문제를 고칠 수 있게 되었다. 서버 컴포넌트의 역할은 매우 단순한다. 데이터를 가져오고 HTML을 렌더링한다. 별도의 라이프 사이클을 가지지 않으며 사용자와 상호 작용할 수 없는 불변의 컴포넌트이므로 &lt;strong&gt;하이드레이션을 해야 할 필요가 없다.&lt;/strong&gt; 따라서 서버 컴포넌트는 사용자에게 전달되지 않으므로 키 노출과 같은 이슈로 부터 안전하며 번들 사이즈가 줄어들 것을 기대할 수 있다.&lt;/p&gt;&lt;p&gt;지금까지 리액트는 클라이언트 기반의 클라이언트 컴포넌트 (이하 RCC)를 기본으로 하였지만 이제부터는 서버 기반의 서버 컴포넌트(이하 RSC)를 기본으로 한다. 어떻게 서버와 클라이언트의 구분을 명확하게 할 수 있었을까? RSC와 RCC에는 룰이 있다. RSC는 자식으로 RSC와 RCC를 가질 수 있지만 RCC는 RCC를 자식으로만 가질 수 있다. 즉, 기본적으로 모두 서버에서 동작하는 것을 전제로 하지만 클라이언트 컴포넌트 아래로는 클라이언트에서 실행되는 환경이라고 생각할 수 있다.&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="https://blex.me/resources/media/images/content/2024/3/11/202431120_kVtGCFNofch2V1YhZjEl.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;&lt;p&gt;RCC는 코드 상단에 &lt;code&gt;'use client'&lt;/code&gt;라는 디렉티브를 사용하여 클라이언트 컴포넌트라고 구분할 수 있다. RCC 아래로는 모두 클라이언트 컴포넌트인 것이 보장되므로 디렉티브를 선언한 RCC 아래의 RCC에서는 디렉티브를 생략할 수 있다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a target="_blank" rel="noopener noreferrer nofollow" href="https://react.dev/reference/react/use-client"&gt;'use client' directive - React&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;또한 각각의 서버 컴포넌트에서 필요한 데이터는 해당 서버 컴포넌트에서 직접 가져와서 사용하거나 내려줄 수 있기 때문에 최소한의 Props Drilling만 발생하게 된다.&lt;/p&gt;&lt;h4&gt;RSC vs RCC&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;✅&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;/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;❌&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;✅&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Browser API 사용 가능&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;React Hook 사용 가능&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;/tbody&gt;&lt;/table&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/3/11/202431121_noHLZo9oIJDRp3BqKPIy.png" alt="" style="object-fit: cover;"&gt;&lt;/figure&gt;</description><pubDate>Mon, 11 Mar 2024 21:12:52 +0900</pubDate><guid>http://blex.me/@baealex/react-server-component</guid></item><item><title>BLEX 2023 12월 개발노트</title><link>http://blex.me/@baealex/blex-2023-12%EC%9B%94-%EA%B0%9C%EB%B0%9C%EB%85%B8%ED%8A%B8</link><description>&lt;h4 id="-추가된-항목"&gt;⭐ 추가된 항목&lt;/h4&gt;&lt;hr&gt;
&lt;h6 id="-사용자-포스트-검색"&gt;💬 사용자 포스트 검색&lt;/h6&gt;&lt;p&gt;&lt;img class="lazy" data-src="/resources/media/images/content/2023/12/15/2023121522_aYxJMtTKvK4giClKTACa.png" src="/resources/media/images/content/2023/12/15/2023121522_aYxJMtTKvK4giClKTACa.png.preview.jpg" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자의 프로필 &amp;gt; 포스트에 검색 입력창이 추가되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-태그-클라우드-페이지-추가"&gt;💬 태그 클라우드 페이지 추가&lt;/h6&gt;&lt;p&gt;&lt;img class="lazy" data-src="/resources/media/images/content/2023/12/16/202312161_zImyhux02A8xmdbtfQwE.jpeg" src="/resources/media/images/content/2023/12/16/202312161_zImyhux02A8xmdbtfQwE.jpeg.preview.jpg" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;블렉스에 존재하는 모든 태그를 모아 볼 수 있는 &lt;a href="/tags"&gt;태그 클라우드 페이지&lt;/a&gt;를 추가하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img class="lazy" data-src="/resources/media/images/content/2023/12/16/202312161_GwNEReguJjClZvu097Ml.jpeg" src="/resources/media/images/content/2023/12/16/202312161_GwNEReguJjClZvu097Ml.jpeg.preview.jpg" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;태그명과 동일한 URL로 작성된 포스트는 태그 상세 페이지에서 태그를 대표하는 설명글로 표기됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-간편-발행-버튼-추가"&gt;💬 간편 발행 버튼 추가&lt;/h6&gt;&lt;p&gt;&lt;img class="lazy" data-src="/resources/media/images/content/2023/12/15/2023121522_Aq1uUaBNxzzzTJUUNqW3.png" src="/resources/media/images/content/2023/12/15/2023121522_Aq1uUaBNxzzzTJUUNqW3.png.preview.jpg" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포스트 발행시 상세 설정이 뜨는 모달이 뜨지 않고 바로 포스트를 발행할 수 있도록 간편 발행 버튼을 추가하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h4 id="-개선된-항목"&gt;🔨 개선된 항목&lt;/h4&gt;&lt;hr&gt;
&lt;h6 id="-검색-페이지-개선"&gt;💬 검색 페이지 개선&lt;/h6&gt;&lt;p&gt;&lt;img class="lazy" data-src="/resources/media/images/content/2023/12/16/202312161_YiI1aESD9bVlRX8VNFgK.jpeg" src="/resources/media/images/content/2023/12/16/202312161_YiI1aESD9bVlRX8VNFgK.jpeg.preview.jpg" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="/search"&gt;검색 페이지&lt;/a&gt;에서 검색어를 입력할 때 추천 검색어를 표기합니다.&lt;/li&gt;
&lt;li&gt;검색시 제목 &amp;gt; 설명 &amp;gt; 태그 &amp;gt; 내용에 매칭되는 순서로 포스트를 나열하며, 방문자의 '도움됐어요' 수치를 활용하여 검색 정확도를 개선하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-포스트-목록-카드-개선"&gt;💬 포스트 목록 카드 개선&lt;/h6&gt;&lt;p&gt;&lt;img class="lazy" data-src="/resources/media/images/content/2023/12/16/202312161_MMGGU8nwdJXcczB7EQ9R.jpeg" src="/resources/media/images/content/2023/12/16/202312161_MMGGU8nwdJXcczB7EQ9R.jpeg.preview.jpg" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포스트 목록 카드에서 댓글 수 확인 및 즉시 관심 포스트로 등록할 수 있도록 개선하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-기타-오류-수정"&gt;💬 기타 오류 수정&lt;/h6&gt;&lt;ul&gt;
&lt;li&gt;이름 변경시 패스워드 검증을 요구하는 오류를 수정하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h4 id="-월간회고"&gt;💻 월간회고&lt;/h4&gt;&lt;p&gt;문득 이 블로그에서 포스트라는 단위를 매우 무겁게 다루고 있다고 느껴졌다. 글을 발행하기 위한 플로우도 복잡한 것 같고, 여하지간 글을 여러개를 발행하는게 좀 복잡하고 피곤하게 느껴졌다. 또 문제라고 생각한 지점은 시리즈와 포스트의 관계다. 포스트와 댓글은 매우 잘 응집된 느낌인데 (포스트 안에서 댓글을 쉽게 만들 수 있으니까) 시리즈와 포스트는 그렇지 못한 것 같았다. 이 부분을 개선하기 위한 방안을 진지하게 고민해 봐야겠다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h4 id="-기타"&gt;🎸 기타&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://discord.gg/cs2XcEwSr9"&gt;디스코드&lt;/a&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;a href="https://github.com/baealex/BLEX"&gt;깃허브&lt;/a&gt;에 공개되어 있어요.&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Mon, 25 Dec 2023 13:06:16 +0900</pubDate><guid>http://blex.me/@baealex/blex-2023-12%EC%9B%94-%EA%B0%9C%EB%B0%9C%EB%85%B8%ED%8A%B8</guid></item><item><title>BLEX 2023 11월 개발노트</title><link>http://blex.me/@baealex/blex-2023-11%EC%9B%94-%EA%B0%9C%EB%B0%9C%EB%85%B8%ED%8A%B8</link><description>&lt;h4 id="-추가된-항목"&gt;⭐ 추가된 항목&lt;/h4&gt;&lt;hr&gt;
&lt;h6 id="-알림-설정-추가"&gt;💬 알림 설정 추가&lt;/h6&gt;&lt;p&gt;&lt;figure class="col-1"&gt;
&lt;img class="lazy" data-src="/resources/media/images/content/2023/11/16/2023111621_VqewPsyi5ch2PCpmAnNh.png" src="/resources/media/images/content/2023/11/16/2023111621_VqewPsyi5ch2PCpmAnNh.png.preview.jpg" alt=""&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원하는 알림만 수신할 수 있도록 알림 설정을 추가하였습니다. &lt;a href="/setting/notify"&gt;[바로가기]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;기존 사용자를 비롯하여 서비스에 가입시 기본적으로 모두 비활성화 된 상태이므로 &lt;strong&gt;알림 수신을 원할 경우 활성화가 필요합니다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-포스트-설정-페이지-필터-추가"&gt;💬 포스트 설정 페이지 필터 추가&lt;/h6&gt;&lt;p&gt;&lt;figure class="col-1"&gt;
&lt;img class="lazy" data-src="/resources/media/images/content/2023/11/19/2023111917_1P46HF8vl3fjsVut2Upp.png" src="/resources/media/images/content/2023/11/19/2023111917_1P46HF8vl3fjsVut2Upp.png.preview.jpg" alt=""&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[설정 &amp;gt; 포스트 관리 &amp;gt; 포스트]에서 정렬 외 포스트에 설정된 태그, 시리즈 및 검색어를 포함하는 제목을 가진 포스트만 필터링 할 수 있도록 필터를 추가하였습니다. &lt;a href="/setting/posts"&gt;[바로가기]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-포스트-태그-자동-생성"&gt;💬 포스트 태그 자동 생성&lt;/h6&gt;&lt;p&gt;&lt;figure class="col-1"&gt;
&lt;img class="lazy" data-src="/resources/media/images/content/2023/11/23/2023112323_mFSatTMbL3yZGA1LBVmg.png" src="/resources/media/images/content/2023/11/23/2023112323_mFSatTMbL3yZGA1LBVmg.png.preview.jpg" alt=""&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포스트 작성시 태그를 자동으로 생성하는 기능을 추가하였습니다. 사용자가 등록해 두었던 태그와 본문내용을 바탕으로 생성됩니다. OpenAI를 활용하는 기능이 아니므로 모든 사용자가 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h4 id="-개선된-항목"&gt;🔨 개선된 항목&lt;/h4&gt;&lt;hr&gt;
&lt;h6 id="-필명-변경-개선"&gt;💬 필명 변경 개선&lt;/h6&gt;&lt;p&gt;&lt;figure class="col-1"&gt;
&lt;img class="lazy" data-src="/resources/media/images/content/2023/11/16/2023111622_reZ1L2XR8M91mBP6ISLU.png" src="/resources/media/images/content/2023/11/16/2023111622_reZ1L2XR8M91mBP6ISLU.png.preview.jpg" alt=""&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포스트가 존재하더라도 필명을 변경할 수 있도록 개선하였습니다. 6개월에 한번씩 필명을 변경할 수 있도록 하였으며 이 기간은 추후에 연장될 수 있습니다. &lt;a href="/setting/account"&gt;[바로가기]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-시리즈-순서-변경-개선"&gt;💬 시리즈 순서 변경 개선&lt;/h6&gt;&lt;p&gt;&lt;figure class="col-1"&gt;
&lt;img class="lazy" data-src="/resources/media/images/content/2023/11/16/2023111621_hkUCPMOdNuut6xMbzB2n.png" src="/resources/media/images/content/2023/11/16/2023111621_hkUCPMOdNuut6xMbzB2n.png.preview.jpg" alt=""&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[설정 &amp;gt; 시리즈 관리 &amp;gt; 시리즈]에서 시리즈 순서를 드래그 앤 드롭으로 변경할 수 있도록 개선하였습니다. &lt;a href="/setting/series"&gt;[바로가기]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-사용자-소셜-주소-등록-개선"&gt;💬 사용자 소셜 주소 등록 개선&lt;/h6&gt;&lt;p&gt;&lt;figure class="col-1"&gt;
&lt;img class="lazy" data-src="/resources/media/images/content/2023/11/16/2023111621_XN4O2TzJZy2IDtyClq3R.png" src="/resources/media/images/content/2023/11/16/2023111621_XN4O2TzJZy2IDtyClq3R.png.preview.jpg" alt=""&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[설정 &amp;gt; 사용자 설정 &amp;gt; 프로필]에서 소셜 정보를 등록하는 부분을 개선하였습니다. &lt;a href="/setting/profile"&gt;[바로가기]&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;as-is : 고정된 주소 및 고정된 순서로 노출되는 소셜 정보 등록&lt;/li&gt;
&lt;li&gt;to-be : 원하는 아이콘을 선택하여 자유로운 주소 등록, 중복된 소셜 링크 등록 허용, 소셜 링크의 순서 변경 지원 (필요한 소셜 아이콘이 있다면 말씀해주세요.)&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h6 id="-그-외-변경-사항"&gt;💬 그 외 변경 사항&lt;/h6&gt;&lt;ul&gt;
&lt;li&gt;패스워드 규칙이 강화되었습니다.&lt;/li&gt;
&lt;li&gt;이미지 업로드시 발생하는 오류를 수정하였습니다.&lt;/li&gt;
&lt;li&gt;포스트 제목의 최대 길이가 65자로 변경되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h4 id="-기타"&gt;🎸 기타&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://discord.gg/cs2XcEwSr9"&gt;디스코드&lt;/a&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;a href="https://github.com/baealex/BLEX"&gt;깃허브&lt;/a&gt;에 공개되어 있어요.&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Thu, 23 Nov 2023 23:17:57 +0900</pubDate><guid>http://blex.me/@baealex/blex-2023-11%EC%9B%94-%EA%B0%9C%EB%B0%9C%EB%85%B8%ED%8A%B8</guid></item></channel></rss>