블로그

이양구

[인프런 워밍업 클럽 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미션과제발자국

이양구

[인프런 워밍업 클럽 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프론트프론트엔드과제미션발자국

xicodey

[인프런 워밍업 클럽 0기] BE 3일차 과제

자바의 람다식은 왜 등장했을까? 자바는 람다식 함수형 프로그램밍이 사용한 이유는 불필요한 코드를 줄이고, 가독성을 높이기 위해서다 .그리고 함수 만드는 과정없이 한번에 처리 할 수 있기 때문에 생산성이 높아진다.병령 프로그램밍에 용이하다. 람다식과 익명 클래스는 어떤 관계가 있을까? 함수형 프로그래밍이란 함수를 정의하고 이 함수를 데이터 처리부로 보내 데이터를 처리하는 기법이다.데이터 처리부는 데이터만 가지고 있을 뿐, 처리 방법이 정해져 있지 않아 외부에서 제공져 있지 않아 외부에서 제공된 함수에 의존한다.데이터 처리부는 제공된 함수의 입력값으로 데이터를 넣어 함수에 정의된 처리 내용을 실행하고, 동일한 데이터라도 함수 A를 제공한 결과 값과 함수 B를 제공하여 처리된 결과 값은 다를 수 있다 이것이 함수형 프로그램의 특징인 데이터 처이의 다형성이다.람다식은 함수를 하나의 식으로 표현하여 익명 함수를 반환한다.익명 클래스는 선언된 클래스 내에서만 한 번만사용될 경우 별도로 변수에 담을 필요가 없다.람다식을 쓰면 함수형 인터페이스로 인스턴스를 만들 수 있으며 코드를 줄 일 수 있다.메서드 매개변수와 리턴 타입, ㅌ변수로 만들어 사용도 가능하다. 람다식의 문법은 어떻게 될까? 데이터 처리부에 제공되는 함수 역활 하는 매개변수를 가진 중괄호 블록이다.{매개변수, ...} -> {처리 내용};  인터페이스가 단 하나의 추상 메소드를 가질 경우 이를 함수형 인터페이스라고 한다.public interface Runnable { void run(); }람다식() -> { ...}; @FunctionalInterface public interface Calculable { void calculate(int x, int y); }람다식(x, y ) -> { ...}@FunctionalInterface 어노테이션을 사용한 이유는 인터페이스가 함수형 인터페이스를 보장하기 위해서다.붙이는 것은 선택사항이지만, 컴파일 과정에서 추상 메소드가 하나인 검사하기 때문에 정확한 함수형 인터페이스를 작성하게 도와주는 역활을 해준다.매 매개변수가 없는 람다식실행문이 하나일 경우 중괄호를 생략 가능하고 두개 이상일 경우는 생략할 수 없다.() -> 실행문; () -> { 실행문; 실행문; } 매개변수가 있는 람다식매개변수를 선언할 때 타입은 생략할 수 있고, 구체적인 타입 대신에 var를 사용할 수 있다.(타입, 매변수, ... ) -> { 실행문; 실행문; }(타입, 매변수, ... ) -> 실행문;(var 매개변수, ...) -> { 실행문; 실행문; }(var 매개변수, ...) -> 실행문;(매개변수, ...) -> { 실행문; 실행문; }(매개변수, ...) ->실행문;매개변수 -> { 실행문; 실행문 };매개변수 -> 실행문; 리턴값이 있는 람다식return 문 하나만 있는 경우에는 중괄호와 함께 return 키워드를 생략가능하다.(매개변수, ...) -> { 실행문; return 값 )(매개변수, ...) -> 값 메소드 참조메소드를 참조하여 매개변수의 정보 및 리턴 타입을 알아내 람다식에서 불필요한 매개변수를 제거한다. (left, right) -> Math.max(left, right);Math.max() 메소드의 매개값은 전달하는 역활만 하기 때문에 다음과 같이 생략이 가능하다.Math :: max;정적 메소드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 메소드 이름을 기술한다.클래스 :: 메서드참조변수 :: 메소드 매개변수의 메소드 참조(a, b) -> {a.instaceMethod(b);}메소드 참조는 a 클래스 이름 뒤에 :: 기호를 붙이고 매소드 이름을 기술한다.작성 방법은 메소드 참조와 동일하지만, a의 인스턴스 메소드가 사용된다는 점이 다르다.클래스 :: instaceMethod Reference이것은 자바다(책)

워밍업클럽과제

xicodey

[인프런 워밍업 클럽 0기] BE 1일차 과제

어노테이션 사용하는 이유어노테이션은 사용 용도로 3가지가 있습니다.1. 컴파일 시 사용하는 정보 전달2. 빌드 툴이 코드를 자동으로 생성할 때 사용하는 정보 전달3.실행 시 특정 기능을 처리할 때 사용하는 정보 전달컴파일 시 사용하는 정보 전달의 대표적인 예는 @Override 어노테이션입니다.컴파일러가 메소드 재정의 검사를하도록 설정합니다. 재정의되지 않았다면 컴파일러는 에러를 발생시킵니다.웹 개발에 많이 사용하는 Spring Framework 또는 Spring Boot는 다양한 종류의 어노테이션을 사용해서 웹 애플리케이션을 설정하는데 사용합니다.나만의 어노테이션은 어떻게 만들 수 있을까? 어노테이션을 정의하는 방법은 인터페이스를 정의하는것과 유사합니다.@interface 뒤에 사용할 어노테이션을 이름을 정의합니다.오노테이션은 속성을 가질 수 있으며, 속성은 타입과 이름으로 구성됩니다. 속성은 기본값 default 키워드로 지정할 수 있습니다.어떤 대상에 설정 정보를 적용할 것인지, 적용대상을 정의 해야 합니다.클래스 명위에 @Target 어노테이션을 붙어 정의 합니다.적용할 수 있는 대상의 종류는 ElememtType 열거 상수로 정의되어 있습니다.TYPE : 클래스, 인터페이스 열거타입ANOTATION_TYPE: 어노테이션FIELD: 필드CONSTERUCTOR: 생성자METHOD: 메서드LOCAL_VARIABLE: 로컬 변수PACKAGE: 패키지@Target의 기본 속성인 value는 배열을 값을 가질 수 있습니다.어노테이션을 정의할 때 한 가지 더 추가해야 할 내용은 @AnnotationNamed 언제까지 유지 할 것인지 지정하는 우지 정책을 정해야합니다.RetentionPolicy 열거 상수

워밍업클럽과제

이혜리

[인프런 워밍업 클럽 스터디1기] 백엔드 - 6차 과제

진도표 6일차와 연결됩니다우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍   controller, service, repository로 분리해보자.  파일의 폴더 구조는 아래와 같다. controller, domain, dto, repository, service 폴더로 이루어져 있으며, 각각에 해당하는 파일들이 위치한다.  FruitController.javapackage com.group.libraryapp.controller.fruit; import com.group.libraryapp.domain.Fruit; import com.group.libraryapp.dto.fruit.FruitCreateRequest; import com.group.libraryapp.dto.fruit.FruitOverviewResponse; import com.group.libraryapp.service.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @RestController public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } //문제1 @PostMapping("/api/v1/fruit") public void saveFruit(@RequestBody FruitCreateRequest request) { fruitService.saveFruit(request); } //문제2 @PutMapping("/api/v1/fruit") public void saledFruit(@RequestParam int id){ fruitService.saleFruit(id); } //문제3 @GetMapping("/api/v1/fruit/stat") public FruitOverviewResponse overviewFruit(@RequestParam String name){ return fruitService.overviewFruit(name); } } controller 파일은 심플하게 service의 메소드를 불러오는 방식으로 리팩토링했다.즉, 메소드만 명시하고, 실제적인 일은 service, controller가 한다는 뜻!위와 같이 코드를 짬으로서, api 의 진입점의 역할을 잘 하고 있다 볼 수 있다.FruitService.javapackage com.group.libraryapp.service; import com.group.libraryapp.domain.Fruit; import com.group.libraryapp.dto.fruit.FruitCreateRequest; import com.group.libraryapp.dto.fruit.FruitOverviewResponse; import com.group.libraryapp.repository.FruitRepository; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.util.List; @Service public class FruitService { private final FruitRepository fruitRepository; private FruitOverviewResponse fruitOverviewResponse; public FruitService(@Qualifier("sql") FruitRepository fruitRepository, FruitOverviewResponse fruitOverviewResponse) { this.fruitRepository = fruitRepository; this.fruitOverviewResponse = fruitOverviewResponse; } public void saveFruit(FruitCreateRequest request){ fruitRepository.saveFruit(request.getName(),request.getPrice(),request.getWarehousingDate()); } public void saleFruit(int id) { fruitRepository.saleFruit(id); } public FruitOverviewResponse overviewFruit(String name) { List<Fruit> list = fruitRepository.overviewFruit(name); long notsalesamount = 0; long salesamount = 0; for(Fruit e : list){ if (e.getSaled() == 1) notsalesamount += e.getPrice(); else salesamount += e.getPrice(); } fruitOverviewResponse.setNotSalesAmount(notsalesamount); fruitOverviewResponse.setSalesAmount(salesamount); return fruitOverviewResponse; } }FruitService는 FruitRepository의 메소드를 불러오는 역할과 동시에 유저가 있는지 없는지를 확인하고 예외처리를 하는 부분이다.위 클래스의 overviewFruit 함수에서선별한 정보를 아래와 같은 HTTP 응답 Body로 반환하기 위한 처리를 하도록 수정하였다. list 형태의 반환값을 FruitRepository의 메소드로 부터 받은 후, 이를 FruitOverviewResponse 객체로 반환하는 역할을 하고 있다.  FruitRepository.javapackage com.group.libraryapp.repository; import com.group.libraryapp.domain.Fruit; import java.time.LocalDate; import java.util.List; public interface FruitRepository { public void saveFruit(String name, long price, LocalDate warehousingDate); public void saleFruit(int id); public List<Fruit> overviewFruit(String name); }  문제2의 요구사항을 위해, repository를 바꿔가며 동작시킬 수 있도록 강의에서 이 경우에 interface를 작성하였기 때문에 interface 클래스를 작성하였다. @Primary 어노테이션 대신, @Qualifer 어노테이션을 FruitService 클래스에 사용했다.FruitMemoryRepository.javapackage com.group.libraryapp.repository; import com.group.libraryapp.domain.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @Repository public class FruitMemoryRepository implements FruitRepository{ private final JdbcTemplate jdbcTemplate; private List<Fruit> memory = new ArrayList<>(); private int num = 0; public FruitMemoryRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruit(String name, long price, LocalDate warehousingDate) { memory.add(new Fruit(num+1,name, warehousingDate,price,1)); } public void saleFruit(int id){ for (Fruit e : memory){ if (e.getId() == id) e.setSaled(0); else throw new IllegalArgumentException(); } } public List<Fruit> overviewFruit(String name){ List<Fruit> list = new ArrayList<>(); for (Fruit e : memory){ if (e.getName().equals(name)){ list.add(e); }else throw new IllegalArgumentException(); } return list; } }위 클래스는 클래스 내에 List<Fruit> 를 만들어 저장함으로써, 프로그램을 재실행시키면, 기존 정보가 없어지는 repository이다. db와 연결이 없다.FruitMySqlRepository.javapackage com.group.libraryapp.repository; import com.group.libraryapp.domain.Fruit; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.List; @Repository @Qualifier("sql") public class FruitMySqlRepository implements FruitRepository{ private final JdbcTemplate jdbcTemplate; public FruitMySqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruit(String name, long price, LocalDate warehousingDate) { String sql = "INSERT INTO fruits(name, warehousingDate, price,saled) Values (?,?,?,?)"; jdbcTemplate.update(sql, name, warehousingDate,price, 1); } public void saleFruit(int id){ String readSql = "SELECT * FROM fruits WHERE id = ?"; boolean fruitNotExist = jdbcTemplate.query(readSql, (rs,rowNum)-> 0,id).isEmpty(); if (fruitNotExist){ throw new IllegalArgumentException(); } //fruit exists String sql = "UPDATE fruits SET saled = ? WHERE id = ?"; jdbcTemplate.update(sql, 0,id); } public List<Fruit> overviewFruit(String name){ String readSql = "SELECT * FROM fruits WHERE name = ?"; List<Fruit> list = jdbcTemplate.query(readSql, (rs, rowNum) -> { String rs_name = rs.getString("name"); long rs_price = rs.getLong("price"); LocalDate rs_warehousingDate = rs.getDate("warehousingDate").toLocalDate(); int rs_saled = rs.getInt("saled"); return new Fruit(rs_name,rs_warehousingDate,rs_price,rs_saled); }, name); if (list.isEmpty()) throw new IllegalArgumentException(); return list; } }위 부분은 반대로 db와의 연결이 있고, 직접적인 sql 문을 사용하여 작성하였다. @Qualifier 어노테이션이 클래스 위에 붙어있는 것을 확인할 수 있다.회고controller 클래스만 쓰면, 여러 기능이 뭉쳐있어 코드의 가독성이 떨어지는데,위와 같이 3단 분리 및 interface 를 활용하니, 전보다는 클린코드로 작성이 된 것 같다.

백엔드워밍업1기6차과제백엔드

이슬

인프런 워밍업 클럽 스터디 1기 FE 과제(4번, 5번, 6번 과제)

[4번 과제(Day5) - 책 리스트 나열 앱]따라하며 배우는 자바스크립트 A-Z학습 범위: Section 5 ~ 6https://github.com/helloleesul/inflearn-warmup-club-study/tree/main/book-list-app과제 이미지input 전체에 값의 유무에 따라 submit 버튼 활성화를 설정해줘서 정확한 제출 요청이 되도록 작업했다.setTimeout으로 추가, 삭제 알림이 3초 뒤에 사라지게 했다.리스트 그룹에 이벤트리스너를 설정해줘서 이벤트타겟의 태그가 버튼이라면 삭제버튼을 가진 부모요소를 삭제하도록 이벤트 위임으로 작업했다.[5번 과제(Day5) - Github Finder 앱]따라하며 배우는 자바스크립트 A-Z학습 범위: Section 5 ~ 6https://github.com/helloleesul/inflearn-warmup-club-study/tree/main/github-finder-app과제 이미지import { Octokit } from "https://esm.sh/@octokit/core";위 스크립트를 import를 작동시키기위해서 html에 작성한 script태그에 type을 module로 설정하였다.유저의 프로필 정보는 고정적이라 값만 제외하고 퍼블리싱했고, 레포지토리 리스트는 script에서 innerHTML으로 한번에 삽입했다. li 태그를 계속 create하는 것보다 효율적이라고 생각했다.!username && alert("유저 이름을 입력해주세요."); // 변경 전 if (!username) return alert("유저 이름을 입력해주세요."); // 변경 후둘 다 username가 비어있거나 존재하지않을 때에만 조건이 참이 된다.변경 전 usename이 빈 칸일 경우 알림창이 뜨도록 조건식을 작성했는데, 알림창을 한번 나타나게하면 조건식이 끝나도 함수를 이어서 실행시켰다.내가 원하는 건 조건이 참이면 함수를 중지하는 로직을 원하기때문에 return문이 들어갈 수 있도록 조건식을 조건문으로 변경하였다.식과 문을 잘 구별해서 쓰자.[6번 과제(Day6) - 비밀번호 생성 앱]따라하며 배우는 자바스크립트 A-Z학습 범위: Section 7 ~ 8https://github.com/helloleesul/inflearn-warmup-club-study/tree/main/password-generator-app과제 이미지비밀번호를 생성하는 generator 객체를 활용해보았다.generator 안에 아래 작성된 최소, 최대 길이에 맞게 참이 될때만 실행하도록 while 조건문을 설정했다.navigator.clipboard로 복사하기 기능을 구현했다.generator를 이번 강의에서 처음 배우고 사용해봤는데 목적에 맞게 가독성이 좋아보여서 흥미로웠다. 하지만 generator로 구현하는 것과 일반 함수로 구현하는 것의 차이를 아직은 잘 모르겠다.

웹 개발인프런워밍업클럽FE1기과제

이슬

인프런 워밍업 클럽 스터디 1기 FE 과제(1번, 2번, 3번 과제)

[1번 과제(Day2) - 음식 메뉴 앱]따라하며 배우는 자바스크립트 A-Z학습 범위: Section 1 ~ 3https://github.com/helloleesul/inflearn-warmup-club-study/tree/main/food-menu-app과제 이미지데이터를 json파일로 만들어놓고 처음 로드할 때, 불러와서 작업했다.강의에서 배운 이벤트위임을 활용해서 버튼 그룹에 이벤트리스너를 설정해주고 이벤트타겟의 태그가 버튼일 때에만 메뉴를 필터링할 수 있는 함수를 실행하게 했다.[2번 과제(Day3) - 가위 바위 보 앱]따라하며 배우는 자바스크립트 A-Z학습 범위: Section 4(1~8)https://github.com/helloleesul/inflearn-warmup-club-study/tree/main/rock-scissors-paper-app과제 이미지남은 횟수가 끝나면 결과를 알려주는 부분을 잊고 완성했다가 다시 이어서 작업했다.다시 도전하기를 누를때 화면을 초기화하는 함수를 만들었다.[3번 과제(Day4) - 퀴즈 앱]따라하며 배우는 자바스크립트 A-Z학습 범위: Section 4(9~17)https://github.com/helloleesul/inflearn-warmup-club-study/tree/main/js-quiz-app과제 이미지1번 처럼 데이터를 json파일로 만들어놓고 처음 로드할 때, fetch로 불러와서 작업했는데 데이터 객체 안에 문제와 옵션들, 그리고 answer(정답) 키값을 넣었었다. 하지만 브라우저 네트워크에 답까지 노출이 되는 게 문제라고 생각됐다.그래서 answer(정답) 키값을 없애고, 문제와 옵션들만 데이터로 구성하고 script 안에서 정답 유무를 가릴 수 있게 바꿨다. 풀이는 문제를 배열로 해체하고 ×를 *으로, ÷를 /으로 바꾼 후(-,+는 그대로) eval()함수를 쓰지않고 new Function() 새로운 함수를 생성해서 풀이하게 했다. 

웹 개발인프런워밍업클럽FE1기과제

코파

인프런 워밍업 클럽 1기 FE 2번 과제 (가위바위보 게임 앱)

2번 과제 (Day2)(가위바위보 게임 앱)깃허브 저장소 주소 :https://github.com/devellybutton/frontEnd_warmingUp_study/tree/master/02.%EA%B0%80%EC%9C%84%EB%B0%94%EC%9C%84%EB%B3%B4 게임 규칙:사용자는 바위, 가위, 보 중 하나를 선택한다.컴퓨터는 무작위로 바위, 가위, 보 중 하나를 선택한다.사용자와 컴퓨터의 선택을 비교하여 승부를 결정한다.각 회차별 승부 결과와 점수는 화면에 표시된다.10회 진행 후 게임 종료. 최종 승부 결과와 재시작 버튼이 나타난다.재시작 버튼을 누르면 게임을 다시 시작할 수 있다. GIF:게임의 각 회차별 기록, 시작과 끝을 명확히 알 수 있도록 화면에 각 수치들을 렌더링 함. 가위바위보 버튼을 지나치게 빨리 누르는 것을 방지하기 위해 최소 0.5초에 1번씩 작동하도록 함. 간단한 후기 :간단한 로직이었기에 게임 자체는 금방 완성했지만, 예상보다 훨씬 많은 시간(^^)을 버그 수정에 투자해야 했습니다. 이 과정에서 코드의 중요성, 특히 로직 분리의 중요성을 절실히 깨달았습니다. 처음에는 모든 코드를 하나의 덩어리로 작성했기 때문에 버그를 찾기가 매우 어려웠습니다. 하지만 로직을 함수단위로 분리한 후에는 각 부분을 차근차근 검사하면서 문제를 해결할 수 있었습니다.특히, 게임 재시작 시 count가 2회씩 감소하는 문제는 이벤트 리스너가 중복 등록되어 발생하였습니다. 이 문제를 해결하기 위해 등록된 이벤트 리스너를 제거하고 로직을 분리하는 방식으로 코드를 수정했습니다. 아직 이벤트에 대해 깊게 공부해보지 못하였고, 다양한 이벤트를 사용해보지는 못해서 아쉽습니다.이번 과제를 통해 깔끔하고 체계적인 코드 작성의 중요성을 다시 한번 느꼈습니다. 이미지 출처 :https://www.vectorstock.com/royalty-free-vector/rock-paper-scissors-hand-gesture-vector-25169740개발할 때 참고한 링크 :https://oursmalljoy.com/css-%EC%9B%90-%EA%B7%B8%EB%A6%AC%EA%B8%B0-border-radius%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%9E%88-%EB%A7%8C%EB%93%A4-%EC%88%98-%EC%9E%88%EB%8B%A4/

웹 개발워밍업클럽FE과제