본문 바로가기
JavaScript

[JavaScript] 자바스크립트 기본, 심화 문법 및 엔진 동작 원리

by 도전하는 린치핀 2024. 2. 5.

1. 함수형 프로그래밍 패러다임

자바스크립트는 함수형 프로그래밍 패러다임을 추구하며, 추가로 객체지향 프로그래밍 패러다임도 지원한다.

자바스크립트의 핵심은 함수(함수형 프로그래밍 패러다임)와 객체(객체지향 프로그래밍 패러다임)이다.

 

  • 객체의 경우 객체지향 프로그래밍 패러다임의 여러 패턴들을 적용하고 싶다면, 클래스를 활용할 수 있다.

그 중 함수형 프로그래밍 패러다임에 대해 알아볼 것인데 함수형 프로그래밍은 아래의 2개 조건을 만족하는 것을 의미한다.

  • 일급함수 : 함수 변수 할당 + 함수 파라미터 + 함수 반환
    • 함수 변수 할당 = 함수 표현식(Expression)
    • 함수 파라미터
    • 함수 반환
  • 순수 함수 : No-Side-Effects (참조 투명성) = Thread-Safe = 함수 정의할 때 완전한 문장을 만들어야한다.
    • 기본적으로 함수는 [파라미터(input) - 함수로직 - 반환(output) ] 3개의 구성 요소로 이루어져 있다.
    • 순수 함수의 경우 [파라미터 - 로직 - 반환 ] 이외의 것들에 의존하지 않아야 한다.
    • 함수는 명확한 명사(주어, 목적어), 동사에 따라 [파라미터 - 로직 - 반환] 에 명시되어야 한다.
  • Thread-Safe : 멀티 스레드를 활용한 개발 시 순수함수 특성은 매우 중요하다.
    • 병렬적으로 수행되는 모든 함수들은 서로에게 영향을 주어선 안되고, 외부의 값에 접근해서도 안된다.

2. 자바스크립트 함수 정의 및 사용

 

2-0-1. 자바스크립트 변수 선언 방법 : var → let, const

함수 정의와 사용에 들어가기에 앞서, 변수에 대해 배워야한다.

변수 선언은 자바스크립트 버전인 ECMAScript6(2015) 이전에는 var 하나의 방법으로만 선언하였다.

그 이후로 변수 선언은 let과 const로 선언할 수 있다.

  • let : 가변 변수 선언 시
  • const : 불변 변수 선언 시 

 

  • 위의 그림을 통해 var, let, const 차이에 대해 정리해보자
    • var : 재할당 가능 + 재선언 가능 + 함수 레벨 스코프
    • let : 재할당 가능 + 재선언 불가 + 블록레벨 스코프
    • const : 재할당 불가 + 재선언 불가 + 블록레벨 스코프
  • 그렇다면 함수 레벨 스코프와 블록 레벨 스코프의 차이는 무엇일까?
    • 스코프 : 변수에 대한 접근 영역
      • 함수 스코프 : 함수가 생성 시 스코프 영역이 생겨난다.
      • 블록 스코프 : 블록이 생성 시 스코프 영역이 생겨난다.

2-0-2. 자바스크립트 엔진의 개략적 수행 방식 및 호이스팅과 var와 let의 호이스팅 차이점

자바스크립트 엔진이 수행하는 것은 사실상 자바스크립트 파일이고, 이것 또한 하나의 큰 함수라고 볼 수 있다.

따라서 자바스크립트 엔진의 자바스크립트 파일 구동 방식자바스크립트의 함수 구동 방식과 동일하다.

이에 자바스크립트 엔진의 자바스크립트 함수 구동 방식에 대해 아래 간단하게 2개의 Phase로 나누어 설명하겠다.

 

프로그램에 메모리와 CPU 자원을 할당한 뒤 구동한다면 이것은 프로세스라고 한다.

모든 함수의 구동은 그 함수 구동을 위한 메모리 영역을 확보하는 것에서부터 시작한다.

 

  • 함수의 실행 = 실행 컨텍스트는 아래 절차대로 수행된다.
    1. Creation / Compile Phase = Pre-Parsing (Lexer ~= Paser)
      • 변수 선언 + 함수 선언 (Declaration) = 렉시컬 환경 내 적재 + var 변수 초기화
    2. Excution Phase
      • 변수 할당 + 함수 실행 + let, const 변수 초기화
      • 만약 변수를 할당 할 때 선언이 없다면 찾아나서야 한다 (Scpoe Chain)
      • 함수 실행 시 함수 선언부로 가서 박제된 함수를 실행한다.

 

var, let, const의 변수 호이스팅에 대한 차이점은 해당포스팅에서 다루었으니 넘어가도록 하자

 

[ASAC_04/JavaScript] 호이스팅(Hoisting)

1. 호이스팅(Hoisting) 호이스팅(Hoisting)이란 함수내의 변수 및 함수의 선언들을 모두 유효 범위의 최상단으로 끌어올려 주는 JavaScript의 기능 중 하나이다. 실제 코드가 끌어올려 코드의 변화가 있

rnclf1005.tistory.com

 

 

2-1. 함수 작성 방법

자바스크립트 기본 문법에 앞서 가장 기본이 되는 함수 정의 방법에 3가지에 대해 알아보자

 

  1. 함수 선언문 (Declartion) : 함수 선언 + 정의
    • 함수의 선언과 정의가 한꺼번에 되었으므로 함수 호이스팅이 발생한다.
      hoisting() // 함수 호이스팅 = function is hoisted
      
      function hoisting() {
      	console.log('function is hoisted')
      }​
  2. 함수 표현식 (Expression) : 표현식 = 식별자(변수) + 연산자(=) + 리터럴(함수)로 이루어짐
    • 함수 표현식의 경우 변수에 함수를 할당했다고 생각하면 되기 때문에 변수 호이스팅이 발생한다.
      hoisting() // 변수 호이스팅 = ReferenceError: hoisting is not defined
      
      let hoisting = function () {
      	console.log('variable is hoisted')
      }​
  3. 화살표 함수 : 단순히 표현만 간략해진게 아니라 내부 동작 또한 간략화하기 위해 사용
    • 함수 리터럴은 2가지 방식으로 표현 : function() {} 과 () => {}

 

2-2. 함수 정의 방식

함수는 아주 단순하게 인풋(파라미터) → 로직 → 아웃풋(반환) 으로 구성되어 있다.

그렇다면 이 세가지를 어떻게 구성할지가 함수의 전부라고 할 수 있다.

  • 함수에게 전달인자를 전달하는 방식
    • 인자 형태로 전달(ex. function(a, b, c, d) ) : 순서 - 수가 적고 명확하게 지정할 때(반드시 전달해야 한다)
    • 객체 형태로 전달(ex. function({c, a, b, d} ) : 비순서 - 앞으로 개수가 늘어날 가능성이 높을 때
  • 함수의 값을 반환하는 방식
    • 객체를 통한 반환 : 비순서 - 2개 이상 반환시, 사용할 프로퍼티만 사용
    • 배열을 통한 반환 : 보편성 - 이름을 다양하게 활용하고 싶을 때

 

 

3. 자바스크립트 객체 정의 및 사용

  • 객체 명칭 : 객체 = 프로퍼티 집합 = 필드 + 메서드
    • 메서드 표현법은 Default ( a: function() {} ) 과 Shorten ( a: () => {} ) 로 나뉜다.
  • JavaScript는 Java와 같이 객체 사용에 꼭 클래스가 필요한 것은 아니다.
  • 하지만 조금이라도 SOLID나 디자인 패턴같은 객체지향적 요인으로 코드 재사용성을 증대시키기 위해서는 클래스를 사용해야 한다.
    • 클래스를 사용해야하는 이유 → Encapsulation : 단순히 감추는 것이 아닌 독립된 시스템을 구축할 수 있어야 한다.
    • 이처럼 객체 지향 프로그래밍을 얼마나 잘하는 가는 얼마나 클래스(독립된 시스템 구축)를 잘 만드는가로 이어진다.

3-1. 객체 혹은 클래스 내 Getter / Setter

  • Private 변수 : 클래스 필드 앞에 #을 붙인다.
class User {
  #firstname = "Aaron"
  #lastname = "Ryu"

  get fullname() {
    return `${this.#firstname} ${this.#lastname}`
  }

  set fullname(value) {
    [this.#firstname, this.#lastname] = value.split(" ")
  }
}

var user = new User()
user.fullname = "Baron Kim"
console.log(user.firstname)
console.log(user.lastname)
console.log(user.fullname)
Object.getOwnPropertySymbols(user)

 

 

4. 모듈 시스템 : ES Modules(ESM)의 import / export 표현법

자바스크립트 모듈에는 두개의 방식이 존재하나, 프론트엔드에서 ES6 이후 표준화되어 있는 ESM에 대해서만 다뤄보자.

 

4-1. ESM = ES(ECMAScript) Modules : import / export

웹 브라우저(프론트엔드)에서 자바스크립트는 하나의 전역 Scpoe에서 모든 자바스크립트 모듈들이 Scope 공유한다.

 

  1. Named Export 예시 : 고정된 명칭
    export const sum = (x, y) => x + y
    
    import { sum } from './util.mjs'
    console.log(sum(2, 4))​
  2. Default Export 예시 : 명칭 변경 가능
export default (x, y) => x + y

import ssam from './util.mjs'
console.log(ssam(2, 4))


const sum = (x, y) => x + y
export default sum

import ssam from './util.mjs'
console.log(ssam(2, 4));

 

4-2. CJS = CommonJS : require / module.exports

웹 브라우저(프론트엔드)가 아닌 웹 서버(백엔드)에서 자바스크립트 모듈을 쓰려면 파일 단위 모듈화 절실하다.

jQuery 등 다양한 라이브러리 등장 및 웹 서버 자바스크립트 코드량이 많아지면서 변수, 함수의 모듈화 필요

  • Node.js 12 부터 새로운 모듈 시스템으로 ESM 을 채택하긴했지만, 여전히 Node 에선 CJS 가 지배적

모듈 시스템에 대한 요구 등장 ← 과거에는 IIFE, 클로저, 스코프 개념들을 조합하여 모듈을 힘들게 흉내

4-3. 모듈 시스템을 어떤 상황에서 어떤 방법을 사용해야 할까

CJS 는 Node.js(서버) 에 ESM 은 브라우저에 좋다.

  • CJS 를 지원하는것이 중요한 이유 = Node.js 활용한 SSR 사용 서비스를 위해 CJS 지원 필요
  • ESM 을 지원하는것이 중요한 이유 = Tree-shaking 을 지원하는 ESM 이 브라우저 성능에 중요
    • CJS - 모듈 동기 로드 : 빌드타임에 정적 분석 불가 → 런타임에 모듈 관계 파악
    • ESM - 모듈 비동기 로드 : 정적 모듈 의존 강제 = 빌드타임에 Tree-shaking 가능

 

5. 자바스크립트 내 비동기 처리 : Promise 및 Async / Await

5-1. Callback (이때 Callback은 Asynchronous가 아니다.)

함수(콜백)를 파라미터로 넘겨(일급함수) 파라미터를 받은 함수에게 실행권을 넘기는 것

// ex.1
function callback(param1, param2) {
  console.log(param1 + " and " + param2);
}

function caller(callback) {
  callback(arguments[1], arguments[2]);
}

caller(callback, "hello", "goodbye");

// ex.2
function callback(param1, param2) {
  console.log(param1 + " and " + param2);
}

function caller(callback) {
  setTimeout(() => {
		callback(arguments[1], arguments[2])
	}, 2000);
}

caller(callback, "hello", "goodbye");

 

  • Callback 자체로는 동기 혹은 비동기 함수와 관계는 없으니 Callback이 많이 사용되는 곳은 비동기일 뿐
    • 비동기 함수에게 실행권을 넘기기 위해 Callback을 많이 사용할 뿐이지 Callback만으로 비동기는 아니다.

5-2. Callback Hell

Callback 결과값을 순차적으로 연결할 때 발생한다.

  • Callback의 결과가 그 다음 Callback의 실행에 필요한 경우
// ex.1
step1(function (resultfromstep1) {
    step2(resultfromstep1, function (resultfromstep2) {
        step3(resultfromstep2, function (resultfromstep3) {
            step4(resultfromstep3, function (resultfromstep4) {
								console.log(resultfromstep4)
            });
        });
    });
});

// ex.2
step1(function (resultfromstep1) {
    step2(function (resultfromstep2) {
				console.log(resultfromstep1 + resultfromstep2)
        step3(function (resultfromstep3) {
						console.log(resultfromstep2 + resultfromstep3)
            step4(function (resultfromstep4) {
								console.log(resultfromstep3 + resultfromstep4)
            });
        });
    });
});

 

 

5-3. Promise (Callback과 Asynchronous의 합으로 이해하자)

  • Callback 함수(자신의 제어권 넘김) :  Callee = Consumer (파라미터를 받길 기다림)
  • Asynchronous 함수 (Executor) : Caller = Producer (파라미터를 주입)

그래서 Promise를 Producer-Consumer Pattern On Asynchronous 라고 표현

function caller(callee) {
  var produced = producing()
  callee(produced)
}

caller(function callee(produced) {
  consuming(produced)
})

 

Promise의 상태와 그에 따른 콜백

  • Promise는 비동기를 위해 탄생한 개념(Callback + Asynchronous)이기에 상태를 가진다.
    • Promise의 Producing은 성공 / 실패 2개의 상태로 나뉘며, 성공 / 실패에 따른 콜백 설정
  •  [ Pending → Fulfilled(성공, Resolve) / Rejected(실패, Reject) ]
  • 코드 설명 : new Caller((succeded_callback, failed_callback) => {...})
    1.  Callback 형태 (Pseudo)
      function caller(resolve, reject) {
        const produced = producing() // API 호출해줘, 이미지 가져와줘
        if (succeeded) resolve(produced)
        if (failed) reject(produced)
      }
      
      caller(
        function resolve(produced) { consuming(produced) },
        function reject(produced) { consuming(produced) },
      )​
    2. 1. Promise 형태 (Pseudo)
      	new Promise(
      		function caller(resolve, reject) {
      			const produced = producing()
      			if (succeeded) resolve(produced)
      			if (failed) reject(produced)
      		}
      	)
      		.then(function resolve(produced) { consuming(produced) })
      		.catch(function reject(produced) { consuming(produced) })​
    3. 2. Promise 실무에서 사용하는 형태
      	new Promise((resolve, reject) => {
      		const result = getUserInformationAPI()
      		if (result.success) { resolve(result.user) }
      		if (result.failed) { reject({ type: 'No User', message: 'Error Occured' }) }
      	})
      		.then((user) => { console.log(user) })
      		.catch((error) => { console.log(error.message) })​
       
    • 성공은 .then 로 정의된 성공 콜백으로
    • 실패는 .catch 로 정의된 실패 콜백으로
  • ResolveReject
    • 하나의 Promise 객체는 Caller 로 인스턴스화되고
    • Resolve 성공 / Reject 실패 Callback 은 .then 과 .catch 로 주입된다.
      • .then()내부에 Resolve Callback 함수
      • **.catch()** 내부에 Reject Callback 함수

5.4. Promise Hell = Nested Promise

Promise 결과값을 순차적으로 연결할때 발생 (Callback Hell 과 동일)
 
    • Promise 의 결과가 그 다음 Promise 실행에 필요한 경우
step1(value1)
	.then((value2) => {
			step2(value2)
					.then((value3) => {
							step3(value3)
									.then((value4) => {
											console.log(value4)
									})
					})
	})

5.5. Promise Chain

  • 코드 : Promise Hell 해결 방법 1 = Promise Chain 으로 아래와 같이 변환 → 장점 : 한번에 한 값 사용
step1(value1)
		.then((value2) => {
				return step2(value2)
		})
		.then((value3) => {
				return step3(value3)
		})
		.then((value4) => {
				console.log(value4)
		})

 

5.6.1. Async / Await

  • 코드 : Promise Hell 해결 방법 2 = Async / Await 으로 아래와 같이 변환 → 장점 : 모든 값 사용 가능
  • 복습 : Promise 는 Caller (Executor) 와 Callee (Callback) 가 하나의 Promise 객체로 뭉쳐진것
    • Async/Await 은 이 둘을 분리한것 = Produce-Consumer 둘을 똑 뗀것
      • Async 은 Caller 를 정의하는곳에서 사용하고 **async** function caller() {}
      • Await 은 Callee 를 정의하는곳에서 사용된다 const result = **await** caller()

 

5.6.2. Async / Await 쉽게 이해하기

  • async = Promise 상자반환 (Promise.resolve 로 감싸져있으면 바로 반환, 아니면 상자 포장 반환)
  • await = Promise 상자열기 (Promise 객체를 기다렸다가 상자를 열어 내부 값을 반환)
  • async 가 붙은 함수는 반드시 Promise 를 반환하고, Promise 가 아닌 것은 Promise 로 감싸 반환
    • async 가 붙은 함수는 반드시 Promise 를 반환
    • 반환값이 Promise 가 아닌 것은 Promise 로 감싸 반환 (위와 동일)
  • await 키워드를 만나면 Promise 가 처리될 때까지 기다린다.

5.6.3. Promise 내 Caller (Executor) 실행 시점

new Promise 가 생성되는 즉시 Caller (Executor) 함수가 실행 = 즉시 실행

Promise 객체의 3가지 상태

  • 대기 Pending : 약속된 결과 값이 반환(다음 상태로 전이)되지 않은 상태, 초기 상태
    • Non-Pending.finally (Non-Pending 콜백)
      • 이행 Fulfilled : 연산이 성공적으로 완료된 상태 ⇒ .then (성공 콜백)
      • 거부 Rejected : 연산이 실패한 상태 ⇒ .catch (실패 콜백)

5.7. Promise 사용 시 주의할 점

Promise 안에서는 무조건 반환을 위해서는 Resolve 사용, Return 절대 아님

5.8. Promise 에러 처리 방법 & Async / Await 에러 처리 방법

  • Promise 에러 처리 = .catch (실패 콜백)
    • .catch 에서 처리하기 위해서는 Promise 내 Caller 에서 Reject 로 반환해야함

6. 자바스크립트 실무에서 많이 활용되는 ES6+ 문법

자바 버전 : Java 11, Java 17 등 지원하는 문법들이 점차 추가되고 발전

자바스크립트 버전 : 자바스크립트도 자바나 다른 여느 언어와 같이 ECMAScript 으로 매년 새 표준이 등장

  • Async / Await 에러 처리 = Try-Catch
    • Await 에서 에러 처리하려면 어쩔 수 없이 Try-Catch 를 사용해야함
      • Reject 로 단순히 우리가 원하는 값을 반환했을때

 

 

6.1. 객체 관련

  1. 객체 비구조화 Destructure : const address → const { country, city } = address
  2. 객체 프로퍼티 초기화 단축 Property Initializer Shorthand (콜론 제거)
  3. 계산된 프로퍼티명 혹은 메서드명 Computed Named Property / Method
    • 깊은 복사를 지원하기 위해 어떻게 해야할까?
      • 깊이가 깊지않다면 그냥 조금만 고생해서 복사하고, 깊다면 깊은 복사 함수를 구현하여 복제하라
        • 재귀함수를 활용했다. 재귀함수는 코딩테스트에 많이 나오는 개념이야 이해하는법을 익히길
        • 추가 : Array 도 따로 처리 필요

    6.2. 배열 관련

    1. 객체 프로퍼티 / 메서드 혹은 배열 요소 Trailing Comma
    2. 특정 요소 포함 여부 확인 Array.prototype.includes()

    6.3. 함수 관련

    1. Spread SyntaxRest Parameter
      • Spread Syntax : 함수 인자(Argument 전달인자 = 밖) 펼치기 + 배열 및 객체 조작
        • 함수 인자 펼치기 : Argument 전달인자 = 밖
        • 배열 펼치기 : 배열 복사 + 배열 연결 + 배열 요소(Element) 추가
          const arr = ["a", "b"]
          const arr1 = ["c", "d"]
          const arr2 = [...arr, ...arr1] // 두 배열의 요소를 합친 새로운 배열 생성
          
          const arr = ["b", "c"]
          const arr1 = ["a", ...arr, "d"] // 중간에 다른 배열의 요소를 추가하여 새로운 배열 생성
          
        • const arr = ["a", "b", "c", "d"] const arr1 = arr // 이것을 복사라고 말하는 사람은 경계 대상 1호. const arr2 = [...arr] // 배열 복사. 새로운 배열의 생성하되 그 요소들을 arr을 펼쳐서 새로운 배열의 요소로써 채우는 것.
        • 객체 펼치기 : 객체 복사 + 객체 연결 + 객체 프로퍼티(Property) 추가

        • Rest Parameter : 함수 파라미터(Parameter 매개변수 = 안) 펼치기
          • 함수 파라미터 펼치기 : Parameter 매개변수 = 안
    2. 함수 기본 파라미터 값 Default Parameters
    3. 커링을 통한 파라미터 쪼개기 Currying
    4. Exception Handling : Try / Catch / Finally / Throw얕은 복사 : Object.assign()

    6.4. 문자열 및 자료구조 관련

    1. 문자열 포맷팅 String Literal : 백틱을 통한 문자열 직관적 결합
    2. Map : 유일 Key 및 Key-Value 집합
    3. Set : 유일 Key 집합

    6.5. 반복문 및 조건문 관련

    1. for .. of 와 for .. in 의 차이 + forEach
    2. Null Guarding (null or undefined, Null 로 통칭)
      • ?? : Default = 앞엣값이 Null 인 경우 표시할 값 (Nullish coalescing)
      • ?. : Null 이라면 넘겨버린다 Null Cascading 이라고도 부른다
      • ! + !. : Non-null Assertion - 절대 Null 이면 안된다는것을 개발자가 보장하는것
    3. 선택적 노출 / 호출 : && (if 간단히 대체) + ? (Ternary Operator, if-else 간단히 대체)
      • && 의 단점 : 앞의 구문이 false 혹은 undefined 일때 그대로 false 혹은 undefined 를 반환
        • 예시) className 안에 undefined 혹은 false 클래스명이 들어갈 수 있음
    4. Value Comparison Operator == and ===
    5. Type Conversion (명시적 타입 변환) vs Coercion (암묵적 타입 변환)