[모던JS: Core] 자료구조와 자료형 (1)

KG·2021년 5월 10일
0

모던JS

목록 보기
5/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

원시값의 메서드

자바스크립트의 자료형은 원시형과 객체형로 크게 구분할 수 있고, 각 자료형은 다음의 값을 가진다.

  • 원시형: 문자(string), 숫자(number), bigint, 불린(boolean), 심볼(symbol), null, undefined

  • 객체(형): 순수객체, 내장객체, 함수, 배열 등..

객체의 경우는 이전 포스트에서 프로퍼티로 함수를 가질 수 있고 이들을 보통 메서드라고 칭한다고 했다. 때문에 객체는 내장매서드를 호출하여 자체 기능을 수행할 수 있다.

someObj.func();

그러나 원시형의 값은 가능한 빠르고 가벼워야 하기에 값 자체가 단일값이고 따라서 내장된 메서드라는 개념이 존재하지 않는다. 그러나 문자열이나 숫자와 같은 원시값을 다루어야 하는 작업이 많은 상황에서 이러한 메서드를 사용하면 조금 더 수월한 작업을 할 수 있는 모순적인 상황에 빠지게 되었다. 이를 해결하기 위해 다음과 같은 컨셉을 취하게 되었다.

  1. 원시값은 원시값 그대로를 유지한다.
  2. 문자열, 숫자, 불린, 심볼의 메서드와 프로퍼티에 원시값이 접근할 수 있도록 언어 차원에서 허용한다.
  3. 이를 가능하게 하기 위해, 원시값이 메서드 또는 프로퍼티에 접근하려는 경우엔 특수한 객체인 원시 래퍼 객체(Object Wrapper)를 임시로 생성 후 사용이 끝나면 다시 제거한다.

래퍼객체는 객체를 생성하는 3가지 방법에서도 잠깐 살펴보았는데, 원시값의 종류에 따라 그 이름을 그대로 차용해 String, Number, Boolean 등과 같이 사용한다.

원시값에서 제공하는 메서드에 접근 시에 다음과 같은 과정이 수반된다.

let str = 'hello world!';

console.log( str.toUpperCase() );
  1. 문자열 str은 원시값이고, 따라서 toUpperCase()와 같은 메서드를 가지고 있지 않다. 따라서 이에 접근 시 래퍼 객체가 만들어진다.
  2. 래퍼 객체에 의해 메서드가 실행되고 메서드 내부 동작에 따라 값이 반환된다.
  3. 반환이 완료되면 래퍼 객체는 파괴되고 원시값 str만 남는다.

이러한 과정을 통해 자바스크립트는 원시값을 가볍게 유지하면서 다양한 기능의 메서드를 이용할 수 있다. 자바스크립트 엔진은 위 프로세스처럼 최적화에 많은 신경을 쓰고 있기에 직접 원시 래퍼 객체 생성없이 위의 동작이 정상적으로 동작하게 만들어준다.

그러나 nullundefined의 경우에는 래퍼객체가 없고 따라서 메서드 역시 제공하지 않는다.

자바의 경우와 래퍼 객체는 어느 정도 유사한 면모가 많이 보이지만, 자바스크립트에서는 래퍼 객체를 보통 new연산자와 같이 사용하는 생성자로 쓰는 것을 추천하지 않는다. (객체의 래퍼 객체 Object 는 제외)
생성자를 통해 값을 만드는 경우엔 해당 값이 원시값이 아닌 객체로 인식되기 때문이다.

console.log( typeof 0 );	// "number"
console.log( typeof new Number(0) );	// "object"
// new 연산자 없이 사용하면 원시값을 반환한다.
console.log( typeof Number(0) );	// "number"

숫자형 (Number)

모던 자바스크립트에서는 숫자를 나타내는 방식을 두 가지 자료형으로 다시 세분화할 수 있다.

  1. 일반적인 숫자는 배정밀도 부동소수점 숫자 방식으로 64비트 형식으로 표현한다.
  2. 만약 위의 방식으로 표현할 수 없는 큰 수 또는 작은 수의 경우는 BigInt자료형을 사용한다.

1) 숫자 입력

숫자를 입력하는 경우 10억, 100억과 같은 매우 큰 수는 일일이 0을 입력하면 실수가 발생하기 쉽다. 이 같은 일을 방지하기 위해 e를 사용해 쉽게 위의 값을 표현할 수 있다.

const billion = 1e9;	// 10억, 0이 9개
const ms = 1e-6;	// 0.000001; 소수점도 가능

2) 진법

자바스크립트에서 기본적으로 표현할 수 있는 진법은 3가지로 각각 2진법, 8진법, 16진법을 지원한다.

let binary = 0b11111111;	// 2진법, 255
let octal = 0o377;		// 8진법, 255
let hexa = 0xff;		// 16진법, 255

이 외의 진법을 나타내려면 parseInt()를 통해 변환이 가능하다.

console.log( parseInt('0xff', 16) );	// 255
console.log( parseInt('2n9c', 36) );	// 123456

toStirng(base) 메서드를 이용해 숫자를 base진법으로 변환 후 이를 문자열로 반환할 수 있다.

let num = 255;

console.log( num.toString(2) );		// "11111111"
console.log( num.toString(8) );		// "377"
console.log( num.toString(16) );	// "ff"

3) 부정확한 계산

숫자는 BigInt 형으로 선언하지 않는 이상 64비트 형식으로 표현되는데, 이중에 52비트를 숫자 저장에 사용하고, 나머지 11비트는 소수점 위치, 그리고 최상위비트 1개는 부호값으로 사용한다. 따라서 이를 넘어서는 범위의 숫자는 제대로 표현할 수 없다.

이는 숫자가 0과 1로 이루어진 이진수로 변환되어 메모리에 저장되는 것을 의미하는데, 소수의 경우 이진수로 변환하면 대부분 무한소수가 된다 (2의 거듭제곱으로 나눈 값 제외). 따라서 소수를 아주 정확하게 저장할 수 있는 방법은 없고, 이들은 무한소수로 인식되기에 다음과 같은 일이 발생할 수 있다.

console.log( 0.1 + 0.2 === 0.3 );	// false
// 0.1 + 0.2 = 0.30000000000000004

자바스크립트만의 이슈가 아닌, 해당 방식으로 숫자를 표현하는 다른 언어 (JAVA, PHP, C ...) 에서도 동일하다.

따라서 정확한 소수점 값(쇼핑몰 제품 가격 등)을 위해서는 여러 메서드를 이용해 어림수를 만드는 방법을 차선책으로 사용할 수 있다.

  1. toFixed(n)
  2. Math 내장객체의 메서드 사용

4) 유연한 형변환

단항 연산자 + 또는 Number(), 그리고 곱셈 등의 연산을 통해 숫자형으로 변환하는데엔 엄격한 규칙이 적용된다. 피연산자가 숫자가 아니면 형 변환에 실패한다.

const num = '100';
const nnum = '100px';

console.log( +num );	// 100
console.log( +nnum );	// NaN

이때 parseInt()parseFloat() 메서드를 이용하면 숫자만 추출하여 형변환을 할 수 있다. 그러나 읽을 수 없는 숫자를 먼저 맞닦드리는 경우엔 NaN을 반환한다.

const nnum = '100px';
const nfloat = '12.5rem';
const nx = 'aabb12';

console.log( parseInt(nnum) );		// 100
console.log( parseFloat(nfloat) );	// 12.5
console.log( parseInt(nx) );		// NaN

문자열 (String)

자바스크립트에서 문자열은 페이지 인코딩 방식과는 상관없이 항상 UTF-16형식을 따른다. 이는 16비트(2바이트)로 문자열을 표현하는 방식이다.

1) 문자열 표현 방식

문자열을 나타내기 위해서는 다음과 같이 3가지 방식을 사용할 수 있다. 보통 유연성을 생각한다면 3번 방식을 많이 사용한다.

  1. 작은따옴표
  2. 큰따옴표
  3. 백틱

특히 3번의 방식은 템플릿 리터럴을 표현할 때 유용하다. 비슷하게 함수에서도 관련 기능을 사용할 수 있는데 이는 태그드 템플릿(tagged template)이라고 부른다. 자바스크립트 자체에서는 자주 쓰이지는 않지만 리액트 라이브러리 중 styled-components에서 해당 문법을 베이스로 사용한다.

2) 문자열 불변성

자바스크립트에서 문자열은 변경할 수 없는 데이터이다. 만약 문자열 중간 글자 하나만 바꾸려고 한다면 에러가 발생한다.

let str = 'hello';
str[1] = a;	// Error!

에러를 피하기 위해서는 완전히 새로운 문자열을 만들어야 한다.

3) 문자열 비교

문자열 비교는 알파벳 순서를 기준으로 글자끼리 비교가 이루어진다. 이때 모든 문자열은 UTF-16을 사용해 인코딩 되는데 이로인해 각 글자가 특정 숫자값을 가지게 된다. 따라서 해당값을 이용해 각 문자를 비교하게 된다. 그로인해 다음과 같은 특징을 가지게 된다.

  1. 소문자는 대문자보다 항상 크다.
  2. 발음 구별 기호(영어 알파벳 외 특수 기호문자)는 알파벳 순서 기준과 무관하다.
console.log( 'a' > 'Z' );		 // true
console.log( 'Österreich' > 'Zealand' ); // true

언어마다 문자 체계가 조금씩 다르기 때문에 문자열을 제대로 비교하기 위해서는 이에 대한 모든 경우를 고려해주어야 한다. 다행히 모던 브라우저에서는 대부분 국제화 관련 표준 ECMA-402을 지원하고, 이에 따라서 언어가 다를 때 적용할 수 있는 문자열 비교 규칙 및 메서드가 정의되어 있다.

localeCompare() 함수를 사용하면 ECMA-402에서 정의한 규칙에 따라 문자열을 비교할 수 있다. 해당 함수는 두 개의 문자열을 받아 다음의 조건에서 특정 값을 반환한다.

  1. str1str2보다 작으면 음수 반환
  2. str1str2보다 크면 양수 반환
  3. str1str2이 같으면 0을 반환
console.log( 'Österreich'.localeCompare('Zealand') ); // -1

4) 써로게이트 쌍 (Surrogate Pair)

자주 사용되는 글자들은 보통 2바이트 코드(16비트 : UTF-16)를 가지고 있다. 그런데 2바이트는 2의 16승(65536)개의 조합밖에 만들지 못하기 때문에 현존하는 기호 모두를 표현하기에는 턱없이 부족하다. 이를 극복하려는 시도는 여러가지가 있는데, 그 중에 한 가지 방식으로 사용 빈도가 낮은 기호의 경우는 써로게이트 쌍이라 불리는 2바이트 글자들의 쌍을 이용해 인코딩한다. 따라서 다음과 같은 문자열의 길이는 2가 된다.

alert( '𝒳'.length ); // 2, 수학에서 쓰이는 대문자 X(그리스 문자 카이 - 옮긴이)
alert( '😂'.length ); // 2, 웃으면서 눈물 흘리는 얼굴을 나타내는 이모티콘
alert( '𩷶'.length ); // 2, 사용 빈도가 낮은 중국어(상형문자)

두 번째 예시에서 처럼 써로게이트 쌍으로 가장 유명한 예시는 오늘날 많이 쓰이는 이모지(Emoji)가 아닐까 싶다. 여담으로 이모지는 써로게이트 쌍으로 이루어진 값이기 때문에 다음과 같은 동작 역시 가능하다.

// 해당 이모지는 여러 써로게이트 쌍으로 조합된 문자이다.
const emoji = "👨‍👨‍👦‍👦"
console.log( [ ...emoji] );
// ["👨", "‍", "👨", "‍", "👦", "‍", "👦"]
console.log( a.length );	// 11

배열 (Array)

자바스크립트에서는 배열 이라는 자료형은 따로 존재하지 않는다. 즉 배열은 객체형에 속하는 자료형이다. 객체는 키와 값을 이용한 값의 집합이고, 앞서 살펴보았듯이 순서가 정렬되지 않는 컬렉션이다. 반면 배열의 경우는 값의 집합으로 이루어져 있고, 이들 값은 원소라고 부르며 일정한 순서에 따른 정렬이 가능한 자료구조이다.

엄연히 말하자면 배열 역시 객체이기 때문의 키와 값의 구조를 띄고 있다. 다만 배열은 키가 객체와는 달리 숫자(또는 인덱스)로 이루어진 객체라고 생각할 수 있다.

1) 배열 선언

보통 다음과 같은 두 가지 방식으로 배열 선언이 가능하다. 또한 배열의 원소가 가질 수 있는 자료형에는 제약이 없다. 보통 대괄호를 사용해 선언하는 경우가 많다. Array를 이용해 선언하는 경우는 초기화 방식과 결과를 반환하는 구조가 대괄호와 다르기에 이 부분을 잘 숙지하는것이 필요하다.

// 1. Array 생성자 이용 (객체이므로 new 연산자와 함께 생성)
const arr = new Array();
// 2. 대괄호 사용
const arr = [];

const array = [ 1, 2, 3, 4, 5 ];
const specialArray = [ 1, 'hello', function hi() { console.log('hi') }, { name: 'longroadhome' } ];

2) 스택(Stack) 또는 큐(Queue)

자바스크립트에서는 배열을 이용해 스택과 큐를 구현할 수 있다. 보통 다른 언어의 경우에는 스택과 큐를 구현할 수 있는 내장 라이브러리가 제공되는데 자바스크립트는 관련 라이브러리는 없고 배열의 내장메서드를 통해 로직만 구현할 수 있다. 스택의 경우는 크게 문제가 없지만 (배열 자체가 스택이다) 큐의 경우에는 해당 이유로 시간복잡도가 다른 언어의 큐 라이브러리에 비해 매우 높다. 때문에 코딩테스트와 같이 시간복잡도가 민감한 상황에서는 별도로 큐 자료구조를 구현해야 하는 경우가 있다.

기본적으로 제공하는 다음 4개의 배열 메서드를 이용해 스택과 큐의 구조를 구현할 수 있다.

  • push : 배열 마지막에 원소를 추가
  • pop : 배열 마지막 원소를 제거 후 반환
  • shift : 배열 첫 원소를 제거 후 반환
  • unshift : 배열 처음에 원소를 추가

이때 shiftunshift는 연산 후 배열을 재정렬하는 과정이 필요하기 때문에 낮은 퍼포먼스를 보인다. 큐는 보통 shift 메서드로 구현하기 때문에 위와 같은 문제가 종종 발생할 수 있다.

3) 반복문

배열은 여러 원소의 집합으로 이루어진 컬렉션이기 때문에 각각의 원소에 접근하기 위해 반복문을 사용한다. 자바스크립트에서는 배열에 접근하기 위해 다음 3가지 방식의 반복문을 사용할 수 있다. (자체적으로 제공하는 내장메서드 forEach는 이후에 살펴본다)

  1. 전통적인 for 반복문
const arr = ['a', 'b', 'c', 'd', 'e'];

for(let i = 0; i < arr.length; i ++) {
  console.log(arr[i]);
}
  1. for...of 사용
const arr = ['a', 'b', 'c', 'd', 'e'];

for(let el of arr) {
  console.log(el);
}

for...ofItreator에 사용할 수 있는 순회 반복문이다. Itreator에 대해서는 다음 포스트에서 자세히 살펴보도록 하자. 배열은 Itreator 속성을 가지고 있기 때문에 for...of를 통해 각 원소에 접근할 수 있다. 이때 보통의 경우 for...of에서는 배열 인덱스에 접근할 수 없다. 물론 arr.entries()를 통해 접근이 가능하지만 보통 인덱스가 필요한 경우에는 1번의 방법 또는 forEach()를 사용하는 경우가 많다.

  1. for...in 사용
const arr = ['a', 'b', 'c', 'd', 'e'];

for(let el in arr) {
  console.log(el);
}

for...of와 생김새가 크게 다르지 않아 보인다. for...in은 객체와 같이 키 값을 가지고 있는 자료형을 대상으로 순회할 수 있다. 배열 역시 객체이므로 for...in을 사용할 순 있다. 그러나 다음 두 가지 이유로 보통 배열을 순회할 때 for...in은 잘 사용하지 않는다.

  • for...in은 객체 대상이므로 모든 프로퍼티에 대해 순회한다. 즉 키가 배열처럼 숫자가 아닌 프로퍼티 역시 순회 대상에 포함된다. 자바스크립트 환경에서는 배열은 아니지만, 이와 유사한 형태를 보이는 유사 배열(array-like) 객체가 있는데 이는 배열처럼 length 프로퍼티도 있고 인덱스 또한 존재한다. 하지만 유사 배열은 배열과 달리 키가 숫자형이 아닐 수 있다. 따라서 유사 배열을 상대로는 이 모든 것을 대상으로 순회가 이루어지며 필요 없는 프로퍼티에 접근하여 문제를 일으킬 가능성이 있다.

  • for...in은 배열이 아닌 객체에 최적화가 되어있다. 따라서 배열에 사용하면 객체 대비 약 10~100배 정도 느린 성능을 보인다.

4) length 프로퍼티

배열에 무언가 조작을 가하면 length 프로퍼티가 자동으로 갱신된다. 이때 length 프로퍼티를 배열 원소의 개수와 동일시하는 경우가 많은데 엄밀히 말하면 length는 배열 내 요소 개수가 아닌 가장 큰 인덱스 + 1의 값을 가진다.

let arr = [];
arr[123] = 1;

console.log( arr.length );	// 124

따라서 배열을 위와 같이 사용하지 않도록 주의해야 한다. 또한 자바스크립트에서 length를 이용해 배열 초기화를 할 수 있다. length값을 수동으로 증가시키는 것은 배열에 별다른 영향을 미치지 않지만, 감소시키는 경우엔 배열이 잘리고, 잘린 배열은 다시 되돌릴 수 없다.

let arr = [1, 2, 3, 4, 5];

arr.length = 2;
console.log(arr);	// [1, 2];

arr.length = 5;
console.log(arr[3]);	// undefined (복구X)

arr.length = 0;
console.log(arr)	// [];

5) Array.isArray()

자바스크립트에서 배열은 독립된 자료형으로 취급되지 않고 객체형에 속한다. 따라서 typeof 연산자로는 배열을 구분할 수 없다.

console.log( typeof { name: 'KG' } );	// object
console.log( typeof [ 'KG' ] );		// object

배열은 자주 사용되는 자료구조이기 때문에 특정 상황에서 배열임을 체크해야 할 경우가 생길 수 있다. 이를 위해 지원되는 내장메서드가 Array.isArray()이다.

console.log(Array.isArray({}));	// false
console.log(Array.isArray([]));	// true

6) 배열메서드와 thisArg

위에서 살펴본 배열 메서드 외에도 다양한 메서드가 존재한다. 모든 배열 메서드에 대한 예제와 설명은 해당 페이지를 참고하자.

  • map
  • filter
  • find
  • findIndex
  • reduce
  • indexOf
  • includes
  • sort
  • ...

이때 함수를 호출하는 배열 메서드(sort는 제외)는 thisArg라는 매개변수를 옵션으로 받을 수 있다. 자주 사용되지 않지만 thisArg는 다음의 기능을 수행한다.

마지막 인수로 thisArg를 지정해주면 이 값은 해당 함수의 this가 된다.

const army = {
  limit: 20,
  canJoin(person) {
    return person.age >= this.limit;
  }
}

const users = [
  { age: 12 },
  { age: 21 },
  { age: 22 },
  ...,
];

const soldiers = users.filter(army.canJoin, army);

이때 thisArgarmy를 지정해주지 않았다면 army.canJoin은 단독 함수처럼 취급되고, 함수 본문 내 thisundefined가 되어 에러가 발생했을 것이다.

또는 화살표 함수를 사용해 이를 대체할 수도 있다.

...

const soldiers = users.filter(user => army.canJoin(user));

7) Immutability

자바스크립트는 객체지향형 패러다임 외에 함수형 프로그래밍으로도 많이 설계되는데, 이때 가장 중요한 핵심은 Immutability를 고수하는 것이다. 이는 변경 불가성을 의미하는데 쉽게 말해 원본 데이터를 유지함을 의미한다. 이 개념은 프론트엔드 라이브러리 React에서도 렌더링 최적화와 관련해 중요하게 사용되는 개념이다. 또한 이를 위한 라이브러리 Immutalbe.js 역시 존재한다.

Immutability 자체에 대한 소개는 다음 기회로 미루고 배열의 메서드 중에는 이러한 변경불가성을 지키는 메서드가 있고, 이를 무시하는 메서드가 있다. 보통 원본 배열의 값을 변경하지 않고 새로운 배열을 반환하는 메서드는 해당 원칙을 지킨다고 볼 수 있다. 반면 다음 메서드는 원본 배열 자체를 변경시키기 때문에 Immutability를 깨뜨린다고 볼 수 있다.

  • splice
  • sort
  • reverse
  • 그 외 push, pop 등...

위 메서드는 변경불가성을 깨뜨리기 때문에 사용하면 안 된다의 개념이 아니라 위의 이슈가 있다는 것을 알고 사용하는 것이 좋다.

References

  1. https://ko.javascript.info/data-types
  2. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array
  3. https://poiemaweb.com/js-immutability
profile
개발잘하고싶다

0개의 댓글