[DAY 12] VanillaJS를 통한 JS 기본 역량 강화(4)

63949 단어 TIL데브코스TIL

[1] ES6 Module 사용하기

module을 사용하는 경우, script에 type=”module”을 적어줘야 합니다!

[1-1] import

import는 export 키워드로 내보내진 변수, 함수 등을 불러올 수 있는 키워드입니다.

  • module-name 내에 export default로 내보내진 것을 가져옵니다.
import default Export from "module-name";

  • module-name 내에서 export 된 모든 것을 모두 가져옵니다.
  • as 이후 이름은 중복되지만 않으면 자유롭게 정할 수 있습니다.
import * as allItems from "module-name";

  • module-name 내에서 export 된 것 중에 특정 값만 가져옵니다.
import { loadItem } form "module-name";

  • module-name 내에 export 된 것 중에 특정 값만 이름을 바꿔서 가져옵니다.
import {
	loadItem as loadSomething
} from "module-name";

  • export defult 된 것과 개별 export 된 것을 한 번에 가져올 수 있습니다.
import default Function, {
	loadItem
} from "module-name";

  • 별도의 모듈 바인딩 없이 불러오기만 합니다.
  • 불러오는 것만으로 효과가 있는 스크립트의 경우 사용됩니다.
import "module-name"; 

→ 이렇게 import를 사용하면 스크립트의 의존성을 훨씬 간편하게 관리할 수 있습니다!!

[1-2] Simple Todo App에 적용하기

이렇게 하면 무엇이 좋은 걸까요?🤔

  • 각 JS별로 사용되는 모듈을 명시적으로 import 해오기 때문에, 사용되거나 사용되지 않는 스크립트를 추적할 수 있습니다.
  • script 태그로 로딩하는 경우 불러오는 순서가 중요하지만, import로 불러오는 경우 순서는 무관합니다.
  • script src로 불러오는 것과 다르게, 전역오염이 발생하지 않습니다.

[+] import를 사용하려면 웹 서버가 필요합니다.

→ 하지만 지금은 serve 모듈로 로컬 웹서버를 띄워서 진행하기 때문에 상관없습니다!

[+] 뭔가 잘 안 된다면 일단 from 이후 모듈 이름 맨 뒤에 .js를 잘 적었는지 확인해보세요!


[2] 비동기 다루기 - callback

[2-1] 비동기 처리란?

특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고, 다음 코드를 먼저 실행하는 JS의 특성을 이야기합니다!

[2-2] 첫 번째 예시! addEventListener 함수

  • addEventListener 함수는 두 번째 인자로 넘겨진 함수는 바로 실행되지 않고, 이벤트 리스너가 정의한 이벤트가 발생할 때 실행됩니다.
function onButtonClick() {
	alert('눌렀군요!');
}
document.querySelector('.save-button').addEventListener('click', onButtonClick);

[2-3] 두 번째 예시! setTimeout과 setInterval

  • setTimeout과 setInterval은 첫 번째 인자로 넘겨진 함수는 바로 실행되지 않고, setTimeout 또는 setInterval의 시간만큼 지난 후에 실행됩니다.
  • setTimeout의 시간을 0 또는 지정하지 않아도 바로 실행되지 않습니다!
    → 함수가 모두 실행된 이후에 실행됩니다.
function work() {
	console.log('work!');
}
setTimeout(work, 1000);
setTInterval(work, 5000);

[2-4] 세 번째 예시! XMLHttpRequest(XHR)

  • 데이터를 비동기로 요청하고, 요청 후의 동작을 비동기로 처리합니다.
function request(url, successCallback, failCallback) {
  const xhr = new XMLHttpRequest();
  xhr.addEventListener("load", (e) => {
    if (xhr.readyState === 4) {
      successCallback(JSON.parse(xhr.responseText));
    } else {
      failCallback(xhr.statusText);
    }
  });
  xhr.addEventListener("error", (e) => failCallback(xhr.statusText));

  xhr.open("GET", url);
  xhr.send();
}

  • 앞에서 정의한 request 함수를 이용해서 다음의 시나리오를 처리하려면??
    1. https://~~/todos 를 조회합니다.
    2. 조회를 성공했다면, todos 목록중 isCompleted가 true인 todo의 id로 comments를 불러옵니다.
      https://~~/comments?todo.id={todoId}
    3. 2에서 불러온 데이터 중 content를 화면에 그립니다.
// async
const API_ENDPORT = "https://~~";

request(`${API_ENDPOINT}/todos`, (todos) => {
  const completeTodo = todos.find((todo) => todo.isCompleted);

  if (completeTodo) {
    request(`${API_ENDPOINT}/comments?todo.id=${completedTodo.id}`, (comments) => {
        comments.forEach((comment) => console.log(comment.content));
      });
  }
});

→ 이런 경우, 일반적인 callback 패턴인 경우는 callback이 중첩됩니다.

그런데 만약 순차적으로 처리해야 하는 비동기 작업이 더 많아지면 어떻게 될까요?
→ 그것이 바로 유명한 callback hell 입니다.

  • XMLHttpRequest의 경우 비동기를 포기하고, 동기 방식으로 동작하게 되면 아래처럼도 가능합니다!
function syncRequest(url) {
  const xhr = new XMLHttpRequest();

  xhr.open("GET", url, false); // 동기방식으로 처리하도록 false를 넣어줍니다!
  xhr.send();

  if (xhr.status === 200) {
    return JSON.parse(xhr.responseText);
  } else {
    throw new Error(xhr.statusText);
  }
}
// sync
const API_ENDPORT = "https://~~";

const todos = syncRequest(`${API_ENDPOINT}/todos`)
const completeTodo = todos.find((todo) => todo.isCompleted);

if (completeTodo) {
	const comments = syncRequest(`${API_ENDPOINT}/comments?todo.id=${completedTodo.id}`)
  comments.forEach((comment) => console.log(comment.content));
}

→ 코드의 depth가 깊어지지 않아, 가독성도 좋고 이해하기도 더 편합니다.
하지만 sync 방식을 사용하게 되면, 요청 후 응답이 오기 전까지 브라우저가 굳어버립니다.
(만약 API 조회에 10초가 걸린다면 10초간 브라우저가 먹통이 되버릴 것입니다.)


[3] Promise

[3-1] Promise란?

Promise는 비동기 작업을 제어하기 위해 나온 개념으로, callback hell에서 어느 정도 벗어날 수 있게 해줍니다.

promise로 정의된 작업끼리는 연결할 수 없으며, 이를 통해 코드의 depth가 크게 증가하지 않는 효과가 있습니다.

  • Promise 만들기
const promise = new Promise((resolve, reject) => {
	// promise 내부에서 비동기 상황이 종료될 대, resolve 함수 호출
	// promise 내부에서 오류 상황일 때, reject 함수 호출
})

  • Promise에서는 then을 이용해 비동기 작업 이후 실행할 작업을 지정합니다.
function asyncPromiseWork() {
	// some code ...
	return new Promise((resolve, reject) => {
		// some code ...
		return resolve('complete')
	})
}
// then의 result에는 resolve를 호출하면 넘긴 complete가 들어있음
asyncPromiseWork().then(result => console.log(result))

  • Promise의 then 내에서 promise를 return할 경우 이어집니다.
promiseWork()
	.then(result => {
		return pormiseNextWork(result)
	}).then(result => {
		return promiseThirdWork(result)
	}).then(result => {
		return promiseFinalWork(result)
	})

  • Promise chain 중 작업이 실패했을 경우, .catch로 잡을 수 있습니다.
  • catch를 중간에 넣고 이후 작업을 연결해도 동작합니다.
  • catch를 넣지 않을 경우, promise chain 중 에러가 발생했을 때 chain이 멈추니 가급적 넣는게 좋습니다.
promiseWork()
	.then(result => {
		return promiseErrorWork(result)
	}).then(result => {
		return promiseThirdWork(result)
	}).then(result => {
		return promiseFinalWork(result)
	}).catch(e => {
		alert('저런! 에러가 발생했네용')
	})

  • 성공과 실패 여부 상관없이 호출해야 하는 코드가 있다면 finally에서 처리합니다.
promiseWork()
	.then(result => {
		return promiseErrorWork(result)
	}).then(result => {
		return promiseThirdWork(result)
	}).then(result => {
		return promiseFinalWork(result)
	}).catch(e => {
		alert('저런! 에러가 발생했네용')
	}).finally(() => {
		alert('어쨌든 작업은 끝났습니닷')
	})

  • 기존의 callback 함수를 promise 형태로 만들 수 있습니다.
  • resolve를 작업이 끝나는 순간에 호출하면 됩니다.
const delay = (delayTune) => new Promise((resolve) {
	setTimeout(resolve, delayTime)
})

delay(5000)
	.then(() => {
		doSomething()
		return delay(3000)
	).then(() => {
		console.log('complete!!')
	})

[3-2] Promise 내장함수

  • Promise 처리된 delay와 조합하기
request(`${API_END_POINT}/todos`)
	.then((data) => {
		this.setState({
			isLoading: flase,
			todoList: data,
			comments: []
		})
		return delay(5000)
}).then(() => {
	console.log('completed!!')
})

  • Promise.all(iterable) : 여러 promise를 동시에 처리할 때 유용합니다.
const promise1 = delay(1000)
const promise2 = delay(1000)
const promise3 = delay(1000)

Promise.all([promise1, promise2, promise3]).then(() => {
	// promise1, promise2, promise3이 모두 처리된 이후 호출
})

  • Promise.race(iterable) : 여러 promise 중 하나라도 resolve 혹은 reject 되면 종료됩니다.
function getRandomInt(min, max) {
	min = Math.ceil(min);
	max = Math.floor(max);
	return Math.floor(Math.random() * (max - min)) + min;
}

const promises = [1, 2, 3, 4, 5].map( n => {
	const delayTime = getRandomInt(1000, 5000)
	return new Promise(resolve => {
		setTimeout(() => {
			console.log(`${n}번 고양이 완주!`)
			resolve(`${n}번 고양이 승리!`)
		},delayTime)
	})
})
Promise.race(promises).then(message => console.log(message))

  • Promise.any(iterable) : 여러 promise 중 하나라도 resolve 되면 종료됩니다.
function getRandomInt(min, max) {
	min = Math.ceil(min);
	max = Math.floor(max);
	return Math.floor(Math.random() * (max - min)) + min;
}

const promises = [1, 2, 3, 4, 5].map( n => {
	const delayTime = getRandomInt(1000, 5000)
	return new Promise(resolve => {
		if(n===1) {
			return reject(`${n}번 고양이 기권!`)
		}
		setTimeout(() => {
			console.log(`${n}번 고양이 완주!`)
			resolve(`${n}번 고양이 승리!`)
		},delayTime)
	})
})

  • Promise.allSettled(iterable) : 여러 promise들이 성공했거나 실패했거나 상관 없이 모두 이행된 경우 처리할 수 있습니다.
function getRandomInt(min, max) {
	min = Math.ceil(min);
	max = Math.floor(max);
	return Math.floor(Math.random() * (max - min)) + min;
}

const promises = [1, 2, 3, 4, 5].map( n => {
	const delayTime = getRandomInt(1000, 5000)
	return new Promise((resolve, reject) => {
		if(n % 2 === 0) {
			return reject(`${n}번 고양이 완주 실패 T_T`)
		}
		setTimeout(() => {
			resolve(`${n}번 고양이 승리!`)
		},delayTime)
	})
})

  • Promise.resolve : 주어진 값으로 이행하는 Promise.then 객체를 만듭니다. 주어진 값이 Promise인 경우 해당 Promise가 반환됩니다.
const cached = {
	'jieun' : 'bassist'
}

const findMember = (memberName) => {
	if (cached[memberName]) {
		return Promise.resolve(cached[memberName])
	}

 return rerquest(`/members/${memberName}`).then((member) => {
		cache[member.memberName] = memberName
		return memberName
	})

findMember('jieun').then((memberName) => console.log(memberName)

  • Promise.reject : 주어진 값으로 reject처리된 Promise.then 객체를 만듭니다. 주어진 값이 Promise인 경우 해당 Promise가 반환됩니다. → 사용할 일이 거의 없을 겁니당


[4] async, await

Promise가 callback depth를 1단계로 줄여주긴 하지만, 여전히 불편한 것은 사실입니다.
여기에 async, await를 이용하면 Promise를 동기 코드처럼 보이게 할 수 있습니다.
물론 실행은 여전히 비동기로 실행됩니다!

const delay = (delayTime) => {
	return new Promise(resolve => setTimeout(resolve, delayTime))
}
// promise 사용
const work = () => {
	console.log('work run')
	delay(1000)
		.then(() => {
			console.log('work 1 compelet.')
			return delay(1000)
		})
		.then(() => {
			console.log('work 2 compelet.')
			return delay(1000)
		})
		.then(() => {
			console.log('work 3 compelet.')
			return delay(1000)
		})
		.then(() => {
			console.log('work all complete!')
		})
	consoel.log('work running...')
}
work()
// async, await 사용
const work = () => {
	console.log('work run')

	await delay(1000)
	console.log('work 1 compelet.')

	await delay(1000)
	console.log('work 2 compelet.')

	await delay(1000)
	console.log('work 3 compelet.')

	await delay(1000)
	console.log('work all complete!')
}
work()

  • 다음과 같이 두 가지 방식이 있습니다.
async function asyncFunction () {
	const res = await request(...)
}
const asyncFunction = async () {
	const res = await request(...)
}

  • async 키워드 함수가 붙은 함수는 실행 결과를 Promise로 감싸서 출력합니다.

→ async 키워드는 결국 promise 내에서 돌아간다는 것이 중요합니다!

  • 기본적으로 await는 async로 감싸진 함수 스코프에서만 사용이 가능했지만, top level await가 등장하여 top level에서도 사용 가능합니다!

좋은 웹페이지 즐겨찾기