Skip to main content
Summary

시간이 들어오면 로봇이 움직이기 시작한다

지금까지 우리는 로봇의 위치와 방향을 주로 "공간" 안에서 바라보았다. 로봇이 어디에 있는지, 어느 방향을 보고 있는지, 카메라가 본 물체를 로봇 기준 좌표로 어떻게 바꾸는지 살펴보았다.

하지만 실제 로봇은 한 장의 그림처럼 멈춰 있지 않다. 바퀴가 굴러가고, 팔이 움직이고, 드론은 기울기를 바꾸며 날아간다. 로봇을 제대로 이해하려면 이제 질문을 하나 바꿔야 한다.

"로봇이 어디에 있는가?"에서 "로봇의 위치가 시간에 따라 어떻게 바뀌는가?"로 말이다.

이 질문이 바로 미분과 적분으로 들어가는 문이다. 겁먹을 필요는 없다. 이번 장에서는 복잡한 공식을 외우기보다, 로봇의 움직임을 시간별 숫자 기록으로 바라보는 감각을 만들 것이다. 시간표에 따라 로봇의 위치를 적고, 위치가 얼마나 바뀌었는지 보면 속도가 나온다. 속도가 얼마나 바뀌었는지 보면 가속도가 나온다.

움직임은 갑자기 어려워지는 것이 아니다. 단지 좌표에 시간이 붙었을 뿐이다.

위치, 속도, 가속도

위치는 특정 시각의 로봇 상태다

2D 평면에서 점 로봇의 위치는 보통 다음처럼 표현했다.

[ mathbf{p} = begin{bmatrix} x \ y end{bmatrix} ]

여기서 (x)와 (y)는 로봇이 월드 좌표계에서 어디에 있는지를 나타내는 숫자다. 그런데 로봇이 움직이면 (x)와 (y)는 고정된 숫자가 아니다. 시간이 흐를 때마다 값이 바뀐다.

그래서 위치를 다음처럼 생각할 수 있다.

[ mathbf{p}(t) = begin{bmatrix} x(t) \ y(t) end{bmatrix} ]

괄호 안의 (t)는 시간이다. (x(t))는 "시간 (t)일 때의 x 위치", (y(t))는 "시간 (t)일 때의 y 위치"라는 뜻이다. 즉 위치는 시간에 따라 달라지는 함수다.

예를 들어 로봇이 0초에 ((0, 0))에 있고, 1초에 ((1, 0)), 2초에 ((2, 0))에 있다면 이렇게 말할 수 있다.

시간 t위치 x위치 y
0초00
1초10
2초20

이 로봇은 x축 방향으로 움직이고 있다. 아직 속도라는 말을 꺼내지 않았지만, 표만 봐도 느낌이 온다. 1초마다 x 위치가 1씩 증가한다. 이 "시간당 위치 변화"가 바로 속도다.

로봇의 상태를 조금 더 현실적으로 표현하면 위치뿐 아니라 방향도 함께 저장한다.

[ text{pose}(t) = begin{bmatrix} x(t) \ y(t) \ theta(t) end{bmatrix} ]

여기서 (theta(t))는 시간 (t)에서 로봇이 바라보는 방향이다. 2부와 3부에서 계속 사용했던 ([x, y, theta])가 이제 시간에 따라 변하는 상태가 된 것이다.

속도는 위치가 변하는 빠르기다

로봇이 두 시각 사이에서 얼마나 움직였는지 보자. (t_1)에서 위치가 (mathbf{p}_1), (t_2)에서 위치가 (mathbf{p}_2)라면 위치 변화량은 다음과 같다.

[ Delta mathbf{p} = mathbf{p}_2 - mathbf{p}_1 ]

시간 변화량은 다음과 같다.

[ Delta t = t_2 - t_1 ]

속도는 위치 변화량을 시간 변화량으로 나눈 값이다.

[ mathbf{v} = frac{Delta mathbf{p}}{Delta t} ]

이 식은 어렵게 생겼지만, 사실 매우 일상적인 말이다. "1초 동안 2미터 갔다"면 속도는 초당 2미터다. "0.5초 동안 1미터 갔다"면 역시 초당 2미터다.

2D에서는 속도도 벡터다.

[ mathbf{v} = begin{bmatrix} v_x \ v_y end{bmatrix} ]

(v_x)는 x 방향 속도, (v_y)는 y 방향 속도다. 예를 들어 속도가 ([1, 0])이라면 로봇은 x축 양의 방향으로 움직인다. 속도가 ([0, 1])이라면 y축 양의 방향으로 움직인다. 속도가 ([1, 1])이라면 대각선 방향으로 움직인다.

속도 벡터의 크기는 실제 이동 속력이다.

[ text{speed} = |mathbf{v}| ]

여기서 "속도"와 "속력"을 구분하면 좋다. 속도는 방향까지 포함한 벡터이고, 속력은 그 벡터의 크기다. 자동차 계기판에 나오는 60km/h는 방향 정보가 없는 속력이다. 반면 로봇 제어에서는 "동쪽으로 초당 1m"처럼 방향까지 필요할 때가 많으므로 속도 벡터를 자주 사용한다.

아주 작은 시간 간격으로 움직임을 본다

실제 프로그램에서는 연속적인 시간을 완벽하게 다루기 어렵다. 컴퓨터는 보통 시간을 잘게 쪼개서 계산한다. 예를 들어 (dt = 0.1)초마다 로봇 상태를 업데이트한다고 생각해 보자.

0.0초, 0.1초, 0.2초, 0.3초처럼 시간을 나누고, 각 시각의 위치를 배열에 저장한다. 그러면 인접한 두 위치의 차이를 이용해 속도를 근사할 수 있다.

[ mathbf{v}(t) approx frac{mathbf{p}(t + dt) - mathbf{p}(t)}{dt} ]

여기서 (approx)는 "정확히 같다"가 아니라 "가깝게 계산한다"는 뜻이다. 이것을 근사라고 한다. 로봇 시뮬레이션에서는 이런 근사를 매우 자주 사용한다.

시간 간격 (dt)가 작을수록 움직임을 더 촘촘하게 관찰할 수 있다. 카메라 영상을 떠올리면 쉽다. 1초에 한 장만 찍으면 로봇이 순간이동하는 것처럼 보인다. 1초에 60장을 찍으면 움직임이 부드러워 보인다. 시뮬레이션도 마찬가지다.

Python으로 시간별 위치 데이터 만들기

먼저 1차원에서 시작하자. 로봇이 x축 위를 움직인다고 하자. 위치가 시간에 따라 다음처럼 변한다고 가정해 보겠다.

[ x(t) = 0.5t^2 ]

이 식은 시간이 지날수록 로봇이 점점 더 빠르게 움직인다는 뜻이다. 처음에는 천천히 출발하지만, 시간이 갈수록 위치가 더 크게 증가한다.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.1
t = np.arange(0, 5 + dt, dt)

x = 0.5 * t**2

plt.figure(figsize=(7, 4))
plt.plot(t, x)
plt.xlabel('time [s]')
plt.ylabel('position x [m]')
plt.title('Position over time')
plt.grid(True)
plt.show()

여기서 t는 시간 배열이고, x는 각 시간에 해당하는 위치 배열이다. np.arange(0, 5 + dt, dt)는 0초부터 5초까지 0.1초 간격으로 시간을 만든다.

이제 위치 배열에서 속도를 계산해 보자.

v = np.diff(x) / dt

t_v = t[:-1]

plt.figure(figsize=(7, 4))
plt.plot(t_v, v)
plt.xlabel('time [s]')
plt.ylabel('velocity [m/s]')
plt.title('Velocity estimated from position')
plt.grid(True)
plt.show()

np.diff(x)는 인접한 원소끼리 뺀 값을 만든다. 예를 들어 x[1] - x[0], x[2] - x[1], x[3] - x[2] 같은 값이 차례대로 나온다. 이것이 위치 변화량이다. 여기에 dt를 나누면 속도가 된다.

주의할 점이 하나 있다. 원래 위치 배열 x의 길이가 51이었다면, np.diff(x)의 길이는 50이 된다. 두 점 사이의 차이를 계산하기 때문이다. 그래서 속도를 그릴 때 시간 배열도 t[:-1]처럼 하나 줄여서 맞춘다.

이번에는 NumPy의 gradient를 사용해 보자.

v_grad = np.gradient(x, dt)

plt.figure(figsize=(7, 4))
plt.plot(t, v_grad)
plt.xlabel('time [s]')
plt.ylabel('velocity [m/s]')
plt.title('Velocity with np.gradient')
plt.grid(True)
plt.show()

np.gradient는 배열의 변화율을 계산한다. np.diff보다 결과 배열의 길이를 유지하기 편해서, 센서 데이터나 시뮬레이션 로그를 다룰 때 자주 쓴다. 지금 단계에서는 np.diff는 "직접 차이를 구하는 방법", np.gradient는 "변화율 계산을 조금 더 편하게 해 주는 도구" 정도로 이해하면 충분하다.

가속도는 속도가 변하는 빠르기다

속도가 위치의 변화라면, 가속도는 속도의 변화다.

[ mathbf{a} = frac{Delta mathbf{v}}{Delta t} ]

속도가 점점 커지면 양의 가속도가 있다. 속도가 줄어들면 감속하고 있는 것이다. 정지해 있는 로봇이 출발할 때도 가속도가 필요하고, 달리던 로봇이 멈출 때도 가속도가 필요하다.

가속도는 단순히 "빨라지는 정도"만 의미하지 않는다. 방향이 바뀌어도 가속도가 생긴다. 일정한 속력으로 원을 도는 로봇을 생각해 보자. 계기판에 표시되는 속력은 그대로일 수 있다. 하지만 속도 벡터의 방향은 계속 바뀐다. 속도 벡터가 바뀌고 있으므로 가속도가 있다.

로봇에서 가속도는 특히 중요하다. 갑자기 큰 가속도를 주면 바퀴가 미끄러질 수 있고, 로봇 팔은 진동할 수 있으며, 드론은 자세를 잃을 수 있다. 그래서 실제 로봇은 목표 지점까지 최단 시간으로만 움직이지 않는다. 너무 거칠게 출발하거나 멈추지 않도록 속도와 가속도를 함께 고려한다.

앞에서 만든 속도에서 가속도를 계산해 보자.

a = np.gradient(v_grad, dt)

plt.figure(figsize=(7, 4))
plt.plot(t, a)
plt.xlabel('time [s]')
plt.ylabel('acceleration [m/s^2]')
plt.title('Acceleration estimated from velocity')
plt.grid(True)
plt.show()

위치가 (x(t) = 0.5t^2)일 때, 속도는 대략 (t)처럼 증가하고, 가속도는 거의 1에 가까운 값이 나온다. 즉 로봇이 매초 같은 정도로 속도를 높이고 있다는 뜻이다.

2D 로봇 궤적에서 속도와 가속도 계산하기

이제 2D 평면으로 돌아가 보자. 로봇이 곡선을 따라 움직인다고 가정하겠다. x 위치는 시간에 따라 앞으로 나아가고, y 위치는 좌우로 부드럽게 흔들린다.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.05
t = np.arange(0, 10 + dt, dt)

x = 0.5 * t
y = np.sin(t)

positions = np.column_stack([x, y])

plt.figure(figsize=(6, 6))
plt.plot(positions[:, 0], positions[:, 1])
plt.scatter(positions[0, 0], positions[0, 1], label='start')
plt.scatter(positions[-1, 0], positions[-1, 1], label='end')
plt.axis('equal')
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.title('2D robot trajectory')
plt.grid(True)
plt.legend()
plt.show()

positions는 각 시간의 로봇 위치를 저장한 배열이다. 모양은 (시간 개수, 2)가 된다. 각 행은 하나의 시각을 의미하고, 두 열은 각각 x와 y를 의미한다.

print(positions.shape)
print(positions[:5])

이런 형태의 배열은 앞으로도 매우 많이 등장한다. 로봇 시뮬레이션 로그, 센서 궤적, 추정 위치, 목표 경로 모두 비슷한 방식으로 저장할 수 있다.

2D 위치 배열에서 속도를 계산하려면 x와 y 각각의 변화율을 구하면 된다.

velocities = np.gradient(positions, dt, axis=0)

vx = velocities[:, 0]
vy = velocities[:, 1]
speed = np.linalg.norm(velocities, axis=1)

plt.figure(figsize=(7, 4))
plt.plot(t, vx, label='vx')
plt.plot(t, vy, label='vy')
plt.plot(t, speed, label='speed', linewidth=2)
plt.xlabel('time [s]')
plt.ylabel('velocity')
plt.title('Velocity components and speed')
plt.grid(True)
plt.legend()
plt.show()

axis=0은 시간 방향으로 변화율을 계산하라는 뜻이다. positions 배열에서 행이 시간 순서이므로, 위아래 행끼리 비교해야 한다. 만약 axis=1로 계산하면 x와 y 사이의 차이를 계산하게 되어 의미가 달라진다.

np.linalg.norm(velocities, axis=1)은 각 시간의 속도 벡터 크기를 계산한다. 즉 속력이다.

가속도도 같은 방식으로 계산할 수 있다.

accelerations = np.gradient(velocities, dt, axis=0)

ax = accelerations[:, 0]
ay = accelerations[:, 1]
accel_mag = np.linalg.norm(accelerations, axis=1)

plt.figure(figsize=(7, 4))
plt.plot(t, ax, label='ax')
plt.plot(t, ay, label='ay')
plt.plot(t, accel_mag, label='acceleration magnitude', linewidth=2)
plt.xlabel('time [s]')
plt.ylabel('acceleration')
plt.title('Acceleration components and magnitude')
plt.grid(True)
plt.legend()
plt.show()

이제 우리는 위치 데이터만 가지고도 속도와 가속도를 추정할 수 있다. 이것은 매우 강력한 도구다. 로봇이 실제로 어떻게 움직였는지 기록만 남아 있다면, 나중에 그 움직임이 빨랐는지, 급하게 방향을 바꿨는지, 어느 구간에서 흔들림이 컸는지 분석할 수 있다.

속도 벡터를 궤적 위에 그려 보기

숫자만 보면 움직임이 잘 느껴지지 않을 때가 많다. 로봇의 궤적 위에 속도 벡터를 화살표로 그려 보자.

step = 10

plt.figure(figsize=(7, 6))
plt.plot(positions[:, 0], positions[:, 1], label='trajectory')
plt.quiver(
    positions[::step, 0], positions[::step, 1],
    velocities[::step, 0], velocities[::step, 1],
    angles='xy', scale_units='xy', scale=2,
    color='orange', label='velocity'
)
plt.axis('equal')
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.title('Velocity vectors on trajectory')
plt.grid(True)
plt.legend()
plt.show()

화살표는 각 위치에서 로봇이 어느 방향으로 움직이는지를 보여 준다. 궤적이 위로 휘어지는 구간에서는 속도 벡터도 위쪽 성분을 가진다. 아래로 내려가는 구간에서는 y 방향 속도가 음수가 된다.

여기서 중요한 감각이 있다. 로봇이 "어디를 보고 있는가"와 "어디로 움직이고 있는가"는 항상 같지 않을 수 있다. 자동차처럼 앞바퀴 방향으로 주로 움직이는 로봇은 둘이 비슷할 때가 많다. 하지만 드론, 로봇 팔 끝점, 미끄러지는 이동 로봇에서는 자세 방향과 실제 속도 방향이 다를 수 있다. 그래서 로봇 상태를 분석할 때 위치, 방향, 속도를 구분해서 생각해야 한다.

출발, 정지, 방향 전환을 가속도로 읽기

로봇이 멈춰 있다가 움직이기 시작하면 속도는 0에서 어떤 값으로 커진다. 이때 가속도가 생긴다. 반대로 로봇이 멈추려면 속도를 줄여야 하므로 가속도는 현재 속도와 반대 방향으로 작용한다.

방향 전환도 마찬가지다. 로봇이 오른쪽으로 가다가 위쪽으로 방향을 바꾸면 속도 벡터가 바뀐다. 이때 속력의 크기가 거의 같더라도 가속도는 0이 아니다.

간단한 예제를 만들어 보자. 로봇이 처음에는 x축 방향으로 움직이다가, 중간 이후에는 y축 방향으로 움직인다고 하자.

dt = 0.1
t = np.arange(0, 8 + dt, dt)

positions = []

for ti in t:
    if ti < 4:
        x = ti
        y = 0
    else:
        x = 4
        y = ti - 4
    positions.append([x, y])

positions = np.array(positions)
velocities = np.gradient(positions, dt, axis=0)
accelerations = np.gradient(velocities, dt, axis=0)

speed = np.linalg.norm(velocities, axis=1)
accel_mag = np.linalg.norm(accelerations, axis=1)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

axes[0].plot(positions[:, 0], positions[:, 1])
axes[0].scatter(positions[0, 0], positions[0, 1], label='start')
axes[0].scatter(positions[-1, 0], positions[-1, 1], label='end')
axes[0].axis('equal')
axes[0].set_xlabel('x [m]')
axes[0].set_ylabel('y [m]')
axes[0].set_title('Path with a corner')
axes[0].grid(True)
axes[0].legend()

axes[1].plot(t, speed, label='speed')
axes[1].plot(t, accel_mag, label='acceleration magnitude')
axes[1].set_xlabel('time [s]')
axes[1].set_title('Speed and acceleration')
axes[1].grid(True)
axes[1].legend()

plt.tight_layout()
plt.show()

경로는 단순한 ㄱ자 모양이다. 로봇은 x축을 따라 가다가 4초 부근에서 위쪽으로 방향을 바꾼다. 이때 속력 그래프만 보면 크게 변하지 않는 것처럼 보일 수 있다. 하지만 가속도 크기를 보면 방향 전환 지점에서 변화가 튀는 것을 볼 수 있다.

이것이 로봇 경로를 부드럽게 만들어야 하는 이유다. 위치만 보면 꺾인 경로도 문제없어 보인다. 하지만 실제 로봇이 그 경로를 따라가려면 꺾이는 순간 속도 방향을 갑자기 바꿔야 한다. 갑작스러운 속도 변화는 큰 가속도를 요구한다. 실제 로봇은 무한히 큰 가속도를 낼 수 없으므로, 경로를 둥글게 만들거나 속도를 줄여야 한다.

다음 부에서 경로와 궤적을 다룰 때 이 감각이 중요해진다. 경로는 "어디를 지나갈 것인가"이고, 궤적은 "언제, 어떤 속도로 지나갈 것인가"까지 포함한다.

로봇 제어에서 속도 명령은 무엇을 의미할까

앞 장들에서 목표 추종 로봇을 만들 때 우리는 로봇에게 속도 명령을 주었다. 예를 들어 목표 방향으로 선속도 v를 주고, 방향을 맞추기 위해 각속도 omega를 주었다.

여기서 선속도는 로봇이 앞으로 얼마나 빠르게 나아가는지를 뜻한다. 각속도는 로봇의 방향 각도 (theta)가 얼마나 빠르게 변하는지를 뜻한다.

[ omega = frac{Delta theta}{Delta t} ]

위치의 변화율이 선속도라면, 각도의 변화율은 각속도다. 따라서 2D 이동 로봇의 상태는 다음과 같이 업데이트할 수 있다.

[ x_{t+dt} = x_t + v cos(theta) dt ]

[ y_{t+dt} = y_t + v sin(theta) dt ]

[ theta_{t+dt} = theta_t + omega dt ]

이 식은 이전에 만든 시뮬레이터와 연결된다. 로봇이 바라보는 방향 (theta)로 속도 (v)만큼 전진하고, 각속도 (omega)만큼 방향을 바꾼다.

코드로 쓰면 다음과 같다.

def update_robot(pose, v, omega, dt):
    x, y, theta = pose

    x_next = x + v * np.cos(theta) * dt
    y_next = y + v * np.sin(theta) * dt
    theta_next = theta + omega * dt

    return np.array([x_next, y_next, theta_next])

pose = np.array([0.0, 0.0, 0.0])
dt = 0.1

trajectory = []

for _ in range(100):
    trajectory.append(pose.copy())
    pose = update_robot(pose, v=1.0, omega=0.3, dt=dt)

trajectory = np.array(trajectory)

plt.figure(figsize=(6, 6))
plt.plot(trajectory[:, 0], trajectory[:, 1])
plt.axis('equal')
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.title('Robot motion from velocity commands')
plt.grid(True)
plt.show()

이 예제에서 로봇은 앞으로 움직이면서 계속 회전한다. 그래서 궤적은 완만한 곡선이 된다. 만약 omega를 0으로 바꾸면 직선으로 움직이고, v를 0으로 바꾸면 제자리에서 회전한다.

이처럼 속도 명령은 단순한 숫자가 아니라, 다음 시각의 위치와 방향을 만들어 내는 원인이다. 시뮬레이션 루프는 이 원인을 작은 시간 간격마다 적용해서 움직임을 만든다.

시간 간격 dt가 결과를 바꾼다

시뮬레이션에서 dt는 아주 중요한 값이다. dt가 크면 계산은 빠르지만 움직임이 거칠어질 수 있다. dt가 작으면 움직임은 부드럽고 정확해지지만 계산량이 늘어난다.

같은 속도 명령을 사용해도 dt에 따라 결과가 조금씩 달라질 수 있다. 특히 회전이 빠르거나 가속도가 큰 상황에서는 차이가 더 커진다.

def simulate(dt):
    pose = np.array([0.0, 0.0, 0.0])
    total_time = 10.0
    steps = int(total_time / dt)

    traj = []
    for _ in range(steps):
        traj.append(pose.copy())
        pose = update_robot(pose, v=1.0, omega=0.8, dt=dt)

    return np.array(traj)

traj_01 = simulate(0.1)
traj_05 = simulate(0.5)

plt.figure(figsize=(6, 6))
plt.plot(traj_01[:, 0], traj_01[:, 1], label='dt = 0.1')
plt.plot(traj_05[:, 0], traj_05[:, 1], 'o-', label='dt = 0.5')
plt.axis('equal')
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.title('Effect of time step')
plt.grid(True)
plt.legend()
plt.show()

dt = 0.5인 경우에는 로봇이 0.5초마다 한 번씩 크게 움직인다. 반면 dt = 0.1이면 더 자주 업데이트하므로 곡선이 부드럽다. 실제 로봇 제어에서도 제어 주기가 중요하다. 너무 늦게 명령을 갱신하면 로봇이 목표를 지나치거나 흔들릴 수 있다.

위치, 속도, 가속도를 하나의 흐름으로 보기

이제 세 가지 개념을 한 줄로 연결할 수 있다.

위치는 로봇이 특정 시각에 어디에 있는지를 나타낸다. 속도는 그 위치가 시간에 따라 얼마나 빠르게 변하는지를 나타낸다. 가속도는 그 속도가 시간에 따라 얼마나 빠르게 변하는지를 나타낸다.

[ text{위치} rightarrow text{속도} rightarrow text{가속도} ]

더 로봇답게 말하면 다음과 같다.

[ mathbf{p}(t) rightarrow mathbf{v}(t) rightarrow mathbf{a}(t) ]

이 관계는 앞으로 계속 등장한다. 센서 데이터에서 속도를 추정할 때도 필요하고, 속도 명령으로 위치를 업데이트할 때도 필요하다. 경로를 부드럽게 만들 때는 가속도를 제한해야 하고, 나중에 최적화 문제를 풀 때는 "너무 급격한 움직임"에 비용을 줄 수도 있다.

이번 장에서 중요한 것은 공식 자체가 아니다. 로봇의 움직임을 시간 순서의 데이터로 바라보는 것이다. 위치 배열이 있고, 그 차이를 보면 속도가 나온다. 속도 배열의 차이를 보면 가속도가 나온다. 이 감각을 잡으면 미분은 더 이상 칠판 위의 기호가 아니라, 로봇이 실제로 움직인 흔적을 읽는 방법이 된다.

다음 장에서는 이 흐름을 한 단계 더 깊게 들어간다. 위치 변화량을 시간 간격으로 나누는 단순한 계산에서 출발해, "순간 변화율"이라는 미분의 핵심 감각을 로봇 예제로 이해해 볼 것이다.

책 목록

로봇을 위한 수학과 Python: 좌표에서 시뮬레이터까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기