Step 74. requests 라이브러리 — 파이썬으로 웹과 대화하기

Step 74. requests 라이브러리 — 파이썬으로 웹과 대화하기

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 73을 마쳤다. HTTP 요청/응답 구조, GET과 POST, 헤더와 상태코드를 안다. 파이썬 스크립트를 작성·실행할 수 있고 pip을 써 봤다.

  • 준비물: 파이썬, 터미널 두 개(서버용 하나 + 실험용 하나). 인터넷 연결은 필요 없습니다 — 오늘도 대화 상대는 우리가 직접 만드는 localhost 서버입니다.
  • 주의: 오늘 실습은 100% 안전합니다. 모든 요청은 127.0.0.1 안에서만 오갑니다.

지난 시간 우리는 curl로 서버와 대화했습니다. 명령 한 줄에 요청 하나. 훌륭하지만 한계가 있습니다 — 백 개의 페이지를 긁으려면 명령을 백 번 쳐야 하고, 응답에서 원하는 값만 뽑으려면 눈으로 찾아야 합니다. 자동화가 없으면 우리는 수집가가 아니라 타이핑 노예입니다. requests는 파이썬에서 HTTP 요청을 보내는 가장 유명한 라이브러리로, 이 하나로 GET/POST, 파라미터, 헤더, 쿠키를 코드 몇 줄로 다룹니다. 웹 스캐너, 수집기, 테스트 도구의 심장이 바로 이 라이브러리입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • POST와 쿠키를 이해하는 연습용 서버(lab.py)를 파이썬으로 직접 만든다
  • requests.get/post로 요청을 보내고 응답 객체의 status_code, headers, text를 읽는다
  • params(주소)와 data(본문)의 차이를 코드로 구분해 쓴다
  • Session으로 쿠키를 유지하며 "로그인된 척 연속 행동"의 원리를 실험한다
  • timeout과 예외 처리로 멈추지 않는 요청 코드를 만든다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3 + requests 라이브러리 (pip install requests). 실습 상대는 127.0.0.1:8010의 자체 제작 서버
오늘의 도구 requests.get/post, 응답 객체, requests.Session, 그리고 서버 쪽의 http.server 모듈
오늘의 함수 requests.get(주소, params=, headers=, timeout=), requests.post(주소, data=), r.status_code / r.headers / r.text / r.json() / r.url / r.history
필요한 개념 요청/응답 구조(Step 73), 파이썬 사전, 쿠키와 세션
오늘의 산출물 lab.py — 연습용 서버, analyze.py — 응답 분석기

2-1. 응답 객체라는 선물 상자

requests.get(...)을 호출하면 돌아오는 것은 응답 객체(response object)입니다. "속성(주머니)이 여러 개 달린 선물 상자"라고 생각하세요.

속성 내용 예시
r.status_code 상태코드 (숫자) 200
r.headers 응답 헤더 (사전처럼 사용) r.headers["Content-Type"]
r.text 응답 본문 (문자열) "<!DOCTYPE html>..."
r.json() 본문을 JSON으로 풀어 사전으로 반환 JSON 응답일 때만
r.url 실제로 요청이 간 최종 주소 리다이렉트 확인용
r.history 거쳐 온 리다이렉트 기록 경로 추적용

지난 시간 curl -v에서 눈으로 읽던 ><의 내용이, 이제는 코드에서 점(.)으로 꺼내 쓰는 속성이 된 것입니다.

2-2. params와 data — 싣는 위치의 차이

Step 73에서 배운 GET과 POST의 차이가 코드에서는 이렇게 갈립니다.

  • params={"id": "admin"} → 주소 뒤에 붙습니다 (?id=admin). GET 요청의 데이터입니다.
  • data={"pw": "1234"} → 본문에 들어갑니다. POST 요청의 데이터입니다.

파이썬 사전(dictionary, 이름표-값 쌍의 묶음)을 그대로 넘기면 requests가 알아서 HTTP 형식으로 포장합니다. 우리가 손으로 ?id=admin&pw=1234를 조립할 필요가 없습니다.

2-3. 세션 — 신분증을 계속 내미는 손

HTTP는 원래 "기억력이 없는" 규약입니다. 서버는 각 요청을 별개의 손님으로 봅니다. 로그인에 성공해도 다음 요청에서는 다시 낯선 사람 취급입니다. 이것을 해결하는 것이 쿠키(cookie, 서버가 발급한 신분증 쪽지)이고, 코드에서 쿠키를 자동으로 챙겨 주는 것이 requests.Session()입니다. 세션 객체로 요청하면, 첫 요청에서 받은 쿠키를 다음 요청에 자동으로 붙여 줍니다 — 브라우저가 하던 일의 흉내입니다.


3. 따라 하기

3-1. 설치와 연습용 서버 만들기

입력

pip install requests

이번에는 대화 상대가 될 서버를 직접 만듭니다. 지난 시간의 python -m http.server는 GET만 알았지요. 이 서버는 POST도 받고, 쿠키도 심어 줍니다. lab.py를 만드세요.

from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

QUOTES_PAGE = """<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>명언 연습장</title></head>
<body>
  <h1>오늘의 명언</h1>
  <div class="quote">
    <span class="text">가장 큰 위험은 위험 없는 삶이다.</span>
    <small class="author">알레르 허브</small>
    <div class="tags"><a class="tag" href="/tag/life">life</a></div>
  </div>
  <div class="quote">
    <span class="text">백 번 듣는 것이 한 번 보는 것만 못하다.</span>
    <small class="author">속담</small>
    <div class="tags"><a class="tag" href="/tag/wisdom">wisdom</a></div>
  </div>
  <div class="quote">
    <span class="text">완벽함은 아무것도 더할 것이 없을 때가 아니라, 뺄 것이 없을 때다.</span>
    <small class="author">생텍쥐페리</small>
    <div class="tags"><a class="tag" href="/tag/design">design</a> <a class="tag" href="/tag/wisdom">wisdom</a></div>
  </div>
</body>
</html>"""

class LabHandler(BaseHTTPRequestHandler):
    def _send(self, code, body, ctype="text/html; charset=utf-8", extra=None):
        data = body.encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(data)))
        for k, v in (extra or {}).items():
            self.send_header(k, v)
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self):
        path = urlparse(self.path).path
        if path == "/":
            self._send(200, QUOTES_PAGE)
        elif path == "/headers":
            lines = [f"{k}: {v}" for k, v in self.headers.items()]
            self._send(200, "\n".join(lines), "text/plain; charset=utf-8")
        elif path == "/set":
            self._send(200, "쿠키를 심었습니다.",
                       extra={"Set-Cookie": "session_id=abc123; Path=/"})
        elif path == "/who":
            cookie = self.headers.get("Cookie", "(쿠키 없음)")
            self._send(200, f"서버가 받은 쿠키: {cookie}",
                       "text/plain; charset=utf-8")
        elif path == "/old":
            self.send_response(301)
            self.send_header("Location", "/")
            self.end_headers()
        else:
            self._send(404, "<h1>404 — 그런 페이지 없습니다</h1>")

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        raw = self.rfile.read(length).decode("utf-8")
        fields = parse_qs(raw)
        flat = {k: v[0] for k, v in fields.items()}
        self._send(200, f"서버가 받은 POST 본문: {raw}\n풀어 헤친 결과: {flat}",
                   "text/plain; charset=utf-8")

if __name__ == "__main__":
    print("실습 서버 시작: http://127.0.0.1:8010  (종료: Ctrl+C)")
    HTTPServer(("127.0.0.1", 8010), LabHandler).serve_forever()

입력 (터미널 1 — 서버용)

python lab.py

출력 (2026-09-09 실측):

실습 서버 시작: http://127.0.0.1:8010  (종료: Ctrl+C)

읽는 법: 겁내지 마세요. 길지만 하는 일은 단순합니다 — 경로(path)마다 정해진 대답을 돌려주는 분기문입니다. /headers는 받은 헤더를 그대로 돌려주는 거울, /set은 쿠키를 심는 창구, /who는 받은 쿠키를 보여 주는 창구, /echo는 POST 본문을 돌려주는 창구입니다. 이 서버는 이 챕터와 크롤링 챕터에서 계속 씁니다.

: 서버를 직접 만들면 "서버는 요청 줄과 헤더를 읽고, 정해진 응답을 돌려주는 프로그램"이라는 것이 몸에 남습니다. 공격자의 눈으로 서버를 보려면 서버의 입장을 한 번은 되어 봐야 합니다.

3-2. 첫 요청과 응답 해부

터미널 2를 열고 req1.py를 만듭니다.

입력

import requests

r = requests.get("http://127.0.0.1:8010/")
print("상태코드:", r.status_code)
print("Content-Type:", r.headers["Content-Type"])
print("본문 앞 60자:", repr(r.text[:60]))

출력 (2026-09-09 실측):

상태코드: 200
Content-Type: text/html; charset=utf-8
본문 앞 60자: '<!DOCTYPE html>\n<html>\n<head><meta charset="utf-8"><title>명언'

읽는 법: 세 줄의 코드로 GET 요청이 날아갔고, 돌아온 상자에서 상태코드와 헤더와 본문을 꺼내 찍었습니다. 본문은 우리가 lab.py에 적은 그 HTML입니다. 지난 시간의 curl 한 줄이 코드 몇 줄로 바뀐 것에 불과해 보이지만, 이제 이 코드를 반복문에 넣을 수 있다는 점이 다릅니다.

3-3. 예측해 보기 — params는 주소 어디에 붙을까

예측입니다. 아래 코드에서 r.url에 찍히는 최종 주소는 어떤 모습일까요?

입력

r = requests.get("http://127.0.0.1:8010/headers", params={"q": "security", "page": "2"})
print("최종 주소:", r.url)

출력 (2026-09-09 실측):

최종 주소: http://127.0.0.1:8010/headers?q=security&page=2

읽는 법: 사전으로 넘긴 두 개의 이름표-값이 ? 뒤에 &로 이어져 붙었습니다. 손으로 조립하던 GET 주소를 requests가 대신 만들어 준 것입니다. 참고로 우리 서버의 /headers는 주소 뒷부분을 무시하고 헤더만 돌려주지만, 주소 자체는 보시다시피 완성되어 날아갔습니다.

: 검색창에 단어를 칠 때 주소가 길어지는 현상의 정체가 이것입니다. 그리고 "이 주소의 파라미터를 바꾸면 서버가 다르게 반응할까?"라는 웹 테스트의 첫 질문이 바로 이 자리에서 나옵니다.

3-4. POST와 헤더 변장

입력

r = requests.post("http://127.0.0.1:8010/echo", data={"id": "admin", "pw": "1234"})
print(r.text)

r2 = requests.get("http://127.0.0.1:8010/headers",
                  headers={"User-Agent": "MySecurityStudyBot/1.0"})
print(r2.text)

출력 (2026-09-09 실측):

서버가 받은 POST 본문: id=admin&pw=1234
풀어 헤친 결과: {'id': 'admin', 'pw': '1234'}

Host: 127.0.0.1:8010
User-Agent: MySecurityStudyBot/1.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

읽는 법: 첫 번째는 POST 본문에 실린 데이터가 서버에 그대로 도착해 풀어 헤쳐진 증거입니다 — 이름=값&이름=값 형태로 실려 갔다는 것도 보입니다. 두 번째는 서버가 본 헤더 목록입니다. 내가 바꾼 User-Agent가 그대로 보이고, requests가 기본으로 붙인 헤더(Accept-Encoding, Connection 등)도 함께 보입니다.

: "요청의 모든 부분은 내가 조립할 수 있다"는 확신을 갖는 단계입니다. 웹 테스트 도구의 본질이 이 조립 자유도입니다.

3-5. 세션 — 쿠키가 유지되는지 실험

입력

s = requests.Session()
s.get("http://127.0.0.1:8010/set")          # 쿠키를 심는 창구
r = s.get("http://127.0.0.1:8010/who")      # 같은 세션으로 확인
print("세션으로:", r.text)

r2 = requests.get("http://127.0.0.1:8010/who")   # 세션 없이 새로 요청
print("세션 없이:", r2.text)

출력 (2026-09-09 실측):

세션으로: 서버가 받은 쿠키: session_id=abc123
세션 없이: 서버가 받은 쿠키: (쿠키 없음)

읽는 법: 세션으로 쿠키를 심은 뒤 같은 세션으로 물으니 쿠키가 살아 있습니다. 세션 없이 보낸 요청은 빈 쿠키입니다. 세션이 "신분증 지갑" 역할을 한다는 직접적인 증거입니다.

: 로그인 → 글 목록 → 글쓰기처럼 "인증이 필요한 연속 동작"을 코드로 할 때 세션이 필수입니다. 이 흐름을 한 세션에 담는 것이 웹 자동화의 골격입니다. 반대로 공격자가 남의 쿠키를 훔치면(XSS 등으로) 비밀번호 없이 그 사람인 척할 수 있습니다 — 쿠키가 신분증이라는 말의 무게입니다.

3-6. 예측해 보기 — 리다이렉트는 어떻게 기록될까

우리 서버의 /old는 "이사갔다(301), 새 주소는 /"라고만 대답합니다. requests로 요청하면 최종 도착지는 어디이고, 그 기록은 어디에 남을까요?

입력

r = requests.get("http://127.0.0.1:8010/old")
print("최종 주소:", r.url)
print("거쳐온 안내:", [(x.status_code, x.url) for x in r.history])

출력 (2026-09-09 실측):

최종 주소: http://127.0.0.1:8010/
거쳐온 안내: [(301, 'http://127.0.0.1:8010/old')]

읽는 법: requests는 curl과 달리 리다이렉트를 기본으로 따라갑니다. 그리고 r.history에 거쳐 온 발자국(301 안내)이 남습니다. "최종적으로 어디에 도착했는가"와 "어떤 길로 왔는가"가 분리되어 보입니다.

: 단축 주소의 실제 목적지 확인, 피싱 사이트의 우회 경로 추적 — 이런 분석이 전부 이 history 속성에서 출발합니다.

3-7. 멈추지 않는 코드 — timeout과 예외 처리

자동화 도구의 생존 장치를 붙입니다.

입력

import requests

try:
    r = requests.get("http://127.0.0.1:59999/", timeout=3)
except requests.exceptions.ConnectionError:
    print("요청 실패: 서버에 닿지 않습니다")

try:
    r = requests.get("http://127.0.0.1:8010/", timeout=3)
    print("성공:", r.status_code)
except requests.exceptions.ConnectionError:
    print("요청 실패: 서버에 닿지 않습니다")

출력 (2026-09-09 실측):

요청 실패: 서버에 닿지 않습니다
성공: 200

읽는 법: 59999번에는 서버가 없으니 ConnectionError가 나고, 우리가 잡아서 안내 문구를 냈습니다. timeout=3은 "3초 안에 답이 없으면 포기하라"는 장치입니다. 예외를 잡지 않았다면 프로그램은 긴 오류 메시지와 함께 죽었을 것입니다 — 실제 메시지는 ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=59999): Max retries exceeded ... 같은 모양입니다 (2026-09-09 실측).

: requests는 timeout 없이는 서버가 침묵하면 무한정 기다립니다. 백 페이지를 긁는 도구가 37번째에서 영원히 멈추는 것을 막는 한 줄입니다.


4. 미션과 연습문제

미션 — 응답 분석기 만들기

analyze.py를 만들어 다음을 수행하세요 (lab.py 서버가 켜져 있어야 합니다).

  1. /headers에 params 두 개를 달아 GET 요청하고, 최종 URL과 상태코드를 출력합니다
  2. 응답 본문에서 서버가 본 User-Agent 줄만 골라 출력합니다
  3. User-Agent를 자기만의 문자열로 바꿔 같은 요청을 다시 보내고, 서버가 본 내 신상이 바뀌었는지 출력으로 확인합니다
  4. /echo에 POST로 이름표-값 세 개를 보내고, 서버가 풀어 헤친 결과를 출력합니다
  5. 모든 요청을 try/except로 감싸고 timeout=3을 붙여, 서버가 꺼져 있어도 "요청 실패" 안내를 남기고 정상 종료하게 합니다

연습문제

문제 1. paramsdata의 차이를 "데이터가 실리는 위치"와 "함께 쓰는 메서드"로 설명해 보세요.

문제 2. r.json()을 호출했더니 JSONDecodeError: Expecting value: line 1 column 1 (char 0)가 떴습니다 (2026-09-09 실측). 무슨 상황이며, 호출 전에 무엇을 확인해야 하나요?

문제 3. 세션 없이 requests.get(...)만으로 로그인과 글쓰기를 연달아 하면 왜 로그인이 풀리는지, 쿠키의 개념으로 설명해 보세요.

문제 4. r.urlr.history의 차이를 "최종 도착지"와 "거쳐 온 길"로 설명해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

import requests

BASE = "http://127.0.0.1:8010"

try:
    # 1. params 달기
    r = requests.get(f"{BASE}/headers", params={"q": "security", "page": "2"}, timeout=3)
    print("최종 URL:", r.url)
    print("상태코드:", r.status_code)

    # 2. 서버가 본 User-Agent 줄 고르기
    for line in r.text.splitlines():
        if line.startswith("User-Agent"):
            print("서버가 본 UA:", line)

    # 3. 변장 후 재확인
    r2 = requests.get(f"{BASE}/headers",
                      headers={"User-Agent": "MyAnalyzer/2.0"}, timeout=3)
    for line in r2.text.splitlines():
        if line.startswith("User-Agent"):
            print("변장 후 서버가 본 UA:", line)

    # 4. POST
    r3 = requests.post(f"{BASE}/echo",
                       data={"id": "admin", "pw": "1234", "memo": "첫 실습"},
                       timeout=3)
    print(r3.text)

except requests.exceptions.ConnectionError:
    print("요청 실패: lab.py 서버가 켜져 있는지 확인하세요")
except requests.exceptions.Timeout:
    print("요청 실패: 서버가 3초 안에 답하지 않았습니다")

실행 결과 (2026-09-09 실측, 일부):

최종 URL: http://127.0.0.1:8010/headers?q=security&page=2
상태코드: 200
서버가 본 UA: User-Agent: python-requests/2.33.1
변장 후 서버가 본 UA: User-Agent: MyAnalyzer/2.0
서버가 받은 POST 본문: id=admin&pw=1234&memo=%EC%B2%AB+%EC%8B%A4%EC%8A%B5
풀어 헤친 결과: {'id': 'admin', 'pw': '1234', 'memo': '첫 실습'}

아무 변장도 안 했을 때 requests는 스스로를 python-requests/버전이라고 신고한다는 것 — 이것도 오늘의 발견입니다. 그리고 POST 본문에서 한글 "첫 실습"이 %EC%B2%AB...으로 바뀐 것을 보세요. 폼 데이터는 URL 인코딩(주소에 실을 수 있는 글자로의 변환)되어 전송되고, 서버는 그것을 다시 풀어서 씁니다. Step 50의 인코딩 감각이 여기서도 쓰입니다.

검증하는 법: ① 서버를 끄고 실행했을 때 프로그램이 죽지 않고 안내를 남기는가. ② 변장 전후의 User-Agent 줄이 다른가. ③ POST의 세 이름표가 풀어 헤친 결과에 모두 있는가. 셋이 전부 ‘예’이면 완성입니다.

연습문제 해답

문제 1 해답. params는 주소 뒤(?이름=값)에 실리고 주로 GET과 함께 쓰며, data는 요청 본문에 실리고 주로 POST와 함께 씁니다. 둘 다 사전으로 넘기면 requests가 HTTP 형식으로 포장합니다.

문제 2 해답. 응답 본문이 JSON이 아닌 상황입니다 — 우리 실습 서버의 /처럼 HTML이 돌아온 경우입니다 (char 0, 즉 첫 글자부터 JSON이 아니라는 뜻). json() 호출 전에 r.status_code를 보고 r.text[:200]으로 본문 앞부분을 먼저 들여다보는 습관이 필요합니다.

문제 3 해답. HTTP는 요청마다 기억이 없어서, 로그인 성공의 증거(세션 쿠키)를 다음 요청에 다시 내밀어야 합니다. requests.get(...)을 매번 새로 부르면 쿠키를 보관할 지갑이 없어서 매번 낯선 사람이 됩니다. Session 객체가 그 지갑입니다 (3-5 실측: 세션은 session_id=abc123을 유지했고 세션 없는 요청은 빈 쿠키였습니다).

문제 4 해답. r.url은 리다이렉트를 다 따라간 뒤의 최종 주소이고, r.history는 그때까지 거쳐 온 안내(301 등)의 목록입니다. 3-6 실측처럼 최종 주소는 /인데 history에는 (301, .../old)가 남는 식입니다.

완료 기준 체크리스트

  • [ ] lab.py 연습 서버를 만들고 켜고 끌 수 있다
  • [ ] requests.get/post로 요청하고 status_code와 headers를 읽을 수 있다
  • [ ] params와 data의 차이(주소 vs 본문)를 설명할 수 있다
  • [ ] Session으로 쿠키가 유지되는 것을 실험으로 확인했다
  • [ ] timeout과 try/except를 습관적으로 붙인다
  • [ ] r.url과 r.history로 리다이렉트를 추적할 수 있다
  • [ ] 미션: 응답 분석기를 완성했다

6. 흔한 실수와 해결

벽 1. 서버를 안 켜고 클라이언트부터 실행했다

증상 (2026-09-09 실측):

requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8010): Max retries exceeded ... (Caused by NewConnectionError(... [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다))

원인: 대화 상대(lab.py)가 켜져 있지 않습니다.
해결: 터미널 1에서 python lab.py로 서버를 먼저 켜세요. 서버 창은 계속 떠 있어야 합니다.

벽 2. 서버를 켤 때 포트가 막혀 있다

증상: OSError: [WinError 10048] ... — 지정한 포트를 이미 쓰고 있습니다.
원인: 꺼지지 않은 이전 서버나 다른 프로그램이 그 번호를 점유 중입니다.
해결: lab.py의 8010을 8020 등으로 바꾸고, 클라이언트 코드의 주소도 같이 바꾸세요.

벽 3. r.json()에서 오류가 난다

증상 (2026-09-09 실측): requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

원인: 응답 본문이 JSON이 아닙니다 (HTML 페이지 등). 상태코드가 200이 아닐 때도 흔합니다.
해결: json() 전에 r.status_code == 200인지 보고, print(r.text[:200])로 본문 앞부분을 먼저 확인하세요.

벽 4. 요청이 한참 멈춰 있다

증상: get()이 몇 분째 돌아오지 않습니다.
원인: 상대가 응답하지 않는데 requests는 기본으로 무한정 기다립니다.
해결: timeout=5를 습관적으로 붙이세요. 답이 없으면 오류를 내며 포기합니다. 자동화 도구의 무한 대기는 곧 멈춤입니다.

벽 5. 분명 쿠키를 받았는데 다음 요청에서 사라진다

증상: /set으로 쿠키를 심었는데 /who에서 "(쿠키 없음)"이 옵니다.
원인: 매번 requests.get(...)으로 새 요청을 보냈습니다. 쿠키가 저장될 지갑이 없습니다.
해결: s = requests.Session()을 만들고 모든 요청을 s.get(...), s.post(...)로 보내세요 (3-5 실측이 그 증거입니다).


7. 정리

오늘의 개념

개념 한 줄 설명
requests 파이썬의 대표 HTTP 클라이언트 라이브러리
응답 객체 status_code, headers, text, json() 등 주머니 달린 선물 상자
params / data 주소에 싣기(GET) / 본문에 싣기(POST)
쿠키 서버가 발급하는 신분증 쪽지 — 응답의 Set-Cookie로 심고, 요청의 Cookie 헤더로 내민다
Session 쿠키를 자동으로 보관·첨부하는 지갑 객체
timeout "이 시간 안에 답 없으면 포기" — 자동화의 생존 장치

오늘의 함수

함수·속성 하는 일
requests.get(주소, params=, headers=, timeout=) GET 요청
requests.post(주소, data=, timeout=) POST 요청
r.status_code / r.headers / r.text 상태코드 / 응답 헤더 / 본문 문자열
r.json() JSON 본문 → 파이썬 사전 (JSON일 때만!)
r.url / r.history 최종 주소 / 거쳐 온 리다이렉트 목록
requests.Session() 쿠키 지갑 만들기
python lab.py 연습용 서버 켜기 (종료: Ctrl+C)

명령어보다 중요한 감각

오늘의 핵심 감각은 응답을 의심하는 습관입니다. 초보는 "응답이 왔으니 성공"이라 믿지만, 실무자의 확인 순서는 정해져 있습니다 — ① status_code 보기 ② r.text 앞 200자 찍어 보기 ③ 그다음에 json()이나 파싱. 이 삼단이 "왜 안 되지?" 하는 디버깅 시간의 절반을 지워 줍니다. 보안과의 연결: 오늘 배운 것은 남용하면 위험합니다. 같은 요청을 쉬지 않고 때리는 코드는 그 자체로 서비스 방해가 되므로, 반복 요청에는 time.sleep(1) 같은 쉼표를 넣는 것이 기술 이전의 태도입니다. 그리고 오늘 직접 만들어 본 lab.py를 기억하세요 — 서버는 결국 "요청을 읽고 정해진 대답을 내놓는 분기문"입니다. 이 그림이 있으면 웹의 어떤 신기술을 만나도 겁이 나지 않습니다.


전부 체크되면 Step 74 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.