JavaScript 자주 하는 실수부터 콜백 지옥까지

Yudrey·2022년 4월 17일
0

패스트캠퍼스 강의를 정리한 내용입니다.
"The RED : 프론트엔드 Back to the Basics : 지속 가능한 코드작성과 성능 향상법 by 김태곤"

자바스크립트의 현재와 미래

ES6

  • ECMAScript 6th edition 부터는 연도 표기
  • 2015년부터 해마다 새 명세가 갱신
  • 보통 'ES6'라고 하면 'ES6와 그 이후'를 의미

ESM으로의 전환

  • CommonJS, AMD가 ESM으로 전환
  • ESM
    • HTML5 표준에 포함됨
    • Node 12버전부터 ESM 방식 지원
    • Webpack, Rollup 같은 대다수 번들러의 기본 모듈 로딩 방식은 ESM
    • 별도 라이브러리가 필요한 다른 방식과 달리 ESM 방식은 웹 브라우저에서 네이티브로 지원
      → 아직은 브라우저 호환성 문제가 있으나 그럼에도 널리 사용될 것으로 전망
    • <script> 태그도 ESM을 지원함
      <script> 태그에서 type을 모듈로만 써두면 모듈 방식을 사용해서 스크립트 파일을 불러올 수 있음

한 때 잠깐 사용되었던 AMD 방식은 이제는 번들러의 영향으로 널리 사용되지 않지만, NodeJS의 기본 모듈 로딩 방식인 CommonJS와 ESM 방식은 아직도 널리 사용되고 있다.

자바스크립트 미래

  • 브라우저, Node 발전에 따라 babel과 같은 트랜스파일러의 필요성 하락
  • JS가 아닌 JS: TS, WebASM, Rust 등
    • TypeScript
      • 자바스크립트의 확장판
      • 강한 타입 설정으로 안전한 프로그래밍 가능
      • 실행 전 정적 분석을 통해 에러 탐지 가능
    • Deno
      • Rust(모질라에서 만든 언어)로 제작됨
    • WebASM(WebAssembly)
      • 자바스크립트 코드를 특별한 규칙과 코드로 작성하면 parser가 빠르게 인식하고 동작
  • 새로운 번들러 등장: Webpack의 독주는 언제까지?


어휘적 환경

Lexical Environment

정의

  • 변수나 함수 등의 식별자를 정의할 때 사용되는 명세
  • 중첩된 어휘적 환경에 기반하여 동작함
  • Environment Record와 outer 속성을 포함
    - outer는 자기 자신보다 밖에 있는 Lexical Environment를 참조하는데, global 보다 밖에 있는 것은 없으므로 global에서 outer는 항상 null

관련 문법

  • 함수 선언(Function declaration)
  • 블럭문(Block statement)
  • Try~Catch문의 Catch 절

종류

  • 전역 환경(global environment)
  • 모듈 환경(module environment)
  • 함수 환경(function environment)

실행 컨텍스트

Execution Context

정의

  • 자바스크립트 코드가 실행되는 환경
  • 모든 JS 코드는 실행 컨텍스트 내부에서 실행됨

종류

  • 전역 실행 컨텍스트(global execution context)
    • 모든 스크립트 코드는 전역 실행 컨텍스트 내에서 실행됨
  • 함수 실행 컨텍스트(functional execution context)
  • eval 실행 컨텍스트(eval function execution context)
    자바스크립트에 eval이라는 함수가 있는데 문자열로 된 자바스크립트 코드를 전달하면 그대로 실행되는 환경
    → 속도나 보안 등 단점이 많으므로 이 항목은 제외!

*실행 컨텍스트는 함수를 실행하면서 발생하고 함수 실행이 종료되면 사라짐


어휘적 범위

Lexical Scope

  • 같은 범위 혹은 그보다 안쪽의 코드에서 바깥 영역에는 접근할 수 있지만, 그 반대는 성립하지 않음
  • 어휘적 범위는 함수 선언, 블럭문(if, for, while), Try-Catch의 catch 절에서 구분됨
<script>
// 아래 코드 실행 시 Uncaught Reference 에러 발생
function hello() {
	{
    	const greeting = '안녕하세요';
    }
    console.log(greeting);
}
hello();
</script>

클로져

Closure

  • 처음 만들어질 때의 어휘적 범위를 그대로 유지한 함수
  • 어휘적 범위 바깥에서 해당 범위에 접근 가능
<script>
function hello() {
	const greeting = '안녕하세요';
    
    // 함수 객체를 반환    
    return function() {
    	console.log(greeting);
    };
}

// hello() 함수를 실행하면 hello() Execution Context가 만들어짐
// 전역 실행 컨텍스트에 반환된 함수 객체 저장
const say = hello();

say();
</script>

엄격한 모드

  • Strict mode
  • 문법과 런타임 동작을 모두 검사하여 에러 검출

참고: https://beomy.tistory.com/13

진입 방법

  • "use strict": 전역 영역, 함수 내 표기
  • ES2015 모듈 사용 (자동 적용)
    → Webpack 같은 번들러를 사용하면 자동으로 'use strict'가 추가됨

*Babel이나 번들러없이 Vanilla 자바스크립트를 사용시에는 직접 추가해서 엄격한 모드를 기본으로 사용하는 것을 추천

일반 모드와의 차이

  • 조용한 에러 대신 명시적 에러 발생
  • JS 엔진 최적화를 어렵게 하는 실수 방지
  • 향후 ES2015에 포함될 예약어/문법 대비
<script>
"use strict"

// 1.선언하지 않은 변수에 값을 할당할 수 없음
str = "hello, world";

// 2.읽기 전용 전역 객체에 값을 할당하면 에러 발생
// (일반 모드에서는 조용한 에러로 처리됨)
var undefined = 5;
var Infinity = 5;
NaN = "Wow";

// 3.지울 수 없는 값을 지우려고 하면 에러가 발생
// (일반 모드에서는 조용한 에러로 처리됨)
delete Object.prototype;

// 4.함수 파라미터에 중복 이름을 사용할 수 없음
function sum(a, a, c) {
   console.log(a + a + c);
}
sum(1, 2, 3);

// 5. ES5의 8진수 리터럴 사용 불가 (애초에 표준도 아니었음)
const hex = 0xff;   // 16진수
const octal = 020;const octal = 0o20;// ES2015의 8진수 표현 방식
console.log(octal);
</script>

엄격한 모드 외의 엄격함

  • JS의 이상한 동작은 독특한 형변환도 원인
  • 일치 연산자(===) 사용 습관화
  • 명시적 형변환 활용
<script>
// 일치 연산자를 사용하지 않은 경우

1 == 1				// → true
1 == '1'			// → true
1 == 2				// → false

'' == false			// → true
[] == false			// → true, []는 falsy 값도 아님
null == undefined	// → true



// 일치 연산자를 사용한 경우

1 === 1				// → true
1 === '1'			// → false
1 === 2				// → false

'' === false		// → false
[] === false		// → false
null === undefined	// → false

</script>

비동기 자바스크립트

Asynchronous JavaScript

비동기 처리는 필연

  • 기능 대부분을 외부 API에 의존하고 있기 때문
  • 외부 API를 호출하고 결과를 콜백으로 전달 받음

자바스크립트의 동작 원리

  • 자바스크립트는 싱글 스레드 언어
    *싱글 스레드: 한번에 한가지 동작만 처리함
  • 이벤트 루프와 스택을 통해 스케줄링
  • UI 업데이트, 사용자 이벤트도 모두 같은 스레드에서 처리

이벤트 루프(Event Loop)

  • 자바스크립트의 동시성(concurrency) 처리 모델의 기본 원리
  • 코드를 실행하고, 이벤트를 처리하고 다음에 처리할 동작을 정하는 것 모두 이벤트 루프가 기반이 됨


콜백 지옥은 해결된 문제

  • 더 우아한 비동기 처리 방법: Promise, async, await
  • 함수 분리 등의 코딩 패턴 적용
<script>
// 콜백 지옥 예제
fs.readdir(source, function(err, files){
	if(err) {
    	console.log('Error finding files: ' + err)
    } else {
    	files.forEach(function (filename, fileIndex) {
        	console.log(filename)
            gm(source + filename).size(function (err, values) {
            	if(err) {
                	console.log('Error identifying file size: ' + err)
                } else {
                	console.log(filename + ' : ' + values)
                    aspect = (values.width / values.height)
                    widths.forEach(function (width, widthIndex) {
                    	height = Math.round(width / aspect)
                        console.log('resizing ' + filename + 'to ' + height + 'x' + height)
                        this.resize(width, height).write(dest + 'w' + width) + '_' + filename, function(err) {
                        	if(err) console.log('Error writing file: ' + err)
                        })
                    }.bind(this))
                }
            })  
        })
    }
})
</script>

❗ 위의 예제 코드 개선 TIP

  • "fail fast". 에러가 발생하거나 실패 조건에 도달했을 때 애플리케이션이나 함수를 빠르게 종료하는 방법
  • 별도 함수로 로직 분리. 익명 함수 부분을 별도 파일로 분리
    files.forEach 부분 → resizeSingleImage(),
    widths.forEach 부분 → writeImageTo (arrow function 사용 고려)
  • 별도 파일로 모듈 분리. 공통적으로 사용할 수 있는 부분이 있다면 다른 모듈로 분리해두기
<script>
// 위의 콜백 지옥 예제를 개선한 코드
function writeImageTo(aspect, widthIndex, width) {
    height = Math.round(width / aspect)
    console.log('resizing ' + filename + 'to ' + height + 'x' + height)
    this.resize(width, height).write(dest + 'w' + width) + '_' + filename, function(err) {
      	if(err) console.log('Error writing file: ' + err)
    })
}

function resizeSingleImage(filename) {
  console.log(filename)
    gm(source + filename).size(function (err, values) {
        if(err) {
            console.log('Error identifying file size: ' + err)
            return;
        }

        console.log(filename + ' : ' + values)
        aspect = (values.width / values.height)
        widths.forEach(writeImageTo.bind(this, aspect, widthIndex));
    })  
}

fs.readdir(source, function(err, files){
    if(err) {
        console.log('Error finding files: ' + err)
        return;
    } 

    files.forEach(resizeSingleImage);
})
</script>

Promise, async, await

1. Promise: 비동기 처리를 위한 객체

  • 세 가지 상태: 대기(pending), 이행(fulfilled), 거부(rejected)
  • 비동기 처리 후 뒤의 두 가지 상태(then, catch) 반환
  • 성공 시 .then() / 실패 시 .then() 또는 .catch()
  • 한 번 상태가 결정된 Promise의 상태는 변경 불가
  • Promise.resolve, Promise.reject는 상태가 결정된 Promise 반환
  • Promise의 정적 메소드를 통해 다중 Promise 처리

mdn Promise 명세: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

2. async/await: 보다 편리한 비동기 처리

  • async 함수는 항상 Promise를 반환
  • async 함수에서 성공은 return, 실패는 에러를 throw 함
  • await과 함께 비동기 함수를 실행하면 마치 동기식인 듯 동작
<script>
async function getName() {
	return '김태곤';
}

// 위의 코드는 아래와 같음

function getName() {
	return Promise.resolve('김태곤');
}
</script>

비동기 처리 예제: https://codesandbox.io/s/fancy-river-ltjcu?file=/src/index.js

profile
Frontend Developer

0개의 댓글