배송대행 폼에서 박스의 가로·세로·높이를 받는다고 해 보겠습니다. 화면에 숫자가 입력됐다는 사실만으로 계산을 시작해도 될까요? 빈 값, 음수, 단위 혼동, 아직 포장하지 않은 예상값을 구분하지 않으면 계산 결과가 정밀해 보여도 잘못된 안내가 될 수 있습니다. 작성자는 김빠른 운영에 참여합니다. 이 글은 별도로 만든 교육용 코드이며 김빠른 운영 서버의 요금 계산 코드나 실제 요율을 공개하는 글이 아닙니다. 원고와 설명 이미지에 AI를 활용했고, 아래 예제의 정상·오류 입력 12가지를 로컬에서 실행했습니다. 1. 입력값과 적용 규칙을 다른 필드로 받습니다 박스 치수에는 cm처럼 단위가 필요합니다. 여기에 부피를 무게로 환산하는 계수, 올림 단위, 적용 서비스가 함께 있어야 실제 청구 기준을 논할 수 있습니다. 이 예제는 치수 입력 검증과 나눗셈까지만 다룹니다. 5000과 6000은 동작 비교용 가정값이며 김빠른이나 특정 운송사의 적용 계수가 아닙니다. AI 생성 설명 이미지입니다. 실제 고객 택배나 창고 사진이 아닙니다. 2. Decimal로 계산해도 입력 검증은 필요합니다 Python의 Decimal은 문자열에서 십진수를 다룰 수 있지만 NaN, Infinity도 표현합니다. 따라서 숫자 변환이 성공했다고 유효한 박스 치수로 받아들이면 안 됩니다. 이 예제는 유한한 양수만 통과시키며, bool도 숫자로 취급하지 않습니다. 세부 동작은 Python Decimal 공식 문서 를 참고했습니다. from decimal import Decimal, InvalidOperation def positive_decimal(raw): if isinstance(raw, bool): raise ValueError('measurement must be a positive finite number') try: value = Decimal(str(raw).strip()) except (InvalidOperation, ValueError): raise ValueError('measurement must be a positive finite number') from None if not value.is_finite() or value <= 0: raise ValueError('measurement must be a positive finite number') return value def volumetric_kg(length_cm, width_cm, height_cm, *, divisor): l, w, h, d = map(positive_decimal, (length_cm, width_cm, height_cm, divisor)) return l * w * h / d assert volumetric_kg('40', '30', '20', divisor='5000') == Decimal('4.8') assert volumetric_kg(' 40.5 ', '30', '20', divisor='5000') == Decimal('4.86') assert volumetric_kg('40', '30', '20', divisor='6000') == Decimal('4') 첫 세 가지는 정상 결과 비교입니다. 추가로 길이에 0, -1, 빈 문자열, abc, NaN, Infinity, True, None을 각각 넣는 8가지와 계수를 0으로 넣는 1가지를 확인했고 모두 ValueError로 거절됐습니다. 정상 3건과 오류 9건, 총 12건입니다. 3. 계산값이 곧 청구 무게는 아닙니다 예제의 4.8이라는 결과를 화면에서 ‘확정 청구 무게 4.8kg’으로 바꾸면 검증하지 않은 결정을 덧붙인 셈입니다. 실중량과 비교하는 방식, 최소 과금 무게, 올림 단위, 포장 후 재측정, 서비스별 적용 규칙은 이 함수에 없습니다. 화면에서는 최소한 입력 치수·단위, 적용 계수의 근거, 측정 여부, 계산 시점을 별도로 보여주는 편이 좋습니다. 예상 치수로 계산한 값은 예상이라고 표시하고, 최종 측정값으로 바뀌면 어떤 입력이 달라졌는지도 확인할 수 있어야 합니다. AI 생성 설명 이미지입니다. 실제 운송 기록이나 앱 화면이 아닙니다. 4. 운영에 넣기 전에 남은 일 이 예제는 모든 입력을 위한 범용 검증기가 아닙니다. 과도하게 긴 문자열, 지수 표기, 매우 큰 값에 대한 상한, 소수점 자릿수, Decimal 연산 문맥, 오류 메시지의 현지화, 재계산 이력은 별도로 정해야 합니다. 이미 float로 오염된 값을 문자열로 바꾼다고 원래 입력을 복원할 수도 없습니다. 서비스 안내를 연결할 때도 국가 선택과 입력 폼의 맥락을 유지해야 합니다. 김빠른 스페인 배송대행 안내 는 국가별 이용 흐름을 확인할 수 있는 자사 참고 페이지입니다. 위 가정 계수나 교육용 함수가 그 페이지의 운영 요금 기준이라는 뜻은 아닙니다. 핵심은 계산 함수를 복잡하게 만드는 것보다, ‘입력한 수치’, ‘적용한 규칙’, ‘사용자에게 약속한 값’을 분리하는 것입니다. 작은 예제의 테스트 통과와 실제 요금 시스템 검증도 구분해야 합니다.
대상 기간 : 2026-09-21 ~ 2026-09-23 (5~7일차) | 형식 : 4L | 작성일 : 2026-09-28 Liked 1주차에는 파이썬과 Git처럼 어느 정도 알고 있던 내용을 다시 배우는 부분이 많아서, 예습과 복습은 필요했지만 크게 무리하지 않는 선에서 혼자 보충하고 머릿속에 구조화해 나갈 수 있었다. 반면 2주차에는 처음 배우는 FastAPI 와 웹 프레임워크의 구조가 본격적으로 등장했다. 생각보다 훨씬 많은 시간이 필요했지만, 이해되지 않는 부분을 그냥 넘기지 않고 질문을 계속 쪼개서 다시 확인했다. 5일차 에는 FastAPI의 요청·응답 구조 와 경로·쿼리 매개변수 를 배우며 도서 목록 API를 구현해봤다. limit=0 과 /tasks/99 를 비교해 422 와 404 가 발생하는 지점을 구분하고, total 과 실제 반환되는 items 가 왜 다를 수 있는지도 직접 계산하며 확인했다. 처음 접하는 내용이었지만, 큰 흐름과 구조를 내 언어로 바꾸어 기록하고 그림으로 순서를 그려보면서 비교적 높은 이해도로 마무리했다고 생각한다. 6일차 에는 Pydantic 요청 검증, 응답 모델, 의존성 주입 이 한꺼번에 등장하면서 지금까지의 일일학습 중 가장 오랜 시간이 걸렸다. 학습하다가 회의감이 들 정도로 막히기도 했지만 정리를 포기하지 않고, 파이썬의 함수와 클래스 구조까지 되돌아가 다시 확인하려 노력했다. 6일차 당일에는 세부 코드를 전부 설명하지 못했어도, 이후 카페 주문 API를 다시 살펴보며 요청 모델·응답 모델·내부 기록의 역할을 구분하고 전체 흐름을 다시 연결할 수 있었다. 7일차 에는 비동기 처리, 외부 API, 예외 처리, 미들웨어, CORS 처럼 새로운 용어와 구조가 더해졌다. 수업 중에는 한 번 흐름을 놓친 뒤 코드를 따라 적기 바빴고, 트러블슈팅이 발생했을 때 이유를 이해하지 못한 채 AI에게 묻고 수정하기도 했다. 그래도 이후 질문을 작은 단위로 쪼개 다시 정리하면서, 당일에는 이유를 몰랐던 오류가 왜 발생했는지 설명할 수 있게 됐다. 추석 연휴 동안 완전히 쉬기보다 매일 조금씩이라도 학습하려고 했다. 6일차 카페 주문 API 와 7일차 상품 목록 페이지 API 를 다시 복습하면서, 5~7일차의 내용이 각각 떨어진 기능이 아니라 요청 한 건이 들어와 검사되고 처리된 뒤 응답으로 나가는 하나의 흐름이라는 점을 조금씩 연결할 수 있었다. 연휴가 없었다면 수업 진도를 따라가는 것만으로도 벅찼을 것 같지만, 쉬는 동안 부족했던 부분을 다시 볼 수 있었던 것이 다행이었다고 생각한다. 왕복 3시간의 출퇴근과 9시부터 6시까지 이어지는 교육, 캠퍼스에서의 자율학습 루틴에도 아직 적응하는 중이다. 부족한 점은 많지만, 첫 주보다 생활과 학습 리듬을 조금씩 만들어가고 있다는 점에서는 앞으로가 더 기대된다. Lacked 1주차에는 이미 접해본 파이썬과 Git을 중심으로 학습했기 때문에 모르는 부분을 혼자 보충하고 구조화하는 것이 가능했다. 하지만 2주차에는 FastAPI라는 웹 프레임워크 자체를 익혀야 했고, 클라이언트의 요청부터 서버의 검증과 처리, 응답까지 이어지는 전체 흐름도 함께 이해해야 했다. 여기에 실제 서비스에서는 각 기능이 왜 필요한지, 서비스 기획과는 어떻게 연결되는지까지 생각하려 하니 확실히 더 많은 시간 투자가 필요했다. 5일차에는 개념 이해도를 ‘상’이라고 생각했지만, 실제로 API를 실행하고 PR을 제출하는 과정에서는 서버 상태, 터미널 작업 위치, Git/GitHub 절차를 매 단계 AI에게 확인했다. 개념을 안다고 느끼는 것과 실제로 혼자 실행하는 것 사이에는 아직 간극이 있었다. 자기 지식에 대한 확신이 부족해 작업 과정 하나하나가 늦어지는 문제도 있었다. 6일차에는 “모델 객체를 함수에 전달한다”는 문장에서 막혀 파이썬의 함수와 클래스까지 다시 돌아가야 했다. 요청 모델과 응답 모델을 왜 따로 만드는지, 내부 기록과 공개 응답을 왜 분리하는지, 의존성 함수는 누가 언제 실행하는지 등이 한꺼번에 겹치면서 확실히 멘붕이 왔다. 모델과 함수를 관련 있다는 이유로 같은 클래스 안에 넣으려 했던 것처럼, 코드의 소속과 역할을 나누는 기준도 아직 부족했다. 7일차에는 비동기·외부 API가 예외·미들웨어·CORS보다 어렵게 느껴졌다. async def , 코루틴, await , 이벤트 루프, 블로킹·논블로킹처럼 비슷해 보이는 용어가 한꺼번에 등장했고, 코드도 점점 길어졌다. 큰 흐름은 잡았지만 실습 코드를 보면서 모든 줄의 입력, 실행 시점, 반환값을 바로 설명할 수 있는 수준은 아니었다. 수업 중 실습을 따라가고 당일 내용을 모두 복습하는 것만으로는 시간이 부족했다. 새로운 주제의 구조를 모르는 상태에서 정규 수업에 들어가면, 초반에 한 번 흐름을 놓친 뒤 뒤의 코드까지 계속 따라가기 어려워지는 문제가 있었다. 6~7일차에 특히 이 문제가 크게 드러났다. Learned 5일차에는 클라이언트와 서버, HTTP 요청·응답, GET과 POST, REST의 기본 설계 원칙을 배웠다. 경로 매개변수는 자원 하나를 특정하고, 쿼리 매개변수는 목록을 검색·필터링·정렬·페이지 처리하기 위한 조건이라는 기준을 익혔다. 요청 값의 형식이나 범위가 잘못되면 422 , 형식과 범위는 맞지만 실제 데이터가 없으면 404 가 된다는 것을 알게 됐다. offset 과 limit 은 조건에 맞는 결과 중 실제로 보여줄 구간을 정한다. total 은 이 범위를 적용하기 전에 조건에 맞았던 전체 개수다. 처음에는 limit=1 이면 total 도 1이라고 생각했지만, 포장 주문이 2개라면 한 건만 반환되더라도 total 은 2라는 것을 예시로 계산하면서 정정했다. 6일차에는 요청 검증, 의존성 주입, 응답 모델이 요청 처리 흐름의 서로 다른 위치에서 작동한다는 것을 배웠다. 요청 모델은 클라이언트가 보낸 본문을 경로 함수 실행 전에 검사하고, 의존성 함수는 여러 경로가 함께 사용하는 쿼리와 공통 준비물을 만든다. 응답 모델은 경로 함수가 처리한 결과 중 클라이언트에게 공개하기로 약속한 필드만 내보낸다. 카페 주문 API에서는 클라이언트가 menu_name , quantity , takeout 을 보내고, 서버가 id , status , staff_note 를 추가한다. 이 중 staff_note 는 내부 기록에만 보관하고 응답에는 포함하지 않는다. 생성·상세 응답과 목록 응답의 필드도 서로 다르기 때문에 각각의 공개 응답 모델이 필요하다. 요청 모델과 응답 모델은 단순히 클래스를 여러 개 만드는 것이 아니라, 들어오는 데이터와 나가는 데이터의 계약을 분리하기 위한 구조라는 점을 이해했다. 두 목록 경로는 같은 의존성 함수를 사용해 status , takeout , offset , limit 을 받는다. Depends(get_order_filters) 처럼 함수 자체를 전달해두면 FastAPI가 요청 시점에 호출하고, 검증된 결과를 경로 함수에 넣어준다. 메뉴별 목록은 먼저 경로의 menu_name 으로 주문을 고른 뒤 같은 공통 조건을 적용한다. 카페 주문 API를 다시 확인하면서 정상 생성은 201 , 잘못된 수량은 422 , 존재하는 주문 상세 조회는 200 , 없는 주문은 404 가 되는 것을 확인했다. response_model 을 통해 내부의 staff_note 가 응답에서 제외되는 것도 확인했다. 처음에는 각각의 상태 코드와 모델을 따로 외웠지만, 지금은 요청이 어느 단계에서 실패했는지에 따라 결과가 달라진다고 이해하고 있다. 7일차에는 비동기가 한 작업 자체를 빠르게 만드는 것이 아니라, 외부 응답을 기다리는 동안 다른 요청을 처리할 수 있게 하는 실행 방식이라는 것을 배웠다. async def 함수를 호출하면 바로 결과가 나오는 것이 아니라 코루틴 객체가 만들어지고, await 해야 함수가 실행되어 반환값을 받을 수 있다. 비동기 함수 안에서도 time.sleep() 같은 블로킹 코드를 사용하면 이벤트 루프 전체를 막을 수 있기 때문에, async def 라고 썼다는 사실만으로 비동기 처리가 완성되는 것은 아니다. 상품 목록 페이지 API에서는 우리 API의 page 와 size 를 외부 API의 skip 과 limit 으로 변환한다. 외부 API의 응답은 외부 응답 모델로 먼저 검사하고, 그중 필요한 필드만 우리 공개 응답 모델로 옮긴다. 외부의 title 을 우리 응답의 name 으로 바꾸고, 이미지·리뷰·내부 메타데이터는 공개하지 않는다. 이는 6일차에 배운 “내부 기록과 공개 응답의 분리”를 외부 데이터에도 적용한 구조였다. 외부 API를 호출할 때 우리 FastAPI는 브라우저 앞에서는 서버지만, 외부 API 앞에서는 HTTP 클라이언트가 된다. 외부 응답 시간 초과는 504 , 연결 실패·외부 오류 상태·예상과 다른 JSON 구조는 502 로 구분한다. 입력 검증 실패 422 , 업무상 존재하지 않는 데이터 404 , 외부 서비스 문제 502·504 처럼 실패가 발생한 위치에 따라 상태 코드가 달라진다는 흐름도 연결할 수 있었다. 결국 5~7일차에 배운 내용은 요청 한 건의 흐름 위에 놓는 게 핵심인 것 같다. 클라이언트가 값을 보내면 입력 검증이 먼저 이루어지고, 필요한 공통 조건은 의존성 함수가 준비한다. 경로 함수가 내부 로직이나 외부 API 호출을 수행하고, 예외는 실패 위치에 맞게 처리된다. 마지막에는 응답 모델이 공개 범위를 제한하며, 미들웨어는 이 요청과 응답의 앞뒤에서 공통 처리를 담당한다. Longed for 정규 수업이 끝난 뒤 복습만 빠르게 따라가는 방식에서 한 단계 더 나아가고 싶다. 퇴근길에는 다음 날 자료의 소제목을 먼저 훑고, 이미 아는 것 / 처음 보는 것 / 이전 흐름과 연결되는 부분 으로 짧게 나누어 적어봐야겠다. 완전히 이해하려 하지 않고, 다음 날 무엇을 배우는지에 대한 지도라도 먼저 만들어둔 상태로 들어가야 흐름을 빠르게 따라갈 수 있을 것 같다. 수업 중 새로운 기능이 나오면 문법부터 전부 이해하려 하지 않고, 먼저 요청 → 입력 검증 → 의존성 → 경로 함수 → 내부·외부 처리 → 예외 → 응답 모델 중 어디에 놓이는 기능인지 표시한다. 전체 지도에서 위치를 먼저 잡은 뒤 세부 코드를 보는 순서로 접근한다. 복습할 때는 그날 배운 코드를 처음부터 끝까지 다시 보는 것보다, 가장 막힌 개념 하나를 골라 정상 요청과 실패 요청을 각각 한 번씩 실행한다. 실행 전 예상 상태 코드와 응답을 먼저 적고, 실제 결과와 다른 부분만 다시 확인해볼 것. AI에게 코드를 요청하거나 수정하게 할 때는 먼저 목표, 입력, 출력, 검증 규칙, 상태 코드, 수정 범위를 직접 정리한다. 생성된 코드는 요청 범위를 벗어나지 않았는지, 배우지 않은 문법이 추가되지 않았는지, 내부 필드가 공개되지 않는지, 정상·실패 요청 결과가 계약과 일치하는지를 확인한다. “AI가 짜준 코드”가 아니라 “내가 이해하고 검증한 코드”로 남기는 것을 계속 연습하고자 한다. 아직 모든 코드를 백지에서 완성하거나 한 줄씩 바로 설명할 수 있는 단계는 아니다. 우선은 요청 한 건이 들어와 응답으로 나가기까지의 흐름을 놓치지 않고, 각 모델과 함수가 왜 그 위치에 있는지를 설명할 수 있는 수준을 단단하게 만들고 싶다. 이 기반을 바탕으로 다음 주의 API 테스트와 CRUD 통합에서는 새로운 내용을 따라가는 데만 급급하지 않고, 앞서 배운 FastAPI 구조를 실제 기능 안에서 반복해서 적용해보고 싶다. 📎 2주차 (9월 4주차) 일일회고 velog <5일차> FastAPI 설치 및 매개변수 <6일차> 요청 검증-응답모델-의존성 <7일차> 비동기처리-예외처리-미들웨어-CORS #FastAPI #웹프레임워크 #API구현
안녕하세요, shekhar입니다. 2년 동안 웹 플랫폼(POS 시스템, 대시보드, 마켓플레이스 같은 것들)을 만들어온 개인 개발자입니다. 먼저 밝히고 시작할게요. 이 글에서 소개하는 오픈소스 프로젝트 Andromity 의 개발자가 바로 저입니다. 내 제품을 내가 소개하는 글이니, 좋은 점도 솔직하게, 아직 부족한 점도 솔직하게 쓰겠습니다. 🌐 공식 사이트: https://andromity.agenticmarket.dev 💻 GitHub: https://github.com/agenticmarket/andromity AI 코딩 에이전트를 쓸 때마다 저는 같은 생각을 했습니다. "이거 참 똑똑한데... 내 말은 제대로 듣고 있는 걸까?" Cursor도, Claude Code도 정말 잘 쓰고 있습니다. 그런데 쓰면 쓸수록 신경 쓰이는 게 있었어요. 에이전트가 확인도 없이 파일을 막 고쳐나가는 겁니다. 시킨 수정은 했는데, 시키지도 않은 부분까지 바뀌어 있는 거죠. 특히 밤늦게 피곤할 때 diff를 대충 보고 커밋할 뻔한 적이 한두 번이 아닙니다. 그래서 생각을 바꿨습니다. 자율성과 통제는 트레이드오프가 아니라, 설계로 양립할 수 있다. 그렇게 만든 게 Andromity입니다. 핵심 아이디어는 단순해요. 제가 에이전트에게 바라는 건 똑똑함이 아니라 '말을 잘 듣는 것'입니다. 똑똑한데 말을 안 듣는 부하보다, 조금 서툴러도 시킨 범위 안에서만 움직이고, 위험한 부분에서는 멈춰주는 부하가 함께 일하기 편하더라고요. 만들고 나서 일주일 동안 직접 쓰면서 기록해봤습니다. velog 특유의 사용기 스타일로, 날짜별로 정리해볼게요. 1일차: 설치는 30초, 첫 질문은 "이 폴더를 신뢰하십니까?" Andromity는 두 가지로 쓸 수 있습니다. VS Code 확장, 그리고 터미널 CLI. VS Code 확장 🔗 Andromity AI Coding Agent for VS Code - Visual Studio Marketplace code --install-extension agenticmarket.andromity-agent 터미널 CLI (Linux / macOS) curl -fsSL https://raw.githubusercontent.com/agenticmarket/andromity/main/install.sh | bash Windows (PowerShell) irm https://raw.githubusercontent.com/agenticmarket/andromity/main/install.ps1 | iex 처음 실행하면 에이전트가 묻습니다. "이 폴더를 신뢰하십니까?" 폴더를 신뢰한다고 답하기 전에는 아무것도 실행하지 않아요. 이게 Andromity의 전부라고 해도 과언이 아닙니다. 권한을 먼저 묻고, 그 다음에 일하는 에이전트. 이럴 때 편합니다: 처음 받아본 외주 코드, 남의 레거시 프로젝트를 열었을 때. 일단 "신뢰 안 함"으로 시작하면 에이전트가 멋대로 건드릴 걱정이 없습니다. 2일차: SAFE 모드 — 가면허 기간 Andromity에는 권한 모드가 4단계 있습니다. SAFE → TRUST → FULL → YOLO. 모드 계획 승인 파일 수정 명령 실행 SAFE (기본값) 하나씩 확인 하나씩 확인 하나씩 확인 TRUST 확인 있음 바로 실행 바로 실행 FULL 자동 (로그 표시) 바로 실행 바로 실행 YOLO 자동 (표시 없음) 표시 없이 실행 표시 없이 실행 첫날부터 셋째 날까지 저는 SAFE 모드 그대로 썼습니다. 에이전트가 뭘 할 때마다 하나씩 확인하는 거죠. 솔직히 좀 느립니다. 근데 이 기간이 필요했어요. 에이전트가 어떤 판단을 하는지, 어디서 자주 실수하는지 파악하는 기간이니까요. 자동차 가면허 같은 겁니다. 이럴 때 편합니다: 처음 쓰는 저장소, 운영 코드 만질 때. 에이전트의 습관을 파악하기 전에는 SAFE에서 시작하세요. 3일차: /waterfall — 에이전트의 머릿속을 들여다보기 에이전트를 쓰면서 제일 스트레스인 건 "지금 뭐 하고 있는 거지?"라는 블랙박스 느낌입니다. /waterfall 이라고 치면 에이전트의 생각 단계와 도구 실행이 시간순 타임라인으로 보입니다. 어디서 시간이 잡아먹히는지 한눈에 보여서, "이 도구 호출에서 막히고 있구나" 하고 병목을 찾을 수 있어요. 분산 시스템 트레이싱이랑 같은 발상이에요. 에이전트 내부를 관측 가능하게 만든 거죠. 이럴 때 편합니다: 에이전트가 느리거나 이상하게 굴 때. 워터폴을 보면 프롬프트의 어디를 고쳐야 할지도 보입니다. 4일차: 권한 올리기 — TRUST, FULL, 그리고 플래닝 모드 에이전트의 습관을 파악했으니, 이제 권한을 올려봅니다. 정형 작업만 시킬 때는 FULL 모드로. 계획은 자동으로 세우되 로그로 보여주고, 실행은 바로 합니다. 그리고 큰 변경은 무작정 시키지 않고 플래닝 모드 로 먼저 계획을 세우게 합니다. "이 방향으로 진행해도 돼?"라고 합의를 보고 실행하는 거죠. 플래닝 모드는 에이전트와의 설계 리뷰 같은 겁니다. 방향이 틀어지기 전에 잡을 수 있어서, 나중에 되돌리는 수고가 확 줄어요. 이럴 때 편합니다: 새 기능 구현, 여러 파일에 걸친 리팩토링. 5분의 합의가 1시간의 삽질을 막아줍니다. ❌ 무작정 "이 기능 만들어줘" ⭕ 플랜 모드로 방향 합의 → 승인 후 실행 5일차: 밤에는 일시키고, 실수하면 되돌리기 이날은 두 가지 명령어를 써봤습니다. /cron — 스케줄러입니다. 예를 들면: /cron → "매일 밤 2시에 pytest 돌리고, 실패하면 고쳐서 커밋해" → 0 2 * * * 아침에 일어나면 고쳐진 코드가 커밋되어 있습니다. 밤에는 FULL 모드로 일시키고, 낮에는 SAFE로 쓰는 식의 운용도 가능해요. /undo — 에이전트의 직전 턴 변경사항을 통째로 되돌립니다. git 히스토리는 건드리지 않아요. /undo 이럴 때 편합니다: "아, 지시를 잘못 줬네" 싶을 때. 실패 비용이 거의 0이 되면, 이상하게도 과감하게 맡기게 됩니다. 세이브 포인트가 있는 게임은 마음 놓고 즐길 수 있잖아요. 6일차: Ollama로 완전 무료 — 비용 걱정 끝 이날은 돈 이야기를 해보죠. Andromity는 Claude, GPT-4o, Gemini, DeepSeek, Groq, OpenRouter 등 396종 이상 의 모델을 지원합니다. 방식은 BYOK(자기 API 키를 가져오는 방식)이라 코드는 오직 내가 고른 제공자에게만 갑니다. 솔직히 선택지가 너무 많아서 처음엔 좀 헤맸습니다. 제 용도별 정리: 용도 모델 비용 일단 써보기, 공부용 Ollama (로컬) 무료 일상 코딩 Claude / GPT-4o (BYOK) API 사용량만큼만 회사 코드를 외부에 못 보낼 때 Ollama (로컬) 무료 이럴 때 편합니다: 학생, 일단 무료로 써보고 싶은 분, 회사 코드를 외부 API에 보낼 수 없는 분. 저는 평소에 "Ollama로 테스트해보고, 실전은 Claude"로 씁니다. 7일차: 솔직한 한계 — 측정한 것만 말합니다 일주일을 마무리하면서, 안 좋은 이야기도 해야겠죠. 측정하지 않은 건 측정하지 않았다고 씁니다. 아직 GitHub 스타 20개 정도의 작은 프로젝트입니다. 버그도 있어요. 너그럽게 봐주시면 감사하겠습니다. Claude Code나 Cursor와의 엄밀한 벤치마크 비교는 안 했습니다. "빠르다", "싸다" 같은 말은 안 하겠습니다. 토큰 비용 실측 데이터는 아직 없습니다. BYOK이라 비용은 모델에 따라 다르다고만 말씀드립니다. 문서는 아직 영어 중심입니다. 한국어 문서는 없습니다. (관심 있으시면 기여 환영합니다!) Windows 지원은 늦게 시작해서 검증이 얇습니다. Windows 사용자 피드백을 특히 환영합니다. 그리고 하나 자랑을 하자면, v0.2.8에서 한글 IME 입력 문제 (조합 중 Enter로 오발송되는 문제)를 고쳤습니다. 한국어 사용자에게는 은근히 치명적인 버그였는데, 바로 잡았습니다. 작은 프로젝트지만 "그 나라 사람들이 매일 만지는 부분"은 소중히 하고 싶어요. 총정리: 7일 써본 결론 일주일 동안 직접 만들어서 직접 써본 입장에서 정리해봅니다. 이런 분 추천 사용법 AI 에이전트가 멋대로 고치는 게 무서운 분 SAFE 모드로 시작 (가면허 1주일) 에이전트가 뭘 하는지 보고 싶은 분 /waterfall 로 타임라인 확인 밤에 일시키고 싶은 분 FULL 모드 + /cron 돈 안 들이고 써보고 싶은 분 Ollama 로컬 모드 (완전 무료) 회사 코드를 외부에 못 보내는 분 Ollama 오프라인 모드 결국 핵심은 하나입니다. 에이전트의 자율성은 "언제든 멈출 수 있다"는 보장이 있을 때 비로소 가치가 됩니다. 액셀만 있는 차에는 아무도 타고 싶지 않잖아요. 🌐 공식 사이트: https://andromity.agenticmarket.dev 💻 GitHub: https://github.com/agenticmarket/andromity 🧩 VS Code 마켓플레이스: Andromity AI Coding Agent for VS Code 스타 하나, 이슈 하나가 개발자에게는 큰 힘이 됩니다. 특히 한국어 환경에서의 버그 제보는 대환영이에요. 읽어주셔서 감사합니다! 궁금한 점 있으시면 댓글로 남겨주세요. 다음 글에서 만나요! 테스트 환경 Andromity v0.2.11 (2026-09-25 릴리스) 작성일: 2026-09-28 대응 OS: Linux / macOS / Windows (각 OS별 설치 스크립트 제공)
문제 요약 A팀의 출전 순서는 이미 정해져 있고, B팀은 출전 순서를 자유롭게 정할 수 있다. B팀 선수가 A팀 선수보다 큰 숫자를 낼 때만 1점을 얻는다. B팀이 얻을 수 있는 최대 승점을 구한다. 핵심 아이디어 실제 출전 순서보다 어떤 숫자가 어떤 숫자를 이기는지가 중요하다. 따라서 A와 B를 모두 오름차순 정렬한다. B의 작은 숫자부터 확인하면서, 아직 이기지 않은 A 숫자 중 가장 작은 숫자를 이길 수 있는지 확인한다. B의 숫자 > A의 가장 작은 미매칭 숫자 : 이 A 선수를 이기고 승점을 얻는다. 그렇지 않다: 이 B 선수는 어떤 남은 A 선수도 이길 수 없으므로 승점 없이 넘긴다. 이길 수 있는 B 선수를 더 큰 A 숫자에 쓰면 더 작은 A 숫자를 이길 기회를 잃을 수 있다. 따라서 가능한 가장 작은 A 숫자와 매칭하는 선택이 항상 최적이다. 풀이 과정 A와 B를 오름차순 정렬한다. A에서 아직 이기지 않은 가장 작은 숫자의 인덱스를 a_index 로 둔다. 정렬된 B를 앞에서부터 확인한다. 현재 B 숫자가 A[a_index] 보다 크면 승점을 1 늘리고 a_index 를 다음 A 선수로 옮긴다. 모든 B 선수를 확인한 뒤 승점을 반환한다. Python 코드 def solution(A, B): A.sort() B.sort() score = 0 a_index = 0 for b_number in B: # 현재 B 선수가 가장 작은 미매칭 A 선수를 이길 수 있다. if a_index < len(A) and b_number > A[a_index]: score += 1 a_index += 1 return score 예시 A = [5, 1, 3, 7] -> [1, 3, 5, 7] B = [2, 2, 6, 8] -> [2, 2, 6, 8] B 숫자 이길 A 숫자 승점 2 1 1 2 이길 수 없음 1 6 3 2 8 5 3 따라서 B팀은 최대 3 점을 얻는다. 시간 복잡도 N 을 팀 인원 수라고 하자. A, B 정렬: O(N log N) B 배열 순회: O(N) 시간 복잡도: O(N log N) 공간 복잡도: 정렬 구현에 따라 O(N) 정리 이길 수 있는 B 숫자는 남은 A 숫자 중 가장 작은 수를 이기는 데 사용한다. 이 단순한 그리디 기준을 정렬된 두 배열에 적용하면 B팀의 최대 승점을 구할 수 있다.
유니티에서 그림자 처리를 직접 진행했던 기록을 위한 글 입니다. 커스텀 2D Shadow 커스텀 2D Light 에서 생성한 빛 객체는 그림자를 적용할 수 없던 반쪽자리 빛으로, 여기에 그림자 또한 동작하도록 구현해보았다. 이 그림자 구현에는 기존 커스텀 빛 관련 스크립트와 셰이더를 일부 이용해서 구현해주었다. 왼쪽의 노란 빛과 사과의 그림자가 커스텀 라이트와 섀도우, 오른쪽의 오렌지가 유니티 자체적인 빛과 그림자이다. 관련 코드 ScriptableRendererFeature 이전 빛 처리에 사용했던 Custom2DLightFeature에 그림자 관련 코드들을 넣어서 업데이트 해주었다. //셰이더에서 사용할 패스별 이름 설정 private readonly ShaderTagId m_OccluderShaderTagId = new ShaderTagId("Custom2DOccluder"); private readonly ShaderTagId m_ShadowExtrusionShaderTagId = new ShaderTagId("Custom2DShadowCaster"); //그림자 텍스처를 연결할 프로퍼티 ID 생성 private static readonly int CustomShadowTextureID = Shader.PropertyToID("_CustomShadowTexture"); 그림자 처리에 사용할 두 종류의 셰이더 패스 태그를 준비하고, 이후 완성된 그림자 텍스쳐를 전역으로 등록하기 위한 프로퍼티 ID를 생성해주었다. 이번 그림자 처리에는 빛과 다르게 한번이 아닌 두번의 렌더링이 필요하며, 그를 위한 두 패스 태그와 최종 그림자 텍스쳐를 외부에서 사용할 수 있도록 프로퍼티 ID를 선언해둔 것이다. private class ShadowPassData { public RendererListHandle occluderRendererList; public RendererListHandle shadowExtrusionRendererList; } 같은 이유로, 두개의 렌더러 리스트가 묶여있는 ShadowPassData를 생성하여, 뒤쪽의 실행 단계에서 두번의 렌더링을 진행할 수 있게 준비한다. //기존 커스텀 빛 생성 때의 RecordRenderGraph 메소드 public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { //기존 빛 관련 내용들.. //그림자가 그려질 그림자 텍스쳐 생성, 기존 빛의 설정(RGB 색상용의 desc) 사용 TextureHandle shadowTexture = UniversalRenderer.CreateRenderGraphTexture( renderGraph, desc, "_CustomShadowTexture", clear: false, filterMode: FilterMode.Bilinear ); //현재 카메라 설정을 가져옴 RenderTextureDescriptor depthDesc = cameraData.cameraTargetDescriptor; //생성할 텍스쳐를 색상(RGB)기반 대신, Depth/Stencil 기반 텍스쳐로 설정 depthDesc.colorFormat = RenderTextureFormat.Depth; depthDesc.depthBufferBits = 24; depthDesc.msaaSamples = 1; //방금 생성한 depthDesc를 사용하는 스텐실 버퍼용 텍스쳐 TextureHandle shadowDepthTexture = UniversalRenderer.CreateRenderGraphTexture( renderGraph, depthDesc, "_CustomShadowDepthTexture", clear: false, filterMode: FilterMode.Point ); //그림자용 래스터 패스 등록, 다음 파트에서 추가 설명 예정 using (var builder = renderGraph.AddRasterRenderPass<ShadowPassData>("Custom 2D Shadow Pass", out var passData)) } 텍스쳐를 생성하고, 패스를 등록하는 RecordRenderGraph 메소드 내부에 그림자 관련 코드들이 추가되었다. 그림자 처리를 위해 텍스쳐가 두개 생성되었는데, 첫번째로는 기존 빛 텍스쳐 생성에 사용됐던 desc 설정값을 이용한, 색상 처리용 그림자 텍스쳐이다. 이 그림자 텍스쳐에는 최종적으로 실제 그림자들이 그려지게 될 예정이다. 그 다음으로는 새롭게 카메라 설정을 가져와서, 색상 기반 대신 Depth/Stencil 기반의 텍스쳐가 될 수 있도록 설정해주었는데, 실제로는 뎁스는 사용하지 않고, 스텐실만 사용해줄 계획이다. 이렇게 설정된 depthDesc를 이용하여, 두번째 텍스쳐인 스텐실 버퍼용 텍스쳐를 생성해주는데, 이 두번째 텍스쳐는 그림자에서 현재 물체 범위를 제외해주기 위해 사용된다. 빛과 빛을 받는 객체에 대해 그림자 생성을 판단할 때, 가장 간단한 방법은 객체의 영역에서 빛을 받았을 때, 그 영역부터 빛의 방향에 따라서 쭉 그림자를 늘어뜨려 주는것이다. 다만 이렇게 되면 빛을 받은 객체 자신 또한 그림자에 휩싸여서 그림자의 색인 검은 계열의 색으로 물들게 된다. 이러한 현상을 방지하기 위해, 두번째 텍스쳐를 이용해서 현 객체에 맞도록 스텐실 버퍼를 생성해주고, 그림자 텍스쳐를 작업하면서 이 스텐실 버퍼 텍스쳐를 참고해서 현재 객체의 범위에는 그림자가 생성되지 않도록 예외처리를 진행해준다. 렌더패스 등록 //렌더패스 등록 using (var builder = renderGraph.AddRasterRenderPass<ShadowPassData>("Custom 2D Shadow Pass", out var passData)) { //작업이 진행될 각 텍스쳐들을 연결해줌 builder.SetRenderAttachment(shadowTexture, 0, AccessFlags.Write); builder.SetRenderAttachmentDepth(shadowDepthTexture, AccessFlags.Write); //렌더러들의 작업 순서 지정(카메라 기본 정렬 기준을 그대로 사용) SortingCriteria sortingCriteria = cameraData.defaultOpaqueSortFlags; //그림자를 생성할 렌더러 필터 설정 FilteringSettings filteringSettings = new FilteringSettings(RenderQueueRange.all, m_Settings.shadowLayerMask); //Custom2DOccluder 패스 태그를 이용한 DrawingSettings 생성 DrawingSettings occluderDrawSettings = RenderingUtils.CreateDrawingSettings( m_OccluderShaderTagId, renderingData, cameraData, lightData, sortingCriteria ); //설정들을 이용해서 첫번째 렌더러 리스트 생성 RendererListParams occluderParams = new RendererListParams(renderingData.cullResults, occluderDrawSettings, filteringSettings); passData.occluderRendererList = renderGraph.CreateRendererList(occluderParams); //Custom2DShadowCaster 패스 태그를 이용한 DrawingSettings 생성 DrawingSettings extrusionDrawSettings = RenderingUtils.CreateDrawingSettings( m_ShadowExtrusionShaderTagId, renderingData, cameraData, lightData, sortingCriteria ); //설정들을 이용해서 두번째 렌더러 리스트 생성 RendererListParams extrusionParams = new RendererListParams(renderingData.cullResults, extrusionDrawSettings, filteringSettings); passData.shadowExtrusionRendererList = renderGraph.CreateRendererList(extrusionParams); //렌더러 리스트 등록 builder.UseRendererList(passData.occluderRendererList); builder.UseRendererList(passData.shadowExtrusionRendererList); //그림자 텍스쳐 전역 등록 builder.SetGlobalTextureAfterPass(shadowTexture, CustomShadowTextureID); builder.AllowPassCulling(false); //실제 동작 등록, 다음 파트에서 추가 설명 예정 builder.SetRenderFunc((ShadowPassData data, RasterGraphContext context) } 렌더 패스는 커스텀 라이트 때의 흐름과 비슷하게 구성되어있다. 먼저 각 작업별 텍스쳐를 등록해주는데, SetRenderAttachment와 SetRenderAttachmentDepth로 등록 작업에 차이가 있는 것을 확인할 수 있다. 이는 렌더 패스에서 작업된 컬러 텍스쳐와 뎁스 텍스쳐를 의미하는것으로, 그에 맞춰 그림자 텍스쳐와 스텐실 버퍼용 텍스쳐를 각각 등록해주었다. 다음으로는 그림자 생성 필터링 설정과 DrawingSettings을 생성하고, 이런 설정들을 통해 각각 렌더러 리스트를 생성해주었다. 각 텍스쳐와 목표에 맞춰서, 그림자용 렌더러 리스트는 Custom2DShadowCaster 패스 태그를 의미하는 m_ShadowExtrusionShaderTagId를, 스텐실용 렌더러 리스트는 Custom2DOccluder 패스 태그를 의미하는 m_OccluderShaderTagId를 지정해서 생성해주었다. 이렇게 생성된 두 리스트를 각각 패스에 등록해주고, 그림자 텍스쳐를 다른 위치에서도 호출할 수 있게 전역으로 등록해준 다음, 작업이 자동으로 제외되지 않게 AllowPassCulling 설정을 해주며, 실제 동작 파트로 넘어간다. builder.SetRenderFunc((ShadowPassData data, RasterGraphContext context) => { //렌더 타겟 전부 초기화(색상은 블랙, 깊이 버퍼는 1, 스텐실 버퍼는 0) context.cmd.ClearRenderTarget(RTClearFlags.All, Color.black, 1.0f, 0); //스텐실 버퍼 기록 context.cmd.DrawRendererList(data.occluderRendererList); //스텐실 버퍼 영역 확인 후, 영역 외에 그림자 영역 지정 context.cmd.DrawRendererList(data.shadowExtrusionRendererList); }); 작업은 총 3단계로, 렌더 타겟을 전부 깔끔하게 초기화 하며 시작한다. 초기화 후, 두번째 작업은 먼저 스텐실 버퍼와 관련된 작업으로, 간단하게 요약하면 오브젝트 범위에 맞춰서 스텐실 버퍼에 데이터를 등록해준다. 마지막인 세번째 작업 또한 간단하게 요약하면, 스텐실 버퍼를 확인해서 그림자 텍스쳐에 그림자 영역을 그려주도록 되어있다. 각 동작들은 셰이더쪽 코드를 통해 동작하기 때문에 자세한건 각 파트에서 설명할 예정이다. 커스텀(그림자) 셰이더 이번 셰이더는 기존 빛 셰이더와 다르게, 두개의 패스로 구성되어있다. 셰이더 공용 코드 Shader "Universal Render Pipeline/2D/Custom2DShadow" { //입력받는 설정값 Properties { //그림자로 이용할 텍스쳐, 그림자의 길이, 컷오프 알파값 기준 _MainTex ("Sprite Texture", 2D) = "white" {} _ShadowLength ("Shadow Extrusion Length", Float) = 5.0 _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" } //블렌딩, 컬링, 깊이처리 전부 Off Blend Off Cull Off ZWrite Off } //계속해서 설명할 두 패스 Pass {...} Pass {...} } } 이번 셰이더의 공용 코드는 위와 같다. 머티리얼을 통해 전달받는 값은 그림자로 이용할 텍스쳐와, 만들어질 그림자의 길이, 컷 오프 될 알파값 기준이 존재한다. 텍스쳐에 애초에 알파값이 낮아 반투명, 혹은 투명한 부분에 대해 그림자가 생성되면 어색하기 때문에 그에 대한 예외처리를 위해 컷오프 기준값 설정을 전달받는다. 첫번째 패스(스텐실 마스킹) Pass { Name "Custom2DOccluder" //Custom2DOccluder를 통한 호출인 경우 실행되는 패스 Tags { "LightMode" = "Custom2DOccluder" } //렌더 타겟에 색상을 그리지 않음을 의미 ColorMask 0 //스텐실 버퍼 설정 Stencil { Ref 0 Comp Always //항상 통과 Pass IncrSat //스텐실 값 1 증가 } HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/Core2D.hlsl" #pragma vertex LitVertex #pragma fragment LitFragment #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" //버텍스 셰이더에서 사용 struct Attributes { //오브젝트의 버텍스 위치 float3 positionOS : POSITION; }; //픽셀 셰이더에서 사용 struct Varyings { //버텍스 셰이더에서 처리한 좌표 전달용 float4 positionCS : SV_POSITION; }; //버텍스 셰이더 Varyings LitVertex(Attributes input) { Varyings output; //오브젝트 기반 정점의 좌표를 클립 기반 좌표로 변경해서 전달 output.positionCS = TransformObjectToHClip(input.positionOS); return output; } //픽셀 셰이더는 ColorMask 0으로 인해 의미가 없는 상태 half4 LitFragment(Varyings input) : SV_Target { return 0; } ENDHLSL } 첫번째 패스의 목표는 스텐실 버퍼에 현재 오브젝트의 모양을 기록해두는 것이 목표이기 때문에 별 다른 작업은 진행되지 않는다. 가장 중요한건 스텐실 영역으로, 현재 스텐실 버퍼는 렌더 타겟 초기화로 인해 전부 0으로만 채워져있는 상태이다. 이 셰이더 기반 오브젝트를 렌더링하면서, 이번 패스가 동작할 때, 스텐실 버퍼에 오브젝트 범위에 맞게 1이 채워지게 된다. 광원 좌표 전달 public class CustomLightBinder : MonoBehaviour { private static readonly int CustomLightPosWSID = Shader.PropertyToID("_CustomLightPosWS"); void Update() { // 현재 광원 오브젝트의 월드 좌표를 전역 셰이더 변수로 업데이트 Vector4 lightPosWS = transform.position; Shader.SetGlobalVector(CustomLightPosWSID, lightPosWS); } } 두번째 패스로 넘어가기 전에 짧은 스크립트를 하나 봐야한다. 이 스크립트는 광원에 붙게 되는 스크립트인데, 단순하게 매 프레임 광원의 위치를 계속해서 업데이트 해주는 역할을 담당한다. 광원의 위치는 다음에 설명할 두번째 패스에서 사용된다. 두번째 패스(그림자 마스킹) Pass { Name "Custom2DShadowCaster" //Custom2DShadowCaster를 통한 호출인 경우 실행되는 패스 Tags { "LightMode" = "Custom2DShadowCaster" } //스텐실 버퍼, 현재 픽셀의 스텐실이 1이 아닌 경우에만 작업을 적용한다는 의미 Stencil { Ref 1 Comp NotEqual } HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/Core2D.hlsl" #pragma vertex LitVertex #pragma fragment LitFragment #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" //메인 텍스쳐와 샘플러 선언 TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); //CustomLightBinder 스크립트에서 공유중인 실시간 광원 위치 float4 _CustomLightPosWS; //텍스쳐(타일링 / 오프셋), 그림자 길이, 컷오프 기준값 CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; float _ShadowLength; float _Cutoff; CBUFFER_END //버텍스 셰이더에서 사용 struct Attributes { float3 positionOS : POSITION; float2 uv : TEXCOORD0; //이 버텍스가 어느 인스턴스(객체)에 속해있는지 구별하기 위한 값 UNITY_VERTEX_INPUT_INSTANCE_ID }; //픽셀 셰이더에서 사용 struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; Varyings LitVertex(Attributes input) { Varyings output; //현 버텍스의 인스턴스(객체) 정보 세팅 UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); //광원 위치를 오브젝트 공간(Object Space)으로 변환 float3 lightPosOS = TransformWorldToObject(_CustomLightPosWS.xyz); //광원에서 오브젝트 중심(0,0,0)으로 향하는 방향 벡터 //(0, 0, 0) - lightPosOS.xy = -lightPosOS.xy float2 lightToCenterDirOS = -lightPosOS.xy; //광원과 오브젝트 중심 위치가 완전히 겹쳐 0이 될 때의 예외 처리 float dirLen = length(lightToCenterDirOS); if (dirLen < 0.0001) { lightToCenterDirOS = float2(0, 1); //기본 방향 지정 } //두 벡터에 대한 내적 진행(오브젝트 중심 -> 현재 정점 위치, 광원 -> 오브젝트 중심 float facing = dot(input.positionOS.xy, lightToCenterDirOS); //빛을 받는 방향을 구함 float3 positionWS = TransformObjectToWorld(input.positionOS); float2 lightToVertDirWS = positionWS.xy - _CustomLightPosWS.xy; float dist = length(lightToVertDirWS); float2 lightToVertNormWS = lightToVertDirWS / max(dist, 0.0001); //광원 반대편 정점(facing > 0)만 Extrusion 수행 if (facing > 0.0) { //빛을 받는 방향에 맞춰서 그림자 거리 설정만큼 밀어내기 positionWS.xy
문제 요약 중복 없는 학습 단어들이 주어졌을 때, 각 단어를 다른 단어와 구분해 자동완성하려면 몇 글자를 입력해야 하는지 구한다. 모든 단어에 필요한 입력 글자 수의 합을 반환한다. 핵심 아이디어 단어를 사전순으로 정렬하면, 어떤 단어와 가장 긴 접두사를 공유할 수 있는 단어는 정렬된 목록에서 바로 앞 또는 바로 뒤에 있다. 따라서 현재 단어가 필요한 입력 글자 수는 다음처럼 구할 수 있다. max(앞 단어와의 공통 접두사 길이, 뒤 단어와의 공통 접두사 길이) + 1 단, 현재 단어 자체가 다른 단어의 접두사라면 더 입력할 문자가 없으므로 단어 전체를 입력해야 한다. required = min(len(word), longest_common_prefix + 1) 왜 인접한 단어만 비교할까? 사전순 정렬에서 같은 접두사를 가진 단어들은 항상 연속해서 모인다. 예를 들어 word 와 wor... 로 시작하는 단어들은 모두 한 구간에 모인다. 현재 단어와 가장 긴 접두사를 공유하는 단어는 그 구간에서 바로 앞이나 바로 뒤에 있으므로, 두 이웃만 비교하면 충분하다. 풀이 과정 단어 목록을 사전순으로 정렬한다. 각 단어와 앞 단어의 최장 공통 접두사 길이를 구한다. 각 단어와 뒤 단어의 최장 공통 접두사 길이를 구한다. 둘 중 큰 값에 1을 더한다. 현재 단어 길이를 넘지 않도록 제한한 값을 답에 더한다. Python 코드 def common_prefix_length(first, second): limit = min(len(first), len(second)) index = 0 while index < limit and first[index] == second[index]: index += 1 return index def solution(words): words.sort() total = 0 for index, word in enumerate(words): longest_common_prefix = 0 if index > 0: longest_common_prefix = max( longest_common_prefix, common_prefix_length(word, words[index - 1]) ) if index + 1 < len(words): longest_common_prefix = max( longest_common_prefix, common_prefix_length(word, words[index + 1]) ) # word가 다른 단어의 접두사인 경우에는 단어 전체를 입력해야 한다. total += min(len(word), longest_common_prefix + 1) return total 예시 words = ["go", "gone", "guild"] 는 이미 사전순으로 정렬되어 있다. 단어 이웃 단어와의 가장 긴 공통 접두사 필요한 입력 go go (gone과 공유) 2 gone go 3 guild g 2 go 는 gone 의 접두사다. 공통 접두사 길이는 2이지만 go 뒤에 입력할 글자가 없으므로, 단어 전체인 2글자를 입력해야 한다. 총 입력 글자 수는 2 + 3 + 2 = 7 이다. 시간 복잡도 N 을 단어 개수, L 을 모든 단어 길이의 합, M 을 가장 긴 단어 길이라고 하자. 단어 정렬: 최대 O(N log N * M) 인접 단어 공통 접두사 계산: 각 단어는 앞뒤 단어와만 비교하므로 O(L) 시간 복잡도: O(N log N * M + L) 공간 복잡도: 정렬 구현을 포함해 O(N) 정리 자동완성에 필요한 글자 수는 해당 단어와 가장 비슷한 다른 단어를 구분할 수 있는 위치로 결정된다. 사전순 정렬 후 앞뒤 단어의 공통 접두사만 비교하면, 트라이 없이도 필요한 총 입력 횟수를 구할 수 있다.
📋 STEP1. AI 활용 자가진단 나는 ChatGPT를 내 상황에 맞는 결과물을 만드는 ‘도구형’ 으로 활용하는 편이다. 짧게 질문을 시작하더라도 배경과 조건을 추가하고 답변을 수정하면서, 지원서 작성이나 계획 수립 등에 활용한다. 앞으로는 처음부터 목표와 원하는 출력 형식을 명확히 전달하고, 답변의 사실 여부와 근거를 확인하는 데도 더 신경 쓰려고 한다. 🤔 STEP2. "지식인형 vs 도구형" 프롬프트 비교 실습 차이: 일반적인 공부 방향을 묻는 질문에서, 실제 수강할 과목을 비교하고 선택하는 요청으로 구체화 하였으며, SQL프로그래밍 과목을 추가로 알려주면서, 추천이 웹서비스와애플리케이션과 SQL프로그래밍 중심으로 조정됨. 🙋🏻 STEP3. 내 전공 맥락으로 도구형 질문 직접 만들기 작곡을 전공한 경험을 바탕으로, 영상디자인과 BGM을 결합한 포트폴리오를 주제로 질문을 만들었다. AI에게 포트폴리오를 지도하는 멘토 역할을 맡기고, 혼자 제작할 수 있는 1분 영상 기획안 3가지를 요청했다. 답변은 주제, 장면 구성, BGM 방향, 필요한 작업, 보여줄 수 있는 역량을 표로 정리하도록 했다. 마지막으로 첫 작품으로 만들기 적합한 기획안과 추천 이유도 함께 물었다. 💻 STEP 4. 결과 정리 1️⃣ 자가진단에서 체크된 항목 수 총 1개로, ‘한 줄로 물어보는 편이다’에 체크했다. 2️⃣ 가장 차이가 컸던 예시 번호와 이유 실제 사례 1 ‘백엔드 선수학습을 위한 수강과목 선택’이다. 미션 안내의 예시 3에 해당한다. 학습 목표와 선택 가능한 과목을 구체적으로 알려주자, 추천이 ‘웹서비스와애플리케이션’과 ‘SQL프로그래밍’ 중심으로 정리됐다. 내 상황에 맞게 과목을 선택하는 데 활용할 수 있다는 점에서 차이가 컸다. 3️⃣ 내가 만든 도구형 질문 너는 영상디자인 포트폴리오를 지도하는 멘토야. 나는 작곡을 전공한 경험이 있고, 영상디자인과 음악을 결합한 포트폴리오를 만들고 싶어. 영상디자인과 BGM 작곡 역량을 함께 보여줄 수 있는 1분 길이의 포트폴리오 영상 기획안 3가지를 제안해줘. 혼자 제작할 수 있는 규모로 구성하고, 영상과 음악이 어떻게 어우러지면 좋을지도 설명해줘. 각 기획안은 ‘주제 / 장면 구성 / BGM 방향 / 필요한 작업 / 보여줄 수 있는 역량’ 순서의 표로 정리해줘. 마지막에는 첫 작품으로 만들기 적합한 기획안 하나를 골라 이유도 알려줘. 4️⃣ GPT에게 받은 답변 중 가장 유용했던 부분 먼저 60초 BGM을 만들고, 음악의 변화 지점에 맞춰 화면을 구성하라는 조언이 가장 유용했다. 💪🏻 보너스 미션 총평! 이번 미션을 통해 내가 평소 ChatGPT를 어떻게 쓰고 있는지 돌아볼 수 있었다. 질문은 짧게 시작하는 편이지만, 대화하면서 조건을 추가하고 답변을 수정해 필요한 결과물을 만들어가고 있었다. 짧은 질문이라도 앞선 대화의 맥락과 목적이 담겨 있을 수 있다는 점도 생각해봤다. 작곡 경험을 바탕으로 영상 포트폴리오 기획안을 요청하는 과정에서는 역할, 배경, 구체적인 요청, 출력 형식을 정해주는 방법을 적용해봤다. 장면 구성과 BGM 방향, 필요한 작업까지 정리된 답변을 보니 무엇부터 시작하면 좋을지 이해하기 쉬웠다. 앞으로도 내 상황과 목표를 분명하게 전달하고, AI가 준 답변은 근거와 실현 가능성을 확인하면서 내 판단으로 선택하고 수정해가려고 한다.
문제 요약 주어진 단어 조각을 원하는 만큼 사용해 문자열 t 를 완성한다. 문자열을 완성하는 데 필요한 단어 조각 수의 최솟값을 구하고, 만들 수 없다면 -1 을 반환한다. 핵심 아이디어 dp[i] 를 t 의 앞에서부터 i 글자까지 완성하는 데 필요한 최소 조각 수라고 정의한다. dp[0] = 0 어떤 위치 i 까지 만들 수 있고, 그 위치부터 시작하는 조각 piece 가 t 와 일치한다면 다음 위치를 갱신한다. dp[i + len(piece)] = min( dp[i + len(piece)], dp[i] + 1 ) 각 조각을 무한히 사용할 수 있으므로, 같은 조각을 여러 위치에서 반복해서 사용해도 된다. 풀이 과정 길이가 len(t) + 1 인 DP 배열을 만들고, 도달할 수 없는 값은 큰 값으로 초기화한다. dp[0] = 0 으로 시작한다. 문자열의 각 위치에서, 해당 위치부터 일치하는 모든 단어 조각을 확인한다. 일치하는 조각의 끝 위치를 최소 조각 수로 갱신한다. dp[len(t)] 가 여전히 큰 값이면 -1 을 반환한다. Python 코드 def solution(strs, t): length = len(t) infinity = length + 1 # dp[i]: t의 앞 i글자를 만드는 데 필요한 최소 조각 수 dp = [infinity] * (length + 1) dp[0] = 0 for start in range(length): if dp[start] == infinity: continue for piece in strs: end = start + len(piece) if end <= length and t.startswith(piece, start): dp[end] = min(dp[end], dp[start] + 1) return -1 if dp[length] == infinity else dp[length] 예시 strs = ["ba", "na", "n", "a"] , t = "banana" 인 경우를 보자. dp[0] = 0 "ba" 사용 -> dp[2] = 1 "na" 사용 -> dp[4] = 2 "na" 사용 -> dp[6] = 3 따라서 "ba" + "na" + "na" 로 문자열을 만들 수 있고, 필요한 조각 수는 3 이다. 시간 복잡도 N 을 t 의 길이, S 를 단어 조각 개수, L 을 조각의 최대 길이라고 하자. 각 위치에서 모든 조각을 확인하고, 문자열 비교는 최대 L 글자를 확인한다. 시간 복잡도: O(N * S * L) 공간 복잡도: O(N) 이 문제에서는 N <= 20,000 , S <= 100 , L <= 5 이므로 충분히 빠르게 동작한다. 정리 문자열을 앞에서부터 완성하는 최소 비용 문제로 바꾸면 된다. dp[i] 가 도달 가능한 위치인지 확인하고, 그 위치에 이어 붙일 수 있는 단어 조각으로 다음 상태를 갱신한다.
부제: 나보다 훨씬 똑똑하다고 믿었던 모델이 전원 버튼 하나에 막혔다. 그 시행착오를 줄여 측정을 자동화하기까지 서두 windowinsets.info 를 만들고 있다. Android 기기별 상태 표시줄, 내비게이션 바, 제스처 영역, 화면 잘림과 폴더블 힌지 정보를 그림으로 보여 주는 사이트다. 개발자가 자기 화면에서 콘텐츠가 어디까지 안전하게 놓일 수 있는지 살펴볼 수 있게 하려는 프로젝트다. 이 값은 기종과 화면, 회전 방향, 내비게이션 방식에 따라 달라진다. 그래서 작은 Android 앱인 InsetsProbe 를 만들어 실기기와 Samsung Remote Test Lab(RTL)에서 값을 JSON으로 기록해 왔다. 기기가 많아질수록 측정 코드보다 기기를 조작하고 파일을 내 컴퓨터까지 옮기는 과정 이 더 큰 일이 됐다. 처음에는 당시 사용자에게 제공된 OpenAI의 최상위 GPT-6 모델, Astra에게 RTL 기기를 맡기면 이 반복 작업이 빨리 끝날 줄 알았다. 그런데 검은 화면을 깨울 사이드 버튼을 못 누르고, 설정을 한 번 스크롤하면 메뉴를 다시 찾느라 헤맸다. JSON을 저장한 뒤에는 RTL File Browser에서 내려받는 일도 오래 걸렸다. 어제까지만 해도 내가 직접 파일을 받아 Astra에게 넘기는 편이 빨랐다. 삼성 기기만 약 120종이다. 가로 화면을 추가하려면 이 기기들을 다시 예약해 같은 일을 반복해야 하는데, Pixel 지원까지 더하면서 측정 대상은 더 늘어났다. 수작업으로 파일을 모으는 방식은 도저히 감당할 수 없었다. 이 글은 그 병목을 겪고, Probe에서 GitHub PR까지 직접 업로드하는 흐름을 만든 기록이다. 마지막에는 가로 90°와 270°를 각각 측정해야 하는지 S23+와 Fold8·Flip8에서 먼저 측정한 결과로 판단했다. 문제 1. 검은 화면에서 사이드 버튼을 누르지 못했다 문제 발생 RTL WebClient에 Probe APK를 설치해도 기기 화면이 검게 보일 때가 있었다. 화면을 켜려면 앱 안의 버튼이 아니라 WebClient가 그린 휴대폰 프레임의 사이드 버튼 을 눌러야 했다. 브라우저 메뉴는 접근성 트리에서 이름을 찾을 수 있지만, 원격 Android 화면과 기기 프레임은 주로 픽셀로 보인다. Astra는 스크린샷에서 버튼처럼 보이는 돌출부를 찾아 좌표를 누르고, 다음 스크린샷에서 성공했는지 다시 확인해야 했다. Fold8의 검은 화면. 눌러야 할 사이드 버튼은 앱 화면 밖, RTL이 그린 기기 프레임에 있다. 문제는 버튼을 알고도 클릭이 실제 기기에 전달됐는지 확신하기 어려웠다는 점이다. Fold8과 Flip8을 시험할 때도 Astra가 버튼을 눌러 보았지만 화면은 계속 검었다. 내가 화면을 깨우고 잠금을 풀어 준 뒤에야 다음 단계로 갈 수 있었다. 혹시 Claude의 최상위 모델인 Opus 5.5 는 다를까 싶어 Fold7 RTL에서 APK 설치와 잠금 해제도 맡겨 봤다. 하지만 업로드와 클릭이 뜻대로 되지 않아 결국 내가 직접 두 단계를 끝냈다. 이번에는 모델을 바꿔도 원격 기기 조작의 어려움이 그대로였다. 그래도 응답은 Opus 쪽이 좀 더 빠릿빠릿하게 느껴져 마음에 들었다. Astra와 GPT도 이 부분은 분발했으면 싶다. Opus 5.5로도 업로드와 클릭이 되지 않아 내가 설치와 잠금 해제를 마친 뒤 이어서 작업했다. 문제 해결 이번 측정에서는 Probe APK 설치, 사이드 버튼, 잠금 해제, Flip8 커버 위젯 등록과 실행에 내 손이 필요했다. 기기가 측정할 화면에 앱을 띄운 뒤에야 Astra가 Probe의 측정과 업로드를 이어갈 수 있었다. Flip8 커버 화면은 특히 손이 많이 갔다. 내가 사이드 버튼을 눌러 화면을 켜 줘도, Astra가 다음 스크린샷을 확인하고 클릭할 좌표를 정하는 동안 화면이 다시 꺼졌다. 위젯을 등록한 뒤에는 커버 화면을 좌우로 스와이프해 해당 위젯을 찾아 실행해야 했는데, 이 조작도 Astra가 안정적으로 끝내지 못해 내가 직접 스와이프하고 위젯을 눌렀다. 문제 2. 설정을 스크롤하자 이전 스크린샷이 틀렸다 문제 발생 WindowInsets는 제스처와 3버튼 내비게이션에서 값이 달라진다. 두 모드를 측정하려면 Android 설정에서 내비게이션 방식을 바꿔야 한다. 그런데 Astra가 스크린샷에서 찾은 메뉴 위치는 한 번 스크롤한 뒤 더 이상 맞지 않았다. 사람은 목록이 움직이는 동안 같은 항목을 눈으로 따라간다. Astra의 조작은 화면 A를 보고 판단 → 스크롤 → 화면 B를 다시 관찰 하는 단계로 끊긴다. B를 확인하지 않고 A의 좌표로 누르면 엉뚱한 메뉴를 열게 된다. Flip8 설정 메뉴를 스크롤하기 전과 후. 다음 클릭은 반드시 바뀐 화면에서 다시 찾아야 했다. 처음에는 앱에서 Settings.Secure.putInt(navigation_mode, ...) 로 모드 변경도 시도했다. 호출 경로의 오류를 고쳐도 Samsung RTL 기기는 요청을 적용하지 않았다. 새 JSON은 계속 3버튼 모드였다. 문제 해결 Probe에 Display / navigation settings 버튼과 × 종료 버튼 을 넣어 설정 앱으로 이동하기 쉽게 했다. 설정을 스크롤한 뒤에는 반드시 새 화면에서 항목을 다시 찾도록 했다. 모드 변경 여부는 요청 코드가 아니라 Android 설정과 새 캡처의 navigation.mode 로 판단한다. 이번 시험 측정에서는 3버튼 모드의 Fold8·Flip8 데이터를 우선 모았다. 제스처로 바꾸지 않은 캡처를 제스처 데이터라고 부르지 않았다. 문제 3. 파일은 저장됐는데 내려받기가 오래 걸렸다 문제 발생 기존에는 RTL File Browser에서 Android/data/info.windowinsets.probe/files 로 들어가 JSON을 내려받았다. 파일 행에 마우스를 올려야 다운로드 아이콘이 나타났다. 호스트에 도착한 이름도 main-gesture.json 대신 content , content (1) 처럼 바뀌었다. Flip8 RTL File Browser. JSON 파일 행에 마우스를 올려야 오른쪽에 다운로드 아이콘이 나타났다. 다운로드를 눌렀다는 사실과 파일이 내 컴퓨터에 도착했다는 사실도 별개였다. Flip3에서는 기기에 저장된 제스처 JSON을 끝내 받지 못해 다음 예약에서 다시 측정했다. 한 파일씩 열어 모델, 화면, 모드와 크기를 확인하는 수작업도 계속됐다. 문제 해결 InsetsProbe가 JSON을 /api/captures 로 직접 보내게 했다. Vercel 함수가 요청을 받아 GitHub의 capture-inbox 브랜치에 원본을 커밋한다. 업로드마다 PR을 만들지 않고 여러 캡처를 하나의 PR 에 모은다. 병합 여부는 내가 결정하고, 병합할 때는 스쿼시 커밋으로 묶는다. 첫 프리뷰에서는 API가 404였고, 배포 뒤에는 ECMAScript Modules(ESM) import 오류로 500이 났다. 이를 고친 다음 GitHub PAT와 업로드 키를 Production에 넣었다. 기존 S24+ JSON 재전송에서 HTTP 201과 PR #30 의 커밋을 확인했다. 여기까지는 업로드 경로만 시험한 것이었다. 이후 Fold8과 Flip8을 실제 RTL에서 예약해 APK를 설치했다. Probe 버튼을 눌러 만든 새 JSON이 같은 PR에 도착하는 것까지 확인했다. Fold8 커버와 Flip8 메인은 세로·가로 양방향 캡처가 올라왔다. 이제 RTL File Browser를 거치지 않고, Probe가 측정한 JSON을 직접 전송한다. 문제 4. 업로드가 성공해도 화면 라벨은 틀릴 수 있었다 문제 발생 Fold8의 과거 main JSON은 1248 × 1972px였다. 이 크기는 내측이 아니라 커버 화면과 맞는다. 예전 Measure All 의 Cover/Main 선택은 실제 디스플레이를 바꾸지 않고 파일 라벨만 바꿨다. 실제 기기를 다시 측정하면서도 비슷한 실수를 했다. Fold8을 펼친 직후 Cover 라벨을 그대로 둔 채 한 번 측정해, 실제 2448 × 1848px 내측 화면이 cover-threeButton.json 으로 올라갔다. API 성공은 전송 성공 이지, 화면 분류가 맞다는 뜻이 아니었다. 문제 해결 폴더블 화면은 RTL의 접힘 제어로 실제 전환하고, Probe가 보고한 창 크기와 디스플레이를 확인한다. Fold8 커버는 1248 × 1972px, 내측은 2448 × 1848px로 구분했다. 잘못 표기된 원본은 고치지 않고 증거로 남기되, 유효한 커버 측정으로 사용하지 않는다. Probe의 회전 스윕도 화면이 실제로 돌아갔을 때만 파일을 저장한다. Flip8 커버 위젯은 Display 1에서 948 × 1048px 세로 캡처를 업로드했지만, 가로 요청은 적용되지 않아 가로 파일을 만들지 않았다. Flip8 커버 화면에서 직접 측정하고 업로드했다. 기기가 허용하지 않은 가로 값은 채우지 않았다. 문제 5. 가로 90° 하나를 뒤집어 270°로 쓸 수 있을까? 문제 발생 가로 데이터를 다시 모을 때 가장 궁금했던 건 이거였다. 90°와 270°가 같은 값이라면 하나만 찍고 뒤집어 쓰면 된다. 다르다면 삼성 기기 약 120종에 더해 Pixel 기기까지 두 방향을 각각 측정해야 한다. S23+ 바형, Fold8 커버, Flip8 메인을 같은 3버튼 조건으로 비교했다. 세 기기 모두 화면 크기는 같았지만, 상태 표시줄은 두 방향 모두 위쪽 에 남았다. 내비게이션 바와 카메라 잘림 영역은 오른쪽에서 왼쪽으로 옮겨 갔다. 기기 90° 상태 표시줄 / 내비게이션 바 / 잘림 영역 270°에서 달라진 점 S23+ 위 84px / 오른쪽 135px / 왼쪽 74px 상태 표시줄은 위 84px, 나머지는 좌우 반대 Fold8 커버 위 79px / 오른쪽 126px / 왼쪽 104px 상태 표시줄은 위 79px, 나머지는 좌우 반대 Flip8 메인 위 90px / 오른쪽 144px / 왼쪽 108px 상태 표시줄은 위 90px, 나머지는 좌우 반대 90° 캡처를 180° 돌리면 상태 표시줄까지 아래로 가므로 270° 실측값과 틀린다. 이 세 사례에서는 좌우 반전이 맞았지만, 모두 3버튼 모드였다. 제스처 모드와 다른 기종까지 같은 규칙이 적용된다고 단정할 수는 없다. 문제 해결 90°와 270°는 각각 실측한다 고 결정했다. Probe의 회전 스윕은 한 번 누르면 두 방향을 연달아 시도한다. 별도 예약을 한 번 더 할 필요는 없다. 기기가 회전을 허용하지 않으면 그 방향은 저장하지 않고 미측정으로 남긴다. 이렇게 해야 사이트에서 보여 주는 값이 실제 기기에서 나온 측정인지 분명해진다. 좌우 반전으로 만든 그림이 필요하더라도 실측한 270° 데이터인 것처럼 표시하지 않는다. 결론 한 대만 보면 내가 직접 버튼을 누르고 파일을 받는 편이 빨랐다. 하지만 삼성 기기만 약 120종이고 Pixel까지 지원 범위를 넓히면서, 화면·방향·내비게이션 조합을 손으로 모으는 일은 감당하기 어려워졌다. 그래서 사람이 기기를 준비한 뒤 Probe가 가능한 회전을 측정하고 JSON을 전송하는 흐름 으로 바꿨다. Fold8·Flip8 실기기에서 Probe → Vercel → GitHub PR까지 확인했다. 90°와 270°는 단순 회전으로 같은 값이 되지 않아 둘 다 실측하기로 했다. APK 설치와 사이드 버튼·잠금 해제, Flip8 커버 위젯 등록·실행에는 여전히 사람의 도움이 필요했다. 잘못 고른 화면 라벨도 업로드 전에 자동으로 막지 못했다. 원본 파일은 PR #30에서 스쿼시 머지했다. 이 머지는 증거 보관 단계이며, 사이트에 방향별 실측값을 연결하는 작업은 별개다. Astra가 간단한 버튼에서 헤맨 이유도 조금은 알 것 같다. 웹 메뉴, 원격 화면, 기기 프레임은 조작 방식이 달랐다. 특히 스크린샷 한 장은 스크롤이나 화면 꺼짐 뒤의 상태를 보장하지 않는다. 다음 행동 전에 현재 화면을 다시 확인하는 절차가 필요했다. P.S. 인간 데이터 수집기를 졸업하며 내가 직접 파일을 받아 넘기는 편이 빠르다고 느낀 게 불과 어제였다. 오늘은 그 일을 없애려고 코드를 고쳤다. 친구와 얘기하다 보니, 내가 하던 자리를 내가 줄이고 있다는 생각도 들었다. 작업하면서 기기 자체에 대해서도 새로 알았다. Flip8은 APK를 설치했다고 커버 화면에서 앱이 바로 뜨지 않았다. 접은 채 RTL의 Applications에서 실행하면 숨겨진 메인 화면에 열리기도 했다. 결국 FlexWindow용 위젯 을 만들어 커버 디스플레이에서 앱을 열고, 실제 화면 크기까지 확인해야 했다. 위젯을 만드는 것과 커버 화면에서 위젯을 등록하고 찾아 실행하는 것은 또 다른 일이었다. 180° 회전도 뜻밖이었다. 내가 확인한 Galaxy 바형 폰과 Fold·Flip의 일반 화면은 자동 회전만으로 세로 화면을 거꾸로 돌리지 않았다. Fold를 펼쳐도 이 점은 태블릿과 달랐다. 반면 Galaxy Tab은 거꾸로 든 세로 방향도 지원했다. 여기서 180°는 힌지 각도가 아니라 화면 방향이다. 이런 예외를 모르고 파일만 쌓았다면, 숫자는 늘어도 믿을 수 있는 데이터는 늘지 않았을 것이다. 이 고민에 깔끔한 답은 없을 것 같다. 그래도 이 프로젝트를 시작한 이유는 분명하다. 내가 겪은 번거로움을 다른 사람은 덜 겪게 하는 제품을 만들고 싶었다. 이번에는 그 ‘다른 사람’에 미래의 나도 들어간다. 손으로 파일을 옮기는 시간 대신 화면 데이터를 더 잘 설명하는 데 시간을 쓰고 싶다. 참고 자료 windowinsets.info 프로젝트 저장소와 InsetsProbe 캡처 업로드 구현 PR #29 실제 RTL 업로드가 모인 PR #30 Android setRequestedOrientation()
2026년 9월 28일 ~ 10월 4일 코드잇 9주차 이번주 할일 미션6 제출(제출 마감 : 10월 2일) 스터디 - 이미지 EDA 스터디 자료 만들기 스터디 발표(9월 29일 3시) 멘토링(9월 28일) 질문 적어두기 위클리 페이퍼 #8 (9월 30일) 이번주 다짐 저번주는 추석 연휴라서 흐지부지 지나갔햄..😭 이번주는 제대로 계획 세워서 못하거나 안하는거 없게 해보자햄..!!🐹✨ 9월 28일 오늘 할일 미션6 진행 계획 세우기 데이터 불러오기 Baseline 전체적인 흐름 정리 진행 방향 잡기 < 제출 안내 내용 > 1. 분석 과정과 결과 데이터 로드, 전처리, 모델 학습, 예측, 성능 평가 등의 모든 과정을 포함해야 합니다. 사전 학습된 모델을 활용한 Transfer Learning을 적용해 보세요. Fine-Tuning을 적용해 보세요. Frozen 모델, Partial Fine-Tuning, Full Fine-Tuning을 비교하며 실험을 진행해 봅시다. 모델별 성능 비교, 분석 결과를 코드와 함께 정리해 주세요. 2. 마크다운을 활용한 설명 코드의 각 단계에서 어떤 작업을 수행하는지, 어떤 의도를 가지고 접근했는지 명확히 표현할 수 있도록 마크다운을 적극 활용해 주세요. 코드와 실행 결과를 설명하는 문구를 추가하여, 전체 코드의 흐름을 이해할 수 있도록 작성해 주세요. 보고서를 따로 작성하지 않으므로, 노트북 파일 내에 모든 설명이 잘 드러나야 합니다. 3. 모델 성능 평가 및 제출 평가 지표(Accuracy, Precision, Recall, F1-score 등)를 활용해 모델 성능을 분석하고 비교해 보세요. 제공된 데이터셋의 테스트 파일을 사용하여 모델을 테스트해 보세요. 모델별 성능 평가 결과를 포함한 노트북 파일을 제출하세요. 멘토링(7시~) 회고 적기 질문 적기 자료 적기 스터디 자료 만들기 오늘 회고 및 일기
JavaScript의 작업 관리 방식 자바 스크립트는 싱글 스레드 로 동작한다. 즉, 한 번에 한 가지 일만 할 수 있는 언어이다. 그래서 만약 오래 걸리는 작업을 동기적으로 실행하면 다른 작업이 블로킹되는 문제가 발생할 수 있다. 그러나 실제 웹 어플리케이션은 I/O 관리, 네트워크 요청, 타이머 등 여러 가지 동작을 동시에 처리해야 하는 경우가 많다. 한 번에 한 가지 일만 처리할 수 있다고 했지만, 실제 웹앱에서는 여러 가지 작업이 동시에 진행 된다. (ex) 타이머를 기다리면서 입출력 관리, api를 받아오면서 버튼 이벤트 진행 등) 자바 스크립트는 이러한 동시성(Concurrency) 이슈를 어떻게 해결하고 있을까? 그 답은 바로 ‘ 이벤트 루프(Event Loop) ’에 있다. 이벤트 루프의 구성 요소와 동작 과정에 대해 살펴보자. 이벤트 루프(Event Loop) 이벤트 루프는(Event Loop)는 브라우저 내부의 Call Stack, Web APIs, Callback Queue 등의 요소들을 모니터링하고 제어하는 런타임 루프이다.(like 관리자) 런타임 요소 1. Memory Heap 객체가 저장되는 메모리 공간 실행 컨텍스트는 이 메모리 힙에 저장된 객체를 참조하여 값을 가져오게 된다. 2. Call Stack 실행중인 작업의 실행 컨텍스트가 쌓이는 스택 LIFO(Last In Final Out) 구조이다. 3. Web API 네트워크 요청, 파일 입출력, 타이머 등 브라우저에서 제공하는 다양한 API를 말한다. 이벤트 루프에서의 Web API는 비동기 작업들을 전담하여 처리한다. Web API는 브라우저(Chrome)에서 멀티 쓰레드로 구현되어 있기 때문에, 비동기 작업을 처리할 수 있다. ❗모든 Web API가 비동기적으로 실행되는 것은 아니다! 동기적/비동기적으로 처리되는 것이 모두 있다. Event Loop에서 Web API의 역할이 그런 것이지, 모두 비동기로 동작하는 것이 아니라는 점을 잊지 말자~! 4. Callback Queue(콜백 큐) 완료된 비동기 작업의 콜백 함수가 대기하는 공간. 이 때 Callback Queue는 두 가지 종류가 있다. 여기서 알아둬야할 점은 마이크로태스크 큐의 우선순위가 더 높다는 것이다. +) ✅ 매크로테스크큐(태스크 큐) : setTimeout , fetch , addEventListener 등 비동기로 처리되는 함수들의 콜백 함수가 저장된다. +) ✅ 마이크로테스크 큐 : Promise.then , MutationObserver 등 우선적으로 처리되는 비동기로 처리되는 함수들의 콜백 함수가 저장된다. 이벤트 루프의 동작 과정 이제부터는 이벤트 루프가 어떤 과정으로 동작하는지 살펴보도록 하자. 가장 먼저 코드가 실행되어 함수가 호출되면 실행 컨텍스트가 Call stack 에 쌓인다. 이 때, Call stack 은 말 그대로 스택이기 때문에 하나씩 쌓이게 되고 위에서부터 하나씩 실행된다. 비동기 작업(setTimeout, promise 등)은** webAPI**를 통해 브라우저에게 넘겨진다. (넘겨진 작업은 Call stack에서 즉시 pop 된다.) 브라우저는 비동기 작업들에게 각각 별도의 쓰레드를 배정하고 실행한다. 작업이 완료된 후 callback 함수는 Callback Queue 에 넣어준다. 이 때 작업의 종류에 따라 MacroTask Queue / MicroTask Queue 로 각각 구분되어 들어간다. 이벤트 루프는 반복해서 콜 스택이 비어 있는지, Callback Queue 에 콜백 함수가 있는지 계속해서 확인한다. 이 때, 콜 스택이 비어있다면 콜백 큐에 쌓여있는 콜백 함수를 콜 스택으로 넘겨준다. 설명이 거의 간장공장공장장급이라 이해가 잘 안 될 거 같아 움직이는 이미지를 첨부한다. 아래 이미지를 참고하면 이해가 훨씬 수월할 것이다! 🤔: 콜 스택은 왜 queue가 아니라 stack인가요? 💁♀️: 함수 호출의 구조적인 특성과 실행 흐름 제어 방식을 고려했을 때, stack 구조가 적합하기 때문입니다. (LIFO 방식) function A() { B(); } function B() { C(); } A(); // A → B → C 순으로 호출됨 위 코드에서 A()가 먼저 호출되고, B()가 A 안에서 실행되었으며, C()는 B 안에서 실행됩니다. 이 흐름에서 중요한 것은 " C가 끝나야 B가 끝날 수 있고, B가 끝나야 A가 끝날 수 있다는 것 "이에요. 즉, 나중에 호출된 함수가 먼저 끝나야 앞선 함수로 돌아갈 수 있다는 것인데 이게 사실상 LIFO 구조 인 셈이지요. JavaScript 이벤트 루프 시각화 아무래도 흐름이 중요한 개념이다 보니 이미지만 봐서는 이해하기가 어려웠는데, 찾아보니 이벤트 루프 과정을 애니메이션으로 볼 수 있는 시각화 사이트가 있다. 해당 사이트에서 다양한 비동기 코드를 추가하여 비동기 동작이 실제로 어떤 과정으로 일어나는지 확인해볼 수 있다. JavaScript 이벤트 루프 시각화 사이트 코드로 이해하는 이벤트 루프 동작 과정 이번에는 실제 코드를 사용해 이벤트 루프 동작 과정을 이해해보자. (아래 예시 코드는 ‘매일메일’ 서비스를 구독해서 받은 메일의 일부이다. 매일메일 최고👍 ) setTimeout(() => { console.log('1') setTimeout(() => {console.log('2')} ) Promise.resolve().then(()=> console.log('3')) console.log('4') }) Promise.resolve().then(() => { console.log('5') setTimeout(() => {console.log('6')} ) Promise.resolve().then(()=> console.log('7')) console.log('8') }) console.log('9') 정답: 9 5 8 7 1 4 3 6 2 실행 과정 1️⃣ setTimeout() , Promise 객체는 비동기 코드이기 때문에 webAPI로 넘어가고, 가장 하단의 console.log(’9’) 부터 실행된다. →** 9 출력** 2️⃣ setTimeout() 은 매크로테스크 큐에, Promise.then() 은 마이크로테스크 큐에 저장된다. 콜 스택이 비어있고, 테스크 큐에 콜백 함수가 있기 때문에 event loop가 동작한다. 이 때 마이크로테스크 큐의 우선순위가 더 높기 때문에 Promise.then() 부터 실행된다. 3️⃣ 5 출력 후, Promise.then() 내부의 setTimeout() 함수와 Promise 객체 역시 비동기 코드로 webAPI 넘어간다. 그 후 console.log(’8’) 이 실행된다. → 순서대로 5, 8 출력 4️⃣ Promise.then() 내부의 Promise 가 먼저 실행된다. → 7 출력 5️⃣ 그 후 첫 번째 setTimeout() 의 콜백 함수가 실행된다. 이 때도 역시 동기 함수 먼저 실행된다. → 1, 4 출력 6️⃣ 다음으로 setTimeout() 내부의 Promise.then() 의 콜백 함수가 마이크로 테스크 큐로 들어간다. → 3 출력 7️⃣ 마지막으로 매크로테스크 큐에 있는 3과 4의 setTimeout() 의 콜백 함수가 들어온 순서대로 실행된다. → 순서대로 6, 2 출력 8️⃣ 콜 스택과 이벤트 콜백 큐가 모두 비었기 때문에 event loop의 동작도 끝이 난다. +) 추가 참고 영상 이벤트 루프 과정을 이해하는데 큰 도움을 받았던 영상을 참고하며 글을 마친다. 애니메이션 영상과 코드를 사용해서 과정 그대로를 시각적으로 보여줘서 개념을 이해하기가 정말 수월했다! https://www.youtube.com/watch?v=eiC58R16hb8 참고 https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Execution_model https://inpa.tistory.com/entry/🔄-자바스크립트-이벤트-루프-구조-동작-원리 https://velog.io/@leehyunho2001/자바스크립트-이론-부시기#자바스크립트-런타임
SOAP SOAP란? SOAP은 XML을 기반으로 메시지를 교환하는 프로토콜로, 기업 간 시스템 통합이나 복잡한 분산 시스템에서 널리 사용되었다. DTD를 지원하긴 하지만 스키마(XSD)의 사용을 강력하게 권장했다. SOAP의 단점 SOAP의 단점 중 많은 부분은 스키마의 단점에서 왔다. 강제된 스키마 : 명확한 스키마 구조를 설계해야 하기에 소규모 프로젝트에선 부담스럽다. 높은 파일 용량 : XML의 여러 태그와 구조는 저장 데이터를 무겁게 만든다. 복잡한 구조 : XSD의 엄격한 구조는 배우기 어렵고, 숙련자도 유지보수가 어렵다. REST REST란? REST는 HTTP의 기본 기능을 활용해 리소스를 url로 식별하고, HTTP 메서드를 통해 처리한다. 특정한 형식이나 프로토콜을 강제하지 않으며, JSON의 사용률이 높다. 스마트폰의 등장으로 간결한 통신에 대한 수요가 증가했고, REST가 이해 적합했다. REST의 장점 HTTP 메서드 : (GET,POST,등등)의 메서드를 사용하여 직관적이고, 예측 가능하다. 높은 개발 생산성: 일반적으로 JSON을 사용하며, 복잡한 메시지 규격이 없어 비교적 쉽게 구현할 수 있다. 효율적인 데이터 전송: JSON은 일반적으로 XML보다 데이터가 간결하여 용량을 줄일 수 있다. 무상태성: 서버가 클라이언트의 상태를 기억하지 않아, 수평 확장에 유리. 캐싱 지원: HTTP의 캐싱 기능을 활용하여 반복적인 요청에 대한 속도를 높이고, 서버의 부하를 주일 수 있다.(모든 메서드가 캐싱 되는 것은 아님) SOAP가 REST보다 유리한 점 표준화된 통신: 엄격한 규칙을 통해 서로 다른 시스템 간의 통신을 표준화함. 보안 및 신뢰성: 높은 수준의 보안(Web Services Security)과 신뢰성(WS-ReliableMessaging, WS-AtomicTransaction)을 제공. 다양한 전송 프로토콜: REST는 HTTP에 의존하지만 SOAP는 다른 전송프로토콜도 사용할 수 있음.