[인프런 워밍업 스터디 클럽 3기 FE] 1주차 발자국
강의 내용을 정리하기엔 많아서 강의 내용 보다는 들으면서 궁금했던 내용 위주로 정리해보았다.강의를 들으며 궁금했던 점 🧐1. Symbol 타입강사님이 생략하고 넘어간 타입인데 궁금해서 찾아봤다.Symbol이란?ES6에서 추가된 Primitive 타입 중 하나. 고유하고 변경 불가능한(immutable) 값을 생성할 때 사용된다. 또한 값을 은닉하고 싶을 때 사용한다.const sym1 = Symbol(); const sym2 = Symbol(); console.log(sym1 === sym2); // falseSymbol() 함수를 호출해 생성함위 코드는 각각 새로운 Symbol을 생성하므로 다른 값을 가짐const sym3 = Symbol("description"); const sym4 = Symbol("description"); console.log(sym3 === sym4); // false생성 시 description을 받을 수 있지만 심볼의 고유성과는 연관이 없다.디버깅 용도로 사용된다.Symbol의 특징고유성 위와 같이 같은 설명을 가진 Symbol도 항상 다른 값이다.변경 불가능한번 생성된 Symbol은 변경 불가능하다.자동 형 변환 불가자동으로 문자열이나 숫자로 변환되지 않는다.Symbol과 객체 프로퍼티객체의 고유한 프로퍼티 키로 사용이 가능하다const ID = Symbol("id"); const user = { name: "Alice", [ID]: 12345 // Symbol을 키로 사용 }; console.log(user[ID]); // 12345 console.log(Object.keys(user)); // ["name"] console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]Object.keys(), Object.values() 등으로 확인할 수 없음 (은닉)Object.getOwnPropertySymbols()로만 가져올 수 있다.Symbol.for과 전역 심볼 레지스트리Symbol.for()를 사용하면 Global Symbol Registry에 심볼을 저장하고 검색할 수 있다.Symbol.for("key")를 사용하면 같은 key를 가진 Symbol을 공유할 수 있다.const sym1 = Symbol.for("sharedKey"); const sym2 = Symbol.for("sharedKey"); console.log(sym1 === sym2); // true (같은 키를 공유하므로 동일한 심볼)Symbol.keyFor()로 전역 레지스트리에 저장된 Symbol의 key를 찾을 수 있다.const globalSym = Symbol.for("globalSymbol"); console.log(Symbol.keyFor(globalSym)); // "globalSymbol" const localSym = Symbol("localSymbol"); console.log(Symbol.keyFor(localSym)); // undefined (전역 레지스트리에 등록되지 않음)전역 심볼 레지스트리, 그래서 이거 언제 쓸까?모듈간 데이터 공유 시, 같은 키를 사용하면 다른 모듈에서도 동일한 Symbol을 재사용 할 수 있다.싱글톤 패턴 구현 시 사용한다.애플리케이션 내에서 유일한 인스턴스 유지할 때 활용한다.내장 심볼 (Well-known Symbols)ECMAScript에서는 특정 기능을 커스터마이징 할 수 있도록 미리 정의된 Symbol을 제공한다.Javascript 엔진의 기본 동작을 변경할 수 있도록 설계된 심볼이다.활용 시 객체의 이터러블 구현, 원시값 반환, instanceof 동작 변경 등을 할 수 있다. 내장 심볼 예시Kotlin의 override와 비슷한 듯 하다. 2. for 문에서 출력하는 부분const user = { name: "Ko", province: "경기도", city: "용인시", }; for (let x in user) { console.log(`${x}: ${user[x]}`); }x 그리고 user[x]로 출력하고 있다.user로 출력하면 어떻게 나오는지 확인해보니 [object Object]라고 나온다.찾아보니 for .. in .. 으로 반복 시 객체의 key 값으로 꺼내온다고 한다.따라서 값을 얻기 위해서는 user[x] 와 같은 방식을 사용해야한다. for .. of ..for .. of .. 의 경우 iterable 객체(Array, Set, Map)의 value 값을 순회한다.const arr = ["A", "B", "C"]; for (let item of arr) { console.log(item); // A, B, C }Array의 경우 정상 출력된다.for (let value of user) { console.log(value); } // TypeError: user is not iterable객체에 사용하면 에러가 난다. 객체에서 for .. of .. 를 사용하려면?Object.keys(), Object.values(), Object.entries() 를 사용하면 가능 // key만 순회 for (let key of Object.keys(user)) { console.log(key); } // value만 순회 for (let value of Object.values(user)) { console.log(value); } // key-value 쌍 순회 for (let [key, value] of Object.entries(user)) { console.log(`${key}: ${value}`); } 3. forEach의 인자const locations = ['서울', '부산', '경기도', '대구']; locations.forEach(function (location, index, array) { console.log(`${index} : ${location}`); console.log(array); })여기서 어떻게 location, index, array를 지정해서 쓸 수 있나 궁금해서 찾아봤다.Array.prototype.forEach()는 배열의 각 요소에 대해 한 번씩 지정된 콜백 함수를 실행하는 메서드이다.기본적으로 매개변수 element, index, array로 내부 콜백함수로 정의되어 있다.element : 현재 순회 중인 배열 요소 (ex: 서울, 부산..)index: 현재 요소의 인덱스 (ex: 0, 1..)forEach()가 호출된 원본 배열 (ex: locations) 4. Function과 Method의 차이Function (함수)독립적으로 정의되고 실행될 수 있는 일반적인 코드 블록특정 객체에 속하지 않는다function 키워드 또는 const 함수명 = () => {} 형식으로 사용 가능function sayHello() { console.log("Hello, world!"); } const add = (a, b) => a + b; Method (메서드)객체의 프로퍼티로 정의된 함수객체에 속하기 때문에 해당 객체의 데이터를 조작하는 역할을 한다.const person = { name: "Alice", greet: function() { console.log(`Hello, my name is ${this.name}`); } };This 키워드 차이Method는 객체 내부에서 this를 사용해 객체 프로퍼티에 접근이 가능함Function의 경우 this가 글로벌 객체(ex: window 또는 undefined(strict mode))를 가리킨다.5. Constructor Functionfunction Audio(title) { this.title = title; console.log(this); } const audio = new Audio('a')강의를 보다가, 해당 코드가 왜 title을 따로 설정(?)도 안하고 바로 this.title을 쓸 수 있는지 이해가 안갔다.관련해서 찾아보니 아래와 같았다.생성자 함수를 사용해 새로운 객체를 만들고, 그 객체의 속성(title)을 동적으로 할당하는 것 new Audio('a')를 실행시 new 키워드의 동작새로운 빈 객체 {}를 생성this를 새로 생성된 객체로 바인딩생성자 함수의 코드를 실행 (this.title = title;)새로 생성된 객체를 반환new 없이 호출한다면 this가 전역 객체를 가리키게 되서 title이 전역 변수로 선언되거나 strict mode에서는 오류가 발생한다. 따라서 의도하지 않은 동작을 방지하려면 new를 꼭 사용해야한다. 6. Strict Mode"use strict"; 추가시 사용이 가능함var, let, const 없이 변수 선언이 금지된다.읽기 전용 속성 변경이 금지된다. (애초에 왜 되는데?)같은 이름의 매개변수가 금지 된다.delete로 변수 삭제가 금지된다.this 값이 undefined가 된다.일반적으로 JS에서는 this는 전역 객체를 가리키는데 strict mode에서는 this가 undefined로 설정된다.따라서 잘못된 this 사용을 막는다.모든 최신 JavaScript 프로젝트에서 기본적으로 사용하는게 좋다.7. IIFE (Immediately Invoked Function Expression)기본 구조 (function() { console.log("즉시 실행됨!"); })(); (() => { console.log("즉시 실행됨!"); })();왜 IIFE를 사용할까?전역 변수 오염 방지JS는 기본적으로 전역 스코프(Global Scope)를 가져서 코드가 길어질 수록 충돌할 가능성이 큼IIFE를 사용하면 함수 내부의 변수는 외부에서 접근할 수 없도록 보호할 수 있음 // 전역 변수를 오염시키는 경우 let counter = 0; function increment() { counter++; console.log(counter); } increment(); // 1 increment(); // 2 console.log(counter); // 2 (외부에서 counter 변경 가능) // IIFE를 사용하여 보호 const increment = (() => { let counter = 0; // 외부에서 접근 불가 return () => { counter++; console.log(counter); }; })(); increment(); // 1 increment(); // 2 console.log(typeof counter); // undefined (외부에서 접근 불가) 한 번만 실행되는 초기화 코드즉시 실행 되므로 초기화 코드 실행하는데 유용함 클로저 활용 가능const increment = (() => { let counter = 0; // 클로저로 보호된 변수 return () => { counter++; console.log(counter); }; })(); increment(); // 1 increment(); // 2 increment(); // 3 console.log(typeof counter); // undefined (외부에서 접근 불가)counter가 클로저에 의해 보호되어 있기 때문에 외부에서 직접 변경이 불가능하고 오직 함수 호출을 통해서만 조작이 가능함 객체를 사용해 상태를 관리하는 방식과의 차이IIEF 사용하는 경우const increment = (() => { let counter = 0; return () => { counter++; console.log(counter); }; })(); increment(); // 1 increment(); // 2 console.log(typeof counter); // undefined (외부 접근 불가)IIFE 내부에 counter가 있어서 외부에서 변경이 불가능객체를 사용하는 경우const obj = { counter: 0, increment: function() { this.counter++; console.log(this.counter); } }; obj.increment(); // 1 obj.increment(); // 2 console.log(obj.counter); // 2 (외부에서 변경 가능)counter 변수가 객체 내부에 있지만 외부에서 수정 가능 차이점 정리IIFE 방식 / 객체 방식스코프 보호 : counter 외부에서 접근 불가 / counter 외부에서 수정 가능전역 오염 방지 : 변수는 IIFE 내부에만 존재 / 변수는 객체에 저장되어 외부에서 접근 가능사용 목적 : 한정된 기능 (ex: 특정 함수의 상태 유지) / 여러 속성과 메서드를 관리하는 구조 8. Curry Function커링 함수란?function addCurry(a) { return function (b) { return a + b; }; } const add3 = addCurry(3); console.log(add3(5)); // 8 console.log(addCurry(3)(5)); // 8함수를 쪼개서 여러 단계로 호출할 수 있도록 변환하는 기법하나의 함수에서 여러 개의 인자를 한 번에 받는 대신, 부분적으로 받아서 실행하는 방식이다.강의를 들으면서, 어떻게 쓰는지는 알겠는데 왜 쓰는지 모르겠어서 찾아보았다. 🤔 커링 함수, 왜 쓸까?재사용성 증가 function multiply(a) { return function (b) { return a * b; }; } const double = multiply(2); // 2를 미리 적용 const triple = multiply(3); // 3을 미리 적용 console.log(double(5)); // 10 console.log(triple(5)); // 15여기서 double은 2를 미리 적용해서 항상 2 * b를 실행하는 함수가 된다.함수 조합(Function Composition)에 유용함const add = a => b => a + b; const multiply = a => b => a * b; const add5 = add(5); const multiply3 = multiply(3); console.log(add5(10)); // 15 console.log(multiply3(10)); // 30함수를 조합해 더 작은 단위의 함수를 쉽게 만들 수 있다.부분 적용, 함수 조합, 설정 저장 등에 유용하다.코드 가독성 및 유지보수성 향상const log = level => message => console.log(`[${level}] ${message}`); const errorLog = log("ERROR"); const warningLog = log("WARNING"); errorLog("서버가 다운되었습니다."); // [ERROR] 서버가 다운되었습니다. warningLog("메모리가 부족합니다."); // [WARNING] 메모리가 부족합니다.코드를 더 읽기 쉽게 만들고 유지보수하기 쉬운 형태로 관리할 수 있다. 9. 생성자 함수와 Class, Object.create()의 차이생성자 함수function Person(name, age) { this.name = name; this.age = age; } Person.prototype.greet = function() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); }; const john = new Person("John", 30); john.greet(); // "Hello, my name is John and I am 30 years old."생성자 함수는 new 키워드와 함께 호출될 때 새로운 객체를 생성함.this를 사용해 객체의 속성을 설정한다ES6에 도입된 class 문법이 좀 더 직관적임.명시적으로 프로토타입을 지정해야 메서드 공유가 가능복잡해질수록 코드가 길어지고 가독성이 낮아진다.Classclass Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } const john = new Person("John", 30); john.greet(); // "Hello, my name is John and I am 30 years old."class 키워드를 사용해 정의메서드를 클래스 내부에 선언해 프로토 타입을 활용함자동으로 프로토타입을 활용해 메서드를 공유함객체지향 언어와 유사한 형대로 직관적인 코드 작성이 가능하다.기능적으로 생성자 함수와 동일함Object.create()const personPrototype = { greet: function() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } }; const john = Object.create(personPrototype); john.name = "John"; john.age = 30; john.greet(); // "Hello, my name is John and I am 30 years old." 특정 객체를 프로토타입으로 명확하게 설정 가능같은 프로토타입을 공유하면서 새로운 객체 생성 가능클래스 없이도 객체 상속이 가능프로토타입을 명확히 컨트롤할 필요가 있을 때 사용하면 유용함하지만 class가 더 직관적이고 가독성이 좋아서 class를 사용하는 것이 좋음 미션 1메뉴판 화면에서 메뉴 카테고리 선택 시 카테고리별로 보여주는 화면을 만드는 것해결 과정해결 방안 고민HTML을 여러개 사용해 메뉴 클릭시 화면을 전환하게 한다.기본 틀 HTML을 하나를 두고 카테고리 클릭 시 JS로 화면을 변경한다여기서 1번은 비효율적으로 보이고, 또 미션에서 보이는 화면이 각 메뉴별로 동일한 화면을 쓰고 있어서 2번을 채택함.개발 과정일단 기본적으로 div로 html/css 뼈대를 잡음<body> <h1 class="food-menu text-3xl font-bold underline"> Food Menu </h1> <div class="button-container"> <button class="food-button">All</button> <button class="food-button">Breakfast</button> <button class="food-button">Lunch</button> <button class="food-button">Shakes</button> <button class="food-button">Dinner</button> </div> <div class="image-container"> <div class="column"> <img class="food-img" src="images/food1.jpg"/> <img class="food-img" src="images/food2.jpg"/> <img class="food-img" src="images/food3.jpg"/> </div> <div class="column"> <img class="food-img" src="images/bread.jpg"/> <img class="food-img" src="images/breakfast.jpg"/> <img class="food-img" src="images/cake.jpg"/> </div> <div class="column"> <img class="food-img" src="images/potato.jpg"/> <img class="food-img" src="images/shake.jpg"/> <img class="food-img" src="images/salad.jpg"/> </div> </div> <script src="js/script.js"></script> </body>카테고리 값을 구분하려면 어떻게 해야할까?사용자 정의 속성을 넣을 수 있음<body> <h1 class="food-menu text-3xl font-bold underline"> Food Menu </h1> <div class="button-container"> <button class="food-button" data-category="all">All</button> <button class="food-button" data-category="breakfast">Breakfast</button> <button class="food-button" data-category="lunch">Lunch</button> <button class="food-button" data-category="shakes">Shakes</button> <button class="food-button" data-category="dinner">Dinner</button> </div> <div class="menu-items"> <div class="menu-item" data-category="breakfast"> <img class="food-img" src="images/food1.jpg"/> </div> <div class="menu-item" data-category="lunch"> <img class="food-img" src="images/food2.jpg"/> </div> <div class="menu-item" data-category="shakes"> <img class="food-img" src="images/food3.jpg"/> </div> <div class="menu-item" data-category="dinner"> <img class="food-img" src="images/bread.jpg"/> </div> <div class="menu-item" data-category="breakfast"> <img class="food-img" src="images/breakfast.jpg"/> </div> <div class="menu-item" data-category="lunch"> <img class="food-img" src="images/cake.jpg"/> </div> <div class="menu-item" data-category="shakes"> <img class="food-img" src="images/potato.jpg"/> </div> <div class="menu-item" data-category="dinner"> <img class="food-img" src="images/shake.jpg"/> </div> <div class="menu-item" data-category="breakfast"> <img class="food-img" src="images/salad.jpg"/> </div> </div> <script src="js/script.js"></script> </body>사용자 정의 속성으로 속성을 구분하도록 수정const buttons = document.querySelectorAll(".food-button"); buttons.forEach(button => { button.addEventListener("click", function() { const category = this.dataset.category; if (category === "all") { console.log("all"); } else { console.log("else"); } }); });사용자 정의 속성 사용시 dataset으로 사용이 가능하다data- 이후의 이름으로 가져올 수 있다.const buttons = document.querySelectorAll(".food-button"); const menuItems = document.querySelectorAll(".menu-item"); buttons.forEach(button => { button.addEventListener("click", function() { const category = this.dataset.category; menuItems.forEach(item => { if (category === "all") { item.style.display = "flex"; } else { if (item.dataset.category === category) { item.style.display = "flex"; } else { item.style.display = "none"; } } }); }); });display=none 옵션으로 일부 메뉴를 가려준다.const buttons = document.querySelectorAll(".food-button"); const menuItems = document.querySelectorAll(".menu-item"); buttons.forEach(button => { button.addEventListener("click", function () { const buttonCategory = this.dataset.category; menuItems.forEach(item => { const itemCategory = item.dataset.category; item.style.display = ["all", itemCategory].includes(buttonCategory) ? "flex" : "none" }); }); });코드를 좀 더 함수형으로 리팩토링 해본다. 회고CSS가 더 어렵다 😔아직 JS에 익숙하지 않아서 머리로 로직을 세우는 것과 실제로 어떤걸 써야할지 감이 잘 안오는 것 같다.실제로 구현해보니 강의만 듣는 것보다 어떤식으로 개발해야할지 감이 오는 것 같다. 미션2가위바위보플레이어와 컴퓨터가 있고 가위/바위/보 선택시 승리한 경우 플레이어 또는 컴퓨터의 카운트가 올라감최종 게임 승리 여부를 출력해 줌.해결 과정기능 파악플레이어와 컴퓨터는 각각의 승리 카운트 상태값을 가지며 승패 여부에 따라 카운트가 업데이트 됨.가위/바위/보 클릭 시, 컴퓨터는 랜덤하게 가위/바위/보 값을 가짐가위/바위/보 클릭 시, 선택하기 카운트가 줄어듬.플레이어의 가위/바위/보 상태와 컴퓨터의 상태를 비교해 승리 여부를 판단함최종 플레이어 승리 카운트와 컴퓨터의 승리 카운트를 비교해 최종 승패 여부를 출력함.다시 시작 클릭시 게임을 다시 시작할 수 있음.개발 과정html과 연동 하는 부분이 익숙하지 않아서, 일단 JS로 로직부터 개발함let playerWinCount = 0; let computerWinCount = 0; let roundCount = 10; function computerPlay() { let choices = ['rock', 'paper', 'scissors']; return choices[Math.floor(Math.random() * choices.length)]; }; function playRound(playerSelection, computerSelection) { if (playerSelection === computerSelection) { return '무승부'; } else if ( (playerSelection === 'rock' && computerSelection === 'scissors') || (playerSelection === 'paper' && computerSelection === 'rock') || (playerSelection === 'scissors' && computerSelection === 'paper') ) { playerWinCount++; return '플레이어 승리'; } else { computerWinCount++; return '컴퓨터 승리'; } } function playerPlay() { document.querySelectorAll('.player-choice').forEach((button) => { button.addEventListener('click', function() { return button.id; }); }); } function game() { for (let i = 0; i < roundCount; i++) { let playerSelection = playerPlay(); let computerSelection = computerPlay(); console.log(playRound(playerSelection, computerSelection)); } if (playerWinCount > computerWinCount) { console.log('플레이어 승리'); } else if (playerWinCount < computerWinCount) { console.log('컴퓨터 승리'); } else { console.log('무승부'); } }이후 html 연동하는 부분을 붙임 (길어서 코드 생략)최종 코드let playerWinCount = 0; let computerWinCount = 0; let roundCount = 10; const resultDiv = document.querySelector('#result'); const resultMessage = document.querySelector('#result-message'); const playerWinCountDiv = document.querySelector('#player-win-count'); const computerWinCountDiv = document.querySelector('#computer-win-count'); const roundCountDisplay = document.querySelector('#round-count'); const resetButton = document.querySelector('#reset'); function computerPlay() { let choices = ['rock', 'paper', 'scissors']; return choices[Math.floor(Math.random() * choices.length)]; }; function playRound(playerSelection, computerSelection) { if (playerSelection === computerSelection) { return '무승부'; } else if ( (playerSelection === 'rock' && computerSelection === 'scissors') || (playerSelection === 'paper' && computerSelection === 'rock') || (playerSelection === 'scissors' && computerSelection === 'paper') ) { playerWinCount++; updateUI(); return '플레이어 승리'; } else { computerWinCount++; updateUI(); return '컴퓨터 승리'; } } function updateUI() { playerWinCountDiv.textContent = playerWinCount; computerWinCountDiv.textContent = computerWinCount; roundCountDisplay.textContent = roundCount; } function game(playerSelection) { if (roundCount === 0) { alert('게임이 끝났습니다. 게임을 다시 시작하려면 [다시 시작] 버튼을 누르세요.'); return; } roundCount--; updateUI(); let computerSelection = computerPlay(); let playResult = playRound(playerSelection, computerSelection); console.log(playResult); resultDiv.style.display = 'block'; resultMessage.textContent = playResult; if (roundCount === 0) endGame(); } function endGame() { if (playerWinCount > computerWinCount) { console.log('게임에서 이겼습니다.'); resultMessage.textContent = '게임에서 이겼습니다.'; } if (playerWinCount < computerWinCount) { console.log('게임에서 졌습니다.'); resultMessage.textContent = '게임에서 졌습니다.'; } if (playerWinCount === computerWinCount) { console.log('무승부입니다.'); resultMessage.textContent = '무승부입니다.'; } resultDiv.style.display = 'none'; resetButton.style.display = 'block'; } function resetGame() { playerWinCount = 0; computerWinCount = 0; roundCount = 10; updateUI(); resultMessage.textContent = ''; resetButton.style.display = 'none'; } document.querySelectorAll('.player-choice').forEach((button) => { button.addEventListener('click', function() { game(button.id); }); }); resetButton.addEventListener('click', function() { resetGame() });회고CSS는 신경 안쓰고 기능 개발에만 집중하니 훨씬 수월했다.처음에는 막막했는데 일단 BE 개발하듯 로직부터 접근하니 개발하기 쉬웠다. 미션3퀴즈 앱해결 과정기능 파악정답을 틀리거나 맞출 경우 Next 버튼이 생성되며 다음 퀴즈를 할 수 있음퀴즈는 회색 화면에 answer 3가지가 존재 (값/정답이 없는 경우)정답을 맞출 경우 초록 화면 / 틀릴 경우 빨간 화면퀴즈 종료 시 restart 버튼이 생성 됨.개발 과정로직개발까지는 문제 없었으나 next 버튼과 reset이 제대로 동작안하는 이슈가 발견됨알고보니 addEventListener 를 함수 내부에 선언해 함수를 중복해서 사용해 리스너가 중복으로 실행된 것리스너를 함수 밖으로 빼고 해결함.회고Listener의 동작 방식을 모르고 사용한 것 같다.글로벌 변수를 안쓰는 방향으로 가고 싶은데 어떤식으로 해야할지 아직은 잘 모르겠다. 미션 4책 관리 앱 해결 과정기능 파악form에 책 데이터를 입력 받는다데이터 입력 후 submit 시 하단에 책목록에 추가된다책목록에 있는 데이터는 X 버튼으로 삭제할 수 있다.책 추가 또는 삭제시 알림 메세지가 뜬다.개발 과정submit 이벤트를 어떻게 받는지부터 고민 했는데 addEventListener에서 가능했다받아온 event는 event.target 을 통해 값을 꺼내올 수 있다.createElement를 통해 책목록, 알림요소들을 추가해주었다.회고 이전 과제들보다 오히려 쉬운 느낌이었다. (특히 퀴즈..) 미션 5Github Finder 앱해결 과정기능 파악유저가 입력하는대로 Github User를 검색하는 AppSearch 버튼을 따로 누르지 않고 입력 받는대로 API 호출을 한다.개발 과정일단 API 호출을 하기 위해 await fetch 를 사용함그리고 UI를 만듦...API를 여러번 호출하는데 debounce 라는 개념이 있어서 알아보았다 회고debounce 를 적용하는 것 외에는 딱히 어려운 점은 없었다.