<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>조용한 기록실</title><description>조용한 기록</description><link>https://blog.ga-ut.com/</link><language>ko</language><item><title>코드 에디터를 아예 열지 않는 시대</title><link>https://blog.ga-ut.com/archive/distance-between-thought-and-execution/</link><guid isPermaLink="true">https://blog.ga-ut.com/archive/distance-between-thought-and-execution/</guid><description>오른쪽 Option 키로 AI 에이전트에게 일을 맡기며, “코드 에디터를 아예 열지 않게 될 것”이라고 했던 예측을 다시 생각한다.</description><pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate><content:encoded>“개발자는 코드 에디터를 아예 열지 않게 될 것이다.”

예전에 내가 했던 말이다.

당시에는 꽤 과감한 예측처럼 들렸다. 요즘은 그 말을 자주 떠올린다. 비슷한 변화가 시작됐는데, 모습은 내가 막연히 상상했던 것과 조금 다르다.

## 오른쪽 Option 키로 일이 시작된다

최근 macOS에서 Codex를 사용할 때 오른쪽 Option 키로 음성 입력을 시작하도록 설정해두었다. 어느 화면에 있든 키 하나를 누르면 바로 말할 수 있다.

문제가 생겼을 때 가장 먼저 여는 것은 IDE가 아니다. Option 키를 누르고 이렇게 말한다.

“이거 원인 찾아.”

“공식 문서 확인해서 고쳐.”

“테스트까지 돌려.”

그러면 Codex가 코드를 찾고, 문서를 확인하고, 파일을 수정하고, 명령을 실행한다. 나는 변경 내용과 테스트 결과, 실제 화면을 확인하고 다음 방향을 정한다.

에디터는 아직 열리지 않았다.

처음에는 음성 입력이 편해졌다고만 생각했다. 그런데 음성이 내 말을 텍스트로 옮기는 데서 끝났다면 작업 방식까지 달라지지는 않았을 것이다. 지금은 내 말이 받아쓰기로 끝나지 않고 파일 수정과 테스트까지 이어진다.

에디터를 열더라도 훨씬 나중이다. 어떤 작업은 에디터를 열지 않은 채 끝나기도 한다. 예전에 상상했던 장면이 조금씩 실제 작업 안으로 들어오고 있다.

## 줄어든 것은 타이핑보다 번역이었다

컴퓨터로 무언가를 하려면 머릿속의 의도를 검색어와 파일 경로, 명령어와 코드로 바꿔야 했다. 원하는 결과를 알고 있어도, 컴퓨터가 실행할 수 있는 절차로 한 번 더 번역해야 했다.

AI 에이전트는 그 번역의 일부를 맡는다. 나는 모든 조작을 순서대로 지정하는 대신, 무엇이 이상한지, 원하는 결과가 무엇인지, 어디까지 확인해야 하는지를 먼저 설명한다.

사람이 목표를 정하고 시스템이 일부 실행을 맡는 방식은 완전히 새로운 발상은 아니다. 1999년에도 [mixed-initiative 인터페이스 연구](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/11/chi99horvitz.pdf)는 사람이 직접 조작하는 방식과 에이전트의 자동화를 함께 사용해야 한다고 설명했다. 다만 그때 연구로 이야기하던 방식이 이제는 일상적인 개발 도구에서 체감되기 시작했다.

그렇다고 입력이 없어지는 것은 아니다. 말로 시키는 일이 늘수록 내가 확인해야 할 결과도 늘었다. [생성형 코딩 도구 사용자 연구](https://glassmanlab.seas.harvard.edu/papers/copilot_lbw_chi22.pdf)에서도 AI는 작업을 시작하는 데 도움이 됐지만, 생성된 코드를 이해하고 수정하고 디버깅하는 일은 별도로 남았다.

병목이 사라진 것이 아니다. 절차를 입력하는 비용은 줄고, 의도를 설명하고 결과를 검증하는 비용이 앞으로 나왔다.

실행을 넘기는 것과 판단을 넘기는 것은 다르다.

## 음성 다음을 생각하면

나는 이 흐름의 다음을 생각하면 BCI(Brain-Computer Interface)를 떠올린다. 음성도 결국 생각을 언어로 바꾸고, 다시 소리로 전달하는 과정을 거치기 때문이다.

물론 BCI가 음성 다음의 입력 방식으로 곧바로 이어진다는 뜻은 아니다. 대표적인 [뇌-말하기 인터페이스 연구](https://www.nature.com/articles/s41586-023-06377-x)도 뇌에 미세전극을 이식한 ALS 참여자 한 명이 말을 시도할 때 나타나는 신호를 해독한 결과다. 자유로운 생각을 읽는 기술과는 거리가 멀다.

시기는 알 수 없지만, 사람의 의도를 더 적은 변환으로 컴퓨터에 전달하려는 방향은 계속될 가능성이 높다고 생각한다.

## 예측을 다시 말하면

나는 여전히 에디터를 연다. 직접 코드를 따라가며 생각해야 할 때도 있고, 짧은 수정은 내가 바로 하는 편이 빠를 때도 있다.

달라진 것은 에디터를 여는 시점이다. 예전에는 대부분의 작업이 에디터에서 시작됐다. 지금은 AI에게 목적을 말하고, 결과가 나오면 필요에 따라 에디터로 들어간다.

코드 에디터를 아예 열지 않는 시대는 에디터가 사라져서 오는 것이 아니라, 에디터를 열지 않아도 끝낼 수 있는 일이 늘어나면서 올지도 모른다.

그래서 요즘 오른쪽 Option 키를 누를 때마다, 예전에 했던 말이 생각보다 먼 미래의 이야기는 아니었다고 느낀다.</content:encoded></item><item><title>줄어든 시간은 어디에 남는가</title><link>https://blog.ga-ut.com/archive/time-that-remains-in-ai-era/</link><guid isPermaLink="true">https://blog.ga-ut.com/archive/time-that-remains-in-ai-era/</guid><description>AI가 첫 형태를 빠르게 만드는 시대에, 시간을 남기는 방식을 생각한다.</description><pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate><content:encoded>AI는 이제 무언가의 첫 형태를 아주 빠르게 만든다.

글의 초안, 랜딩페이지, 디자인 에셋, 간단한 코드, 서비스 아이디어까지. 예전 같으면 시작하기 위해 꽤 긴 시간이 필요했던 것들이 이제는 몇 번의 요청만으로 눈앞에 놓인다. 없던 것을 처음 만들어내는 시간은 확실히 줄어들었다.

그런데 AI를 쓰면 쓸수록, 그 시간이 모두 내게 남는 것은 아니라는 생각도 함께 든다.

무언가를 많이 만들 수 있다는 감각은 자주 얻는다. 하지만 조금 더 들여다보면, 우리가 실제로 많이 만드는 것은 대체로 첫 형태에 가깝다. 초안, 시안, 샘플, 프로토타입, 방향성. 물론 이것들도 중요하다. 아무것도 없던 상태에서 첫 형태를 얻는 일은 늘 어려웠고, AI는 그 어려움을 크게 낮췄다.

## 첫 형태 이후에 남는 것

오래 살아남는 것들은 보통 첫 형태만으로 대체 불가능해지지 않는다.

OS나 프로토콜, 네트워크를 떠올리면 조금 선명해진다. 처음의 설계가 아무리 좋아도 그것만으로 지금의 위치에 오르지는 못한다. 오래 사용되며 도구가 붙고, 문서가 남고, 깨지는 지점이 고쳐진다. 사람들은 그 위에서 일하고, 다른 시스템은 그 존재를 전제로 설계된다. 그렇게 시간이 지나면 기능만으로는 설명하기 어려운 층이 생긴다. 호환성, 신뢰, 관성, 그리고 사람들이 그것을 당연하게 여기는 방식이다.

나는 그 누적된 층을 시간이라고 부르고 싶다.

AI 시대에 대비해야 한다면, 나는 결국 이 시간을 어떻게 쌓을 것인가를 생각해야 한다고 느낀다. 더 빠르게 초안을 만드는 법보다, 내가 쓴 시간이 무엇으로 남는지를 봐야 한다. 도구가 남을 수도 있고, 기록이 남을 수도 있고, 판단 기준이 남을 수도 있다. 생활 방식이 조금 변할 수도 있고, 내가 반복해서 사용하는 작업 환경이 달라질 수도 있다. 넓든 깊든, 중요한 것은 그 시간이 어딘가에 남아야 한다는 점이다.

## 빠르게 떠도는 시간

시간이 남는 구조가 없으면, AI를 쓰는 시간은 쉽게 흩어진다.

새로운 도구를 따라가고, 유행하는 프롬프트를 저장하고, 남이 만든 결과물을 보고 따라 해본다. 그 순간에는 배우고 있는 것처럼 느껴진다. 실제로 배우는 것도 있다. 하지만 시간이 지나도 내 환경이 바뀌지 않고, 내 판단이 달라지지 않고, 내 방식이 조금도 정리되지 않는다면 그 시간은 축적되었다기보다 이동한 것에 가깝다.

AI 시대에 가장 조심해야 할 것은 느리게 만드는 것이 아니라, 빠르게 떠돌기만 하는 것일지도 모른다.

대중적 생성형 AI가 사람들에게 본격적으로 서비스로 제공된 지는 이제 약 3년 반이 되었다. 이 글을 쓰는 2026년 5월 기준으로, OpenAI가 [ChatGPT를 공개한 날](https://openai.com/index/chatgpt/)은 2022년 11월 30일이다.

그동안 많은 것이 바뀌었다. 사람들은 글을 쓰기 전에 AI에게 초안을 요청하고, 코드를 짜기 전에 에이전트에게 일을 맡기고, 디자인이나 기획의 첫 단계를 예전보다 훨씬 빠르게 시작한다.

하지만 그럼에도 불구하고, 개인이 짧은 기간 안에 AI만으로 OS를 만들거나, 새로운 표준이 되는 도구를 만들거나, 지속적으로 수익을 내는 서비스를 쉽게 런칭하는 일은 아직 흔하지 않다. 이건 AI가 별로라는 뜻이 아니다. 오히려 반대에 가깝다. AI는 첫 형태를 만드는 비용을 크게 낮췄지만, 어떤 것이 실제 세계에서 오래 쓰이고 신뢰를 얻는 시간까지 없애지는 못했다.

## 반복은 사라지지 않는다

첫 형태를 빨리 얻어도, 그것이 충분한 결과인지 판단하는 일은 별개의 영역으로 남는다.

AI의 결과를 볼 때는 두 값을 나눠서 생각해야 한다. 하나는 지금 나온 결과가 얼마나 내 의도와 맥락에 가까운가, 즉 완성도다. 다른 하나는 그런 수준의 결과가 얼마나 안정적으로 반복되는가, 즉 성공률이다. 좋은 결과가 한 번 나오는 것과, 같은 수준의 결과가 계속 나오는 것은 다르다.

AI는 점점 더 나은 결과를 더 자주 내놓고 있다. 그래서 많은 일은 예전보다 훨씬 앞에서 시작된다. 하지만 결과가 그럴듯해질수록 오히려 판단의 기준은 더 중요해진다. 이 정도면 충분한가. 실제 사용자나 실제 환경에서도 버틸 수 있는가. 지금은 맞지만 시간이 지나도 맞을까. 이런 질문들은 생성 속도만으로 사라지지 않는다.

그래서 반복은 사라지지 않는다. 다만 반복의 위치가 조금 바뀐다.

예전에도 첫 결과물 자체가 언제나 오래 걸렸던 것은 아니다. 메모든 시안이든 대략의 초안은 빠르게 만들 수 있었다. 오래 걸렸던 것은 대개 그것을 실제 맥락에 맞추고, 부족한 부분을 발견하고, 다시 손보는 과정이었다. AI는 시작을 크게 가볍게 만들지만, 그 뒤에 남는 판단과 검증까지 자동으로 내 시간으로 바꿔주지는 않는다. 결국 중요한 것은 아낀 시간을 어디에 다시 쌓을 것인가다.

## 자기 시간을 남기는 사람

그래서 앞으로 개인에게 필요한 것은 자기만의 생태계에 가깝다고 생각한다. 여기서 생태계는 거창한 플랫폼을 뜻하지 않는다. 내가 자주 꺼내 쓰는 기록, 내가 반복해서 쓰는 도구, 내 취향에 맞게 정리된 환경, 내가 만든 작은 결과물들, 시간이 지날수록 선명해지는 판단 기준 같은 것들이다.

이것들은 하루아침에 만들어지지 않는다. 그리고 꼭 깊어야만 하는 것도 아니다. 어떤 사람은 하나의 도구를 깊게 다듬을 수 있고, 어떤 사람은 여러 시도를 넓게 연결할 수 있다. 중요한 것은 그 시간이 지나간 뒤에도 흔적이 남는가다. 과거의 내가 만든 것들이 현재의 나를 조금 더 잘 움직이게 만드는가다.

AI는 시간을 줄여준다. 하지만 시간이 남게 해주지는 않는다.

시간이 남으려면, 내가 그것을 남는 형태로 바꿔야 한다. 빠르게 만든 초안을 내 문장으로 고치고, 흩어진 시도를 내 도구로 묶고, 남의 결과물을 구경하는 데서 멈추지 않고 내 환경을 조금씩 바꿔야 한다. 그렇게 쌓인 것들은 처음에는 작고 느려 보일 수 있다. 하지만 시간이 지나면, 그것이 나를 대신해서 다음 시간을 줄여준다.

AI 시대에 시간의 값어치는 사라지는 것이 아니라, 오히려 더 선명해진다.

모두가 더 빨리 첫 형태를 만들 수 있게 되면, 차이는 첫 형태 이후에 생긴다. 무엇을 오래 붙들었는지, 무엇을 고쳐왔는지, 무엇이 내 안에 남았는지. 결국 대체 불가능한 것은 빠르게 생성된 결과물이 아니라, 그 결과물이 시간을 통과하며 쌓아 올린 층이다.

그래서 나는 AI 시대에 더 많은 것을 빨리 만드는 사람보다, 자기 시간을 남기는 사람이 더 오래 갈 것이라고 생각한다.</content:encoded></item><item><title>개인은 AI를 어떻게 써야 할까</title><link>https://blog.ga-ut.com/archive/how-individuals-should-use-ai/</link><guid isPermaLink="true">https://blog.ga-ut.com/archive/how-individuals-should-use-ai/</guid><description>스킬 설계에 매달리기보다, 나에게 맞는 AI 제품을 고르고 깊게 활용하는 개인의 전략을 정리한다.</description><pubDate>Sat, 07 Feb 2026 00:00:00 GMT</pubDate><content:encoded>AI를 더 잘 쓰기 위해 개인이 무언가를 더 해야 하는가?
나는 이 질문에 아니라고 대답할 것이다.

요즘은 AI를 잘 쓰기 위해 스킬을 만들고, 오케스트레이터를 설계하고, 복잡한 구조를 이해해야 한다는 이야기가 많다. 하지만 AI 회사들은 우리가 그 과정에서 쌓은 노력들을 보장해주는 방향으로 움직이지 않는다. 널리 사용되는 오픈소스나 운영체제처럼 하위 호환성을 전제로 발전하는 구조라고 보기도 어렵다. 새로운 기능이나 확장이 등장할 때 Breaking Change는 자연스러운 전제로 받아들여야 한다.

게다가 AI 생태계 자체가 아직 완숙 단계에 이르렀다고 보기도 어렵다. 모델마다 철학과 접근 방식이 다르고, 표준 역시 정립되지 않은 상태다. 지금도 통합과 분열을 반복하는 과정에 있다. 이런 상황에서 개인이 특정 방식이나 구조에 깊게 투자하는 것이 과연 합리적인 선택인지에 대해서는 꽤 회의적이다.

## 스킬과 오케스트레이터, 그리고 토큰 효율성

최근 각광받고 있는 스킬 기반 접근이나 오케스트레이터는 대부분 AI 파운데이션 기업이 아닌 개인이나 조직 단위에서 만들어지고 있다. 다들 좋다고 말하지만, 실제로는 두 가지 문제가 있다고 본다.

첫째, ‘좋다’는 것에 대한 정의가 없다.
벤치마크도 없고, 비교 가능한 지표도 없다. “편해 보인다”, “구조가 좋아 보인다” 같은 감각적인 평가가 전부다. 애초에 무엇이 좋은 것인지에 대한 합의 자체가 없다 보니, 정말로 좋은지 판단하기 어렵다.

둘째, 토큰 사용이 지나치게 비효율적이다.
모델을 쌩으로 사용한 상태에서 오케스트레이션을 얹는 구조를 개인 단위에서 운영하는 것은 토큰 효율성 측면에서 매우 좋지 않다. 컨텍스트는 계속 누적되고, 비용과 복잡도는 함께 증가한다. 이 구조는 켜켜이 쌓이는 이메일 답신과 비슷하다. 처음에는 문제가 없어 보이지만, 시간이 지날수록 읽기 어렵고, 관리하기 힘들어지며, 결국 부담이 된다.

그래서 나는 이런 영역은 개인이 흉내 내기보다는, 모델을 직접 다룰 수 있는 회사나 엔터프라이즈급 조직에서 모델 수준에서 최적화하며 오케스트레이션까지 책임지는 것이 맞다고 생각한다. 대규모 트래픽과 비용 구조를 감당할 수 있는 주체가 맡아야 오래 간다.

## 그렇다면 개인은 무엇을 해야 할까

이건 정답이라기보다는, 내가 꽤 확신하는 방향이다.

먼저 분기를 타야 한다.
나는 모델을 직접 다룰 수 있는 사람인가, 아니면 그들이 만든 제품을 사용하는 쪽인가.
대부분은 후자일 것이다. 지금 우리가 사용하는 수많은 AI 제품에 직접 기여할 수 있는 사람은 많지 않다.

그렇다면 전략은 비교적 명확해진다.

첫 번째는, 잘 고르는 능력을 기르는 것이다.
AI 제품을 고르는 일은 생각보다 어렵다. 좋은 식재료를 고르는 것과 비슷하다. 직접 먹어보지 않으면 알 수 없고, 남의 말만 들어서는 감이 오지 않는다. 어릴 때 다양한 음식을 접해본 사람이 나중에 취향이 분명해지는 것처럼, AI도 많이 써봐야 한다. 직접 써보고, 실패해보고, 비교해보는 과정이 필요하다.

두 번째는, 기호가 생겼다면 깊게 써보는 것이다.
하나만 파라는 이야기는 아니다. 그렇게 하면 오히려 편식이 된다. 여러 제품을 골고루 써보되, 좋아하는 것은 반복해서 써보고, 다른 방식으로도 써보며, 한계를 직접 느껴보는 과정이 필요하다. 찌기도 해보고, 굽기도 해보고, 튀겨도 보면서 재료의 성질을 익히는 것과 같다. 이 과정을 거치면 자연스럽게 이해도가 올라간다.

## 우리는 식재료를 파는 사람이 아니라, 요리해서 파는 사람이다

우리는 모델을 만드는 사람들이 아니다. 식재료를 공급하는 입장이 아닌 것이다.
우리는 그들이 제공한 것을 바탕으로 실제 요리를 하고, 제품을 만들어 파는 사람들이다.

AI를 잘 쓰는 것도 중요하지만, 그들이 제공하는 생태계를 충분히 이해하고 그 위에서 사용자에게 실제로 가치 있는 결과물을 만들어내는 것에 더 방점이 찍히는 문화가 더 건강하다고 생각한다.

요즘 SNS나 강의, 유튜브를 보면 FOMO를 부추기는 메시지가 너무 많다. 하지만 본질은 단순하다.
많이 써보고, 취향을 만들고, 이해도를 쌓고, 실제로 만들어서 팔아보는 것.

그 과정에서 양질의 제품이 늘어나고, 경쟁이 생기며, 또 다른 국면이 열린다.
나는 그 흐름이 더 오래 간다고 믿는다.</content:encoded></item><item><title>JavaScript의 export는 왜 존재할까</title><link>https://blog.ga-ut.com/archive/meaning-of-export/</link><guid isPermaLink="true">https://blog.ga-ut.com/archive/meaning-of-export/</guid><description>export를 문법이 아닌 설계 의사결정으로 바라보며, 접근 제어자와 정보 은닉 원칙을 함께 살펴본다.</description><pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate><content:encoded># JavaScript의 `export`가 가지는 진짜 의미

JavaScript의 `export`는 종종 이렇게 이해된다.

&gt; 다른 파일에서 쓰기 위해 붙이는 키워드

하지만 이 설명만으로는 왜 `export`라는 개념이 필요한지, 왜 어떤 것은 외부로 드러내고 어떤 것은 내부에 남겨두는지, 그리고 왜 때로는 파일을 굳이 쪼개지 않는 선택이 나오는지를 충분히 설명하기 어렵다.

이 글은 문법 사용법을 정리하려는 목적이 아니다. JavaScript의 `export`를 하나의 기능이 아니라 **설계 의사결정의 결과**로 바라보는 관점에 대해 이야기해보려 한다.

## 접근 제어자는 왜 존재하는가

대부분의 프로그래밍 언어에는 접근 제어자가 존재한다.

```java
public
private
protected
```

이 키워드들은 단순히 보이게 하거나 숨기기 위한 장치가 아니다. 핵심은 누가 이 코드에 의존해도 되는지를 명확히 하는 데 있다. 조금 더 풀어 말하면, 무엇은 비교적 자유롭게 바뀔 수 있고 무엇은 신중하게 다뤄져야 하는지를 구분하기 위한 장치다.

`public`으로 선언된 요소는 외부 코드가 의존해도 괜찮다는 의미를 가진다. 반대로 `private`은 내부 구현으로, 언제든 변경될 수 있다는 전제를 담고 있다. 결국 접근 제어자는 가시성의 문제가 아니라 **변경 가능성을 통제하기 위한 도구**라고 볼 수 있다.

## JavaScript에는 접근 제어자가 없다, 대신 `export`가 있다

JavaScript에는 `public`이나 `private`와 같은 키워드가 없다. 대신 모듈 시스템이 있고, 그 중심에 `export`가 있다.

```ts
export function foo() {}
```

이 선언은 단순히 다른 파일에서 사용할 수 있다는 의미로 끝나지 않는다. 이 함수는 외부에 대해 책임지는 API이며, 외부 코드가 의존해도 되는 안정적인 경계라는 신호에 가깝다.

반대로 다음과 같은 함수는 전혀 다른 성격을 가진다.

```ts
function internalFoo() {}
```

이 함수는 외부에서 알 필요가 없는 내부 구현이다. 언제든 변경될 수 있고, 그 변경에 대해 외부 코드는 기대를 가져서는 안 된다. JavaScript에서 모듈은 하나의 캡슐화 단위가 되며, `export`는 그 경계를 드러내는 역할을 한다.

## `export`는 계약이다

다음 두 코드는 문법적으로는 동일하지만, 전달하는 설계적 의미는 다르다.

```ts
export function process() {}

export function normalizeUserPayload() {}
```

`process`라는 이름은 내부 구현처럼 느껴진다. 이 함수가 무엇을 처리하는지, 외부에서 어떤 기대를 가져야 하는지가 명확하지 않다. 반면 `normalizeUserPayload`는 함수의 역할과 책임을 비교적 분명하게 드러낸다.

이 차이는 `export`된 이름이 단순한 식별자가 아니라 **공개된 언어(public vocabulary)**라는 점에서 비롯된다. 외부와 공유되는 개념이기 때문에, `export`된 식별자는 자연스럽게 설계 문서와 비슷한 역할을 하게 된다.

그래서 내부 변수나 함수는 비교적 자유로운 이름을 가져도 되지만, 외부로 노출되는 이름만큼은 길어지더라도 의도가 충분히 드러나는 편이 좋다. 이는 스타일의 문제가 아니라 계약을 명확히 하기 위한 선택에 가깝다.

## 파일을 나누는 기준에 대해

기능이 늘어날수록 파일을 계속 쪼개는 구조를 흔히 볼 수 있다. 하지만 파일 분리는 그 자체가 목적이 될 수 없다. 설계 관점에서 더 중요한 것은 변경이 함께 일어나는 단위가 무엇인지다.

여러 함수가 항상 함께 수정되고, 외부에서는 하나의 개념으로 사용된다면 한 파일에 남겨두는 편이 오히려 더 안정적인 구조가 된다. 파일을 나누지 않는 선택 역시, `export`를 최소화하는 선택과 마찬가지로 의도적인 경계 설정의 결과다.

## 소프트웨어 설계 원칙과의 연결: Information Hiding

이 관점은 새로운 개념은 아니다. 소프트웨어 설계 원칙 중에는 **Information Hiding(정보 은닉)**이라는 오래된 원칙이 있다. 구현의 세부사항을 숨기고, 외부에는 안정적인 인터페이스만을 노출하라는 생각이다.

JavaScript의 `export`는 이 Information Hiding을 모듈 단위에서 실천할 수 있게 해주는 수단이라고 볼 수 있다. `export`되지 않은 코드는 자연스럽게 숨겨진 구현이 되고, `export`된 요소만이 외부와의 인터페이스로 남는다.

다만 JavaScript에서는 이 경계가 문법적으로 강제되지 않기 때문에, 무엇을 노출하고 무엇을 숨길지에 대한 판단이 설계자의 몫으로 남는다. 어떤 요소를 `export`할지 결정하는 과정 자체가, 어떤 부분을 안정적인 계약으로 삼을지를 정하는 일과 맞닿아 있다.

## 정리

JavaScript의 `export`는 파일 간 코드를 공유하기 위한 문법을 넘어, 설계자가 의도적으로 그은 경계에 가깝다. 모듈은 캡슐화 단위가 되고, `export`는 외부와의 인터페이스를 정의하며, 네이밍은 그 의도를 설명하는 언어가 된다.

이 관점은 코드를 평가하거나 비교하기 위한 기준이 아니라, 코드를 어떻게 바라보고 유지해 나갈지에 대한 하나의 참고점으로 작용할 수 있다.</content:encoded></item><item><title>Request-scoped state라는 개념을 React에서 구현하는 방법</title><link>https://blog.ga-ut.com/archive/request-scoped-state-in-react/</link><guid isPermaLink="true">https://blog.ga-ut.com/archive/request-scoped-state-in-react/</guid><description>props나 Context 없이, 요청 단위 상태를 주입·공유하는 React cache 기반 패턴.</description><pubDate>Wed, 24 Dec 2025 00:00:00 GMT</pubDate><content:encoded>React로 서버 코드를 작성하다 보면, 한 번쯤은 설명하기 애매한 불편함을 마주하게 된다.

컴포넌트에는 state가 있고, Context에는 scope가 있다. 하지만 **요청(request) 자체에 묶인 상태**를 다루는 방법은 명확하지 않다. 그 결과 우리는 값을 props로 전달하거나, Context에 얹거나, 혹은 같은 요청 안에서 인증 정보나 계산 결과를 여러 번 다시 읽게 된다.

이 글은 바로 그 지점에서 출발한다.

&gt; **“요청 하나의 생명주기에만 묶인 상태를,  
&gt; React에서는 어떻게 다뤄야 할까?”**

서버 프로그래밍에서는 이 개념이 낯설지 않다.  
Spring의 request scope, Node의 request context, Python의 context-local / request-local state처럼, request-scoped state는 오래전부터 사용되어 온 개념이다.

다만 React는 오랫동안 클라이언트 중심의 라이브러리였고, ‘요청’이라는 실행 단위 자체를 전제로 하지 않았다. 그래서 React의 상태 모델에는 이 층위가 명시적으로 존재하지 않았다.

React Server Component가 도입되면서 상황이 달라졌다. React는 이제 요청 단위로 코드를 실행하고, 요청이 끝나면 해당 실행 컨텍스트를 정리한다. 그리고 이 실행 모델 위에 놓인 `cache()` API는, 의도하지 않았지만 중요한 가능성을 하나 제공한다.

&gt; **요청 동안만 유지되는 상태를 만들 수 있는 최소한의 공식 메커니즘**

이 글에서는 React의 `cache()`를 성능 최적화 도구가 아니라, **request-scoped state를 구현하기 위한 기반**으로 사용한다.

목표는 단순하다.

- 요청 동안만 유지되고
- props drilling 없이 접근 가능하며
- 컴포넌트와 API 로직 어디서든 사용할 수 있고
- 필요하다면 요청 중간에 값을 주입하거나 수정할 수 있는 상태

이 글은 그 상태를 어떻게 만들고, 왜 이 방식이 현재 React 실행 모델에서 가장 자연스러운 선택인지 정리한 기록이다.


## 왜 기존 방법들은 아쉬운 지점이 있었을까

### React Context의 한계

React Context는 UI 트리 안에서 값을 공유하는 데에는 매우 유용하다. 하지만 Server Component 환경에서 API 호출이나 서비스 레이어까지 고려하면 제약이 분명해진다.

- Context는 컴포넌트 트리에 종속된다
- 훅이 아니기 때문에 일반 함수나 서비스 코드에서 직접 사용할 수 없다

```ts
// service/get-user-policy.ts
export async function getUserPolicy() {
  const user = useUserContext(); // 사용할 수 없음
}
```

결국 Context로 해결되지 않는 영역에서는 다시 인자를 전달해야 하고, 이는 처음 의도했던 “전역 접근”과는 거리가 생긴다.

cookies / headers API의 한계

cookies()와 headers()는 요청 정보를 읽어오는 데에는 유용하지만, request-scoped state를 구성하기에는 역할이 다르다.
	•	서버에서만 사용할 수 있고
	•	재사용하려면 server function으로 감싸야 하며
	•	그 경우 동일 요청 맥락의 값을 공유하는 것이 아니라, 다시 조회하는 호출이 된다

또한 이 API들은 본질적으로 읽기 전용 입력값이다.
요청 중간에 계산된 결과를 저장하거나, 다른 로직에서 재사용할 상태로 승격시키기에는 적합하지 않다.


## 우리가 원했던 조건

정리해보면, 이 글에서 다루고 싶은 상태는 다음 조건을 만족해야 했다.
	1.	요청 단위로 생성되고 요청이 끝나면 사라질 것
	2.	props drilling이 필요 없을 것
	3.	Server Component 어디서든 접근 가능할 것
	4.	컴포넌트뿐 아니라 API / 서비스 코드에서도 사용 가능할 것
	5.	요청 중간에 초기값 주입이나 결과 저장이 가능할 것

이 조건을 모두 만족하는 선택지는 많지 않다.


## React cache가 잘 맞아떨어지는 지점

React의 cache() API는 같은 요청 안에서 동일한 함수 호출을 하나로 묶어준다. 이 특성 덕분에, 요청마다 독립적인 메모리 공간을 가진 것처럼 사용할 수 있다.

이를 조금 확장해, 요청 스코프의 작은 저장소처럼 다뤄보자는 것이 이 패턴의 출발점이다.


## Cache Manager 형태로 감싸기

```ts
// cache/react-cache-manager.ts
import { cache } from &quot;react&quot;;

export class ReactCacheManager {
  private store = new Map&lt;string, unknown&gt;();

  get&lt;T&gt;(key: string): T | undefined {
    return this.store.get(key) as T | undefined;
  }

  set&lt;T&gt;(key: string, value: T): T {
    this.store.set(key, value);
    return value;
  }
}

// 요청마다 하나의 인스턴스가 생성된다
export const getCacheManager = cache(() =&gt; {
  return new ReactCacheManager();
});
```

이 코드의 핵심은 단순하다.
	•	getCacheManager()는 요청당 한 번만 실행된다
	•	내부 Map은 요청 동안 유지된다
	•	요청이 끝나면 함께 정리된다

전역 변수처럼 보이지만, 실제 수명은 요청 스코프에 한정된다.


## 유저 컨텍스트를 요청 전역 상태처럼 사용하기

여기서 중요한 점은, 유저 인증 정보를 이 함수 안에서 직접 읽지 않는다는 것이다.

인증 값은 보통 요청 초기에 한 번만 계산된다. 예를 들어 미들웨어, 서버 액션, 혹은 서버 컴포넌트의 최상단에서 인증을 수행하고, 그 결과를 요청 스코프 캐시에 주입(set) 한다.

이후의 로직들은 인증 과정을 다시 알 필요 없이, 동일한 캐시에서 값을 조회(get)하기만 하면 된다.

```ts
// auth/get-user-context.ts
import { getCacheManager } from &quot;@/cache/react-cache-manager&quot;;

const USER_KEY = &quot;auth:user&quot;;

export type UserContext = {
  userId: string;
  locale: string;
  timezone: string;
};

export async function getUserContext(): Promise&lt;UserContext | null&gt; {
  const cache = getCacheManager();

  const cached = cache.get&lt;UserContext&gt;(USER_KEY);
  if (cached) return cached;

  // 이 함수는 직접 인증 정보를 읽지 않는다.
  // 실제 인증 값은 요청 초기에 다른 레이어에서 주입된다.

  const injected = cache.get&lt;UserContext&gt;(USER_KEY);
  if (injected) return injected;

  return null;
}
```

이 함수는 이제 다음과 같은 특징을 가진다.
	•	어디서 호출하든 동일한 값을 반환하고
	•	중복 계산은 한 번으로 수렴하며
	•	요청 중간에 값을 저장할 수도 있다


## 요청 초기에 인증 값을 주입하기

인증 값은 반드시 서버에서만 계산될 필요는 없다. 실제 서비스에서는 클라이언트 컴포넌트 Provider가 서버로부터 인증 결과를 전달받아, 이를 요청 스코프 캐시에 주입하는 형태가 더 자연스러운 경우도 많다.

중요한 점은, 이 Provider가 Client Component이면서도 SSR 과정에서는 서버에서 함께 실행된다는 사실이다. 이 특성을 이용하면, 클라이언트 전용 상태처럼 보이는 코드에서도 서버 요청 스코프에 값을 안전하게 주입할 수 있다.

```ts
// auth/init-user-context.ts
import type { UserContext } from &quot;@/auth/get-user-context&quot;;
import { getCacheManager } from &quot;@/cache/react-cache-manager&quot;;

export function initUserContext(user: UserContext | null) {
  const cache = getCacheManager();
  cache.set(&quot;auth:user&quot;, user);
}
```

이 초기화 함수는 보통 다음과 같은 위치에서 호출된다.
	•	서버 컴포넌트의 진입점
	•	클라이언트 컴포넌트 Provider에서 서버로부터 받은 값 전달
	•	혹은 인증 미들웨어 이후의 첫 진입 지점

핵심은 요청 초기에 한 번만 set 하고, 이후에는 get만 한다는 점이다.

```ts
// app/AuthProvider.tsx
&apos;use client&apos;

import type { UserContext } from &quot;@/auth/get-user-context&quot;;
import { initUserContext } from &quot;@/auth/init-user-context&quot;;

export function AuthProvider({
  user,
  children,
}: {
  user: UserContext | null;
  children: React.ReactNode;
}) {
  // 이 컴포넌트는 Client Component이지만,
  // 초기 SSR 단계에서는 서버에서도 실행된다.
  // 따라서 이 시점의 set은 요청 스코프 캐시에 반영된다.

  initUserContext(user);

  return &lt;&gt;{children}&lt;/&gt;;
}
```

이 Provider는 다음과 같은 역할만 가진다.
	•	서버로부터 전달받은 인증 결과를
	•	요청 스코프 캐시에 한 번 주입하고
	•	이후의 컴포넌트와 로직이 이를 공통으로 사용하게 만든다


## 컴포넌트와 API에서 동일하게 사용하기

```ts
// app/page.tsx
export default async function Page() {
  const user = await getUserContext();
  const policy = await getUserPolicy(); // 내부에서 getUserContext 사용

  return &lt;Main /&gt;;
}

// service/get-user-policy.ts
export async function getUserPolicy() {
  const user = await getUserContext();
  if (!user) return null;

  // 도메인 로직
}
```

Context로는 다루기 어려웠던 구조지만, 이 패턴에서는 자연스럽게 성립한다.


## 마무리하며

이 패턴은 모든 상황에 대한 정답은 아니다. 다만,
	•	요청 스코프라는 경계를 명확히 하고
	•	상태 전달 부담을 줄이며
	•	컴포넌트와 API 로직을 같은 모델로 묶고 싶을 때

실용적인 선택지가 될 수 있다.

React cache()를 성능 최적화 도구로만 보지 않고,
요청 단위 상태 관리의 기반으로 바라보면 이런 설계도 가능해진다.

필요에 따라 이 아이디어를 조금 더 단순하게, 혹은 팀 상황에 맞게 변형해 사용할 수도 있을 것이다.</content:encoded></item><item><title>제어 역전(IoC)과 의존성 주입(DI)</title><link>https://blog.ga-ut.com/archive/di-and-ioc/</link><guid isPermaLink="true">https://blog.ga-ut.com/archive/di-and-ioc/</guid><description>프레임워크 기능이 아니라, 제어권 배치라는 구조적 선택으로 IoC와 DI를 다시 바라본다.</description><pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate><content:encoded>IoC(Inversion of Control)와 DI(Dependency Injection)는 종종 특정 프레임워크의 핵심 기능처럼 설명된다. 특히 Spring을 경험한 개발자라면, 이 두 개념을 자연스럽게 Spring 컨테이너와 함께 떠올린다.

하지만 이 개념들의 본질은 프레임워크에 있지 않다. IoC와 DI는 **프로그램의 제어권을 어디에 둘 것인가에 대한 구조적 선택**이다. 그리고 우리는 이미, 프레임워크를 쓰지 않아도 이 선택을 매일 하고 있다.



## 우리는 이미 IoC를 쓰고 있다 — React에서

React에서 IoC는 특별한 패턴이 아니다. props, hook, context는 모두 **구현을 내부에 고정하지 말고, 조립 단계에서 결정하라**는 같은 메시지를 담고 있다.

### 1) Hook 인자 주입 = DI

```tsx
type User = { id: string; name: string }
type FetchUser = (id: string) =&gt; Promise&lt;User&gt;

function UserName({ userId, fetchUser }: { userId: string; fetchUser: FetchUser }) {
  const [name, setName] = React.useState(&quot;&quot;)

  React.useEffect(() =&gt; {
    fetchUser(userId).then(u =&gt; setName(u.name))
  }, [userId, fetchUser])

  return &lt;span&gt;{name}&lt;/span&gt;
}
```

이 컴포넌트는 데이터를 **어떻게** 가져오는지 모른다. HTTP 요청인지, 캐시 조회인지, 테스트용 mock인지도 관심 없다.

&gt; 이 컴포넌트는 **의존성을 소유하지 않는다.**

어떤 구현을 쓸지는 부모 컴포넌트가 결정한다. 이 결정권의 이동이 IoC이며, props를 통해 전달되는 구현이 DI다.



### 2) 조립(Composition) 단계가 컨테이너 역할을 한다

```tsx
function createHttpFetchUser(baseUrl: string): FetchUser {
  return async (id) =&gt; {
    const res = await fetch(`${baseUrl}/users/${id}`)
    return res.json()
  }
}

export function App() {
  const fetchUser = createHttpFetchUser(&quot;/api&quot;)
  return &lt;UserName userId=&quot;42&quot; fetchUser={fetchUser} /&gt;
}
```

React에서는 컴포넌트 트리 상단이 **의존성을 조립하는 지점**이 된다. 별도의 DI 컨테이너가 없어도 구조는 동일하다.



### 3) Context Provider = 런타임 DI 컨테이너

```tsx
type Deps = { fetchUser: FetchUser }
const DepsContext = React.createContext&lt;Deps | null&gt;(null)

function useDeps() {
  const ctx = React.useContext(DepsContext)
  if (!ctx) throw new Error(&quot;DepsProvider missing&quot;)
  return ctx
}

function UserName({ userId }: { userId: string }) {
  const { fetchUser } = useDeps()
  React.useEffect(() =&gt; {
    fetchUser(userId)
  }, [userId, fetchUser])
  return &lt;span /&gt;
}

export function App() {
  const deps: Deps = { fetchUser: createHttpFetchUser(&quot;/api&quot;) }
  return (
    &lt;DepsContext.Provider value={deps}&gt;
      &lt;UserName userId=&quot;42&quot; /&gt;
    &lt;/DepsContext.Provider&gt;
  )
}
```

Context는 React에서의 **런타임 DI 컨테이너**에 가깝다. Provider를 교체하는 것만으로 테스트 환경과 실행 환경을 분리할 수 있다.



### 4) React 내부의 IoC — 호출권은 프레임워크에 있다

```tsx
function Button({ onClick }: { onClick: () =&gt; void }) {
  return &lt;button onClick={onClick}&gt;+&lt;/button&gt;
}
```

개발자는 `onClick`을 전달하지만, **언제·어떻게 호출할지는 React가 결정**한다. `useEffect` 역시 동일하다. 실행 시점과 순서는 컴포넌트가 아니라 프레임워크가 소유한다.

이것이 React 내부에 이미 내재된 IoC다.



## IoC의 핵심은 “누가 결정하는가”다

전통적인 구조에서는 다음과 같은 흐름이 자연스러웠다.

* 함수나 컴포넌트가 직접 객체를 생성하고
* 구현을 선택하며
* 실행 시점까지 소유한다

이 구조는 단순하지만, 변경과 테스트에 취약하다. 구현이 코드 내부에 고정되기 때문이다.

IoC는 이 흐름을 뒤집는다.

* 컴포넌트는 **무엇이 필요한지만 선언**하고
* 어떤 구현을 쓸지는 외부에서 결정한다

의존성 주입(DI)은 이 선택을 구현하는 여러 방법 중 하나일 뿐이다. 중요한 것은 ‘주입’이라는 행위가 아니라, **결정 주체가 이동했다는 사실**이다.



## 그렇다면 Spring은 무엇을 했는가

Spring은 IoC나 DI를 발명하지 않았다. Spring이 한 일은 이 구조적 선택을 **대규모 시스템에서도 유지 가능하게 만든 것**이다.

객체 수가 수백, 수천 개로 늘어나면 다음과 같은 문제가 생긴다.

* 생성 시점은 언제인가
* 생명주기는 어떻게 관리할 것인가
* 어떤 조합이 허용되는가

Spring의 컨테이너는 이 복잡도를 감당하기 위한 자동화 장치다. 개념의 근원이 아니라, 개념을 확장한 구현체에 가깝다.



## 내가 DI를 이해하는 방식

나는 DI를 “의존성을 주입받는 기술”로 이해하지 않는다. DI는 **제어권을 어디에 둘 것인가에 대한 설계 결정**이다.

* 구현을 내부에서 고정할 것인가
* 외부에서 선택하게 할 것인가
* 그리고 그 선택을 누가 책임질 것인가

Spring은 그 결정을 돕는 강력한 도구다. 하지만 그 도구를 사용한다고 해서, 자동으로 구조를 이해하게 되는 것은 아니다.



## 마무리

프레임워크는 변한다. 하지만 **제어권을 어떻게 배치할 것인가**라는 문제는 변하지 않는다.

IoC와 DI를 프레임워크 기능이 아닌 **프로그램 구조의 문제로 이해할 때**, 우리는 도구를 단순히 사용하는 단계를 넘어, 왜 이런 도구가 필요해졌는지를 설명할 수 있게 된다.</content:encoded></item></channel></rss>