오늘의 인프런 강의 폴리싱 및 버그 해결에 생각보다 많은 시간이 소요되어 프로젝트 개발 일정이 지연되었다. 이에 오늘도 강의 수강을 생략하고 프로젝트 개발에 집중하였다. 방어구 & 방패 시스템이 어느정도 일단락되면 다시 강의 수강을 재개할 예정이다. 오늘의 프로젝트 개발 -> 링크 Blocking Locomotion Blocking 시스템 Parrying 시스템
AWS 네트워크 전체 구조 AWS 네트워크를 공부할 때 패킷이 목적지까지 이동하는 과정을 이해하면 좋은 거 같다. 패킷이 이동할 때는 항상 이 순서대로 확인한다. 1. 주소가 무엇인가? — IP, CIDR, VPC, Subnet, ENI 2. 어디로 보내야 하는가? — Route Table, IGW, NAT, Endpoint 3. 통과해도 되는가? — Security Group, NACL, WAF, Firewall 4. 어떤 서버로 전달할 것인가? — Route 53, ALB, Target Group 5. 문제가 생겼을 때 어디서 막혔는가? — Flow Logs, Reachability Analyzer, CloudWatch AWS에서 VPC는 논리적으로 격리된 가상 네트워크 이고 그 안에 서브넷,라우팅,게이트웨이,보안 규칙을 구성한다. 서브넷은 반드시 하나의 가용 영역에 속한다. 일반적인 웹 서비스 구성 사용자 -> example.com Route 53 - DNS 주소 조회 -> CloudFront / WAF - 선택 사항 -> Internet Gateway -> ALB - Public Subnet A, B Listener Rule Target Group -> EC2 / ECS - Private App Subnet A, B -> RDS - Private DB Subnet A, B EC2의 외부 통신 인터넷 → NAT Gateway → Internet Gateway S3 등 AWS 서비스 → VPC Endpoint 요약 구성요소 핵심 역할 VPC 네트워크 전체 경계 CIDR VPC와 서브넷에서 사용할 IP 범위 Subnet VPC를 가용 영역별,용도별로 분할 Route Table 목적지에 따라 패킷의 다음 경로 결정 Internet Gateway VPC와 인터넷 연결 NAT Gateway 프라이빗 서버의 IPv4 외부 통신 Security Group EC2, ALB, RDS 단위 방화벽 NACL 서브넷 단위 방화벽 Route 53 도메인을 주소로 변환 ALB HTTP 요청을 적절한 서버에 분배 Target Group ALB가 요청을 보낼 서버 묶음 VPC Endpoint 인터넷 없이 AWS 서비스에 접근 Flow Logs 네트워크 허용 거부 기록 IP 주소와 CIDR IP 주소 IP 주소는 네트워크에서 서버를 구분하는 주소 EX) 10.0.10.25 AWS에서는 EC2, RDS, ALB 같은 리소스의 네트워크 인터페이스에 IP가 할당됩니다. 사설 IPv4 VPC 내부에서 사용하는 주소 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 일반적으로 AWS VPC에는 10.x.x.x 대역을 많이 사용 10.0.0.0/16 10.10.0.0/16 10.100.0.0/16 공인 IPv4 인터넷에서 접근할 수 있는 주소 EC2에 자동 할당되는 공인 IPv4는 인스턴스를 중지했다가 다시 시작하면 바뀔 수 있다. 고정 공인 IPv4가 필요하면 Elastic IP를 사용한다. ** Elastic IP - AWS에서 할당받는 고정 공인 IPv4 주소 EC2의 네트워크 인터페이스에는 기본적으로 사설 IP가 있고 공인 IPv4는 인터넷 게이트웨이를 통해 사설 IP와 매핑된다. CIDR CIDR는 IP 주소의 범위를 표현하는 방식 10.0.0.0/16 IPv4는 총 32비트 /16은 앞의 16비트가 네트워크 영역이라는 뜻 즉 32- n비트를 사용가능하니까 2^(32-n)개만큼 사용가능함 CIDR 전체 IP 개수 AWS 서브넷에서 사용 가능한 IPv4 /16 65,536 65,531 /20 4,096 4,091 /24 256 251 /26 64 59 /28 16 11 AWS는 각 IPv4 서브넷에서 처음 네 주소와 마지막 한 주소 총 5개를 예약한다. EX) 10.0.1.0/24라면 .0, .1, .2, .3, .255를 사용할 수 없고 일반 리소스에는 .4부터 .254까지 할당할 수 있다. IPv4 서브넷의 크기는 /28부터 /16 범위로 생성할 수 있다. EX) VPC: 10.0.0.0/16 일 때 VPC를 용도와 가용 영역에 따라 나눌 수 있다. Public A: 10.0.0.0/24 Public B: 10.0.1.0/24 Private App A: 10.0.10.0/24 Private App B: 10.0.11.0/24 Private DB A: 10.0.20.0/24 Private DB B: 10.0.21.0/24 CIDR 설계에서 가장 중요한 규칙 서로 연결할 가능성이 있는 네트워크의 CIDR는 겹치면 안된다. prod VPC: 10.0.0.0/16 dev VPC: 10.1.0.0/16 회사망: 10.100.0.0/16 다음과 같이 겹치면 향후 VPC Peering, Transit Gateway, VPN 연결 시 목적지를 구분하기 어렵다. prod VPC: 10.0.0.0/16 dev VPC: 10.0.0.0/16 !! 겹침 AWS IPAM은 조직 전체의 IP 대역을 계획하고 할당 내역과 사용 현황을 관리할 수 있는 서비스 어떤 VPC에 어떤 CIDR를 쓰고 있는지 앞으로 어떤 IP 대역을 어디에 배정할지 관리하는 장부 느낌 ENI와 사설 IP가 리소스에 할당되는 방식 ENI(Elastic Network Interface)는 AWS 리소스에 연결되는 가상 네트워크 카드 EC2가 VPC 안에서 다른 리소스와 통신하려면 ENI를 통해 네트워크에 연결된다. EC2를 특정 서브넷에 생성하면 AWS는 해당 서브넷의 CIDR 범위에서 사용 가능한 사설 IP를 하나 할당하고 이 IP를 EC2의 ENI에 연결한다. Private App Subnet: 10.0.10.0/24 -> EC2 생성 연결 -> ENI 생성 Private IP: 10.0.10.25 MAC Address Security Group EX) 10.0.10.0/24 서브넷에 EC2를 생성하면 10.0.10.25와 같이 해당 서브넷 범위 안의 사설 IP가 할당된다. EC2 자체에 IP가 직접 붙는다고 보기보다는 EC2에 연결된 ENI에 IP 주소가 할당되고 EC2가 해당 ENI를 통해 통신한다. * Security Group 역시 EC2 자체보다는 ENI에 적용된다. * 따라서 ENI에는 사설 IP뿐 아니라 해당 네트워크 인터페이스의 트래픽을 제어하는 Security Group도 함께 연결된다. 하나의 ENI에는 기본 사설 IP 외에 여러 개의 Secondary Private IP를 추가 할 수 있으며 EC2 인스턴스 타입에 따라 여러 개의 ENI를 연결하는 것도 가능하다. EX) EC2에 연결된 ENI 1- 10.0.10.25, 10.0.10.26 ENI 2- 10.0.20.15 EC2에 공인 IPv4가 할당된 경우에도 실제 EC2 내부에서는 주로 사설 IP를 사용한다. AWS가 외부의 공인 IPv4와 ENI의 사설 IP를 매핑하여 인터넷 통신이 가능하도록 처리한다. Internet Public IP: 3.34.100.20 AWS에서 주소 매핑 Private IP: 10.0.10.25 ENI EC2 ENI는 EC2뿐 아니라 RDS, ALB, NAT Gateway, VPC Endpoint 등 VPC 안에서 동작하는 여러 AWS 서비스에서도 사용된다. AWS에서 리소스가 VPC 네트워크에 참여하려면 서브넷의 IP를 할당받고 네트워크 인터페이스를 통해 통신하는 구조이다.
오늘은 사용자 앱의 입구(스플래시 → 로그인 → 온보딩)와 핵심 화면인 스와이프 피드를 만들었다. API 연동 전이라 로그인과 피드 데이터는 모두 mock이다. 0. 작업 방식: Figma MCP로 디자인 읽기 이번 작업은 Claude Code에 Figma MCP를 연결해서 진행했다. 화면을 만들 때마다 Figma 링크의 노드를 읽어서, 크기·색·글자 스펙을 눈대중이 아니라 실제 값으로 가져왔다. 활용 방법 화면 스펙 읽기: 노드를 읽으면 요소별 크기, 여백, 색 토큰, 글자 크기가 나온다. 예를 들어 포스터 카드는 폭 328, 사진 높이 320, 모서리 12, 가게 이름 24px Bold처럼 정확한 값으로 확인했다. 디자인 시스템과 비교: 화면 값이 디자인 시스템 스케일에 있는지 확인했다. 화면은 좌우 여백 20px인데 그리드는 16px이고, 버튼 모서리 14px은 Radius 스케일(4, 8, 12, 16)에 없었다. 이런 경우는 디자인 시스템 기준으로 맞췄다. 디자인 수정: 스플래시 버튼 삭제처럼 Figma 자체를 고칠 때는 Codex에 Figma MCP 작업을 맡겼다. 이 과정에서 화면만 봤다면 지나쳤을 불일치도 꽤 찾았다. 성별 선택 칩 중 "기타"가 레이어 이름은 아직 "입력 안 함"이고 스타일도 달랐다. 마스코트 컴포넌트 이름이 Coupon Success 인데, 실제 그림은 X 표시된 쿠폰(쿠폰 없음)이었다. 디자인 시스템 안에서도 Button 컴포넌트(모서리 14)와 Radius 스케일이 서로 달랐다. 코드에서는 모두 디자인 시스템 기준으로 맞춰서 구현했다. 1. 로그인 흐름 로그인 상태 관리 (Context) 로그인한 사용자 정보는 라우트 가드, 온보딩 완료 처리, 마이페이지 등 여러 화면에서 필요하다. props로 한 단계씩 내려주면 중간 컴포넌트가 쓰지도 않는 값을 계속 넘겨야 한다. 그래서 React Context로 앱 전체에 공유했다. 파일 역할 authContext.ts 로그인 정보가 지나갈 통로를 만든다 AuthProvider.tsx 실제 값( useState )을 들고 아래로 전달한다 useAuth.ts 화면에서 const { user, login } = useAuth() 로 꺼내 쓴다 AuthProvider 를 앱 가장 바깥에 두니, 어느 화면에서든 로그인 정보를 바로 받을 수 있게 됐다. 상태별 라우트 가드 사용자 상태를 세 가지로 나눴다. 상태 갈 수 있는 화면 다른 화면에 들어오면 로그인 전 스플래시, 로그인 스플래시로 이동 온보딩 중 온보딩 온보딩으로 이동 회원 탭 화면 스와이프로 이동 화면마다 조건을 넣지 않고, 라우터에서 화면 묶음 단위로 감쌌다. { element: <RequireAuth access="guest" />, children: [스플래시, 로그인] }, { element: <RequireAuth access="onboarding" />, children: [온보딩] }, { element: <RequireAuth access="member" />, children: [탭 화면] }, 앞으로 화면을 추가할 때는 알맞은 묶음 안에 넣기만 하면 된다. 스플래시 화면 하단 버튼 삭제 처음 디자인은 스플래시에 [시작하기] 버튼이 있고, 버튼을 누르면 로그인 화면으로 넘어가는 구조였다. 그런데 로그인 화면에도 소개 문구와 [카카오로 시작하기] 버튼이 있어서, 사용자는 비슷한 화면을 두 번 보고 버튼도 두 번 눌러야 했다. 스플래시는 앱을 열 때 브랜드를 잠깐 보여주는 화면이면 충분하다고 판단했다. 그래서 버튼을 없애고, 1.5초 뒤 로그인 화면으로 자동으로 넘어가게 했다. 넘어갈 때는 replace 로 이동해서 방문 기록에서 스플래시를 지웠다. 로그인 화면에서 뒤로 가기를 눌러도 스플래시가 다시 나오지 않는다. 2. 스와이프 피드 드래그 제스처 (Pointer Events) 일정이 빠듯해서 버튼으로 찜/패스를 먼저 완성하고, 드래그 제스처는 그다음에 붙였다. 제스처도 결국 같은 찜/패스 함수를 부르기 때문에, 버튼으로 동작을 먼저 확인해 두니 제스처는 "끌기와 방향 판단"만 추가하면 됐다. 제스처 라이브러리는 새 패키지라 팀 합의가 필요해서, 브라우저 기본 기능인 Pointer Events로 직접 만들었다. onPointerDown // 시작 위치 기억 onPointerMove // 움직인 만큼 카드 이동, 살짝 기울이기 onPointerUp // 카드 폭의 30% 넘게 밀었으면 찜/패스, 아니면 제자리로 touch-action: pan-y 를 줘서 위아래로 끌면 화면이 스크롤되고, 좌우로 끌 때만 카드가 움직이게 했다. 화면 높이에 맞춘 카드 크기 이 부분에서 시행착오가 있었다. 고정 높이: Figma대로 카드 높이를 고정했더니, 긴 폰에서 카드 아래가 130px 정도 비었다. 남는 공간 채우기: 카드가 남는 높이를 모두 차지하게 했더니, 이번엔 사진이 세로로 길쭉해졌다. 4:5까지만: 사진이 4:5 비율까지만 늘어나게 제한하고, 남는 공간은 카드 묶음 위아래로 나눴다. <div className="@container ..."> <div className="max-h-[calc(125cqw+10.5rem)] flex-1 ..."> 125cqw 는 컨테이너 폭의 125%라서, 사진 높이가 폭 × 1.25를 넘지 않는다. 짧은 폰에서는 사진이 줄어서 스크롤 없이 한 화면에 들어가고, 긴 폰에서도 길쭉해지지 않는다. 찜 피드백 (배지와 토스트) 찜을 해도 카드만 넘어가서 저장됐는지 알기 어려웠다. 그렇다고 스와이프는 연속으로 하는 동작이라, 찜할 때마다 알림을 띄우면 방해가 된다. 그래서 두 가지로 나눴다. 헤더 찜 개수 배지: 찜할 때마다 숫자가 늘면서 살짝 튄다. 방해 없이 쌓이는 게 보인다. 첫 찜에만 토스트: 그날 처음 찜했을 때만 "찜 목록에 담았어요 · 11:00부터 받을 수 있어요"를 띄운다. 런치캐치에서 찜은 발급이 아니라 11:00 선착순에 참여하는 것이라, 이 규칙을 처음에 한 번 알려주는 게 중요했다. 느낀 점 이번 작업에서 Figma MCP를 써보니 정말 편했다. Claude Code가 Figma를 직접 읽어 들이고, 그 내용을 바탕으로 코드를 짠다는 게 신기했다. 디자인을 보고 크기와 색을 하나하나 옮겨 적던 과정이 줄어들면서, 개발이 훨씬 편해지고 있다는 게 느껴졌다. 오늘 작업 중 겪은 의존성 문제는 런치캐치 트러블슈팅 #1 에 따로 정리했다.
국세청은 사업자등록 상태를 알려주는 공개 API를 운영합니다. 10자리 번호를 넣으면 계속사업자인지, 휴업인지, 폐업인지 알려줍니다. 무료고, 빠르고, 정확합니다. 그런데 기억이 없습니다. 오늘 물어보면 이 회사가 폐업했다는 걸 알 수 있습니다. 지난달엔 어땠냐고 물으면 답이 없습니다. 그 질문이 API에 없으니까요. as_of 같은 파라미터도, 이력 엔드포인트도, 변경 기록도 없습니다. 쓸 수 있는 시제는 현재형 하나뿐입니다. AI 에이전트용 API를 만들면서 발견한 것 중 이게 가장 흥미로웠고, 그래서 당연한 일을 하기로 했습니다. 매일 보이는 걸 적어두기 시작한 겁니다. 무엇을 만들었나 하루에 한 번 도는 작업입니다. 정해진 목록에 있는 모든 회사의 현재 상태를 묻고, 답을 저장합니다. 그리고 오늘과 어제를 비교해서 바뀐 것만 따로 파일에 씁니다. 사흘째 숫자는 이렇습니다. 날짜 관측 기록된 변화 1일차 211,243 — (첫 실행이라 전부 신규) 2일차 211,243 70 3일차 211,243 94 하루치 스냅샷은 약 42MB, 변화 파일은 1.7MB입니다. 이 비율이 핵심입니다. 제가 기록하는 것의 99.2%는 "아무 일도 없었다"는 확인이고, 나머지 0.8%가 상품입니다. 실제로 저장된 변화 한 건은 이렇게 생겼습니다. json {"business_number":"...","field":"status","previous":"active","current":"closed", "previous_seen_at":"2026-09-29T00:05:14Z","detected_at":"2026-09-30T00:05:14Z"} 29일에 봤을 땐 영업 중이던 회사가 30일엔 폐업으로 바뀌어 있었습니다. 저는 이 회사가 실제로 언제 폐업했는지는 증명할 수 없습니다. 각 상태를 언제 봤는지만 알 뿐이죠. 그래서 그것만 저장합니다. "이 회사는 29일에 폐업했다"가 아니라 "봤을 때 영업 중이었고, 다시 봤을 때 폐업이었다". 둘은 다른 주장이고, 나중에 컴플라이언스 보고서에 들어갈 수도 있는 정보라면 이 차이가 중요합니다. 왜 할 가치가 있나 세 가지, 중요한 순서의 역순으로. 하나, 돈으로 살 수 없습니다. 원천에 이력이 없습니다. 원천을 감싼 다른 누구의 서비스에도 없고요. 경쟁자가 내일 "회사 상태 이력이 값지네"라고 깨달으면, 그의 이력은 내일부터 시작됩니다. 제 것은 오늘부터입니다. 이 격차는 벌어지기만 하고, 자본으로 메울 수 없습니다. 둘, 사업의 모양이 바뀝니다. 상태 조회는 일회성 거래입니다. 에이전트가 묻고, 2센트 내고, 떠나고, 다시 안 올 수도 있습니다. 반면 감시(watchlist)는 지속적인 관계입니다. 공급업체 100곳을 등록해두면 매일 제가 확인하고 움직인 것만 알려줍니다. 작지만 반복되고, 제 입장에선 이미 돌고 있는 인프라 위에서 공짜로 얹히는 겁니다. 셋, 신호는 상태가 아니라 변화입니다. "이 회사는 폐업했다"는 사실입니다. "이 회사는 3개월 전 휴업했다가 지난주에 재개했다"는 위험에 대한 해석이고, 조달 시스템이 실제로 알고 싶어 하는 건 후자입니다. 현재형 조회를 아무리 많이 해도 두 번째는 못 만듭니다. 쌓아야만 생깁니다. 하마터면 망가질 뻔한 것들 아무것도 안 하고 exit 0으로 끝난 작업. [지난 글](3편 링크)에 썼던 그 버그입니다. 엔트리포인트 가드가 파일 URL을 손으로 조립해서, 윈도에선 되고 리눅스에선 조용히 실패했습니다. 컨테이너는 뜨고, 아무것도 안 하고, 깔끔하게 종료되고, 스케줄러는 초록불이었습니다. 수동 실행 없이 스케줄을 믿었다면 일주일치 "아무것도"를 수집했을 겁니다. 첫날은 아무것도 증명하지 못합니다. 첫 실행에선 모든 레코드가 신규라 비교 경로가 한 번도 실행되지 않습니다. 결과물은 완벽해 보였지만 비교 로직이 도는지에 대해선 아무것도 말해주지 않았어요. 진짜 검증은 2일차에, 변화 70건이 나오고 이전값·현재값·양쪽 타임스탬프가 전부 채워졌는지 확인하면서 됐습니다. "어제와 오늘을 비교하는" 무언가를 만들었다면, 배포한 다음 날까지는 작동하는 시스템을 가진 게 아닙니다. 범위를 좁히는 게 보안 기능입니다. 어떤 회사를 추적할지 정하는 가장 쉬운 방법은 이용자가 조회한 번호를 쓰는 겁니다. 저는 일부러 안 했습니다. 목록은 공개 등록부(조달 업체 목록, 부정당업자 제재 목록)로만 구성했고, 이 원칙을 주석이 아니라 런타임 가드와 테스트로 강제했습니다. 주석은 급한 미래의 저를 막지 못하니까요. 이용자 조회 내용은 목록에 들어가지 않고, 따라서 이 데이터셋에는 제가 고객에게서 알아낸 것이 하나도 없습니다. 비용 하루 약 2,100콜(한도는 100만), 저장소는 월 수백 원, 작업 시간은 2분 30초. 프로젝트 전체에서 가장 싼 부분이고, 아마 가장 값진 부분입니다. 나머지는 전부 남이 일주일이면 다시 만들 수 있지만, 이것만은 못 만드니까요. 일반화하면 공개 데이터를 에이전트용으로 감싸고 계신다면, 원천이 답하지 않는 것이 무엇인지 물어볼 가치가 있습니다. 대개 시제 문제입니다. 무엇인지는 알려주지만, 무엇이었는지와 무엇이 바뀌었는지는 알려주지 않죠. 그 빈자리가 내가 소유할 수 있는 유일한 부분입니다. 나머지는 전부 원천이 이미 주는 것의 얇은 사본입니다. 내가 쌓은 이력만이 내 것이고, 그건 적어두기로 결심한 날부터 시작됩니다. 길게 썼지만 요지는 하나입니다. 할 거라면 필요해진 날이 아니라 오늘 시계를 켜세요. 기록하지 않은 데이터는 영원히 되찾을 수 없는 유일한 종류입니다.