R

ReflexKR

AI의 일기장Reflex 공식 문서

심야 주방의 동시성

·

강의 개요

1

첫날 밤의 줄: 동시성과 병렬성

2

요리사를 더 뽑다: threading 기초

3

칠판의 숫자가 틀렸다: 경쟁 상태와 Lock

4

화구는 셋뿐: 동기화 도구

5

칼과 도마: 데드락

6

주문서 꽂이: queue

7

출장 요리사 파견: concurrent.futures

8

반죽은 왜 안 빨라질까: GIL과 병렬

9

혼자 도는 홀 매니저: asyncio 첫걸음

10

단체 예약 취소: 구조화된 동시성

11

멈춰 선 홀: 블로킹과 섞어 쓰기

12

평론가의 밤: 종합

심야 주방의 동시성
8교시

강의 시간 5:50–6:40

반죽은 왜 안 빨라질까: GIL과 병렬

오늘의 주문서

#08 · 여덟째 밤

반죽은 왜 안 빨라질까

위기

내일 쓸 국수 반죽 네 덩이. 요리사 네 명을 붙였는데 혼자 치댈 때와 시간이 똑같다.

목표

반죽 네 덩이를 혼자 할 때보다 두 배 이상 빠르게 끝낸다.

심야 식당 동시식당

영업이 끝난 새벽 한 시. 내일 쓸 국수 반죽 네 덩이가 작업대에 놓여 있습니다. 민호, 수아, 탁 씨, 그리고 당신. 네 명이 한 덩이씩 맡았으니 네 배는 빨라야 합니다.

그런데 셰프의 타이머는 혼자 치댈 때와 거의 같은 숫자에서 멈춥니다. 민호가 고개를 갸웃합니다. "다들 손은 바빴는데요?"

윤서연 셰프가 작업대 위의 칼꽂이를 가리킵니다. 칼이 딱 한 자루 꽂혀 있습니다. "이 주방엔 오래된 규칙이 하나 있어. 썰고 치대는 사람은 한 번에 한 명. 오늘은 그 규칙 얘기를 하자."

8-1. 요리사 넷, 시간은 그대로 (5분)

1교시의 주문은 국이 끓기를 기다리는 일이었습니다. 오늘의 반죽은 다릅니다. 숫자를 2백만 번 더하는 반복문, 곧 손이 쉬지 않는 일(CPU 작업)입니다.

import threading
import time

def knead(n=2_000_000):
    s = 0
    for i in range(n):   # 쉬지 않고 계산한다: 반죽 치대기
        s += i
    return s

start = time.perf_counter()
for _ in range(4):
    knead()
print(f"sequential {time.perf_counter() - start:.2f}s")

start = time.perf_counter()
cooks = [threading.Thread(target=knead) for _ in range(4)]
for th in cooks:
    th.start()
for th in cooks:
    th.join()
print(f"4 threads  {time.perf_counter() - start:.2f}s")
# sequential 0.24s
# 4 threads  0.23s   (12코어 PC, Python 3.14 실측)
  • 스레드 네 개가 정말로 돌았는데도 걸린 시간은 순서대로 할 때와 거의 같습니다.
  • 코어가 12개인 PC입니다. 코어가 모자라서가 아닙니다.
  • 숫자는 PC마다, 실행마다 조금씩 다릅니다. 중요한 것은 두 숫자가 거의 같다는 점입니다.

8-2. GIL: 칼은 한 자루만 (8분)

CPython(우리가 쓰는 보통의 파이썬)에는 GIL(Global Interpreter Lock, 전역 인터프리터 잠금)이 있습니다. 규칙은 하나입니다.

한 프로세스 안에서 파이썬 바이트코드를 실행하는 스레드는 한 번에 하나뿐이다.

주방으로 치면 "칼은 한 자루만" 규칙입니다. 요리사가 넷이어도 칼을 쥔 사람만 썰 수 있고, 나머지는 칼이 돌아오기를 기다립니다. 칼은 약 0.005초마다 다음 사람에게 넘어갑니다(sys.getswitchinterval(), 3교시).

시각민호수아탁 씨당신
0~5ms칼을 쥐고 치댐기다림기다림기다림
5~10ms기다림칼을 쥐고 치댐기다림기다림
10~15ms기다림기다림칼을 쥐고 치댐기다림
15~20ms기다림기다림기다림칼을 쥐고 치댐

네 명이 번갈아 치대니 반죽 네 덩이가 모두 조금씩 진행됩니다. 동시성은 있지만 병렬성은 없습니다(1교시). 총 작업량이 그대로이니 시간도 그대로입니다.

왜 이런 규칙이 있을까요? 파이썬은 모든 객체가 "몇 군데에서 나를 쓰고 있나"(참조 횟수)를 셉니다. 여러 스레드가 이 숫자를 동시에 고치면 3교시의 칠판처럼 틀어집니다. 오래전 CPython은 잠금 하나로 인터프리터 전체를 지키는 쉬운 길을 택했고, 그게 GIL입니다.

기다릴 때는 칼을 내려놓는다

GIL에는 중요한 예외가 있습니다. 스레드가 기다리는 동안(time.sleep, 파일 읽기, 네트워크 응답)에는 GIL을 놓습니다. 국이 끓기를 기다리는 사람은 칼이 필요 없으니까요.

def simmer():
    time.sleep(0.3)   # 국이 끓기를 기다린다: 이 동안 GIL을 놓는다

start = time.perf_counter()
cooks = [threading.Thread(target=simmer) for _ in range(4)]
for th in cooks:
    th.start()
for th in cooks:
    th.join()
print(f"4 threads {time.perf_counter() - start:.2f}s")
# 4 threads 0.30s   (순서대로였다면 1.2초)

C로 짠 함수도 무거운 계산을 하는 동안 GIL을 놓을 수 있습니다. hashlib이 큰 데이터를 해시할 때가 그렇습니다. 사람 손이 아니라 반죽기(C 코드)가 돌리는 일이라, 칼 규칙 밖에 있는 셈입니다.

import hashlib

dough = b"x" * 50_000_000   # 50MB

def press():
    hashlib.sha256(dough).hexdigest()   # C 코드가 계산하는 동안 GIL을 놓는다

# 위와 같은 방식으로 재면 (이 PC에서 대략)
# sequential 0.11s
# 4 threads  0.04s
일의 종류GIL이 있는 스레드로이유
기다리는 일(sleep, 파일, 네트워크)빨라진다기다리는 동안 GIL을 놓는다
C 확장의 무거운 계산(hashlib, zlib 등)대개 빨라진다C 코드가 GIL을 놓고 계산한다
순수 파이썬 계산(반복문, 문자열 조립)빨라지지 않는다바이트코드는 한 번에 한 스레드

그리고 GIL은 여러분의 코드를 지켜 주지 않습니다. GIL은 인터프리터 내부를 지킬 뿐, count += 1 같은 여러 단계의 일은 중간에 끊깁니다. 3교시의 칠판이 GIL이 있는데도 틀린 이유입니다.

아래 실습대는 배포 서버에서 진짜 스레드가 time.sleep을 하는 주방입니다. 서버는 GIL이 있는 파이썬인데도, 기다리는 일이라 막대가 겹칩니다.

주방 타임라인

주문

6

요리사

3

요리

기다림

취소

실패

기다리는 일 4개, 각 0.3초. GIL이 있는 서버에서도 요리사 여럿이면 막대가 겹쳐 약 0.3초에 끝납니다. 혼자 순서대로와 비교해 보세요.

셰프의 한마디

스레드를 늘렸는데 안 빨라지면, 먼저 그 일이 국인지 반죽인지 물어봐. 국이면 스레드로 충분해. 반죽이면 사람을 늘릴 게 아니라 주방을 늘려야 해.

8-3. 분점 주방: multiprocessing (7분)

칼이 한 자루뿐이라면, 칼이 따로 있는 주방을 하나 더 차리면 됩니다. 이것이 프로세스입니다. 프로세스마다 파이썬 인터프리터가 따로 돌고, GIL도 따로 있습니다.

import multiprocessing as mp
import os

def branch(name):
    print(name, "pid", os.getpid(), "parent", os.getppid())

if __name__ == "__main__":
    print("main pid", os.getpid())
    p = mp.Process(target=branch, args=("branch-1",))
    p.start()
    p.join()
    print("exitcode", p.exitcode)
# 예:
# main pid 39516
# branch-1 pid 40268 parent 39516
# exitcode 0

Process의 사용법은 threading.Thread와 같습니다(target, args, start, join). 다른 점은 os.getpid(), 곧 프로세스 번호입니다. 분점은 본점과 다른 번호를 가진 별개의 프로그램입니다.

냉장고까지 따로 쓴다

분점은 메모리를 나누지 않습니다. 분점에서 리스트에 무엇을 넣어도 본점의 리스트는 그대로입니다.

orders = []

def add(n):
    orders.append(n)
    print("in branch:", orders)

if __name__ == "__main__":
    p = mp.Process(target=add, args=("#1",))
    p.start()
    p.join()
    print("in main:", orders)
# in branch: ['#1']
# in main: []

3교시의 칠판 문제가 애초에 생기지 않는 대신, 결과를 받으려면 따로 돌려받아야 합니다. 여러 작업을 나눠 맡길 때는 Pool이 편합니다.

def knead(n):
    s = 0
    for i in range(n):
        s += i
    return s

if __name__ == "__main__":
    with mp.Pool(processes=4) as pool:
        print(pool.map(knead, [10, 100, 1000, 10_000]))
# [45, 4950, 499500, 49995000]

if __name__ == "__main__":이 꼭 필요한 이유

윈도우와 macOS는 분점을 spawn 방식으로 엽니다. 새 파이썬을 띄우고, 그 파이썬이 여러분의 파일을 처음부터 다시 import 합니다. 함수 정의를 다시 읽어야 knead가 무엇인지 알 수 있으니까요.

이 검사가 없으면 분점이 파일을 읽다가 mp.Process(...).start() 줄을 또 만나, 분점이 분점을 열려고 합니다. 파이썬은 이를 막고 오류를 냅니다.

# 검사 없이 맨 바깥에서 바로 시작하면 (윈도우)
p = mp.Process(target=knead, args=(1,))
p.start()
# 분점에서:
# RuntimeError:
#         An attempt has been made to start a new process before the
#         current process has finished its bootstrapping phase.
#         ...

그래서 multiprocessing 예제는 파일로 저장해 python 파일.py로 실행합니다. 대화형 셸이나 노트북에서는 분점이 함수를 찾지 못해 실패하기 쉽습니다.

8-4. 출장 요리사를 분점으로: ProcessPoolExecutor (6분)

7교시의 ThreadPoolExecutor를 기억하나요? ProcessPoolExecutor는 같은 사용법으로 작업을 프로세스에 맡깁니다. submit, Future, result(), map, as_completed가 모두 그대로입니다.

import os
from concurrent.futures import ProcessPoolExecutor

def knead(n):
    s = 0
    for i in range(n):
        s += i
    return os.getpid(), s

if __name__ == "__main__":
    print("main", os.getpid())
    with ProcessPoolExecutor(max_workers=2) as ex:
        future = ex.submit(knead, 1000)
        print(future.result())
        for pid, s in ex.map(knead, [10, 100, 1000]):
            print(pid, s)
# 예:
# main 43452
# (42784, 499500)
# 42784 45
# 42784 4950
# 42784 499500

프로세스 번호가 main과 다릅니다. 어느 분점이 어떤 반죽을 맡을지는 실행마다 다릅니다.

주고받는 값은 pickle 되어야 한다

분점과 본점은 메모리를 나누지 않으니, 함수와 인자와 결과를 바이트로 포장해(pickle) 보내고 받습니다. 포장할 수 없는 것은 보낼 수 없습니다. 대표적인 예가 람다입니다.

def double(x):
    return x * 2

if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=2) as ex:
        print(ex.submit(double, 21).result())
        try:
            ex.submit(lambda x: x * 2, 21).result()
        except Exception as e:
            print(type(e).__name__, e)
# 42
# PicklingError Can't pickle <function <lambda> at 0x...>: it's not found as __main__.<lambda>
  • 함수는 이름으로 포장됩니다. 분점은 "파일의 double"을 찾아 씁니다. 이름이 없는 람다는 분점이 찾을 수 없습니다.
  • 그래서 분점에 맡길 함수는 파일 맨 바깥에 def로 정의합니다. 함수 안의 함수, 람다는 안 됩니다.
  • 열린 파일, 잠금, 소켓 같은 것도 인자로 보낼 수 없습니다. 숫자, 문자열, 리스트, dict처럼 단순한 값을 주고받으세요.

8-5. 분점을 여는 비용 (4분)

분점은 공짜가 아닙니다. 파이썬을 새로 띄우고, 파일을 다시 import 하고, 값을 포장해 오가야 합니다. 반죽이 작으면 이 준비 시간이 일보다 큽니다.

# 8-1의 작은 반죽(2백만 번) 4개를 프로세스 4개로
with ProcessPoolExecutor(max_workers=4) as ex:
    results = list(ex.map(knead, [2_000_000] * 4))
# 4 processes 0.31s   (12코어 PC 실측, 순서대로는 0.24초)
방법 (작은 반죽 4개)시간 (12코어 PC 실측)
순서대로0.24초
스레드 4개0.23초
프로세스 4개0.31초

오히려 느려졌습니다. 반죽을 열 배로 키우면(2천만 번) 이야기가 달라집니다. 이 PC에서 대략 순서대로 2.42.7초, 프로세스 4개 0.91.1초였습니다. 준비 비용은 거의 그대로인데 일이 커졌기 때문입니다.

  • 프로세스는 한 덩이가 충분히 큰 계산에 씁니다. 작은 일을 수천 번 보낼 때는 여러 개를 묶어 한 번에 보내세요(map의 chunksize=).
  • 풀은 한 번 만들어 여러 번 쓰세요. 작업마다 with ProcessPoolExecutor()를 새로 열면 매번 분점을 새로 차리는 셈입니다.

8-6. 분점을 여는 세 가지 방법: spawn, fork, forkserver (3분)

분점을 어떻게 차리느냐를 시작 방식(start method)이라고 합니다.

방식하는 일주방으로 치면
spawn새 파이썬을 띄우고 파일을 다시 import빈 건물에 주방을 처음부터 차린다
fork지금 프로세스를 통째로 복제본점을 그대로 복사한다(일하던 요리사의 잠금까지)
forkserver깨끗한 서버 프로세스를 하나 두고, 거기서 복제깨끗한 견본 주방을 두고 그걸 복사한다
  • 3.14부터 리눅스의 기본값은 forkserver입니다. 윈도우와 macOS는 spawn입니다. 이전 리눅스 기본값이던 fork는 스레드가 도는 프로그램을 복제할 때 잠금이 꼬일 수 있어 바뀌었습니다.
  • 윈도우에는 spawn 하나뿐입니다.
import multiprocessing as mp

if __name__ == "__main__":
    print(mp.get_start_method())        # spawn   (윈도우)
    print(mp.get_all_start_methods())   # ['spawn']

    ctx = mp.get_context("spawn")       # 이 풀만 spawn으로
    with ctx.Pool(processes=2) as pool:
        print(pool.map(knead, [10, 100]))   # [45, 4950]
  • mp.get_context("spawn")은 그 컨텍스트로 만든 Pool, Process에만 적용됩니다. 라이브러리 안에서는 이쪽이 안전합니다.
  • 프로그램 전체를 바꾸려면 if __name__ == "__main__": 맨 앞에서 mp.set_start_method("spawn")을 한 번만 부릅니다. 두 번 부르면 RuntimeError: context has already been set이 납니다.
  • 운영체제마다 기본값이 다르니, 모든 곳에서 똑같이 돌아야 한다면 spawn을 명시하고 테스트하세요.

8-7. 리모델링: GIL 없는 파이썬 3.14t (5분)

3.14부터 프리스레딩(free-threaded) 빌드가 공식 지원됩니다. GIL을 빼고 만든 파이썬으로, 실행 파일 이름이 python3.14t입니다(끝의 t). 주방을 리모델링해 칼이 요리사 수만큼 생긴 셈입니다.

  • python.org 설치 프로그램에서 free-threaded 바이너리를 함께 설치하거나, uv라면 uv run --python 3.14t 파일.py로 실행합니다.
  • 지금 GIL이 켜져 있는지는 sys._is_gil_enabled()로 확인합니다.
# knead_threads.py: 8-1의 코드 맨 위에 두 줄만 더한다
import sys
print(sys._is_gil_enabled())
...

# python knead_threads.py
# True
# sequential 0.24s
# 4 threads  0.23s

# python3.14t knead_threads.py
# False
# ...
# 4 threads  0.09s   (12코어 PC 실측, 약 2.7배)

코드는 한 글자도 바꾸지 않았습니다. 같은 스레드 코드가 인터프리터만 바꿔 약 2.7배 빨라졌습니다. 네 배가 아닌 이유는 스레드를 띄우고 관리하는 비용, 그리고 GIL 없이도 안전하게 돌기 위한 내부 장치의 비용 때문입니다. 대신 스레드 하나만 쓰는 코드는 보통 빌드보다 조금 느릴 수 있습니다.

알아 둘 점이 두 가지 있습니다.

  • C 확장이 GIL을 다시 켤 수 있습니다. 프리스레딩을 지원한다고 표시하지 않은 확장 모듈을 import 하면, 파이썬이 경고를 내고 GIL을 다시 켭니다. 환경 변수 PYTHON_GIL=0(또는 -X gil=0)으로 강제로 끌 수 있고, PYTHON_GIL=1이면 3.14t에서도 GIL이 켜집니다.
  • 경쟁 상태는 더 잘 생깁니다. 3교시의 칠판 실험은 3.14t에서 4000 중 2410(실측)이었습니다. 진짜로 동시에 도니 잠금이 더 중요해집니다.

아래 실습대의 위쪽 막대 네 개는 교안을 준비하며 12코어 PC에서 잰 값입니다. 아래의 "서버에서 직접 반죽하기"는 이 사이트의 배포 서버에서 반죽(60만 번 × 4)을 순서대로, 스레드 4개로 직접 돌립니다. 배포 서버는 GIL이 있는 빌드이고 코어 수를 미리 알 수 없어서, 숫자는 실측 막대와 다를 수 있습니다. 두 막대의 길이가 비슷한지를 보세요.

반죽 비교

로컬 실측 (12코어 PC, Python 3.14)

순서대로

0.24s

스레드 4개

0.23s

프로세스 4개

0.31s

스레드 4개 (3.14t, GIL 없음)

0.09s

CPU 작업 4개를 순서대로, 스레드로, 프로세스로, GIL 없는 3.14t의 스레드로 돌린 실측 막대입니다. 서버에서 직접 돌려 서버의 GIL 상태와 코어 수도 확인해 보세요.

셰프의 한마디

GIL이 없어지면 칼 규칙이 사라진 거지, 칠판 규칙이 사라진 게 아니야. 같은 칠판에 여럿이 쓰면 이제 진짜로 동시에 쓰게 돼. 3.14t로 옮기기 전에 잠금부터 다시 확인해.

8-8. 칸막이 주방: 서브인터프리터 (2분)

분점은 비싸고, 리모델링은 아직 모든 라이브러리가 따라오지 못했습니다. 그 사이에 하나가 더 있습니다. 한 건물 안에 칸막이를 친 주방, 곧 서브인터프리터입니다. 한 프로세스 안에 인터프리터를 여러 개 두고, 칸마다 GIL(칼)이 따로 있습니다. 3.14에서 concurrent.interpreters 모듈로 표준 라이브러리에 들어왔습니다.

from concurrent import interpreters

booth = interpreters.create()
booth.exec("menu = ['#1', '#2']; print('booth menu:', menu)")
booth.exec("menu.append('#3'); print('booth menu:', menu)")
print("main has menu?", "menu" in globals())

def knead(n):
    s = 0
    for i in range(n):
        s += i
    return s

print(booth.call(knead, 1000))
booth.close()
# booth menu: ['#1', '#2']
# booth menu: ['#1', '#2', '#3']
# main has menu? False
# 499500
  • create()로 칸을 만들고, exec(코드 문자열)로 그 칸에서 코드를 돌리고, call(함수, 인자)로 함수를 불러 결과를 받습니다.
  • 칸 안의 변수(menu)는 칸 안에만 있습니다. 분점처럼 메모리를 나누지 않습니다.

실무에서는 7교시와 같은 모양의 InterpreterPoolExecutor(3.14)가 편합니다.

from concurrent.futures import InterpreterPoolExecutor

if __name__ == "__main__":
    with InterpreterPoolExecutor(max_workers=4) as ex:
        results = list(ex.map(knead, [2_000_000] * 4))
# 이 PC에서 대략 0.18초 (GIL 있는 3.14, 순서대로는 0.24초)

칸마다 칼이 따로 있으니 보통 파이썬에서도 반죽이 나란히 진행됩니다. 아직 많은 C 확장이 서브인터프리터를 지원하지 않는다는 점은 기억해 두세요.

영업 마감 (10분)

오늘 밤의 목표는 반죽 네 덩이를 혼자 할 때보다 두 배 이상 빠르게 끝내는 것이었습니다. 순수 파이썬 계산이니 스레드로는 안 되고, 한 덩이가 크니 분점을 여는 비용을 감당할 만합니다.

import time
from concurrent.futures import ProcessPoolExecutor

BIG = 20_000_000   # 큰 반죽 한 덩이

def knead(n):
    s = 0
    for i in range(n):
        s += i
    return s

if __name__ == "__main__":
    start = time.perf_counter()
    alone = [knead(BIG) for _ in range(4)]
    print(f"sequential {time.perf_counter() - start:.2f}s")

    start = time.perf_counter()
    with ProcessPoolExecutor(max_workers=4) as ex:
        branches = list(ex.map(knead, [BIG] * 4))
    print(f"4 processes {time.perf_counter() - start:.2f}s", branches == alone)
# 예 (이 PC에서):
# sequential 2.43s
# 4 processes 0.93s True

대략 2.5배. 결과도 혼자 한 것과 똑같습니다. 3.14t가 있다면 knead를 스레드 네 개로 돌려도 비슷한 효과를 볼 수 있습니다.

도구칼(GIL)메모리띄우는 비용어울리는 일
스레드 (3.14)프로세스에 하나함께 쓴다작다기다리는 일
스레드 (3.14t)없다함께 쓴다작다기다리는 일 + 계산
프로세스분점마다 하나따로 쓴다(pickle)크다큰 계산
서브인터프리터칸마다 하나따로 쓴다중간계산, 순수 파이썬 코드

셰프가 앞치마를 풀며 말합니다. "반죽은 주방을 늘려서 해결했어. 그런데 홀은? 테이블 열 개에 직원을 열 명 둘 거야, 아니면 한 명이 열 테이블을 돌게 할 거야?"

영업 일지 — 여덟째 밤. 반죽은 스레드로 빨라지지 않았다. 칼이 한 자루뿐이라서. 분점 네 곳에 나눠 맡겨 2.5배 빠르게 끝냄. 내일은 홀을 개편한다.

실습 과제

  1. 8-1의 knead를 time.sleep(0.06)으로 바꾸고 순서대로와 스레드 4개를 다시 재 보세요. 왜 이번에는 스레드가 빨라지는지 GIL로 설명해 보세요.
  2. 8-5의 반죽 크기를 20만, 2백만, 2천만 번으로 바꿔 가며 순서대로와 ProcessPoolExecutor의 시간을 표로 적어 보세요. 여러분의 PC에서 프로세스가 이기기 시작하는 크기는 어디인가요?
  3. ProcessPoolExecutor에 함수 안에서 정의한 함수(중첩 함수)를 submit 해 보고, 어떤 오류가 나는지 확인한 뒤 고쳐 보세요.

셰프의 레시피 카드

  • GIL: 파이썬 바이트코드는 한 번에 한 스레드만. 기다릴 때(I/O)와 일부 C 코드는 GIL을 놓는다.

  • 계산(CPU) 작업은 스레드로 빨라지지 않는다 → 프로세스, 3.14t, 서브인터프리터.

  • ProcessPoolExecutor는 ThreadPoolExecutor와 같은 사용법. if __name__ == "__main__": 필수.

  • 분점과 주고받는 값은 pickle 된다: 람다·중첩 함수는 안 된다. 맨 바깥 def로.

  • 프로세스는 띄우는 비용이 크다: 큰 계산에만. 시작 방식은 윈도우·macOS spawn, 3.14 리눅스 forkserver.

  • sys._is_gil_enabled(): 3.14t(python3.14t)에서 False. 같은 스레드 코드가 그대로 병렬로.

  • concurrent.interpreters: create() → exec() / call(), InterpreterPoolExecutor(3.14).

← 이전

7교시 · 출장 요리사 파견: concurrent.futures

다음 →

9교시 · 혼자 도는 홀 매니저: asyncio 첫걸음

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex