이번 호의 중심은 9월 8일 공개된 Meta의 개인 AI 에이전트 Muse입니다. 발표 직후만 보면 브라우저를 사용하고, 앱을 연결하고, 장기 작업을 수행하는 또 하나의 에이전트 제품처럼 보일 수 있었습니다. 하지만 이후 실제 사용 사례와 논란이 이어지면서 오히려 이 제품이 던진 질문이 더 선명해졌습니다.

에이전트가 사용자의 계정으로 쇼핑하고, 메시지와 일정에 접근하며, 앱이 닫힌 뒤에도 계속 일하기 시작하면 문제는 더 이상 모델 성능만이 아닙니다. 어디까지 접근할 수 있는지, 언제 사람에게 승인을 요청하는지, 외부 서비스와 충돌하면 어떻게 처리하는지, 무엇을 실행했는지 사용자가 확인할 수 있는지가 제품의 핵심이 됩니다.

Muse는 브라우저·장기 메모리·앱 연결·결제·사람의 승인·감사 기록을 하나의 개인 에이전트 제품으로 묶었습니다. 출시 후 불과 몇 주 사이 외부 서비스의 접근 차단, 데이터 접근 방식 논란, 클라이언트 보안 취약점까지 등장하면서 이런 운영 구조가 왜 중요한지도 실제 사례로 드러나고 있습니다.

같은 시기 OpenAI는 GPT-6를 Sol과 Luna로 나눠 작업별 비용 선택지를 넓혔고, Vercel은 샌드박스가 종료된 뒤에도 작업 상태를 보존하는 Drive를 공개했습니다. 모델, 실행 환경, 지속 상태가 각각 상품화되면서 에이전트 개발의 중심은 프롬프트에서 운영 구조로 이동하고 있습니다.

이번 주 가장 흥미로운 변화는 벤치마크 1위 모델이 아니라, 이 구성요소를 일반 사용자가 쓸 수 있는 하나의 제품으로 만든 Muse입니다.


1. Meta Muse, 개인 에이전트를 하나의 완성된 제품으로 묶다

PROOF LEVEL 03 · 공식 확인

사실 요약

Meta는 2026년 9월 8일 개인 AI 에이전트 Muse를 발표했습니다. Muse는 단순히 질문에 답하는 것을 넘어 사용자를 대신해 작업을 수행하는 것을 목표로 합니다. 앱이 닫힌 뒤에도 장기 작업을 이어가고, 브라우저를 열어 웹서비스를 사용하며, 여러 앱과 연결해 사용자의 목표를 실제 행동으로 옮깁니다.

실행 기반은 전용 클라우드 컴퓨터인 Muse Secure VM입니다. Meta는 사용자별 실행 환경에서 데이터와 연결 자격 증명을 관리하고, 사용자가 어느 수준까지 접근 권한을 부여할지 결정할 수 있도록 설계했다고 설명합니다.

이메일 발송이나 구매처럼 되돌리기 어렵거나 민감한 행동에는 사람의 승인을 요청하는 구조가 들어가 있으며, 사용자는 연결 앱과 권한 범위를 관리하고 작업 기록을 확인할 수 있습니다. 결제에는 Stripe Link의 일회용 카드와 구매 보호가 연결됩니다.

현재 미국을 중심으로 iOS·Android·웹에서 출시되고 있으며 AI 안경 지원도 예고됐습니다. 다만 개인정보 보호와 보안에 관한 설명은 현재 Meta가 공개한 제품 설계에 따른 것이며, 독립적인 보안 검증 결과와 동일하게 볼 수는 없습니다.

출시 이후 드러난 것

Muse가 흥미로운 이유는 발표 문서에 적힌 구조만이 아닙니다. 출시 직후부터 그 구조가 실제 서비스 환경에서 시험받고 있습니다.

Amazon은 Muse가 자사 쇼핑 서비스에 접근하는 것을 차단했습니다. Amazon은 Muse가 에이전트임을 명확하게 식별하지 않은 상태로 서비스를 이용한다는 점과 개인정보·보안 문제 등을 이유로 들었습니다. 이 사건은 사용자의 허가를 받은 에이전트라 해도 외부 서비스 사업자가 그 접근을 허용해야 하는지는 별개의 문제라는 점을 보여줍니다.

앞으로 에이전트는 사용자와 서비스 사이의 새로운 행위 주체가 될 수 있습니다. 서비스 사업자 역시 사람과 자동화된 에이전트를 어떻게 구분하고, 어떤 권한을 허용할지 결정해야 합니다.

사용자 데이터 접근 방식에 대한 혼란도 있었습니다. Muse가 사용자가 예상하지 못했던 메시지 내용을 참조했다는 사례가 알려졌고, Meta의 David Singleton은 사용자가 시스템 권한을 허용한 상태에서 데이터에 접근한 것이지만 Muse가 자신이 어떻게 해당 정보를 얻었는지를 잘못 설명했다고 밝혔습니다.

여기서 중요한 문제는 단순히 “권한을 받았는가”만이 아닙니다. 사용자는 에이전트가 지금 어떤 데이터를 보고 있는지, 왜 볼 수 있는지, 그 정보를 어디에 사용했는지 이해할 수 있어야 합니다. 실제 권한 구조가 올바르게 설계돼 있더라도 에이전트가 자신의 행동을 잘못 설명한다면 사용자는 시스템을 신뢰하기 어렵습니다.

보안 문제도 현실화됐습니다. macOS용 Muse에서는 로컬 머신에 이미 악성 코드가 존재하는 경우 에이전트의 동작을 가로챌 수 있는 취약점이 발견됐고, Meta는 이를 패치했습니다. 공격을 위해 사전에 로컬 시스템이 침해돼 있어야 한다는 제한은 있었지만, 에이전트가 카메라·파일·브라우저처럼 실제 시스템 권한과 연결될수록 실행 환경 자체가 새로운 공격면이 된다는 점을 보여줍니다.

이 사례들은 Muse의 보안 구조가 실패했다는 결론도, 반대로 충분히 안전하다는 증거도 아닙니다. 오히려 개인 에이전트가 실제 사람의 계정과 데이터를 다루기 시작하면서 지금까지 설계 문서 속 문제였던 권한, 승인, 정책 충돌, 감사 기록, 실행 환경 보안이 실제 제품 문제로 바뀌고 있다는 신호에 가깝습니다.

개발자에게 중요한 점

Muse가 보여준 것은 단순한 브라우저 자동화가 아닙니다. 장기 실행, 영속 메모리, 비밀정보 격리, 정책 검사, 세분화된 권한, 중요 행동 전 승인, 감사 기록, 결제 보호를 하나의 사용자 경험으로 연결했습니다.

개인 에이전트를 만드는 팀이라면 이제 “우리 모델이 무엇을 할 수 있는가”만 비교해서는 부족합니다. 사용자가 앱을 닫은 뒤에도 작업을 이어가는 방식, 외부 서비스에 부여한 권한을 제한하는 방식, 되돌리기 어려운 행동을 실행하기 전에 사용자를 다시 호출하는 방식까지 제품의 일부가 됩니다.

여기에 에이전트가 무엇을 보고 무엇을 실행했는지 기록하는 방식, 문제가 발생했을 때 어느 상태까지 되돌릴 수 있는지까지 포함하면 에이전트 제품의 경쟁력은 모델 바깥에서 크게 갈리기 시작합니다.

Lee's Take

이번 발표에서 가장 큰 게임 체인저는 Muse의 모델 성능이 아닙니다. Meta가 에이전트를 채팅창 안의 기능이 아니라 개인용 운영체제에 가까운 제품으로 정의했다는 점입니다.

특히 실행하는 주체와 정책을 검사하는 주체를 분리하고, 돈이나 메시지가 실제로 움직이는 지점에 사람의 승인선을 넣은 구조가 중요합니다. 출시 이후 Amazon과의 정책 충돌, 데이터 접근 설명 문제, 로컬 보안 취약점이 등장한 것도 오히려 이런 구조가 왜 필요한지를 보여줍니다.

이제 개인 에이전트의 경쟁은 “누가 더 똑똑한 모델을 가졌는가”만으로 결정되지 않습니다. 누가 더 안전하게 권한을 주고, 행동을 설명하고, 실제 서비스를 건드리면서도 통제 가능한 시스템을 만드느냐가 새로운 경쟁 영역이 되고 있습니다.

Meta의 막대한 사용자 접점까지 고려하면 성공 여부와 별개로 개인 에이전트 시장의 제품 기준을 크게 끌어올린 발표라고 봅니다.

이번 주 해볼 일

  • 에이전트가 수행하는 행동을 자동 허용, 실행 전 승인, 절대 금지로 분류합니다.
  • 실행 주체와 정책 검사 주체를 분리할 수 있는지 아키텍처를 검토합니다.
  • 장기 작업에 작업 이력, 권한 사용 기록, 외부 변경 결과를 남깁니다.
  • 에이전트가 현재 어떤 권한과 데이터를 사용하고 있는지 사용자에게 설명할 수 있는지 점검합니다.
  • 비밀번호나 결제정보를 모델 컨텍스트에 직접 노출하지 않는 경로를 설계합니다.
  • 외부 서비스가 에이전트 접근을 거부했을 때 이를 실패로 처리할지, 사람에게 다시 넘길지 결정합니다.

원문: Meta — Introducing Muse: The World’s First Personal AI Agent Built for Everyone


2. GPT-6 Sol과 Luna, 하나의 최고 모델보다 작업별 라우팅을 택하다

PROOF LEVEL 03 · 공식 확인

사실 요약

OpenAI는 2026년 9월 22일 GPT-6 Sol과 GPT-6 Luna를 API에 출시했습니다. 두 모델은 텍스트와 이미지를 입력받으며 Responses API와 Chat Completions API에서 사용할 수 있습니다.

272K 토큰 이하 입력의 표준 가격은 100만 토큰당 다음과 같습니다.

  • GPT-6 Sol: 입력 2달러, 캐시 입력 0.20달러, 출력 10달러
  • GPT-6 Luna: 입력 0.10달러, 캐시 입력 0.01달러, 출력 0.50달러

동일한 GPT-6 계열 안에서도 표준 입력·출력 가격 차이는 각각 20배입니다.

개발자에게 중요한 점

이 가격 구조에서는 모든 요청을 가장 강한 모델에 보내는 설계가 빠르게 비경제적이 됩니다. 요청을 분류하고, 저비용 모델이 처리할 수 있는 작업을 분리하며, 실패하거나 불확실할 때만 상위 모델로 올리는 라우팅이 기본 설계가 됩니다.

에이전트 하나를 구성하더라도 모든 단계에 같은 모델을 사용할 이유는 없습니다. 단순 분류나 반복 처리는 저렴한 모델이 맡고, 복잡한 의사결정이나 계획 수정에는 더 강한 모델을 호출할 수 있습니다. 정책 검사 역시 실행 모델과 별도의 경량 모델이나 규칙 시스템으로 분리할 수 있습니다.

결국 모델 평가는 단일 벤치마크 점수보다 실제 업무 완료율, 재시도 횟수, 도구 호출 비용, 사람 검토 시간을 합친 성공한 작업 1건당 총비용으로 이동할 가능성이 큽니다.

Lee's Take

GPT-6의 핵심은 새 이름보다 20배의 가격 간격이라고 봅니다. “어떤 모델을 쓸 것인가”보다 “어떤 순간에 비싼 판단을 호출할 것인가”가 중요해졌습니다.

Muse 같은 개인 에이전트 역시 모든 행동에 최고급 추론을 사용한다면 대규모 서비스가 되기 어렵습니다. 빠른 실행 모델, 고난도 판단 모델, 정책 검사 모델을 역할별로 구성하고 필요한 순간에만 높은 비용을 지불하는 쪽이 현실적인 구조입니다.

모델 선택도 이제 프롬프트 엔지니어링보다 시스템 설계에 가까워지고 있습니다.

이번 주 해볼 일

  • 최근 요청을 단순 처리, 도구 실행, 고난도 판단 세 등급으로 나눕니다.
  • Luna 우선 실행 후 품질 신호에 따라 Sol로 승격하는 작은 평가를 만듭니다.
  • 모델별 성공률뿐 아니라 재시도와 사람 검토까지 포함한 비용을 기록합니다.
  • 토큰 가격이 아니라 성공한 작업 1건당 총비용을 측정합니다.

원문: OpenAI — API Changelog, September 22


3. Vercel Sandbox Drives, 일회성 실행에 영속 상태를 붙이다

PROOF LEVEL 03 · 공식 확인

사실 요약

Vercel은 2026년 9월 22일 Sandbox Drives를 공개 베타로 출시했습니다. Drive는 특정 샌드박스에 종속되지 않는 영속 저장소입니다. 샌드박스가 종료돼도 Drive에 저장한 데이터는 남아 있으며, 이후 새로운 샌드박스에서 같은 Drive를 다시 마운트할 수 있습니다.

이를 통해 여러 실행에 걸쳐 같은 작업공간, 온디스크 메모리, 데이터셋, 모델, 의존성 트리를 재사용할 수 있습니다. 하나의 Drive에는 동시에 하나의 읽기·쓰기 마운트만 허용되며, 한 번 기록된 Drive는 특정 시점의 읽기 전용 스냅샷 형태로 여러 샌드박스에 병렬 마운트할 수 있습니다.

기본 최대 용량은 Pro와 Enterprise 기준 1TiB이며 최대 16TiB까지 구성할 수 있습니다. Hobby는 기본 제한이 더 작습니다.

개발자에게 중요한 점

에이전트에게 샌드박스를 매번 새로 제공하면 안전한 격리는 얻지만 비용이 생깁니다. 저장소를 다시 체크아웃하고, 의존성을 다시 설치하고, 인덱스를 생성하고, 직전 실행의 중간 결과를 복구해야 합니다.

영속 Drive는 격리된 실행 환경과 지속되는 작업 상태를 분리할 수 있게 합니다. 오늘 실행된 에이전트의 샌드박스는 사라져도 작업공간은 남겨두고, 내일 새로운 샌드박스에서 이어갈 수 있습니다. 장시간 코딩 에이전트나 연구 에이전트에게 특히 의미가 큽니다.

하지만 상태가 남는 순간 반대쪽 문제도 생깁니다. 오래된 의존성, 오염된 작업공간, 에이전트가 잘못 기록한 메모리와 중간 결과도 함께 남습니다. 영속 상태는 작업을 빠르게 이어가게 하지만 실수 역시 오래 보존하기 때문에 스냅샷 버전, 신뢰 가능한 상태의 기준, 초기화와 폐기 정책이 함께 필요합니다.

Lee's Take

지난 호가 장기 실행 에이전트의 등장에 관한 이야기였다면, 이번 발표는 그 에이전트가 어디에 작업대를 남겨둘 것인가에 관한 답에 가깝습니다.

모델이 기억하는 컨텍스트와 실제 작업 상태는 같은 것이 아닙니다. 코드베이스, 생성한 파일, 설치한 의존성, 인덱스, 중간 결과처럼 수십 GB까지 커질 수 있는 상태를 모델 컨텍스트에 넣을 수는 없습니다. 결국 오래 일하는 에이전트에는 모델 바깥의 지속 가능한 작업 공간이 필요합니다.

하지만 영속 저장소는 에이전트를 강하게 만드는 만큼 실수도 오래 보존합니다. “기억한다”는 기능보다 어떤 상태를 신뢰하고 언제 버릴 것인가를 결정하는 운영 규칙이 먼저입니다.

이번 주 해볼 일

  • 재사용할 상태와 실행마다 새로 만들어야 할 상태를 구분합니다.
  • 에이전트 작업 시작 시 사용할 Drive 스냅샷 ID와 코드 리비전을 기록합니다.
  • 민감정보가 Drive에 평문으로 남지 않는지 점검합니다.
  • 실패한 실행의 상태를 다음 작업이 자동 상속하지 않도록 폐기 기준을 만듭니다.
  • 장기 작업에서 모델 메모리와 실제 작업공간 상태를 별도로 관리합니다.

원문: Vercel — Drives for Vercel Sandbox are now in public beta


이번 호의 결론

Meta Muse는 개인 에이전트 경쟁의 기준을 모델 성능에서 제품 구조로 옮기려는 시도입니다. 전용 실행 환경, 장기 상태, 권한 제어, 사람의 승인, 감사 기록, 결제를 하나의 흐름으로 연결했기 때문입니다.

그리고 출시 이후 벌어진 일들은 이 구조가 단순한 기능 목록이 아니라는 것을 보여주고 있습니다. 에이전트가 실제 웹서비스를 사용하면 외부 사업자의 정책과 충돌할 수 있고, 사용자 데이터를 읽으면 단순한 접근 권한을 넘어 “왜 이 정보를 알고 있는가”를 설명해야 합니다. 컴퓨터와 직접 연결되면 모델뿐 아니라 실행 환경 자체가 새로운 보안 경계가 됩니다.

OpenAI의 GPT-6 Sol과 Luna는 그 안에서 어떤 판단에 얼마의 비용을 쓸 것인가라는 문제를 보여줍니다. Vercel Sandbox Drives는 에이전트가 여러 실행에 걸쳐 어디에 작업 상태를 남길 것인가라는 문제에 답합니다.

세 발표를 연결하면 한 가지 방향이 보입니다. 앞으로 강한 에이전트는 가장 좋은 모델 하나를 붙여 만든 제품이 아닐 가능성이 큽니다. 적절한 비용의 모델을 골라 쓰고, 안전한 실행 환경에서 행동하고, 필요한 상태는 오래 보존하되, 중요한 행동에서는 사람에게 다시 결정권을 돌려주고, 무엇을 했는지 나중에 확인할 수 있어야 합니다.

모델은 점점 교체 가능한 구성요소가 되고 있습니다.

그 모델을 어디에서 실행하고, 무엇을 기억하게 하고, 어디까지 행동하게 하며, 언제 사람에게 멈춰 서게 할 것인가. 게임 체인저는 점점 그 구조 쪽으로 이동하고 있습니다.


그런데 왜 Claude Opus 5.5는 이번 호에서 다루지 않았을까?

Anthropic도 9월 22일 Claude Opus 5.5를 발표했습니다. Anthropic에 따르면 Opus 5.5는 Opus 5보다 성능을 높이면서 실행 비용을 40% 낮췄고, 대부분의 작업에서 Claude Fable 5.1 수준의 성능을 제공하는 것을 목표로 합니다. 충분히 이번 주 주요 뉴스로 다룰 만한 발표입니다.

그럼에도 이번 호에서는 의도적으로 제외했습니다. 이번 호에서 GPT-6 Sol과 Luna를 다룬 이유도 단순히 “새 모델이 나왔기 때문”이 아닙니다. 20배의 가격 차이를 같은 모델 계열 안에 배치하면서 에이전트가 작업에 따라 서로 다른 비용의 모델을 라우팅하는 구조를 이야기하기에 적합했기 때문입니다.

Opus 5.5의 비용 효율 개선 역시 중요한 흐름이지만 함께 넣으면 이번 호가 다시 여러 최신 모델의 성능과 가격을 비교하는 이야기로 확장될 가능성이 있었습니다. 이번 호에서 보고 싶었던 것은 모델 경쟁 자체보다 그 모델들이 들어갈 개인 에이전트의 전체 시스템입니다.

그래서 Opus 5.5는 중요하지 않아서 제외한 것이 아니라, 이번 호의 질문을 하나로 유지하기 위해 다음 이야기로 남겼습니다.

에이전트가 오래 일할수록, 사람의 승인선이 제품이 된다

이번 주의 공통점은 ‘더 강한 모델’보다 더 오래, 더 조직적으로 일하는 에이전트입니다.

OpenAI와 Cursor는 장시간 실행과 다중 에이전트 협업을 제품의 중심으로 끌어올렸고, GitHub도 코드 리뷰를 단순한 코멘트 생성에서 검증과 재검토가 이어지는 흐름으로 확장하고 있습니다.

에이전트가 한 번의 질문에 답하는 도구를 넘어 실제 업무를 계속 수행하기 시작하면 개발자가 설계해야 할 것도 달라집니다.

중요한 것은 프롬프트만이 아닙니다.

무엇을 할 수 있는가.

어디까지 스스로 진행할 수 있는가.

언제 사람에게 돌아와야 하는가.

실패하면 어디서 멈추는가.

그리고 이번 주 직접 Jev를 사용해보며 한 가지를 더 확인했습니다.

모든 자동화를 장기 실행 에이전트나 범용 LLM으로 만들 필요도 없습니다.

 

1. OpenAI Agents API 공개 베타

PROOF LEVEL 03 · 공식 확인

OpenAI는 9월 10일 Codex를 구동하는 관리형 에이전트 하네스를 개발자가 사용할 수 있도록 Agents API를 공개 베타로 발표했습니다.

긴 세션과 컨텍스트 관리, 도구 실행, 서브에이전트 조율, 파일과 코드를 다루는 실행 환경 등을 OpenAI가 관리합니다.

개발자에게 중요한 점

에이전트 실행기를 직접 조립해야 하는 비용은 줄어듭니다.

반대로 에이전트가 더 오래, 더 많은 일을 할 수 있게 될수록 어떤 행동을 허용하고 어디에서 사람에게 승인을 받을 것인지가 중요해집니다.

Lee's Take

에이전트 제품의 차이는 점점

“무엇을 자동화할 수 있는가?” 보다

“어디까지 자동화하고, 어디에서 사람이 책임지는가?”

에서 생길 것 같습니다.

L-Proof-AI 역시 자료 수집과 초안 생성까지는 AI가 진행하더라도 최종 발송은 사람이 승인해야 한다는 선을 제품 규칙으로 두는 편이 맞다고 봅니다.

이번 주 해볼 일

긴 작업 하나를 골라 자동 실행 구간 / 사람 승인 구간으로 나눠보기

외부 쓰기, 결제, 고객 발송처럼 되돌리기 어려운 행동은 별도 승인 단계로 분리하기

실패했을 때 재시도할지, 중단할지, 사람에게 돌려보낼지 미리 정하기

원문: https://openai.com/index/introducing-the-agents-api/

 

2. GitHub Copilot 코드 리뷰가 ‘댓글’에서 ‘검증 루프’로 이동

PROOF LEVEL 03 · 공식 확인

GitHub는 9월 11일 Copilot code review의 업데이트를 발표했습니다.

새로운 커밋이 기존 리뷰 의견을 해결했는지 다시 확인해 처리된 코멘트를 자동으로 정리하고, Copilot의 수정 제안을 적용할 때 변경 내용에 맞는 커밋 메시지를 생성합니다.

리뷰 과정에서 사용할 수 있는 셸 도구도 확대됐습니다. 빌드, 테스트, 스크립트 실행 등을 이용해 리뷰 결과를 검증할 수 있으며 Lite 리뷰에는 여러 에이전트의 분석을 합치는 방식도 적용됐습니다.

개발자에게 중요한 점

AI 리뷰의 병목이 단순히

“버그를 몇 개 찾아냈는가”

에서

“그 지적이 실제 수정으로 이어졌고, 수정됐다는 것을 다시 검증할 수 있는가”

로 이동하고 있습니다.

Lee's Take

AI 리뷰의 성공 지표를 ‘코멘트 수’로 잡으면 오히려 소음이 늘어날 수 있습니다.

제가 보고 싶은 것은 이런 숫자입니다.

AI가 지적한 문제 중 실제 반영된 비율

같은 문제가 다시 발생한 비율

AI 수정 이후 사람이 다시 수정한 비율

수정 후 테스트에서 실제로 문제가 해결됐는지

AI가 리뷰어 역할까지 가져간다면 발견보다 검증 루프가 중요합니다.

이번 주 해볼 일

AI 리뷰 코멘트 가운데 실제 반영된 비율을 기록해보기

자동 수정 후 diff와 테스트 결과를 함께 확인할 수 있는 흐름 만들기

원문: https://github.blog/changelog/2026-09-11-auto-resolution-and-analysis-updates-in-copilot-code-review/

 

3. Cursor Projects: 에이전트의 단위가 ‘채팅’에서 ‘프로젝트’로

PROOF LEVEL 03 · 공식 확인

Cursor는 9월 10일 Projects를 발표했습니다.

Projects는 기능 개발이나 마이그레이션처럼 오래 걸리는 작업의 컨텍스트를 유지하고, 코디네이터 에이전트가 작업을 여러 서브에이전트에게 나눠 맡길 수 있게 합니다.

프로젝트는 클라우드 환경에서 계속 실행될 수 있으며, 일정이나 Slack, PR 같은 신호에 따라 반복 작업을 수행하는 구조도 제공합니다.

개발자에게 중요한 점

에이전트의 단위가

한 번의 채팅 → 계속 운영되는 프로젝트

로 변하고 있습니다.

그 순간부터 AI가 기억하는 정보와 현재 작업 상태도 일종의 애플리케이션 상태가 됩니다.

누가 무엇을 맡았는지, 어떤 결과를 신뢰할 수 있는지, 실패했을 때 다음 작업을 계속할 것인지 같은 것을 코드와 비슷한 수준으로 관리해야 합니다.

Lee's Take

장기 에이전트가 가치 있는 이유를 단순히

“기억을 오래 한다”

라고 생각하지 않습니다.

오히려 중요한 것은 사람이 며칠 뒤 다시 들어왔을 때

왜 지금 이런 상태가 되었는지 설명할 수 있는가

입니다.

긴 기억보다 추적 가능한 상태가 더 중요할 수 있습니다.

이번 주 해볼 일

반복 업무 하나를 골라 다음 네 가지를 문서화해보기.

입력

결과물

승인자

실패 시 동작

그리고 승인되지 않은 결과는 다음 단계로 넘어가지 않는 것을 기본값으로 두기.

원문: https://cursor.com/changelog

 

4. 직접 써본 Jev: 모든 판단에 LLM이 필요한 것은 아니었다

PROOF LEVEL 03 · 공식 확인 + 직접 테스트

TypeSafe AI는 9월 15일 첫 번째 System One Model인 Jev를 공개했습니다.

일반적인 LLM처럼 자유로운 문장을 생성하는 대신, 프로그램이 미리 정의한 선택지에 대해 typed decision과 확률·confidence를 반환하는 모델입니다.

처음에는 저도 “이걸 굳이 LLM 대신 왜 사용하지?”라는 생각이 있었습니다.

직접 작은 프로젝트에 적용해보니 답은 꽤 명확했습니다.

Fit한 상황에서는 굉장히 강하다

제가 테스트한 HowMuchCaloriesLeft라는 칼로리 기록 프로젝트를 예로 들겠습니다.

사용자가 이렇게 말할 수 있습니다.

“아까 밥은 반 정도 남겼어.”

애플리케이션이 알아야 하는 것은 멋진 답변이 아닙니다.

먼저 이 입력이

새로운 음식 추가인지

기존 기록 수정인지

기록 삭제인지

단순 질문인지

판단해야 합니다.

그리고 기존 기록 수정이라면 어떤 기록을 수정해야 하는지, 확신이 낮다면 사용자에게 다시 물어볼지도 결정해야 합니다.

이런 분류·라우팅·점수화·분기에서는 Jev의 구조가 상당히 잘 맞았습니다.

결과가 자연어가 아니라 처음부터 프로그램이 사용할 수 있는 선택지와 확률로 돌아오기 때문입니다.

그런데 Fit하지 않은 곳까지 쓰면 오히려 불편하다

반대로 이런 작업은 일반 LLM이 훨씬 자연스러웠습니다.

음식 이름과 양을 자연어에서 해석하기

사용자의 애매한 표현을 이해하기

자유로운 대화 생성하기

후보를 새로 만들어내기

음식 정보를 바탕으로 칼로리를 추정하기

Jev는 범용 LLM의 작은 버전도, Codex나 Claude Code 같은 개발 에이전트도 아니었습니다.

정해진 선택지가 존재하고 프로그램이 그 판단을 바로 사용해야 하는 구간에서 잘 맞는 별도의 도구에 가까웠습니다.

Lee's Take

이번 테스트에서 가장 흥미로웠던 점은

AI가 더 강해졌다고 모든 문제를 하나의 강한 LLM에게 맡기는 것이 최선은 아니라는 것

이었습니다.

문장을 만들어야 하는 곳에는 LLM을 쓰고,

복잡한 일을 오래 수행해야 하는 곳에는 에이전트를 쓰고,

정해진 상태 안에서 빠르게 판단해야 하는 곳에는 Jev 같은 decision model을 쓸 수 있습니다.

결국 중요한 것은 어떤 모델이 가장 강한지가 아니라

“이 판단에는 어느 정도의 자유가 필요한가?”

인 것 같습니다.

자유도가 높아질수록 할 수 있는 일은 많아집니다.

동시에 검증해야 할 것도 늘어납니다.

이번 주 해볼 일

현재 LLM으로 처리하고 있는 작업 하나를 골라 질문해보기.

“여기서 정말 문장을 생성해야 하는가?”

결과가 결국 A/B/C, true/false, 점수, 라우팅처럼 몇 개의 선택지로 귀결된다면 범용 LLM 호출이 과한 구조일 수도 있습니다.

반대로 새로운 후보를 만들거나 맥락을 깊게 해석해야 한다면 억지로 판단 모델에 넣지 말고 LLM에게 맡기는 편이 낫습니다.

이번 호의 결론

이번 주 발표들을 보면 AI 개발은 점점 모델을 호출하는 것에서 AI가 실제 업무를 수행하도록 만드는 것으로 이동하고 있습니다.

그런데 자동화가 커질수록 사람이 사라지는 것은 아닙니다.

오히려 사람이 개입해야 할 지점을 더 명확하게 설계해야 합니다.

그리고 Jev를 직접 써보면서 여기에 하나를 더 추가하고 싶어졌습니다.

모든 AI에게 최대한 많은 자유를 줄 필요도 없습니다.

생성해야 할 때는 생성하게 하고,

판단해야 할 때는 판단하게 하고,

사람이 책임져야 할 순간에는 멈추게 하는 것.

결국 AI 제품의 설계는 점점

모델 선택보다 권한 설계에 가까워지고 있습니다.


L-Proof-AI · 문의: this_is_laugh@naver.com

 문제 상황


최근 React의 새로운 기능인 `useEffectEvent`를 알게 되었고, 실제 서비스에 적용해보려고 했다.

공식 문서 기준으로 보면:

* React 19.2 이상 → 안정적으로 사용 가능
* React 19.0 ~ 19.1 → 실험적 (동작할 수도, 안 할 수도 있음)
* 그 이하 → 미지원

우리 프로젝트는 **React 19.2.3**을 사용 중이었기 때문에
자연스럽게 “문제없이 사용할 수 있겠지”라고 생각했다.

 이번 테스트 환경

이번에 확인한 환경은 다음과 같았다.

* next: 15.5.9
* react: 19.2.3
* react-dom: 19.2.3
* 실행: `next dev --turbopack`

👉 즉, package 레벨만 보면 `useEffectEvent`를 써도 이상하지 않은 조합이었다.


 왜 useEffectEvent를 도입하려고 했는가

내가 이 기능에 관심을 가지게 된 이유는 `/consult` 페이지의 채팅 구조 때문이었다.

이 페이지에서는:

* 담당자 ↔ 고객 간 채팅
* 브라우저 알림
* sidebar / menu에 unread 표시 (green dot)

같은 기능이 동시에 동작한다.

그래서 채팅 로직을 페이지 단이 아니라 **Layout 레벨로 끌어올려 `ChatProvider`로 구성**했고:

* 채팅방 접속 상태
* 구독 변경
* 딥링크 진입

등은 `/consult` 페이지에서 제어하도록 설계했다.

 

 문제의 본질


문제는 여기서 시작됐다.

이 서비스는:

* 사용자 권한에 따라 UI가 달라지고
* 전역 Provider가 많고
* 상태 흐름이 꽤 복잡하다

그래서 현재는:

* “처음 진입 여부”
* “로딩 상태”

같은 내부 변수를 두고
👉 **소켓 연결 타이밍을 제어하는 구조**를 쓰고 있었다.

 

 기존 코드의 한계

기존 구조에서는 이런 문제가 있었다:

* pathname 변경 → 소켓 effect 재실행
* 알림 설정 변경 → 재연결 발생 가능
* stale closure 방지를 위해 ref 동기화 필요
* (`showBannerRef`, `activeIdRef` 등)

👉 결과적으로 코드가 점점 복잡해지고 있었다

 그래서 useEffectEvent를 도입하려고 했다


의도는 명확했다:

> “연결의 생명주기(effect deps)와
> 최신 상태 읽기(callback)를 분리하자”

즉:

* 소켓 연결은 최소한의 deps로 유지하고
* 이벤트 핸들러는 최신 state를 읽게 하자

 그런데


바로 터진 에러

useEffectEvent is not a function



런타임에서 바로 터졌다.

“React 버전은 맞는데 왜 안 되지?”

처음에는 단순히 의아했다.

그래서 확인을 해봤다:

npm list react react-dom

→ 둘 다 19.2.3

 

node -e "require('react').useEffectEvent"

→ function

여기까지는 정상


---

 그런데 진짜 문제는

Next.js 환경에서 실제 클라이언트 런타임은
단순히 `react` 패키지를 쓰지 않는다.

👉 내부적으로:

next/dist/compiled/react


를 사용한다.

그리고 여기서 확인해보면:

node -e "require('next/dist/compiled/react').useEffectEvent"


👉 결과: `undefined`


 핵심 원인



👉 **내가 설치한 React와
실제 런타임에서 사용되는 React가 다르다**

즉:

> “package.json에서 React 버전이 맞다”
> = “해당 API를 런타임에서 쓸 수 있다”

가 아니었다.

그래서 내린 결론은

이 상태에서 계속 진행하면:

👉 기능 도입이 아니라 장애 생성

그래서:

-  useEffectEvent 적용 시도 → 전부 revert
-  기존 패턴 유지 (ref 동기화 + effect 분리)

로 방향을 잡았다.


 같은 문제를 만났을 때 체크리스트


혹시 나와 같은 에러를 만났다면 아래를 먼저 확인해보자.

# 1. 설치 버전 확인
npm list react react-dom

# 2. 앱 기준 React export 확인
node -e "console.log(require('react').version, typeof require('react').useEffectEvent)"

# 3. Next 런타임 React export 확인
node -e "console.log(require('next/dist/compiled/react').version, typeof require('next/dist/compiled/react').useEffectEvent)"



👉 이 결과가 다르다면
package.json만 보고 기능 도입을 판단하면 안 된다.

 이번에 배운 것


1. 문서만 믿으면 안 된다

버전 조건만 보고 도입하면 안 된다.
👉 실제 런타임 export를 확인해야 한다



2. Next.js 환경은 다르게 동작한다

Next는 내부적으로 compiled dependency를 사용한다.

👉 내가 설치한 패키지 ≠ 실제 실행 패키지



3. 기능 도입보다 중요한 것

👉 되돌릴 수 있는 단위로 실험해야 한다

이번 케이스도:

* 작은 범위에서 시도했기 때문에
* 빠르게 원복 가능했다

4. ai 끼리 업무를 보게 했을 때 인간이 필요한 이유

👉 명세서끼리 읽게 시켰을 때 서로 오류가 없어서 진행하게 했지만 실제로 그것을 확인하고 검증하는 내 역할이 없었다면 잘못된 파일이 서버에 올라갈 뻔했다. 작은 단위로 계속 모니터링 하는 습관이 사고를 예방했다.



 오해 방지를 위해



여기서 중요한 건
👉 “Next.js에서는 useEffectEvent를 못 쓴다”가 아니다

정확히는:

> 내가 테스트한 버전 조합 / 런타임에서는 사용할 수 없었다

즉, 기능 자체의 문제가 아니라
👉 런타임과 패키지 간의 불일치 문제에 가깝다.


 마치며



`useEffectEvent`는 분명 좋은 기능이다.

특히:

* socket
* polling
* subscription

같은 케이스에서는 코드 복잡도를 크게 줄일 수 있다.

이번 포스팅은 JS 엔진의 작동 원리에 대한 시리즈 중 두 번째 글입니다.

  1. JS엔진은 우리가 작성한 코드를 어떻게 처리하나? (이전 포스팅)
  2. 스코프는 무엇인가? (현재 포스팅)
  3. 스코프의 작동 방식과 쓰임새 (예정)
  4. 스코프를 다룰 때 주의할 점 (예정)

지난 포스팅에서 자바스크립트 코드가 어떻게 컴파일되고 실행되는지 살펴봤습니다. 이번에는 그 과정에서 핵심적인 역할을 하는 '스코프'에 대해 깊이 있게 알아보겠습니다.

 

왜 스코프를 이해해야 하는가?

스코프는 단순히 알면 좋은 개념이 아닌, 자바스크립트 프로그래밍의 근간이 되는 필수 개념입니다. 스코프를 제대로 이해하지 못하면 다음과 같은 문제들이 발생합니다:

  1. 예상치 못한 버그 발생: 변수가 어디서 접근 가능한지 모르면 의도치 않은 동작이 발생합니다.
  2. 메모리 누수: 불필요하게 긴 변수 생존 기간으로 인해 메모리가 낭비됩니다.
  3. 코드 유지보수 어려움: 변수의 범위를 명확히 알지 못하면 코드 수정 시 부작용이 생깁니다.
  4. 디버깅 복잡성 증가: 어디서 변수가 변경되었는지 추적하기 어려워집니다.

실제로 많은 자바스크립트 초보자들이 겪는 혼란의 상당 부분은 스코프에 대한 이해 부족에서 비롯됩니다. "왜 이 변수가 여기서는 접근 가능하고 저기서는 안 되지?", "왜 이 함수는 외부 변수를 볼 수 있는데 저 함수는 못 보지?" 같은 질문들의 답이 모두 스코프에 있습니다.

 

스코프의 정의와 중요성

스코프(Scope)는 프로그래밍 언어에서 변수와 함수의 접근성과 생존 기간을 결정하는 규칙의 집합입니다. 쉽게 말해 "이 변수는 어디서부터 어디까지 유효한가?"를 정의하는 경계입니다.

자바스크립트에서 스코프의 중요성:

  • 변수 충돌 방지: 동일한 이름의 변수를 서로 다른 스코프에 안전하게 사용할 수 있습니다.
  • 정보 은닉과 캡슐화: 특정 데이터와 기능을 외부로부터 보호할 수 있습니다.
  • 메모리 효율성: 변수가 필요한 범위에서만 살아있도록 하여 메모리를 효율적으로 사용합니다.
  • 코드 가독성: 변수의 사용 범위가 명확해져 코드 이해가 쉬워집니다.

 

자바스크립트의 스코프 종류

자바스크립트에는 크게 세 가지 유형의 스코프가 있습니다:

1. 전역 스코프(Global Scope)

전역 스코프는 코드의 가장 바깥쪽 층으로, 어디서든 접근할 수 있는 변수와 함수가 위치합니다.

 
javascript
// 전역 스코프에 선언된 변수와 함수
var globalVar = "전역 변수입니다";
let globalLet = "전역 let 변수입니다";
const globalConst = "전역 const 상수입니다";

function globalFunction() {
  console.log("전역 함수입니다");
}

// 다른 함수나 블록 내에서도 접근 가능
function testAccess() {
  console.log(globalVar);     // "전역 변수입니다"
  console.log(globalLet);     // "전역 let 변수입니다"
  console.log(globalConst);   // "전역 const 상수입니다"
  globalFunction();           // "전역 함수입니다"
}

testAccess();

전역 스코프에 대한 추가 정보:

  • 브라우저 환경: 전역 객체는 window입니다. 전역 변수는 window의 속성이 됩니다.
  • Node.js 환경: 전역 객체는 global입니다.
  • 모듈 시스템: ES 모듈에서는 최상위 레벨 변수도 모듈 스코프에 속하며 자동으로 전역이 되지 않습니다.
  • 전역 오염: 전역 스코프에 너무 많은 변수를 선언하면 이름 충돌 위험이 높아지고, 메모리 사용량이 증가합니다.
 
javascript
// 브라우저 환경에서 전역 변수와 window 객체의 관계
var x = 10;
console.log(window.x);  // 10

// let과 const로 선언한 전역 변수는 window 객체의 속성이 되지 않음
let y = 20;
console.log(window.y);  // undefined

2. 함수 스코프(Function Scope)

함수 스코프는 함수 내부에 선언된 변수와 함수가 속하는 영역입니다. var 키워드로 선언된 변수는 함수 스코프를 가집니다.

 
javascript
function exampleFunction() {
  var functionScopedVar = "함수 스코프 변수";
  let blockScopedLet = "블록 스코프지만 함수 내부에 있음";
  
  console.log(functionScopedVar);  // "함수 스코프 변수"
  console.log(blockScopedLet);     // "블록 스코프지만 함수 내부에 있음"
  
  function innerFunction() {
    console.log("내부 함수");
    // 외부 함수의 변수에 접근 가능
    console.log(functionScopedVar);  // "함수 스코프 변수"
  }
  
  innerFunction();  // "내부 함수", "함수 스코프 변수"
  
  if (true) {
    var sameVarAgain = "var는 함수 스코프라 if 블록 외부에서도 접근 가능";
  }
  
  console.log(sameVarAgain);  // "var는 함수 스코프라 if 블록 외부에서도 접근 가능"
}

exampleFunction();
// console.log(functionScopedVar);  // ReferenceError
// innerFunction();                 // ReferenceError

함수 스코프의 중요한 특징:

  • 함수 내부에서 선언된 var 변수는 함수 전체에서 접근 가능합니다.
  • 함수 내부의 블록(if, for 등)에서 선언된 var 변수도 함수 전체에서 접근 가능합니다.
  • 함수 스코프는 함수가 호출될 때마다 새로 생성됩니다.

 

3. 블록 스코프(Block Scope)

ES6에서 도입된 let과 const 키워드로 선언된 변수는 블록 스코프를 가집니다. 블록은 중괄호({})로 묶인 코드 영역입니다.

 
javascript
function blockScopeExample() {
  // 함수 레벨의 변수
  var functionVar = "함수 레벨 var";
  let functionLet = "함수 레벨 let";
  
  if (true) {
    // 블록 레벨의 변수
    var blockVar = "var는 함수 스코프";
    let blockLet = "let은 블록 스코프";
    const blockConst = "const도 블록 스코프";
    
    console.log(functionVar);  // "함수 레벨 var"
    console.log(functionLet);  // "함수 레벨 let"
    console.log(blockVar);     // "var는 함수 스코프"
    console.log(blockLet);     // "let은 블록 스코프"
    console.log(blockConst);   // "const도 블록 스코프"
  }
  
  console.log(functionVar);  // "함수 레벨 var"
  console.log(functionLet);  // "함수 레벨 let"
  console.log(blockVar);     // "var는 함수 스코프"
  // console.log(blockLet);  // ReferenceError: blockLet is not defined
  // console.log(blockConst); // ReferenceError: blockConst is not defined
  
  for (let i = 0; i < 3; i++) {
    // 'i'는 이 for 루프 블록에서만 유효
  }
  // console.log(i);  // ReferenceError: i is not defined
  
  for (var j = 0; j < 3; j++) {
    // 'j'는 함수 전체에서 유효
  }
  console.log(j);  // 3
}

blockScopeExample();

블록 스코프의 중요한 특징:

  • let과 const로 선언된 변수는 해당 블록 내에서만 접근 가능합니다.
  • 블록 스코프는 더 정확한 변수 생명주기 제어를 가능하게 합니다.
  • 블록 스코프는 코드 블록이 끝나면 메모리에서 변수가 제거될 수 있어 메모리 효율이 높습니다.

스코프 체인(Scope Chain)

스코프 체인은 중첩된 스코프 간의 연결을 의미합니다. 자바스크립트 엔진은 변수를 찾을 때 현재 스코프에서 시작하여 변수를 찾지 못하면 바깥쪽 스코프로 순차적으로 검색합니다.

 
javascript
var globalVar = "전역 변수";

function outerFunction() {
  var outerVar = "외부 함수 변수";
  
  function innerFunction() {
    var innerVar = "내부 함수 변수";
    
    console.log(innerVar);   // "내부 함수 변수" (현재 스코프)
    console.log(outerVar);   // "외부 함수 변수" (바깥 스코프)
    console.log(globalVar);  // "전역 변수" (전역 스코프)
  }
  
  innerFunction();
  console.log(innerVar);  // ReferenceError (내부 함수의 변수에 접근 불가)
}

outerFunction();

이 코드에서 스코프 체인은 다음과 같이 구성됩니다:

  • innerFunction 스코프 → outerFunction 스코프 → 전역 스코프

스코프 체인의 동작 방식:

  1. 현재 스코프에서 변수를 찾습니다.
  2. 찾지 못하면 바로 바깥쪽 스코프로 이동합니다.
  3. 계속해서 바깥쪽으로 이동하며 변수를 찾습니다.
  4. 전역 스코프까지 검색했는데도 변수를 찾지 못하면 ReferenceError가 발생합니다.

스코프 체인과 관련된 중요 개념

변수 섀도잉(Variable Shadowing)

안쪽 스코프에서 바깥쪽 스코프의 변수와 같은 이름의 변수를 선언하면, 안쪽 변수가 바깥쪽 변수를 '가리는' 현상입니다.

 
javascript
var name = "전역 이름";

function printName() {
  var name = "함수 이름";  // 전역 변수 'name'을 가림
  console.log(name);       // "함수 이름"
  
  if (true) {
    let name = "블록 이름";  // 함수 변수 'name'을 가림
    console.log(name);      // "블록 이름"
  }
  
  console.log(name);  // "함수 이름"
}

printName();
console.log(name);    // "전역 이름"

전역 언섀도잉(Global Unshadowing)

window 객체(브라우저 환경)를 통해 가려진 전역 변수에 직접 접근할 수 있습니다.

 
javascript
var count = 10;

function updateCount() {
  var count = 100;           // 전역 변수 'count'를 가림
  console.log(count);        // 100
  console.log(window.count); // 10 (전역 변수 직접 접근)
  
  // 전역 변수 수정
  window.count = 20;
}

updateCount();
console.log(count);  // 20 (전역 변수가 수정됨)

스코프와 호이스팅(Hoisting)

호이스팅은 자바스크립트 엔진이 코드를 실행하기 전에 변수와 함수 선언을 메모리에 저장하는 동작을 말합니다. 이로 인해 선언 전에도 변수와 함수에 접근할 수 있는 것처럼 보입니다.

변수 호이스팅

 
javascript
console.log(varVariable);  // undefined
var varVariable = "var 변수";

// console.log(letVariable);  // ReferenceError: letVariable is not defined
let letVariable = "let 변수";

// console.log(constVariable);  // ReferenceError: constVariable is not defined
const constVariable = "const 변수";

var로 선언한 변수는 선언 전에 접근해도 undefined가 출력되지만, let과 const로 선언한 변수는 선언 전에 접근하면 에러가 발생합니다. 이는 let과 const가 TDZ(Temporal Dead Zone)에 의해 초기화 전 접근이 제한되기 때문입니다.

함수 호이스팅

 
javascript
// 함수 선언문: 호이스팅됨
sayHello();  // "안녕하세요!"
function sayHello() {
  console.log("안녕하세요!");
}

// 함수 표현식: 변수만 호이스팅됨
// sayHi();  // TypeError: sayHi is not a function
var sayHi = function() {
  console.log("안녕하세요!");
};

// 화살표 함수: 변수만 호이스팅됨
// sayHola();  // ReferenceError: Cannot access 'sayHola' before initialization
let sayHola = () => {
  console.log("Hola!");
};

함수 선언문은 완전히 호이스팅되어 선언 전에도 호출 가능하지만, 함수 표현식과 화살표 함수는 변수 호이스팅 규칙을 따릅니다.

ES 모듈과 스코프

ES6부터 도입된 모듈 시스템은 스코프에 중요한 변화를 가져왔습니다. 모듈의 최상위에 선언된 변수와 함수는 전역 스코프가 아닌 모듈 스코프에 속합니다.

 
javascript
// module.js
var studentName = "카일";

function hello() {
  console.log(`${studentName} 님, 안녕하세요!`);
}

hello();
// 카일 님, 안녕하세요!

export hello;
 
javascript
// main.js
import { hello } from './module.js';

// module.js의 studentName에는 접근할 수 없음
// console.log(studentName);  // ReferenceError

hello();  // "카일 님, 안녕하세요!"

모듈에서 studentName과 hello 함수는 모듈 범위(module-wide) 스코프의 변수가 되며, 전역 변수로 등록되지 않습니다.

예제 1: 루프 내 비동기 함수와 스코프

 
javascript
function createButtons() {
  // 버튼 5개 생성하기
  for (var i = 0; i < 5; i++) {
    var button = document.createElement("button");
    button.innerText = "버튼 " + i;
    
    // 클릭 이벤트 핸들러 추가
    button.addEventListener("click", function() {
      console.log("버튼 " + i + "가 클릭됨");
    });
    
    document.body.appendChild(button);
  }
}

createButtons();

 

예상 결과: 각 버튼을 클릭하면 "버튼 0가 클릭됨", "버튼 1가 클릭됨" 등이 출력될 것 같습니다.

실제 결과: 모든 버튼이 "버튼 5가 클릭됨"을 출력합니다!

원인 분석

이 문제는 스코프와 클로저의 이해 부족에서 발생합니다:

  1. var i는 함수 스코프를 가집니다 (for 블록 내부가 아님).
  2. 이벤트 리스너 함수는 버튼 클릭 시 나중에 실행됩니다.
  3. 그 시점에 for 루프는 이미 종료되었고 i의 값은 5가 되었습니다.
  4. 모든 이벤트 리스너는 같은 i 변수를 참조하고 있습니다.

해결 방법 1: 블록 스코프 변수 사용하기

 
javascript
function createButtons() {
  // let으로 변경하여 블록 스코프 사용
  for (let i = 0; i < 5; i++) {
    const button = document.createElement("button");
    button.innerText = "버튼 " + i;
    
    button.addEventListener("click", function() {
      console.log("버튼 " + i + "가 클릭됨");
    });
    
    document.body.appendChild(button);
  }
}

createButtons();

이제 각 루프 반복마다 새로운 i 변수가 생성되므로, 각 이벤트 리스너는 자신의 고유한 i 값을 참조합니다.

해결 방법 2: 즉시 실행 함수로 스코프 생성하기

 
javascript
function createButtons() {
  for (var i = 0; i < 5; i++) {
    // 즉시 실행 함수를 사용하여 새로운 스코프 생성
    (function(index) {
      var button = document.createElement("button");
      button.innerText = "버튼 " + index;
      
      button.addEventListener("click", function() {
        console.log("버튼 " + index + "가 클릭됨");
      });
      
      document.body.appendChild(button);
    })(i);
  }
}

createButtons();

즉시 실행 함수는 각 반복마다 새로운 스코프를 생성하고, 현재 i 값을 매개변수 index로 복사합니다. 이렇게 하면 각 이벤트 리스너는 자신의 스코프에 있는 index 값을 참조합니다.

이 시뮬레이션은 스코프의 중요성과 특히 비동기 코드에서 발생할 수 있는 문제를 보여줍니다. var와 let의 스코프 차이를 이해하고, 클로저가 어떻게 외부 스코프의 변수를 "기억"하는지 이해하면 이러한 문제를 쉽게 해결할 수 있습니다.

실제 개발에서의 스코프 활용

1. 전역 스코프 오염 방지

전역 스코프에 너무 많은 변수를 선언하면 이름 충돌과 의도치 않은 부작용이 발생할 수 있습니다. 이를 방지하는 방법:

 
javascript
// 즉시 실행 함수 표현식(IIFE)으로 스코프 생성
(function() {
  var privateVar = "비공개 변수";
  
  function privateFunction() {
    console.log("비공개 함수");
  }
  
  // 필요한 경우만 전역으로 노출
  window.myApp = window.myApp || {};
  window.myApp.publicAPI = {
    doSomething: function() {
      privateFunction();
      return "작업 완료!";
    }
  };
})();

// privateVar, privateFunction에 직접 접근 불가
// window.myApp.publicAPI.doSomething()으로만 접근 가능

2. 모듈 패턴

스코프를 활용한 모듈 패턴:

 
javascript
var Counter = (function() {
  // 비공개 변수
  var count = 0;
  
  // 비공개 함수
  function validateCount(newCount) {
    return newCount >= 0;
  }
  
  // 공개 API
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      if (validateCount(count - 1)) {
        count--;
      }
      return count;
    },
    getValue: function() {
      return count;
    },
    reset: function() {
      count = 0;
      return count;
    }
  };
})();

console.log(Counter.getValue());  // 0
Counter.increment();
Counter.increment();
console.log(Counter.getValue());  // 2
Counter.reset();
console.log(Counter.getValue());  // 0
// console.log(count);  // ReferenceError (비공개 변수에 접근 불가)

3. 블록 스코프 활용하기

 
javascript
// 임시 변수의 생명 주기를 제한하기
{
  let temp = calculateSomething();
  processTempData(temp);
  // temp는 이 블록 외부에서 접근 불가
}

// 루프에서 블록 스코프 활용
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);  // 0, 1, 2, 3, 4
}

// var를 사용하면 다른 결과
for (var j = 0; j < 5; j++) {
  setTimeout(() => console.log(j), 100);  // 5, 5, 5, 5, 5
}

마치며: 스코프의 중요성과 올바른 활용

지금까지 자바스크립트에서 스코프의 개념과 종류, 스코프 체인의 작동 방식에 대해 살펴봤습니다. 스코프는 자바스크립트의 가장 기본적이면서도 중요한 개념 중 하나로, 이를 제대로 이해하면 더 깨끗하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

특히 스코프 체인과 전역 스코프에 대한 이해는 자바스크립트 개발자로서 성장하는 데 필수적입니다. 변수가 어느 스코프에 속하는지, 스코프 체인을 통해 어떻게 변수를 찾는지, 그리고 각 선언 키워드(var, let, const)가 스코프에 어떤 영향을 미치는지 이해하면 많은 일반적인 자바스크립트 문제를 방지할 수 있습니다.

다음 포스팅에서는 "스코프의 작동 방식과 쓰임새"에 대해 더 깊이 알아보겠습니다. 특히 클로저, 렉시컬 환경, 그리고 스코프 체인의 실제 활용 사례를 자세히 다룰 예정입니다.

참고 자료:

  • "You Don't Know JS Yet" by Kyle Simpson
  • ECMAScript 명세서
  • MDN Web Docs - JavaScript 가이드

이번 포스팅에서는 JS 엔진이 우리가 작성한 코드를 어떻게 처리하는지 깊이 있게 알아보겠습니다.

많은 개발자들이 자바스크립트를 사용하면서도 그 내부 동작 원리에 대해서는 깊이 이해하지 못하는 경우가 많습니다. 그러나 코드가 어떻게 처리되는지 이해하면 더 효율적인 코드 작성은 물론, 디버깅 능력도 크게 향상됩니다. 특히 스코프와 클로저 같은 고급 개념을 마스터하기 위한 기본 토대가 됩니다.

본 포스팅은 4번에 걸쳐서 업로드 됩니다.

  1. JS엔진은 우리가 작성한 코드를 어떻게 처리하나?
  2. 스코프는 무엇인가?
  3. 스코프의 작동 방식과 쓰임새
  4. 스코프를 다룰 때 주의할 점

 

자바스크립트와 컴파일 과정

자바스크립트 코드는 실행되기 전에 반드시 처리 과정을 거칩니다. 이 과정은 명확히 "컴파일레이션(compilation)"이라고 할 수 있습니다. 왜냐하면:

  1. 코드는 실행되기 전에 먼저 파싱됩니다
  2. 다양한 최적화가 수행됩니다
  3. 실행 가능한 형태로 변환됩니다

전통적인 컴파일 언어(C++, Java)와의 가장 큰 차이점은 자바스크립트에서는 컴파일과 실행 사이의 시간이 매우 짧다는 것입니다. 그러나 분명한 컴파일 단계가 존재합니다.

 

코드 컴파일 과정

자바스크립트 엔진이 코드를 처리하는 과정을 더 구체적으로 살펴봅시다. 이 과정은 크게 세 단계로 나뉩니다:

1. 토크나이징/렉싱(Tokenizing/Lexing)

토큰화는 문자열을 의미 있는 토큰(token)으로 나누는 과정입니다. 예를 들어:

var greeting = "안녕하세요";

위 코드는 다음과 같은 토큰으로 나뉩니다:

  • var (키워드)
  • greeting (식별자)
  • = (할당 연산자)
  • "안녕하세요" (문자열 리터럴)
  • ; (세미콜론)

이 단계에서 구문 오류가 발견되면 바로 SyntaxError가 발생합니다:

var greeting = "안녕하세요; // 닫는 따옴표 누락
// Uncaught SyntaxError: Invalid or unexpected token

2. 파싱, AST 생성

파싱 단계에서는 토큰 배열을 프로그램 문법에 맞는 중첩 구조인 AST(Abstract Syntax Tree, 추상 구문 트리)로 변환합니다.

var greeting = "안녕하세요"; 코드의 AST는 대략 다음과 같은 구조를 가집니다:

VariableDeclaration
  ├── kind: "var"
  └── declarations: [
        VariableDeclarator
          ├── id: Identifier(name: "greeting")
          └── init: Literal(value: "안녕하세요", raw: "\"안녕하세요\"")
      ]

AST Explorer(https://astexplorer.net/)와 같은 도구를 사용하면 실제 자바스크립트 코드의 AST를 시각적으로 확인할 수 있습니다. 복잡한 코드의 동작 방식을 이해하는 데 큰 도움이 됩니다.

3. 코드 생성

마지막으로, AST는 실행 가능한 코드로 변환됩니다. 이 단계에서 다음과 같은 중요한 작업이 수행됩니다:

  • 변수와 함수 선언을 처리하고 스코프와 연결
  • 코드 최적화
  • 바이트코드 또는 기계어 생성

코드 생성 단계에서는 변수나 함수 선언들의 스코프가 결정되고, 이후 실행 단계에서 활용될 준비가 됩니다.

코드 실행 과정

컴파일 과정이 완료되면 생성된 코드가 실행됩니다. 실행 단계에서는 앞서 결정된 스코프를 기반으로 변수와 함수들이 메모리에 할당되고 접근됩니다.

실행 흐름 이해하기

간단한 예제를 통해 전체 프로세스를 이해해 봅시다:

console.log(greeting);  // undefined
var greeting = "안녕하세요";
console.log(greeting);  // "안녕하세요"

function sayHello() {
  console.log("Hello!");
}
sayHello();  // "Hello!"

이 코드의 컴파일 및 실행 흐름:

  1. 컴파일 단계:
    • greeting 변수 선언 인식 및 스코프에 등록
    • sayHello 함수 선언 인식 및 스코프에 등록
    • 실행 코드 생성
  2. 실행 단계:
    • 첫 번째 console.log(greeting) 실행: greeting은 선언만 되고 아직 할당되지 않아 undefined 출력
    • greeting = "안녕하세요" 실행: 변수에 값 할당
    • 두 번째 console.log(greeting) 실행: 할당된 값 "안녕하세요" 출력
    • sayHello() 함수 호출: "Hello!" 출력

여기서 greeting이 첫 번째 로그에서 undefined로 출력되는 현상이 바로 호이스팅(hoisting)이라고 하는데, 이는 컴파일 단계에서 선언문이 먼저 처리되기 때문에 발생합니다. 호이스팅에 대해서는 다음 포스팅에서 더 자세히 다루겠습니다.

타깃과 소스의 개념

자바스크립트 엔진이 코드를 컴파일할 때 마주하는 중요한 과제 중 하나는 모든 변수와 함수의 스코프를 올바르게 결정하는 것입니다. 이 과정에서 컴파일러는 각 식별자(변수, 함수명 등)가 "타깃(target)"인지 "소스(source)"인지 판단해야 합니다.

타깃과 소스란?

  • 타깃(Target): 값이 할당되는 변수 (할당문의 왼쪽에 위치)
  • 소스(Source): 값을 제공하는 변수 (할당문의 오른쪽이나 표현식에 위치)

다음 코드에서 타깃과 소스를 식별해봅시다:

var students = [
  { id: 14, name: "카일" },
  { id: 73, name: "수지" },
  { id: 112, name: "지영" },
  { id: 6, name: "푸른" }
];

function getStudentName(studentID) {
  for (let student of students) {
    if (student.id == studentID) {
      return student.name;
    }
  }
}

var nextStudent = getStudentName(73);
console.log(nextStudent);  // "수지"

이 코드에서:

  • students는 배열 리터럴이 할당되는 타깃
  • studentID는 함수 호출 시 인자 값(73)이 할당되는 타깃
  • student는 for 루프에서 배열의 각 요소가 할당되는 타깃
  • getStudentName 함수 호출에서 73은 소스
  • nextStudent = getStudentName(73)에서 nextStudent는 타깃, 함수 호출 결과는 소스
  • console.log에서 nextStudent는 소스

실제 개발에서의 응용

타깃과 소스 개념을 이해하면 코드의 흐름과 변수 사용을 더 명확하게 파악할 수 있습니다. 예를 들어 다음과 같은 코드가 있다고 가정합시다:

var x = 10;
var y = x + 5;
var z = y * 2;

여기서 변수들의 역할을 분석하면:

  • 첫 번째 라인: x는 타깃, 10은 소스(리터럴)
  • 두 번째 라인: y는 타깃, x와 5는 소스
  • 세 번째 라인: z는 타깃, y와 2는 소스

이렇게 코드를 분석하면 데이터의 흐름을 더 명확하게 이해할 수 있습니다. 특히 복잡한 함수나 클로저를 다룰 때 이러한 분석이 도움이 됩니다.

렉시컬 스코프와 컴파일의 관계

지금까지 살펴본 자바스크립트의 컴파일 과정은 렉시컬 스코프의 개념과 직접적으로 연결됩니다. 자바스크립트에서 스코프는 컴파일 타임에 결정되며, 이를 렉시컬 스코프(어휘적 스코프)라고 합니다.

렉시컬 스코프의 핵심 원리

렉시컬 스코프의 가장 중요한 특징은 함수나 블록, 변수 선언의 스코프가 전적으로 코드의 물리적 배치에 따라 결정된다는 점입니다. 간단히 말해, 코드를 작성할 때 변수와 함수를 어디에 배치하느냐에 따라 스코프가 정해집니다.

var globalVar = "전역 변수";

function outer() {
  var outerVar = "외부 함수 변수";
  
  function inner() {
    var innerVar = "내부 함수 변수";
    console.log(globalVar);  // 전역 스코프에서 찾음
    console.log(outerVar);   // outer 함수 스코프에서 찾음
    console.log(innerVar);   // 현재 스코프에서 찾음
  }
  
  inner();
}

outer();

이 코드에서:

  • inner 함수는 자신의 렉시컬 스코프(inner 함수 내부), outer 함수의 스코프, 그리고 전역 스코프에 접근할 수 있습니다.
  • 반면, outer 함수는 inner 함수의 스코프에 접근할 수 없습니다.

컴파일 시 스코프 맵 생성

컴파일 과정에서 JS 엔진은 프로그램 전체의 스코프 구조를 담은 일종의 "지도"를 생성합니다. 이 지도는 어떤 변수가 어떤 스코프에 속하는지를 정의하며, 런타임에 변수를 찾을 때 사용됩니다.

중요한 점은 컴파일레이션 중에는 스코프를 식별하기만 하고, 실제 스코프 객체는 코드가 실행되는 런타임에 생성된다는 것입니다. 컴파일 단계에서는 스코프와 변수의 메모리 예약 관점에서 실제로는 아무것도 실행되지 않습니다.

스코프 체인과 변수 검색

변수를 참조할 때, JS 엔진은 현재 스코프에서 시작해 외부 스코프로 단계적으로 이동하며 변수를 찾습니다:

  1. 현재 스코프에서 변수명 검색
  2. 찾지 못하면 바로 바깥 스코프로 이동하여 검색
  3. 다시 찾지 못하면 또 바깥 스코프로 이동
  4. 전역 스코프까지 검색했는데도 찾지 못하면 ReferenceError 발생

이 과정은 변수가 어디에서 선언되었는지를 기준으로 한 렉시컬 스코핑이며, 코드가 어디서 호출되는지가 아니라 어디에 작성되었는지가 중요합니다.

렉시컬 스코프의 실용적 의미

렉시컬 스코프를 이해하면 코드를 더 예측 가능하게 작성할 수 있습니다. 몇 가지 실용적인 팁:

  1. 변수 선언은 사용 범위에 가장 가까운 스코프에서 하기: 전역 변수 사용을 최소화하고, 필요한 스코프에서만 변수를 선언합니다.
  2. 같은 이름의 변수 중복 선언 피하기: 특히 중첩된 스코프에서 같은 이름의 변수를 사용하면 예상치 못한 결과가 발생할 수 있습니다.
  3. 블록 스코프 활용하기: ES6의 let과 const를 사용해 변수의 스코프를 최소화합니다.
// 피해야 할 패턴
var userId = 123;

function processUser() {
  // 의도치 않게 전역 변수를 덮어씀
  userId = 456;
  // ...
}

// 권장하는 패턴
var userId = 123;

function processUser() {
  // 지역 변수로 선언하여 스코프 분리
  let userId = 456;
  // ...
}

마치며: 렉시컬 스코프와 코드 작성의 중요성

지금까지 JS 엔진이 코드를 처리하는 방식에 대해 살펴봤습니다. 자바스크립트는 컴파일 과정을 거쳐 코드를 실행하며, 이 과정에서 렉시컬 스코프가 결정됩니다.

자바스크립트에서 스코프가 컴파일 타임에 결정된다는 사실은 코드를 작성하는 방식이 매우 중요하다는 것을 의미합니다. 변수와 함수를 어디에 배치하느냐에 따라 전체 프로그램의 동작이 결정됩니다.

컴파일 중에는 스코프를 식별하기만 하고, 실제 각 스코프를 실행해야만 하는 런타임 전까지는 스코프가 생성되지 않습니다. 컴파일 단계에서는 프로그램 실행에 필요한 모든 렉시컬 스코프가 들어간 '지도'를 만들어냅니다. 이것이 런타임에 사용할 모든 코드가 들어간 계획안이라고 생각하면 됩니다.

이러한 이해를 바탕으로 코드를 작성하면:

  • 의도하지 않은 변수 참조나 충돌을 방지할 수 있습니다
  • 코드의 예측 가능성과 유지보수성이 향상됩니다
  • 스코프 관련 디버깅을 더 쉽게 할 수 있습니다

다음 포스팅에서는 "스코프는 무엇인가?"라는 주제로, 자바스크립트의 다양한 스코프 유형과 작동 방식에 대해 더 깊이 알아보겠습니다. 특히 함수 스코프와 블록 스코프의 차이, 중첩 스코프, 그리고 스코프 체인의 동작 원리를 자세히 살펴볼 예정입니다.


참고 자료:

  • "You Don't Know JS Yet" by Kyle Simpson
  • ECMA-262 명세서
  • MDN Web Docs

이 내용은 nginx를 쓰고 있는 가상서버에서는 공통적으로 가능한 ssl 세팅입니다.

1. SSL 인증서 파일 준비하기

일반적인 웹사이트의 경우, SSL 인증서를 구매하거나 Let's Encrypt 같은 무료 서비스에서 발급받아 다음과 같은 파일들을 준비합니다:

  • ssl.crt 또는 certificate.crt: SSL 인증서 파일
  • ssl.key 또는 private.key: 프라이빗 키 파일

 

2. 필요한 디렉토리 생성하기

# 해당하는 폴더를 만듭니다.
sudo mkdir -p /etc/ssl/certs
sudo mkdir -p /etc/ssl/private

 

3. 인증서와 키 파일 복사 및 권한 설정

# 준비된 crt와 key 파일에 대한 권한을 설정합니다.
# 보통 filezilla를 통해 파일을 업로드한 뒤 작업합니다.
sudo chmod 644 /etc/ssl/certs/ssl.crt
sudo chmod 600 /etc/ssl/private/ssl.key

 

4. 암호화된 키 파일 처리하기

키 파일에 암호가 설정되어 있다면, 서버 재시작시 자동으로 SSL이 적용되도록 암호를 제거합니다:

# 원본 키 파일 백업
cp /etc/ssl/private/ssl.key /etc/ssl/private/ssl.key.orig

# 암호 없는 키 파일로 변환
openssl rsa -in /etc/ssl/private/ssl.key.orig -out /etc/ssl/private/ssl.key

# 적절한 권한 다시 설정
chmod 600 /etc/ssl/private/ssl.key

 

5. Nginx 설정 파일 수정하기

웹사이트의 설정 파일을 수정합니다:

# nginx의 경우 sites-available 에서 http, https 등의 블록을 위해 파일을 손봐야합니다.
# 파일의 경로가 꼭 이렇진 않습니다. sites-available만 참고해주세요
vim /etc/nginx/sites-available/{YOUR-WEBSITE}

- 일반적인 웹사이트의 Nginx 설정 예시:

# HTTP 설정 - HTTPS로 리다이렉트
server {
    listen 80;
    server_name example.com www.example.com;
    
    # HTTPS로 리다이렉트
    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS 설정
server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # SSL 인증서 설정
    ssl_certificate /etc/ssl/certs/ssl.crt;
    ssl_certificate_key /etc/ssl/private/ssl.key;

    # SSL 프로토콜 설정
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
    
    # 정적 파일이 있는 루트 디렉토리 설정
    root /var/www/html;
    index index.html index.htm index.php;
    
    # 기본 위치 설정
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    # PHP 처리 설정 (PHP 사용 시)
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # PHP 버전에 맞게 수정
    }
    
    # 접근 로그와 에러 로그 위치
    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log;
}

 

6. Nginx 설정 테스트 및 재시작

# 설정이 올바른지 테스트
nginx -t

# 설정이 올바르면 Nginx 재시작
systemctl restart nginx

 

7. 방화벽 설정 확인 (필요한 경우)

# UFW 방화벽 사용 시, HTTPS 포트 개방
sudo ufw allow 443/tcp

 

8. 확인 및 문제 해결

웹 브라우저에서 https://example.com으로 접속하여 SSL이 제대로 적용되었는지 확인합니다. 주소 표시줄에 자물쇠 아이콘이 표시되면 성공적으로 적용된 것입니다.

문제가 발생한 경우 다음 로그 파일을 확인합니다:

# Nginx 오류 로그 확인
tail -n 100 /var/log/nginx/error.log

 

이런 과정으로 가상서버에서 호스팅하는 웹사이트에 SSL을 적용하는 과정이 완료되었습니다.

가비아나 AWS에서는 간편하게 설정할 수 있었고, 자동으로 적용이 되었던 부분인데

SSL을 신청 받고 /.well-known/pki-validation/ 에 http 인증용 파일을 올렸는데도 발급이 안돼서 무슨 문제인가 했어요

다른 분들에게도 도움이 되었으면 좋겠습니다

+ Recent posts