최근에 생성자, 클로저에 대한 개념을 배웠습니다.
생성자 함수의 형태를 가지며, new
키워드와 함께 사용합니다. 새로운 객체를 할당하기 때문에 생성자 함수 내부에서는 this
를 써주어 해당 프로퍼티가 새로운 식별자를 가리킬 수 있도록 해줍니다.
클로저는 함수를 return
하는 중첩함수를 사용할 때, 내부 함수(return하는 함수)가 외부 함수 내부의 변수를 참조할 때 생깁니다. 내부 함수를 return
해줄 때 호출된 외부 함수는 메모리 상에서 사라지지만, 렉시컬 스코프 특징으로 인해 이미 사라져야 할 변수들을 계속 참조합니다. 이런 변수들은 내부 함수에 값을 바꿔주는 별도의 장치가 없다면 접근할 수 없습니다. 말그대로 폐쇄된 상태를 가집니다.
이해한 것을 토대로 개념을 써서 틀린 부분이 있을 수도 있습니다!
이 두 개념이 새로 배우는 것인데도 어디에 활용할 수 있을지 생각하면서 재미있게 코드를 짜다 보니 빠르게 익숙해졌습니다ㅎㅎ
실습 내용은 단순히 음식을 랜덤으로 출력하는 robot들을 만드는 것입니다. 이를 위해서 RobotFactory라는 생성자를 만들고, 인스턴스를 생성할 땐 배열을 값으로 받습니다.
저는 여기에서 더 어렵게 가보았습니다. 음식과 음식의 재고를 같이 입력받는다면 어떨까 했습니다. 재고에 맞게 음식을 출력을 하는데, 랜덤하게 출력까지 해주는 걸로ㅎㅎ
문제: 음식과 재고를 배열로 입력받아 재고에 맞춰 음식을 랜덤으로 출력하고, 남은 음식들을 확인할 수 있는 생성자 만들기
입력: 배열 형태, ex) ["피자", 1, "햄버거", 3, "라면", 2]
출력:
serveFood()의 경우: 피자 (재고에 맞춰 랜덤하게 출력)
remainFoods()의 경우: 햄버거/3 라면/2 (재고가 0인 것을 제외하고 보여줌)
//생성자 함수는 아니지만, 새로운 인스턴스를 반환해주기 때문에 대문자로 시작했다.
function RobotFactoryStore(foodNames){
//closure ~ 내부 로직을 보여주지 않기 위해 별도로 함수를 만들어준다.
const getFood = () => {};
const countFood = () => {};
//generator function
function Robot(){
this.serveFood = getFood;
this.remainFoods = countFood;
}
//새로운 인스턴스를 반환해준다.
return new Robot();
}
내부에서 편하게 사용하기 위해 다음과 같이 바꿔주었습니다.
//음식 종류 파악하기
const foodList = foodNames.filter((e) => typeof e === "string");
//음식 재고 파악하기
const foodStore = foodNames.filter((e) => typeof e === "number");
//모든 음식 재고 파악하기
let allFoodCount = foodStore.reduce((a, c) => (a += c), 0);
처음에 제대로 입력을 받았다면, 음식의 종류와 재고는 같은 인덱스값을 가집니다!
여기에서 추가를 한다면, 예외처리로 같은 음식이 여러번 입력된 경우와, 재고가 표시되지 않고 음식만 입력한 경우를 모두 처리하면 좋을 듯 합니다.
처음에는 Map을 사용하기 위해 객체로 받으려고 했지만, 아직 객체가 익숙하지 않아 배열로 처리해주었습니다!
랜덤하게 음식을 출력합니다. 이전 음식과 겹치지도 않게 해주었습니다!
let randomNum = parseInt(Math.random() * foodList.length);
let prevNum = -1;
prevNum
으로 현재 랜덤한 인덱스와 비교하기 때문에, 인덱스에 없는 음수값으로 초기화했습니다. 또한, foodList의 길이로 랜덤한 인덱스를 생성하도록 하였습니다. 음식의 종류의 개수에 맞추기 위해서 입니다.randomNum
과 prevNum
을 비교해야 합니다. 그리고 현재 재고가 있는지도 파악해야 합니다.while
문을 사용했습니다.//랜덤한 수와 기존 출력한 음식 비교
//이전 인덱스와 랜덤한 수가 같거나, 재고가 남아있지 않은 경우
while (prevNum === randomNum || foodStore[randomNum] === 0) {
//다시 랜덤한 수를 생성
randomNum = parseInt(Math.random() * foodList.length);
}
여기에서 prevNum
과 randomNum
의 경우 함수가 종료되도 기존 값을 가지고 있어야 하기 때문에, 함수 외부에 변수를 선언해줘야 합니다~!
//현재 랜덤한 인덱스는 다음 함수가 호출될 때 이전 인덱스로 기록
prevNum = randomNum;
//음식의 재고를 하나 줄인다.
foodStore[randomNum]--;
//전체 음식의 재고도 하나 줄인다.
allFoodCount--;
//최종 반환
return foodList[randomNum];
getFood()
를 만들었습니다! const getFood = () => {
//재고가 없는 경우 + 모든 음식이 떨어진 경우
if (allFoodCount === 0) return "재고없음";
//하나의 음식만 남은 경우
if (foodStore.filter((e) => e != 0).length === 1) prevNum = -1;
while (prevNum === randomNum || foodStore[randomNum] === 0) {
randomNum = parseInt(Math.random() * foodList.length);
}
prevNum = randomNum;
foodStore[randomNum]--;
allFoodCount--;
return foodList[randomNum];
};
재고가 남은 음식이란 건 foodStore
배열의 값이 0보다 크다는 의미입니다. 따라서 filter()
를 사용해서 0보다 큰 값들만 뽑아줍니다. 그리고 해당 배열의 길이가 0이라면 즉, 남은 재고가 없다면 바로 재고없음을 반환해줍니다.
const countFood = () => {
const remainFoodList = foodStore.filter((e) => e > 0);
if (remainFoodList.length === 0) return "재고없음";
let list = "";
foodStore.forEach((e, i) => {
if(e != 0) list += `${foodList[i]}/${e} `;
});
return list;
};
이렇게 쓰고 나니 filter()
를 사용한 게 좀 걸립니다. foodStore
의 forEach()
내부에서도 조건이 한 번 더 들어가야 하니, 이 걸 이용해 개선할 수 있을 것 같습니다.
const countFood = () => {
let list = "";
foodStore.forEach((e, i) => {
if(e != 0) list += `${foodList[i]}/${e} `;
});
if(list === "") return "재고없음"
return list;
};
이렇게 글을 쓰니 개선할 점도 찾고 좋네요ㅎㅎ
list
에 0이 아닌 경우에만 문자열을 연산하니, 모두가 0이라면, 즉 재고가 없다면 list
는 처음과 같은 빈문자열로 남습니다. 이걸 이용하여 조건을 간단하게 바꾸었습니다. 혹은 별도로 선언한 allFoodCount
를 이용해도 되겠네요!
const countFood = () => {
if(allFoodCount === 0) return "재고없음"
let list = "";
foodStore.forEach((e, i) => {
if(e != 0) list += `${foodList[i]}/${e} `;
});
return list;
};
이렇게 하면 필요없이 forEach()
를 돌지 않습니다. 성능에도 좋은 듯 합니다.
이렇게 해서 전체 코드를 다음과 같습니다!
//음식과 재고를 같이 입력받는다면?
//"피자", 1, "햄버거", 3, "라면", 2
function RobotFactoryStore(foodNames) {
//closure ~ 접근 불가
const foodList = foodNames.filter((e) => typeof e === "string");
const foodStore = foodNames.filter((e) => typeof e === "number");
let allFoodCount = foodStore.reduce((a, c) => (a += c), 0);
let randomNum = parseInt(Math.random() * foodList.length);
let prevNum = -1;
const getFood = () => {
if (allFoodCount === 0) return "재고없음";
if (foodStore.filter((e) => e != 0).length === 1) prevNum = -1;
while (prevNum === randomNum || foodStore[randomNum] === 0) {
randomNum = parseInt(Math.random() * foodList.length);
}
prevNum = randomNum;
foodStore[randomNum]--;
allFoodCount--;
return foodList[randomNum];
};
const countFood = () => {
if(allFoodCount === 0) return "재고없음"
let list = "";
foodStore.forEach((e, i) => {
if(e != 0) list += `${foodList[i]}/${e} `;
});
return list;
};
function Robot() {
this.serveFood = getFood;
this.remainFoods = countFood;
}
return new Robot();
}
const newRobot = RobotFactoryStore(["피자", 1, "햄버거", 3, "라면", 2]);
📝콘솔에 찍어본 결과입니다.
remainFoods()
를 펼쳐서 스코프를 확인해보면
클로저 스코프에 위와 같이 있는 것을 발견할 수 있습니다.
새로 배운 개념을 위주로 활용하고자 한 것도 있지만, 클로저를 안 쓰고 생성자 함수를 만들게 된다면 다음의 코드가 됩니다.
function RobotFactoryStore(foodNames) {
this.foodList = foodNames.filter((e) => typeof e === "string");
this.foodStore = foodNames.filter((e) => typeof e === "number");
this.allFoodCount = this.foodStore.reduce((a, c) => (a += c), 0);
this.randomNum = parseInt(Math.random() * this.foodList.length);
this.prevNum = -1;
this.serveFood = function() {
if (this.allFoodCount === 0) return "재고없음";
if (this.foodStore.filter((e) => e != 0).length === 1) prevNum = -1;
while (this.prevNum === this.randomNum || this.foodStore[this.randomNum] === 0) {
this.randomNum = parseInt(Math.random() * this.foodList.length);
}
this.prevNum = this.randomNum;
this.foodStore[this.randomNum]--;
this.allFoodCount--;
return this.foodList[this.randomNum];
};
this.remainFoods = function() {
if(this.allFoodCount === 0) return "재고없음"
let list = "";
this.foodStore.forEach((e, i) => {
if(e != 0) list += `${this.foodList[i]}/${e} `;
});
return list;
};
}
const newRobots1 = new RobotFactoryStore(["피자", 3, "라면", 5, "햄버거", 0]);
📝this.
가 매번 변수를 따라다니고, 매우 복잡해 보여서 코드 자체의 가독성이 많이 떨어진다고 판단하였습니다. 또한, newRobots
을 콘솔에 출력해 보면,
📝다음과 같이 필요가 없는 변수들까지 바로 사용이 가능합니다. 물론 재고를 채울 수 있다는 조건이 있다면 위와 같이 써도 무방하나, 현재 로봇의 역할은 음식을 제공하고, 재고를 파악해주는 것입니다. 따라서 목적에 맞게 serveFood()
와 remainFoods()
만을 사용할 수 있도록 하였습니다!
클로저를 사용해서 중첩함수를 만들고, 내부의 메인 함수는 생성자 함수로 새로운 인스턴스를 반환해주는 코드를 작성해보았습니다. 2시간동안 머리 싸매고 2시간동안 글을 쓰니 코드를 다시 보게 되고 개선할 수도 있었습니다. 다음에는 해당 코드를 클래스를 이용해서 다시 작성해봐야겠습니다!