0%

쉘로 시작하는 시스템 프로그래밍 기본

쉘 스크립트

쉘을 사용한 프로그래밍 작업으로서 서버 작업 자동화 및 운영(DevOps)를 위해서 기본적으로 익혀둘 필요가 있다.

로그가 많이 쌓여서 서버가 죽을 경우 스크립트를 생성해서 로그를 주기적으로 삭제 하도록 하는 등의 작업을 들 수 있다.

기본 문법

#!/bin/bash로 시작해야 하고 실행 권한을 가지고 있어야 한다. 파일 이름은 일반적으로 파일이름.sh와 같은 형태로작성한다.

실습

echo 함수를 이용하여 화면에 Hello bash!"를 출력 할 수 있도록 스크립트 작성

1
2
3
#!/bin/bash

echo "Hello bash"

실행 권한이 있어야 한다. chmod +x test.sh

실행은 test.sh가 있는 폴더로 들어간 후 ./test.sh 입력.

주석

#으로 시작하면 주석이다.

변수

선언

  • 변수명=데이터로 사용한다. 띄어씌기는 허용되지 않는다.

사용

  • $변수명 으로 사용된다.
1
2
3
4
5
6
7
#!/bin/bash

mysql_id='root'
mysql_directory='/etc/mysql'

echo $mysql_id
echo $mysql_directory

실습

아이디 관련 정보 변수만들기 (실제 이름, 나이, 직업)

1
2
3
4
5
6
7
#!/bin/bash

name='koo'
age=27
job='no job'

echo $name $age $job

리스트

선언

  • 변수명=(데이터1 데이터2 데이터3 …)

사용

  • ${변수명[인덱스번호]} 로 사용한다.
1
2
3
4
5
6
7
8
9
10
#!/bin/bash

daemons=("httpd" "mysqld" "vsftpd")
echo ${daemons[1]}
echo ${daemons[@]} # $daemons 배열의 모든 데이터 출력
echo ${daemons[*]} # $daemons 배열의 모든 데이터 출력
echo ${#daemons[@]} # daemons 배열 크기 출력

filelist=( $(ls) ) # 해당 쉘스크립트 실행 디렉토리의 파일 리스트를 배열로 $filelist 변수에 받음.
echo ${filelist[*]}

실습

아이디 관련 정보 리스트 변수로 만들고, 각 정보 출력하기

사전에 정의된 지역 변수

1
2
3
4
5
6
7
8
9
$$ : 쉘의 프로세스 번호
$0 : 쉘스크립트 이름
$1 ~ $9 : 명령줄 인수
$* : 모든 명령줄 인수리스트
$# : 인수의 개수
$? : 최근 실행한 명령어의 종료 값
- 0 (성공), 1~125 (에러)
- 126(파일이 실행가능하지 않음),
- 128~255 (시그널 발생)

실습

쉘 프로세스 번호, 쉘 스크립트 이름, 명령줄 인수, 모든 명령줄 인수리스트, 인수 개수 출력해보기

1
2
3
#!/bin/bash

echo $$ $0 $1 $* $#

연산자

expr: 숫자 계산

expr를 사용하는 경우 백틱을 사용해야 함. 연산자 *와 괄호 앞에는 역슬래시를 넣어야 한. 연산자와 숫자, 변수 사이에는 space를 넣어야 함.

1
2
3
4
#!/bin/bash

num=`expr \( 10 + 20 \) / 8 - 8`
echo $num

조건문 문법

기본 if 구문

  • 명령문을 꼭 탭으로 띄워야 하는 것은 아님(then과 if 안에만 들어가 있으면 됨.)

두 인자값을 받아서 두 인자값이 다르면 differnt values 출력

1
2
3
4
5
6
#!/bin/bash

if [ $1 != $2 ]
then
echo 'different values'
fi

조건 문에 띄어쓰기를 주의한다.

조건 작성이 다른 프로그래밍 언어와 달리 가동성이 현저히 떨어짐, 필요할때마다 찾아보길..

파일 검사

1
2
3
4
5
6
7
8
9
-e 파일명 # 파일이 존재하면 참
-d 파일명 # 파일이 디렉토리면 참
-h 파일명 # 심볼릭 링크파일
-f 파일명 # 파일이 일반파일이면 참
-r 파일명 # 파일 읽기 가능이면 참
-s 파일명 # 0 파일 크기가 0이 아니면 참
-u 파일명 # 파일이 set-user-id가 설정되면 참
-w 파일명 # 파일 쓰기 가능 상태이면 참
-x 파일명 # 파일이 실행 가능 상태이면 참

해당 파일이 있는지 없는지 출력하는 쉘 스크립트 작성해 보기

1
2
3
4
if [ -e $1 ]
then
echo "file exist"
fi

if else

1
2
3
4
5
6
7
8
#!/bin/bash

if [ $1 == $2 ]
then
echo 'same values'
else
echo 'different values'
fi

쉘 크립트 해석하기

1
2
3
4
5
6
7
8
9
#!/bin/bash

ping -c 1 192.168.0.1 1> /dev/null
if [ $? == 0 ]
then
echo "게이트웨이 핑 성공!"
else
echo "게이트웨이 핑 실패!"
fi

ping -c 1 192.168.0.1 1> /dev/null 응답 확인 요청 -c 1를 통해 1번만 요청, 1> /dev/null를 통해 응답 결과 표준 출력을 버림,

반복문

1
2
3
4
5
6
#!/bin/bash

for database in $(ls)
do
echo $database
done

for 문을 while로 바꿔서 사용도 가능. (일반적인 프로그래밍 언어처럼 사용 가능.

시그널 동작 메커니즘

본래는 다른 용도 이지만 IPC 로도 사용 가능한 기법

  • 시그널(signal) - 프로세스에 이벤트 전달.
  • 소켓(socket) - 네트워크

시그널

유닉스에서 오래전부터 사용되온 기술이다. 커널 또는 프로세스에서 다른 프로세스에서 어떤 이벤트가 발생되었는지를 알려주는 기법이다.

시그널 동작

프로그램에서 특정 시그널의 기본 동작 대신 다른 동작을 하도록 구현 가능하다.

각 프로세스에서 시그널 처리에 대해 다음과 같은 동작 설정이 가능하다.

  • 시그널 무시
  • 시그널 블록
  • 프로그램 안에 등록된 시그널 핸들러로 재정의한 특정 동작 수행
  • 등록된 시그널 핸들러가 없다면, 커널에서 기본 동작 수행

시그널과 프로세스

프로세스는 PCB 에서 해당 프로세스가 블록 또는 처리해야하는 시그널 관련 정보를 관리한다. 커널 모드에서 사용자 모드 전환시 시그널 정보를 확인해서 해당 처리를 한다.

리눅스 프로세스

프로세스 vs 바이너리

바이너리 - 코드 이미지 (실행 파일)
실행 중인 프로그램을 프로세스라고 한다.

리눅스는 다양한 프로세스 실행 환경

유닉스 철학 : 여러 프로그램이 유기적으로 각자의 일을 수행하면서 전체 시스템이 동작하도록 하는 모델

foreground process / background process

foreground process: 쉘 에서 해당 프로세스가 실행되고 있으면 수행 종료 전까지 다른 입력을 하지 못하는 프로세스

background process: 사용자 입력과 상관 없이 실행되는 프로세스.
쉘(shell) 에서 해당 프로세스 실행시, 맨 뒤에 &를 붙여줌.

1
2
find / -name '*.py' > list.txt &
[1] 57

[1]은 작업 번호 57은 pid를 나타냄.

1
2
3
kill -9 [pid번호]

jobs

kill 명령어를 통해 프로세스를 종료시키고, jobs 명령어를 통해 background process로 실행중인 프로세스를 확인한다.

foreground process 제어하기

컨트롤 + z : foreground 프로세스를 실행 중지 상태로 변경

맨 마지막 컨트롤 + z 로 중지된 프로세스는 bg 명령으로 background로 실행될 수 있다.

jobs 명령어를 통해 진행 또는 중지된 프로세스를 보여준다.

컨트롤 + c : 프로세스를 완전히 종료 시킴

프로세스 상태 확인

ps 프로세스 상태 확인

데몬 프로세스(daemon process) : 사용자 모르게 시스템 관리를 위해 실행되는 프로세스로 보통 시스템이 부팅될 때 자동 실행.

보통 ps aux를 많이 사용한다.

USER: 소유자 정보

PID : process id

%CPU : CPU 사용하고 있는 퍼센트

%MEM : memory 사용하고 있는 퍼센트

VSZ : 가상메모리에서 실제로 쓰고 있는 메모리

RSS : 페이징 시스템을 사용해서 실제 물리 메모리에서 사용하고 있는 사이즈

TTY : 어떤 터미널과 연결되 있는가? 하드웨어 리소스

STAT : 상태

프로세스 죽이기

kill -9 <pid> -9 강제로 죽이기

9.5 내장 클래스 확장하기

배열, 맵과 같은 내장 클래스도 확장이 가능하다.

PowerArray 라는 클래스를 내장 클래스 Array 를 상속받아서 구현해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 메서드 하나를 추가합니다(더 많이 추가하는 것도 가능).
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

주목할 점은 filter, map 등의 내장 메서드가 상속받은 클래스인 PowerArray를 반환한다는 점이다.

arr.filter()가 호출될때 내부에선 기본 Array 가 아닌 arr.constructor를 기반으로 새로운 배열이 만들어지고 여기에 필터 후 배열이 담긴다.

이러한 동작 방법을 바꾸기 특수 정적 getter인 [Symbol.species]를 추가할 수 있다.

getter에서 명시한 클래스로 내장 메서드에 반환값을 결정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}

// 내장 메서드는 반환 값에 명시된 클래스를 생성자로 사용합니다.
static get [Symbol.species]() {
return Array;
}
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter는 arr.constructor[Symbol.species]를 생성자로 사용해 새로운 배열을 만듭니다.
let filteredArr = arr.filter(item => item >= 10);

// filteredArr는 PowerArray가 아닌 Array의 인스턴스입니다.
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

내장 객체와 정적 메서드 상속

일반적으로 한 클래스가 다른 클래스를 상속 받으면 정적 메서드와 정적 프로퍼티를 상속 받는다.

하지만 내장 클래스는 정적 메서드를 상속받지 못한다.

예를 들어 Date의 경우 Object를 상속 받기 때문에 Object.prototype에 구현된 메서드를 사용할 수 있다.

하지만 Object 자체를 차조하는 어떠한 변수도 없기 때문에 Object에 선언된 정적 메서드는 사용할 수 없는 것이다.

DateObject와의 관계는 다음과 같다.

instanceof 로 클래스 확인하기

instanceof 를 사용하면 특정 클래스에 속하는지 확인할 수 있다.

이를 통해 특적 인스턴스의 상속 관계까지도 확인할 수 있다.

다양한 곳에서 사용할 수 있는데 이번 챕터에서는 다형적인 함수를 만드는데 사용해 보도록 한다.


instanceof 연산자

기본 사용법

1
obj instanceof Class

obj 가 Class에 속하거나 Class를 상속받는 클래스에 속하면 true를 반환한다.

instanceof는 보통 프로토타입 체인을 거슬러 올라가며 인스턴스 여부나 상속 여부를 확인한다.

그런데 정적 메서드 Symbol.hasInstance를 사용하면 직접 확인 로직을 설정할 수 도 있다.

동작 방식은 다음과 같다.

  1. Class 안에 Symbol.hasInstance가 구현되어 있으면, obj instanceof Class문이 실행될때 Class[Symbol.hasInstance](obj) 가 실행됨. 호출결과는 true 이거나 false 이어야 한다.
1
2
3
4
5
6
7
8
9
10
11
// canEat 프로퍼티가 있으면 animal이라고 판단할 수 있도록
// instanceOf의 로직을 직접 설정합니다.
class Animal {
static [Symbol.hasInstance](obj) {
if (obj.canEat) return true;
}
}

let obj = { canEat: true };

alert(obj instanceof Animal); // true, Animal[Symbol.hasInstance](obj)가 호출됨
  1. 대부분의 클래스엔 Symbol.hasInstance가 구현되어 있지 않음. 따라서 프로토타입 체인을 거치면서 하나와 일치하면 true를 반환함.

보너스: 타입 확인을 위한 Object.prototype.toString

toString 을 사용하면 typeOf 나 instancof의 좋은 대안이 될 수 있다.

toString 알고리즘은 내부적으로 this를 검사하고 this에 상응하는 결과를 반환한다.

1
2
3
4
5
let s = Object.prototype.toString;

alert( s.call(123) ); // [object Number]
alert( s.call(null) ); // [object Null]
alert( s.call(alert) ); // [object Function]

Symbol.toStringTag

특수 객체 프로퍼티 Symbol.toStringTag를 사용하면 toString의 동작ㅇ을 커스터마이징 가능하다.

1
2
3
4
5
let user = {
[Symbol.toStringTag]: "User"
};

alert( {}.toString.call(user) ); // [object User]

객채의 확인 뿐만 아니라 문자열로 받고 싶다면 {}.toString.call를 활용하면 됨.

과제1


믹스인

자바스크립트에서는 단일상속만을 허용하는 언어이다.

다중 상속이 필요한 경우는 어떻게 해야 할까?

믹스인이라는 기능을 통해서 다른 클래스의 상속 없이 이들 클래스에 구현되어 있는 메서드를 담게 할 수 있다.

믹슥인은 특정 행동을 실행해주는 메서드를 제공하는데 단독으로 쓰이지 않고 다른 클래스에 행동을 더해주는 용도로 사용된다.


믹스인 예시

유용한 메서드가 여러 개가 담긴 객체를 하난 만든다.

이렇게 하면 다수의 메서드를 원하는 클래스의 프로토타입에 쉽게 병합할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 믹스인
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};

// 사용법:
class User {
constructor(name) {
this.name = name;
}
}

// 메서드 복사
Object.assign(User.prototype, sayHiMixin);

// 이제 User가 인사를 할 수 있습니다.
new User("Dude").sayHi(); // Hello Dude!

믹스인을 사용했다면 만약 User가 다른 클래스를 상속 받고 있더라도 믹스인에 구현된 메서드를 사용할 수 있다.

1
2
3
4
5
class User extends Person {
// ...
}

Object.assign(User.prototype, sayHiMixin);

믹스인인에서 클래스를 상속 받는것도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let sayMixin = {
say(phrase) {
alert(phrase);
}
};

let sayHiMixin = {
__proto__: sayMixin, // (Object.create를 사용해 프로토타입을 설정할 수도 있습니다.)

sayHi() {
// 부모 메서드 호출
super.say(`Hello ${this.name}`); // (*)
},
sayBye() {
super.say(`Bye ${this.name}`); // (*)
}
};

class User {
constructor(name) {
this.name = name;
}
}

// 메서드 복사
Object.assign(User.prototype, sayHiMixin);

// 이제 User가 인사를 할 수 있습니다.
new User("Dude").sayHi(); // Hello Dude!

참조 관계


이벤트 믹스인

클래스나 객체의 이벤트 관련함수를 쉽게 추가할 수 있는 믹스인을 만들어 보도록 한다.

  • 믹스인은 중요한 일이 발생했을 때 이벤트를 생성하는 메서드인 .trigger(name, [...data])를 제공함.
  • 메서드 .on(name, handler)를 통해서 trigger 됬을 때 이벤트를 등록할 수 있음.
  • 메서드 .off(name, handler)를 통해서 handler 리스너를 제거한다.

믹스인을 축하면 사용자가 로그인할 때 객체 user 가 “login” 이라는 이벤트를 생성 한다든지의 기능을 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let eventMixin = {
/**
* 이벤트 구독
* 사용패턴: menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},

/**
* 구독 취소
* 사용패턴: menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},

/**
* 주어진 이름과 데이터를 기반으로 이벤트 생성
* 사용패턴: this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // no handlers for that event name
}

// 핸들러 호출
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};

사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 클래스 생성
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// 이벤트 관련 메서드가 구현된 믹스인 추가
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// 메뉴 항목을 선택할 때 호출될 핸들러 추가
menu.on("select", value => alert(`선택된 값: ${value}`));

// 이벤트가 트리거 되면 핸들러가 실행되어 얼럿창이 뜸
// 얼럿창 메시지: Value selected: 123
menu.choose("123");

try .. catch 와 에러 핸들링

에러가 발생하면 스크립트가 죽는다.

try catch 를 사용하면 에러를 잡아서 합당한 무언가를 할 수 있게 된다.

try..catch 문법

1
2
3
4
5
6
7
8
9
try {

// 코드...

} catch (err) {

// 에러 핸들링

}
  1. 먼저 try 실행
  2. 에러가 없다면 try 마지막 줄까지 실행되고 catch 블럭은 건너 뜀
  3. 에러가 있다면 catch 블럭이 실행되고, error 의 정보를 담은 err(이름 바뀔 수 있음.) 객체를 포함한 코드 블럭을 실행한다.
  • try .. catch 는 런타임 에러에서만 동작함.
  • try .. catch 는 동기적으로 동작함.

에러 객체

에러가 발생하면 에러 상세 내용이 담긴 에러 객체를 생성하고 catch 블럭에 인수로 넘겨준다.

주요 프로퍼티

  • name : 에러 이름
  • message : 에러 상세 내용을 담고 있는 문자 메세지
  • stack : 현제 호출 스택, 에러가 어디서 발생했는지에 대한 정보

선택적 catch 바인딩

자세한 정보가 필요 없으면 catch 블럭에서 인수로 error 객체를 생략할 수 있다.


try .. catch 사용하기

JSON.parse를 통해 예시를 들어봄.

잘못된 json 형식이 들어오면 JSON.parse 호출시 스크립트가 죽음.

1
2
3
4
5
6
7
8
9
10
11
12
13
let json = "{ bad json }";

try {

let user = JSON.parse(json); // <-- 여기서 에러가 발생하므로
alert( user.name ); // 이 코드는 동작하지 않습니다.

} catch (e) {
// 에러가 발생하면 제어 흐름이 catch 문으로 넘어옵니다.
alert( "데이터에 에러가 있어 재요청을 시도합니다." );
alert( e.name );
alert( e.message );
}

직접 에러 만들어서 던지기

문법적으로 잘못되진 않았지만 예상한 동작은 아니여서 error를 던지고 싶을땐 어떻게 해야 할까?

1
2
3
4
5
6
7
8
9
10
let json = '{ "age": 30 }'; // 불완전한 데이터

try {

let user = JSON.parse(json); // <-- 에러 없음
alert( user.name ); // 이름이 없습니다!

} catch (e) {
alert( "실행되지 않습니다." );
}

name 을 찾으면 undefined 가 아니라 error 를 던졌으면 좋겠다고 생각할 수 있다.

throw 연산자

에러를 생성함. 기본 문법

1
throw <error object>

어떤 객체도 사용할 수 있지만 객체 안에 name 과 message 프로퍼티를 추가하기를 권장함.

자바스크립트에 표준 에러 관련 생성자가 존재함.

1
2
3
4
5
let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

name 프로퍼티가 없을때 에러 생성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let json = '{ "age": 30 }'; // 불완전한 데이터

try {

let user = JSON.parse(json); // <-- 에러 없음

if (!user.name) {
throw new SyntaxError("불완전한 데이터: 이름 없음"); // (*)
}

alert( user.name );

} catch(e) {
alert( "JSON Error: " + e.message ); // JSON Error: 불완전한 데이터: 이름 없음
}

에러 다시 던지기

위에 같은 구조는 error를 종류에 맞게 처리하기 보다는 한꺼번에 처리하는 구조임.

이런 문제를 피하기 위해서는 다시 던지기 기술을 사용해야 함(rethrowing)

catch는 알고 있는 에러만 처리하고 나머지는 다시 던저야 함.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let json = '{ "age": 30 }'; // 불완전한 데이터
try {

let user = JSON.parse(json);

if (!user.name) {
throw new SyntaxError("불완전한 데이터: 이름 없음");
}

blabla(); // 예상치 못한 에러

alert( user.name );

} catch(e) {

if (e instanceof SyntaxError) {
alert( "JSON Error: " + e.message );
} else {
throw e; // 에러 다시 던지기 (*)
}

}

던져진 에러에 대한 catch 도 해줘야 함.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function readData() {
let json = '{ "age": 30 }';

try {
// ...
blabla(); // 에러!
} catch (e) {
// ...
if (!(e instanceof SyntaxError)) {
throw e; // 알 수 없는 에러 다시 던지기
}
}
}

try {
readData();
} catch (e) {
alert( "External catch got: " + e ); // 에러를 잡음
}

try…catch…finally

finally 는 에러가 있든 없든 마지막에는 꼭 실행되는 코드 블럭이다.

1
2
3
4
5
6
7
8
try {
alert( 'try 블록 시작' );
if (confirm('에러를 만드시겠습니까?')) 이상한_코드();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}

에러를 만들겠다고 하면 catch 절이 실행 된다음 finally 블럭이 실행됨.

에러를 안만들겠다고 하면 try 절만 실행되고 finally 블럭이 실행됨.

  • try.. catch.. finally 안의 변수는 지역 변수이다.
  • finally와 return ( try catch 절에서 return 하면 에러 발생 여부 상관 없이 fianlly 가 실행됨. finally 는 try catch 코드 블럭을 벗언나면 무조건 실행되는 코드 블럭임.

전역 catch

코어 자바스크립트가 아님.

try catch 밖에서 발생한 에러는 어떻게 처리해야 할까?

호스트 마다 다르지만 이런 에러를 처리하는 것은 중요한 일이기 때문에 지원하고 있다.

Node.js 에서는 process.on("uncaughtException") 브라우저 환경에서는 window.onerror

문법

1
2
3
window.onerror = function(message, url, line, col, error) {
// ...
};
  • message : 에러 메세지
  • url : 에러가 발생한 스크립트의 url
  • line, col : 에러가 발생한 곳의 줄과 열 번호
  • error : 에러 객체

window.onerror를 사용해서 죽어버린 스크립트를 복구하기에는 문제가 있음.

개발자에게 어디서 에러가 났는지 알려주는 목적으로 많이 사용함.

이벤트 위임

비슷한 방식으로 여러 요소를 다루어야 할때 사용한다.

공통 조상 이벤트 핸들러를 단 하난만 할당해도 여러 요소를 한꺼번에 다룰 수 있게 된다.

공통 종상에 할당한 이벤트 핸들러에서 event.target을 이용하면 실제 어디서 이벤트가 발생했는지 알 수 있다.

예를 들어 tabel 요소에 각 td에 다 이벤트 핸들러를 등록하는 것이 아니라 table 안에다가 이벤트를 등록하고 event.target으로 어떤 요소에 이벤트 가 발생했는지 확인하는 방식이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let selectedTd;

table.onclick = function(event) {
let target = event.target; // 클릭이 어디서 발생했을까요?

if (target.tagName != 'TD') return; // TD에서 발생한 게 아니라면 아무 작업도 하지 않습니다,

highlight(target); // 강조 함
};

function highlight(td) {
if (selectedTd) { // 이미 강조되어있는 칸이 있다면 원상태로 바꿔줌
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // 새로운 td를 강조 함
}

만약 td 안에 strong이라는 요소가 있어서 strong 요소를 클릭했다면 event.target은 strong 이 될 것이다.

이런 부분을 반영하여 코드를 고친다.

1
2
3
4
5
6
7
8
9
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)

if (!td) return; // (2)

if (!table.contains(td)) return; // (3)

highlight(td); // (4)
};
  1. event.target.closet(selector)을 활용하여 elem의 상위 요소 중 selector와 일치하는 가장 근접한 조상 요소를 반환 함.
  2. 선택한 요소가 td 안에 없으면 null을 반환함.
  3. table 요소 밖에 td 요소를 선택했으면 return 함.
  4. 강조

이벤트 위임 활용하기

save, load, search 기능을 수행하는 버튼을 만든다고 했을때, 메뉴 전체에 핸들러를 하나 추가해주고, 각 버튼의 data-action 속성에 호출할 메서드를 할당해 주는 방법을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<div id="menu">
<button data-action="save">저장하기</button>
<button data-action="load">불러오기</button>
<button data-action="search">검색하기</button>
</div>

<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}

save() {
alert('저장하기');
}

load() {
alert('불러오기');
}

search() {
alert('검색하기');
}

onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}

new Menu(menu);
</script>

* onClick의 bind로 this를 주지 않으면 this 가 Menu 가 아닌 DOM 요소 elem을 참조하게 되서 this[action]을 사용할 수 없다.

장점?

  • 버튼마다 핸들러를 할당해주는 코드를 작성할 필요가 없어짐.
  • 언제든지 버튼을 추가하고 제거할 수 있어 html 구조가 유연해 짐.

행동 패턴

이벤트 위임은 요소에 선언적 방식으로 행동을 추가할 때 사용할 수 도 있다.

행동 패턴은 두 부분으로 구성됨.

  1. 요소의 행동을 설명하는 커스텀 속성을 요소에 추가
  2. 문서 전체를 감지하는 핸들러가 이벤트를 추적하게 함. 1에서 추가한 속성이 있는 요소에서 이벤트가 발생하면 작업을 수행함.

카운터 구현

버튼을 클릭하면 숫자가 증가하는 행동을 부여하는 속성인 data0counter를 살펴봄

1
2
첫 번째 카운터: <input type="button" value="1" data-counter>
두 번째 카운터: <input type="button" value="2" data-counter>
1
2
3
4
5
6
7
8
9
10

<script>
document.addEventListener('click', function(event) {

if (event.target.dataset.counter != undefined) { // 속성이 존재할 경우
event.target.value++;
}

});
</script>

문서 레벨의 핸들러를 만들 땐 항상 addEventListener를 사요할 것

토글러 구현하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<button data-toggle-id="subscribe-mail">
구독 폼 보여주기
</button>

<form id="subscribe-mail" hidden>
메일 주소: <input type="email">
</form>

<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;

let elem = document.getElementById(id);

elem.hidden = !elem.hidden;
});
</script>

브라우저 기본 동작

상당수 이벤트는 브라우저에서 기본적인 동작이 정해져 있음.


브라우저 기본 동작 막기

기본 동작 막기

  1. event.preventDefault()
  2. on<event> 라면 return false
1
2
3
<a href="/" onclick="return false">이곳</a>
이나
<a href="/" onclick="event.preventDefault()">이곳을</a> 클릭해주세요.

핸들러에서 false를 반환하는 것은 예외 사항.
보통의 경우 return 반환 값은 무시됨.

메뉴 구현하기

1
2
3
4
5
<ul id="menu" class="menu">
<li><a href="/html">HTML</a></li>
<li><a href="/javascript">JavaScript</a></li>
<li><a href="/css">CSS</a></li>
</ul>

button으로 만들지 않은 이유?

button 으로 만들면 마우스 오른쪽 클릭으로 새창 열기 안됨.

검색 엔진은 a 테그를 이용해서 인덱싱함.

기본 동작 막기

1
2
3
4
5
6
7
8
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;

let href = event.target.getAttribute('href');
alert( href ); // 서버에서 데이터를 읽어오거나, UI를 새로 만든다거나 하는 등의 작업이 여기에 들어갑니다.

return false; // 브라우저 동작을 취소합니다(URL로 넘어가지 않음).
};

후속 이벤트
이벤트는 보통 순차적으로 발생함. (예를 들어서 moundown 다음에 focus가 발생함. )
그래서 전에 이벤트를 막았다면 후에 일어날 이벤트도 막게 됨.


addEventListener의 ‘passive’ 옵션

passive: true 옵션은 브라우저에게 preventDefault()를 호출하지 않겠다고 알려주는 것임.

스크롤 할때 브라우저는 preventDefault를 호출했는지 확인하면서 호출했으면 스크롤링을 멈추는 방법을 사용한다.

이때 사용자 입장에서는 스크롤할때 화면이 끊켜서 보이게됨.

명시적으로 호출안할거라는 옵션을 줌으로서 이런 현상을 막을 수 있음

firefox나 crome 같은 브라우저는 기본값이 true 임.


event.defaultPrevented

기본 동작을 막을 경우 이 값이 true 이고, 그렇지 않은 경우 false 임.

버블링과 캡처링에서 event.stopPropagation()을 사용하는 것이 않좋다는것을 알았음.

이 속성을 사용해 똑같은 기능을 구현해 봄.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<p>문서 레벨 컨텍스트 메뉴(event.defaultPrevented를 확인함)</p>
<button id="elem">버튼 레벨 컨텍스트 메뉴</button>

<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("버튼 컨텍스트 메뉴");
};

document.oncontextmenu = function(event) {
if (event.defaultPrevented) return;

event.preventDefault();
alert("문서 컨텍스트 메뉴");
};
</script>

커스텀 에러와 에러 확장

개발하다면 자체 에러를 만드는 것이 직관적이기 때문에 필요할 때가 있다.

직접 에러 클래스를 만든다면 name, message 프로퍼티를 만들어야 하고 가능하다면 stack 프로퍼티도 지원해야 한다. 물론 추가 프로퍼티 사용은 무엇이든 가능하다.

throw 인수엔 무엇이든 사용 가능해서 꼭 Error 객체를 상속받아야 하는것은 아니지만, Error 객체를 상속받으면 obj instance Error를 사용하여 에러 여부를 확인하는것이 가능하기 때문에 그냥 만드는 것보다 Error 객체를 상속받아서 만드는것을 추천한다.


에러 확장하기

user 정보를 읽는 readUser(json)을 만들것이다.

readUser(json)JSON.parse() 를 내부적으로 사용할거라 형식에 맞지 않으면 SyntaxError발생.

하지만 user라면 반드시 가져야 할 name 이나 age 같은 속성이 없을때는 이런 에러를 던지면 안된다. 따로 데이터를 검증할 것인데 이때 발생하는 에러를 validationError라고 만들것이다.

먼저 Error를 상속 받기 전에 어떤 객체인지 살펴보자.

1
2
3
4
5
6
7
8
// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
this.stack = <call stack>; // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
}
}

상속 받기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}

function test() {
throw new ValidationError("에러 발생!");
}

try {
test();
} catch(err) {
alert(err.message); // 에러 발생!
alert(err.name); // ValidationError
alert(err.stack); // 각 행 번호가 있는 중첩된 호출들의 목록
}

message 프로퍼티는 부모 생성자의 의해 설정된다.

name 프로퍼티는 ‘Error’ 로 설정되는데 원하는 이름으로 재설정 해준다.

이제 readUser(json) 안에서 ValidationError를 사용해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}

// 사용법
function readUser(json) {
let user = JSON.parse(json);

if (!user.age) {
throw new ValidationError("No field: age");
}
if (!user.name) {
throw new ValidationError("No field: name");
}

return user;
}

// try..catch와 readUser를 함께 사용한 예시

try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // 알려지지 않은 에러는 재던지기 합니다. (**)
}
}

에러 유형에 맞게 처리해 주었다.

에러 유형은 instanceof 말고도 err.name으로 확인 가능하다.


더 깊게 상속하기

ValidationError는 너무 포괄적이니 필요한 프로퍼티가 없는 경우에 상세한 에러를 만들어서 상속 하도록 한다.

PropertyRequiredError 만들기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}

class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}

// 사용법
function readUser(json) {
let user = JSON.parse(json);

if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}

return user;
}

// try..catch와 readUser를 함께 사용하면 다음과 같습니다.

try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No property: name
alert(err.name); // PropertyRequiredError
alert(err.property); // name
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // 알려지지 않은 에러는 재던지기 합니다.
}
}

Error 클래스를 상속받아 직접 커스텀한 에러들은 this.name 을 수동으로 할당해 주고 있다.

이런 방법은 상당히 귀찮은 작업이 될 수 있다.

이런걸 피하기 위해서는 기본 에러 클래스를 만들고 상속 받으면 된다.

기본 에러 클래스는 this.name = this.constructor.name 을 추가해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}

class ValidationError extends MyError { }

class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.property = property;
}
}

// 제대로 된 이름이 출력됩니다.
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

예외 감싸기

readUser 가 점점 커지면 에러도 다양해 질 것이고 VallidationError 는 이런 에러를 포함해야 할 것이다.

그런데 이렇게 에러를 추가하다보면 처리하는 곳에서 분기별로 처리하는 코드를 늘려야 할까?

대부분의 경우 그렇지 않다. 데이터를 읽었을때 에러 발생 여부만 알려주고 필요할때 세부 내용을 알려주는 방버을 사용한다.

wrapping exception을 해보자.

  1. ReadError 만들기
  2. ValidationError, SyntaxError 등의 에러는 readUser 내부에서 잡고 이때 ReadErrorㄹㄹ 생성.
  3. ReadError 객체의 cause 프로퍼티엔 실제 에러에 대한 참조 저장.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class ReadError extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'ReadError';
}
}

class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }

function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}

if (!user.name) {
throw new PropertyRequiredError("name");
}
}

function readUser(json) {
let user;

try {
user = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Syntax Error", err);
} else {
throw err;
}
}

try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Validation Error", err);
} else {
throw err;
}
}

}

try {
readUser('{잘못된 형식의 json}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}

이런 기법은 객체 지향 프로그래밍에서 널리 쓰이는 패턴임.

과제1
내장된 SyntaxError 클래스를 상속하는 FormatError 클래스를 만들어 봅시다.

만들어진 클래스에서 message, name, stack를 참조할 수 있어야 합니다.