블로그

장서윤

[인프런 워밍업 클럽 0기] 2주차 발자국 👣

이번 발자국은 과제 진행 과정을 작성해 볼 예정이다. 목차는 다음과 같다.1. 기능 목록 작성2. 공통 컴포넌트 구현3. 결과물4. 고민했던 부분 ✅️ 1. 기능 목록 작성1. 항목 추가/수정할 수 있다.지출 항목 + 비용을 입력받는다 (조건을 만족할 때까지)지출 항목은 문자열이여야 한다.비용은 숫자여야 한다.지출 항목 + 비용 모두 입력되어야 한다. (공백 제외)  조건을 만족할 경우, 버튼이 enable 된다. 추가/수정을 완료했을 경우, toast 메시지를 띄워준다. 2. 항목 리스트를 보여준다. (테이블 형식)각 항목(row) 마다 수정/삭제 버튼이 존재한다.수정 버튼을 누른 경우[ 기능목록 1. 항목 추가/수정할 수 있다 ] 로 이동한다.삭제 버튼을 누른 경우항목이 삭제되며, toast 메시지를 띄워준다. 총 지출 금액을 보여준다.항목 리스트가 없을 경우, '존재하지 않습니다' 를 보여준다.   3. 모든 항목을 삭제할 수 있다.목록 지우기 버튼을 누른 경우모든 항목이 삭제되며, toast 메시지를 띄워준다. ✅ 2. 공통 컴포넌트 구현MUI와 같은 UI 라이브러리를 쓰지 않고, tailwind css로만 구현할 것이기에, 필요한 컴포넌트는 직접 만들어야한다. 먼저 공통된 디자인을 위해 rounded만을 사용하고자 했다. ( 컴포넌트마다 rounded를 다 다르게 사용하지 않는다)색상은 emerald + slate 만을 최대한 사용했다. 그렇게 해서 필요한 컴포넌트는 다음과 같다. Outlined Input시간을 투자한 부분이다. 기본 html의 input은 굉장히 단순하기에 커스텀이 필요했다.focus 했을 때, label 을 input 박스 위로 가게 하고, 부드러운 애니메이션을 넣고자 했다.Contained Button + Text Button배경을 채운 Contained Button오직 text 만 존재하는 Text ButtonTable기본 table에 css만 추가했다.Toast 메시지 success : 초록색 체크 아이콘추가, 수정, 삭제가 성공적으로 완료되었음을 표시한다. warn : 노란색 경고 아이콘입력값의 유효성검사가 일치하지 않을때 표시한다.  ✅ 3. 결과물 ✅ 4. 고민했던 부분 지출 항목이 문자여야하는데, 이를 어떻게 판별하는가? 이다.애매한 부분이 지출 항목이 "맥북 pro 16" 처럼 오직 문자 type으로 이뤄지지 않고, 여러 type이 같이 존재할 수 있다. 그래서 이걸 어떻게 예외처리 해줄지 고민했었는데, 일단은 지출 항목을 절대 숫자로만 이뤄지지는 않을 것 같아서, isNaN()만을 판별했다. 이 부분은 고민이 더 필요할 것 같다.. toast 메시지의 색상을 어떻게 할 것인가? 이다.상품을 "삭제" 했을 경우, toast 메시지의 아이콘 색상을 고민했었다.빨간색 -> 부정적 의미 -> 삭제와 연관된다! -> 그러나 error 와도 연관됨 -> 사용자 입장에서 "에러가 났다..!" 로 혼동할 수 있음.초록색 -> 삭제가 완료되었다! 에 의미를 둠-> 그러나, 추가, 수정, 삭제도 어쨌든 완료이기에, 다른 기능임에도 아이콘 색상이 같아서 구별이 어렵다는 문제가 존재함.고민 끝에 error와 연관되는 빨간색보다는 초록색으로 가되, "추가", "수정", "삭제" text 를 크고 두껍게 처리해주는 것으로 합의를 보았다!후기UI 라이브러리만 사용하다가, tailwind css로 직접 컴포넌트를 만드니까, 생각보다 시간이 오래 걸렸다. 중간에 계속 애니메이션도 적용된 라이브러리를 사용하고 싶었지만, 이를 css로(비록 tailwind css지만) 직접 완성시켰을때 상당히 뿌듯했다!또한, 기능목록을 제대로 작성한건지 모르겠다. 조금 더 깔끔하게 작성하고 싶은데, 어디까지 자세하게 적어야하는지, ui 부분도 자세하게 적어야하는지(버튼 색깔이 바뀐다거나)를 잘 모르겠다. 더 공부해봐야겠다!  

웹 개발프론트FE워밍업클럽

이양구

[인프런 워밍업 클럽 FE 0기] 미션8 - 디즈니 플러스 앱

🎞 Disney Plus APP GitHub 🎞 Disney Plus APP DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 여덟 번째 미션인 '디즈니 플러스 앱' 입니다. 따라하며 배우는 리액트 섹션 4~5(리액트로 Netflix 앱 만들기) 목표swiper 라이브러리 커스텀해보기react-oauth/google 로 구글 로그인 연동해보기 구현swiper 라이브러리 커스텀해보기// LoginPage import "swiper/css/effect-fade"; <Swiper modules={[Autoplay, EffectFade, Pagination, A11y]} autoplay={auto} effect={"fade"} pagination={{ clickable: true, }} loop={true} fadeEffect={{ crossFade: true }} slidesPerView={1} speed={2000} > {...} </Swiper> // Row.tsx import "swiper/css/mousewheel"; <Swiper modules={[Navigation, Pagination, Scrollbar, A11y, Mousewheel]} navigation pagination={{ clickable: true }} mousewheel speed={1000} spaceBetween={10} > {...} </Swiper> 2024년 3월 10일의 디즈니 플러스 메인 페이지를 그대로 옮겨보고자 swiper 라이브러리를 커스텀해봤다.로그인 페이지에서는 좌우로 넘기는 슬라이드가 아닌 fade-in-out의 슬라이드를 구현하기 위해 swiper에 EffectFade 모듈을 추가하고 fadeEffect 속성을 추가했다.이 fadeEffect가 제대로 작동하기 위해선 반드시 해당 이펙트의 css를 추가해야 한다.다른 모듈이나 컴포넌트를 추가할 때처럼 자동으로 추가되지 않으니 주의해야 한다. (이걸 몰라서 한참을 찾았다. 😥)Row 컴포넌트는 마우스 휠에 따라 움직이는 슬라이드를 만들기 위해 Mousewheel 모듈과 속성을 이용했다.이렇게 슬라이드 속성을 정한 뒤에 swiper가 렌더링하는 요소의 class를 찾아 CSS에서 원하는 디자인으로 변경하면 된다.이때 라이브러리의 CSS와 겹치는 속성이 있을 수 있기 떄문에 '!important'를 붙이는 게 좋다. react-oauth/google 로 구글 로그인 연동해보기// index.js <GoogleOAuthProvider clientId={process.env.REACT_APP_CLIENT_ID}> <BrowserRouter> <App /> </BrowserRouter> </GoogleOAuthProvider> // App.jsx const navigate = useNavigate(); const [isLogin, setIsLogin] = useState( localStorage.getItem("user") ? true : false ); useEffect(() => { isLogin ? navigate("/") : navigate("/login"); }, [isLogin]); <Routes> {isLogin ? ( <Route path="/" element={<Layout setIsLogin={setIsLogin} />}> <Route index element={<MainPage />} /> <Route path=":movieId" element={<DetailPage />} /> <Route path="search" element={<SearchPage />} /> </Route> ) : ( <Route path="login" element={<LoginPage setIsLogin={setIsLogin} />} /> )} </Routes> react-oauth/google는 구글 로그인을 지원하는 라이브러리로, 사전에 구글의 Cloud에서 API 등록을 하고 Client ID를 발급받아야 사용할 수 있다.먼저 프로젝트의 최상위에 GoogleOAuthProvider로 감싸준다.그리고 사용자의 로그인 여부에 따라 페이지를 이동시키기 위해 라우터를 설정한 App 컴포넌트에서 관련 코드를 작성했다.페이지가 렌더링 될 때 로컬 스토리지에 저장된 유저 정보를 받아오고 만약 없다면 로그인 페이지로 보내도록 했다. // loginPage const googleLogin = async (credentialResponse) => { localStorage.setItem( "user", JSON.stringify(jwtDecode(credentialResponse.credential)) ); setIsLogin(true); }; <GoogleLogin onSuccess={(credentialResponse) => googleLogin(credentialResponse)} /> GoogleLogin 컴포넌트는 react-oauth/google 라이브러리에서 지원하는 버튼 컴포넌트로 디자인 및 로그인 관련 함수가 내장되어 있다.onSuccess는 사용자의 로그인이 성공했을 때 실행되는 콜백 함수이며, 인자로 로그인한 유저의 정보를 담은 데이터를 갖는다.여기서 credential이라는 값은 유저의 정보를 담고 있는 토큰으로 암호화되어 있기 때문에 jwt-decode 라이브러리를 이용해 디코딩하여 사용해야 한다.여기서 받은 picture는 사용자의 프로필 이미지 링크를 포함하고 있어서 Nav 컴포넌트에서 사용해 로그인한 유저의 프로필 이미지로 변경했다. 회고'Netflix 앱 만들기'를 하면서 사용했던 기술이 대부분이라 오래 걸리지 않을 것 같았지만...라이브러리 알아보고 문서 읽고 실행해보고... 하는 데 너무 오래 걸린 것 같다.배너 하단의 카테고리 부분은 이전에 같은 과제를 하셨던 분의 깃허브를 참고했다. (https://github.com/kimneighbor/clone-disney-plus-app)로그인 페이지는 따라하기 싫어서 현재 디즈니 플러스 홈페이지를 보고 참고했다.그대로 하면 얼마 안 걸릴 거라 생각했는데 생각보다 라이브러리 커스텀에서 좀 애를 먹었다. 😅with_networks: "2739" 2739는 TMDB에서 디즈니 플러스 방송사(networks) 코드라서 axios의 instance 기본 값에 추가했다.몇몇 요청은 해당 파라미터가 통하지 않거나 오류를 보내기도 해서 완벽하진 않다.디즈니 플러스에서 API를 제공했다면 더 알맞게 페이지를 구현할 수 있었을 텐데 하는 아쉬움이 남는다.한편 영화 정보 API를 제공해주는 TMDB(The Movie Database) 같은 곳이 있어 감사하고 다행이라는 생각이 들었다.프론트엔드 공부하는데 API를 제공해주는 곳이 아예 없었다면 혹은 매번 일정 비용을 지불해야 했다면 얼마나 힘들었을까로그인도 사실 좀 더 좋은 라우팅 구조나 상태 관리 라이브러리를 공부하고 사용해보고 싶었지만...계속 욕심만 커지는 것 같아 최대한 간단하게 구현하려 했다.(사실 과제 밀려서 조바심에 아무것도 못 했다... 😂) 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

장서윤

[인프런 워밍업 클럽 0기] 1주차 발자국 👣

✅ 학습 내용1일차자바스크립트 기초Window 객체 및 DOMEvent2,3 일차자바스크립트 중급 4일차객체지향 프로그래밍(OOP), 비동기5일차Iterator + Generator디자인 패턴 ✅ 미션 과정1⃣ 미션 메뉴mock data 생성하면서 💡고민사항이 생겼다.[ { "name": "비빔밥", "country": "Korea", "imageUrl" : "./images/food/korea/bibimbap.png", "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, ... 중략 " }, ] 💡 description 에는 무슨 내용을 넣을 것인가?해당 음식과 관련된 내용을 찾아서 넣는다로렘 입숨을 넣는다. => html, js로 기능 구현이 목표이기 때문에 아무 의미 없는 텍스트, 로렘 입숨을 넣어줬다.💡 image 폴더구조는 적절한가?폴더 구조는 항상 고민인 것 같다. 사실상 해당 프로젝트에는 food관련 image만 들어가기 때문에, images/korea/~ 등으로 food라는 폴더가 중첩해도 있지 않아도 될 것이다. 그런데 만약 다른 유형의 image가 들어간다면,,?(background image 등) 폴더 구조를 엎어야할 것이다.=> 이 부분은 개발해보면서 다른 이미지가 넣을지 말지가 결정될 때 정할 것이다!✅ 회고사실 아직 강의와 미션을 전부 완료하진 못했다... 커리큘럼 그대로 진행해야지!를 목표로 삼았으나, 계획대로 지켜지지 못해 아쉬울 따름이다. 다음 발자국은 조금 더 의미있는 내용을 기록하고 싶다.  

웹 개발워밍업FE프론트

이양구

[인프런 워밍업 클럽 FE 0기] 미션7 - 예산 계산기 앱

💸 Budget Calculator APP GitHub 💸 Budget Calculator APP DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 일곱 번째 미션인 '예산 계산기 앱' 입니다. 따라하며 배우는 리액트 섹션 0~3(To-Do 앱) 목표의존성 배열(Dependency Array) 을 이용해 함수 실행하기state 를 전역 변수처럼(?) 사용해보기 구현구조|-- App | |-- Form | |-- Lists | | |-- List  의존성 배열(Dependency Array) 을 이용해 함수 실행하기// App.jsx const [budgetList, setBudgetList] = useState( JSON.parse(localStorage.getItem("budgetList")) || [] ); useEffect(() => { localStorage.setItem("budgetList", JSON.stringify(budgetList)); }, [budgetList]); const totalCost = useCallback(() => { return budgetList.reduce((acc, cur) => acc + cur.cost, 0); }, [budgetList]); 최상위 컴포넌트인 <App> 컴포넌트에서 만든 'budgetList'이라는 state를 useEffect와 useCallback의 의존성 배열에 추가했다.useEffect에서는 해당 state가 변경되면 로컬 스토리지의 budgetList를 최근의 리스트로 변경한다.이렇게 하면 일일이 setBudgetList가 호출되는 곳마다 함수를 사용하지 않아도 된다.다음은 예산의 총 금액을 반환하는 함수가 리스트가 변경될 때마다 실행되도록 useCallback으로 감싸고 의존성 배열에 state를 추가했다.// Form.jsx const budgetNameRef = useRef(); const [budgetName, setBudgetName] = useState(""); const [budgetCost, setBudgetCost] = useState(0); useEffect(() => { if (isEdit) { setBudgetName(budget.name); setBudgetCost(budget.cost); budgetNameRef.current.focus(); } }, [isEdit]); <Form> 컴포넌트에서는 useEffect에 'isEdit'이라는 state를 의존성 배열에 추가했다.사용자가 예산을 수정하기 위해 list의 Edit 버튼을 클릭하면 해당 budget의 name과 cost를 최근 state로 불러오고, useRef를 이용해 name을 입력하는 <input> 요소에 focus 상태가 되도록 했다.state 를 전역 변수처럼(?) 사용해보기// App.jsx const [currentBudget, setCurrentBudget] = useState({ isEdit: false, budget: {}, }); // List.jsx const handleEdit = () => { setCurrentBudget({ isEdit: true, budget: list, }); setHandleStatus({ type: "edit", message: "Editing..." }); }; // Form.jsx const handleBudgetSubmit = (e) => { const newBudget = { id: Date.now(), name: budgetName, cost: budgetCost, }; // isEdit의 값에 따라 새로 추가할지 수정할지 결정 if (isEdit) { setBudgetList((prevBudgetList) => { const newBudgetLists = [...prevBudgetList]; const index = newBudgetLists.findIndex(({ id }) => id === budget.id); newBudgetLists[index] = newBudget; return newBudgetLists; }); setCurrentBudget({ isEdit: false, budget: {} }); setHandleStatus({ type: "submit", message: "Edit Success!" }); } else { setBudgetList((prevBudgetLists) => [...prevBudgetLists, newBudget]); setHandleStatus({ type: "submit", message: "Submit Success!" }); } // submit 종료 시 input의 데이터를 초깃값으로 설정 setBudgetName(""); setBudgetCost(0); }; 배웠던 To Do 앱은 List의 Edit 버튼을 클릭했을 때 해당 List의 요소를 input 요소로 변경시키고 수정을 했다.하지만 과제는 클릭을 했을 때 List의 요소를 변경시키는 게 아니라 Form의 input에 해당 예산의 데이터를 전달해야 했다.그래서 마치 전역 변수처럼 사용할 'currentBudget'이라는 state를 생성하고 'isEdit'이라는 boolean 값과 수정할 예산의 데이터를 담을 'budget'이라는 값을 설정했다.'isEdit'의 상태 값이 true일 때 수정하기와 삭제하기 <button> 요소를 disabled로 변경한다.또한 submit 함수는 새로운 입력 값을 budgetList에 추가하지 않고 해당 예산의 index를 찾아 수정하고 리스트를 변경한다.이렇게 하니 onSubmit과 onEdit 처럼 비슷한 기능을 하는 함수를 여러 개 만들지 않아도 되었다. ⚠ setTimeout 렌더링const { type, message } = handleStatus; const handleStyle = useCallback(() => { if (type === "edit") { return "text-gray-500 block"; } else if (type === "none") { return "hidden"; } else { // 2초 뒤에 실행 --> App - Form - Status 1번 더 렌더링 setTimeout(() => { setHandleStatus({ type: "none", message: "" }); }, 2000); if (type === "submit") { return "text-green-400 block"; } else { return "text-red-400 block"; } } }, [type]); 추가, 삭제, 수정의 완료 및 진행 중 상태를 보여주는 <Status> 컴포넌트를 만들었다.App에서 만든 'handleStatus'라는 state를 전달하고 메세지가 나타난 뒤에 사라지게 만들고 싶어서 setTimeout() 메서드를 이용해 2초 뒤에 상태를 초기화했다.하지만 이 상태가 App과 Form 컴포넌트에서 참고하다 보니 나타나고 사라질 때마다 렌더링이 발생했다.CSS의 opacity로 처리하기엔 state의 값을 변경해야 했기에 알맞는 방법은 아니라 생각했다.뭔가 <Status> 컴포넌트 내부에서만 렌더링이 일어나게 하고 싶었는데 아직 다른 방법을 찾지 못했다.😢😢😢 회고다른 컴포넌트의 클릭 이벤트로 변경된 state를 이용하는 부분이 생각보다 오래 걸렸다.처음엔 콜백 함수처럼 App 컴포넌트에서 함수 만들고 prop으로 넘겨봤지만 List와 Form은 종속적인 관계가 아니라 힘들었다. 😢그래서 생각해낸 게 state를 이용해서 상태의 변경을 이벤트처럼 사용하는 것이었다.pub-sub 혹은 observer 패턴 같다는 생각도 했지만, 이렇게 최상위에서 선언한 state가 이곳저곳 돌아다니는 게 좋은 방법은 아닐 것 같다는 생각이 들었다.규모가 커지면 렌더링 관리도 힘들고 props를 쫓아다녀야 하기 때문이다.이래서 상태 관리 라이브러리가 나왔나 보다. 🤔 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션4-2 - GitHubFinder 앱

🔍 github-finder-app GitHub 🔍 github-finder-app 개요인프런 워밍업 클럽 FE 0기의 네 번째 미션인 'GitHubFinder 앱' 입니다.따라하며 배우는 자바스크립트 섹션 5(OOP), 섹션 6(비동기) 목표Fetch API 를 이용해 깃허브 유저 목록 불러오기Closure 를 이용해 Debounce Function 만들기 구현Fetch API 를 이용해 깃허브 유저 목록 불러오기async function loadUser(input) { prevInputValue = input; try { // const response = await fetch('./src/javascript/user.json'); const response = await fetch(`${url}/${input}`); if (!response.ok) { throw new Error('Failed to fetch user json'); } const json = await response.json(); setUserAvatar(json); setUserInfo(json); await loadUserRepos(json); } catch (error) { console.error(error); } }fetch() 메서드의 응답은 HTTP 응답 전체를 나타내는 'response' 객체를 반환한다.response의 ok 속성은 응답의 성공 여부를 불리언 값으로 가지고 있다.따라서 응답이 성공이 아닐 경우 오류 객체(new Error())를 반환하고 catch 문으로 Promise의 오류를 처리한다.응답에 성공한 response 객체를 JSON으로 사용하기 위해선 json() 메서드를 이용해 파싱해야 한다. Closure 를 이용해 Debounce Function 만들기// debounce debounceInput.addEventListener('input', debounce(loadUser, 1000)); // debounceInput.addEventListener('input', e => callback(e)); function debounce(callback, delay = 0) { // timer는 부모 함수에서 선언된 지역 변수 let timer = null; return (arg) => { // 여기서 arg는 input event if (timer) { // 이미 타이머가 있는데 또 실행되면 타이머 삭제 clearTimeout(timer); } // 변수 timer는 부모 함수에서 선언되었지만 내부 함수에서 사용(클로저) timer = setTimeout(() => { callback(arg.target.value); }, delay); }; }<input> 요소의 'input' 이벤트는 요소의 value가 변경될 때마다 발생한다.만약 사용자가 입력할 때마다 서버에 데이터를 요청한다면 서버의 부하가 커지기 때문에 좋은 방법은 아니다.이럴 때 사용자의 입력이 끝난 뒤 마지막 value를 이용해 서버로 요청하는 게 효율적인 방법이라 할 수 있다.함수의 실행 요청이 반복될 때 마지막 요청만으로 실행하는 걸 '디바운싱(debouncing)'이라고 부른다.debounce 함수는 인자로 실행할 함수를 받고 자식 함수를 반환한다.부모 함수인 debounce 함수에서 선언한 변수(timer)를 자식 함수에서 사용할 수 있는 클로저(Closure)를 이용해 자식 함수의 setTimeout() 메서드의 반환 값인 'timeoutID'를 할당한다.변수 'timer'에 할당한 timeoutID를 이용해 setTimeout() 메서드의 지연 시간(delay)이 종료되기 전에 요청이 들어왔다면 이전에 생성한 타이머를 clearTimeout() 메서드를 이용해 종료하고 다시 타이머를 할당한다. 이렇게 delay로 설정한 시간 이내에 사용자의 입력이 없을 경우 API 요청 함수를 실행하게 된다. 반복적인 함수의 실행을 다루는 방법으로 디바운싱(debouncing)와 쓰로틀링(throttling)이 있다.여러 변수를 고려해 'lodash' 라이브러리의 debounce를 많이 사용한다. 회고이번 미션은 debounce가 반환하는 자식 함수의 인자(argument)가 어떤 타입인지 알기 때문에 callback 함수에 전달하는 인자를 수정해서 미숙한 debounce 함수라고 볼 수 있다.늘 라이브러리를 통해 사용하던 함수를 만들려고 하니 모르는 것도 많고, 고려해야 할 부분이 많다는 걸 알게 됐다.자바스크립트의 기초를 잘 알아야 이런 라이브러리 메서드의 원리를 이해하기도 쉽고, 커스텀하기에 수월한 것 같다.(외의로 GitHub의 API 요청이 API key 없이도 되어서 신기했고, 그 덕에 조금은 수월했다. 아주 조금... 😵) DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션1 - 음식 메뉴 앱

🍝 food-recipe-app API from TheMealDBGitHub food-recipe-app 개요인프런 워밍업 클럽 FE 0기의 첫 번째 미션인 '음식 메뉴 앱' 만들기입니다.따라하며 배우는 자바스크립트의 섹션 1~3(자바스크립트 기초, Window 객체 및 DOM, Event)를 보고 자바스크립트의 DOM 요소를 조작하는 데 중점을 두었습니다.음식 데이터는 TheMealDB의 API를 이용했습니다. 사용한 API가 '음식 레시피'라서 이름을 변경했습니다. 목표문서 객체 모델(The Document Object Model, 이하 DOM)의 메소드(methods)를 이용해 요소(element)에 접근하고 생성하고 교체하기이벤트 리스너(Event Listener) 메소드를 이용해 요소에 이벤트를 등록하고 이벤트 객체 이용하기메뉴 데이터를 Fetch API를 사용해 불러오기 구현이벤트 위임(Event Delegation)을 이용한 이벤트 생성/* <nav id="food-navigation"> <div class="food-navigation-item"> <button id="Beef"> <figure> <img src="https://www.themealdb.com/images/category/beef.png"> <figcaption> Beef </figcaption> </figure> </button> </div> // ... </nav> */ // Not Event Delegation foodNavigation.querySelectorAll('button').forEach((button) => { button.addEventListener('click', async () => { const targetId = button.id; await setFoodList(targetId); }); }); // Event Delegation foodNavigation.addEventListener('click', async (event) => { const targetElement = event.target; // closest() 메서드는 주어진 CSS 선택자와 일치하는 요소를 찾을 때까지, // 자기 자신을 포함해 위쪽(부모 방향, 문서 루트까지)으로 문서 트리를 순회합니다. const targetDiv = targetElement.closest('.food-navigation-item'); if (!targetDiv) { return; } const targetButton = targetDiv.querySelector('button'); const targetId = targetButton.id; await setFoodList(targetId); });이벤트 위임이란 '상위 요소에서 하위 요소의 이벤트를 제어하는 것'을 의미합니다.이벤트를 위임하는 이유이벤트를 하나의 핸들러로 처리함으로써 메모리 사용량을 줄이고 성능을 향상시킬 수 있다.새로운 요소가 추가되거나 제거되는 경우 이벤트 리스너는 상위 요소에 연결되어 있어 재연결의 필요성이 줄어든다.저는 nav 태그에 이벤트를 등록하고 closest 메서드를 이용해 버튼의 id를 찾는 방법을 사용했습니다. 하위 요소를 제거하고 생성한 요소를 추가하기/* <div id="food-list"> <div class="food-list-item"> <figure> <img src="img src" /> </figure> <div class="food-list-item-desc"> <p>food name</p> <hr /> <div> food recipe </div> </div> </div> </div> */ const foodList = await getFoodList(strCategory); const foodListElement = document.getElementById('food-list'); const foodListItem = document.querySelectorAll('.food-list-item'); foodListItem.forEach((item) => item.remove()); // foodListElement.innerHTML = ''; foodList.map(async (food) => { // ... const foodElement = getFoodElement( idMeal, strMeal, strMealThumb, strInstructions ); foodListElement.appendChild(foodElement); });배웠던 removeChild()와 replaceChild() 메서드를 이용하고자 했으나...'만약 해당 카테고리의 음식 리스트의 개수가 다르다면 어떻게 하지?'라는 생각에 한번에 제거하기로 결정했습니다.처음엔 innerHTML을 이용해 하위 코드를 공백으로 만들었지만, 뭔가 이건 너무 이상하다는 생각(요소의 참조나 연결 같은 게 깨지진 않을까)이 들어 찾아보았습니다.stack overflow의 Remove child nodes (or elements) or set innerHTML=""?라는 글에서는 innerHTML은 하위 요소의 이벤트 핸들러가 완전히 제거되지 않을 수도 있다고 한다.또한 Why InnerHTML Is a Bad Idea and How to Avoid It?에서는 innerHTML이 보안상 좋지 않다는 점을 말하고 있다. Stack Overflow의 글을 자세히 읽어 보니 다음과 같은 글이 있었다.What is the best way to empty a node in JavaScript그리고 MDN 문서에도 이렇게 소개하고 있다.replaceChildren() provides a very convenient mechanism for emptying a node of all its children. You call it on the parent node without any argument specified:즉 replaceChildren()메서드를 빈 인자로 실행하면 하위 자식 노드를 모두 지워준다는 것...!😅 회고빈 폴더를 놓고 코드를 작성해본 게 너무 오랜만인 것 같다.자료를 찾기 귀찮다는 마음과 첫 미션이니까 API를 써볼까 하며 자만했던 순간도 있었다.미션의 목적보다 어느새 다른 부분을 신경 쓰느라 배보다 배꼽이 점점 커지는 것 같았다.딸랑 script 태그 한 줄 작성하고 js 파일을 제대로 못 불러와서 몇 시간을 해결 방법을 찾아서 해매기도 했다.😭이벤트 위임 코드를 작성할 때 이은재 님의 시나브로 자바스크립트에서 배웠던 부분을 참고했다.음식 레시피를 불러올 때 요소를 지우고 불러와서 그런지 해당 부분이 사라지고 나타나서 페이지가 늘었다 줄었다 하는 게 눈에 띈다.이래서 가상 돔을 쓰는걸까? 아니면 태그의 속성을 하나하나 수정하면 되는걸까?일단 진도를 따라잡고 배워서 발전시켜야겠다. DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽FE프론트프론트엔드과제미션발자국

박나영

인프런 워밍업 클럽 스터디 1기 FE | 1주차 발자국

(Day2) (음식 메뉴 앱)깃허브 저장소 주소: https://github.com/nayoungpk/Inflear_Front.git  <과제>(Day3) (가위 바위 보 앱)깃허브 저장장소 주소 - https://github.com/nayoungpk/Inflear_Front.git (Day4) (퀴즈 앱)깃허브 저장장소 주소 - https://github.com/nayoungpk/Inflear_Front.git  <공부내용> 자바스크립트 기본 기초 console 객체디버깅 콘솔에 접근 할 수 있는 메서스를 제공하며, 전 영역의 객체에서 접근 할 수 있다. var, let, const다음으로는 자바스크립트에서 변수를 선언 할 때, var let const를 사용한다.변수의 선언 방식에서는var >> 중복 선언과 재할당이 가능let >> 중복 선언은 불가능하며, 재할당은 가능const >> 중족 선언과 재할당 둘가 가능 호이스팅호이스팅의 뜻은 무언가를 들어 올리거나 끌어 올리는 동작을 설명한다.자바스크립트에서 호스팅은 코드가 실행되기 전에 변수 및 함수 선언이 로컬 범위의 맨 위로 들어올려지는 경우를 설명한다.>> 쉽게 말해 변수에 값을 할당하고 사용해야함.>>변수 생성할 때 재할당이 필요없다면 const를 사용하고 재할당이 필요하면 let을 사용해서 scope를 최대한 좁게 사용!!! 타입원시 타입과 참조 타입을 가지고 있음. (참조 타입의 경우에는 heep이라는 별도의 메모리 공간 사용)타입은 크게 primitive 과 object 로 구분 가능 연산의 경우는 다른 언어들과 동일!!! // 덧셈 함수 function add(a, b) { return a + b; } // 뺄셈 함수 function subtract(a, b) { return a - b; } // 곱셈 함수 function multiply(a, b) { return a * b; } // 나눗셈 함수 function divide(a, b) { // 0으로 나누는 경우를 처리 if (b === 0) { return "나눗셈 오류: 0으로 나눌 수 없습니다."; } return a / b; } // 주어진 배열의 합계를 계산하는 함수 function sum(numbers) { let total = 0; for (let number of numbers) { total += number; } return total; } // 사용 예시 console.log(add(5, 3)); // 출력: 8 console.log(subtract(10, 4)); // 출력: 6 console.log(multiply(2, 6)); // 출력: 12 console.log(divide(8, 2)); // 출력: 4 console.log(divide(5, 0)); // 출력: "나눗셈 오류: 0으로 나눌 수 없습니다." const numbers = [1, 2, 3, 4, 5]; console.log(sum(numbers)); // 출력: 15  백틱자바 스크립트에서 ` 문자를 사용하여 문자열을 표한한 것을 템플릿 리터럴이라 표현함.이렇게 사용하면 줄 바꿈을 쉽게 할 수 있고, 문자열 내부에 표현식을 포함할 수 있게됨. <공부 후기>자바스크립트의 경우에는 개념은 모두 읽어보았으나 직접 코딩하는 부분을 더 진행해볼 생각이다.자바스트립트 언어에 부족함을 많이 느꼈음.과제 보안점으로는 DAY에서 분류를 나누는 기준을 누르면 사진의 크기가 달라진다. css 부분 수정 및 공부 예정리엑트에 대한 기초 지식이 많이 부족하다고 느낌

프론트자바스크립트

이양구

[인프런 워밍업 클럽 FE 0기] 미션6 - 타이핑 테스트 앱

⌨ Typing Test APP GitHub ⌨ Typing Test APP DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 여섯 번째 미션인 '타이핑 테스트 앱' 입니다. 따라하며 배우는 자바스크립트 섹션 9(오브젝트 만들기)  목표setInterval() 메서드를 이용해 타이머 만들기split() 메서드를 이용해 문자열 관리(는 위험하다) 구현setInterval() 메서드를 이용해 타이머 만들기textarea.addEventListener('focus', gameStart); let timer = null; function gameStart() { // 만약 마우스를 다른 곳에 뒀다가 다시 focus 할 때 막기 if (timer) { return; } makeExample(); timer = setInterval(() => { if (remainTime > 0) { remainTime--; passedTime++; timeCount.textContent = remainTime; } if (remainTime <= 0) { finishGame(); } }, 1000); } function finishGame() { clearInterval(timer); // ... }'timer'라는 변수를 gameStart() 함수 외부에 생성하고(다른 함수에서도 사용하기 위해) 함수 내부에서 setInterval() 메서드를 할당한다.setInterval(func, delay) 메서드는 실행할 함수(func)와 지연 시간(delay)을 인자로 받는다.setInterval() 메서드는 setTimeout() 메서드의 반환 값처럼 'intervalID'을 반환하고, 이를 이용해 생성한 interval을 취소할 수 있다.1초마다 화면에 보여주는 시간 값을 줄여서 보여주고, 남은 시간(remainTime)이 0이 되면 finishGame() 함수에서 clearInterval(intervalID) 메서드를 호출한다.예제는 textarea에 focus 이벤트가 발생하면 gameStart() 함수를 호출하고 있어서 이미 타이머가 생성된 이후라면 하위 코드가 실행되지 않도록 조건을 만들었다. split() 메서드를 이용해 문자열 관리(는 위험하다)(정리글을 쓰면서 알아보다가 발견...)function makeExample() { exampleContainer.replaceChildren(); // String.split('') better than [...String] or Array.from(String) // better than grapheme-splitter library Array.from(examples[exampleIndex]).forEach((char) => { const span = document.createElement('span'); span.textContent = char; exampleContainer.appendChild(span); }); exampleIndex++; if (exampleIndex >= examples.length) { exampleIndex = 0; } }split(separator, limit) 메서드는 구분자(separator)와 문자열 최대 개수(limit)을 인자로 받는다.구분자는 문자열에서 일치하는 부분을 기준으로 나누는데, 빈 문자열('')을 할당하면 공백이므로 각 문자로 나눈다.처음엔 문자열의 각각 문자를 배열의 요소처럼 다루기 위해서 split() 메서드를 이용했다.⚠ split('')은 문자 단위로 나누지 않는다!하지만 이 split() 메서드에서 공백을 구분자로 하는 게 큰 위험이 있다는 걸 MDN 문서를 통해 확인했다.console.log('𝟘𝟙𝟚𝟛'.split('')); // ["�","�","�","�","�","�","�","�"] console.log('😎😜🙃'.split('')); // ["�", "�", "�", "�", "�", "�"] console.log('अनुच्छेद'.split('')); // ["अ", "न", "ु", "च", "्", "छ", "े", "द"] console.log('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞'.split('')); // ["Z","͑", "ͫ", "̓", ... ]이런 현상이 발생하는 이유는 split() 메서드의 구분자가 빈 문자열인 경우, 전체 문자열을 UTF-16으로 인코딩하기 때문이라고 한다.위와 같은 특수문자는 UTF-16로 표현할 수 없어서 2바이트 인코딩의 값을 쌍으로 묶어 표현했고, 이를 '써로게이트 페어'라고 부른다.즉 split('')은 이 써로게이트 페어가 망가지면서 원하는 결과를 얻지 못하게 된다.이런 다양한 특수문자와 흰두어 같은 다른 나라의 언어를 제대로 나누기 위한 'grapheme-splitter' 라이브러리도 있다.소스를 보면 생각보다 고려해야 하는 게 많아서 엄청난 코드 양을 볼 수 있다.예제는 비교적 간단하고 미리 정해진 문자열을 다루고 있기에 Array.from() 메서드로 변경했다. 회고사용자가 입력한 문자와 문제의 문자를 비교해서 CSS와 스크립트를 작성하는 게 중요한 과제였다.처음엔 늘 하던대로 String.split('')으로 문자열을 나누고 관리했지만 글을 작성하기 위해 알아보다가 이 방식이 문제가 있다는 걸 알게 됐다.MDN 문서를 보면서 문서화가 정말 중요하다는 걸 느끼기도 했고, Stack Overflow 같은 커뮤니티에 문제를 공유하고 함께 고민하는 게 개발 문화의 큰 힘이자 감사한 일이라 생각했다. (감사... 또 감사...)이런 다양한 경우의 수 때문에 테스트 코드를 중요하게 여기고 작성하는 게 아닐까!?훌륭한 개발자의 머리 속에는 if가 가득할 것 같다. 🙄 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션5 - 비밀번호 생성 앱

🔐 Password Genrator GitHub 🔐 Password Genrator DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 다섯 번째 미션인 '비밀번호 생성 앱' 입니다.따라하며 배우는 자바스크립트 섹션 7~8(Iterator, Generator, Design Pattern) 목표Array.from, fromCharCode() 메서드를 이용해 숫자, 소문자, 대문자 배열 생성동적 변수를 이용해 DOM 요소 조작 및 비밀번호 생성하기 구현Array.from, fromCharCode() 메서드를 이용해 숫자, 소문자, 대문자 배열 생성// index를 이용한 0~9 배열 [0, 1, 2, ...] const numbersArray = Array.from({ length: 10 }, (_, index) => index); // 유니코드를 이용한 소문자 배열 [a, b, c, ...] const smallLettersArray = Array.from({ length: 26 }, (_, index) => String.fromCharCode(97 + index) ); // 유니코드를 이용한 대문자 배열 [A, B, C, ...] const capitalLettersArray = Array.from({ length: 26 }, (_, index) => String.fromCharCode(65 + index) ); const symbolsArray = ['@', '!', '#', '$', '%'];Array.from(arrayLike, mapFn, thisArg) 메서드는 세 개의 인자를 받는다.첫 번째 인자는 순회가 가능한 유사 배열 객체(arrayLike), 두 번째 인자는 배열의 각 요소에 호출할 함수(mapFn), 세 번째 인자로 mapFn 함수 실행 시에 사용할 this(thisArg) 값을 받는다.여기서 유사 배열 객체의 속성인 'length'를 이용해 원하는 길이를 지정하고 두 번째 함수에서 인덱스를 이용해 각 요소의 값을 인덱스로 지정하면 0부터 length-1의 값을 갖는 배열을 만들 수 있다.String.fromCharCode(num1[, ...[, numN]]) 메서드는 UTF-16 코드 유닛의 시퀀스로부터 문자열을 생성해 반환한다.알파벳은 총 25개이며, 알파벳 소문자의 유니코드는 97부터 122까지, 대문자는 65부터 90까지이므로 길이와 index를 각각 알맞게 설정해주면 된다. 동적 변수를 이용해 DOM 요소 조작 및 비밀번호 생성하기function isChecked() { const checkboxes = form.querySelectorAll('input[type="checkbox"]'); let anyCheck = false; checkboxes.forEach((checkbox) => { if (checkbox.checked) { anyCheck = true; } }); return anyCheck; }<form> 요소 하위의 'checkbox' 타입인 <input> 요소를 모두 선택하고, 'checked' 속성을 확인해 하나라도 true라면 'anyCheck'라는 동적 변수에 true를 할당한다.form.addEventListener('click', (e) => { isChecked() ? passwordLength.removeAttribute('disabled') : passwordLength.setAttribute('disabled', ''); });이렇게 isChecked() 함수를 실행해서 체크 여부를 확인하고 입력 <input> 요소의 속성을 변경한다.const resultArray = []; let password = ''; let requiredNumbers = false; // ... if (checkNumbers.checked) { resultArray.push(...numbersArray); requiredNumbers = true; } // ...생성할 비밀번호에서 반드시 포함해야 하는 속성을 확인하는 required 변수를 만들고 'input[type="checkbox"]' 요소의 각 checked 값을 확인해 true로 변경한다.do { for (let i = 0; i < passwordLength.value; i++) { const randomIndex = Math.floor(Math.random() * resultArray.length); password += resultArray[randomIndex]; } } while ( (requiredNumbers && !password.split('').some((char) => numbersArray.includes(Number(char)))) || (requiredSmallLetters && !password.split('').some((char) => smallLettersArray.includes(char))) || (requiredCapitalLetters && !password.split('').some((char) => capitalLettersArray.includes(char))) || (requiredSymbols && !password.split('').some((char) => symbolsArray.includes(char))) // 정규 표현식으로 검사 // (requiredNumbers && !/[0-9]/.test(password)) || // (requiredSmallLetters && !/[a-z]/.test(password)) || // (requiredCapitalLetters && !/[A-Z]/.test(password)) || // (requiredSymbols && !/[!@#$%]/.test(password)) );for문을 이용해 사용자가 입력한 비밀번호의 자릿수(passwordLength.value)만큼 비밀번호를 생성한다.생성한 비밀번호를 split() 메서드를 이용해 문자마다 나눈 뒤 배열로 만들고, 문자를 하나하나 기존에 생성한 배열의 요소와 비교한다.만약 생성한 비밀번호에 반드시 포함해야 하는 속성이 없다면(required가 true인데 다음 조건식을 만족하지 않는 경우) 다시 생성한다.이를 좀 더 쉽게 검사하기 위한 정규 표현식도 있지만, 모른다는 가정 하에(잘 모르기도 했지만 😂) 구현을 해보려 했다. 회고다양한 플랫폼에서 회원가입과 로그인은 필수적인 사항일 텐데, 여기서 사용하는 검사식은 플랫폼만큼이나 다양하다고 들었다.왜냐하면 이 부분은 보안과 연결되어 있어서 꽤 민감하기 때문이다.코드를 작성하다 보니 비밀번호를 무작위로 생성하는 것보다 조건을 만들고 통과시키는 게 더 어려웠다.특히 input의 checked 여부에 따라 결과에 반드시 포함시켜야 한다고 작성하는 부분이 꽤 오래 걸렸다.처음엔 전체가 아니라 하나하나 만들 때마다 검사했지만, while의 조건에서 OR( || ) 연산자로 검사하다 보니 무한 루프에 빠져버렸다.결국 전체를 비교하고 다시 만드는 코드로 변경했지만 뭔가 좋은 방법이 아닌 것 같아서 찜찜하다. 🤔물론 보통은 생성이 아니라 사용자가 입력한 값을 비교하겠지만...여러 변수와 다양한 조건을 고려해서 효율적인 코드를 작성할 수 있는 개발자가 되고 싶다! 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션4-1 - 책 리스트 앱

📚 book-list-app GitHub 📚 book-list-app 개요인프런 워밍업 클럽 FE 0기의 네 번째 미션인 '책 리스트 앱' 입니다.따라하며 배우는 자바스크립트 섹션 5(OOP), 섹션 6(비동기) 목표class 를 이용해 책 리스트 객체 활용하기setTimeout 을 이용해 DOM 요소 관리하기배열 메서드(some(), splice())를 이용해 조건에 맞는 코드 실행 및 배열 관리 구현class 를 이용해 책 리스트 객체 활용하기class Book { constructor(name, author) { this.name = name; this.author = author; } } submitBtn.addEventListener('click', () => { const bookName = document.getElementById('book-name').value; const bookAuthor = document.getElementById('book-author').value; const book = new Book(bookName, bookAuthor); addBookToList(book); }); 'Book'이라는 클래스(class)는 'name'과 'author'이라는 속성을 갖는 객체를 생성합니다.책의 이름과 저자는 버튼의 클릭 이벤트로 input 요소의 value를 가져옵니다. setTimeout 을 이용해 DOM 요소 관리하기function addSubmitInfo(state, message) { const submitInfo = document.createElement('p'); submitInfo.classList.add('info'); submitInfo.textContent = message; switch (state) { case 'success': submitInfo.classList.add('success'); break; case 'error': submitInfo.classList.add('error'); break; } requestAnimationFrame(() => { submitInfo.style.opacity = '1'; }); infoWrap.appendChild(submitInfo); setTimeout(() => { requestAnimationFrame(() => { submitInfo.style.opacity = '0'; }); setTimeout(() => { submitInfo.remove(); }, 500); }, 1000); }버튼의 이벤트의 성공 유무에 따라 사용자에게 보여줄 정보를 담은 p 태그를 생성하는 코드입니다.성공 혹은 실패에 대한 상태(state)를 받고 보여줄 메시지를 받습니다.requestAnimationFrame() 메서드는 브라우저에 애니메이션을 업데이트할 때 사용하는 메서드입니다. 생성한 요소가 등장하고 사라질 때 서서히 나타나는 효과를 주기 위해 사용했습니다.setTimeout(callback, delay)은 첫 번째 인자로 콜백 함수를 받고, 두 번째로 밀리초 시간 단위를 받아서 해당 시간이 경과하면 콜백 함수를 실행하는 메서드 입니다.먼저 1초 뒤에 requestAnimationFrame() 메서드를 실행하고, 0.5초 뒤에 요소를 지우게 됩니다. 배열 메서드(some(), splice())를 이용해 조건에 맞는 코드 실행 및 배열 관리const bookList = []; function addBookToList(book) { if ( !bookList.some( (bookOfBookList) => bookOfBookList.name === book.name && bookOfBookList.author === book.author ) ) { bookList.push(book); // ... bookDeleteBtn.addEventListener('click', () => { addSubmitInfo('success', '✅ Book deleted to list successfully'); bookList.splice(bookList.indexOf(book), 1); bookTr.remove(); }); // ... addSubmitInfo('success', '✅ Book added to list successfully'); } else { addSubmitInfo('error', '❗ Book already in list'); } }some(callback) 메서드는 배열의 요소가 인자로 받은 함수를 통과하는지 확인하고 불리언 값을 반환합니다.만약 'bookList' 배열에 이미 똑같은 이름과 저자의 책을 등록하는 경우 else 문을 실행하게 됩니다.splice(start, deleteCount, item) 메서드는 변경할 인덱스(start), 제거할 요소의 수(deleteCount), 추가할 요소(item)를 인자로 받습니다. 만약 item이 없다면 start index부터 deleteCount의 수만큼 제거합니다.indexOf(searchElement, fromIndex) 메서드는 찾을 요소(searchElement)와 검색 시작 인덱스(fromIndex)를 인자로 받습니다. 만약 fromIndex가 없다면 0부터 시작합니다. 또한 찾을 수 없는 경우 -1을 반환합니다.즉, bookList에 indexOf를 이용해 해당 book의 index를 찾아서 splice로 제거하는 방식입니다. 객체를 indexOf로 비교하면 객체의 속성 값이 동일하더라도 참조가 다르다면 음수를 반환하는 경우도 있기에 정확한 속성 값을 비교하는 게 안전하다.또한 배열의 데이터가 객체인 경우는 findIndex()를 이용하는 게 좋다고 한다.findIndex()는 주어진 판별 함수를 이용해 비교하기 때문!bookDeleteBtn.addEventListener('click', () => { addSubmitInfo('success', '✅ Book deleted to list successfully'); // bookList.splice(bookList.indexOf(book), 1); const index = bookList.findIndex( (item) => item.name === book.name && item.author === book.author ); if (index !== -1) { bookList.splice(index, 1); } bookTr.remove(); console.log(bookList); }); 회고원래는 class로 생성한 Book 객체에 delete 이벤트도 함께 넣으려고 했었다.그런데 생성한 tr 요소와 bookList라는 array도 참고해서 지워야 했기 때문에 코드가 복잡해져 사용하진 못했다.(애초에 설계를 잘못하지 않았나...😂)아마 좀 더 복잡한 모듈을 만든다면 다르게 작성하지 않았을까...!물론 설계하는 데 충분한 시간을 두고 해야겠지만 🙄 DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽프론트엔드프론트미션과제발자국FE

이양구

[인프런 워밍업 클럽 FE 0기] 미션3 - 퀴즈 앱

🤔 quiz-app GitHub 🤔 quiz-app 개요인프런 워밍업 클럽 FE 0기의 세 번째 미션인 '퀴즈 앱' 만들기입니다.따라하며 배우는 자바스크립트 섹션 4(9~17) 목표Math.random() 메소드를 이용해 무작위 문제와 보기 만들기do while 문으로 중복된 데이터를 처리하고 오답 생성배열 메소드(includes(), sort())를 이용해 보기 관리  구현Math.random() 메소드를 이용해 무작위 문제와 보기 만들기const operatorArray = ['+', '-', '*']; function makeQuiz() { btnWrap.replaceChildren(); nextBtnWrap.replaceChildren(); emoji.innerText = '🤔'; const number1 = Math.floor(Math.random() * 10); const number2 = Math.floor(Math.random() * 10); const operator = operatorArray[Math.floor(Math.random() * operatorArray.length)]; quiz.innerText = `${number1} ${operator} ${number2}`; // 정답 저장 const correct = calculator(number1, number2, operator); // 정답 + 오답 3개 const optionArray = makeOptions(correct); for (let i = 0; i < 4; i++) { const btn = document.createElement('button'); btn.innerText = optionArray[i]; btnWrap.appendChild(btn); } } function calculator(num1, num2, operator) { switch (operator) { case '+': return num1 + num2; case '-': return num1 - num2; case '*': return num1 * num2; } }Math.random() 메소드는 0과 1 사이에서 무작위 난수를 생성합니다.정확한 계산을 위해 Math.floor() 메소드로 정수 이하의 소수점을 버립니다.무작위로 생성한 난수에 배열의 길이를 곱해서 무작위 인덱스(index)를 생성한 뒤에, 연산자(operater)에 따라 switch문을 이용해 정답을 반환합니다.또한 오답도 만들어야 하기에 범위가 정답에서 -10 ~ +10의 무작위 값을 생성하고 배열에 담습니다. do while 문으로 중복된 데이터를 처리하고 오답 생성 && 배열 메소드(includes(), sort())를 이용해 보기 관리function makeOptions(correct) { const optionArray = []; const range = 10; // 정답 범위 for (let i = 0; i < 3; i++) { let wrongAnswer; do { // 정답 주변의 랜덤한 오답 생성 wrongAnswer = correct + (Math.random() > 0.5 ? 1 : -1) * Math.floor(Math.random() * range); } while (optionArray.includes(wrongAnswer) || wrongAnswer === correct); // 이미 포함된 오답이거나 정답과 같은 경우 다시 생성 optionArray.push(wrongAnswer); } optionArray.push(correct); // 보기 섞기 optionArray.sort(() => Math.random() - 0.5); return optionArray; }do while문은 while의 특정 조건을 만족한다면 다시 do 구문을 실행하는 루프를 뜻합니다.includes() 메소드는 해당 배열에 일치하는 값이 있는지 검사하고, 있다면 true를 없다면 false를 반환합니다.따라서 생성한 wrongAnswer라는 값이 해당 배열에 존재하거나, 정답과 중복된 경우 다시 do 구문을 실행해 새로운 값을 생성합니다.3개의 오답을 모두 생성했다면 마지막에 정답을 추가합니다.이렇게 된다면 항상 마지막에 정답이 위치하므로 sort() 메소드를 이용해 배열을 섞습니다.sort() 메소드는 매개변수로 정렬 순서를 정의하는 함수를 받습니다.만약 해당 매개변수가 제공되지 않으면 요소를 문자열로 변환하고 유니 코드 코드 포인트 순서로 문자열을 비교하여 정렬됩니다.저는 Math.random()을 이용해 음수를 반환하면 false, 양수를 반환하면 true가 되도록 해서 배열의 순서를 변경했습니다. 회고강의에서 배웠던 커링(Curry Function)과 IIFE(Immediately Invoked Function Expression)를 사용하고자 했지만...평소에 보던 코드에서 마땅한 예시가 없던 탓인지 바로 적용할 좋은 예시가 생각나진 않았다. 😥IIFE는 라이브러리나 오픈 소스에서 사용하는데, 변수의 전역 선언을 피하고 다른 라이브러리나 오픈 소스와의 충돌을 피하기 위해서 라고 한다. (IIFE 내부 안으로 다른 변수가 접근할 수 없고, 전역 스코프에 불필요한 변수가 추가되지 않기 때문에 그렇다!)main.js 파일의 시작을 IIFE로 변경하긴 했지만... 나중에 제대로 된 예시를 참고해보거나, 라이브러리 제작 혹은 오픈 소스 제작 및 기여를 하는 수준까지 갔을 때 잊지 말고 참고해봐야겠다!Math.random()으로 여러 무작위 숫자를 생성해서 문제를 만들면서 생각보다 숫자와 문자열 타입이 난무하고 있다는 걸 많이 깨달을 수 있었다.내가 쓰는 메소드의 반환(return) 타입을 잘 이해하고 있어야겠다는 생각과 함수를 만들 때도 return 값을 잘 의도해서 만들어야겠다는 생각이 함께 들었다. 🙂 DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽프론트프론트엔드미션과제발자국FE

이양구

[인프런 워밍업 클럽 FE 0기] 미션2 - 가위 바위 보 앱

👊Rock-🖐Paper-✌Scissors-app GitHub 👊Rock-🖐Paper-✌Scissors-app 개요인프런 워밍업 클럽 FE 0기의 두 번째 미션인 '가위 바위 보 앱' 만들기입니다.따라하며 배우는 자바스크립트 섹션 4(1~8) 목표생성자 함수(Function() constructor) 를 이용해 객체를 생성하기this 를 이용해 객체의 프로퍼티(Property)와 메소드(Method) 관리하기 구현생성자 함수(Function() constructor)를 이용한 객체 생성function Player() { this.win = 0; this.weapon = ''; } function Game() { this.count = 0; this.play = function (player, robot) { // ...play function this.count++; if (this.count === TOTAL_CHANCE) { // ... } } this.reset = function (player, robot) { this.count = 0; player.win = 0; robot.win = 0; player.weapon = ''; robot.weapon = ''; // ... } } const game = new Game(); const player = new Player(); const robot = new Player();생성자 함수는 객체를 만드는 함수이며 두 가지 규칙이 있다.함수명은 '대문자'로 시작한다.'new' 연산자를 붙여서 실행해야 한다.new 연산자와 함께 생성자 함수를 호출하게 되면 다음과 같은 과정을 통해 this를 만들고 반환한다.this = {} this를 빈 객체로 생성한다.함수 내부의 프로퍼티와 메소드를 this에 바인딩한다.return this this를 반환한다.저는 'Player'라는 생성자 함수를 만들고 이기면 1씩 증가하는 'win' 프로퍼티와 가위, 바위, 보 중에 하나가 들어갈 'weapon'이라는 프로퍼티를 만들었습니다.그리고 게임 진행 상태를 기록할 'Game'이라는 생성자 함수도 만들었습니다.'count'는 몇 회의 게임이 진행됐는지 기록하는 프로퍼티이며, 'play'와 'reset'은 게임을 실행하고 초기화하는 메소드입니다. 버튼 클릭 이벤트에 생성한 객체의 메소드 실행하기const WEAPON_LIST = { rock: '👊', paper: '🖐', scissors: '✌', }; btnWarp.addEventListener('click', function (e) { const target = e.target; if (target.classList.contains('btn')) { if (target.id === 'retry') { return game.reset(player, robot); } player.weapon = target.textContent; // Object.values() --> object의 value를 array로 반환해준다. robot.weapon = Object.values(WEAPON_LIST)[Math.floor(Math.random() * 3)]; playerWeaponElement.innerText = player.weapon; robotWeaponElement.innerText = robot.weapon; game.play(player, robot); } });버튼 요소를 감싸고 있는 부모 요소인 btnWarp에 이벤트를 등록했습니다.game 객체의 play 메소드를 실행하기 전에 robot의 weapon에 Math.random() 메소드를 이용해 무작위의 weapon을 할당합니다.재시작 버튼 이벤트를 함께 처리하기 위해서 game의 play 함수에서 this.count를 이용해 게임이 마지막까지 진행됐다면 id가 'retry'인 버튼을 추가합니다. 회고뭔가 'player'라고 부르니 게임 캐릭터를 만드는 느낌이 들어서 신기하면서도 게임 개발자는 어떻게 캐릭터를 구성하는지 궁금해졌다.프로퍼티나 메소드를 객체에 바인딩해서 반환하는 게 class 문법과 닮아있다고 생각했다.Class and Object Constructor Function in JavaScript궁금해서 찾아보니 class가 좀 더 편리한 문법을 제공하며, 상속이 큰 차이점이라고 한다. (아직은 잘 모르겠다)생성자 함수나 class를 제대로 이해하고 사용한 적이 없어서 그런지 감이 잘 안 온다.😢라이브러리와 오픈 소스의 코드를 보고 class로 도배가 되어 있어 놀랐던 기억이 새록새록 난다.얼른 배우고 싶다. 습관적으로 window.onload나 body 태그에 onload를 사용했었는데, 다른 스터디 러너 분께서 'DOMContentLoaded' 이벤트를 쓰시는걸 보고 슬쩍 배워보았다.- DOMContentLoaded : 이벤트는 HTML의 파싱이 완료되면 발생하며, 스타일시트, 이미지 및 하위 프레임 로드를 기다리지 않습니다.- onload : 속성은 페이지의 모든 자원(이미지, 스크립트, 스타일시트 등)이 다운로드되고 웹 페이지가 완전히 로드되었을 때 발생합니다. DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽발자국thisconstructor프론트프론트엔드미션과제FE

최지호

[인프런 워밍업 클럽] FE 0기 Day 2 👣

#1 자바스크립트 기초 Console 객체아무 전역 객체에서나 접근 가능 var, let, const함수 선언문 호이스팅 : 올바른 출력변수 생성시,재할당 필요 X ⇒ const 사용재할당 필요 O ⇒ scope를 최대한 좁게 만들어 let 사용 자바스크립트 타입원시 타입 : 고정된 크기로 Call Stack 메모리에 저장, 실제 데이터가 변수에 할당참조 타입 : 데이터 크기가 정해지지 X, Call Stack 메모리에 저장, 데이터의 값이 heap에 저장되며 변수에 heap 메모리의 주소값이 할당원시 타입과 참조 타입으로 나뉨, JS는 동적 타입이다. 자바스크립트 타입 변환JS 함수 사용ex) String(2) ⇒ ‘2’, 2.toString() ⇒ ‘2’JS 자체에 의해 자동으로ex) string + number = string 자바스크립트 연산 및 Math Object+, -, *, /, %Math Object Template literalsbacktick(`)을 사용해 문자열 표현줄 바꿈 쉽게 가능문자열 내부에 표현식 포함 가능Loopsforfor/in주로 객체를 순회할 때 사용for/ofiterable(반복 가능한) 객체의 요소들을 순회할 때 사용whiledo/while코드 한번 실행 후 조건문을 통과for, forEach, mapforEach ⇒ break X#2 Window 객체, DOMWindow Object브라우저의 객체브라우저 창 정보 알 수 있고, 창 제어 가능var 키워드로 변수 선언 or 함수 선언 시 window 객체의 프로퍼티가 됨 DOM이란?Document Object Model메모리에 웹 페이지 문서 구조를 트리구조로 표현해서 웹 페이지가 HTML 페이지를 인식하게 해줌CRP(Critical Rendering Path)? 웹 페이지 빌드 과정 Document Object 사용해보기window 객체 ⇒ 브라우저 창document 객체 ⇒ 브라우저 내에서 콘텐츠를 보여주는 웹 페이지 자체// 하나의 요소에 접근 document.getElementById('요소아이디') // 파라미터로 전달한 ID를 가진 태그를 반환 document.getElementsByName('name속성값') // 파라미터로 전달한 name 속성을 가진 태그를 반환 document.querySelector('선택자') // 파라미터로 전달한 선택자에 맞는 첫 번째 태그를 반환 // 여러 요소에 접근 document.getElementsByTagName('태그이름') // 파라미터로 전달한 태그이름을 가진 모든 태그들을 반환(배열) document.getElementsByClassName('클래스이름') // 파라미터로 전달한 클래스 이름을 가진 모든 태그들을 반환(배열) document.querySelectorAll('선택자') // 파라미터로 전달한 선택지에 맞는 모든 태그들을 반환(배열) innerHTML : html까지 포함innerText : 사용자에게 보여지는 텍스트, 여러 공백을 무시하고 하나의 공백만 처리textContent: (display:none)이 적용된 텍스트도 보여줌 DOM 탐색하기자식 노드 탐색하기 (childNodes, firstChild, lastChild)val = list.childNodes : line-break(text)도 나타남배열같아 보이지만, 배열 아님배열로 표현 ⇒ Array.from(list.childNodes)// nodeType // 1 : Element // 2 : Attribute (deprecated) // 3 : Text node // 8 : Comment // 9 : Document itself // 10 : Doctype val = list.firstChild : list.firstChild === list.childNodes[0]val = list.firstElementChild : textNode 포함 X CreateElementdocument.createElement('태그이름')document.querySelector('상위 태그').appendChild('새로만든 태그') removeChild & replaceChildparentNode.removeChild(node) : 삭제parentNode.replaceChild(newChild, oldChild) : 교체 #3 EventEvent Listener & Event 객체Event Listener : 이벤트 발생 시 호출하는 함수자바스크립트 코드에 프로퍼티로 등록HTML 태그에 속성으로 등록addEventListener 메소드를 사용 Event 종류UI 이벤트load, change, resize, scroll, error키보드 이벤트keydown, keyup, keypress마우스 이벤트click, dblclick, mousedown, mouseout, mouseover, mousemove, mouseup포커스 이벤트focus, blur폼 이벤트input / textarea, change, select, reset, submit, cut/copy/paste Event Bubbling아래에서 위로 이벤트 전달event.stopPropagation() : 버블링 중단event.target : 실제 이벤트 타깃 요소this(event.currentTarget) : ‘현재’ 요소로, 현재 실행 중인 핸들러가 할당된 요소 Event Capturing위에서 아래 요소로 이벤트가 내려오는 것이벤트의 3단계 흐름캡처링 단계 : 이벤트가 하위 요소로 전파되는 단계타깃 단계 : 이벤트가 실제 타깃 요소에 전달되는 단계버블링 단계 : 이벤트가 상위 요소로 전파되는 단계코드 흐름 확인elem.addEventListener("click",(e) => alert(캡쳐링: ${elem.tagName}),true);→ 캡처링(true!!)elem.addEventListener("click",(e) => alert(캡쳐링: ${elem.tagName}));→ 버블링 Event Delegation하위 요소의 이벤트를 상위 요소에 위임하는 것즉, 하위요소의 이벤트를 상위에서 제어

워밍업FE프론트

채널톡 아이콘