2013년 12월 2일 월요일

[Linux] 리눅스 시스템 서비스를 병렬화하여 부팅 속도 향상하기

Level: Intermediate
James Hunt
소프트웨어 엔지니어, IBM
2003년 9월 17일
가용성을 희생하지 않고 리눅스 시스템의 부팅 속도를 향상시키는 방법을 설명한다. 필요한 기술은, 시스템 서비스와 그들의 의존성을 이해해야 하고 그것을 병렬로 시작할 수 있어야 한다.
Microsoft Windows 사용자들 마저도 리눅스는 탁월한 시스템이라고 목소리를 높이고 있다. 하지만 "on" 버튼을 누르고 실제로 리눅스 시스템을 사용할 수 있을 때 까지 걸리는 긴 시간은 리눅스의 가장 큰 문제점이다. 리눅스는 부팅 시간이 너무 길다.
시작하기 전에..
리눅스 설정 스크립트에 익숙할 때에만 이 글의 내용을 실험할 수 있다. 시스템 시작 설정을 변경하는 것은 위험하고 부팅이 안되는 시스템을 만들 수도 있다. 만약 이런 일이 발생한다면 싱글 유저 모드(runlevel 1)로 재부팅하고 변경한 것을 취소하고 재부팅한다. 변경한 모든 파일들은 백업하고 최악의 상황에 대비하여 시스템 백업 이미지 하나 정도를 갖고있어야한다.
내가 제안한 대로 시스템을 변경하기 전에 만만한 테스트 시스템을 사용하기 바란다. 머신이 하나 뿐이라면 가장 유용한 것이 UML(User Mode Linux)이다. UML은 커널 패치로서 리눅스 커널이 바이너리로 컴파일 될 수 있도록 한다. 정상 시스템의 프로세스 처럼 전체 리눅스 시스템을 실행할 수 있다는 것을 의미한다. (참고자료)
리눅스 부트 시퀀스와 runlevel
리눅스 시스템이 부팅되면 많은 단계를 거치게된다. 이 글에서는 모든 단계를 다루지는 않겠다. 커널이 로딩된 후 단계가 흥미롭다.
현재 머신의 runlevel을 확인하려면 /sbin/runlevel 명령어를 실행한다.
커널이 로딩되고 실행이 시작되면 /sbin/init 프로그램이 호출된다. 이 프로그램은 root로서 실행되며 "runlevel"을 초기 부팅 시간에 요청된 것으로 설정한다.
runlevel
runlevel 은 단순한 숫자이다. 리눅스는 이를 사용하여 머신이 부팅되는데 필요한 고급 설정 유형들을 구별한다. Red Hat Linux 시스템의 예를 들어본다. (표 1).
표 1. Red Hat Linux runlevel
Runlevel 의미
0 시스템 정지
1 Single user mode (관리 목적에만 사용)
2 Multi-user mode(네트워크 불가)
3 Multi-user mode(네트워크 가능)
4 사용되지 않은 runlevel
5 Multi-user mode(네트워크 가능) & X-Windows (그래픽 로그인)
6 재부팅
init이 시스템을 초기화하는 방법
init은 ASCII 설정 파일(/etc/inittab)을 사용하여 runlevel을 변경하는 방법을 명령한다. 일반적으로 이 설정파일은 init에게 /etc/rc.d/rc 스크립트를 실행할 것을 지시하며 여기에 runlevel 넘버를 보낸다.
rc.sysinit 스크립트
Red Hat 시스템에서 rc 스크립트를 실행하기 전에, init은 /etc/rc.d/rc.sysinit 스크립트를 실행한다. 이것은 시스템 시계 세팅, 에러 디스크 점검, 파일 시스템 마운팅 같은 저수준 셋업 태스크를 수행한다.
시스템 서비스
rc 스크립트는 사용자가 필요로하는 모든 서비스를 시작하는 것을 담당하고 있다. 이름에서 시사하는 바와 같이 서비스들은 시스템이 제공하는 유용한 장치들이다. 시작해야 할 서비스들이 상당히 많다. 대부분의 리눅스 시스템들은 sshd (SecureShell service), syslog (system logging facility), lpd (printing service)를 시작하지만 더 많을 수도 있다. 예를 들어 Red Hat 9 시스템은 현재 29개의 서비스를 실행한다. 하지만 모든 서비스를 다 구동한다면 50개에 이르게된다.
각 서비스들은 특정 runlevel에서 시작해야만 한다는 것을 이해하는 것이 중요하다.
서비스는 어디에 있을까?
서비스 디렉토리 대안
몇몇 리눅스 시스템에서는 /etc/init.d/ 디렉토리에 서비스가 위치해 있다.
서비스들은 대게 /etc/rc.d/init.d/ 디렉토리에 있다.
이 디렉토리를 검색해 보면 몇몇 서비스들은 힘든 작업을 실제로 수행 할 다른 프로그램을 호출하는 쉘 스크립트라는 것을 알 수 있다.
rc 스크립트가 각 runlevel에서 실행될 스크립트를 판단하는 방법
특정 서비스가 특정 runlevel에서 시작하는 것을 원하지 않는다는 주제로 돌아가서 어떻게 이것을 수행하도록 시스템에게 명령할 것인가? 해답은 /etc/rc.d/ 디렉토리와 init.d/ 디렉토리이다. 이 디렉토리들의 이름은 rc<runlevel>.d/ 식으로 이름이 붙여진다. 예를 들어 runlevel 5에 대한 디렉토리는 /etc/rc.d/rc5.d/가 된다. 이 rc.d 디렉토리들은 /etc/rc.d/init.d/ 디렉토리의 실제 서비스 프로그램으로 가는 심볼릭 링크를 포함하고 있다. 사실 각 서비스 당 두 개의 심볼릭 링크가 있다.
서비스 링크 이름
실제 서비스 프로그램에 대한 심볼릭 링크의 이름은 중요하다. rc 스크립트가 이들을 핸들할 방법을 알 수 있도록 하기위해 엄격한 네이밍 규칙을 따라야한다.
각 링크의 이름은 링크되는 서비스의 이름이 뒤에 붙여진다.
접두사는 두 부분으로 이루어져있다. 하나의 대문자, 그 다음이 두 자리 숫자 이다. 대문자는 "S" (시작) 또는 "K"(죽이다(kill) 또는 정지)이다. 두 자리 숫자의 범위는 00에서 99까지 이다.
서비스 링크 이름 정규식
서비스에 대한 심볼릭 링크의 이름은 egrep 정규식으로 요약된다; [SK][0-9]{2}[a-zA-Z]+.
서비스의 시작과 종료
리눅스 머신을 그래픽 모드(runlevel 5)로 부팅하려고 한다면 init이 rc 스크립트를 호출하고 이를 runlevel 넘버로 전달할 때 rc 스크립트는 /etc/rc.d/rc5.d/ 디렉토리에서 이를 보고 찾는 모든 링크를 실행할 것이다. 두 개의 구별된 단계에서 링크를 실행한다. 첫 번째는 "K"로 시작하는 모든 링크를 실행하면서 이 링크를 "stop" 매개변수에 전달한다. 이것은 관련된 모든 서비스를 정지시킨다.
필요한 모든 서비스를 정지시키면서 "S"로 시작하는 모든 링크를 실행할 것이다. 동시에 여기에 "start" 매개변수를 전달한다. 관련된 모든 서비스를 시작하는 것이다. rc 스크립트는 각 프로그램에 "start" 매개변수를 전달한다.
rc가 각 서비스 프로그램에 "start" 또는 "stop" 매개변수를 전달하는 이유는 같은 서비스 프로그램이 이 서비스를 시작하고 종료할 때 사용될 수 있도록 하기 위해서이다. 이 서비스 프로그램은 시스템이 부팅되었는지 또는 정지되었는지를 전달된 매개변수 값을 통해 안다.
아직 설명하지 않은 중요한 부분이 있다. 링크 이름의 숫자 부분이다. 각 링크 이름에서 "S"나 "K" 뒤에 있는 두 자리 숫자는 rc 스크립트가 사용하여 링크의 실행 순서를 정한다. 낮은 숫자를 가진 링크는 높은 숫자 링크보다 먼저 실행된다.
어려운가? 예제를 보면 이해하는데 도움이 될 것이다.
Listing 1. 서비스 프로그램에 링크되는 Runlevel 5

# cd /etc/rc.d/rc5.d
# ls -al
total 8
drwxr-xr-x    2 root     root     4096 Jul 15 09:29 .
drwxr-xr-x   10 root     root     4096 Jun 21 08:52 ..
lrwxrwxrwx    1 root     root       19 Jan  1  2000 K05saslauthd -> ../init.d/saslauthd
lrwxrwxrwx    1 root     root       20 Feb  1  2003 K15postgresql -> ../init.d/postgresql
lrwxrwxrwx    1 root     root       13 Jan  1  2000 K20nfs -> ../init.d/nfs
lrwxrwxrwx    1 root     root       14 Jan  1  2000 K24irda -> ../init.d/irda
lrwxrwxrwx    1 root     root       17 Jan  1  2000 K35winbind -> ../init.d/winbind
lrwxrwxrwx    1 root     root       15 Jan  1  2000 K50snmpd -> ../init.d/snmpd
lrwxrwxrwx    1 root     root       19 Jan  1  2000 K50snmptrapd -> ../init.d/snmptrapd
lrwxrwxrwx    1 root     root       16 Jun 21 09:43 K50vsftpd -> ../init.d/vsftpd
lrwxrwxrwx    1 root     root       16 Jun 21 08:57 K73ypbind -> ../init.d/ypbind
lrwxrwxrwx    1 root     root       14 Jun 21 08:54 K74nscd -> ../init.d/nscd
lrwxrwxrwx    1 root     root       18 Feb  8 11:15 K92iptables -> ../init.d/iptables
lrwxrwxrwx    1 root     root       19 Feb  1  2003 K95firstboot -> ../init.d/firstboot
lrwxrwxrwx    1 root     root       15 Jan  1  2000 S05kudzu -> ../init.d/kudzu
lrwxrwxrwx    1 root     root       14 Jun 21 08:55 S09isdn -> ../init.d/isdn
lrwxrwxrwx    1 root     root       17 Jan  1  2000 S10network -> ../init.d/network
lrwxrwxrwx    1 root     root       16 Jan  1  2000 S12syslog -> ../init.d/syslog
lrwxrwxrwx    1 root     root       17 Jan  1  2000 S13portmap -> ../init.d/portmap
lrwxrwxrwx    1 root     root       17 Jan  1  2000 S14nfslock -> ../init.d/nfslock
lrwxrwxrwx    1 root     root       18 Jan  1  2000 S17keytable -> ../init.d/keytable
lrwxrwxrwx    1 root     root       16 Jan  1  2000 S20random -> ../init.d/random
lrwxrwxrwx    1 root     root       16 Jun 21 08:52 S24pcmcia -> ../init.d/pcmcia
lrwxrwxrwx    1 root     root       15 Jan  1  2000 S25netfs -> ../init.d/netfs
lrwxrwxrwx    1 root     root       14 Jan  1  2000 S26apmd -> ../init.d/apmd
lrwxrwxrwx    1 root     root       16 Jan  1  2000 S28autofs -> ../init.d/autofs
lrwxrwxrwx    1 root     root       14 Jan  1  2000 S55sshd -> ../init.d/sshd
lrwxrwxrwx    1 root     root       20 Jan  1  2000 S56rawdevices -> ../init.d/rawdevices
lrwxrwxrwx    1 root     root       16 Jan  1  2000 S56xinetd -> ../init.d/xinetd
lrwxrwxrwx    1 root     root       14 Feb  1  2003 S58ntpd -> ../init.d/ntpd
lrwxrwxrwx    1 root     root       13 Jun 21 10:42 S60afs -> ../init.d/afs
lrwxrwxrwx    1 root     root       13 Jan  1  2000 S60lpd -> ../init.d/lpd
lrwxrwxrwx    1 root     root       16 Feb  8 17:26 S78mysqld -> ../init.d/mysqld
lrwxrwxrwx    1 root     root       18 Jan  1  2000 S80sendmail -> ../init.d/sendmail
lrwxrwxrwx    1 root     root       13 Jan  1  2000 S85gpm -> ../init.d/gpm
lrwxrwxrwx    1 root     root       15 Mar 22 08:24 S85httpd -> ../init.d/httpd
lrwxrwxrwx    1 root     root       15 Jan  1  2000 S90crond -> ../init.d/crond
lrwxrwxrwx    1 root     root       13 Jan  1  2000 S90xfs -> ../init.d/xfs
lrwxrwxrwx    1 root     root       17 Jan  1  2000 S95anacron -> ../init.d/anacron
lrwxrwxrwx    1 root     root       13 Jan  1  2000 S95atd -> ../init.d/atd
lrwxrwxrwx    1 root     root       15 Jun 21 08:57 S97rhnsd -> ../init.d/rhnsd
lrwxrwxrwx    1 root     root       14 Jul 15 09:29 S98wine -> ../init.d/wine
lrwxrwxrwx    1 root     root       13 Feb  8 17:26 S99db2 -> ../init.d/db2
lrwxrwxrwx    1 root     root       11 Jun 21 08:52 S99local -> ../rc.local
# 
복잡한 시스템 처럼 보이지만 상당한 유연성을 갖고 있다. 특정 runlevel에서 서비스를 일시적 불가 상태로 하려면 해당 링크를 제거하면 된다. 하지만 링크 조작은 조심해야한다. 에러를 유발하기 때문이다. 지루한 것도 감안해야 한다. 여기 보다 나은 방법이 있다. chkconfig이라는 명령어 형식을 사용해보자.
chkconfig & xinetd
chkconfig의 새 버전이 있다면 xinetd의 설정을 보여주는 메인 아웃풋 말미에 한 개의 섹션을 갖게된다. 이 섹션은 Listing 2에서는 생략하였다.
사용가능한 서비스 발견하는 방법
사용가능한 서비스를 찾으려면, 다음 명령어를 실행한다:
/sbin/chkconfig --list
Listing 2는 이 명령어의 결과이다. 한 라인 당 여덟 개의 칼럼이 있다.
chkconfig 명렁어는 서비스의 온/오프 변환에도 사용될 수 있다.
Listing 2. chkconfig 아웃풋--list|sort

afs             0:off   1:off   2:off   3:on    4:off   5:on    6:off
anacron         0:off   1:off   2:on    3:on    4:on    5:on    6:off
apmd            0:off   1:off   2:on    3:on    4:on    5:on    6:off
atd             0:off   1:off   2:off   3:on    4:on    5:on    6:off
autofs          0:off   1:off   2:off   3:on    4:on    5:on    6:off
crond           0:off   1:off   2:on    3:on    4:on    5:on    6:off
db2             0:off   1:off   2:off   3:on    4:off   5:on    6:off
firstboot       0:off   1:off   2:off   3:off   4:off   5:off   6:off
gpm             0:off   1:off   2:on    3:on    4:on    5:on    6:off
httpd           0:off   1:off   2:off   3:off   4:off   5:on    6:off
iptables        0:off   1:off   2:off   3:off   4:off   5:off   6:off
irda            0:off   1:off   2:off   3:off   4:off   5:off   6:off
isdn            0:off   1:off   2:on    3:on    4:on    5:on    6:off
keytable        0:off   1:on    2:on    3:on    4:on    5:on    6:off
kudzu           0:off   1:off   2:off   3:on    4:on    5:on    6:off
lpd             0:off   1:off   2:on    3:on    4:on    5:on    6:off
mysqld          0:off   1:off   2:off   3:on    4:off   5:on    6:off
netfs           0:off   1:off   2:off   3:on    4:on    5:on    6:off
network         0:off   1:off   2:on    3:on    4:on    5:on    6:off
nfs             0:off   1:off   2:off   3:off   4:off   5:off   6:off
nfslock         0:off   1:off   2:off   3:on    4:on    5:on    6:off
nscd            0:off   1:off   2:off   3:off   4:off   5:off   6:off
ntpd            0:off   1:off   2:off   3:on    4:off   5:on    6:off
pcmcia          0:off   1:off   2:on    3:on    4:on    5:on    6:off
portmap         0:off   1:off   2:off   3:on    4:on    5:on    6:off
postgresql      0:off   1:off   2:off   3:off   4:off   5:off   6:off
random          0:off   1:off   2:on    3:on    4:on    5:on    6:off
rawdevices      0:off   1:off   2:off   3:on    4:on    5:on    6:off
rhnsd           0:off   1:off   2:off   3:on    4:on    5:on    6:off
saslauthd       0:off   1:off   2:off   3:off   4:off   5:off   6:off
sendmail        0:off   1:off   2:on    3:on    4:on    5:on    6:off
snmpd           0:off   1:off   2:off   3:off   4:off   5:off   6:off
snmptrapd       0:off   1:off   2:off   3:off   4:off   5:off   6:off
sshd            0:off   1:off   2:on    3:on    4:on    5:on    6:off
syslog          0:off   1:off   2:on    3:on    4:on    5:on    6:off
vsftpd          0:off   1:off   2:off   3:off   4:off   5:off   6:off
winbind         0:off   1:off   2:off   3:off   4:off   5:off   6:off
wine            0:off   1:off   2:on    3:on    4:on    5:on    6:off
xfs             0:off   1:off   2:on    3:on    4:on    5:on    6:off
xinetd          0:off   1:off   2:off   3:on    4:on    5:on    6:off
ypbind          0:off   1:off   2:off   3:off   4:off   5:off   6:off
Listing 2의 첫 번째 칼럼은 서비스의 이름이고 그 다음 칼럼은 runlevel이며 각 runlevel의 서비스 상태이다. 예를 들어 ntpd 서비스는 runlevel 3과 5 에서 시작하는 것으로 설정되어있다. 그리고 sshd 서비스는 runlevel 2,3,4,5에서 켜진다.
어떤 서비스도 runlevel 0과 6에서는 시작하지 않는다는 것에 주목하라. 표 1을 보면 이유는 명확해진다. runlevel 0은 시스템의 정지를 의미한다. runlevel 6도 같은 의미이다.
Runlevel 1 -- "single-user mode" --은 무엇인가 잘못되어갈 때 사용되는 특별한 runlevel이다. 전통적으로 runlevel 1에서 실행되는 유일한 애플리케이션은 쉘이다. 이는 수퍼유저가 시스템 손상복구를 하거나 안전한 환경에서 시스템을 변경할 때 허용된다. 오직 수퍼유저만이 시스템에 액세스 할 수 있기 때문에 안전하다.
사용가능한 서비스 vs 실행 서비스
가끔, 몇 가지 이유로 서비스가 시작할 수 없는 경우가 있다. 서비스가 실행중인지를 확인하려면 다음 명령어를 실행한다:
/sbin/service --status-all
이 명령어는 서비스 당 한 개 이상의 라인을 내보내면서 각 서비스가 실행중인지를 보여주고 만약 실행중이면 PID (process id) 같은 서비스 스팩 아웃풋을 보여준다.
전통적인 서비스 프레임웍의 한계
시작으로 설정된 모든 서비스가 시작되었을 때 리눅스 시스템에 로그인 할 수 있다. 50개의 서비스가 시작되기를 기다리는 것은 수분이 걸릴 수 있다.
나는 이 프로세스의 속도를 향상시킬 방법을 발견했다. 서비스를 정지하라는 것을 의미하지 않는다. 하지만 사용되지 않는 서비스를 정지시키는 것은 매우 합리적인 일이다. 부팅 속도를 높일 뿐 아니라 보안을 위해 사용된 부분의 노출도 줄일 수 있다.
리눅스 시스템이 부팅되면 직렬 방식(하나씩 순서대로)으로 시작으로 설정된 모든 서비스가 실행된다. 이는 시간 소비가 많은 작동이라 할 수 있다.
서비스 시작 속도를 높일 수 있는 분명한 방법은 모든 서비스를 병렬로 실행하는 것이다. 이렇게 하여 동시에 모두를 시작할 수 있다. 하지만 이는 분명 상당히 매력적인 제안이지만 작동은 하지 않는다. 서비스들 간 의존성 문제 때문이다. 리눅스는 이러한 의존성을 완전히 드러내지 않는다. 하지만 있기는 있다. 서비스 프로그램으로의 링크의 네임 포맷을 기억하는가? "S"나 "K" 뒤에 있는 두 자리 숫자들이 링크 실행 순서를 결정한다. 이 숫자는 미완성된 순서이다. 따라서 가끔씩은 강제적이다.
서비스들 간 의존성
Listing 1을 보면 네트워크 서비스(S10network)는 ntpd 서비스(S58ntpd) 전에 실행된다. ntpd 서비스는 네트워크가 사용가능 한 상태가 되어 로컬 타임 서버와 연결할 수 있어야 하기 때문에 당연한 수순이다. 하지만 이러한 미완의 순서는 우리가 원하는 모든 것을 말해주지 안는다. 예를 들어 Listing 1에서 lpd 서비스(S60lpd)는 네트워크 서비스 다음에 실행되었다. 네트워크에 연결되어 네트워크 프린터를 사용하는 리눅스 시스템에 있어서는 맞는 것일지라도 lpd 서비스가 네트워크 서비스 다음에 실행되어야 한다는 것을 따르지 않았다. 실제로 이 경우 네트워크 전에 lpd를 시작하는게 더 옳다.
또 다른 예제를 들어본다. crond (cron daemon) 서비스(S90crond Listing 1)는 네트워크가 시작된 후에 실행된다. 하지만 원격 머신에서 파일을 사용하는 cron 파일이 없다면 crond가 네트워크 전에 시작해서는 안된다 .
일반적인 경향은 안전하게 작동하는 것이고 중요한 서비스 먼저 시작하는 것이다. 그런다음 남아있는 것을 실행하게 된다. 이것이 리눅스에서는 전통이다.
몇몇 서비스는 다른 서비스에 의존적이지 않는 "스탠드얼론" 서비스이지만 어떤 서비스들은 다른 것에 의존한다는 점이다.
모든 서비스들을 병렬로 시작할 수 없더라도 의존성이 없는 서비스들을 병렬로 시작할 수 있다. 이렇게 독립적 서비스들이 시작하면 의존성을 지닌 서비스들을 시작할 수 있다. 이 과정은 모든 서비스가 시작할 때 까지 반복된다.
복잡한 문제 같지만 이 문제를 풀 수 있는 프로그램도 이미 작성되었다. 다름아닌 make이다.
소프트웨어 컴파일과 관련하여 make는 우리가 필요로하는 프레임웍을 제공한다. 우리가 필요로하는 것은 make에게 서비스들 간 의존성을 가진것이 무엇인지 말하는 것이다. 이것은 -j 플래그를 이용하여 상호 의존성을 계산하는 힘든일을 수행한다 .
서비스들 간 의존성 분석
전통적인 리눅스 시스템은 서비스들간 의존성을 드러내지 않는다. 이제는 의존성 문제를 규명하는 힘든일을 해야한다.
서비스 이해하기
/sbin/chkconfig --list 명령어를 실행할 때, 여러분이 인식하지 못하는 서비스에 맞딱드린다면 천천히 문제를 분석하도록 한다. 가장 쉬운 방법은 서비스를 제어하고 있는 스크립트 상단의 주석을 검색하는 것이다. 장치가 필요하지 않다면 서비스를 off 시킬 수 있다.
이제부터 간단한 예제를 사용한다. ntpd는 네트워크가 필요하다. 따라서 ntpd 서비스는 네트워크 서비스에 의존적이다.이는 make 신택스로 다음과 같이 표현된다:
ntpd : network
netfs 서비스가 네트워크에 의존적이라고 확실히 말할 수 있다. 내 시스템에서는 autofs 서비스 역시 네트워크 서비스에 의존적이다. 우리의 "의존성 표"는 다음과 같다:
ntpd : network
netfs : network
autofs : network

일단 네트워크 서비스가 시작되면 ntpd, netfs, autofs 서비스를 병렬로 시작할 수 있음을 의미한다.
모든 서비스가 실행하는데 10초가 걸린다고 상상해보자. 전통적은 서비스 시작 메소드를 가지고서는 네트워크, ntpd, netfs, autofs 서비스를 시작하는 데 40초가 걸린다. 이 기술을 사용하면 20초가 걸린다. 50%나 절약된다.
구현 샘플
아래 참고자료의 zip 파일에는 구현샘플이 포함되어 있다. make 명령어를 호출하는 변경된 rc 스크립트를 비롯하여 GNU makefile인 runlevel.mk, start5.mk, stop5.mk 등이 포함되어 있다. runlevel.mk makefile은 제어 프로그램이고, start5.mk와 stop5.mk는 서비스의 시작과 종료 시 서비스 의존성을 인코딩한다.
참고자료

홈페이지 jQuery 라이브러리에서 CVE-2019-11358 취약점 패치 여부 확인 방법

현재 홈페이지에서 사용 중인 jQuery 라이브러리가 CVE-2019-11358 취약점 패치를 적용했는지 확인하는 방법은 다음과 같습니다. 1. jQuery 버전 확인 홈페이지 소스 코드를 확인하여 jQuery 라이브러리 버전을 직접 확인합니다. 웹 ...