OpenAIのDotsが常駐エージェントを標準化、Anthropicは2兆ドルIPOへ
AIタグが付けられた新着記事 - Qiita
OpenAIはDevDayで常駐型エージェントをデスクトップへ押し出し、その直後に自社のフロンティアモデルを安全基準未達として出荷見送りにした。Anthropicは2兆ドル規模になり得るIPOの数字を開示し、AMDは82億ドルで世界モデルのラボを買収した。慌ただしかった月...
Score: 57.39Confidence: 54%
View offerLoading the catalog…
THE AI OPPORTUNITY INDEX
Find your next AI tool. Explore free access, trials, and credits — all in one place.
AIタグが付けられた新着記事 - Qiita
OpenAIはDevDayで常駐型エージェントをデスクトップへ押し出し、その直後に自社のフロンティアモデルを安全基準未達として出荷見送りにした。Anthropicは2兆ドル規模になり得るIPOの数字を開示し、AMDは82億ドルで世界モデルのラボを買収した。慌ただしかった月...
Score: 57.39Confidence: 54%
View offerReadhub
美国总统特朗普与人工智能领袖讨论数据中心的布局。
Score: 57.37Confidence: 54%
View offerReadhub
苹果公司将在印度推出 Apple Pay。
Score: 57.37Confidence: 54%
View offerReadhub
九阳股份公告,公司股票 2026 年 9 月 28 日、9 月 29 日连续两个交易日收盘价格涨幅偏离值累计达 23.65%。公司称持有深思考 7.92% 股权,与华为不存在传闻所称的战略合作关系,机器人相关投资处于早期阶段,对当期营业收入及利润无重大影响。公司近期经营情况正常,2026 年上半年营业收入 34.89 亿元,同比下降 12.49%。归属于上市公司股东的净利润 7177.32 万元,同比下降 41.52%。
Score: 57.32Confidence: 54%
View offerReadhub
多家基金管理人集中上报首批跟踪中证红利增长指数的 ETF 产品。该指数定位于 A 股红利增长策略,反映分红连续增长的上市公司整体表现。当前 A 股上市公司分红力度稳步增长,投资者对红利资产配置需求持续提升。中证红利增长 ETF 的推出,既可以将上市公司持续增强的分红能力转化为投资者可分享的稳健回报,提升投资者获得感与长期投资体验,也有助于引导中长期资金加大权益配置,增强资本市场内在稳定性。
Score: 57.31Confidence: 54%
View offerReadhub
OpenAI 宣布与亚马逊云服务合作,利用 AWS 基础设施运行托管式 AI 智能体,还将把 ChatGPT 部署至 Slack 和微软 Teams,用户无需单独购买个人许可证即可使用相关功能,目前每周有 3500 万人通过 ChatGPT Work 和 Codex 进行构建和开发。
Score: 57.3Confidence: 54%
View offervelog
이음 프로젝트에는 학원 Wi-Fi에 연결된 직원만 출퇴근할 수 있도록 하는 근태 인증 기능 이 있다. 방식 자체는 단순했다. 학원에서 사용하는 네트워크의 공인 IP를 미리 등록해두고, 직원이 출근 또는 퇴근을 요청했을 때 현재 요청의 IP와 등록된 Wi-Fi IP를 비교하는 방식이다. 처음에는 Spring Boot에서 다음과 같이 요청 IP를 확인하면 충분하다고 생각했다. request.getRemoteAddr(); 개발 환경에서는 문제없이 동작했다. 하지만 실제 배포 환경에서는 상황이 달랐다. Browser │ ▼ Next Server │ ▼ Load Balancer / Proxy │ ▼ Spring Boot Spring Boot가 직접 사용자의 연결을 받는 구조가 아니었기 때문이다. 결국 문제는 단순히 "사용자의 IP를 어떻게 가져올까?" 가 아니었다. "여러 계층을 거쳐 전달된 IP를 Spring Boot가 어떤 조건에서 신뢰할 수 있을까?" 이번 글에서는 이 문제를 해결하기 위해 Next Server에서 전달된 Client IP를 Spring Boot가 그대로 사용하지 않고, HMAC Signature를 검증한 경우에만 출퇴근 판단에 사용하도록 변경한 과정 을 정리한다. 1. 처음에는 getRemoteAddr() 를 사용했다 초기 출퇴근 인증 흐름은 단순했다. 직원 출근 요청 │ ▼ Spring Boot │ ▼ request.getRemoteAddr() │ ▼ 등록된 Wi-Fi 공인 IP와 비교 │ ┌──┴──┐ │ │ 일치 불일치 │ │ 출근 거부 사용자의 요청 IP가 학원에 등록된 공인 IP와 같다면 출근을 허용하는 구조였다. 개발 환경에서는 이 방식이 정상적으로 동작했고, 처음에는 특별한 문제가 없어 보였다. 2. 운영에서는 Spring이 사용자를 직접 만나지 않았다 문제는 배포 환경이었다. 실제 서비스에서는 사용자의 요청이 바로 Spring Boot로 전달되는 것이 아니었다. 중간에 Next Server나 Load Balancer, Proxy 같은 계층이 존재했다. 사용자 │ ▼ Next / LB / Proxy │ ▼ Spring Boot 이런 구조에서 request.getRemoteAddr() 가 반환하는 값은 반드시 사용자의 네트워크 IP라고 볼 수 없다. Spring Boot 입장에서 getRemoteAddr() 는 자신과 직접 연결된 상대의 주소 를 반환하기 때문이다. 예를 들어 Spring이 다음 주소를 확인했다고 하자. 10.0.2.15 이 값이 사용자의 공인 IP가 아니라 Spring 바로 앞에 있는 Proxy의 주소라면 학원 Wi-Fi 접속 여부를 판단하는 기준으로 사용할 수 없다. 즉, 개발 환경 Client │ ▼ Spring Boot 에서는 문제가 드러나지 않았지만, 운영 환경 Client │ ▼ Next / Proxy │ ▼ Spring Boot 에서는 네트워크 구조가 달라지면서 같은 코드가 다른 의미의 IP를 바라보게 된 것이다. 3. Proxy IP를 등록하면 해결될까? Spring이 확인하는 Proxy IP를 학원 Wi-Fi IP로 등록하면 해결할 수 있을까? 그렇게 하면 출퇴근 인증의 의미 자체가 사라진다. 예를 들어 모든 사용자의 요청이 동일한 Proxy를 통과한다고 하자. 사용자 A ─┐ 사용자 B ─┼─→ Proxy ─→ Spring Boot 사용자 C ─┘ Spring 입장에서는 사용자의 실제 네트워크와 관계없이 모두 같은 Proxy에서 들어오는 요청처럼 보일 수 있다. 이 Proxy IP를 학원 Wi-Fi IP로 등록하면 다음과 같은 문제가 생긴다. 학원 내부 사용자 → Proxy IP → 출근 가능 학원 외부 사용자 → Proxy IP → 출근 가능 결국 "학원 네트워크에 연결된 직원만 출근할 수 있다." 라는 정책 자체를 검증할 수 없게 된다. 4. X-Forwarded-For 만 읽으면 되는 것 아닐까? Proxy 환경에서 원래 Client IP를 전달할 때 흔히 사용하는 값 중 하나가 X-Forwarded-For 다. X-Forwarded-For: 203.0.113.10 처음에는 Spring Boot에서 이 Header를 읽어 사용하면 해결할 수 있을 것처럼 보인다. 하지만 중요한 것은 Header의 이름이 아니라 그 값을 누가 만들었고 왜 신뢰할 수 있는가 였다. X-Forwarded-For 자체를 사용할 수 없는 것은 아니다. 신뢰할 수 있는 Reverse Proxy가 외부에서 전달된 값을 제거하거나 재작성하고 Client IP를 관리하는 구조라면 해당 Header를 신뢰하도록 구성할 수도 있다. 하지만 Client가 임의로 전달할 수 있는 Header 값을 애플리케이션에서 그대로 사용해서는 안 된다. 예를 들어 출퇴근 API가 다음 값을 아무런 검증 없이 사용한다고 가정해보자. X-Forwarded-For: 학원_공인_IP 클라이언트가 해당 값을 직접 지정할 수 있다면 학원 외부에서도 등록된 Wi-Fi IP를 넣어 요청할 가능성을 고려해야 한다. 결국 백엔드에서 확인해야 할 것은 단순히 "IP 값이 등록된 값과 일치하는가?" 만이 아니었다. "이 IP가 백엔드가 신뢰하도록 정한 서버를 거쳐 전달된 값인가?" 까지 확인할 필요가 있었다. 5. 문제를 다시 정의했다 처음에는 문제를 다음과 같이 생각했다. 사용자의 공인 IP를 Spring Boot까지 전달한다. 하지만 운영 구조를 확인하면서 문제의 초점을 바꿨다. 전달된 Client IP를 Spring Boot가 어떤 조건에서 신뢰할 것인지 결정한다. 즉, IP를 가져오는 문제에서 전달된 IP의 출처를 검증하는 문제 로 다시 정의했다. 이번 개선에서는 다음 세 가지를 목표로 했다. Proxy가 존재하는 운영 환경에서도 Client IP를 출퇴근 판단에 사용할 수 있어야 한다. 전달된 IP 값을 Spring Boot가 아무런 검증 없이 신뢰하지 않아야 한다. 운영 환경의 보안 설정이 누락됐을 때 검증이 자동으로 우회되어서는 안 된다. 여기서 한 가지 범위를 명확하게 할 필요가 있다. 이번 글에서는 Next Server가 Client IP를 최초로 어떤 방식으로 식별하는지 자체는 다루지 않는다. 해당 부분은 프론트엔드의 Next Server 구현 영역이며, 백엔드에서는 Next Server에서 전달된 IP와 요청 정보를 어떤 조건에서 신뢰할 것인지 에 초점을 맞췄다. 이를 위해 Next Server와 Spring Boot 사이에 HMAC 기반 검증 구조 를 적용했다. 6. IP와 함께 Signature를 전달하도록 변경했다 최종적인 흐름은 다음과 같다. Browser │ ▼ Next Server │ ├─ Client IP 전달 │ └─ HMAC Signature 생성 │ ▼ Spring Boot │ ├─ Timestamp 검증 ├─ 요청 정보 검증 ├─ HMAC Signature 검증 └─ IP 형식 검증 │ ▼ 검증된 Client IP │ ▼ 등록된 Wi-Fi IP와 비교 │ ▼ 출퇴근 판단 Next Server는 Client IP만 전달하지 않는다. 해당 요청에 대한 Signature도 함께 생성한다. Spring Boot 역시 전달받은 Client IP를 즉시 출퇴근 판단에 사용하지 않는다. 먼저 요청과 함께 전달된 Signature를 검증하고, 검증에 성공한 경우에만 해당 IP를 이후 로직에서 사용하도록 했다. 백엔드 입장에서 검증 경계는 다음과 같다. Browser │ ▼ Next Server │ │ Client IP │ Timestamp │ Signature ▼ ────── Backend Verification Boundary ────── Spring Boot │ ├─ 필수 값 확인 ├─ Timestamp 검증 ├─ Method / Path 확인 ├─ HMAC Signature 검증 └─ IP 형식 검증 │ ▼ 검증된 Client IP │ ▼ Wi-Fi IP 비교 7. 무엇을 서명해야 할까? Client IP 하나만 서명하는 방식도 생각할 수 있다. CLIENT_IP 하지만 IP에만 Signature가 묶여 있으면 동일한 값을 다른 요청에서도 사용할 수 있는 범위가 커진다. 그래서 서명 대상에 다음 정보를 함께 포함했다. METHOD + PATH + CLIENT_IP + TIMESTAMP 예를 들어 출근 요청이라면 개념적으로 다음과 같은 데이터가 된다. POST + /api/attendance/check-ins + 203.0.113.10 + 1720000000 이 데이터를 공유 Secret을 이용해 HMAC Signature로 만든다. METHOD PATH CLIENT_IP TIMESTAMP │ ▼ HMAC Secret │ ▼ Signature 각 값을 포함한 데에는 이유가 있었다. 8. METHOD 와 PATH 를 포함한 이유 IP에만 Signature가 묶여 있다면 한 API에서 생성된 값을 다른 API에서도 사용할 수 있는 범위가 넓어진다. 예를 들어 현재 IP를 확인하는 API가 있다고 하자. GET /api/attendance/wifi-ips/current 그리고 실제 출근 API는 다음과 같다. POST /api/attendance/check-ins Signature가 Client IP에만 의존한다면 요청의 목적과 Signature 사이의 연결이 약하다. 그래서 METHOD 와 PATH 까지 서명 대상에 포함했다. GET + /api/attendance/wifi-ips/current + CLIENT_IP 와 POST + /api/attendance/check-ins + CLIENT_IP 는 서로 다른 Signature가 생성된다. 즉, Signature를 단순한 IP 값이 아니라 특정 요청 정보와 함께 묶은 것 이다. 9. Timestamp로 Signature의 유효 시간을 제한했다 Method와 Path를 포함하더라도 정상적인 요청과 Signature가 오래 보관된 뒤 다시 사용되는 상황은 고려해야 했다. 그래서 Timestamp도 Signature 대상에 포함했다. METHOD + PATH + CLIENT_IP + TIMESTAMP Spring Boot에서는 전달된 Timestamp가 현재 시간을 기준으로 60초 이내인지 확인 한다. 요청 수신 │ ▼ Timestamp 확인 │ ├─ 60초 이내 │ │ │ ▼ │ 검증 계속 │ └─ 60초 초과 │ ▼ 403 여기서 Timestamp의 역할을 정확하게 구분할 필요가 있다. Timestamp를 추가했다고 해서 동일한 요청의 재사용 자체를 완전히 차단하는 것은 아니다. 동일한 Timestamp와 Signature가 유효 시간 안에 다시 전달된다면 Timestamp 검증만으로 두 요청을 구분할 수 없기 때문이다. 따라서 이번 구현에서 Timestamp는 Replay를 완전히 제거하는 장치 라기보다 오래된 Signature가 재사용될 수 있는 시간을 제한하는 장치 로 사용했다. 10. Spring Boot에서는 무엇을 검증할까? Spring Boot가 요청을 받으면 전달된 Client IP를 바로 사용하지 않는다. 먼저 다음 검증 과정을 거친다. Request │ ▼ 필수 Header 확인 │ ▼ Timestamp 확인 │ ▼ METHOD 확인 │ ▼ PATH 확인 │ ▼ CLIENT_IP 확인 │ ▼ HMAC Signature 재계산 │ ▼ 전달받은 Signature와 비교 │ ▼ 검증된 Client IP 사용 Spring Boot에서도 동일한 규칙과 Secret을 이용해 Signature를 다시 계산한다. METHOD + PATH + CLIENT_IP + TIMESTAMP 전달받은 Signature와 서버에서 계산한 Signature가 일치해야만 Client IP를 이후 출퇴근 판단에 사용한다. 즉, 단순히 Client IP = 203.0.113.10 이라는 Header가 존재한다는 이유만으로 값을 신뢰하지 않는다. 백엔드는 해당 요청 정보와 Client IP가 공유 Secret을 이용해 생성된 Signature와 일치하는가? 를 확인한 뒤 값을 사용한다. 11. Signature 비교에도 일반 문자열 비교를 사용하지 않았다 Signature를 비교할 때는 일반 문자열 비교 대신 상수 시간 비교(Constant-time comparison) 를 사용했다. 일반적인 문자열 비교는 구현에 따라 어느 위치에서 값이 달라졌는지에 따라 비교 시간이 달라질 수 있다. Signature와 같이 보안 검증에 사용하는 값에서는 이러한 시간 차이를 통한 정보 노출 가능성도 줄이는 편이 안전하다고 판단했다. 개념적인 흐름은 다음과 같다. Expected Signature │ ▼ Constant-time Compare ▲ │ Received Signature Signature 값의 일치 여부만 판단하고, 비교 과정에서 불필요한 정보를 드러내지 않도록 했다. 12. 검증 실패 이유를 외부에 세분화하지 않았다 Signature 검증은 여러 이유로 실패할 수 있다. Signature 누락 Timestamp 만료 잘못된 Signature IP 형식 오류 요청 정보 불일치 하지만 외부 응답에서는 각 실패 원인을 세분화하지 않았다. 모두 동일하게 403 Forbidden 으로 처리했다. Signature 누락 ──┐ Timestamp 만료 ──┤ Signature 오류 ──┼──→ 403 Forbidden IP 형식 오류 ────┤ 요청 불일치 ─────┘ 클라이언트가 검증 과정 내부의 세부 조건을 불필요하게 알 필요가 없다고 판단했기 때문이다. 내부에서는 필요한 로그를 남기더라도 외부 응답에서는 검증 구조에 대한 정보를 최소화했다. 13. Signature는 Header 전송에 맞게 처리했다 Signature는 HTTP Header를 통해 전달해야 했다. 이를 위해 Signature는 Base64 URL-safe 형식 으로 처리했다. Next Server에서 Spring Boot로 전달되는 검증 정보는 개념적으로 다음과 같다. Client IP Timestamp Signature Spring Boot는 이 값과 현재 요청의 Method, Path를 이용해 동일한 Signature를 계산한다. 중요한 것은 Header가 존재한다는 사실이 아니라, 해당 요청 정보와 전달된 Client IP가 Signature와 일치하는지 를 검증하는 것이다. 14. 모든 API에 Signature 검증을 적용하지는 않았다 Client IP가 실제 비즈니스 판단에 사용되는 API에만 동일한 검증 방식을 적용했다. Signature 검증이 필요한 API는 다음과 같다. 현재 공인 IP 조회 GET /api/attendance/wifi-ips/current 현재 접속 IP를 사용하므로 검증이 필요하다. Wi-Fi IP 등록 POST /api/attendance/wifi-ips 현재 네트워크 IP가 등록 과정에 사용되므로 검증이 필요하다. 출근 POST /api/attendance/check-ins 현재 Client IP가 출근 가능 여부를 결정하므로 검증이 필요하다. 퇴근 POST /api/attendance/check-outs 현재 Client IP가 퇴근 가능 여부를 결정하므로 검증이 필요하다. 반대로 현재 접속한 Client IP가 비즈니스 판단에 사용되지 않는 API에는 동일한 검증을 강제하지 않았다. GET /api/attendance/wifi-ips DELETE /api/attendance/wifi-ips/... 즉, 모든 요청에 일괄적으로 적용하기보다 Client IP가 실제 비즈니스 판단에 사용되는 API에만 검증을 적용한다. 는 기준을 사용했다. 15. 로컬 개발 환경까지 같은 구조를 강제하지 않았다 운영에서는 요청이 Next Server를 거쳐 Spring Boot로 전달되기 때문에 Signature 검증이 필요했다. 하지만 로컬에서 백엔드 기능을 개발할 때까지 반드시 Next Server를 실행해야 한다면 개발 과정이 불편해진다. 그래서 Local과 Production의 IP 확인 전략을 분리했다. Local 로컬 환경에서는 기존 방식인 getRemoteAddr() 를 사용한다. Local Client │ ▼ Spring Boot │ ▼ getRemoteAddr() Next Server 없이도 백엔드 기능을 개발할 수 있도록 했다. Production 운영 환경에서는 Next Server에서 전달된 정보와 Signature를 검증한다. Browser │ ▼ Next Server │ ▼ Signed Request │ ▼ Spring Boot 즉, Local → 개발 편의성을 위한 직접 IP 확인 Production → Signature가 검증된 Client IP 사용 으로 환경별 책임을 나눴다. 16. Secret이 없다고 검증을 생략하지 않았다 HMAC 검증에서 중요한 값은 Next Server와 Spring Boot가 공유하는 Secret이다. 그런데 운영 환경에서 Secret 설정을 누락했다고 가정해보자. 다음과 같이 구현하면 문제가 생긴다. Secret 없음 │ ▼ 검증 생략 │ ▼ 요청 허용 환경 변수 하나를 잘못 설정한 것만으로 전체 IP 검증이 사라질 수 있다. 그래서 Production 환경에서는 필요한 Secret이 존재하지 않을 경우 검증을 비활성화하는 대신 애플리케이션 기동 자체가 실패하도록 했다. Application Start │ ▼ Secret 확인 │ ┌──┴──┐ │ │ 있음 없음 │ │ 실행 기동 실패 잘못된 보안 설정을 가진 상태로 서비스가 실행되는 것보다 서비스가 시작되지 않는 쪽을 선택했다. 즉, Fail Open 설정 오류 → 검증 생략 → 서비스 실행 이 아니라 Fail Closed 설정 오류 → 서비스 기동 실패 방향으로 구성했다. 17. 실제로 검증이 동작하는지 테스트했다 검증 구조를 구현하는 것에서 끝내지 않고, 정상적인 요청과 변조된 요청을 Spring Boot가 의도한 대로 구분하는지 테스트했다. 특히 다음 네 가지 경우를 확인했다. 정상 Signature Client IP 변조 Timestamp 만료 Signature 변조 17-1. 정상 Signature 요청 먼저 정상적인 Client IP와 현재 Timestamp를 기준으로 올바른 Signature를 생성했다. 해당 요청을 ClientIpResolver 에 전달했을 때 검증에 성공하고 원래 Client IP가 반환되는지 확인했다. 테스트에서는 Next Proxy 검증을 활성화하고, 현재 Timestamp와 정상적으로 생성된 Signature가 포함된 요청을 전달했다. 검증 결과는 다음 조건을 만족해야 한다. 정상 Client IP + 유효한 Timestamp + 올바른 Signature │ ▼ 검증 성공 │ ▼ Client IP 반환 테스트가 정상적으로 통과하는 것을 확인했다. 17-2. Client IP만 변조했을 때 다음으로 Signature를 생성한 이후 Client IP 값만 변경한 요청 을 테스트했다. 예를 들어 Signature가 다음 Client IP를 기준으로 생성됐다고 하자. 203.0.113.10 그 이후 요청에 포함된 Client IP만 다른 값으로 변경한다. Signature 생성 시 IP 203.0.113.10 ↓ IP만 변경 요청에 전달된 IP 203.0.113.20 Signature는 기존 IP를 기준으로 만들어졌기 때문에 Spring Boot에서 변경된 IP를 기준으로 HMAC을 다시 계산하면 값이 일치하지 않는다. 정상 Signature + 변조된 Client IP │ ▼ HMAC 재계산 │ ▼ Signature 불일치 │ ▼ ForbiddenException 이 테스트를 통해 요청에 포함된 Client IP 값이 변경되더라도 기존 Signature를 그대로 이용해 검증을 통과할 수 없음을 확인했다. 이 부분은 이번 구현에서 특히 중요했다. 백엔드가 X-Client-IP에 값이 있으니 사용한다. 가 아니라 X-Client-IP 값이 Signature 생성에 사용된 값과 일치하는지 검증한 뒤 사용한다. 는 것을 실제 테스트로 확인했기 때문이다. 17-3. Timestamp가 만료됐을 때 다음으로 유효 시간이 지난 Signature를 검증했다. 현재 시각보다 61초 오래된 Timestamp 를 이용해 Signature를 생성하고 요청을 전달했다. 현재 시각 │ │ 61초 차이 ▼ 요청 Timestamp Signature 자체는 해당 Timestamp에 맞게 정상적으로 생성되어 있더
velog
개발자를 위한 넓고 얕은 컴퓨터 지식, 넓얕컴지 JavaScript의 객체는 단순한 key-value 저장소 가 아니다. 프로퍼티마다 별도의 속성이 있고, 객체끼리 프로토타입으로 연결되며, 배열과 함수조차 이 객체 모델 위에서 동작한다. 여기에 반복자(Iterator), Symbol, Generator, 모듈까지 연결하면 JavaScript라는 언어가 데이터를 어떻게 조직하고 확장하는지 보이기 시작한다. JavaScript 1에서는 값 → 변수 → 스코프 → 실행 컨텍스트 → 렉시컬 환경 → 클로저 → this 라는 코드 실행 과정 을 따라갔다. 이번에는 시선을 객체 쪽으로 돌린다. 객체 → 프로퍼티 → 생성자 → 프로토타입 → 클래스 → 배열 → 컬렉션 → 반복 프로토콜 → Generator → 오류 → 모듈 이 흐름을 이해하면 class 나 map() 같은 문법을 사용하는 수준을 넘어 JavaScript가 데이터를 어떤 모델로 표현하고 공유하는지 설명할 수 있다. 1️⃣ JavaScript 객체는 무엇일까? 1-1. 객체(Object)와 프로퍼티(Property) ⭐️⭐️⭐️ JavaScript 객체는 프로퍼티(Property)의 집합이다. 프로퍼티는 키(Key)와 값(Value)의 조합으로 구성된다. const user = { name: "Esther", age: 28, hello() { return `Hello ${this.name}`; } }; name , age , hello 가 프로퍼티 키이고 각각 "Esther" , 28 , 함수 객체가 프로퍼티 값이다. JavaScript 객체의 프로퍼티 키는 기본적으로 문자열(String) 또는 Symbol이다. 숫자로 작성해도 일반 객체에서는 문자열 형태의 키로 취급된다. const obj = { 1: "one" }; obj[1]; obj["1"]; 두 접근은 같은 프로퍼티를 가리킨다. 1-2. 점 표기법과 대괄호 표기법(Dot & Bracket Notation) ⭐️⭐️ user.name; user["name"]; 둘 다 프로퍼티에 접근한다. 다만 런타임에 결정되는 키를 사용하려면 대괄호 표기법이 필요하다. const key = "name"; user[key]; 객체를 단순한 구조체처럼만 생각하면 이 특징을 놓치기 쉽다. JavaScript 객체는 실행 중에도 프로퍼티를 추가·삭제하고 동적으로 탐색할 수 있는 구조 다. 1-3. 계산된 프로퍼티 이름(Computed Property Name) ⭐️ 객체를 만들 때도 표현식을 프로퍼티 키로 사용할 수 있다. const key = "username"; const user = { [key]: "Esther" }; 결과적으로 user.username 에 접근할 수 있다. 이 기능은 설정 객체나 동적으로 구성되는 데이터에서 자주 사용된다. 2️⃣ 객체의 프로퍼티에는 숨겨진 속성이 있다 겉으로 보면 다음 프로퍼티는 단순히 name → Esther 처럼 보인다. const user = { name: "Esther" }; 하지만 ECMAScript의 객체 모델에서 데이터 프로퍼티는 값뿐 아니라 여러 속성을 가진다. 2-1. 프로퍼티 어트리뷰트(Property Attribute) 대표적인 속성은 다음과 같다. [[Value]] [[Writable]] [[Enumerable]] [[Configurable]] 개념적으로 다음과 같다. name ├─ Value → "Esther" ├─ Writable → true ├─ Enumerable → true └─ Configurable → true writable 은 값 변경 가능 여부, enumerable 은 열거 대상 여부, configurable 은 삭제하거나 프로퍼티 정의를 변경할 수 있는지와 관련된다. 일반적인 객체 리터럴로 만든 프로퍼티에서는 대부분 신경 쓰지 않아도 되지만 라이브러리·프레임워크·메타프로그래밍을 이해하려면 중요한 개념이다. 2-2. 프로퍼티 디스크립터(Property Descriptor) 프로퍼티의 속성은 Object.getOwnPropertyDescriptor() 로 확인할 수 있다. Object.getOwnPropertyDescriptor(user, "name"); 반대로 Object.defineProperty() 를 이용하면 프로퍼티 속성을 직접 지정할 수도 있다. Object.defineProperty(user, "id", { value: 10, writable: false, enumerable: false, configurable: false }); 이렇게 만든 id 는 일반적인 프로퍼티와 다른 규칙을 가진다. 2-3. 데이터 프로퍼티와 접근자 프로퍼티(Data & Accessor Property) 프로퍼티가 항상 값을 직접 저장하는 것은 아니다. const user = { firstName: "Esther", lastName: "Kang", get fullName() { return `${this.firstName} ${this.lastName}`; } }; fullName 은 값을 직접 가지고 있는 데이터 프로퍼티가 아니라 Getter를 통해 계산되는 접근자 프로퍼티(Accessor Property)다. Setter를 이용하면 값의 변경 과정도 통제할 수 있다. 2-4. [[Get]]과 [[Set]] user.name 이라는 단순한 코드 뒤에서도 객체 내부에서는 프로퍼티를 찾고 값을 읽는 일련의 동작이 일어난다. ECMAScript 명세에서는 이런 내부 동작을 [[Get]] , [[Set]] 같은 내부 메서드(Internal Method)로 표현한다. 개발자가 obj.[[Get]]() 처럼 직접 호출하는 것은 아니다. [[...]] 표기는 실제 JavaScript API가 아니라 ECMAScript 명세에서 객체 내부 동작을 설명하기 위한 표기 다. 이 개념은 곧 프로토타입 체인과 연결된다. 3️⃣ 객체의 프로퍼티를 어떻게 찾을까? ⭐️⭐️⭐️ 객체에서 프로퍼티를 찾을 때 JavaScript는 객체 자신만 확인하지 않는다. 먼저 객체 자신을 찾고, 없다면 연결된 프로토타입으로 이동한다. 이 구조가 JavaScript 객체 상속의 핵심이다. 3-1. 자체 프로퍼티(Own Property)와 상속 프로퍼티(Inherited Property) ⭐️⭐️⭐️ 객체 자신이 직접 가지고 있는 프로퍼티를 자체 프로퍼티(Own Property)라고 한다. 프로토타입에서 찾아오는 프로퍼티는 상속 프로퍼티(Inherited Property)다. const user = { name: "Esther" }; "name" in user; "toString" in user; 둘 다 true 가 될 수 있다. name 은 객체 자체에 있고 toString 은 프로토타입 체인에서 찾을 수 있기 때문이다. 3-2. Object.hasOwn() ⭐️⭐️ 객체 자체가 가진 프로퍼티인지 확인하려면 Object.hasOwn() 을 사용할 수 있다. Object.hasOwn(user, "name"); // true Object.hasOwn(user, "toString"); // false 따라서 in 과 Object.hasOwn() 은 같은 검사를 하는 것이 아니다. in → 프로토타입 체인까지 검색 Object.hasOwn() → 객체 자신만 검사 3-3. Object.keys(), values(), entries() ⭐️⭐️ 객체를 순회할 때 자주 사용한다. Object.keys(user); Object.values(user); Object.entries(user); 각각 열거 가능한 자체 프로퍼티의 키, 값, [key, value] 쌍을 가져온다. 프로토타입의 프로퍼티까지 가져오는 것은 아니다. 4️⃣ 생성자 함수와 new 4-1. 객체를 만드는 방법 ⭐️⭐️ JavaScript에서 객체를 만드는 방법은 하나가 아니다. const user = {}; const user = new Object(); const user = Object.create(...); const user = new User(); 가장 흔한 방법은 객체 리터럴 {} 이지만 여러 객체가 동일한 초기화 규칙과 동작을 가져야 한다면 생성자 함수(Constructor Function)를 사용할 수 있다. 4-2. 생성자 함수(Constructor Function) ⭐️⭐️⭐️ function User(name) { this.name = name; } const user1 = new User("Esther"); const user2 = new User("Reina"); 같은 User 생성자를 이용해 여러 객체를 만들 수 있다. 4-3. new는 실제로 무엇을 할까? ⭐️⭐️⭐️ new User() 는 단순히 User() 를 호출하는 것과 다르다. 개념적으로 다음 과정이 일어난다. new User() ↓ 새 객체 생성 ↓ 새 객체의 [[Prototype]] → User.prototype 연결 ↓ this → 새 객체 ↓ User 함수 실행 ↓ 새 객체 반환 이 과정에서 생성자 함수와 프로토타입이 연결된다. 4-4. 생성자(Constructor)와 비생성자(Non-constructor) JavaScript의 함수 객체라고 모두 new 와 함께 사용할 수 있는 것은 아니다. 개념적으로 함수에는 호출 가능한 내부 동작인 [[Call]] 이 있고 일부 함수는 new 호출이 가능한 [[Construct]] 까지 가진다. 일반 함수 선언과 일부 함수 표현식은 생성자가 될 수 있지만 화살표 함수는 생성자 함수로 사용할 수 없다. const User = () => {}; new User(); // TypeError 4-5. new.target 함수가 new 와 함께 호출되었는지 확인할 수 있는 new.target 도 존재한다. function User() { console.log(new.target); } 프레임워크 애플리케이션에서 자주 직접 사용할 기능은 아니지만 생성자 호출 모델을 이해하는 데 도움이 된다. 5️⃣ 프로토타입(Prototype) ⭐️⭐️⭐️ JavaScript의 객체지향 모델에서 가장 중요한 개념 중 하나다. JavaScript는 다른 객체를 자신의 프로토타입으로 연결하고 그 연결을 이용해 프로퍼티와 메서드를 공유한다. 『Deep Dive』에서도 프로토타입을 객체 상속과 중복 제거의 핵심 메커니즘으로 다룬다. 5-1. prototype과 [[Prototype]]은 다르다 ⭐️⭐️⭐️ 가장 많이 혼동하는 부분이다. 생성자 함수의 prototype 프로퍼티: User.prototype 인스턴스 객체 내부의 [[Prototype]] : user.[[Prototype]] 개념적으로 다음 관계가 만들어진다. User │ │ prototype ▼ User.prototype ▲ │ [[Prototype]] │ user User.prototype 은 일반 객체다. user 가 가진 내부 [[Prototype]] 연결이 그 객체를 가리킨다. 5-2. 프로토타입에 메서드를 두는 이유 ⭐️⭐️⭐️ 다음 생성자를 생각해보자. function User(name) { this.name = name; this.hello = function () { return this.name; }; } 객체를 1,000개 만들면 hello 함수도 인스턴스마다 생성될 수 있다. 대신 프로토타입에 두면 공유할 수 있다. function User(name) { this.name = name; } User.prototype.hello = function () { return this.name; }; user1 ─┐ user2 ─┼──▶ User.prototype.hello() user3 ─┘ 프로토타입은 상속만을 위한 개념이 아니라 여러 객체가 동작을 공유하는 메커니즘 이다. 5-3. 프로토타입 체인(Prototype Chain) ⭐️⭐️⭐️ user.toString(); user 자신에게 toString 이 없다면 JavaScript는 상위 프로토타입을 탐색한다. user ↓ User.prototype ↓ Object.prototype ↓ null 이 연결이 프로토타입 체인이다. 프로퍼티 검색은 가장 가까운 객체부터 위로 올라간다. 5-4. 프로퍼티 섀도잉(Property Shadowing) ⭐️⭐️ 프로토타입에 name 이 존재하지만 객체 자신에게 같은 프로퍼티를 만들면 객체 자신의 값이 먼저 발견된다. user.name ← 발견 ↓ User.prototype.name 상위 프로퍼티가 삭제된 것은 아니다. 하위 객체의 프로퍼티가 검색 과정에서 가리고 있는 것이다. 5-5. Object.create() ⭐️⭐️⭐️ 특정 객체를 프로토타입으로 가지는 객체를 직접 만들 수도 있다. const parent = { hello() { return "hello"; } }; const child = Object.create(parent); 이 경우: child ↓ [[Prototype]] parent ↓ Object.prototype 생성자 함수 없이도 객체 사이의 프로토타입 관계를 만들 수 있다는 점에서 JavaScript의 상속은 본질적으로 객체 간 연결 이라는 사실이 잘 드러난다. 5-6. instanceof ⭐️⭐️ user instanceof User 이를 단순히 User 생성자로 만들어졌는지 기록을 확인한다 라고 이해하면 부족하다. 본질적으로는 User.prototype 이 user 의 프로토타입 체인 안에 존재하는지를 확인한다. user ↓ User.prototype ← 존재? ↓ Object.prototype 그래서 프로토타입 관계를 변경하면 instanceof 결과도 달라질 수 있다. 6️⃣ 클래스(Class) ⭐️⭐️⭐️ ES6 이후 JavaScript에서도 class 문법을 사용할 수 있다. class User { constructor(name) { this.name = name; } hello() { return this.name; } } 겉모습은 Java 같은 클래스 기반 언어와 비슷하지만 JavaScript의 객체 상속 모델이 프로토타입에서 다른 방식으로 바뀐 것은 아니다. class 역시 프로토타입 객체 모델 위에서 동작한다. 6-1. 클래스 메서드는 어디에 있을까? ⭐️⭐️⭐️ class User { hello() {} } hello() 가 각각의 인스턴스 안에 복제되는 것이 아니다. 개념적으로: User.prototype.hello 에 존재하며 여러 인스턴스가 공유한다. 6-2. constructor ⭐️⭐️ constructor 는 new 로 객체가 만들어질 때 인스턴스를 초기화한다. class User { constructor(name) { this.name = name; } } 6-3. 정적 메서드(Static Method) ⭐️⭐️ class User { static create() { return new User("default"); } } 정적 메서드는 인스턴스가 아니라 클래스 자체에서 사용한다. User.create(); user.create() 와는 다른 위치에 존재한다. 6-4. 인스턴스 필드와 정적 필드(Instance & Static Field) ⭐️⭐️ 현대 JavaScript에서는 클래스 필드도 선언할 수 있다. class User { role = "user"; static count = 0; } role 은 인스턴스마다 존재하고 count 는 클래스 자체에 연결된다. 6-5. private field ⭐️⭐️ class Account { #balance = 0; } # 을 이용한 private field는 클래스 외부에서 직접 접근할 수 없다. 과거에 클로저로 구현하던 정보 은닉과는 다른 언어 수준의 private 상태 다. 6-6. extends와 super ⭐️⭐️⭐️ class Admin extends User { constructor(name) { super(name); } } extends 는 상속 구조를 만들고 super 는 부모 생성자나 부모 메서드에 접근한다. 내부에서는 인스턴스뿐 아니라 생성자 함수 쪽의 프로토타입 관계도 함께 구성된다. 6-7. class는 단순히 문법적 설탕인가? 프로토타입 기반이라는 핵심은 맞지만 그냥 prototype 문법을 예쁘게 바꾼 것뿐 이라고 하면 부족하다. 클래스는 new 없이 호출할 수 없고, 클래스 내부 코드는 엄격 모드(Strict Mode)로 실행되며, extends , super , private field 같은 클래스 전용 규칙도 제공한다. 따라서 프로토타입 기반 객체 모델 위에 클래스 지향 개발 경험을 제공하는 언어 기능 이라고 이해하는 것이 좋다. 7️⃣ 엄격 모드(Strict Mode) JavaScript에는 과거의 느슨한 언어 동작을 제한하는 엄격 모드가 있다. "use strict"; 엄격 모드에서는 선언하지 않은 변수에 값을 할당해 암묵적인 전역 변수를 만드는 것 같은 위험한 동작이 오류가 된다. 일반 함수 호출의 this 가 undefined 가 되는 것도 중요한 차이다. 현대 개발에서는 모든 파일에 "use strict" 를 직접 작성하는 경우보다 ES 모듈이나 클래스 문법을 사용하면서 자연스럽게 엄격 모드가 적용되는 경우가 많다. ES 모듈은 기본적으로 엄격 모드로 실행된다. :chatgpt-content-reference{index="3"} 8️⃣ 배열(Array)은 일반적인 배열과 조금 다르다 ⭐️⭐️⭐️ C의 배열처럼 JavaScript Array를 같은 타입의 값이 메모리에 연속해서 저장된 자료구조 라고 생각하면 정확하지 않다. JavaScript Array는 객체 모델 위에서 특별한 동작을 제공하는 자료구조다. const arr = [10, 20, 30]; arr[0]; arr.length; 배열 인덱스는 배열 특유의 규칙을 따르는 프로퍼티 키와 연결된다. 8-1. 서로 다른 타입도 넣을 수 있다 ⭐️⭐️ const arr = [ 10, "hello", true, {}, function () {} ]; JavaScript 자체는 같은 배열 안에 서로 다른 타입의 값을 넣는 것을 막지 않는다. 실무에서는 데이터 형태의 일관성을 유지하는 편이 코드 이해와 엔진 최적화 측면에서 좋다. 8-2. length는 단순한 개수 변수가 아니다 ⭐️⭐️⭐️ const arr = [1, 2, 3]; arr.length = 1; 이렇게 하면 뒤쪽 요소가 제거될 수 있다. 반대로: arr.length = 100; 이라고 한다고 100개의 실제 값이 생기는 것은 아니다. 8-3. 희소 배열(Sparse Array) ⭐️⭐️ JavaScript 배열은 중간 인덱스가 비어 있는 상태를 가질 수 있다. const arr = []; arr[100] = "hello"; console.log(arr.length); // 101 실제 요소 하나만 넣었는데 length 는 101이 된다. 이런 희소 배열은 반복 메서드별 동작 차이나 성능 문제를 만들 수 있으므로 의도적으로 만드는 것은 보통 피한다. 9️⃣ 배열 메서드는 원본 변경 여부가 중요하다 ⭐️⭐️⭐️ JavaScript의 배열 메서드를 외울 때 단순히 무슨 기능인가 만 기억하면 부족하다. 원본 배열을 변경하는가(Mutating), 새로운 값을 반환하는가(Non-mutating) 를 함께 봐야 한다. 9-1. 원본을 변경하는 메서드 대표적으로: push() pop() shift() unshift() splice() sort() reverse() 등이 있다. const arr = [3, 1, 2]; arr.sort(); console.log(arr); // 원본 변경 9-2. 새로운 배열을 반환하는 메서드 대표적으로: map() filter() slice() concat() flat() flatMap() 등이 있다. 현대 JavaScript에는 원본을 변경하지 않는 toSorted() , toReversed() , toSpliced() , with() 같은 메서드도 추가되었다. 9-3. sort()의 비교 함수(Comparator) ⭐️⭐️⭐️ JavaScript의 sort() 는 기본적으로 값을 문자열 기준으로 정렬한다. [1, 10, 2].sort(); 숫자 정렬이라면 비교 함수를 명시하는 것이 좋다. arr.sort((a, b) => a - b); a - b < 0 이면 a가 앞, > 0 이면 b가 앞에 위치한다. 🔟 배열 고차 함수(Higher-order
velog
🐍 Python Security Scanner 프로젝트 - Day 1 점프 투 파이썬 학습을 마친 후 배운 내용을 실제로 활용하기 위해 시작한 개인 프로젝트이다. Python의 socket 을 이용해 TCP Port Scanner 를 직접 구현하고, 순차 스캔의 속도 문제를 ThreadPoolExecutor 를 이용한 멀티스레딩으로 개선해보았다. 📌 Day 1 목표 오늘의 목표는 다음과 같다. Python socket 모듈 이해 특정 IP / Port에 TCP 연결 시도 OPEN Port 판단 여러 Port를 자동으로 스캔 함수로 Port Scan 기능 분리 timeout 설정 ThreadPoolExecutor 를 이용한 멀티스레딩 스캔 실행 시간 측정 직접 테스트 서버를 만들어 OPEN Port 탐지 검증 ⚠️ 포트 스캔 실습은 본인의 PC 또는 직접 관리하는 테스트 환경을 대상으로 진행한다. 1. 프로젝트 생성 프로젝트 폴더를 생성하고 main.py 파일부터 시작했다. python-security-scanner/ └── main.py 처음부터 복잡하게 파일을 나누기보다는 하나의 파일에서 기능을 구현하고, 프로젝트가 커지면 추후 모듈화할 예정이다. 2. IP와 Port 입력받기 가장 먼저 스캔할 대상의 IP와 Port를 사용자에게 입력받았다. target_ip = input("스캔할 IP를 입력하세요: ") target_port = int(input("스캔할 Port를 입력하세요: ")) 여기서 IP와 Port의 자료형이 다르다는 점이 중요하다. IP "127.0.0.1" → str Port 80 → int IP 주소는 127.0.0.1 처럼 . 이 포함되어 있기 때문에 정수로 변환할 수 없다. 따라서 IP는 문자열 그대로 입력받고, Port만 int() 를 이용해 정수로 변환한다. 출력에는 f-string을 사용했다. print(f"[+] {target_ip}:{target_port}") 실행 예시: 스캔할 IP를 입력하세요: 127.0.0.1 스캔할 Port를 입력하세요: 80 [+] 127.0.0.1:80 3. TCP Socket 생성 실제로 네트워크 연결을 시도하기 위해 Python의 socket 모듈을 사용했다. import socket 그리고 다음과 같이 Socket을 생성한다. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 각 옵션의 의미는 다음과 같다. 코드 의미 socket.socket() Socket 생성 AF_INET IPv4 사용 SOCK_STREAM TCP 사용 즉, socket.socket(socket.AF_INET, socket.SOCK_STREAM) 은 쉽게 말하면 IPv4 주소를 사용하는 TCP Socket을 생성한다. 라는 의미이다. 4. connect_ex()를 이용한 TCP 연결 생성한 Socket을 이용해 대상 IP와 Port에 TCP 연결을 시도한다. result = sock.connect_ex((target_ip, target_port)) connect_ex() 에는 IP와 Port를 하나의 튜플로 전달한다. ("127.0.0.1", 80) 구조는 다음과 같다. sock.connect_ex( (target_ip, target_port) ) connect_ex() 는 연결 결과를 숫자로 반환한다. TCP 연결 성공 ↓ result == 0 TCP 연결 실패 ↓ result != 0 따라서 if 문을 이용해 Port의 상태를 판단할 수 있다. if result == 0: print(f"[+] {target_ip}:{target_port} OPEN") else: print(f"[-] {target_ip}:{target_port} CLOSED") 단, 정확히 말하면 result != 0 은 무조건 하나의 의미인 CLOSED 만 나타내는 것은 아니다. Connection Refused, Timeout 등 여러 이유로 TCP 연결이 성공하지 않을 수 있기 때문에 이후에는 OPEN 여부를 중심으로 판단 하도록 구현했다. 5. Socket 종료 생성한 Socket은 사용이 끝나면 종료해준다. sock.close() 기본적인 흐름은 다음과 같다. Socket 생성 ↓ TCP 연결 시도 ↓ 결과 확인 ↓ Socket 종료 6. 여러 Port 자동 스캔 처음에는 하나의 Port만 입력받았다. target_port = int(input("스캔할 Port를 입력하세요: ")) 하지만 Port Scanner라면 여러 Port를 자동으로 검사할 수 있어야 한다. 따라서 for 문과 range() 를 이용했다. for target_port in range(1, 101): range() 의 마지막 숫자는 포함되지 않기 때문에 range(1, 101) 은 실제로 1 2 3 ... 99 100 까지 반복한다. 각 Port마다 새로운 TCP 연결을 시도해야 하기 때문에 Socket 역시 반복문 내부에서 생성하고 종료했다. for target_port in range(1, 101): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((target_ip, target_port)) if result == 0: print(f"[+] {target_ip}:{target_port} OPEN") sock.close() CLOSED Port까지 모두 출력하면 결과가 너무 많아지기 때문에 이후에는 OPEN Port만 출력 하도록 했다. 7. Timeout 설정 여러 Port를 검사해보니 스캔 속도가 매우 느린 문제가 발생했다. TCP 연결을 시도했는데 상대방이 바로 응답하지 않으면 프로그램이 응답을 기다릴 수 있기 때문이다. 이를 방지하기 위해 Timeout을 설정했다. sock.settimeout(0.5) 의미는 다음과 같다. TCP 연결 시도 ↓ 최대 0.5초 동안 기다림 ↓ 응답이 없으면 연결 시도를 계속 기다리지 않음 따라서 Socket 생성 이후 다음과 같이 Timeout을 설정했다. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) result = sock.connect_ex((target_ip, target_port)) Timeout 값을 너무 크게 설정하면 스캔 속도가 느려질 수 있고, 반대로 너무 작게 설정하면 응답이 느린 서비스의 연결 성공 여부를 놓칠 수 있다. 8. 직접 8000번 Port 열어보기 Scanner가 실제로 OPEN Port를 탐지하는지 검증하기 위해 Python의 간단한 HTTP Server를 사용했다. 새로운 터미널에서 다음 명령어를 실행했다. python -m http.server 8000 실행하면 Python HTTP Server가 TCP 8000번 Port에서 연결을 기다린다. Python HTTP Server ↓ TCP 8000 ↓ OPEN 브라우저에서도 다음 주소로 접근할 수 있다. http://127.0.0.1:8000 Scanner의 범위도 테스트를 위해 다음과 같이 변경했다. for target_port in range(7995, 8006): 그러면 다음 Port들을 검사한다. 7995 7996 7997 7998 7999 8000 8001 8002 8003 8004 8005 실행 결과: [+] 127.0.0.1:8000 OPEN 정상적으로 8000번 Port가 탐지되었다. HTTP Server를 Ctrl + C 로 종료한 뒤 다시 Scanner를 실행하면 8000번 Port가 더 이상 OPEN으로 탐지되지 않는다. 이를 통해 직접 만든 Scanner가 실제 TCP 연결 상태를 기준으로 동작한다는 것을 확인했다. 9. Port Scan 기능 함수화 기존에는 모든 코드를 for 문 안에서 처리했다. 이후 멀티스레딩을 적용하기 위해 Port 하나를 검사하는 기능을 함수로 분리 했다. def scan_port(target_ip, target_port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) result = sock.connect_ex((target_ip, target_port)) if result == 0: print(f"[+] {target_ip}:{target_port} OPEN") sock.close() 함수에 필요한 매개변수는 두 개이다. def scan_port(target_ip, target_port): target_ip : 검사할 대상 IP target_port : 검사할 Port 예를 들어 scan_port("127.0.0.1", 8000) 을 실행하면 target_ip = "127.0.0.1" target_port = 8000 이 함수 내부로 전달된다. 여기서는 return 을 사용하지 않았다. 현재 함수의 목적은 결과값을 다른 곳에 반환하는 것이 아니라 OPEN Port가 발견되었을 때 직접 print() 로 출력하는 것이기 때문이다. 10. 함수 정의와 함수 호출 함수를 사용하면서 다시 정리한 중요한 개념이다. 다음 코드는 함수를 정의 하는 것이다. def scan_port(target_ip, target_port): 함수를 정의했다고 바로 실행되는 것은 아니다. 실제로 실행하려면 함수를 호출 해야 한다. scan_port(target_ip, target_port) 따라서 반복문과 함수를 다음과 같이 분리할 수 있다. def scan_port(target_ip, target_port): # Port 하나 검사 ... for target_port in range(1, 101): scan_port(target_ip, target_port) 구조는 다음과 같다. Port 1 ↓ scan_port(IP, 1) Port 2 ↓ scan_port(IP, 2) Port 3 ↓ scan_port(IP, 3) ... 즉, 함수는 한 번 정의하고, for문에서 필요한 만큼 호출한다. 11. 순차 스캔의 문제 함수화를 했지만 여전히 Port는 순서대로 검사한다. Port 1 검사 ↓ 완료 ↓ Port 2 검사 ↓ 완료 ↓ Port 3 검사 Timeout이 0.5초 라고 가정하면 응답이 늦은 Port가 많을 경우 시간이 상당히 오래 걸릴 수 있다. 예를 들어 1000개의 Port가 각각 최대 0.5초를 기다린다면 이론적으로 상당한 시간이 필요할 수 있다. 따라서 여러 Port를 동시에 검사하도록 개선하기로 했다. 12. ThreadPoolExecutor Python의 concurrent.futures 모듈에서 ThreadPoolExecutor 를 사용했다. from concurrent.futures import ThreadPoolExecutor ThreadPoolExecutor 는 여러 Worker Thread가 작업을 나눠 처리할 수 있도록 해준다. 예를 들어 ThreadPoolExecutor(max_workers=10) 은 최대 10개의 Worker Thread를 이용해 작업을 처리한다. 기존 방식: Worker 1 → Port 1 → 완료 ↓ Port 2 → 완료 ↓ Port 3 → 완료 멀티스레딩: Worker 1 → Port 1 Worker 2 → Port 2 Worker 3 → Port 3 Worker 4 → Port 4 ... Worker 10 → Port 10 네트워크 연결처럼 응답을 기다리는 시간이 많은 I/O 작업에서는 여러 연결을 동시에 진행할 수 있기 때문에 성능 개선 효과를 얻을 수 있다. 13. executor.submit() Thread Pool에 작업을 전달하기 위해 submit() 을 사용했다. executor.submit(scan_port, target_ip, target_port) 구조를 풀어보면 다음과 같다. executor.submit( scan_port, ← 실행할 함수 target_ip, ← 첫 번째 인수 target_port ← 두 번째 인수 ) 즉 Worker에게 scan_port(target_ip, target_port) 작업을 실행하도록 요청하는 것이다. 14. 멀티스레딩 Port Scanner for 문과 ThreadPoolExecutor 를 결합했다. with ThreadPoolExecutor(max_workers=10) as executor: for target_port in range(1, 1001): executor.submit(scan_port, target_ip, target_port) with 블록을 사용하면 Thread Pool을 사용한 뒤 필요한 정리를 처리할 수 있고, 블록을 빠져나올 때 제출된 작업들이 완료될 때까지 기다린다. 이제 하나의 Worker가 Port를 순서대로 검사하는 대신 여러 Worker가 Port Scan 작업을 나누어 처리한다. 15. 스캔 시간 측정 멀티스레딩 적용 전후의 성능을 확인하기 위해 time 모듈을 사용했다. import time 스캔 직전에 시작 시간을 기록한다. start_time = time.time() 모든 작업이 완료된 이후 종료 시간을 기록한다. end_time = time.time() 그리고 두 값의 차이를 계산한다. print(f"스캔 소요 시간: {end_time - start_time}초") 중요한 점은 start_time 과 end_time 의 위치이다. start_time ↓ 전체 Port Scan ↓ 모든 Thread 작업 완료 ↓ end_time 각 Port를 검사할 때마다 시간을 측정하는 것이 아니라 전체 Port Scan 작업의 시작과 끝을 한 번씩 측정 해야 한다. 16. 멀티스레딩 성능 비교 127.0.0.1 의 1~1000번 Port를 대상으로 테스트했다. Timeout: sock.settimeout(0.5) 10 Workers ThreadPoolExecutor(max_workers=10) 실행 결과: 스캔 소요 시간: 약 51.03초 50 Workers ThreadPoolExecutor(max_workers=50) 실행 결과: 스캔 소요 시간: 약 10.22초 결과를 정리하면 다음과 같다. Worker Port 범위 Timeout 소요 시간 10 1~1000 0.5초 약 51.03초 50 1~1000 0.5초 약 10.22초 50 Workers를 사용했을 때 10 Workers보다 약 5배 빠른 결과 를 확인할 수 있었다. 대략적으로 생각하면 10 Workers의 경우: 1000개 Port ÷ 10 Workers = Worker당 약 100개 100개 × 최대 0.5초 ≈ 50초 실제 결과는 약 51초 였다. 50 Workers의 경우: 1000개 Port ÷ 50 Workers = Worker당 약 20개 20개 × 최대 0.5초 ≈ 10초 실제 결과 역시 약 10.22초 였다. 따라서 단순히 "멀티스레딩이 빠르다"가 아니라 실제 실행 시간을 측정하여 성능 차이를 확인할 수 있었다. 단, Worker 수를 무조건 크게 설정한다고 항상 좋은 것은 아니므로 이후 적절한 Worker 수와 Timeout 값도 테스트해볼 예정이다. 17. 실제로 탐지된 OPEN Port 1~1000번 Port를 스캔하면서 내 PC에서 다음과 같은 OPEN Port들이 탐지되었다. [+] 127.0.0.1:135 OPEN [+] 127.0.0.1:445 OPEN [+] 127.0.0.1:902 OPEN [+] 127.0.0.1:912 OPEN [+] 127.0.0.1:945 OPEN 현재 단계에서는 TCP 연결 성공 여부만 판단 하고 있기 때문에 각 Port에서 실제로 어떤 서비스가 동작하고 있는지는 확인하지 않는다. 서비스 식별은 이후 기능으로 추가할 예정이다. 18. Day 1 최종 코드 import time import socket from concurrent.futures import ThreadPoolExecutor # 스캔할 대상 IP 입력 target_ip = input("스캔할 IP를 입력하세요: ") # 하나의 Port를 검사하는 함수 def scan_port(target_ip, target_port): # IPv4(AF_INET) + TCP(SOCK_STREAM) 소켓 생성 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP 연결 시 최대 0.5초까지만 응답을 기다림 sock.settimeout(0.5) # 대상 IP와 Port에 TCP 연결을 시도하고 결과 코드 반환 result = sock.connect_ex((target_ip, target_port)) # connect_ex()의 반환값이 0이면 TCP 연결 성공 if result == 0: print(f"[+] {target_ip}:{target_port} OPEN") # 현재 Port 검사가 끝났으므로 Socket 종료 sock.close() # 전체 Port Scan 시작 시간 기록 start_time = time.time() # 최대 50개의 작업자 Thread를 사용하는 Thread Pool 생성 with ThreadPoolExecutor(max_workers=50) as executor: # 1번부터 1000번 Port까지 반복 for target_port in range(1, 1001): # scan_port() 작업을 Thread Pool에 제출 executor.submit(scan_port, target_ip, target_port) # 모든 Port Scan 작업 완료 후 종료 시간 기록 end_time = time.time() # 전체 Port Scan 소요 시간 출력 print(f"스캔 소요 시간: {end_time - start_time}초") 📊 Day 1 프로그램 동작 흐름 사용자에게 Target IP 입력 ↓ Port 1 ~ 1000 ↓ ThreadPoolExecutor (50 Workers) ↓ scan_port() ↓ TCP Socket 생성 ↓ Timeout 설정 ↓ connect_ex() ↓ TCP 연결 시도 ↓ ┌─────┴─────┐ ↓ ↓ result == 0 result != 0 ↓ ↓ OPEN 출력하지 않음 ↓ Socket 종료 ↓ 모든 작업 완료 ↓ 실행 시간 출력 💡 Day 1에서 배운 내용 이번 프로젝트를 진행하면서 기존에 공부했던 Python 문법이 실제 프로그램에서 어떻게 사용되는지 확인할 수 있었다. Python 학습 내용 프로젝트 적용 import socket, time 모듈 사용 변수 IP, Port, 실행 시간 저장 input() 스캔 대상 IP 입력 if TCP 연결 성공 여부 판단 for 여러 Port 반복 range() Port Scan 범위 지정 함수 def Port Scan 기능 분리 매개변수 IP와 Port를 함수에 전달 f-string Scan 결과 출력 with Thread Pool 관리 그리고 새롭게 다음 내용을 접했다. TCP Socket AF_INET SOCK_STREAM connect_ex() Timeout Thread Thread Pool ThreadPoolExecutor executor.submit() 프로그램 실행 시간 측정 🔍 오늘 발생했던 문제와 해결 1. IP에 int() 를 사용함 처음에는 다음과 같이 작성했다. target_ip = int(input(...)) 하지만 IP는 127.0.0.1 과 같은 문자열이므로 int() 로 변환할 수 없다. target_ip = input(...) 로 수정했다. 2. sock.close() 를 for문 밖에 작성 Port마다 새로운 Socket을 생성하면서 close() 를 반복문 밖에 작성하면 마지막 Socket만 종료하게 된다. 따라서 각 Port의 검사가 끝날 때마다 Socket을 종료하도록 반복문 내부에 배치했다. 3. 함수 내부에 for문을 작성 처음 함수화할 때 def scan_port(target_ip, target_port): for target_port in range(...): 형태로 작성했다. 하지만 scan_port() 의 역할은 Port 하나를 검사하는 것 으로 정하고 반복은 함수 외부에서 담당하도록 분리했다. for target_port in range(...): scan_port(target_ip, target_port) 4. start_time 을 함수 내부에 작성
velog
A productive day does not always mean completing a long list of tasks. In many cases, productivity comes from having a clear routine, managing time wisely, and creating small habits that make everyday responsibilities easier. Whether you work from home, study, run a business, or manage a busy household, a practical daily routine can help you stay organized without feeling overwhelmed. Start Your Morning With a Clear Plan The way you begin your morning can influence the rest of your day. Instead of immediately checking social media or responding to every notification, take a few minutes to think about what you need to accomplish. Write down two or three important tasks and give them priority. A simple morning routine might include drinking water, having breakfast, reviewing your schedule, and spending a few quiet minutes preparing mentally for the day. You do not need an elaborate routine. Consistency is usually more useful than complexity. Set Realistic Daily Goals One common reason people struggle with productivity is trying to do too much at once. A long task list can quickly become stressful, especially when unexpected responsibilities appear. Try dividing your work into smaller goals. For example, instead of writing “finish the project,” break it into research, planning, writing, reviewing, and finalizing. Smaller steps make large responsibilities easier to manage and provide a clearer sense of progress. Digital tools can also make everyday organization easier. Many people use online platforms such as Gold365 as part of their broader digital routine, while calendars, note-taking applications, and task-management tools can help keep important information organized. Take Regular Breaks Working continuously for several hours may seem productive, but regular breaks can help you maintain concentration. After completing an important task, step away from your screen for a few minutes. Stretch, walk around, drink some water, or simply rest your eyes. Short breaks can also make it easier to return to difficult tasks with a fresh perspective. The goal is not to avoid work but to create a sustainable rhythm between focused periods and recovery. Keep Your Workspace Organized Your surroundings can affect how easily you concentrate. A cluttered desk can make it harder to find documents, devices, or other items when you need them. At the beginning or end of each workday, spend five to ten minutes organizing your workspace. Keep frequently used items nearby and remove unnecessary objects. If you work digitally, organize files into clearly named folders and regularly delete documents you no longer need. Make Time for Personal Activities A balanced routine should include more than work or study. Make room for activities that help you relax and recharge. Reading, walking, exercising, listening to music, cooking, or spending time with family and friends can provide a valuable break from daily responsibilities. Personal time should not necessarily be considered wasted time. Rest and recreation can help you return to your responsibilities feeling more refreshed. Review Your Day Before going to bed, take a few minutes to review what you accomplished. Consider which tasks were completed, what needs to be moved to another day, and whether anything unexpected affected your schedule. This simple review can help you identify patterns. Perhaps you are most focused in the morning, or perhaps certain tasks consistently take longer than expected. Understanding these patterns allows you to make better decisions about how you structure future days. Conclusion Building a productive and balanced routine does not require major lifestyle changes. Start with a few realistic habits: plan your priorities, divide large tasks into manageable steps, take regular breaks, maintain an organized environment, and protect time for yourself. The most effective routine is one that fits your lifestyle and can be followed consistently. Small improvements made every day can eventually become habits that make work, study, and personal responsibilities easier to manage.
velog
import os import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import platform if platform.system() == 'Windows': plt.rc('font', family='Malgun Gothic') elif platform.system() == 'Darwin': plt.rc('font', family='AppleGothic') else: plt.rc('font', family='NanumBarunGothic') plt.rcParams['axes.unicode_minus'] = False base_path = r"/Users/kdt_0900_lyn/ml/resoce/dataset" file_name = r"banknote_authentication.csv" file_path = os.path.join(base_path, file_name) os.path.join(base_path, file_name) 데이터 분할 from sklearn.model_selection import train_test_split (sklearn.model_selection: 데이터 분할 함수 , 학습용과 테스트용 세트로 무작위로 나누어줌) (데이터의 단위를 일정하게 맞춰주는 데이터 전처리(스케일링) 코드) from sklearn.prepocessing import StandrdScaler scaler = StandarScaler() \ X_train_scaled = scaler.fit_transform(X_train) (.fit_transform : 평균과 표준편차를 모두 구하고 표준화) X_test_scaled = scaler.transform(X_test) (.transform : 수해진 편균과 표준편차를 이용해서 표준화) 모델 준비 from sklearn.svc import SVC 모델 학습 svc.fit(X_train_scaled, y_traim) 모델 예측 학습된 SVC 모델을 바탕으로 스케일링된 테스트 데이터(X_test_scaled)의 예측값(y_pred)을 생성하는 코드(.predict : 정답 예측하는 사이킷런의 핵심 기능 ) y_pred = svc.predict(X_test_scaled) print(y_pred) print(list(y_test)) ex) y_pred = svc.predict(X_test_scaled) print(y_pred[:10]) print(list(y_test[:10])) (train: 학습 , test: 평가 , scaled: 단위를 맞춘 데이터 ) • X_train -> 데이터 스케일러(Scaler)를 학습시킬 때 딱 한 번 씀 -> 예시 코드: scaler.fit(X_train) • X_train_scaled -> 머신러닝 모델을 실제로 학습(훈련)시킬 때 씁니다. -> 예시 코드: svc.fit(X_train_scaled, y_train) • y_train -> 모델을 학습시킬 때, 특징 데이터(X)에 맞는 실제 정답지로 함께 넣습니다. -> 예시 코드: svc.fit(X_train_scaled, y_train) 모델 평가 (정확도 검사) svc.score(X_train_scaled, y_train) \ svc.score(X_test_scaled, y_test) (confusion_matrix() : 혼동 행렬) array([[ TN, FP ], [ FN, TP ]]) • TN (True Negative) • FP (False Positive) • FN (False Negative) • TP (True Positive) \예시\ confusion_matrix(y_pred, y_test) 조화 평균 (정밀도(precision)와 재현율(recall)) F1-Score : 모델이 예측한 값(y_pred)과 실제 정답(y_test)을 비교하여 이진 분류기준의 F1-Score를 계산하고 출력하는 코드 y_test, y_pred -> 예상값과 정답이 얼마나 맞는지 확인 score = f1_score(y_test, y_pred) print(f"F1-Score 점수: {score}") df[] : 인덱싱(Indexing) 또는 대괄호 연산자 = 필터링 .map() : 이터를 하나씩 순서대로 꺼내서 변환 상자에 집어넣는 반복 도구
Score: 54.34Confidence: 49%
View offervelog
개발자를 위한 넓고 얕은 컴퓨터 지식, 넓얕컴지 JavaScript는 문법만 보면 시작하기 어렵지 않다. 하지만 왜 선언 전에 변수가 보이지? , 왜 객체를 복사했는데 원본도 바뀌지? , 함수 실행이 끝났는데 변수는 왜 살아 있지? , this는 왜 호출할 때마다 달라지지? 같은 질문에 답하려면 문법보다 언어의 실행 원리를 이해해야 한다. JavaScript의 핵심 개념은 서로 독립되어 있지 않다. 값과 데이터 타입을 이해하면 불변성과 참조 공유가 연결되고, 변수 선언을 따라가면 호이스팅(Hoisting)과 일시적 사각지대(Temporal Dead Zone)가 나온다. 여기에 함수 호출을 더하면 실행 컨텍스트(Execution Context)와 호출 스택(Call Stack)이 등장하고, 스코프(Scope)를 따라가면 렉시컬 환경(Lexical Environment)과 클로저(Closure)가 연결된다. 마지막으로 함수가 어떻게 호출되는지를 보면 this 의 동작까지 이해할 수 있다. 이번 글에서는 값 → 데이터 타입 → 메모리와 복사 → 타입 변환 → 변수 선언 → 함수 → 스코프 → 실행 컨텍스트 → 렉시컬 환경 → 클로저 → this 순서로 JavaScript의 실행 원리를 연결한다. 프로토타입, 클래스, 배열, 반복자(Iterator) 같은 객체 모델은 JavaScript2에서, Promise와 이벤트 루프, Node.js Runtime은 JavaScript3에서 이어서 다룬다. 1️⃣ 값과 코드의 기본 단위 1-1. 값(Value)과 리터럴(Literal) ⭐️ 값(Value)은 프로그램이 처리할 수 있는 실제 데이터다. 10 , "hello" , true , null , { name: "Esther" } 모두 값이다. 리터럴(Literal)은 값을 코드에 직접 표현하는 방법이다. 10 은 숫자 리터럴, "hello" 는 문자열 리터럴, [1, 2, 3] 은 배열 리터럴, { name: "Esther" } 는 객체 리터럴이다. 변수는 값 자체가 아니라 값을 다시 사용할 수 있도록 이름을 붙여 접근하는 수단 이라고 생각하면 이후 실행 컨텍스트와 스코프를 이해하기 쉬워진다. 1-2. 표현식(Expression)과 문(Statement) ⭐️⭐️ 표현식(Expression)은 평가(Evaluation)했을 때 하나의 값이 되는 코드다. 1 + 2 , user.name , getUser() , x > 10 등이 표현식이다. 문(Statement)은 프로그램이 수행할 하나의 명령 단위다. 변수 선언문, 조건문, 반복문, return 등이 대표적이다. const total = price * count; price * count 는 값을 만들어내는 표현식이고, 전체 코드는 변수를 선언하는 문이다. 이 구분이 중요한 이유는 JavaScript에서 값을 요구하는 위치에는 표현식을 넣을 수 있지만 모든 문을 넣을 수 있는 것은 아니기 때문 이다. 1-3. 평가(Evaluation) ⭐️⭐️ JavaScript 엔진은 표현식을 만나면 이를 계산해 하나의 값으로 만든다. 1 + 2 는 평가 후 3 이 되고 user.name 은 객체에서 프로퍼티를 찾아 해당 값으로 평가된다. 함수 호출 표현식인 getUser() 는 함수를 실행한 뒤 반환값으로 평가된다. 이 평가 라는 개념은 뒤에서 실행 컨텍스트를 다룰 때 다시 등장한다. JavaScript 엔진은 코드 전체를 단순히 텍스트로 한 줄씩 읽는 것이 아니라 코드를 해석하고 실행 가능한 상태를 준비한 뒤 각 표현식을 평가한다. 1-4. 세미콜론 자동 삽입(Automatic Semicolon Insertion) ⭐️ JavaScript는 특정 조건에서 세미콜론을 자동으로 삽입한다. 하지만 필요한 위치를 엔진이 알아서 완벽하게 찾아준다 는 의미는 아니다. 대표적인 예가 return 이다. return { name: "Esther" } 개발자는 객체를 반환한다고 생각할 수 있지만 return; 으로 해석되어 undefined 가 반환될 수 있다. 따라서 JavaScript의 줄바꿈은 단순한 코드 스타일 문제만은 아니다. 파서(Parser)가 코드를 어떻게 하나의 문으로 해석하는지 에도 영향을 줄 수 있다. 2️⃣ JavaScript의 데이터 타입 2-1. 원시 타입(Primitive Type)과 객체 타입(Object Type) ⭐️⭐️⭐️ JavaScript의 값은 크게 원시값과 객체로 나누어 볼 수 있다. 원시 타입은 undefined , null , boolean , number , string , symbol , bigint 의 7가지다. 배열(Array), 함수(Function), Date, Map, Set 등을 포함한 나머지 복합적인 값은 객체(Object)를 기반으로 한다. JavaScript 값(Value) │ ├─ 원시 타입(Primitive Type) │ ├─ undefined │ ├─ null │ ├─ boolean │ ├─ number │ ├─ string │ ├─ symbol │ └─ bigint │ └─ 객체(Object) ├─ 일반 객체 ├─ 배열 ├─ 함수 ├─ Date ├─ Map / Set └─ ... 이 구분은 단순한 타입 목록이 아니다. 값이 변경 가능한가, 다른 변수에 할당하면 어떤 일이 일어나는가, 동등 비교에서 어떻게 동작하는가 와 연결된다. 2-2. 동적 타입(Dynamic Typing) ⭐️⭐️ JavaScript에서 변수 자체가 영구적으로 하나의 타입을 가지는 것은 아니다. 값이 타입을 가진다. let value = 10; value = "hello"; 처음에는 숫자 값을 가리키고 이후에는 문자열 값을 가리킨다. 따라서 value는 number 타입 변수다 보다 현재 value가 number 값을 가지고 있다 는 식으로 이해하는 편이 JavaScript의 실제 동작에 가깝다. 이 유연성은 빠른 개발에는 도움이 되지만 대규모 프로그램에서는 실행 전 타입 오류를 발견하기 어려워질 수 있다. TypeScript가 JavaScript 생태계에서 중요한 위치를 차지하는 이유도 여기에서 시작한다. 2-3. typeof를 그대로 믿으면 안 되는 이유 ⭐️⭐️ typeof 는 타입을 간단히 확인할 수 있지만 모든 값을 정확하게 분류하는 기능은 아니다. typeof 10 // "number" typeof "hello" // "string" typeof undefined // "undefined" typeof function(){} // "function" typeof null // "object" typeof [] // "object" 특히 typeof null === "object" 는 JavaScript 초창기부터 남아 있는 특성이다. 배열 역시 객체이므로 typeof [] 는 "object" 다. 배열인지 확인하고 싶다면 Array.isArray() 처럼 목적에 맞는 방법을 사용해야 한다. typeof 는 JavaScript 내부 타입 체계를 완전히 보여주는 함수 가 아니라 특정 범주의 값을 구분하기 위한 연산자 로 이해하는 편이 좋다. 2-4. 숫자(Number)는 생각보다 단순하지 않다 ⭐️⭐️ JavaScript는 일반적인 정수와 실수를 모두 주로 number 타입으로 다룬다. 내부적으로 IEEE-754 배정밀도 부동소수점 형식을 사용하기 때문에 다음과 같은 특성이 나타난다. NaN , Infinity , -Infinity , -0 같은 특수 값이 존재하며 typeof NaN 도 "number" 다. 0.1 + 0.2 === 0.3 // false 십진수로는 정확해 보이는 값도 이진 부동소수점으로 표현하면서 오차가 생길 수 있다. 또한 안전하게 정수를 표현할 수 있는 범위에 한계가 있다. Number.MAX_SAFE_INTEGER 이를 넘어서는 큰 정수를 정확하게 다뤄야 한다면 BigInt 를 사용할 수 있다. 금액, 식별자, Timestamp처럼 정확성 자체가 데이터의 의미인 값 을 다룰 때 JavaScript 숫자 모델을 이해해야 하는 이유다. 2-5. undefined와 null ⭐️⭐️⭐️ 둘 모두 값이 없음 과 관련 있지만 일반적인 사용 의미는 다르다. undefined 는 아직 값이 할당되지 않았거나 객체에 해당 프로퍼티가 존재하지 않을 때처럼 JavaScript 실행 과정에서 자연스럽게 등장하는 경우가 많다. null 은 개발자가 의도적으로 값이 존재하지 않음 을 표현할 때 사용한다. let value; console.log(value); // undefined const user = { profile: null }; 실제 API에서는 필드 자체가 제공되지 않았다 와 필드의 값이 명시적으로 비어 있다 가 다른 의미일 수도 있다. 따라서 둘을 무조건 같은 빈 값으로 처리하면 데이터 모델의 의미를 잃을 수 있다. 2-6. 문자열(String)은 원시값인데 어떻게 메서드를 사용할까? 문자열은 원시값이다. 그런데 다음 코드가 가능하다. "hello".toUpperCase() 원시값 자체가 일반 객체로 영구적으로 바뀌는 것은 아니다. JavaScript는 필요한 순간에 String 같은 내장 객체의 기능을 이용할 수 있도록 처리한다. 이러한 동작을 이해하면 원시값(Primitive)과 String , Number , Boolean 같은 래퍼 객체(Wrapper Object)를 혼동하지 않게 된다. 명시적으로 new String("hello") 같은 객체를 만드는 것과 "hello" 라는 원시 문자열은 서로 다른 값이다. 3️⃣ 변수와 메모리를 어떻게 생각해야 할까 3-1. 변수와 식별자(Identifier) ⭐️⭐️ 변수는 값을 저장하고 다시 사용하기 위한 수단이며 식별자(Identifier)는 변수, 함수, 클래스 등의 대상을 구분하는 이름이다. const user = { name: "Esther" }; user 라는 식별자를 이용해 객체에 접근한다. 학습할 때 흔히 변수 하나를 메모리 상자 하나로 그리지만 실제 V8 같은 엔진은 훨씬 복잡한 메모리 최적화를 사용한다. 따라서 메모리 그림은 실제 구현 그대로가 아니라 이름과 값의 연결 관계를 설명하기 위한 개념 모델 로 사용하는 것이 좋다. 3-2. 원시값의 불변성(Immutability) ⭐️⭐️⭐️ 원시값은 불변(Immutable)이다. let str = "hello"; str = "world"; 여기서 "hello" 라는 문자열 자체를 "world" 로 수정한 것이 아니다. str 이 새로운 문자열 값을 가리키도록 재할당한 것이다. 중요한 차이는 다음과 같다. 변수는 변경될 수 있다. 원시값 자체는 변경되지 않는다. 문자열의 특정 위치를 수정할 수 없는 것도 같은 이유다. let str = "hello"; str[0] = "H"; console.log(str); // "hello" 3-3. 객체의 변경 가능성(Mutability) ⭐️⭐️⭐️ 객체는 생성된 이후 내부 프로퍼티를 변경할 수 있다. const user = { name: "Esther" }; user.name = "Reina"; const 인데도 변경 가능한 이유는 const 가 객체 자체를 불변으로 만드는 기능이 아니기 때문이다. 다음은 불가능하다. user = {}; 변수 user 가 다른 값을 다시 가리키게 만드는 재할당은 금지된다. 하지만 다음은 가능하다. user.name = "Reina"; user 가 가리키는 객체는 그대로이고 그 객체 내부의 프로퍼티만 변경했기 때문이다. 4️⃣ 값 복사와 참조 공유 4-1. 원시값 복사 ⭐️⭐️⭐️ let a = 10; let b = a; b = 20; b 를 변경해도 a 는 여전히 10 이다. 원시값은 값의 의미가 독립적으로 전달되기 때문에 두 변수를 각각 변경할 수 있다. 4-2. 객체 참조 공유 ⭐️⭐️⭐️ 객체에서는 다른 현상이 나타난다. const a = { count: 1 }; const b = a; b.count = 2; console.log(a.count); // 2 두 변수가 같은 객체에 접근하고 있기 때문이다. JavaScript를 설명할 때 객체는 참조에 의해 전달된다(Pass-by-Reference) 라고 표현하기도 하지만 정확하게 이해하려면 조금 더 주의해야 한다. 함수에 전달되는 것 역시 값 이다. 다만 객체를 다루는 변수의 값이 객체에 접근할 수 있는 참조이기 때문에 그 참조 값이 복사되면서 양쪽에서 동일한 객체에 접근할 수 있게 된다. a ─┐ ├──▶ { count: 1 } b ─┘ 4-3. 얕은 복사(Shallow Copy) ⭐️⭐️⭐️ 전개 구문(Spread)을 사용하면 객체를 간단히 복사할 수 있다. const copied = { ...original }; 하지만 바깥 객체만 새로운 객체가 된다. const original = { profile: { age: 20 } }; const copied = { ...original }; copied.profile.age = 30; original.profile 과 copied.profile 은 같은 객체를 가리킨다. 따라서 원본의 profile.age 도 변경된다. 4-4. 깊은 복사(Deep Copy) ⭐️⭐️ 중첩된 객체까지 독립적인 객체로 만들려면 깊은 복사(Deep Copy)가 필요하다. 현대 환경에서는 지원 가능한 값이라면 structuredClone() 을 사용할 수 있다. 과거에는 다음과 같은 방식도 자주 사용됐다. JSON.parse(JSON.stringify(obj)) 하지만 undefined , 함수, BigInt , 일부 내장 객체 등의 데이터를 제대로 보존하지 못할 수 있기 때문에 일반적인 깊은 복사 도구로 보는 것은 위험하다. 복사는 단순한 문법 문제가 아니라 새로운 객체가 필요한가, 같은 객체를 공유해도 되는가 를 결정하는 설계 문제다. 5️⃣ 타입 변환과 값 비교 5-1. 암묵적 타입 변환(Type Coercion) ⭐️⭐️⭐️ JavaScript는 서로 다른 타입이 연산에 사용될 때 자동으로 타입을 변환하기도 한다. "10" + 1 // "101" "10" - 1 // 9 + 에서는 문자열 연결이 가능하므로 숫자가 문자열로 변환된다. 반면 - 는 숫자 연산이므로 문자열이 숫자로 변환된다. 이런 결과를 하나씩 암기하는 것보다 현재 연산자가 어떤 값을 필요로 하고 어떤 변환이 일어나는지 를 따라가는 편이 중요하다. 5-2. 명시적 타입 변환(Type Conversion) ⭐️⭐️ 개발자가 변환 의도를 직접 표현할 수도 있다. Number() , String() , Boolean() 이 대표적이다. 명시적인 변환은 코드를 읽는 사람에게도 의도를 분명히 보여주기 때문에 복잡한 데이터 변환에서는 암묵적 변환에 지나치게 의존하지 않는 편이 좋다. 5-3. 참 같은 값(Truthy)과 거짓 같은 값(Falsy) ⭐️⭐️⭐️ JavaScript의 조건식에는 반드시 true 와 false 만 들어갈 필요가 없다. 값이 불리언(Boolean)으로 변환되었을 때 거짓으로 평가되는 대표적인 값은 다음과 같다. false , 0 , -0 , 0n , "" , null , undefined , NaN 나머지 대부분의 값은 Truthy다. 특히 빈 배열 [] 과 빈 객체 {} 도 Truthy다. if ([]) { // 실행됨 } 비어 있으니까 false겠지 라는 직관과 다르게 동작할 수 있기 때문에 자주 실수한다. 5-4. 느슨한 동등 비교(==)와 엄격한 동등 비교(===) ⭐️⭐️⭐️ == 는 타입이 다르면 암묵적 타입 변환 후 비교할 수 있다. 0 == false // true === 는 타입을 변환하지 않고 타입과 값을 함께 비교한다. 0 === false // false 일반적인 애플리케이션에서는 === 를 기본으로 사용하는 편이 예측하기 쉽다. 하지만 === 도 모든 값을 완벽하게 구분하지는 않는다. NaN === NaN // false 0 === -0 // true 이런 특수한 차이까지 구분해야 한다면 Object.is() 를 사용할 수 있다. 5-5. 논리 연산자의 단축 평가(Short-circuit Evaluation) ⭐️⭐️ && , || 는 반드시 Boolean 값만 반환하지 않는다. || 는 왼쪽 값이 Falsy라면 오른쪽 값을 반환한다. 0 || 10 // 10 문제는 0 , "" , false 도 정상적인 데이터일 수 있다는 것이다. 널 병합 연산자(Nullish Coalescing) ?? 는 null 또는 undefined 일 때만 오른쪽 값을 사용한다. 0 ?? 10 // 0 따라서 기본값을 처리할 때 두 연산자를 같은 의미로 사용하면 안 된다. 5-6. 선택적 연결(Optional Chaining) ⭐️⭐️ 선택적 연결 ?. 은 접근 중간의 값이 null 또는 undefined 라면 오류를 발생시키지 않고 undefined 를 반환한다. user.profile?.address?.city 중첩 객체를 안전하게 읽을 때 유용하지만 데이터가 존재해야 하는데도 ?. 를 남용하면 실제 오류를 조용히 숨길 수도 있다. 6️⃣ var, let, const 6-1. var ⭐️⭐️⭐️ var 는 함수 레벨 스코프(Function-level Scope)를 가진다. if (true) { var value = 10; } console.log(value); // 10 블록 {} 을 벗어나도 같은 함수 안이라면 접근할 수 있다. 또한 같은 스코프에서 중복 선언이 가능하다. var value = 10; var value = 20; 큰 코드베이스에서는 이런 느슨한 규칙이 실수로 이어질 수 있다. 6-2. let ⭐️⭐️⭐️ let 은 블록 레벨 스코프(Block-level Scope)를 가진다. if (true) { let value = 10; } console.log(value); // ReferenceError 같은 스코프에서 중복 선언은 허용되지 않지만 재할당은 가능하다. 6-3. const ⭐️⭐️⭐️ const 역시 블록 스코프를 가지며 중복 선언할 수 없다. 또한 반드시 선언과 동시에 초기화해야 하며 이후 재할당할 수 없다. const count = 10; // count = 20; // 불가능 다만 앞에서 보았듯 객체 내부 프로퍼티 변경까지 막는 것은 아니다. 6-4. 현대 JavaScript에서 무엇을 기본으로 사용할까? ⭐️⭐️ 일반적으로는 const 를 기본으로 사용하고 값 자체를 다시 할당해야 할 때 let 을 사용하는 방식이 읽기 쉽다. var 를 무조건 사용하면 안 된다는 규칙이라기보다 블록 스코프와 중복 선언 제한을 제공하는 let/const가 코드의 상태 변경 범위를 더 명확하게 보여준다 는 것이 핵심이다. 7️⃣ 호이스팅(Hoisting) 7-1. 호이스팅은 코드를 위로 끌어올리는 기능이 아니다 ⭐️⭐️⭐️ 흔히 다음 코드를 설명할 때 var 선언이 코드 위로 올라갔다 고 말한다. console.log(value); // undefined var value = 10; 하지만 실제 소스코드가 물리적으로 이동하는 것은 아니다. JavaScript 엔진이 코드를 실행하기 전에 현재 실행 영역에 어떤 선언들이 존재하는지 먼저 처리하기 때문에 선언문보다 앞에서도 식별자의 존재를 알 수 있는 것이다. 이 동작을 이해하려면 평가 단계 와 실행 단계 를 구분해야 한다. 7-2. 변수 선언은 한 번에 일어나지 않는다 ⭐️⭐️⭐️ 개념적으로 변수 선언은 다음 과정으로 나누어 생각할 수 있다. 선언 → 초기화 → 할당 선언은 식별자의 존재를 등록하고, 초기화는 값을 저장할 수 있는 상태를 준비하며, 할당은 실제 값을 연결한다. var 는 선언과 초기화 과정에서 undefined 가 연결되는 반면 let/const 는 초기화되기 전에 접근할 수 없다. 이 차이가 호이스팅 결과의 차이를 만든다. 7-3. 일시적 사각지대(Temporal Dead Zone) ⭐️⭐️⭐️ console.log(value); const value = 10; value 라는 식별자가 아예 존재하지 않는 것은 아니다. 현재 스코프에 이미 등록되어 있지만 선언문이 실행되어 초기화되기 전까지 접근할 수 없다. 이 구간을 일시적 사각지대(TDZ)라고 한다. 따라서 let과 const는 호이스팅되지 않는다 는 설명은 부정확하다. 호이스팅은 되지만 초기화 이전 접근이 차단된다. 8️⃣ 함수는 JavaScript에서 특별한 값이다 8-1. 일급 객체(First-
Score: 54.35Confidence: 49%
Score: 54.35Confidence: 49%
Score: 54.34Confidence: 49%
Score: 54.34Confidence: 49%
Score: 54.33Confidence: 49%