PID 제어 아두이노 프로그래밍



PID 이해를 위해 가장 중요한 것은, 바로 오차의 개념이다. 오차란? 말 그대로 오차다. 위에서 예를 들었듯이 만약 드론이 지금 수평상태로 있는데 내가 10도 기울이란 명령을 줬다고 하자. 그렇다면 지금 오차는 바로 10도가 되는 것이다. 시간이 흘러 드론이 11도까지 왔다면 오차는 -1도가 되는 식이다.


자, 본격적으로 설명하기에 앞서 오차를 어떻게 극복하는지 좀더 구체적으로 생각해보자. 어려울것 없다. 이전 포스팅에서 다 얘기한 거다.

먼저 드론이 수평상태로 있다고 생각해보자.
캡처.jpg

검은색이 드론이고, 단순하게 2개의 모터가 있다고 하자. 수평상태, 즉 각도가 0도인 상태를 유지하기 위해서는 두 모터의 추력이 비슷해야한다.

추력이 비슷하단 이야기는 뭘까? 모터의 추력을 조절하는게 뭘까? 바로 모터에 입력되는 전압의 크기이다.
고로, 이 포스팅에서 모터 추력 = 모터에 입력되는 전압크기 라고 보면 된다.

모터의 추력이 강하다 = 모터에 입력되는 전압이 크다.
모터의 추력이 약하다 = 모터에 입력되는 전압이 작다.

이렇게 생각하면 된다.

위와 같은 상태라면, 모터에 들어가는 전압의 크기가 거의 동일할 것이다.
그런데 만약 내가 이상태에서, 드론보고 10도 기울이라고 명령을 줬다고 해보자. 대체 무슨 수로 10도를 기울일 것인가?

캡처2.jpg

모멘트의 개념이 나오는데, 어려울 것 없다. 그냥 1번 추력을 낮추고 2번 추력을 높이면 된다. 그러면 1번은 가라앉고, 2번은 떠서 결과적으로 회전하게 되는 것이다. 즉, 1번 모터에 들어가는 전압을 낮추고, 2번 모터에 들어가는 전압을 높이면 된다. (전압의 크기는 아두이노에서 pwm 신호로 가능하다. 관련 내용은 추후에 포스팅..)

물론, 모터에 들어가는 전압을 너무 낮춰버리면 모터가 돌지 않게되므로 어느정도 선에서 낮춰야한다. 적어도 드론이 떠있을 추력은 가져야할게 아닌가? 예를들어 가만히 있을 때 드론에 인가되는 전압이 10V였다면, 위와 같은 상태에서는 8V 정도 주면 되는 식이다.

자, 이 상태에서 다시 내가 0도로 가라는 명령을 줬다고 하자. 즉, 다시 수평상태를 맞추라는 것이다.
위와 반대과정으로, 1번 출력은 높이고 2번 출력은 낮춰서 다시 반대로 회전시킬 것이다. 그런데 어떻게 정확히 0도를 맞출까? 그냥 회전하다가 얼추 수평맞으면 바로 모터 출력을 갖게 놓으면 될까? 아니다. 드론몸체가 회전하면서 생기는 회전관성이 있기 때문에 그렇게 단순하게는 절대 수평을 잡을 수가 없다.

그렇다면? 아두이노에서 mpu6050을 통해 얻은 각도를 가지고 연산을 해야한다. 즉, 현재 기울어진 각도값과, 목표값인 0도를 계속 비교하는 것이다.
10도 기울어진 상태에서였으므로, 현재값은 10도가 될 것이다.
그러다 모터에 인가되는 전압을 조절하면, 즉 모터 추력을 조절하면 현재값이 9도, 8도, 7도, 하면서 점점 작아진다. 커지다가 3도, 1도, 0도까지 올 것이다. 그런데 0도에 왔으면 더이상 회전하면 안되고 유지해야한다. 어떻게??

바로 오차를 이용하는 것이다. 예시에서 보듯, 10도에서 0도로 기울어질수록 오차가 점점 작아진다. 처음엔 10이었다가 나중엔 0이 되는 것이다.
따라서 오차값을, 1번과 2번 모터에 인가되는 전압의 차이와 연관시키는 것이다.
오차가 10일때는 그만큼 전압차이가 커서 회전하려고 하고, 오차가 0으로 다가갈수록 전압차이가 작아져서 결국 두 모터에는 같은 전압이 인가되는 것이다.

설명이 복잡하군......... 블록다이어그램은 아래와 같다.
main-qimg-f80e2346eca7905cdc90e38450483be6.gif


제어 공부를 좀 해보면 별로 어렵지 않은 그림인데..
Desired state이 우리가 추구하는 0도이다. 그리고 그 아래 measured state이 바로 현재각도값!
보면 첫번째 동그라미에서 Desired State - measured state이 되게 되어있다. 즉 이게 바로 오차고, 오차가 e(t)라고 표현되어있는 것이다.

그 옆에 네모 3개가 차례대로 P, I, D 제어하는 블록을 나타낸다. 특히 Kp, Ki, Kd를 게인(Gain) 값이라고 한다.
별로 어려운 개념은 아니고, 쉽게 얘기해서 P, I, D 제어의 중요도를 나타내는 지표라고 보면 된다.
이를테면 Kp가 커질수록 PID제어를 하는데 있어서 P 부분의 영향이 더 커지는 셈. 아무튼 하나씩 살펴보자.



먼저 P제어. 비례제어부분이다.
아까 위에서 얘기했듯이, 오차가 크면 그것에 비례해서 전압의 차이가 결정된다. 오차가 0이 되면 전압의 차이도 0이 된다.
그런데, 잘 생각해보자.

기울어진 드론이 0도로 다가왔다고 치자. 이제 오차가 1도밖에 안난다. 그리고 비례제어이기 때문에 처음 전압의 차이보다 10분의 1배가 된 전압 차이가 모터에 인가되고 있을 것이다. 그런데 만약 그 전압차이가 드론을 회전시키기에 충분한 회전력을 발생시킬 수 없다면?

다시말해, 1번과 2번 모터에 분명한 추력차이가 있다. 그런데 그 차이가 너무 작아서 드론을 회전시킬수가 없는것이다. 그렇게 되면 아직 오차가 1도라고 남아있는데, P제어만으로는 영원히~~~~~~ 오차를 0으로 만들 수가 없다는 결론이 나온다. 왜? 힘이 부족하니까 말이다.



다음으로는 I 제어. 적분제어 부분이다.
위에서 P제어만으로는 오차를 0으로 만들 수 없을 수도 있다고 이야기했다. 그래서 I제어를 사용한다. P, I, D 제어는 각각 독립적으로 작용한다!
오차가 10도에서 1도로 줄어들면, P제어는 독립적으로 행동하여, 위와 마찬가지로 처음의 10분의 1배인 전압의 차이를 인가하려고 한다.
그리고 I게인은, 10도에서부터 1도까지 줄어들면서 생긴 오차를 계속 누적한다. 어떻게? 현재 오차와 시간을 곱해줌으로써. 고등학교 수학에서 구분구적법을 생각하면 정확하다. 계속 오차 값을 과거부터 누적해오는 것이 I제어인 것이다.

P제어와 I제어를 같이 하게 되면, 말했듯이 서로 독립적으로 작용한다. 그리고 각각 독립적으로 구한 전압의 차이를 더한 값을, 모터에 인가하게 된다. 따라서 PI 제어를 할 때는 두 제어로 구한 전압 차이를 더한 값을 모터에 인가하게 되는 것이다.
오차가 1도로 줄어들었을 때, P제어는 큰 힘을 발휘하지 못하지만 I제어는 계속 누적해왔기 때문에, 오차가 있는 한 계속해서 전압 차이를 더 늘리게 된다. 현재 오차와 시간을 곱한 것이 I 게인으로 구한 제어값이기 때문이다! 고로 오차가 0이 될때까지 계속 전압의 차이를 만들어내는 것이 I 게인이다.


다음은 D 제어. 미분 제어이다.
이것은 현재의 오차값과, 이전 오차값의 차이를 시간으로 나눈 값이다. 즉, 과거의 오차 값을 전압 차이를 결정하는데 할당시켜 보다 부드럽게 움직이도록 하는 것이다. 정확한 개념은 잘 모르지만, D 제어가 생기게 되면 반응성이 더 부드럽게 나타나게 된다.



아래 움짤을 보면 이해가 편할 것이다.

PID_Compensation_Animated.gif





주목해야 할것은, 빨간선이 목표값이란 것이다. 즉 우리로 치면 0도이다.
그리고 파란선이 (0, 0) 지점부터 출발하는데 x축은 시간이고 y축은 현재 각도값이라고 보면 된다. 빨간선이 목표값인 0도이고, y값이 0인데 우리가 생각하는 10도라고 보면 된다.

1. P게인이 점점 늘어난다. 시간이 지날수록 목표값이 더 근접해가는 것을 알 수 있지만, 목표값인 빨간선에 이르진 못하는 것을 볼 수 있다. 아까 얘기했듯이, 오차가 너무 작아서 충분한 전압차이를 인가할 수 없는 것이다.

2. 이 때 I게인을 점점 늘린다. 빨간선에 이르지 못했던 현재 각도값이 쉽게 이르는 것을 알 수 있다. 그런데 동시에 반응이 조금 거칠어 진다. 시간이 지남에 따라 드론이 요동치듯이 목표값에 이르른다. 요동치는 진폭을 보고 Overshoot이라고 한다. 오버슛이 크다는 것은 크게 요동친다는 것. 별로 좋지 않은 제어가 될 것이다.

3. 이 때 D게인을 점점 늘리면, 요동치던 드론이 보다 부드럽게 목표값에 도달하는 것을 볼 수 있다.

사실 쉽게 설명한다고 했지만... 이해가 쉬울지는 모르겠다.


이게 PID 제어의 전부이다.
PID제어는 제어 이론 중에서도 아주 직관적이고 쉬운 제어기법이라고 한다. 굉장히 직관이고, 드론이라는 것의 역학적 모델링을 세우지 않아도 그냥 직관적으로 PID 게인값들을 찾을 수 있다. 어떻게? 실험하면서. 고로!!! 쫄 필요 없다. 이건 굉장히 쉬운 기법이다.

1. 아두이노 코딩을 위해 반드시 알아야할 팩트들

아두이노 IDE를 먼저 받아야하는데, 그건 지난 포스팅에 있으므로!
오늘은 바로 코딩을 보도록 하겠다.

아두이노 IDE를 실행시키면 다음과 같은 문구들이 뜬다.


void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}


void가 뭔지, setup은 뭔지, loop는 뭔지 ()는 뭐고 {}는 뭔지. 아마 처음하는 사람이라면 아무것도 모를 것이다. 그런데 모든 걸 다 알아야 코딩을 할 수 있는게 아니다. 코딩이 뭔가? 알고리즘아닌가? 결국 아두이노 드론을 만드는데 있어서 가장 중요한건 알고리즘이다!
C언어(컴퓨터 프로그래밍 언어)라는 건 결국 우리가 생각한 알고리즘을 언어적으로 풀어주는 데 필요할 뿐, 알고리즘 자체가 될 수는 없기 때문이다.

고로 알고리즘을 짜고, 그 알고리즘을 코딩으로 적용할 줄만 알면 된다. 물론 엄밀히 말하면 프로그래밍 언어에 대한 지식이 있어야 응답속도와 반응성 등과 같은 정밀한 곳에서 더 이점을 가질 수 있겠지만.. 흔히 만드는 아두이노 드론 정도는 딱히 몰라도 가능하다. 고로 시작해보자.


아두이노는 크게 2부분으로 나뉘어져 있다.
void setup, void loop 가 바로 그것이다.

먼저 셋업 부분은 뭘까?

void setup() {
  // put your setup code here, to run once:

}

친절하게 아두이노에서 설명해주고 있다. PUT YOUR SETUP CODE HERE, TO RUN ONCE:
즉, 딱 한번만 실행될 코드를 setup 부분에 채워넣으라는 것이다.


void loop() {
  // put your main code here, to run repeatedly:

}

다음은 루프 부분. PUT YOUR MAIN CODE HERE, TO RUN REPEATEDLY:
즉, 루프 안에 들어가 있는 코드는 리피트!! 반복된다. 아두이노에 업로드하는 순간부터 영원히 반복된다. loop 안에 있는 건 그냥 영원히~~~ 위에서부터 아래로 코드가 실행되게 되는 것이다. setup은 위에서부터 아래로 한차례 실행되고 마는 부분이고, loop는 영원히 계속 반복되면서 실행되게 된다.



따라서 PID를 코딩으로 한다면, 어떤 부분을 setup에 넣고 어떤 부분을 loop에 넣을지를 생각해야한다.

PID는 이전 포스팅에서 말했지만, P, I, D 제어를 각각 한 후 그 제어값을 모두 더하여 적용하게 된다.
즉, P제어 + I제어 + D제어 = PID 제어값 이 되는 것이다.

그리고 PID는 오차와 관련된 개념이라고 했다. loop가 어느정도 주기를 갖고 계속 반복되면서 실행될텐데, 매번 실행될때마다 오차가 달라질 것이다. 따라서 loop안에는 매번 달라지는 오차를 가지고 P, I, D 제어값을 새롭게 갱신해주어야 할 필요가 있다.


프로그래밍언어에서는 변수라는 게 있다. 말그대로 변하는 수다. P, I, D 게인값들은 변하지 않는 상수로 지정되어야 하고 오차가 변수로 지정되어야 할 것이다. (꼭 그런건 아니지만 일반적으로!)
그런데 아두이노는 P, I, D 라는 개념이 없다. 아무것도 모른다. 고로 그게 뭔지, 미리 선언해줄 필요가 있다.


예를 들어, Kp, Ki, Kd를 각각 PID의 게인값이라고 생각해보자. 그리고 난 그 값에 각각 1.2, 2.5, 3.2을 지정해주고 싶다.


그렇다면 아래와 같이 선언해주면 된다.


double Kp = 1.2;
double Ki = 2.5;
double Kd = 3.2;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

자, 우측은 이해하기가 쉽다. Kp = 1.2 이런 부분은 직관적으로 와닿을 것이다. 그런데 좌측에 double은 뭔가?
아두이노는 우리가 선언하려는 숫자가 정수인지, 실수인지도 모른다. 그걸 우리가 말해주지 않으면 얘는 해석을 못한다. 1.2는 정수가 아니라 실수 영역에 있기 때문에 실수 변수를 얘기하는 double로 선언해주어야 한다. 정수 변수는 int 로 바꿔주면 된다.

자 이제 게인값들을 선언했으니, 오차도 선언해주도록 하자. 오차는 error 라고 하겠다. 오차는 정수보다는 실수인게 더 섬세하고 좋을 것 같으니 실수로 선언!


double Kp = 1.2;
double Ki = 2.5;
double Kd = 3.2;
double error;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

이 경우는 error에 특정 값을 넣지 않았다. 왜? 이건 변수니까 말이다. Kp, i, d는 상수로 고정시킬 것이기 때문에 저렇게 미리 선언해놓은 거고, error는 루프가 계속 반복실행됨에 따라 변하는 수이기 때문에 굳이 넣지 않았다. 저렇게 아무것도 넣지 않으면 초기값이 0으로 자동배정되는 걸로 알고 있다.

이제, 오차에 필요한 현재값과 목표값을 선언해보도록 하자.
목표값은 이전 포스팅에서 예를 든 것처럼, 10도라고 하고 desired_angle로 선언하겠다. 그리고 현재값은 mpu6050을 통해 추출한 각도값으로, 실제 코드는 있다고 가정하고 그 값은 current_angle 이라고 하겠다.

double Kp = 1.2;
double Ki = 2.5;
double Kd = 3.2;
double error;

double desired_angle = 10;
double current_angle;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

자, 이게 error을 정의할 필요가 있다. error는 오차이다.
즉, 목표값에서 현재값을 뺀 수치가 될 것이다.
그리고 이 오차는 매 순간순간 달라질 것이다. 드론이 첨엔 10도 오차가 있다가,
 그 쪽으로 회전을 시작하면 9도 8도 ... 1도 0도까지 점점 오차가 줄어들게 아닌가. 고로 매 순간순간 갱신해줘야하는 변수가 되는 것이다.
그렇다면? 당연히 loop안에서 선언되어야 한다.

double Kp = 1.2;
double Ki = 2.5;
double Kd = 3.2;
double error;

double desired_angle = 10;
double current_angle;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

  current_angle갱신 코드;
  error = desired_angle - current_angle;

}

처음에 넣은 current_angle갱신코드란? 이건 현재 각도값을 의미하는데, 현재 드론이 기울어진 각도값도 mpu6050의 연산에 의해 실시간으로 변화되어야만 한다. 따라서 loop안에 넣어준거고, 이 부분은 mpu6050에 관련된 코딩이 필요하기 때문에 생략하고 위와 같이 표시하였다.

자, 이제 오차인 error가 아두이노에서 선언되어 해석가능해졌으므로, 본격적으로 PID 제어를 시작해보자.

double Kp = 1.2;
double Ki = 2.5;
double Kd = 3.2;
double error;
double error_previous;

double desired_angle = 10;
double current_angle;

double P_control, I_control, D_control;
double Time = 0.004;


void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

  current_angle갱신 코드;
  error = desired_angle - current_angle;

  P_control = Kp * error;
  I_control = I_control + Ki * error * Time;
  D_control = Kd * (error - error_previous) / Time;

  error_previous = error;

}


이번엔 추가된 게 좀 많다. 먼저 loop안을 보면, P컨트롤, I컨트롤, D컨트롤, 그리고 error_previous, Time 등과 같은 새로운 변수들이 선언된 것을 알 수 있다.
고로 맨 윗부분에 선언해준 것이다. 특히 Time은 한 루프가 도는 데 걸리는 시간을 의미한다. 내가 만든 드론은 한 루프 도는데 걸리는 시간이 약 3~4ms기 때문에 0.004로 해준건데, 사실 꼭 이렇게 할 필요는 없다. 그냥 4로 해도되고 4000으로 해도되고. Time이 수식적으로 가지는 의미는 그냥 '상수'일 뿐이다. 따라서 아무렇게나 지정해줘도, 또 다른 상수인 Kd나 Ki로 커버할만한 수준이기만 하면 되는것이다.
쉽게 생각해서, 4 * 6이나, 3 * 8이나 똑같은 수 아닌가. 곱해서 똑같이 나오게 숫자를 조정할 수 있기 때문에 꼭 time을 주기로 맞춰줄 필요는 없다는 것이다.

그리고 가장 아래보면 error_previous = error라고 선언한 부분이 있다. 아두이노에서는 이럴 때 우측에 있는 녀석을 좌측값으로 저장하라고 해석한다.
즉, error_previous라는건 지금 루프가 아니라 바로 이 전에 루프가 실행될 때의 error값을 의미한다.

만약 처음 error가 3이라고 해보자. 그러면 loop는 위에서부터 아래로 실행될 것이므로 error_previous에도 3이 저장 될 것이다.
그리고 loop가 다시 돌았을 때, 갱신된 현재각도에 의해 오차인 error가 5가 되었다고 해보자. 그리고 D_control을 보면, Kd * (5 - 3) / 0.004 가 될 것이다.
즉 컨트롤 부분에서 계산할 때 바로 직전의 에러 값이 previous에 저장되는 것이다.

I_control 부분은, 기존 I_control 값에다가 추가로 I 제어를 통해 얻은 값을 더하라는 것이다. 이전 포스팅에서 I 제어의 역할이 오차를 누적시켜 최종적으로 오차를 0으로 보내는 데 있다고 했는데, 이와 같은 방법으로 '누적' 시킨다.
다만 언어적으로 좀 더 편하게 쓸 수 있다.

I_control += Ki * error * Time

이렇게 +=라고 하면, 위와 똑같은 의미를 가진다. 그냥 언어적인 것이니까 외우고 사용하면 된다.


이제 다음으로 넘어가보자.


double Kp = 1.2;
double Ki = 2.5;
double Kd = 3.2;
double error;
double error_previous;

double desired_angle = 10;
double current_angle;

double P_control, I_control, D_control;
double Time = 0.004;
double PID_control;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

  current_angle갱신 코드;
  error = desired_angle - current_angle;

  P_control = Kp * error;
  I_control += Ki * error * Time;
  D_control = Kd * (error - error_previous) / Time;

  PID_control = P_control + I_control + D_control;
  PID_control = constrain(PID_control, 0, 255);
  analogWrite(6, PID_control);

  error_previous = error;

}

PID_control이라는 변수를 또 만들었다. 말했듯이, PID제어값은 P, I, D 제어를 통해 나온 값들을 모두 더한 값이므로 그냥 더해준 것이다.
그 뒤에 나오는 constrain은, 아두이노에서 제공하는 함수이다.
저걸 쉽게 얘기하면, PID_control이라는 값이 0부터 255까지의 숫자 범위를 가지도록 제한하는 것이다.
예를들어 PID_control이 -100이라는 값이 되면, 0으로 저장이 되고, 1000이라는 값이 되면 255로 저장이 된다. 0과 255사이의 값은 그 값 그대로 저장된다.
이렇게 해주는 이유는, 그 뒤에 나오는 anaglogWrite이라는 함수가 0부터 255사이의 숫자만 받아들이기 때문이다.

analogWrite은 전압을 인가하는 함수로, 6번 핀에 0~255 사이에 해당하는 전압을 주라는 명령신호다. 0은 0V, 255는 5V를 주게 되는데
사실 실제 드론을 만들 때는 이 함수를 쓰지 않을 것이다. servo 라이브러리를 활용하여 다른 함수를 쓸 예정인데
이 포스팅의 목적은 아무튼 PID 제어 코드의 기본을 설명하는 것이기 때문에 그냥 그대로 가는걸로..

최종적으로 나온 PID_control 이라는 값이 가장 중요하다. 이게 PID 제어를 통해 구한 제어값이 되기 때문이다.


다음으로, 조금 보기좋게 코드를 정리하기 위해 다음과 같이 해보았다.

double Kp = 1.2;
double Ki = 2.5;
double Kd = 3.2;
double error;
double error_previous;

double desired_angle = 10;
double current_angle;

double P_control, I_control, D_control;
double Time = 0.004;
double PID_control;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:
  pidcontrol();

}

void pidcontrol() {

  current_angle갱신 코드;
  error = desired_angle - current_angle;

  P_control = Kp * error;
  I_control += Ki * error * Time;
  D_control = Kd * (error - error_previous) / Time;

  PID_control = P_control + I_control + D_control;
  PID_control = constrain(PID_control, 0, 255);
  analogWrite(6, PID_control);

  error_previous = error;


}

뭐가 달라졌나?
바로 void pidcontrol 이라는걸 만들었다는 데 있다. 이건 쉽게 말해, 그냥 그룹으로 묶어놓는거라고 보면 된다.
loop가 가장 중요한 부분인데, 위위처럼 loop안에 모든 코드를 작성하게 되면 보기가 매우 불편하다. 때문에 항목별로 묶어서 따로 이렇게 정리해놓는 것이다. loop안에는 pidcontrol() 이라고만 선언해주면, pidcontrol이라고 선언된 그룹이 실행되게 된다.
고로 위랑 위위는 똑같은 코드이다! 정리해서 보기가 더 수월할 뿐!



이게 전부다!
PID 제어는 이렇게 하면 된다. 시작할때 오차는 10도였겠지만 시간이 흐르면 0으로 점점 줄어들어 원하는 목표값에 도달할 것이다.
단, Kp, Ki, Kd라는 게인값을 잘 맞추었다면 말이다. 이전 포스팅에서 움짤을 하나 사용했었는데, 그것처럼 반응을 보일 것이다. 제대로 게인값을 맞추지 않으면 오차가 오히려 더 커질 수도 있다. 드론이라는 시스템에 맞춰 적당한 값을 맞춰주어야 하는데 이 부분은 실험을 통해서 검증할 수 있다.


아무튼 제어 코드는 이와 같다. 실제 드론을 날릴때는 이중 pid라고 하여, pid 제어를 겹으로 싸서 이중으로 만든 걸 쓸텐데 별로 어려울 건 없다. 똑같다! 그냥 겹으로 한번 싸주기면 하면 된다.
위로 스크롤