[실습] 생성자, 클로저, 랜덤

이지현·2023년 8월 23일
4

javascript

목록 보기
8/9
post-thumbnail

🐾들어가며

최근에 생성자, 클로저에 대한 개념을 배웠습니다.

생성자 함수의 형태를 가지며, new 키워드와 함께 사용합니다. 새로운 객체를 할당하기 때문에 생성자 함수 내부에서는 this를 써주어 해당 프로퍼티가 새로운 식별자를 가리킬 수 있도록 해줍니다.

클로저는 함수를 return하는 중첩함수를 사용할 때, 내부 함수(return하는 함수)가 외부 함수 내부의 변수를 참조할 때 생깁니다. 내부 함수를 return해줄 때 호출된 외부 함수는 메모리 상에서 사라지지만, 렉시컬 스코프 특징으로 인해 이미 사라져야 할 변수들을 계속 참조합니다. 이런 변수들은 내부 함수에 값을 바꿔주는 별도의 장치가 없다면 접근할 수 없습니다. 말그대로 폐쇄된 상태를 가집니다.

이해한 것을 토대로 개념을 써서 틀린 부분이 있을 수도 있습니다!

이 두 개념이 새로 배우는 것인데도 어디에 활용할 수 있을지 생각하면서 재미있게 코드를 짜다 보니 빠르게 익숙해졌습니다ㅎㅎ

✨본격적으로

실습 내용은 단순히 음식을 랜덤으로 출력하는 robot들을 만드는 것입니다. 이를 위해서 RobotFactory라는 생성자를 만들고, 인스턴스를 생성할 땐 배열을 값으로 받습니다.

저는 여기에서 더 어렵게 가보았습니다. 음식과 음식의 재고를 같이 입력받는다면 어떨까 했습니다. 재고에 맞게 음식을 출력을 하는데, 랜덤하게 출력까지 해주는 걸로ㅎㅎ

문제: 음식과 재고를 배열로 입력받아 재고에 맞춰 음식을 랜덤으로 출력하고, 남은 음식들을 확인할 수 있는 생성자 만들기

입력: 배열 형태, ex) ["피자", 1, "햄버거", 3, "라면", 2]

출력:
serveFood()의 경우: 피자 (재고에 맞춰 랜덤하게 출력)
remainFoods()의 경우: 햄버거/3 라면/2 (재고가 0인 것을 제외하고 보여줌)

1. 함수의 기본 형태

//생성자 함수는 아니지만, 새로운 인스턴스를 반환해주기 때문에 대문자로 시작했다.
function RobotFactoryStore(foodNames){
  //closure ~ 내부 로직을 보여주지 않기 위해 별도로 함수를 만들어준다.
  const getFood = () => {};
  const countFood = () => {};
  //generator function
  function Robot(){
    this.serveFood = getFood;
    this.remainFoods = countFood;
  }
  //새로운 인스턴스를 반환해준다.
  return new Robot();
}

2. 입력의 형태 = 배열

내부에서 편하게 사용하기 위해 다음과 같이 바꿔주었습니다.

//음식 종류 파악하기
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);
  • 처음에 제대로 입력을 받았다면, 음식의 종류와 재고는 같은 인덱스값을 가집니다!

  • 여기에서 추가를 한다면, 예외처리로 같은 음식이 여러번 입력된 경우와, 재고가 표시되지 않고 음식만 입력한 경우를 모두 처리하면 좋을 듯 합니다.

    • foodList의 중복을 제거하기 위해 set으로 할까 고민했지만, 재고까지 파악하기 위해서는 별도의 중복 처리를 해주는 로직이 필요할 듯 합니다.
  • 처음에는 Map을 사용하기 위해 객체로 받으려고 했지만, 아직 객체가 익숙하지 않아 배열로 처리해주었습니다!

3. 조건

랜덤하게 음식을 출력합니다. 이전 음식과 겹치지도 않게 해주었습니다!

let randomNum = parseInt(Math.random() * foodList.length);
let prevNum = -1;
  • 랜덤한 수를 위한 변수 선언
    prevNum으로 현재 랜덤한 인덱스와 비교하기 때문에, 인덱스에 없는 음수값으로 초기화했습니다. 또한, foodList의 길이로 랜덤한 인덱스를 생성하도록 하였습니다. 음식의 종류의 개수에 맞추기 위해서 입니다.

4. 랜덤한 음식을 반환해주는 함수

  • 랜덤한 수와 기존 출력한 음식 비교
    조건에 따라 randomNumprevNum을 비교해야 합니다. 그리고 현재 재고가 있는지도 파악해야 합니다.
    그래서 이전 음식과 같거나, 현재 재고가 없다면 다시 랜덤한 수를 할당합니다.
    따라서 다음 조건을 가진 while문을 사용했습니다.
//랜덤한 수와 기존 출력한 음식 비교
//이전 인덱스와 랜덤한 수가 같거나, 재고가 남아있지 않은 경우
while (prevNum === randomNum || foodStore[randomNum] === 0) {
  //다시 랜덤한 수를 생성
  randomNum = parseInt(Math.random() * foodList.length);
}

여기에서 prevNumrandomNum의 경우 함수가 종료되도 기존 값을 가지고 있어야 하기 때문에, 함수 외부에 변수를 선언해줘야 합니다~!

  • 랜덤한 수를 이용해서 음식을 반환합니다.
    주의할 점은 재고 파악을 해야한다는 점입니다. 반환할 음식의 재고를 먼저 -1씩 처리해주어야 하며, prevNum은 현재의 랜덤한 인덱스를 할당받아야 다음 음식이 현재 음식과 겹치지 않게 할 수 있습니다.
//현재 랜덤한 인덱스는 다음 함수가 호출될 때 이전 인덱스로 기록
prevNum = randomNum;
//음식의 재고를 하나 줄인다.
foodStore[randomNum]--;
//전체 음식의 재고도 하나 줄인다.
allFoodCount--;
//최종 반환
return foodList[randomNum];
  • 예외!!
    문제가 생겼습니다. 재고가 다 다르고 랜덤하게 반환하기 때문에, 같은 음식의 재고만 남을 경우 undefined가 되어버립니다. 또한 재고가 다 떨어졌을 때도 그렇습니다. 그래서 다음과 같이 예외도 처리해주면... 랜덤한 음식을 출력해주는 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];
 };

5. 남은 음식을 알려주는 함수

재고가 남은 음식이란 건 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()를 사용한 게 좀 걸립니다. foodStoreforEach()내부에서도 조건이 한 번 더 들어가야 하니, 이 걸 이용해 개선할 수 있을 것 같습니다.

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시간동안 글을 쓰니 코드를 다시 보게 되고 개선할 수도 있었습니다. 다음에는 해당 코드를 클래스를 이용해서 다시 작성해봐야겠습니다!

profile
건축학도의 프론트엔드 개발자 되기

0개의 댓글