FE재남 님의 유튜브에서 미세먼지 팁 영상을 정말 재밌게 봤다. 가끔 코딩을 하다보면 아! 이거 어떻게 리팩토링 했던거 같은데? 하면서 유튜브 영상을 다시 돌려보곤 한다. 매번 까먹느니 이번 기회에 정리해보려고 한다.
//기존
async function getProduct(id) {
const res = await fetch(`https://api.escuelajs.co/api/v1/products/${id}`)
const json = await res.json()
return json
}
async function handleChange(e) {
const id = e.target.value
const product = await getProduct(id)
renderProduct(product)
}
//일단 fetch를 분리시켜보자.
async function fetchProduct(id) {
const res = await fetch(`https://api.escuelajs.co/api/v1/products/${id}`)
const json = await res.json()
return json
}
// 중간단계인 요 녀석은 async를 뺄 수 있다. getProduct는 이미 promise를 리턴해 주는 녀석이다.
// 그렇기에 굳이 다시 async/await으로 promise로 반환시켜줄 이유가 없다.
function getProduct(id){
const json = fetchProduct(id)
return json
}
async function handleChange(e) {
const id = e.target.value
const product = await getProduct(id)
renderProduct(product)
}
fetchProduct를 좀 더 개선시켜보자.
function fetchProduct(id) {
return fetch(`https://api.escuelajs.co/api/v1/products/${id}`).then(res => res.json())
}
어라라.. async/await이 또 떨어져나갔다. fetchProduct 함수 또한 그저 fetch의 결과값을 return 시켜주는 함수다. 즉, 이미 응답값이 promise인 것이다. 굳이 다시 promise로 반환시켜줄 이유가 없다.
마지막으로 전달되는 최종 결과물이 promise이기만 하다면, 종착지에서 async/await을 붙여 비동기 처리가 이뤄질 것이다. 중간 다리에서는 전혀 필요가 없어졌다.
한발짝 더 나가면
const fetchJSON = url => fetch(url).then(res => res.json())
const fetchProduct = (id) => fetchJSON(`https://api.escuelajs.co/api/v1/products/${id}`)
역시 async/await은 붙지 않는다.
물론 async/await을 기존처럼 다 붙여 사용해도 동작한다. 문제없이 동작하는게 가장 중요한게 맞다. 다만 필요가 없음에도 붙였으므로 코드를 볼 때 반드시 비동기 처리를 동기적으로 처리한 후에 다음코드로 진행해야 한다는 오해를 불러일으킬 수 있다는 것 또한 주의하자.
코드는 브라우저 창에서 실행했다!
일반함수를 살펴보자.
function Foo(...args) {
console.log(this) // window
if(this !== window) this.args = args
else return args
}
Foo(1, 2) // [1, 2]
생성자 함수는?
function Foo(...args) {
console.log(this) // Foo {}
if(this !== window) this.args = args
else return args
}
const foo = new Foo(3, 4) // Foo {args: Array(2)}
this가 window가 아니기에 foo라는 인스턴스에 프로터피 arg를 만들어서 배열을 담고 있다.
객체 메서드는?
function Foo(...args) {
console.log(this) // {method: ƒ}
if(this !== window) this.args = args
else return args
}
const bar = {
method: Foo
}
bar.method(5, 6) // {args: Array(2), method: ƒ}
this가 bar로 바뀜!
함수를 활용하는 방법이 이리도 많으니... 좋긴 좋다만, function은 사용하지 말자! 가 영상의 주제이니, 뭐가 문제인지 살펴보자.
// 함수를 하나 만들고 그 내부를 살펴보자.
function malza() {}
console.dir(malza)
prototype이 보인다. 이 친구는 생성자 함수와 관련이 있다. 우리가 함수를 그저 일반 함수로써 사용할 목적이라면 전혀 필요가 없는 친구다.
또한 경우에 따라 this가 바인딩 되는것 또한 문제가 된다. this 바인딩 또한 함수로 사용하고자 할 때는 불필요한 정보다.
this는 실행컨텍스 생성 시점에 this를 바인딩하기 하기에 동적이다.
목적에 따른 함수 사용. 좋은 방법이 있을까?
기존의 ES5까지의 생성자 함수 사용법을 살펴보자.
function Foo(...args) {
if(this !== window) this.args = args
else return args
}
Foo.prototype.getArgs = function() {
return this.args
}
const foo = new Foo(1,2)
foo.getArgs() // [1, 2]
console.dir(foo) // 아래 사진 참고
// ============================================
class Bar {
constructor(...args) {
if(this !== window) this.args = args
else return args
}
getArgs() {
return this.args
}
}
const bar = new Bar(3, 4)
bar.getArgs() // [3, 4]
console.dir(bar) // 아래 사진 참고
getArgs의 색상이 다르다. 그래, 뭐 일단 다르긴 하다. 그렇다면 뭐가 다른걸까?
for(let prop in foo) {
console.log(prop)
}
// args
// getArgs
생성자 함수로 만들어진 foo의 property를 확인하고자 for문을 돌렸다. args, getArgs다 출력되는 것을 확인할 수 있다.
여기서 이제 property의 Enumerable 개념이 나온다. 말 그대로 열거 가능한 속성이다. 프로퍼티의 enumerable값이 true라면 for...in 문에서 찍혀 나오는 것이다.
// for문을 돌릴때 prototype의 프로퍼티는 받고 싶지 않다면 조건문을 걸어줘야 한다.
for(let prop in foo) {
if(foo.hasOwnProperty(prop)) console.log(prop) // args
}
class로 생성한 bar를 for문 돌려보면?
for(let prop in bar) {
console.log(prop) // args
}
getArgs는 어디갔는지 안보인다! 생성자 함수와 다르게 자동으로 enumerable값이 false가 된 것이다.
생성자 함수처럼 조건문을 걸지 않아도 객체 인스턴스 자신에게만 있는 값만 순회를 돌아서 보여준다.
이번엔 Foo와 Bar 자체를 찍어보자.
Foo는 arguments, caller가 null로 표시되고 있다. Bar는 (...) 되어있는 곳에 마우스를 올려보면 속성 getter를 호출하라고 나온다.
TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them at Bar.invokeGetter
기존의 js에서는 가급적 동작하게끔 설계되었다면, 최신 js는 친절하게 에러 메세지를 던져줘서 최대한 빠르게 에러를 포착해 내도록 유도한다.
class는 그 목적에 따라 사용하도록 유도하고 있다. 생성자 함수를 사용할 목적이라면 class를 사용하면 된다.
Arrow function 인 경우 함수 자체를 찍어보자.
function foo(...args) {
console.log(args)
}
const bar = (...args) => {
console.log(args)
}
console.dir(foo) // 이 경우는 위 사진에 있다.
console.dir(bar)
class의 경우도 에러를 던져줬다. arrow function 또한 에러를 던져준다. 또한 function 키워드에는 있는 prototype이 arrow function에는 없는것을 확인할 수 있다. 또한 this 바인딩을 하지 않는다.(고려하지 않아도 된다.) 생각해보면 그냥 함수로써의 역할을 할 것이라면 this를 신경 쓸 이유가 없다.
this를 신경써야겠다면 객체 메서드 선언 방식을 쓰면 된다. 바로 아래서 살펴보자.
메서드 축약형이라고도 불린다.
// 기존의 방식
const obj1 = {
name: 'malza',
method: function() {
console.log(this.name)
}
}
// 축약형
const obj2 = {
name: 'malza 2세',
method() {
console.log(this.name)
}
}
console.dir(obj1.method)
console.dir(obj2.method)
역시 축약형은 prototype이 없으며, 에러를 던져준다. class에서도 확인했고, arrow function에서도 확인했다. 다만? 축약형은 함수로써의 목적을 가지고 있기 때문에 this 바인딩이 된다.
obj1.method() // malza
obj2.method() // malza 2세
축약형 이 친구는 생성자 함수로 사용이 안된다.
console.log(new obj1.method()) // method {}
console.log(new obj2.method()) // Uncaught TypeError: obj2.method is not a constructor
function* generator() {
yield 1
yield 2
}
console.dir(generator)
const gene = generator()
console.log(gene.next().value) // 1
console.log(gene.next().value) // 2
console.log(gene.next().value) // undefined
함수 형태의 generator에서만 function키워드가 필요하다. 객체 안에서 generator를 만든다면 축약형으로 가능하다.
const obj = {
val: [1, 2],
*gene() {
yield this.val.shift()
yield this.val.shift()
}
}
const gene = obj.gene()
console.log(gene.next().value) // 1
console.log(gene.next().value) // 2
console.log(gene.next().value) // undefined
너무 신기해! 다음 영상들도 이번주 안에 정리한다!