Ayden's journal

모던 JS

타입캐스트와 확인

JS에서는 묵시적으로 데이터의 타입을 변화해주기도 한다. 가령 3 + "2"와 같은 계산식에서 그러한데, 이러한 묵시적 타입 변경 대신 사용자가 직접 데이터의 타입을 바꿔줄 수 있다. 이를 타입캐스트라고 부른다. typeof를 사용하면 특정 데이터의 타입을 확인할 수 있다.

Number() // 숫자형으로 변환
String() // 문자형으로 변환
Boolean() // 불린형으로 변환

typeof value3 
typeof(value3)// 자료형을 확인할 수 있다.

 

 

Truthy vs Falsy

불린 값을 판단할 때, 조건식이나 불린 타입의 값이 아니더라도 맥락에 따라 불린 값처럼 평가될 수 있다. 이 때, false 처럼 평가되는 값 [ false, null, undefined, 0, NaN, ''(빈 문자열) ]을 falsy하다 부르고, 그 외 나머지를 truthy하다 부른다.

 

논리 연산자

일반적으로 논리 연산자는 양 쪽의 값을 판단하여 특정 결과를 불린 값으로 도출하지만, 모던 JS에서는 왼쪽의 불린 맥락을 판단하여 둘 중 어느 값을 반환할지 결정한다. AND 와 OR 연산자 사이에서는 AND 연산자의 우선순위가 더 높다.

NOT 연산자 (!) : 참거짓을 바꿔준다

AND 연산자 (&&) // 왼쪽이 truthy하면 오른쪽을 반환, 왼쪽이 falsy하면 왼쪽을 반환한다

OR 연산자 (||) // 왼쪽이 truthy하면 왼쪽을 반환, 왼쪽이 falsy하면 오른쪽을 반환한다

OR 연산자와 비슷하게 작동하는 null 병합 연산자 (??)라는 것도 있는데, truthy falsy를 판단하는 OR 연산자와는 다르게 null 병합 연산자는 null인지 undefined인지만을 판단한다. 이것을 사용하면 여러 피연산자 중 그 값이 확정되어 있는 변수를 찾을 수 있다.

 

삼항 연산자

삼항 연산자를 사용하면 if 문을 사용하지 않고도 참거짓에 따라 필요한 표현식을 빠르게 뱉어낼 수 있다. 일반적인 삼항 연산자의 형태는 [ 조건문 ? 참일 때의 표현식 : 거짓일 때의 표현식 ]이다.

const cutOff = 80;

function passChecker(score) {
	return score > cutOff ? '합격입니다!' : '불합격입니다!';
}

console.log(passChecker(75)); // 문자열 '불합격입니다!'가 반환

 

옵셔널 체이닝 연산자(?.)

이 연산자를 사용하면 체인으로 이루어진 각 참조가 유효한지 명시적으로 검증하지 않고 연결된 객체 체인 내에 깊숙히 위치한 속성 값을 읽을 수 있다.

const newObj = {
	name = "ayden",
    pet = {
    	name = "또리",
        age = 4,
    }
}

console.log(newObj.pet?.name)

가령 위와 같은 코드가 있을 때 옵셔널 체이닝 연산자는 우선 연산자의 앞이 null 또는 undefined인지를 판단한다. 예시에서는 newObj.pet이 존재하기 때문에 이어서 해당 프로퍼티의 name 값을 뱉어낸다. 만약 연산자의 앞이 null 또는 undefined라면 옵셔널 체이닝 연산자는 undefined를 뱉어낸다.

console.log(newObj.pet?.name ?? "털 달린 짐승 안 키웁니다")

앞서 살펴본 null 병합 연산자와 함께 사용하면 더욱 알차게 써먹을 수 있다.

 

 

변수와 함수

스코프

변수와 함수를 어디서 어떻게 선언하는지에 따라 각각은 사용할 수 있는 범위를 가지게 된다. 함수나 코드 블럭 내에서 선언된 변수나 함수는 바깥에서는 사용할 수 없는데, 이를 로컬 스코프를 가진다고 부른다. 코드 블럭 바깥에서 선언되었다면 코드 블럭 안에서도 쓸 수 있는데, 이는 글로벌 스코프를 가진다고 부른다.

 

함수표현식

일반적으로 사용되는 함수선언문 외에도 JS에서는 함수표현식을 사용해 함수를 정의할 수 있다. 이는 JS가 함수를 특별한 종류의 값으로 취급하기에 가능하다. 간단하게 생각하면 node.addEventListener 메소드의 두 번째 아규먼트로 미리 선언된 함수 대신 직접 함수를 작성할 수 있는데, 여기서 쓰이는 함수는 함수표현식으로 정의한 것이다.

function sayHi() {
  console.log('Hi!');
}; // 함수 선언 (function declaration)

const sayHi = function () {
  console.log('Hi!');
}; // 함수 표현식 (function expression)

 

즉시 실행 함수

즉시 실행 함수는 외부에서 재사용할 수 없지만 재사용이 필요 없는, 일회성 동작을 구성할 때 활용된다. 즉시 실행 함수에서 사용하는 변수들은 함수 내에서만 유효하기 때문에 이런 점을 활용하면 일시적으로 사용할 변수의 이름들을 자유롭게 작성할 수 있다.

(function () {
  console.log('Hi!');
})();

함수선언 부분을 소괄호로 감싼 다음에 바로 뒤에 함수를 실행하는 소괄호를 한 번 더 붙여주면 함수가 선언된 순간 바로 실행이 된다.

 

함수의 호이스팅

함수선언문을 통해 정의된 함수는 var로 선언한 변수처럼 글로벌 스코프를 가진다. 때문에 코드 블록 안에서 함수를 선언하더라도 호이스팅(Hoisting, 선언을 해당 스코프의 최상단으로 끌어올리는 행위)된다. 때문에 함수 선언 이전에 호출해도 코드가 정상적으로 동작한다.

text1()
text2()

function text1() {
    return "A"
}

var text2 = function() {
    return "B"
}

// 'A'
// TypeError: text2 is not a function

다만, 위의 예시에서 볼 수 있듯 호이스팅은 선언 자체에만 적용되고 할당된 값은 선언 이후에 사용할 수 있다.  그러니 엥간하면 변수든 함수든 먼저 선언하고 그 다음에 호출하도록 하자.

 

Arrow Function

화살표 함수는 기존의 함수 선언을 보다 간결하게 개선한 방식이다. 언제나 무기명 함수이기 때문에, 즉시 실행 함수나 다른 함수의 아규먼트로 넣어주는 게 아니라면 함수표현식을 통해 변수나 상수에 붙여주어야 한다.

const getTwice = (number) => {
  return number * 2;
}; // 화살표 함수 (Arrow Function) 선언 방식

이 뿐만 아니라, 더욱 함수를 간결하게 표현하기 위해 일정한 조건이 만족되면 몇 가지 개념을 더 생략할 수 있다. 만약 파라미터가 하나 뿐이라면 소괄호를 생략(할 수는 있는데 가독성을 위해 생략하지 않는 것도 고려해봐야)할 수 있고, 함수의 동작이 리턴 뿐이라면 return문은 물론이고 중괄호까지 생략할 수 있다.

const getTwice = number => number * 2;

일반 함수와 달리 이렇게 화살표로 선언된 함수는 arguments 객체를 가지지 못하고, this가 가리키는 값이 일반적인 함수와는 다르다.

 

 

파라미터와 아규먼트

파라미터의 기본값

자바스크립트에서는 함수의 파라미터에 기본값을 부여할 수 있다. 기본값이 있는 파라미터는 함수를 호출할 때 아규먼트(argument, 파라미터에 전달되는 값)를 전달하지 않으면, 함수 내부의 동작은 이 파라미터의 기본값을 가지고 동작한다. 이러한 기본값을 옵셔널 파라미터라고도 부른다.

function sayHi(name = 'Anonymous') {
  console.log(`Hi! ${name}`);
}

sayHi('JavaScript'); // Hi! JavaScript
sayHi(null); // Hi! null
sayHi(); // Hi! Anonymous

함수를 호출할 때 아규먼트로 null 값을 사용하게 되면, 해당 파라미터는 null 값을 그대로 전달받게 된다. 기본값으로 사용되길 희망한다면 파라미터를 비워두거나, 여러 파라미터를 동시에 사용할 경우에는 undefinded를 사용해야 한다.

 

arguments 객체

function printArguments() {
  // arguments 객체의 요소들을 하나씩 출력
  for (const arg of arguments) {
    console.log(arg); 
  }
}

printArguments('Ayden', 'Blair', 'Junior');

arguments 객체는 함수를 호출할 때 전달한 아규먼트로 이루어진 유사 배열 객체이다.

 

Rest Parameter

전달받은 아규먼트 전체를 기준으로 '자동으로' 생성된 arguments 객체와 다르게 Rest Parameter 문법을 사용하면 일부 아규먼트를 대상으로 세밀하고 유연하게 다룰 수 있다. 또한 Rest Parameter 문법으로 생성되는 배열은 arguments 객체와 달리 진짜 배열이기 때문에 배열의 메소드 등을 자유롭게 사용할 수 있다.

function printArguments(...args) {
  // args 객체의 요소들을 하나씩 출력
  for (const arg of args) {
    console.log(arg); 
  }
}

printArguments('Ayden', 'Blair', 'Junior');

또한, rest parameter는 다른 일반 파라미터들과 함께 사용할 수 있다. 앞에 정의된 파라미터에 argument를 먼저 할당하고 나머지 argument를 배열로 묶는 역할을 하기 때문에 일반 파라미터와 함께 rest parameter를 사용할 때는 반드시 가장 마지막에 작성해야 한다.

function printLotteryWinner(first = '당첨자 없음', second = '당첨자 없음', ...others) {
  console.log('본롸 로터리 당첨자 발표');
  console.log(`1등: ${first}`);
  console.log(`2등: ${second}`);
  for (const arg of others) {
    console.log(`꽝: ${arg}`);
  }
}

printRankingList('글쟁', undefined, '골드', '경식', '초코');

 

 

this

JS에서 this는 생각보다 다양한 용법이 존재하는 것 같다. 기본적으로 this는 호출한 객체를 가리키지만, 이러한 "호출한 객체"가 정확히 누구인지는 직관적으로 받아들이기가 조금 난해하다고 생각한다. 당장은 this의 용처에 따라 몇 개 정도의 사례로 분리하여 생각해보기로 했다.

 

this = window

전역 객체(global object)는 일반적으로 모든 객체의 유일한 최상위 객체를 의미하며, 브라우저에서 동작한다면 window 객체를 지칭하게 된다. 전역 변수는 전역 객체의 프로퍼티이고, 전역 함수는 전역 객체의 메소드이다. 이것은 전역 스코프의 작동 원리이기도 하다.

// 전역 객체의 프로퍼티 추가
const A = this;
console.log(A) // window 객체 출력
// 전역 객체의 메소드 추가
function B() {
  return this;
}
console.log(B()); // window 객체 출력

 

메소드에서의 this

객체가 메소드를 호출한다면, 이 메소드 내부에서 사용된 this는 해당 메소드를 호출한 객체를 지칭하게 된다.

function getFullName() {
	return `${this.firstName} ${this.lastName}`;
}

const userC = {
	firstName: "Ayden",
	lastName: "Blair",
	getFullName: getFullName,
}

userC.getFullName() // "Ayden Blair"

 

이벤트 핸들러에서의 this

이벤트 핸들러에서 this는 이벤트를 받는 HTML 요소(e.currentTarget)를 지칭하게 된다.

const inputTag = document.querySelector('input')

inputTag.addEventListener('click', function () {
  console.log(this); // <input>
});

 

 

모─던한 프로퍼티 표기법

단축된 속성명

프로퍼티의 키와 값이 같다면, 하나만 써도 잘 알아듣는다. 이는 메소드를 추가해줄 때도 같다.

const fisrtName = "Ayden"

const user = {
	firstName,
	lastName: "Blair",
	getFullName, // 메소드에서의 this에서 작성한 함수와 같음
}

user.name // "ayden"
user.getFullName() // "Ayden Blair"

 

계산된 속성명

computed property name 이라고도 하는 이 개념은 프로퍼티 이름을 조금 더 화끈한 방식으로 사용할 수 있게 해준다.

const newObj = {
	["get" + "FullName"]: , // 문자열의 합으로 표기할 수도 있고
	[fullName]: , // 변수의 값을 가져올 수도 있으며
	[getFullName()]: , // 함수의 리턴 값을 쓸 수도 있다
}

 

 

Spread

Spread 구문은 배열이나 객체를 복사하거나 혹은 복사해서 새로운 요소들을 추가할 때 유용하게 활용할 수 있다. 이를 사용하면 배열을 객체로 펼쳐낼 수 있지만, [...obj] 와 같은 방식을 통해 객체를 배열로 만들수는 없다. 객체의 spread는 반드시 {…obj} 처럼 객체로 spread 되게 해주어야 오류가 나지 않는다.

const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [...arr1, ...arr2];

console.log(arr3) // [1, 2, 3, 4] (4)
const members = ['글쟁', '레빗', '프롬'];
const newObject = { ...members };

console.log(newObject); // {0: "글쟁", 1: "레빗", 2: "프롬"}

앞서 살펴본 Rest Parameter와 비슷한 형태를 하고 있다. 하지만 Rest Parameter는 여러 아규먼트를 받아서 하나의 객체로 취급하는 것이고, spread는 반대로 하나의 배열을 각각의 내용물로서 취급한다.

 

 

구조 분해 할당

배열과 객체와 같이 내부에 여러 값을 담고 있는 데이터 타입을 다룰 때 구조 분해 할당Destructuring 문법을 활용하면, 배열의 요소나 객체의 프로퍼티 값들을 개별적인 변수에 따로 따로 할당해서 다룰 수 있다. 어떻게 사용하느냐에 따라 코드의 가독성을 드라마틱하게 높일 수 있다.

 

배열에서의 구조 분해

배열을 구조 분해할 때에도 rest parameter를 사용할 수 있다.

const newArray = ["가", "나", "다", "라", "마"];

const [firstAlphabet, secondAlphabet, ...alphabets] = newArray;

firstAlphabet // "가"
secondAlphabet // "나"
alphabets // (3) ["다", "라", "마"]

두 변수의 값을 바꿔줄 때에도 사용할 수 있다

let newA = ["가", "나"];
let newB = ["다", "라"];

[newA, newB] = [newB, newA]

newA // (2) ["다", "라"]

 

객체에서의 구조 분해

요소를 순서대로 할당했던 배열에서의 구조 분해와 달리, 객체에서는 프로퍼티 네임대로 구조 분해가 이루어지게 된다. 때문에 객체에서 구조 분해를 할 때에는 rest parameter 뿐만 아니라 할당 연산자를 통해 기본값을 부여해줄 수도 있다.

const user = {
	name: "Ayden",
	age: 29,
	sex: "male",
	drink: "beer",
}

const {name, age, sex: gender, height = 182, ...restInfo} = user

구조 분해를 진행하면 기본적으로는 프로퍼티 네임과 변수명이 같게 되지만, 원한다면 프로퍼티 네임과 다른 변수명을 부여할 수도 있다. 위의 예시에서 프로퍼티 네임은 sex이지만 변수명은 gender로 할당한 것처럼 말이다. 이러한 테크닉은 변수명의 중복을 방지해주고, 계산된 변수명이나 따옴표를 이용한 문자열 프로퍼티 네임을 사용할 때 문제가 생기지 않도록 도와준다.

 

함수에서의 구조 분해

함수가 리턴하는 값이 배열이나 객체일 때는 앞서 살펴본 방식들과 동일한 방법으로 구조 분해를 진행하면 된다. 코드의 가독성을 높일 때, 함수가 받는 rest parameter가 배열이라는 점을 활용해 구조 분해 해줄 수도 있다.

function lotteryWinner(...arg) {
	const [first, second, ...etc] = arg;
}

혹은 파라미터 부분에서 구조 분해를 바로 적용해줄 수도 있다. 아래의 예시는 위에서 보았던 코드와 정확히 같은 일을 하게 된다.

function lotteryWinner([first, second, ...etc]) { }

마찬가지로 객체 역시 비슷한 방식으로 구조 분해하여 사용할 수 있다. 만약 이런 방식을 사용하지 않는다면 함수 내에서 객체의 프로퍼티에 접근할 때마다 object.title이나 object.color와 같이로 반복하여 객체의 이름을 적어주게 될 것이다.

function printSummary({"product-title": product, color = "red", price}) { }

 

이벤트 핸들러와 구조 분해

const btn = document.querySelector(‘#btn’)

btn.addEventListener(‘click’, (event) => {
	event.target.classList.toggle(‘checked’)
}); // 일반적인 이벤트 리스너

btn.addEventListener(‘click’, ({ target }) => {
	target.classList.toggle(‘checked’)
}); // 구조 분해를 이용해 이벤트 객체의 target 프로퍼티만 뽑아서 간결하게 작성

btn.addEventListener(‘click’, ({ target: { classList } }) => {
	classList.toggle(‘checked’)
}); // 중첩된 객체의 스바라시 구조 분해(nested object destructuring)

 

 

에러와 에러 객체

자바스크립트에서 에러가 발생하면 그 순간 프로그램 자체가 멈춰버리고 이후의 코드가 동작하지 않는다. 이렇게 에러가 발생하면 에러에 대한 정보를 name과 message라는 프로퍼티로 담고 있는 에러 객체가 만들어지는데, 대표적인 에러 객체는 SyntaxError, ReferenceError, TypeError 등이 있다.

ReferenceError : 존재하지 않는 변수나 함수를 호출
TypeError : 잘못된 방식으로 자료형을 다룸
SyntaxError : 문법에 맞지 않음

에러 객체는 new 키워드를 사용해 직접 만들 수 있고, throw 키워드를 사용하면 에러를 발생시킬 수도 있다.

const newError = new TypeError(‘타입 에러가 발생했습니다.’) // 에러 객체를 생성
throw newError; // 에러를 발생

throw new error('타입 에러가 발생했습니다.'); // 생성과 초기화와 발생을 동시에

 

try...catch...finally 문

에러가 발생하면 코드는 즉시 종료되고 에러 이후의 코드는 동작하지 않는다. 에러를 예외 처리(Exception Handling)하고 이후의 동작을 계속 진행하고 싶을 때 이 구문을 사용한다. 주의해야 할 점은 이 구문이 실행 가능한 코드(SyntaxError는 실행 불가능)에서 발생한 에러만을 예외 처리한다는 것이다.

try {
  // 실행할 코드
} catch (error) {
  // 에러가 발생했을 때 실행할 코드
} finally {
  // 항상 실행할 코드
}

try문에서 에러가 발생한다면, 생성된 에러 객체는 콘솔에 출력되지 않고 catch문의 첫 번째 파라미터로 전달된다. 만약, try문에서 에러가 발생하지 않았다면 당연하게도 catch문의 코드는 동작하지 않는다. try문에서 에러 발생 여부와 관계 없이 finally문은 항상 실행된다.

 

 

모듈과 모듈 스코프

큰 규모의 자바스크립트 프로젝트를 진행하다보면 코드의 원활한 관리를 위해 기능별로 나누어 관리하게 되는데, 이를 모듈이라 부른다.

모듈 파일 안에서 선언한 변수는 외부에서 자유롭게 접근할 수 없도록 막아야 한다. HTML 파일에서 자바스크립트를 불러올 때 script 태그에 type 속성을 module이라는 값으로 지정해주면 각 파일들이 모듈 스코프를 갖게 된다.

<script type="module" src="index.js"></script>

JS에서 다루는 모듈 문법이라는 것은 결국은 다른 파일에서도 써먹고 싶은 변수나 상수, 함수 등을 어떻게 뽑아다가 원하는 파일에 넣어줄 것인가에 대한 내용이라고 생각된다.

 

export 키워드

기본적인 export 문법은 내보내고 싶은 변수 상수 함수 앞에 export 키워드를 적어주는 것이다.

// printer.js
export const title = 'CodeitPrinter';
export const print = (value) => {console.log(value);}

변수나 함수 앞에 매번 export 키워드를 붙여줄 수도 있지만, 그 수가 많을 경우 하나의 객체로 모아 한번에 내보낼 수 있다(모─던한 단축 속성명을 사용해서 객체화 하는 것). as 키워드를 사용하면 이렇게 내보내면서 이름을 바꿔줄 수 있다.

// printer.js
export { title as printerTitle, print, printArr };

 

import 키워드

내보내진 변수 상수 함수 등을 다른 파일에서 사용하려면 import 키워드 이후에 아래와 같은 형태로 작성해주면 된다.

// index.js
import { title, print } from './printer.js';

export 때와 마찬가지로 모듈을 불러올 때 as 키워드를 활용하여 import 되는 대상의 이름을 변경할 수 있다. 이름을 바꿔서 import 하면 여러 파일에서 불러오는 대상들의 이름이 중복되는 문제를 해결할 수 있다.

// index.js
import { title as printerTitle, print } from './printer.js';

print(printerTitle);

와일드카드 문자(*)와 as를 활용하면 모듈 파일에서 export하는 모든 대상을 하나의 객체로 불러올 수 있다.

// index.js
import * as printerJS from './printer.js';

console.log(printerJS.title); // CodeitPrinter
console.log(printerJS.print); // ƒ print(value) { console.log(value); }

 

default 키워드

일반적으로 모듈 파일에서 export 대상이 하나라면, default 키워드를 함께 활용하는 것이 조금 더 간결한 코드를 구성하는 데 도움이 된다. 여러 대상을 하나의 객체로 모아 내보낼 때도 사용할 수 있다.

// printer.js
const title = 'CodeitPrinter';
const print = (value) => {console.log(value);}

export default {title, print};

default 키워드를 통해 내보낸 대상을 import하는 방식에는 일반적인 default import 방식과 축약형 default import 방식이 존재한다.

import { default as printerJS } from './printer.js'; // 일반적 default import

import printerJS from './printer.js'; // 축약형 default import

console.log(printerJS.title); // CodeitPrinter
console.log(printerJS.print); // ƒ print(value) { console.log(value); }

다만, default 키워드를 통해 import하게 되면 as 키워드를 사용해 대상의 이름을 바꿔줄 수가 없게 된다.

 

가져와서 다시 내보내기

다양한 파일로 나누어진 것들을 간편하게 관리하기 위해 하나의 파일에 필요한 모듈을 모두 모아서 export하고 , 필요한 곳에서 한 줄로 import 할 수도 있다.

// (modules.js)
import module1 from './sub-module1.js';
import module2 from './sub-module2.js';
import module3 from './sub-module3.js';

export { module1, module2, module3 };
// index.js
import { module1, module2, module3 } from 'modules.js';

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기