sed는 Stream EDitor의 약자로 매우 컴팩트한 명령 체계를 이용하여 텍스트를 파싱하고 변형하는 (고대의) 텍스트 편집 도구이다. sed는 그 전신이 되는 ed의 스크립팅 체계를 기반으로 하고 있다. vim과 같이 편집될 텍스트를 화면상에 보면서 내용을 작성/수정하는 개념의 텍스트 편집기가 개발되기 이전의 텍스트 편집기이다.
요즘의 텍스트 편집기라 하면 (일례로 메모장을 떠올려보면), 텍스트 편집기를 사용해서 텍스트 파일의 일부분을 수정하는 과정은 1) 텍스트 편집기 프로그램을 실행하고 → 2) 편집할 텍스트 파일을 열고 → 3) 커서를 움직여 편집할 위치로 이동해서 → 4) 내용을 지우거나 삽입하는 식으로 편집하고 → 5) 그 파일을 다시 저장하는 식으로 작업하고, 이것은 현대의 대부분의 사용자에게 가장 익숙한 방법일 것이다.
하지만 sed가 사용되던 시절의 “텍스트 편집”은 다음과 같이 처리되었다. 물론 이 때는 GUI 따위는 커녕, 편집할 내용을 미리 화면에 표시하는 것조차 사치이던 시기이다. 따라서 다음과 같은 과정을 통해서 텍스트 파일을 수정해야 했다.
- 수정할 텍스트 파일의 내용은 편집자가 어떻게든 자세히 알고 있었다.
- 그래서 몇 번째줄부터 몇 번째줄까지를 삭제할 것이다… 혹은 어떤 내용이 들어있는 줄에서 어떤어떤 글자를 다른 글자로 교체할 것이다…와 같은 식으로 작업 내용을 스크립트 명령으로 정의한다.
- sed 명령에 해당 스크립트와 파일명을 던져주고 실행한다.
- 변경된 내용을 얻는다. (주로 표준출력으로 출력된다.)
예를 들어 5번째줄의 “hello”라는 단어를 삭제한다는 내용은 5s/hello//
라는 스크립트로 처리할 수 있다. 프로그램이 로드된 상태에서 파일을 열어 커서를 이동하는 대신에 이렇게 스크립트로 편집해야 할 동작을 전달해서, 편집기가 이를 적용한 결과를 출력하는 고전적인 에디터가 바로 sed이다.
개요
sed 명령은 기본적으로 입력받는 파일들과 문자열을 처리할 스크립트를 전달하여 실행한다. 그외의 몇 가지 동작 방식을 제어하는 옵션 스위치가 있다.
$ sed [옵션] 스크립트 입력파일1 [입력파일2 ... ]
아무런 옵션이 주어지지 않을 때의 첫번째 인자는 편집작업에 사용되는 스크립트이며, 이후 입력파일들을 받는다. 입력 파일이 주어지지 않은 경우에는 표준입력으로부터 콘텐츠를 읽게 된다. 입력 파일이 2개 이상인 경우에는 마치 이어진 파일 하나처럼 취급된다.
옵션
sed 명령에 사용되는 옵션은 아래와 같다. 기본적으로 GNU sed를 기준으로 하고 있으며, FreeBSD 및 macOS에서는 일부 옵션이 지원되지 않는 등 살짝 다른 옵션 구성을 가지고 있다. 이 글에서는 GNU sed를 기준으로 설명한다.
옵션/스위치 | 별칭 | 기능 |
---|---|---|
-n | --quiet , --silent | 읽어들인 라인을 암시적으로 자동출력하는 것을 중단한다. |
-e | --expression=script | 실행될 명령에 스크립트를 추가한다 |
-f script_file | --file=script_file | 스크립트 파일의 내용을 가져와서 추가로 실행한다. |
--follow-symlinks | 제자리 처리시에 심볼릭 링크를 따르도록 한다. 하드링크는 깨진다. | |
-i[SUFFIX] | --in-place[=SUFFIX] | 파일을 제자리 처리한다. (즉 변경된 내용을 파일에 적용한다.) |
-c | --copy | 제자리 처리시에 사본을 이용한다. |
-l N | --line-length=N | 한줄의 길이를 정의한다. |
--posix | 모든 GNU확장을 제외한다. | |
-r | --regexp-extended | 확장된 정규식 패턴을 사용한다. |
-s | --separate | 파일을 하나의 긴 스트림이 아닌 분리된 데이터들로 처리한다. |
-u | --unbuffered | 입력으로부터 최소한의 내용만 읽고 더 자주 플러시한다. |
--help | 도움말을 표시한다. |
-e
, -f
옵션이 주어지면 첫 필수 인자(파일이름)는 처리해야하는 스크립트로 받아들이게 된다.
SED의 편집 명령 체계
어드레스
기본적으로 sed의 편집에 사용되는 스크립트명령은 모든 라인에 대해서 반복해서 적용된다. 하지만 특정 행 혹은 특정 행범위나 어떤 주어진 패턴에 매칭하는 식으로 편집 명령이 적용되는 영역을 제한할 수 있는데, 이를 위해 “어드레스”라는 것을 사용한다. 어떤 명령들은 반복되어 수행될 필요가 없기 때문에, 어드레스를 아예 받지 않는 것도 있다. 반면 하나의 어드레스가 주어지는 경우에는 해당 어드레스에 매치되는 입력줄에 명령이 적용된다. 혹은 두 개의 어드레스가 주어지는 경우가 있는데, 이 때에는 첫번째 어드레스로부터 두번째어드레스에 이르는 포괄적인 구간에 대해서 적용된다. 이 문법은 addr1,addr2
로 공백 없이 두 어드레스는 콤마에 의해서 구분된다. 첫번째 어드레스는 항상 적용된다고 볼 수 있다. 또 두 번째 어드레스가 정규식인 경우에는 첫번째 어드레스에 대해서는 테스트되지 않는다. (첫번째 어드레스로 지정한 행은 반드시 적용)
어드레스 (하나 혹은 둘로 이루어진 범위)의 바로 다음과 명령문자 사이에 !
이 삽입된 경우에는 어드레스에 매치되지 않는 범위에 대해 명령이 적용된다
다음은 지원가능한 어드레스의 타입이다.
어드레스 형식 | 해설 |
---|---|
number | 숫자는 특정한 행 번호에 적용된다. |
first~step | 첫번째 어드레스가 가리키는 행부터 step 라인마다 한 번씩 |
$ | 마지막 행 |
/regexp/ | 정규식 패턴에 매치되는 값이 있는 라인 |
\cregexpc | c 는 임의의 문자일 수 있는데, 역시 정규식을 가리킨다. |
0,addr2 | 1,addr2 에서 addr2 가 정규식이면서 1번행에 매치될 때 정상적으로 처리되지 않는 문제를 해결하기 위해 사용한다. |
addr1,+N | addr1 외에 N 행 만큼 더 선택한다. |
addr1,~N | addr1 외에 처음으로 만나는 N 의 배수행까지 선택한다. |
명령
sed내의 명령은 기본적으로 각 라인에 대해 적용된다. 여기에는 삭제나 치환과 같은 일반적인 편집과 관련된 명령외에도 출력이나 파일에 저장 및 “패턴 공간”이라 불리는 편집 버퍼를 제어하는 명령등이 포함된다. 모든 명령은 매 라인에서 호출되는 것을 기본으로 하나, 특정한 줄번호 혹은 패턴에 매치되는 등의 조건을 만족하는 경우에만 적용하게 할 수 있다. 이렇게 명령의 범위를 한정하기 위해 “어드레스”를 사용한다. 먼저 SED 내에서 사용할 수 있는 명령에는 어떤 것들이 있는지 살펴보자.
1개 혹은 0개의 어드레스를 받는 명령
명령 | 의미 |
---|---|
= | 행번호를 출력한다. |
a \ text | 3개행문자앞에 역슬래시를 붙여서 여러 줄을 포함할 수 있는 텍스트를 뒤에 붙인다. |
i \ text | 개행문자 앞에 역슬래시를 붙여서 여러 줄을 포함할 수 있는 텍스트를 해당영역에 삽입한다. |
q[exit_code] | 해당 영역에서 종료한다. |
Q[exit_code] | 더 이상의 입력을 받지 않고 즉시 종료한다. |
r filename | 주어진 파일로부터 읽은 내용을 덧붙인다. |
R filename | 주어진 파일로부터 한 행을 읽어서 덧붙인다. 실행될 때 마다 각 행을 읽게 된다. |
어드레스 범위를 받는 명령
명령 | 의미 |
---|---|
{ | 명령 블럭을 시작한다. (블럭은 } 로 끝난다.) |
b label | 주어진 라벨로 이동한다. 라벨명이 생략되면 스크립트의 끝으로 이동한다. (라벨은 스크립트 내에서 :label_name 의 형식으로 정의한다. |
t label | 마지막으로 읽어들인 라인에 대해서 s/// 치환 명령이 성공적으로 수행되었다면 해당 레이블로 간다. 레이블명이 생략되면 스크립트의 끝으로 간다. |
T label | 마지막으로 읽어들인 라인에 대해서 s/// 치환명령이 실패했다면 해당 레이블로 간다. |
c \ text | 해당 범위를 주어진 텍스트로 치환한다. |
d | 해당 패턴 공간을 제거한다 |
D | 패턴 공간으로부터 첫 개행까지를 제거한다. |
h H | 패턴의 내용을 홀드 영역으로 복사/추가한다. |
g G | 홀드 영역에 복사된 내용을 패턴 공간으로 복사/추가한다. |
x | 홀드 영역과 패턴 영역의 값을 교체한다. |
l | 현재 행을 “시각적으로 명료한 폼”으로 출력한다. |
l width | 현재행을 시각적으로 명료한 폼으로 출력하되, 주어진 길이에서 강제 개행한다. |
p | 현재 패턴 공간의 내용을 출력 |
P | 현재 패턴공간으로부터 개행까지 출력 |
s/rexexp/rep/ | 주어진 패턴을 대체 패턴으로 교체한다. & 은 매치된 전체를 의미하며, \1 ~ \9 까지의 서브 패턴을 적용할 수 있다. |
w filename | 현재 패턴 공간을 주어진 파일에 쓴다. |
W filename | 패턴 공간의 첫줄을 주어진 파일에 쓴다. |
y/source/dest/ | 소스에 정의된 글자들은 dest에 정의된 글자들로 바꾼다. |
n N | 입력파일로부터 한 줄을 더 읽어들여서 패턴 공간에 복사/이어붙이기한다. |
그외에 다음과 같은 명령이 있다.
#comment
:#
으로 시작하는 라인은 라인코멘트로 처리된다.{
…}
: 중괄호로 감싸진 영역은 명령 블럭이다. 중괄호 앞/뒤에 다른 글자가 와서는 안된다.
sed의 명령 처리 방식
sed는 스트림으로 전달되는 일련의 텍스트를 각각의 행으로 끊어서 처리한다. 읽어들인 한 줄 분량의 버퍼 콘텐츠는 패턴공간이라는 메모리상의 버퍼 영역으로 복사되고, 패턴 공간에 대해 실행 시 주어진 스크립트를 반복적으로 적용한다. 입력된 모든 명령이 해당 패턴 공간에 대해 순차적으로 적용된다.
사이클과 패턴공간
sed는 패턴스페이스라는 메모리 영역을 사용하여, 입력 장치로부터 한 줄의 문자열을 읽고 다음과 같은 라인 사이클을 돈다.
- 최초 패턴 공간은 비어 있다.
- 표준입력으로부터 한 줄의 텍스트를 읽어 패턴 공간에 복사한다.
- 스크립트에 정의된 각 명령을 패턴 공간에 대해서 수행한다.
- 패턴 공간의 내용을 출력한다. (디폴트 동작)
- 패턴 공간의 내용을 지운다.
- 파일의 남은 데이터가 있으면 1로 돌아가서 다음 라인을 처리한다.
각 라인에 대해 라인 사이클을 반복해서 처리하며, 명령에 의해 내용이 조작되는 것은 파일이 아닌 패턴 공간이다. 따라서 원본의 내용은 변경되지 않으며1, 처리된 내용은 표준 출력으로 한 줄씩 내보내진다.2
예제
예를 들어 regex
라는 단어를 모두 REGEXP
라고 바꾼다고 하자. 텍스트 치환은 s///
를 사용하며, vim 을 사용해본 사람이라면 명령의 사용체계가 완전히 똑같다는 것을 알 수 있다.
$ sed -e 's/regex/REGEXP/g' infile.txt
이 명령은 파일 내의 단어를 치환하고 그 결과를 표준 출력으로 뿌려준다. 만약 결과를 별도의 파일로 저장하고 싶다면,
$ sed -e 's/regex/REGEXP/g' infile.txt > outfile.txt
위와 같이 실행하여 sed의 출력 내용을 다른 파일로 저장하는 것은 가능하다.
원본 파일 변경
-i
옵션은 원본 파일의 내용을 in-place로 변경하는 옵션이다. 원본의 내용을 바로 변경하여 수정해버리므로, 주의가 필요하다.
$ sed -e 's/regex/REGEXP/g' -i infile.txt
라인의 출력
각 라인의 사이클은 최종적으로 패턴 공간으로 읽어들여지고 조작된 내용을 화면에 출력한 후 패턴 공간을 비우는 것으로 끝난다.
$ sed -e '' infile.txt
위 예시는 주어진 파일에 대해서 어떤한 변형도 하지 않는다. 따라서 라인 처리 사이클에 따라 각 라인을 읽고 아무런 변형을 가하지 않은 상태에서 이를 출력한다. 따라서 cat infile.txt
와 동일한 명령을 수행한다.
디폴트 출력 숨기기 옵션
-n
옵션은 라인 사이클의 마지막에서 자동으로 라인을 출력해주는 처리를 하지 않는다. 따라서 다음 명령의 결과로는 아무것도 출력되지 않는다. 각 라인의 처리에서 디폴트로 해당 내용을 출력하는 작업을 생략하기 때문이다.
$ sed -n -e '' infile.txt
대신에 스크립트 명령중에 p
는 현재 패턴 공간의 내용을 출력하게 한다. 다음은 -n
옵션을 줘서 자동 출력을 방지하면서, 매 라인에 대해 p
명령으로 해당 라인을 출력하게 한다.
$ sed -n -e 'p' infile.txt
명령의 적용 범위
기본적으로 명령이 주어지면 sed는 라인별로 스크립트를 적용하는 사이클을 반복하므로 모든 라인에 명령이 적용된다. 예를 들어 =
명령은 해당 행의 행번호를 출력하는 명령이다.
$ sed -n -e '=' infile.txt
다만, 위 명령은 연속된 줄 번호만 출력할 뿐, 파일의 내용은 출력해주지 않는다. (-n
스위치에 의해 자동 출력이 막힌다.)
sed는 매 사이클의 스크립트에 대해서 각 스크립트가 현재 패턴 공간에 적용될 것인지, 그렇지 않은지를 판단하게 된다. 현재 패턴공간이 명령이 적용될 것인지는 명령 앞에 “어드레스“라고 하는 명령의 적용 범위를 통해서 판단한다. (어드레스가 명시적으로 주어지지 않는 명령은 앞에서도 서술했듯이 전체 라인에 적용된다.)
어드레스를 적용하는 방법은 크게 두 가지이다.
- 숫자 : 숫자는 해당 행의 행 번호를 의미한다.
- /정규식/ :
/regex/
의 형태로 정규식 패턴을 사용하면 해당 패턴에 매치되는 값이 있는 라인만 적용한다.
이렇게 단일 어드레스를 적용하는 방법이 있는가 하면, 두 개의 어드레스를 붙여서 사용할 수 있다. 어드레스 두 개를 사용하는 경우에는 콤마를 통해서 이어붙인다. 각각의 어드레스는 행번호이거나 정규식일 수 있다. 보다 자세한 내용은 앞서 정리한 어드레스 표를 참고하자.
범위로 주어지는 어드레스는 시작행과 끝행을 포함하는 것에 유의하자. 참고로 끝행은 $
으로 표현할 수 있다.
예제
특정한 파일의 1, 2번 행과 끝 행을 삭제하려 한다면 다음과 같이 처리한다. 두 개의 스크립트 명령은 ;
을 통해서 구분할 수 있다.
$ sed -e '1,2d;$d' infile.txt
참고로 d
명령을 써서 행을 삭제하는 경우에는 패턴 공간의 내용이 비어버리므로 라인 사이클의 끝에서 내용을 출력해주지 않는다. (자동출력과 p
명령 동일하게)
참고로 2개 이상의 명령을 각각 전달하는 방법도 있다. -e
옵션은 명령 스크립트가 뒤따르게 되어 있는데, 한번의 sed 실행 구문에서 -e
옵션 스위치는 두 번 이상 사용될 수 있다. (이들은 모두 ;
으로 연결한 하나의 덩어리로 인식된다.
그외 예제들
명령블럭
한번에 두 가지 이상의 명령을 처리하기 위해서는 ;
를 이용해서 명령을 결합할 수 있다고 했는데, 이는 -e
옵션을 여러 개 쓰는 것과 동일하다.
$ sed -e '1,2d' -e '$d' infile.txt
;
로 구분되는 두 개의 명령은 사이클마다 따로 적용된다. 다음 명령을 보자.
$ sed -ne '3,5=;p' infile.txt
여기에는 두 개의 명령이 적용되어 있다.
3,5=
: 3~5번 라인에서 현재 행번호를 출력한다.p
: 모든 라인에서 현재 내용을 출력한다.
따라서 모든 라인이 출력되는 대신에 3,4,5번 라인에서는 행번호가 한줄씩 추가로 출력된다. 만약, 3~5번 라인 사이만 행번호와 해당행을 출력하고 싶다면? 이 때는 두 =, p 두 명령을 하나의 블럭으로 묶으면 된다.
$ sed -ne '3,5{=;p}' infile.txt
1~10번 줄을 출력하는 방법들
특정 범위의 내용을 출력하는 것은 지정된 범위만 출력하거나, 지정된 범위만 출력하지 않는 것으로 구현할 수 있다. 아래 네 가지 명령은 모두 파일의 첫 10줄을 출력하는 것이다.
$ sed -n -e '1,10p' infile.txt
$ sed -n -e '11,$!p' infile.txt
$ sed -e '1,10!d' infile.txt
$ sed -e '11,$!p' infile.txt
그룹 명령 심화 – 계층 그룹 명령
다음 명령은 특정한 두 단어 사이의 내용에 대해서 다시 특정 조건을 만족하는 행만 번호와 함께 출력해준다.
$ sed -n -e '/def/,/ return/{ /r[aeiou]/{=;p} }' infile.py 1~~~~~~~~~~~~~~ 2 ~~~~~~~~~~ 3~~~
위 명령은 다음을 수행한다.
def
,return
이라는 단어 사이에 대해서r모음
의 패턴을 포함하는 라인의- 행번호와 그 내용을 출력
패턴 공간 액세스
sed의 동작은 라인 단위로 콘텐츠를 읽어서 주어진 명령 스크립트를 적용하는 식으로 돌아간다고 했다. 이 때 패턴 공간 자체를 미리 플러시해버리거나, 추가로 다음 라인을 강제로 읽는 명령도 존재한다.
n
은 현재 읽어들인 패턴을 버리고, 다음 패턴을 읽는다. (-n
옵션이 없다면 해당 내용일 출력된다.) N
은 현재 패턴 공간 뒤에 다음 줄을 읽어서 추가한다. 따라서 다음 명령은 각 라인의 줄번호를 앞에 추가해서 표시한다. 파이프 앞 에서 명령은 줄번호와 해당줄이 한줄씩 표시되므로, 파이프 뒤의 sed에서는 번호에 해당하는 줄을 읽고, N
으로 다음 줄을 읽은 다음, 개행 문자를 공백으로 치환하여 출력해주기 때문이다.
$ sed -n '=' infile.txt | sed -e '{N;s/\n/ / }'
혹은 다음과 같이 빈 줄을 찾아서 해당 행을 줄번호로 대체하는 것도 가능하다. 빈줄에 매치하는 패턴 /^$/
을 어드레스로 주고, 매치되는 라인에서는 줄번호를 출력(=
) 후, 패턴 공간을 삭제(d
)하는 일을 하나의 그룹으로 묶어서 실행한다. 따라서 빈줄은 빈줄 대신 행번호가 출력될 것이다.
$ sed -e '/^$/{=;d}' infile.txt
홀드 영역
현재 패턴 공간의 내용을 어딘가 저장해두었다가 나중에 따로 꺼내 쓰는 식으로 사용하고 싶을 지도 모른다. 일종의 복사/붙여넣기와 비슷하다고 할 수 있는데, 실제로 시스템의 클립보드 서비스를 사용하는 것이 아니라, 메모리의 임시 버퍼를 사용해서 패턴 공간 자체를 복사해두는 것이다. 이 방식은 복사/붙여넣기보다는 테트리스 게임의 hold 기능과 더 유사하다고 볼 수 있다. 현재 조작 중인 패턴 공간을 홀드 공간으로 넘겨두고, 나중에 필요할 때 패턴공간의 내용과 홀드 영역의 내용을 서로 맞바꿀 수 있다.
다음은 세줄을 한 덩어리로 출력하면서, 첫줄의 행번호를 함께 출력해주는 내용이다.
$ sed -n -e '=;{h;n;H;n:H;g;p}' infile.txt
=
: 현재 행번호를 출력한다.{
: 명령 그룹이 시작h
: 현재 패턴 공간을 홀드한다.n
: 다음 라인을 읽는다.-n
옵션이 있기 때문에 해당 줄을 출력하지는 않는다.H
: 읽어온 두 번째 줄을 홀드 영역의 뒤에 붙인다. 이제 홀드 영역에는 2줄짜리 텍스트가 들어있다.n;H
: 4, 5를 한 번 더 실행하여 홀드 영역에는 3줄짜리 텍스트가 들어있게 되었다.g
: 홀드 영역의 내용을 다시 패턴 공간으로 가져온다.p
: 패턴공간의 내용을 출력하고 플러시한다.
n
, N
명령이 실행될 때마다 현재 행번호 자체는 계속 증가하기 때문에 이 결과는 1을 출력하고 세줄, 4를 출력하고 세줄… 이런 식으로 3줄 단위로 행번호와 콘텐츠를 출력하게 된다.
흐름제어
sed의 스크립트를 명령줄에서 입력할 수도 있지만, 별도의 파일에 스크립트를 작성하여 실행할 수도 있다. 물론 쉘에서 역슬래시를 이용해서 여러 줄의 명령을 직접 타이핑하는 것도 가능하겠다.
sed의 흐름제어는 스크립트 상에서 특정한 레이블의 위치로 이동하는 수준의 빈약해보이는 장치를 쓰는데, 홀드 영역과 패턴 공간을 활용하는 명령들과 조합하여 제법 쓸모있는 기능을 만들 수 있다.
다음 스크립트는 특정 파일에서 빈 줄로 구분된 문단단위에서 main
이라는 단어를 포함하는 문단만 출력한다. 이를 위해서는 앞에서 언급된 기능들을 모두 사용한다.
/^$/ b print_para # 빈줄을 발견하는 경우 print_para로 건너뛴다.
H # 빈줄이 아닌 경우 이 라인으로 넘어와서 실행하는데, 현재 라인을 홀드영역에 추가한다.
$ b print_para # 끝줄인 경우에 print_para로 건너뛴다.
b # 스크립트의 끝으로 건너뛴다. 따라서 빈줄 및 끝줄이 아닌 경우에는 print_para 가 실행되지 않는다.
:print_para # print_para 영역. 홀드 영역과 현재 패턴 공간을 바꿔치기 하고, "main"이라는 패턴이 있으면 출력한다.
/main/ p
이렇게 작성된 스크립트를 para.sed
로 저장하고 다음과 같이 실행해보자.
$ sed -nf para.sed infile.py
정리
sed는 흔히 s///
를 써서 특정한 파일이나 명령의 출력에서 특정 문자열을 치환하는 툴로 인식되는데, 실질적으로는 엄연한 텍스트 편집기이다. vim을 필두로 편집버퍼의 내용을 화면에 표시하면서 커서를 이동하여 특정 위치에서 편집하는 방식의 시각적 편집기가 도입되면서 이러한 스트림 편집기를 일상 용도로 쓰는 일은 극히 드물어졌다. 하지만 여전히 sed는 간단한 편집을 명령줄 상에서 수행할 수 있으며, 그 대상이 파일의 내용 뿐만 아니라 표준입력을 통해서 전달 받는 텍스트 스트림이 될 수 있기 때문에 쉘 스크립트 상에서 텍스트 콘텐츠를 변형하는데 요긴하게 쓰일 수 있다. awk와 더불어 명령줄에서 특정한 콘텐츠를 편집하는데 지금도 자주 쓰이며, 이 조합을 통해서 쉘 스크립트에서 왠만한 스크립팅 언어 수준의 텍스트 처리를 수행할 수 있게 된다.
정말 감사합니다 이렇게 깔끔하게 정리하는것도 정말 쉽지않는데 많이 알아갑니다^^
댓글이 닫혔습니다.