,

비 올 때 뛰면 덜 젖을까? 같은 100 m를 걷고 뛰는 빗물 모형

코드로 실험하는 일상 과학 · 시즌 1, 3편

같은 거리를 간다면, 수직으로 내리는 비에서는 빨리 이동할수록 몸에 부딪히는 빗물이 줄어든다. 하지만 비가 뒤에서 따라오면, 무조건 더 빠른 속도가 유리하지는 않다. 이번 계산에서는 뒷바람과 같은 속도로 움직일 때 가장 적게 맞는 경우도 나온다.

여기서 말하는 “덜 젖는다”는 옷에 남는 물의 양이 아니라 이동 중 모형 몸체에 부딪힌 빗물의 질량이다. 사람을 실제로 비에 세워 측정한 실험은 아니다. 단순한 모형을 파이썬으로 계산해, 질문의 조건을 바꾸면 답이 어떻게 달라지는지 살펴본다. 현실에서는 미끄럼이나 교통 위험을 감수하면서 달릴 이유가 없다.

코드 없이 읽으려면 아래 결과만 따라가면 된다. 직접 계산하려면 실행 방법과 전체 코드로 이동하자.

먼저, 같은 시간 말고 같은 거리를 비교한다

걷든 뛰든 목적지는 100 m 앞이다. 속도 1.5 m/s이면 약 66.7초, 3 m/s이면 약 33.3초, 5 m/s이면 20초가 걸린다. 같은 시간 동안 비교하면 빠른 쪽이 더 멀리 가므로, “같은 목적지까지 얼마나 맞는가”라는 질문과 달라진다.

사람 대신 폭 0.45 m, 앞뒤 두께 0.25 m, 높이 1.70 m인 직립 직육면체를 사용했다. 앞·뒷면 한 면의 넓이는 0.765 m², 옆면 한 면은 0.425 m², 윗면은 0.1125 m²다. 자세와 면적은 이동 중 변하지 않는다. 인체 치수를 측정한 값이 아니라 비교를 위한 설명용 설정이다.

수직 강우량은 시간당 10 mm, 빗방울의 아래쪽 속도는 5 m/s로 고정했다. 진행 방향을 +x로 두고 빗방울의 수평속도를 0, −3, +3 m/s로 바꿨다. 각각 무풍, 맞바람, 뒷바람 조건에 해당한다. 여기서는 빗방울의 수평속도가 바람과 같다고 가정한다. 모든 조건은 일정하고 공간적으로 균일하다.

수직비에서는 앞보다 위에서 차이가 난다

수직비에서 앞면은 앞으로 지나간 공간 속 빗물을 만난다. 같은 단면으로 같은 거리를 이동하면 그 공간의 부피는 같다. 빠르게 움직일수록 초당 많이 부딪히지만, 그만큼 이동 시간이 짧아진다. 이 모형에서 앞면에 부딪히는 총량은 세 속도 모두 42.5 g이다.

윗면은 다르다. 위에서 내리는 비를 맞는 시간이 줄어든다. 윗면의 양은 20.8 g, 10.4 g, 6.3 g으로 줄고, 앞면과 합친 총량은 각각 63.3 g, 52.9 g, 48.8 g이다. “달리면 앞에서 비를 더 맞는다”는 초당 유입량에 대한 설명과 “목적지까지 맞는 총량은 줄어든다”는 설명이 동시에 성립할 수 있다.

수직비에서 같은 100 m를 이동할 때 앞면은 42.5 g으로 일정하고 윗면의 빗물은 속도가 커질수록 줄어든다.

그림 1. 수직비, 같은 100 m 이동의 계산 결과. 청록색 아래 막대는 앞면의 42.5 g으로 일정하고, 주황색 윗부분은 속도가 커질수록 줄어든다. 가로축은 이동 속도, 세로축은 부딪힌 물의 질량 g이다.

뒤에서 오는 비에는 다른 답이 나온다

맞바람 −3 m/s에서는 앞에서 오는 비와 사람이 서로 가까워진다. 이동 속도 1.5, 3, 5 m/s에서 총량은 148.3, 95.4, 74.3 g이다. 이 직립 직육면체 조건에서는 빨리 이동할수록 줄어든다.

반대로 빗방울이 진행 방향으로 3 m/s씩 이동한다면, 사람도 3 m/s로 움직일 때 앞뒤 상대속도가 0이 된다. 옆바람도 없으므로 앞·뒷면과 옆면의 유입이 사라지고 윗면의 10.4 g만 남는다. 사람이 5 m/s로 더 빠르게 움직이면 앞쪽 빗물을 따라잡으며, 총량은 23.3 g으로 다시 늘어난다. 비를 전혀 맞지 않는다는 뜻은 아니다.

무풍·맞바람 곡선은 내려가며 뒷바람 3 m/s 곡선은 이동 속도 3 m/s에서 최소가 된다.

그림 2. 비의 수평 방향을 바꾼 계산 결과. 청록색 무풍과 주황색 맞바람 곡선은 내려간다. 보라색 뒷바람 곡선은 3 m/s에서 최소가 된 뒤 올라간다. 이동 속도 범위는 0.5–6 m/s다.

아래 숫자는 모두 이 글의 코드가 계산한 값이며, 사람이 실제로 입고 있던 옷의 무게 변화가 아니다.

속도무풍맞바람 −3뒷바람 +3
1.5 m/s63.3 g148.3 g63.3 g
3 m/s52.9 g95.4 g10.4 g
5 m/s48.8 g74.3 g23.3 g
풍속 성분 단위는 m/s. 모든 조건에서 이동거리·몸체·수직 강우량·낙하속도는 같다.

옆에서 들어오는 비까지 더하면

뒷바람 +3 m/s를 유지하고 옆방향 빗방울 속도를 5 m/s로 더하면, 옆면을 맞는 양이 생긴다. 이 예시에서는 3 m/s를 넘어서도 총량이 감소한다. 앞면에서 늘어나는 양보다 짧아진 시간 덕분에 위·옆에서 줄어드는 양이 더 크기 때문이다. 바람을 “앞이냐 뒤냐” 한 항목으로만 표시하면 이 차이를 놓친다.

뒷바람에 옆방향 성분 5 m/s를 더하면 이동 속도 3 m/s 이후에도 빗물 충돌량이 감소한다.

그림 3. 옆바람은 뒷바람에서의 최적 속도 조건을 바꿀 수 있다. 뒷바람 +3 m/s만 있는 청록색은 3 m/s에서 최소다. 옆방향 성분 +5 m/s를 더한 주황색은 그 이후에도 감소한다.

짧은 영상: 먼저 도착하면 누적도 멈춘다

세 속도를 같은 시각에 출발시킨 수직비 계산이다. 왼쪽은 이동거리, 오른쪽은 누적 충돌 수량이다. 100 m에 도착하면 비를 피할 곳에 들어간 것으로 두어 누적을 멈춘다. 66.7초의 모형 시간을 약 12초로 압축했으며, 실제 사람이나 빗방울을 촬영한 영상은 아니다.

같은 100 m를 이동하는 세 속도의 모형 비교. 실제 촬영이 아니다.

영상이 표시되지 않으면 YouTube에서 직접 보기.

계산식은 상대속도 × 면적 × 시간이다

v는 사람의 속도, uₓ·uᵧ는 빗방울의 진행·옆방향 속도, w는 아래쪽 속도의 크기다. A앞·A옆·A위는 각 방향에 수직인 한 면의 면적이고, D는 이동거리다. ρ는 공기 1 m³ 안에 들어 있는 빗물 질량이다. 물 자체의 밀도 1,000 kg/m³와 구별해야 한다.

M = ρ × (D / v) × [A앞 × |v − uₓ| + A옆 × |uᵧ| + A위 × w]
ρ = 1,000 × (R / 1,000 / 3,600) / w

R을 mm/h로 넣으면 ρ의 단위는 kg/m³, 대괄호의 단위는 m³/s, D/v는 초라서 최종 질량 M은 kg가 된다. 이 설정의 ρ는 약 0.0005556 kg/m³다. 앞·뒷면을 둘 다 더하지 않는다. 상대속도 방향에서 비가 들어오는 면 하나만 세므로 절댓값을 사용한다.

뒷바람 uₓ>0일 때 A앞×uₓ가 A옆×|uᵧ|+A위×w보다 크면, 이 모형의 최소값은 v=uₓ에서 나온다. 작으면 더 빠를수록 줄고, 같으면 v≥uₓ 구간에서 같은 최소값이 이어진다. 이 조건은 직립 직육면체에 대한 것이며, 몸을 숙이거나 곡면으로 바꾼 물체에 그대로 적용할 수 없다.

어디까지 검증했고, 어디부터 모르는가

직접 실행한 검산에서는 여섯 면의 법선벡터로 유입 플럭스를 따로 합산한 뒤 시간에 걸쳐 누적한 값과 위 식을 대조했다. 속도 5개, 바람 조건 4개, 시간 분할 10·100·1,000개, 총 60개 조합에서 최대 절대 차이는 약 2.22×10⁻¹⁶ kg였다. 유입량이 시간에 따라 일정한 모형이므로 시간 분할을 늘려도 적분 오차가 본질적으로 생기지 않으며, 남는 차이는 부동소수점 반올림 수준이다.

무풍·맞바람의 감소, 뒷바람의 최소점 3 m/s, 옆바람 5 m/s를 더했을 때의 감소, 수직비 앞면 총량의 일정함도 코드의 검사문으로 확인했다. 이동 속도 0은 이동시간을 정의할 수 없어 오류로 막았다. 임의의 초기값을 조금씩 갱신하는 미분방정식 풀이가 아니라 일정한 플럭스의 계산이므로, 이 검산을 현실의 비나 인체 모델이 정확하다는 검증으로 읽으면 안 된다.

빗방울 크기 분포, 가속·감속, 달릴 때의 자세 변화, 몸 주변의 공기 흐름, 우산·처마의 가림, 바닥에서 튄 물, 옷의 흡수와 흘러내림은 계산하지 않았다. 낙하속도를 바꾸면서 수직 강우량을 고정한다면 ρ도 함께 바뀌어야 한다. R만 커지면 이 모형의 물의 양은 비례해서 커지지만, 최적 속도 조건 자체는 바뀌지 않는다.

생활 질문의 답은 조건부다. 같은 목적지와 같은 직립 형상이라는 이 비교에서는 수직비나 맞바람일 때 노출시간을 줄이는 편이 유리하다. 뒷바람에서는 상대속도와 면적에 따라 답이 달라진다. 실제 이동에서는 수치상 최소량보다 안전하게 비를 피하는 경로가 우선이다.

직접 실행하는 방법

아래 전체 코드를 rain_experiment.py라는 이름의 UTF-8 파일로 저장하고, 해당 파일이 있는 폴더에서 python rain_experiment.py를 실행한다. 별도 데이터나 보조 코드 파일은 필요 없다. NumPy·Matplotlib·Pillow가 필요하며, MP4 생성에는 실행 경로에서 찾을 수 있는 FFmpeg가 추가로 필요하다. FFmpeg가 없으면 그림과 수치 검산만 만들고 영상은 건너뛴다. 그래프의 영문 표기는 운영체제별 한글 폰트 의존을 피하기 위한 것이다.

실행 확인: 2026년 10월 16일(KST), Python 3.12.14, NumPy 2.3.5, Matplotlib 3.10.8. 실행 결과는 코드 파일 옆에 PNG 3개, results.json, MP4와 영상 마지막 프레임으로 생성된다. MP4가 없는 환경에서도 숫자와 그림으로 같은 비교를 할 수 있다. 다른 버전의 모든 조합을 시험한 것은 아니다.

먼저 results.json의 수치를 확인한 뒤 D만 100에서 200으로 바꾸어, 같은 파일에 기록되는 총량이 두 배가 되는지 비교하자. 그림·영상의 축 범위는 기본 100 m 실험에 맞춰져 있으므로 거리를 늘릴 때는 ylim 값도 함께 늘려야 잘리지 않는다. 옆바람의 영향은 이미 그림 3에서 0과 5 m/s를 함께 계산한다. 두 곡선을 위 조건식과 대조해 보자. 동시에 여러 값을 바꾸기보다 한 조건씩 바꾸면 결론이 달라진 원인을 찾기 쉽다.

전체 실행 코드 펼치기
"""Fixed-distance rain interception, not clothing absorption or human measurements.
Python 3.12; numpy, matplotlib, Pillow. MP4 additionally needs ffmpeg on PATH.
Run: python rain_experiment.py. Outputs are written beside this script.
"""
from pathlib import Path
import json, sys, shutil
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, FFMpegWriter
from matplotlib import font_manager

OUT = Path(__file__).resolve().parent
# English labels keep the script portable without requiring a Korean font.
D = 100.0  # m, same trip for every speed
WIDTH, DEPTH, HEIGHT = 0.45, 0.25, 1.70  # m, illustrative cuboid
AF, AS, AT = WIDTH*HEIGHT, DEPTH*HEIGHT, WIDTH*DEPTH  # m^2
R = 10.0  # mm/hour of vertical rainfall, illustrative, not measured
W = 5.0  # m/s downward rain speed, fixed for every scenario
RHO = 1000.0*(R/1000.0/3600.0)/W  # kg of liquid water per m^3 of air

def parts(v, ux=0.0, uy=0.0):
    v = np.asarray(v, dtype=float)
    if np.any(v <= 0):
        raise ValueError('Travel speed must be positive.')
    top = RHO*AT*W*D/v
    front_back = RHO*AF*np.abs(v-ux)*D/v
    side = RHO*AS*abs(uy)*D/v
    return top, front_back, side

def mass(v, ux=0.0, uy=0.0):
    return sum(parts(v, ux, uy))

def face_integral(v, ux, uy, n):
    # Independently sum inward flux on all six faces, then midpoint time steps.
    normals = np.array([[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]])
    areas = np.array([AF,AF,AS,AS,AT,AT])
    relative = np.array([ux-v,uy,-W])
    rate = RHO*np.sum(areas*np.maximum(0.0,-normals@relative))
    return np.sum(np.full(n,rate*D/v/n))

plt.rcParams.update({'font.size':12,'axes.spines.top':False,'axes.spines.right':False,
                     'figure.facecolor':'#faf8f3','axes.facecolor':'#faf8f3','savefig.facecolor':'#faf8f3'})
COLORS=['#147d92','#d26643','#7161a8','#498663']
v = np.linspace(0.5,6,551)
fig, ax = plt.subplots(figsize=(9,5.5),layout='constrained')
for (label,ux,uy),color in zip([('No wind',0,0),('Headwind: -3 m/s',-3,0),('Tailwind: +3 m/s',3,0)],COLORS):
    ax.plot(v,mass(v,ux,uy)*1000,label=label,color=color,lw=3)
ax.set(xlabel='Travel speed (m/s)',ylabel='Intercepted water (g)',title='Same 100 m trip, different rain directions',ylim=(0,400))
ax.legend(frameon=False);ax.grid(alpha=.18)
fig.savefig(OUT/'rain-speed-curves.png',dpi=170);plt.close(fig)
fig,ax=plt.subplots(figsize=(9,5.5),layout='constrained')
speeds=np.array([1.5,3.0,5.0]);top,front,side=parts(speeds)
ax.bar(['1.5 m/s','3 m/s','5 m/s'],front*1000,label='Front: same swept volume',color=COLORS[0])
ax.bar(['1.5 m/s','3 m/s','5 m/s'],top*1000,bottom=front*1000,label='Top: less time in rain',color=COLORS[1])
for i,m in enumerate(mass(speeds)*1000):ax.text(i,m+2,f'{m:.1f} g',ha='center')
ax.set(ylabel='Intercepted water (g)',title='Vertical rain: faster reduces the top contribution',ylim=(0,80));ax.legend(frameon=False);ax.grid(axis='y',alpha=.18)
fig.savefig(OUT/'rain-top-front.png',dpi=170);plt.close(fig)
fig,ax=plt.subplots(figsize=(9,5.5),layout='constrained')
for uy,color in zip([0,5],COLORS):ax.plot(v,mass(v,3,uy)*1000,lw=3,label=f'Tailwind +3 m/s; crosswind {uy} m/s',color=color)
ax.axvline(3,color='#555555',ls=':',label='Speed matches tailwind')
ax.set(xlabel='Travel speed (m/s)',ylabel='Intercepted water (g)',title='A crosswind can change the best speed',ylim=(0,550));ax.legend(frameon=False);ax.grid(alpha=.18)
fig.savefig(OUT/'rain-crosswind.png',dpi=170);plt.close(fig)

errors=[]
for speed in [0.5,1.5,3,5,6]:
    for ux,uy in [(0,0),(-3,0),(3,0),(3,5)]:
        for n in [10,100,1000]:errors.append(abs(face_integral(speed,ux,uy,n)-float(mass(speed,ux,uy))))
assert max(errors)<1e-12
assert np.all(np.diff(mass(v,0,0))<0)
assert np.all(np.diff(mass(v,-3,0))<0)
assert abs(v[np.argmin(mass(v,3,0))]-3)<1e-10
assert np.all(np.diff(mass(v,3,5))<0)
assert np.allclose(parts(speeds)[1],RHO*D*AF)
try: mass(0)
except ValueError: pass
else: raise AssertionError('Zero speed was not rejected')
summary={'python':sys.version.split()[0],'numpy':np.__version__,'matplotlib':matplotlib.__version__,
         'distance_m':D,'rain_mm_hr':R,'rain_down_m_s':W,'water_in_air_kg_m3':RHO,
         'areas_m2':{'front':AF,'side':AS,'top':AT},'max_abs_error_kg':max(errors),
         'cases':[{'speed_m_s':float(s),'time_s':D/s,'no_wind_g':float(mass(s)*1000),
                   'headwind_g':float(mass(s,-3)*1000),'tailwind_g':float(mass(s,3)*1000)} for s in speeds]}
(OUT/'results.json').write_text(json.dumps(summary,indent=2),encoding='utf-8')
print(json.dumps(summary,indent=2))
# Animation is a time-compressed accumulation chart, not a measured rain movie.
if shutil.which('ffmpeg'):
    fig,axes=plt.subplots(1,2,figsize=(10,4.5),layout='constrained')
    tmax=D/1.5; ts=np.linspace(0,tmax,121)
    lines=[]
    for ax,title,ylab,limit in zip(axes,['Distance travelled','Water intercepted, vertical rain'],['Distance (m)','Water (g)'],[105,75]):
        ax.set(xlim=(0,tmax),ylim=(0,limit),xlabel='Elapsed time (s)',ylabel=ylab,title=title);ax.grid(alpha=.2)
        for s,c in zip(speeds,COLORS):lines.append(ax.plot([],[],color=c,lw=3,label=f'{s:g} m/s')[0])
        ax.legend(frameon=False,loc='upper left')
    def update(k):
        t=ts[:k+1]
        for j,s in enumerate(speeds):
            active=np.minimum(t,D/s)
            lines[j].set_data(t,s*active)
            lines[3+j].set_data(t,float(mass(s))*1000*active/(D/s))
        return lines
    ani=FuncAnimation(fig,update,frames=len(ts),interval=100,blit=False)
    ani.save(OUT/'rain-fixed-distance.mp4',writer=FFMpegWriter(fps=10,codec='libx264',extra_args=['-pix_fmt','yuv420p']))
    update(120);fig.savefig(OUT/'video-final-frame.png',dpi=130);plt.close(fig)
else:
    print('MP4 skipped: ffmpeg unavailable; figures and tests still produced.')

원전과 이어 읽기

모형의 출발점은 Franco Bocci, “Whether or not to run in the rain”, European Journal of Physics 33 (2012), 1321–1332다. 이 글은 그 논문의 §5.1 직립 직육면체 식 (10)–(16)을 적용해, 위에서 명시한 설명용 조건을 직접 계산했다. 논문의 일반 형상 논의는 이 단순모형의 결론을 모든 몸체에 일반화하지 말아야 하는 이유를 보여준다. 논문 DOI · 대학이 제공하는 논문 원문. 원문 확인: 2026년 10월 16일(KST).

이전 편: 사고도 없는데 왜 막힐까? 유령정체를 만드는 원형도로 실험. 첫 편: 뜨거운 물이 더 빨리 얼까? 음펨바 효과와 파이썬 수치실험.


다른 글 보기 · 주제 탐색 · 작성자와 편집 기준 · 문의·정정 요청

RUDA DIRECTOR에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기