블로그

최경희

[인프런 워밍업 클럽 0기] FE 2주차

2주차 학습 요약(8일차는 중간 점검으로 학습할 강의가 없었습니다.)7일차 - 프로젝트 만들기자바스크립트만으로 Stop Watch 앱과 할 일을 추가하고 삭제하는 Todo 앱, SpeadSheet(엑셀) 앱의 기능을 구현하고, 꾸미는 방법을 배웠습니다. 앱을 만들면서 localStorage를 사용하는 방법을 새로 알 수 있었습니다. 9일차 - 리액트 기본 및 Todo 앱 만들기리액트란 강의에서 리액트에 대한 개념과 리액트에서 사용하는 컴포넌트에 대한 개념 및 종류를 알 수 있었고, 리액트의 가장 중요한 특징인 가상돔을 알기 위해 브라우저가 렌더링하는 과정을 함께 배웠습니다. 그 외 리액트를 사용하기 위한 여러가지 환경 설정을 하는 방법을 알 수 있었습니다. Todo 앱을 만들면서 리액트의 주요 기능인 JSX와 State, Props, Hooks를 배울 수 있었고, 그 외 CSS 프레임워크 중 하나인 TailWind CSS에 대해서도 배울 수 있었습니다. 10일차 - 리액트로 Netflix 만들기Netfilx 클론 앱을 만들면서 API를 사용하는 방법과 axios를 사용하는 방법, 커스텀 Hooks을 작성하는 방법에 대해 배울 수 있었고, Styled Component 라는 CSS 관련 라이브러리를 사용하여 꾸미는 방법과 react-router-dom을 사용하여 페이지 전환을 하는 방법을 새로 알 수 있었습니다. 미션 해결 과정► 과제 총 합본 : https://www.inflearn.com/blogs/6988Day2 미션 : 음식 메뉴 앱각 메뉴를 클릭할 때, 메뉴에 맞는 음식 리스트를 보여주는 앱 입니다.음식 메뉴가 아닌 투썸 플레이스 카페 메뉴로 변경했고, 메뉴 부분의 스타일과 전체적인 색상을 변경했습니다.이미지는 투썸 플레이스 카페의 홈페이지에서 이미지 주소 복사를 통해 가져왔는데, 저작권 문제 때문에 저작권 문제가 없는 다른 이미지를 구해야하나 고민하고 있습니다. 스타일을 수정할 때 가장 시간이 오래 걸렸는데, 간격 조절이 가장 어려웠던 것 같습니다. 저번주에 막혔던 화면 크기로 인한 넘침 문제를 조금이나마 고칠 수 있게 되어, 현재 화면 크기에 따라 한줄에 나열되는 음식 아이템들의 개수가 동적으로 변경되게 했습니다. 처음에는 맥에서 진행해서 몰랐는데, 윈도우에서는 스크롤 바로 인한 밀림 현상이 있는 것을 알게 되어 스크롤 바를 항상 보이게 하는 것으로 수정했습니다. Day3 미션 : 가위 바위 보 앱컴퓨터와 진행하는 가위 바위 보 게임으로 총 10번을 진행하여 승부를 내는 앱 입니다.전체적인 디자인과 색상을 변경했고, 가위, 바위, 보 대신에 아이콘을 사용하여 표현했습니다.저번 주에 진행했던 것과 다르게 전체적으로 다시 꾸몄습니다. 태그 마다 간격을 설정하는 것이 가장 어려웠는데, 사용하는 맥과 윈도우의 화면 크기 차이로 인해 서로 다르게 보이는 문제가 있었고, 최대한 화면 크기와 상관없이 동일한 위치에서 보일 수 있게 개발 했습니다. Day4 미션 : 퀴즈 앱수학 연산 관련 질문이 나오고, 2~3개의 답변 중 맞는 답변을 선택하면 배경이 초록색이 되고 틀린 답변을 선택하면 배경이 빨간 색이 되는 앱 입니다.맞춤법 관련 퀴즈 앱을 만들었습니다. 전체적인 디자인을 변경했고, 배경이 아닌 선택한 답변의 버튼 색이 바뀌게 했습니다. 또한 마지막에 맞은 답변의 개수를 알 수 있게 했습니다.네이버의 한글날 맞춤법 테스트 사이트를 참고하여 만들었습니다. 질문 개수를 많이 만들어서 질문이 랜덤으로 나오게 하고 싶었지만 시간이 없어서 못한 것이 아쉬웠습니다. 다음에 시간이 생기면 변경해보고 싶고, 이것을 바탕으로 심리 사이트 앱을 만들어도 괜찮겠다는 생각을 하게 되었습니다. Day5 미션 : 책 리스트 나열 앱 책 이름과 저자를 입력하여 제출하면 책 리스트에 추가가 되며, 삭제할 수도 있고 추가 및 삭제가 발생할 때마다 알림이 나오는 앱 입니다.전체적인 색상만 변경하고, 구조는 과제 영상과 비슷하게 만들었습니다. 또한 제출 버튼을 클릭했을 때 이름과 저자를 모두 입력하지 않으면 제출되지 않게 만들었습니다.다른 과제처럼 저만의 디자인으로 변경하고 싶었지만, 시간도 부족하고 생각나는 디자인이 없어 과제 영상과 비슷하게 만든 것이 아쉬웠습니다. 다른 과제를 진행하면서 간격 조절하는 것이 익숙해진 것인지 태그들의 간격을 정하는 것이 다른 과제에 비해 수월하게 진행됐습니다. Day9 미션 : 예산 계산기 앱 지출 항목과 비용을 입력하여 제출하면 리스트에 추가되고 리스트에 있는 모든 항목의 총 지출 값을 확인할 수 있으며, 수정 및 삭제가 가능하고 추가,수정,삭제가 발생할 때마다 알림이 나오는 앱 입니다.전체적인 색상만 변경하고, 과제 영상과 비슷하게 만들었습니다. 또한 지출 항목을 입력하지 않으면 제출이 되지 않게 만들었습니다.처음 만드는 리액트 앱이라 만드는데 시간이 오래 걸렸습니다. 알림이 나오고 일정 시간이 지나면 사라지는 기능을 구현할 때 문제가 생겼었습니다. 알림이 연속적으로 추가됐을 때, 각 알림을 기준으로 3초 후에 사라져야 하는데 마지막으로 추가된 알림을 기준으로 3초 후에 첫 알림이 사라지기 시작하는 오류였습니다. 리액트라서 어려울 거라 생각하고 복잡하게 생각해서 생긴 문제로, 현재는 오류가 해결되어 잘 작동됩니다. Day10 미션 : 디즈니 플러스 앱API를 사용하여, 구글 연동 로그인을 진행하고 영화 리스트와 상세 정보를 확인하고 검색도 가능한 앱 입니다.현재 개발 중으로 아직 완성되지 않았습니다. 회고스터디 이전에 리액트 기초에 대해 조금은 공부하고 와서 개념은 알고 있는 상태였습니다.하지만 리액트를 사용하여 프로젝트를 진행한 적은 한번도 없어서 리액트 강의가 좀 어려웠습니다. 특히, API를 사용하여 개발을 해본 적이 없어 이해하기가 어려웠습니다. 그래서 과제인 디즈니 플러스 앱을 만들어 보면서 API를 사용하는 방법에 대해 자세히 알아보려고 합니다.결국 시간이 없어서 자바스크립트 관련 과제는 4개만 완성할 수 있었습니다. 그리고 지금 제 상태를 봐서는 리액트 관련 과제도 전부 완성할 수는 없을 것 같아 아쉽습니다. 과제를 진행하면서 직접 생각하고 알아보는 과정을 통해 제 실력이 늘고 있다는 것이 느껴져 스터디가 끝나고 난 후에라도 남은 과제를 만들려고 합니다.다음주 동안 최대한 리액트 과제를 열심히 만들어서 적어도 4개의 과제를 완성하고 싶습니다.

프론트엔드

Dream

[ 인프런 워밍업 클럽 Study FE 0기 ] Week 2 발자국

발자국워밍업 스터디 클럽이 2주 차에 접어들었습니다. 해당 발자국은 따라 하며 배우는 리액트 A-Z (섹션 0-5) 중심으로 작성되었습니다.요약React를 사용하려면 Node.js가 필요하다. Node.js를 설치하면 NPM도 같이 설치되니 꼭 Node.js를 설치하자. Node.js 공식 홈페이지에 접속하면 2개의 Node 버전이 있는데, 그중에서 안정적인 버전인 LTS를 설치하면 된다. Section 01. React[ React란? ]리액트는 사용자 인터페이스(user interface, UI)를 만들기 위해서 사용되는 자바스크립트의 라이브러리다. 리액트는 인터렉션이 많은 웹 앱을 개발하기 위해서 주로 사용된다. 이렇게 사용자 인터페이스를 만들기 위해 도움을 주는 TOOL로는 리액트 말고도 Vue.js와 Angular.js가 있다.React: 라이브러리Angular, Vue: 프레임워크[ Framework vs Library ]프레임워크와 라이브러리를 대략 설명하자면 다음과 같다.Framework : 어떠한 앱을 만들기 위해 필요한 대부분의 것을 가지고 있다.Library : 어떠한 특정 기능을 모듈화 해 놓은 것이다.프레임워크는 앱을 만드는데 필요한 대부분의 라이브러리를 가지고 있으며, 라이브러리들은 특정 기능을 위해 모듈화 되어있다.리액트는 라이브러리이다. 왜냐? 리액트는 전적으로 UI를 렌더링 하는 데 관여하기 때문이다. 리액트는 여러 모듈을 사용하며 앱을 관리한다.라우팅: react-router-dom …상태관리: redux, mobx …빌드: webpack, npm …테스팅: Eslint, Mocha …[ React Component ]리액트를 공부하다보면 무조건 마주치는 단어가 있다. 바로 컴포넌트이다. 리액트는 컴포넌트 기반이라고 하는데, 이 컴포넌트는 무엇을 말하는 것일까?컴포넌트(Component): React로 만들어진 웹/앱을 이루는 최소한의 단위리액트는 이 컴포넌트를 통해서 웹/앱을 개발하게 된다.리액트는 여러 컴포넌트 조각으로 되어있다. 이것은 블록같다고 생각하면 된다. 여러 블록 조각을 맞추고 쌓아 올려 하나의 블록 작품을 완성하는 것. 리액트도 마찬가지로 컴포넌트를 이리저리 조합하고 쌓아올려 하나의 웹 페이지를 구성하게 된다.리액트 컴포넌트에는 2가지가 있다.클래스형 컴포넌트함수형 컴포넌트React는 여러 컴포넌트 조각으로 구성된다.개인적인 설명을 덧 붙이자면 리액트는 레고 블럭과 같다고 생각한다. 레고 블럭들을 하나 둘 씩 쌓아 올려 하나의 완성된 레고 작품을 만드는 것이다.[ Component 종류 ]React는 2개의 컴포넌트 종류가 있다.클래스형 컴포넌트(Class Components)class App extends Component { render() { return <h1>Hello, ReactJS!</h1>; } }함수형 컴포넌트(Functioanl Components)function App() { return <h1>Hello, ReactJS!</h1>; }💡 현재 함수형 컴포넌트를 HOOK이랑 해서 많이 사용한다.💡 참고로 컴포넌트를 작성할 때 반드시 대문자 시작을 해야 한다. 소문자 시작 시 body, h1, p 같은 DOM 태그로 인식해 버린다.[ 브라우저가 그려지는 원리와 가상 돔 ]React의 가장 큰 특징은 가상 돔(Virtual DOM)이다. 이것을 사용하는 이유는 인터렉션 때문이다. 이 인터렉션에 의해 DOM에 변화가 발생하면 다시 DOM을 재구성하기 시작한다.JS 발자국에도 남겼었지만 웹 브라우저의 경우 다음과 같은 과정을 겪고 이 과정은 비용이 꽤 든다.Critical Render Path (웹 페이지 렌더링 과정): 데이터 파싱(HTML) ➔ DOM Tree 생성 ➔ CSSOM Tree 생성 ➔ JS 실행 ➔ Render Tree 생성 ➔ Layout 생성 ➔ PaintDOM을 재구성 한다는 것은 위 렌더링 과정을 다시 반복한다는 것이다. 즉, 인터렉션이 일어날때마다 위 과정을 다시 한다. 이것을 보완하기 위해서 나온 것이 가상 돔이다.가상돔 과정을 살펴보자..!데이터가 바뀌면 가상 돔에 렌더링 되고, 이전에 생긴 가상 돔과 비교를 해서 바뀐 부분만 실제 돔에 적용 시킨다.바뀐 부분을 찾는 과정을 Diffing이라고 부른다.바뀐 부분만 실제 돔에 적용 시키는 것을 Reconciliation(재 조정)이라고 부른다.[ Create React App 을 이용해서 리액트 설치하기 ]create-react-app 을 통해서 원하는 위치에 리액트를 설치할 수 있다. 이 때, Webpack과 Babel이 함께 설치가 된다. 따라서 React 앱 생성 전에 Webpack과 Babel에 대해서 간단히 알고 가자.Webpack정의: Webpack: 웹팩은 오픈 소스 자바스크립트 모듈 번들러써 여러 개로 나누어져 있는 파일들을 하나의 자바스크립트 코드로 압축고 최적화하는 라이브러리이다.장점여러 파일의 자바스크립트 코드를 압축하여 최적화할 수 있기 때문에 로딩 줄일 수 있. (네트워크 비용 줄음)모듈 단위로 개발이 가능하여 가독성과 유지 보수가 쉽다.Babel정의: 최신 자바스크립트 문법을 지원하지 않는 브라우저들을 위해서 최신 자바스크립트 문법을 구형 브라우저에서도 돌 수 있도록 변환 시켜주는 라이브러리이다.⇒ 이러한 Webpack과 Babel은 개발자가 React 개발 시 알아서 설정 해야 하지만 Create-React-App을 사용해서 React 앱을 생성하면 Babel이나 Webpack 설정이 이미 되어있기 때문에 준비 시간이 단축된다.[ Create-React-App ]프로젝트를 진행할 폴더 생성VSC에서 해당 프로젝트 폴더 열기Termial에 npx create-react-app 생성할파일명 입력강의에서는 npx create-react-app ./을 입력하였다../는 현재 위치를 뜻한다.[ npx create-react-app 에 대하여 ]npx: 노드 패키지 실행을 도와주는 도구이다.npx create-react-app이란 npm 레지스트리에 잇는 패키지를 ./에 실행시켜서 React를 설치해 주는 것이다.실행하는 방법실행하고자 하는 리액트 파일 위치에서 npm run start 입력(강의에서는 npm run start방법만 소개시켜 주셧는데 npm start도 가능하다.)🤔 개인적으로 요즘 vite에 관한 이야기가 보이는데 이도 조사해 보아야겠다… Section 2. 간단한 To-Do 앱 만들며 리액트 익히기[ create react app ]create-react-app으로 리액트를 설치하면 여러 파일이 등장하는데, 이 중에서 절대로 이름을 수정해서는 안되는 파일이 존재한다.public/index.html: 페이지 템플릿src/index.js: 자바스크립트 시작 점조심하자!💡그리고 우리가 새로 js, jsx, css 등 직접 생성할 파일들은 src 폴더에서 하면 된다. Webpack이 src/ 부분에만 작동하기 때문이라고 한다.[ package.json ]해당 프로젝트에 대한 정보들이 들어있다. 프로젝트 이름, 버전, 필요한 라이브러리와 라이브러리들의 버전이 명시되어 잇다.[ 싱글 페이지 애플리케이션(single-page application, SPA) ]싱글 페이지 애플리케이션(single-page application, SPA)은 서버로부터 완전한 새로운 페이지를 불러오지 않고 현재의 페이지를 동적으로 다시 작성함으로써 사용자와 소통하는 웹 애플리케이션이나 웹사이트를 말한다.위키백과React.js는 SPA이다. 즉, 어떠한 데이터에 관한 교체 이벤트가 발생했을 때, 서버로부터 페이지를 새롭게(html 파) 받아와 구성하는 것이 아니라 content를 바꿔치기 한다. 이는 HTML 5의 History API를사용해서 가능하게 만든다.[ History API ]전통적인 웹 사이트는 a page에서 b page로 이동할 때 a.html을 보여주다가 b.html을 보여주면 되었지만 SPA의 경우 오직 1개의 HTML(index.html)이 존재한다. 따라서 페이징 전환을 하기 위해서 HTML 5 History API를 이용한다.History.back(): 세션 기록의 바로 뒤 페이지로 이동하는 비동기 메서드History.forward(): 세션 기록의 바로 앞 페이지로 이동하는 비동기 메서드History.go(): 특정한 세션 기록으로 이동하게 해 주는 비동기 메서드History.pushState(): 주어진 데이터를 세션 기록 스택에 넣어준다.History.replaceState(): 최근 세션 기록 스택의 내용을 주어진 데이터로 교체한다.생성했던 React 프로젝트에서 public/index.js를 살펴보면 <div id="rood"></div>가 있다.그리고 src/index.js 코드에는 document.getElementById('root')라는 코드가 있다.자바스크립트 파일의 시작 점인 src/index.js에서 id값이 rood인 요소를 찾아 그곳에 해당 요소들을 렌더링하는 것이다. 즉, div라는 최상위 루트 노드 아래에 직접 정의한 요소를 더해 화면을 꾸며나가는 것이다!![ JSX ]JSX는 Javascript Syntax Extension의 약자로 자바스크립트의 확장 문법이다.리액트에서는 이 JSX를 이용해서 화면에서 UI가 보이는 모습을 나타내준다.JSX 사용이 필수는 아니나 사용하면 가독성이 너무 좋아서 필수 아닌 필수이다. (애초에 리액트 개발자들 대부분이 JSX를 사용한다고 한다.)JSX는 createElement를 쉽게 사용하기 위해서 사용한다.모든 UI를 만들때 마다 createElement를 사용해서 컴포넌트를 만들 수 없다.Ract는 React.crateElemnt API를 사용해서 엘리먼트를 생성한 후에 이 엘리먼트를 In-Memory에 저장한다. 그리고 RaectDOM.render 함수를 통해 웹 브라우저에 그린다.JSX를 사용하면 Babel이 사용한 문법을 crateElemnt로 자동 변환해준다. 따라서 그냥 개발자는 자유롭게 JSX 사용하면 된다.단, JSX는 컴포넌트에 여러 요소가 있다면 반드시 부모 요소 하나로 감싸줘야 한다.// 안된다. // 자식 요소가 여러 개 라면 부모 요소로 감싸줘라. function hello() { return ( <div>Hello, Raect!</div> <div>Hello, Wrold!</div> ); } // 이렇게 말이다. function hello() { return <div> <div>Hello, Raect!</div> <div>Hello, Wrold!</div> </div>; }💡 만약 JSX에서 JS 코드를 사용하고 싶다면 { } 내부에 작성해주면 된다.[ React와 Key ]map()을 사용한다면 언제나 명심해야 하는 것. KEY. 이것을 넣지 않는다면 에러가 발생한다.React에서 요소의 리스트를 나열할 때는 Key를 넣어줘야 한다. Key는 React가 변경, 추가 또는 제거된 항목을 식별하는 데 도움이 된다.추가적으로, 이 Key에 지정하는 값은 순회하고자 하는 목록의 아이템에 대한 ID 값이면 된다. 즉, 고유한 값이여야 한다. 정 없으면 index 넣어도 되지만 index 값은 권장하지 않는다.리액트는 가상 돔을 이용해서 바뀐 부분만 실제 돔에 적용한다. 그렇다면 리스트를 나열할 때 바뀐 부분만 어떻게 찾을까? 바로 이 key를 이용해서 어떠한 부분이 바뀌었는 인식하는 것이다.[ state ]정말 정말 중요한 개념!!!리액트에서 데이터가 변할 때 화면을 다시 렌더링 해주기 위해서 React State를 사용한다. State란 무엇일까?간단히 말해서 변수이다.단, 이 변수의 값이 변경되면 컴포넌트들이 재렌더링 된다.state에는 리액트의 흐름에 관한 데이터와 관련이 있다. Section 3. To-Do 앱 최적화 하기[ React HOOK ]엄청나게 중요하다. 이 HOOK은 클래스형 컴포넌트처럼 함수형 컴포넌트에서도 state와 생명주기 메서드를 사용할 수 있도록 해주는 메서드이다.클래스 형 컴포넌트에서는 Mounting, Updating, Unmounting 3단계 따라서 생명주기 메서드를 제공한다.Mounting: componentDidMount()Updating: componentDidUpdateUnmounting: componentWillUnmount()함수형 컴포넌트에서는 이를 위해 HOOK을 사용한다.[ HOC(Higher Order Component) ]화면에서 재사용 가능한 로직만을 분리해서 component로 만들고, 재사용 불가능한 UI와 같은 다른 부분들은 parameter로 받아서 처리하는 방법이다.HOC는 HOOK이 나오기 전에 사용했던 부분이다.Wrapper가 많아지면 흐름 파악이 어려워서 이제 잘 안 쓴다.HOC를 만들고 싶으면 Custom HOOK을 사용하자.[ HOOK ]기본적으로 알고 있어야 할 HOOK은 다음과 같다.useState()리액트의 유동적인 데이터들은 state에 담아 사용하기 위해 이용하는 HOOK클래스형 컴포넌트의 setState와 같이 state 객체에 대한 업데이트 실행단!!! state 변경 시 재 렌더링이 일어남useEffect()사이드 이팩트 처리 HOOK클래스형 컴포넌트의 생명 주기 함수 역할 수행useMemo()최적화 용 HOOK, 의존성 배열에 따라 작동의존성 배열에 있는 값이 변하면 지정한 함수를 실행하여 해당 반환 값을 반환useCallback()최적화 용 HOOK, 의존성 배열에 따라 작동의존성 배열에 있는 값이 변하면 함수를 반환useRef()요소의 참조를 위해 사용하는 HOOK[ Props ]Props는 Properties의 줄임말상위 컴포넌트에서 하위 컴포넌트로 데이터를 전송하고 싶을 경우 사용읽기 전용으로 자녀 컴포넌트에서 강제로 이 값을 변경할 수 없다.전달 받은 props가 state고 이 값을 바꾸고 싶다면 props로 set함수를 넘기고 이것을 이[ TailWindCSS ]HTML 안에서 CSS 스타일을 할 수 있게 해주는 CSS 프레임워크빠른 스타일 작업 가능id 혹은 class 명을 작성하기 위해 머리를 혹사 시키지 않아도 된다.class에 특정 키워드를 넣어서 CSS 조작정해진 속성 키워드가 워낙 많으니 공식 홈페이지 검색 필수다[ 리액트 불변성 ]불변성을 지키며 개발을 하자!참조 타입에서 객체나 배열의 값이 변할 때 원본 데이터가 변경되면 예상치 못한 오류가 발생할 수 있다.불변성을 지킬 수 있는 참조 관련 메서드:spread operator, map, filter, slice, reduce불변성을 해치는 참조 관련 메서드:splice, push[ React.memo ]React.memo는 Higher-Order Components(HOC)이다. 불필요한 컴포넌트 렌더링을 방지할 수 있게해준다. (일종의 최적화 용 HOC) Section 4-5. Netflix 앱 만들기주로 실습 내용 이었다. 정리할 이론만 추려내 보겠다.[ Styled Component ]자바스크립트 파일 안에서 CSS를 처리할 수 있게 해주는 라이브러리[ React Router Dom ]React Router DOM을 이용하면 웹/앱에서 동적 라우팅을 구현할 수 있다. 라우팅이 실행 중인 앱 외부의 구성에서 처리되는 기존 라우팅 아키텍처와 달리 React Router DOM은 앱 및 플랫폼의 요구 사항에 따라 컴포넌트 기반 라우팅을 용이하게 한다.React Router DOM을 사용하기 위해서는 몇 가지 설정을 해야한다.index.js에서 BrowerRouter로 루트 컴폰너트를 감싸준다.BrowserRouter은 HTML 5 History API를 사용하여 UI를 URL과 동기화 된 상태로 유지해준다.import { BrowserRouter } from 'react-router-dom'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <BrowserRouter > <App /> </BrowserRouter> </React.StrictMode> ); 여러 컴포넌트 생성 및 라우트를 정의한다.Routes와 Route를 사용한다.Routes: 앱에서 생성될 모든 개별 경로에 대한 컨테이너 상위 역할을 한다.Route: 단일 경로를 만드는 데 사용된다.path 속성: 원하는 컴포넌트의 URL 경로를 지정한다.element 속성: 경로에 맞게 렌더링 되어야 하는 컴포넌트를 지정한다.import { Routes, Route } from "react-router-dom"; function App() { return ( <div className="app"> <Routes> <Route path="/" element={<Home />}> <Route path="about" element={<About />} /> <Route path="contact" element={<Contact />} /> </Route> </Routes> </div> ); } +) <Link />를 통해 경로 이동하기Link 구성 요소는 HTML의 a 태그와 유사하다.to 속성은 링크가 유저를 데려가는 경로를 지정한다.앱 구성 요소에 나열된 경로 이름을 생성했기 때문에 링크를 클릭하면 경로를 살펴보고 해당 경로 이름으로 구성 요소를 렌더링한다.import { Link } from "react-router-dom"; function Home() { return ( <div> <h1>홈페이지</h1> <Link to="about">About 페이지를 보여주기</Link> <Link to="contact">Contact 페이지를 보여주기</Link> </div> ); } [ 중첩 라우팅 ]라우팅은 중첩 처리가 가능하다.[ Outlet ]자식 경로 요소를 렌더링하려면 부모 경로 요소에서 Outlet를 사용해야한다.하위 경로가 렌더링될 때 중첩된 UI가 표시될 수 있다.부모 라우트가 정확히 일치하면 자식 인덱스 라우트를 덴더링하거나 인덱스 라우트가 없으면 아무것도 렌더링하지 않는다.[ useNavigate ]경로를 바꿔준다.naviate(”/home”) ⇒ localhost:3000[ useParams ]:style 문법을 path 경로에 사용했다면 useParams()로 읽을 수 있다.function test() { return ( <Routes> <Route path="invoices/:invoiceId" element={<Invoice />} /> </Routes> ); } function Invoice() { let params = useParams(); return <h1>Invoice {params.invoiceId}</h1>; } [ useLocation ]현재 위치의 객체를 반환현재 위치가 변경될 때마다 일부 side effect를 수행하려는 경우 유용하다.[ useRoutes ]<Routes>와 기능적으로 동일하나 <Route> 요소 대신 자바스크립트 객체를 사용하여 경로를 정의한다.일반 <Route> 요소와 동일한 속성을 갖지만 JSX가 필요하지 않는다.[ Custom HOOK ]개발자가 정의하는 HOOK이다.HOOK의 이름은 use로 시작해야 한다.참고로 HOOK은 함수형 컴포넌트 또는 커스텀 HOOK에서만 호출이 가능하다.따라서 커스텀 HOOK도, 함수용 컴포넌트 또는 HOOK 내부에서 호출되어야 한다.강의에서는 useDebounce과 useOnClickOutside HOOK을 만들었다.useDebounce: input 요소에서 데이터 입력이 발생하면 설정한 set함수 때문에 매번 state 값이 바뀌고 재 렌더링이 일어난다. 따라서 keyup 이벤트의 처리를 지연시키는 커스텀 HOOK이다. (코드는 강의를 참고하자!)useOnClickOutside HOOK: 모달 창 밖의 부분을 클릭하면 해당 모달 창이 꺼지는 기능을 수행하는 HOOK이다. (코드는 강의를 참고하자!)이런 식으로 HOOK을 만들고 활용하는구나 싶었다…미션과제 총 합본 https://www.inflearn.com/blogs/7021 JS 미션 03. 퀴즈 앱[ 구현 해야하는 기능 ]1. 퀴즈 문제, 문제에 해당 하는 선택지 (선택지의 갯수가 매번 다름)2. 답 선택 시, 정답 여부에 따라 배경의 색상이 변경되어야 함문제는 data.json을 직접 작성하여 동적 생성했습니다. JS 복습 겸으로 해당 주제로 퀴즈 앱을 간단하게 만들어 봤습니다. 미션을 진행하며 문제는 없었습니다. JS 미션 04. 책 나열 앱[ 구현 해야하는 기능 ]1. 책 이름 입력 란2. 책 저자 입력 란3. 제출 버튼을 누르면 입력한 정보를 저장 함3-1. 제출 시 제출 했다는 안내 문구 떠야 함4. 제출된 데이터는 책 리스트에 출력 됨아이템은 다음과 같은 기능을 가져야 함5-1. 표기 할 데이터: 책 이름, 저자5-2. 각 아이템에는 삭제 기능이 있어야 함 구현하는데 문제가 없었습니다. REACT 01. 예산 계산기[ 구현 해야하는 기능 ]1. 지출 항목 입력 란2. 지출 비용 입력 란3. 제출 버튼을 누르면 입력한 정보를 저장 함3-1. 제출 시 제출 했다는 안내 문구 떠야 함아이템은 다음과 같은 기능을 가져야 함5-1. 표기 할 데이터: 지출 항목, 지출 비용5-2. 각 아이템에는 수정 및 삭제 기능이 있어야 함수정 버튼 클릭 시 수정 모드로 변경전체 삭제 기능이 있어야 함정말 막힘 없이 진행되다 딱 한 군데에서 문제를 맞았습니다. 상황에 맞게 알림을 띄우는 기능이었는데, JS에서는 아무런 문제 없이 해결했던 이 기능을 React에서 구현 하려고 하니 이상한 문제가 발생하더군요. 여러 동작을 해서 메시지가 많이 발생할 경우, 메시지가 예시처럼 모두 생성되는 것이 아니라 같은 자리에서 텍스트만 바뀌어서 출력이 되었습니다. 물론 잘 해결해서 과제를 마쳤습니다.회고워밍업 스터디의 2주 차에 진입하며 자바스크립트 공부를 마치고 새롭게 React 공부를 진행하며 React의 다양한 기술을 접하게 되었습니다. 특히 state, props, hook, 그리고 라우팅 부분은 처음에는 이해하기가 어려웠습니다. 그러나 부족한 이해를 보완하기 위해서 강의 내용을 정리하고, 추가적인 학습 자료를 찾아가며 개념을 확실히 파악하려고 노력했습니다.React 학습을 마치고 시작한 미션도 초반에는 막막함을 느꼈지만 코드를 작성해 나가며 수업 때 배운 내용을 적용해 가며 문제를 해결해 나갔습니다.워밍업 스터디도 이제 끝을 향해 가네요. 마무리되는 날까지 열심히 학습에 참여하고 미션 해결을 위해 도전해 보겠습니다. 

프론트엔드워밍업클럽FE

학생

[2주차 발자국] 인프런 워밍업 클럽 스터디 0기 FE

회고벌써 과정의 2/3가 끝났다는 것이 믿기지 않는다.일주일 간의 회고를 해보자면, 아쉬움이 크다. 이번주는 시간 투자를 많이 하지 못했기 때문이다.저번주에 의욕 넘치게 미션을 진행하느라 밀려났던 다른 일을 처리하는데에 또 시간을 썼다.앞으로 일이든 취준이든 제대로 하려면 시간 분배하는 법을 익혀야 한다는 생각이 들었다.직장 등의 일과 병행하는 다른 분들이 존경스러울 뿐이다.하지만 모두 끝냈기 때문에 이제는 공부에 전념할 일밖에 남지 않았다.모든 미션을 반드시 갈아치워버리겠다. 강의를 들으며 그동안 키워드만 알고 이해하지는 못하고 있었던 개념들을 접하면서 굉장히 반가웠다.특히 프로젝트를 구하는 글에서 종종 SPA 할 줄 아시는 분이라는 글을 봤었는데, 정확히 무슨 뜻인지이해를 하지는 못했었다. 하지만 이번에 공부하면서 정확히 알게 되어 상당한 쾌감을 느꼈다. 그밖에도 평소 무료 사이트의클론 코딩을 무작정 따라하면서 접했던 개념들을 짚고 넘어가게 되어 재미있었다.모닥불에 종종 들어오시는 강사님으로부터 유익한 정보를 얻으려면 질문할 거리가 있어야 한다. 질문할 거리가 있으려면 스스로의 힘으로는 알아내지 못하는 부분이 뭔지 구별할 수 있을 정도로 공부가 되어있어야 한다. 따라서 마지막 주 만큼은 진도를 잘 따라가 질문을 생성할 수 있게끔 되었으면 좋겠다. 리액트 및 프로젝트 부분은 특히나 한 번 보고는 잊어버릴 것 같으므로 여러번 공부를 해야할 것 같다. 시간은 한정되어있으므로 다음주는 선택과 집중이 필요해질지도 모르겠다. 이번주 반성공부 및 미션에 시간투자를 얼마 못함다음주 목표미션 모두 보강 및 완료하기(시간 부족하면 CSS 제외하고서라도)필수지식: API 통신, RESTful API, CRUD, CI/CD CSS flexbox, CSS Grid 공부하기Github README에 GIF(혹은 이미지)도 올리기 강의 요약이번주는 JS프로젝트로 자바스크립트를 마무리하고, 리액트를 시작했다.(자바스크립트 섹션 9 & 리액트 섹션 1 ~ 5)강의 요약은 기억에 남는 부분 위주로, 최대한 개념 부분만 진행했다.ReactReact에 관해리액트 컴포넌트가상 돔create react appSPAJSXReact StateReact HooksTailwindCSSStyled ComponentREACTReact에 관해React는 프레임워크가 아닌 라이브러리다.프레임워크: 어떠한 앱을 만들기 필요한 대부분의 것을 가지고있는것. 라이브러리를 포함.라이브러리: 특정 기능을 모듈화해놓은 것.보통 리액트와 잘 맞는 라이브러리와 함께 리액트를 사용한다.React Component리액트로 만들어진 앱을 이루는 최소한의 단위하나의 페이지를 구성하는 각각의 기능을 위한 컴포넌트가 있다.검색 컴포넌트, 프로필 컴포넌트, ...하나의 컴포넌트를 여러 곳에서 사용할 수 있다.리액트 컴포넌트의 두 가지클래스형 컴포넌트함수형 컴포넌트Virtual DOM(가상 돔)실제 DOM을 메모리에 복사해준 것.리액트의 큰 특징 중 하나는 가상 돔을 사용한다는 것.복습: 웹 페이지 빌드 과정(Critical Rendering Path CRP)DOM tree 생성(html문서를 읽고 렌더링할것 결정)Render tree 생성: DOM과 CSSOM 결합. 화면에 표시되는 모든 콘텐츠 및 스타일 정보를 포함Layout: 브라우저가 페이지에 표시되는 각 요소의 크기와 위치 계산Paint: 실제 화면에 그리기어떤 인터랙션에 의해 DOM에 변화가 발생하면 그 때마다 Render Tree가 재생성됨. 즉 모든 스타일을 다시 계산. Layout과 Repaint과정까지 계속 거치게 됨. 인터랙션이 많을 경우 불필요하게 DOM을 조작하는 비용이 너무 커짐.위의 문제로 나오게 된 것이 가상 돔.가상 돔의 작동방식 가상 돔 생성데이터 바뀌면 새로운 가상 돔 생성이전에 생긴 가상 돔과 새로운 가상 돔 비교(diffing: 바뀐 부분을 찾는 과정)바뀐 부분만 실제 돔에 적용(재조정. reconciliation)시킴가상 돔 장점: 요소가 많이 바뀌어도 한 번에 묶어서 실제 돔에 처리하여 돔을 조작하는 비용 줄임 Create React App리액트 설치를 위해 필요한 것들 Node.jsVisual Studio Code 리액트 앱 설치 방법npx create-react-app <폴더이름>폴더 안에 react app이 설치됨webpack과 babel을 모두 설정해줌.Webpack: 여러 파일을 하나로 모아주는 번들러. 로딩에대한 네트워크 비용 줄임.Babel: 최신 자바스크립트 문법을 구형 브라우저에서도 쓸수있게 변환시켜주는 라이브러리.SPA(Single Page Application)하나의 페이지에 담아 동적으로 화면 바꿔가며 표현.단일 html 내의 div id=root를 자바스크립트로 잡음.페이지 전환(브라우징)은 html5의 history API를 이용.전통적: Multi Page ApplicationReact는 SPA를 사용한다.JSX(Javascript syntax extension)자바스크립트의 확장 문법.UI를 나타낼 때 자바스크립트(logic)와 HTML(markup)구조를 같이 사용.JSX를 이용하면이벤트 처리 등을 더 편하게 구현할 수 있음.의무는 아니지만 훨씬 편리하여 거의 모든 사람이 리액트에서 UI를 위해 사용.JSX로 작성 -> babel이 createElement로 바꿔줌JSX는 컴포넌트에 여러 요소가 있다면 반드시 부모 요소 하나로 감싸야 한다.JSX Key 속성리액트에서 요소의 리스트를 나열할 때 넣어주는 값.React가 변경, 추가, 제거된 항목을 식별하는 데 도움이 됨.요소의 안정적인 ID를 부여하려면 배열 내부의 요소에 키를 제공해야 함.key에는유니크한값을 넣어야한다. index는 비추천.key를 이용하면 새로 그리는게 아니라 기존의 리스트를 이용해준다.React State컴포넌트의 렌더링 결과물에 영향을 주는 데이터를 갖고 있는 객체.컴포넌트 안에서 관리한다.setState 이용React Hooksclass 없이 state를 사용할 수 있는 기능.함수형 컴포넌트를 사용한다.클래스형 컴포넌트: 많은 기능, 길고 복잡한 코드, 느림, *리액트의 생명주기 사용 가능함수형 컴포넌트: 적은 기능, 짧고 심플한 코드, 빠름, *리액트의 생명주기 사용 불가*리액트의 생명주기: 컴포넌트의 시작, 업뎃, 언마운트까지의 주기위처럼 함수형 컴포넌트에서는 리액트의 생명주기를 사용하지 못했었다.그러나 React Hooks로 인해 함수형 컴포넌트에서도 생명주기 사용 가능해짐.리액트 훅스에서는 리액트 생명주기를 useEffect 안에서 다 처리를 해줄 수 있다.클래스형 컴포넌트에서는 마운트, 업데이트, 언마운트 함수 따로 생성.리액트 훅스는 HOC 대신 Custom React Hooks를 사용해 Wrapper 과다를 방지.TailWindCSSHTML 안에서 CSS 스타일을 만들 수 있게 하는 CSS 프레임워크.부트스트랩처럼 미리 세팅된 Utility Class를 활용하는 방식으로, html에서 스타일링을 할 수 있다.클래스 이름 짓기 등으로 골머리를 썩지 않아도 됨.Styled ComponentJavascript 파일 안에서 CSS를 처리할 수 있게 해주는 라이브러리.``를 사용하여 그 안에 스타일 속성들을 넣고, 변수에 할당한다.미션미션7(Day7). 타이핑 테스트 앱자바스크립트 섹션 9에서 다루었던 stopwatch앱을 복습하면서 구현을 시작했다.아직 미완성이고, 구현한 부분은 아래 부분이다.시작 화면index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Typing Test App!</title> <link rel="stylesheet" href="style.css" /> </head> <body> <div class="wrapper"> <h1>타이핑 스피드 측정</h1> <div class="data-container"> <div id="errors"> ERRORS <h2>0</h2> </div> <div id="time"> TIME <h2>20S</h2> </div> <div id="accuracy"> % ACCURACY <h2>100</h2> </div> </div> <div id="sentence">아래를 클릭해서 게임을 시작하세요.</div> <input id="input" type="text" /> </div> <script src="script.js"></script> </body> </html>이 시작화면에서는 텍스트 입력 부분을 클릭하면 게임이 시작되도록 해야 하기에, event listener를 이용했다.// 1. 입력 부분에 클릭했을 때 게임 시작 input.addEventListener("click", startGame);게임 화면(이상하게도 글씨가 왼쪽으로 밀린다.)이 게임화면에서는, 기존에 저장되어있는 등의 문장을 표시해야 하며 카운트다운 타이머를 실행시켜야 한다.(input과 sentence는 코드 맨 위에 document querySelector로 가져와 지정했으나 여기선 생략함. let i, err...도.)const startGame = () => { i = 0; err = 0; sec = 20; acc = 0; interval; input.removeEventListener("click", startGame); // 1-1. 문장 보여주기 sentence.textContent = sentences[i]; // 1-2. 시간초 카운트다운 시작 interval = setInterval(timer, 1000); ...const timer = () => { sec -= 1; timeH2.innerText = `${sec}S`; if (sec === 0) { gameEnd(); } };여기서 stopwatch 프로젝트에서 배웠던 setInterval를 사용하였다.끝 화면(배치가 좀 그렇다)시간 초과가 나거나 주어진 문장을 모두 입력할 경우 이 화면으로 온다. 여기서 clearInterval를 사용하였다.const gameEnd = () => { clearInterval(interval); // 3. 타임 오버 혹은 문장 전부 클리어 // 다시 시작 버튼 활성화이 아주 사소한 부분 해결 과정에서 하고자 하는 것은 setInterval과 clearInterval의 복습이다.결론setInterval()은 주어진 함수를 주어진 간격마다 실행하는 함수이다.clearInterval()은 주어진 interval의 반복을 중단하는 함수이다.예시: hello를 1초마다 콘솔에 반복 출력하는 함수const sayHello = () => { console.log("hello"); } let interval; setInterval(sayHello, 1000); clearInterval(interval); // 반복 중단

프론트엔드

Dream

[ 인프런 워밍업 클럽 Study FE 0기 ] Week 1 발자국

들어가기 앞서...2024년의 첫 해가 밝으며, 지금까지는 단순히 관심만 갖고 있었던 웹 개발에 대한 공부를 시작해보기로 결심했습니다. 먼저 HTML과 CSS의 공부를 마치니 운이 좋게 인프런에서 JS와 ReactJS 스터디를 진행한다는 소식을 듣게 되었습니다. 이런 좋은 기회를 놓치지 않겠다고 생각하고, 워밍업 클럽에 참여하게 되었습니다.발자국OT를 참가한지 엊그제 같은데 시간은 정말 순식간에 지나가는 것 같습니다. 벌써 스터디가 시작된 지 1주차가 되었습니다. 이제 그 동안의 강의 내용을 간단하게 요약하고, 회고를 남겨 보려고 합니다. 이번 주 강의는 따라하며 배우는 자바스크립트 A-Z (섹션 0~8) 부분을 진행하였습니다.요약Section 01. 자바스크립트 기초[ Console 객체 ]자바스크립트의 console 객체는 코드를 작성하고 테스트를 할 때, 사용하기 좋은 함수를 제공해준다. 다음은 강의에서 소개한 주요 Console 객체의 함수들이다.console.log(): console에 메시지를 출력한다. console.table(): console에 배열이나 객체의 데이터를 테이블 형태로 출력한다. console.error(): console에 에러 메시지를 출력한다. console.warn(); console에 경고 메시지를 출력한다. console.time(), console.timeEnd(): 세트로 사용되며, 두 함수 사이의 코드 실행 시간을 측정한다. [ var, let, const과 스코프 ]변수/상수를 선언할 때 let, const 그리고 var 키워들 사용한다. let과 const는 ES6에 새롭게 추가된 키워드이다. var 키워드는 오래된 선언 키워드로 let과 const 사용을 권장하고 있다.let: 변수 키워드, 재선언 X, 재할당O const: 상수 키워드, 재선언 X, 재할당Xvar: 재선언 O, 재할당 O 그리고 사용한 let/const와 var 키워드에 따라 스코프가 다르게 처리된다. 스코프(scope, 유효/참조 범위)란 어떠한 변수를 참조하려고 할 때, 그 변수에 접근 가능한 유효 범위이다.let/const: 모든 코드 블록 { } 내부에서 선언된 변수는 코드 블록 내에서만 유효. var: 함수 내에서 선언된 var 변수는 함수 내에서만 유효하며, 함수 내에서 블록 내·외부에 관계없이 접근 가능. [ 호이스팅 ]코드가 실행되기 전에 변수 및 함수 선언을 로컬 범위(유효 범위)의 맨 위로 끌어 올려지는 경우를 말한다.[ 자바스크립트 타입과 타입 변환 ]자바스크립트의 데이터 타입(자료형)은 다음과 같다.원시 타입: Boolean, String, Number, null, undefined, Symbol고정된 크기로 Call Stack 메모리에 저장실제 데이터가 변수에 할당참조 타입: Object, Array데이터 크기가 정해지지 않고 Call Stack 메모리에 저장데이터의 값이 Heap에 저장되며 메모리의 주소 값이 할당 자바스크립트 변수에 저장된 값은 다른 데이터 유형으로 변환될 수 있다.명시적 데이터 변환(개발자가 직접 함수를 사용해서 변환)자동 데이터 변환(자바스크립트 자체에 의해 자동으로 변환) [ 연산 및 Math ]자바스크립트에서는 기본적인 산술 연산자, 논리 연산자, 비교 연산자를 제공한다.Math를 통해 더 많은 연산을 이용할 수 있다. [ Template Literals]Backtick(`)을 사용하여 문자열을 표현한 것을 템플릿 리터럴이라고 한다. 템플릿 리터럴을 이용하면 다음과 같은 이점이 있다.줄 바꿈이 쉽다.${}을 사용하여 내부에 표현식을 포함할 수 있게 한다. [ Loops ]for: 초기식, 조건식, 증감식을 포함하는 반복문으로 주어진 조건이 참일 경우 블록 안의 코드를 반복 실행for/in: 객체의 열거 간으한 속성들을 반복하는데 사용.while: 주어진 조건이 true일 동안 코드 블록을 계속해서 실행.do/while: 먼저 코드 블록을 실행한 후, 조건을 확인한다. 그러고 나서 조건이 true일 동안 반복 실행한다. Section 02. Window 객체 및 DOM[ Window 객체 ]브라우저에 의해 자동으로 생성된다. (자바스크립트 객체 X, 브라우저에서 제공 O)이 window 객체는 다음과 같은 역할을 수행한다.브라우저에 접근 및 조작 가능자바스크립트 코드의 전역 객체 역할 [ DOM ]문서 객체 모델(Document Object Model, DOM)요소로 이루어진 HTML 파일을 Tree 구조로 표현한 객체 모델최상단에는 document 노드가 위치해 있으며, 이를 통해 DOM 접근 및 조작 가능Critical Render Path (웹 페이지 렌더링 콰정)데이터 파싱(HTML) ➔ DOM Tree 생성 ➔ CSSOM Tree 생성 ➔ JS 실행 ➔ Render Tree 생성 ➔ Layout 생성 ➔ Paint +) 강의에서 수 많은 Property 및 Method를 소개해 주셨지만 너무 많은 관계로 생략... Section 03. Event[ Event ]만약 인프런에서 로그인 버튼을 누르면 무엇이 일어날까? 당연히 로그인 페이지로 이동할 것이다. 이렇게 웹 페이지에서 발생하는 사용자의 행동에 대응하여 브라우저에서 일어나는 특정 사건을 "이벤트"라고 한다. 자바스크립트에서는 다음과 같은 이벤트가 존재한다.UI 이벤트load, change, resize, scroll, error 키보드 이벤트keydown, keyup, keypress마우스 이벤트click, dblclick, mousedown, mouseout, mouseover, mousemove, mouseup포커스 이벤트focus, blur폼 이벤트input, change, select, reset, submit, cut/copy/paste 이벤트를 등록하기 위해서는 addEventListener()를 사용하면 된다. 또한 이벤트 흐름에는 이벤트 bubbling과 Capturing라는 2가지의 기본 모델이 존재한다.[ Event Bubbling과 Event Capturing ]이벤트 bubbling은 가장 깊게 중첩된 요소에 이벤트가 발생했을 때, 이벤트가 위로 전달 되는 것을 의미한다. 이벤트 bubbling은 target 이벤트에서 시작해서 요소를 거쳐 document 객체를 만날 때까지 각 노드에서 모두 발생한다. 만약 bubbling 중단을 원한다면 event.stopPropagation()을 호출하면 된다.event.stopPropagation()DOM Tree를 통한 이벤트 흐름 중지 가능브라우저 기본 동작은 취소 X 이벤트 Capturing은 bubbling과 다르게 제일 상단에 있는 요소에서 아래로 이벤트가 내려오는 것을 말한다.[ Event Delegation ]이벤트 bubbling과 이벤트 Capturing을 통해서 이벤트 위임을 이해할 수 있다. 이 이벤트 위임은 '하위 요소의 이벤트를 상위 요소에 위임하는 것'이다. Section 04. 자바스크립트 중급[ this]Method의 this: 해당 객체를 가리킨다.함수에서 this: window 객체를 가리킨다.constructor의 this: 빈 객체를 가리킨다. [ bind, call, apply]call():함수를 호출하는 함수.첫 번째 인자 값으로 어떠한 것을 전달해 주면 호출되는 함수의 this가 인자 값으로 지정apply(): call()과 유사하나 인수 부분에 배열을 넣어줘야함.bind(): 해당 함수가 지정한 인자 값을 가리키도록 하지만 call(), apply()와 다르게 직접 함수 실행 X [ 삼항 연산자 ]다음과 같은 구문을 갖는다.조건 ? true이면 반환 : false이면 반환 [ Event Loop]자바스크립트는 동기 언어이다. 하지만 가끔 비동기로 작동하는 setTimeout()를 사용하는 예시를 볼 수 있다. 자바스크립트는 비동기 코드를 작성하기 위해서 자바스크립트 이외의 도움을 받는다.[ Closure ]다른 함수 내부에 정의된 함수가 있는 경우, 외부 함수가 실행을 완료하고 해당 변수가 해당 함수의 외부에서 더 이상 엑세스할 수 없는 경우에도, 해당 내부 함수는 외부 함수의 변수 및 액세스가 가능하다. 이 기능을 Closure라고 부른다.[ 구조 분해 할당 ]배열이나 객체의 속성을 해제하여 그 값을 개별 변수에 담을 수 있게 해주는 표현 식이다.[ Map, Filter, Reduce ]Map, Filter, Reduce은 배열 메서드의 대표적인 예시이다.map(): 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출하고 나온 결과를 모아 새로운 배열로 만들어 반환한다.filter(): 주어진 함수의 필터를 통과하는 모든 요소를 모아 새로운 배열로 반환한다.reduce(): 배열의 각 요소에 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결괏값을 반환한다.[ 얕은 비교 VS 깊은 비교 ]숫자, 문자열 등 원시 자료형은 값을 비교하게 된다. 하지만 배열, 객체 등의 참조 자료형은 참조되는 위치를 비교하게 된다. 얕은 비교를 하게 되면 원시 값의 경우 문제가 없지만 참조 값의 경우 실제 값이 아닌 저장된 위치(참조 값)이 비교되기 때문에 문제가 된다.깊은 비교를 사용하게 되면 참조 자료형도 실제 값으로 비교할 수 있게 된댜ㅏ.객체 depth가 깊지 않은 경우: JSON.stringify() 사용객체 depth가 깊은 경우: lodash 라이브러리의 isEqual() 사용[ 얕은 복사 VS 깊은 복사 ]위에서 정리한 내용처럼 복사에도 문제가 발생하게 된다. 따라서 참조 자료형의 값의 경우 깊은 복사를 사용하면 된다.JSON.stringify()lodash 라이브러리의 deepCopy[ 함수 표현식, 함수 선언문 ]함수 선언문: 함수 선언은 함수를 만들고 이름을 지정하는 것이다.일반적인 함수 선언 방식으로 function 키워드와 식별자를 표기하여 사용한다.함수 표현식은 함수를 만들고; 변수에 할당하는 것이다.익명 함수(function 키워드는 사용했으나 식별자 X), 화살표 함수 사용 Section 05. OOP[ OOP ]OOP(Object-Oriented Programming, 객체 지향 프로그래밍)란 Java 및 C를 비롯한 많은 프로그래밍 언어의 기본이 되는 프로그래밍 패러다임이다. 완전 간단하게 말하자면 객체 지향 프로그래밍은 객체들의 모임이라고 할 수 있다.OOP 특징으로는 다음과 같다.추상화:불필요한 정보는 숨기고 중요한 정보 만을 표현함으로써 프로그램을 간단히 만드는 것.상속:새로운 클래스가 기존의 클래스의 자료와 연산을 이용할 수 있도록 해주는 것.기존 클래스: 부모 클래스, 상위 클래스새로운 클래스: 자식 클래스, 하위 클래스다형성: 하나의 틀을 가지고 여러 개의 다양한 형태를 만드는 것이다.overriding을 통하여 다형성 구현 일반적인 코드를 재사용하고 작성할 수 있다.캡슐화:클래스 안에 있는 Method, 변수 등을 하나로 묶어준다. [ class와 constructor ]class에서는 constructor라는 특별한 메서드를 제공한다. 이 constructor는 생성자로, 인스턴스화된 객체에서 다른 메서드를 호출하기 전에 수행해야 하는 사용자 지정 초기화를 할 수 있게 해준다.클래스를 new 키워드를 붙여 인스턴스 객체로 생성하면 넘겨 받은 인자 값과 함께 constructor가 가장 먼저 실행이 된다. 따라서 이 곳에 초기화를 해야 하는 작업을 수행한다.[ Super]자바스크립트에서 super는 다음과 같은 역할을 수행한다.자식 클래스 내에서 부모 클래스의 생성자를 호출할 때 사용한다.자식 클래스 내에서 부모 클래스의 메소드를 호출할 때 사용한다. Section 06. 비동기[ 동기와 비동기 ]동기(Synchronous)코드를 순차적으로 실행하는 것. 즉, 한 작업이 끝나기를 기다렸다가 끝나면 다음 작업을 수행한다.각 작업이 완료될 때까지 다음 작업이 실행되지 않는다.비동기(Asynchronous)작업이 종료되지 않아도 다음 작업을 진행할 수 있는 방식비동기적인 코드는 특정 작업을 기다리지 않고 다음 작업을 계속 수행한다. [ Callbacks, ES6 Promise, ES7 Async / Await ]callbacks콜백 함수는 특정 함수에 매개변수로 전달된 함수를 의미한다.콜백 함수는 함수를 전달 받은 함수 내부에서 호출된다.단, 콜백 지옥을 맛볼 수 있다.Promise자바스크립트 비동기 처리에 사용되는 객체이다.new 키워드와 생성자를 사용해서 만들며, 생성자의 매개변수로 실행 함수를 전달한다.new Promise(실행함수) 실행 함수의 1번째 매개변수 resolve는 비동기 작업 성공 값이다.실행 함수의 2번째 매개변수 reject는 작업 실패 값이다.Promise는 다음 중 하나의 대기 상태를 갖는다.대기, 이행, 거부단, 체인 지옥이 시작된다...Async / Await비동기 코드를 마치 동기 코드처럼 작성할 수 있도록 해준다.Promise에서 than 체인 형식으로 호출하는 것보다 가독성이 좋다.await는 async 함수에서만 사용 가능하다.동기식 코드에서 사용하는 try...catch 문을 사용할 수 있다. Section 07. Symbol, Iterator, Generator[ Symbol ]ES6에 새롭게 추가된 원시 타입으로, 유니크한 식별자를 만들기 위해서 사용.단, for...in과 getOwnPropertyNames에서 제외 된다.Symbol 사용 시 기본적으로 Property가 숨겨진다. (찾을 수 있는 방법 有)따라서 for...in과 getOwnPropertyNames에서 symbol로 만든 Property가 안보인다.[ Iterator, Generator ]Iterator대표적인 예시로 배열이 있다.반복 가능한 것을 Iterable하다고 한다. for…of를 이용할 수 있다.[Symbol.iterator()] 값을 가지고 있다.Generator사용자의 요구에 따라 일시적으로 정지할 수 있고, 다시 시작할 수 있는 특별한 기능을 가지고 있다.function다음에 Asterisk (애스터리스크)를 붙인 형태로 사용한다.function*yield 키워드를 이용한다: 제너레이터 함수의 실행을 일시적으로 정지시킴. Section 08. Design Pattern[ 디자인 패턴 ]디자인 패턴은 개발자가 응용 프로그래밍나 시스템을 디자인할 때 일반적인 문제를 히결하는 데 사용할 수 있는 공식화된 모범 사례이다.- 위키 피디아다음과 같은 장점이 있다.최고의 솔루션재사용성풍부한 표현력향상된 의사 소통필요없는 코드 리팩토링코드베이스 크기 감소[ 디자인 패턴 종류 ]Singleton Pattern: 클래스의 인스턴스화를 객체 1개로 제한하는 디자인 패턴Factory Pattern: 비슷한 객체를 반복적으로 쉽게 생성하게 해주는 디자인 패턴Mediator Pattern(중재자 패턴): 객체 그룹에 대한 중앙 권한을 제공해주는 디자인 패턴Observer Pattern: event-driven 시스템을 이용하는 디자인 패턴Module Pattern: 코드를 더 작고 재사용 가능한 조각으로 분할하게 해주는 디자인 패턴 미션완벽히 해결한 미션은 다음과 같습니다.음식 메뉴 앱음식 메뉴 앱 미션은 주어진 카테고리에 해당되는 메뉴를 출력하는 웹을 구현하는 것이었습니다. 저는 카페 메뉴를 주제로 해당 웹을 구현했습니다. 다만 출력할 아이템에 대한 DB가 없어서 직접 data.json을 작성하여 처리했습니다. 미션을 해결하면서 기능 구현에는 특별한 문제가 없었으나 기능 구현보다 데이터 파일 생성이 더 오래 걸린 미션이었습니다... (출력되는 메뉴 이미지는 스타벅스 이미지를 활용했습니다.) 가위 바위 보 앱플레이어와 컴퓨터가 가위 바위 보를 하는 게임을 구현하는 미션입니다. 총 10번의 기회가 주어지며 게임에 대한 스코어 제공 및 승패 결과를 제공해야 했습니다. UI를 어떻게 구현할까 고민하다가 이미지를 추가적으로 더 넣어 구성했습니다. 컴퓨터의 가위 바위 보 선택지는 Math.random()을 이용해 처리했으며, 기능 구현에는 특별한 문제는 없었습니다.회고자바스크립트 강의를 들으며 기초를 다지고 그 지식을 바탕으로 주어진 미션을 해결하는 한 주를 보냈습니다. 특히 웹 개발이 처음이라서 미션을 해결해 나가는 시간이 정말 흥미로웠습니다. 앞으로 워밍업 클럽 Study를 진행하며 제가 얼만큼 발전할 수 있는지 궁금해지기도 합니다. 남은 기간 최선을 다해서 임해보겠습니다!

프론트엔드워밍업클럽

윤대

[인프런 워밍업 0기 Day2] 한 걸음 더! 메서드로 코드 재사용해보기!

!! 해당 글은 독자가 인프런 워밍업 0기를 수강하고 있다는 전제 하에 작성되었습니다 !!과제 수행에 있어 스프링부트 3.2.2 버전을 사용하고 있다는 점을 미리 알려드립니다! 안녕하세요🙌! 인프런 워밍업 2일차 과제입니다!2일차에는 API를 작성하는 방법에 대해서 배우고 그에 대한 과제를 받았습니다😎과제를 자세히 살펴보는 도중, 문제 1번과 3번 모두 덧셈을 수행하는 공통로직이 있다는 사실을 발견했습니다!따라서! 오늘은 제가 한 걸음 더 성장하기 위해 메서드를 통해 공통로직을 처리한 과정을 공유할 예정입니다~ ⚙️메서드 설계하기메서드를 만들 때, 제가 중요하다고 생각하는 것은 3가지입니다!메서드가 무엇을 할 것인가 (메서드 명)메서드가 무엇을 반환할 것인가 (리턴 타입)메서드가 무엇을 필요로 하는가 (파라미터)여기서, 중요한 점은 어떻게는 생각하지 않는다는 것입니다! 🙅‍♂️위의 3가지만 만족한다면 공통로직으로 볼 수 있기 때문에 메소드의 내부 로직은 추후에 생각해도 무방합니다! 자, 그렇다면 지금부터 문제 1번과 3번에서 각 절차를 수행해보겠습니다.첫째, 무엇을 할 것인가?문제 1번과 3번 모두 2개 이상의 숫자를 더해야 하는 상황입니다! 따라서, 저는 메서드 명을 addNumbers로 정했습니다.둘째, 무엇을 반환할 것인가?우리는 두 수를 더 한 값을 반환 받아야 합니다! int와 Inteager 등 다양한 숫자가 가능하겠지만, 저는 Inteager를 택했습니다!셋째, 무엇을 필요로 하는가?우리는 숫자를 더하기 위해, 2개 이상의 숫자를 필요로 합니다! 또한, 문제 3번의 경우 몇 개의 숫자가 입력될 지 알 수가 없습니다! 따라서 저는 List<Integer> numbers로 파라미터 값을 설정했습니다.이제 위의 세 부분을 합치면 제가 만들 메서드는 아래와 같은 형식이 될 것입니다!public Integer addNumbers(List<Integer> numbers);💡 int 타입의 경우 List로 받을 수가 없어 Integer를 반환 값으로 설정하였습니다! ⚙메서드 구현하기이제 설계가 끝났으니, 메서드를 구현할 시간입니다! for each문으로 List에 담긴 값을 하나씩 꺼내 더하여 값을 반환만 해주면 구현은 어렵지 않게 끝이 납니다!😎 public Integer addNumbers(List<Integer> numbers) { Integer result = 0; for (Integer number : numbers) { result += number; } return result; }자~ 이제 구현이 끝났으니 메서드를 적용해봐야겠죠? ⚙메서드 적용하기문제 3번먼저, 문제 3번입니다! 3번부터 풀이하는 이유는 1번 문제를 풀며 발생하는 문제 때문입니다!DTO (데이터를 전송 받고 보내는 객체)@Getter @Setter public class NumbersRequest { private List<Integer> numbers; }Controller @PostMapping("/api/v1/calc") public Integer addNumbers(@RequestBody NumbersRequest numbersRequest) { return addNumbers(numbersRequest.getNumbers()); }List<Integer>를 필드 값으로 갖는 NumberRequest를 입력받아 addNumbers()에 필드값을 전달했습니다!입력받은 숫자가 몇 개던지 메서드 내부에서 값이 잘 합산되어 반환될 것입니다! 문제 1번이번엔, 문제 1번을 해결해보죠! 😎DTO@Getter @Setter public class CalcResponse { private int add; private int minus; private int multiply; }Controller @GetMapping("/api/v1/calc") public CalcResponse calc(Integer num1, Integer num2) { List<Integer> numbers = new ArrayList<>(); numbers.add(num1); numbers.add(num2); CalcResponse response = new CalcResponse(); response.setAdd(addNumbers(numbers)); // minus와 multiply도 메서드로 구현해주었습니다! response.setMinus(minusNumbers(numbers)); response.setMultiply(multiplyNumbers(numbers)); return response; }값을 Integer num1과 Integer num2를 입력받기 때문에 입력 값들을 List로 변환해줘야 합니다.이렇게 하여도 문제는 없겠지만, 코드가 지저분한 게 영 마음에 들지 않습니다!그래서, 다음과 같이 코드를 변경해보았습니다!DTO (Request) 추가@Getter @Setter public class CalcRequest { private Integer num1; private Integer num2; public List<Integer> getNumbers() { List<Integer> numbers = new ArrayList<>(); numbers.add(this.num1); numbers.add(this.num2); return numbers; } }입력 값을 각각의 값이 아닌 하나의 객체로 변환하여 받고, 내부에 getNumbers()를 구현하여 입력받은 값을 바로 List<Integer>로 변환하여 받도록 하였습니다!이 DTO를 적용하면 Controller의 코드가 다음과 같이 바뀌게 됩니다! @GetMapping("/api/v1/calc") public CalcResponse calc(@ModelAttribute CalcRequest calcRequest) { CalcResponse response = new CalcResponse(); response.setAdd(addNumbers(calcRequest.getNumbers())); response.setMinus(minusNumbers(calcRequest.getNumbers())); response.setMultiply(multiplyNumbers(calcRequest.getNumbers())); return response; }훨씬 간결해졌죠? 참고로 @ModelAttribute는 쿼리 스트링으로 입력받은 값들을 확인하여 파라미터 타입으로 지정한 타입의 필드 값과 일치하면 자동으로 객체로 변환해주는 Annotation입니다!이렇게, 메서드를 사용하면 공통로직을 단일 메서드 내부에서 관리할 수 있게 되고, 코드의 반복을 줄일 수 있게 됩니다! 여러분도 코드의 중복이 보인다면 메서드로 한 번 만들어보는 것은 어떨까요? 지금 까지의 내용을 정리하면서 2일차 포스팅을 마치도록 하겠습니다! 💡정리 💡메서드를 설계할 때 중요한 것!메서드를 설계할 때는 어떻게는 생각하지 말자!무엇을 하고, 반환하고, 필요한 지 3가지만 충족된다면 공통로직으로 만들 수 있다!객체 내부에서 변환로직 만들기!여러 개의 Integer를 Controller에서 List로 직접 변환할 시 코드가 지저분해진다! @ModelAttribute를 활용하여 객체 내부에서 값을 변환하여 반환하자!

백엔드Spring인프런워밍업메서드API인프런SpringBoot

코딩웍스(Coding Works)

임시 이미지(Placehod Image) 사용하기

안녕하세요. 코딩웍스입니다.그동안 잘 사용하던 임시 이미지 사이트인 http://placehold.it 이 없어져서 다른 사이트를 이용하셔야 합니다.물론 임시 이미지 사이트는 엄청 많습니다. 아래 코딩웍스가 소개하는 사이트가 아니어도 임시 이미지를 사용하는건 아무 상관 없습니다.임시 이미지 사용하기공식 웹사이트 : https://placehold.co/✅ 임시 이미지 기본 사용법https://placehold.co/가로x세로<img src="https://placehold.co/600x400" alt="Placeholder Image">✅ 정사각형 임시 이미지 만들기가로 세로 400픽셀 임시 이미지https://placehold.co/숫자<img src="https://placehold.co/400" alt="Placeholder Image">✅ 배경색과 글자 넣은 임시 이미지 만들기https://placehold.co/배경색/글자색<img src="https://placehold.co/600x400/000000/FFFFFF" alt="Placeholder Image">색상 값은 000/fff로 해도 상관없음색상 값은 색상 이름으로 사용해도 상관없음 ex) <img src="https://placehold.co/600x400/crimson/yellowgreen" alt="Placeholder Image">✅ 글자 변경한 임시 이미지 만들기https://placehold.co/600/400/?text=텍스트+텍스트<img src="https://placehold.co/600x400?text=Slider+Image" alt="Placeholder Image">단어가 2개 이상이면 텍스트와 텍스트 사이에 띄어쓰기 불가(반드시 +로 연결해야 함)  ✅ 배경색과 텍스트 변경하고 글자도 변경한 임시 이미지 만들기<img src="https://placehold.co/600x400/000/fff?text=Slider+Image" alt="Placeholder Image">

임시이미지placehoderimage

디아

이제는 진짜 알고 싶은 재무제표 101 (1) - 재무제표는 그래서 무슨 표인가요?

안녕하세요, 인프랩 디아입니다!지난주에 저의 숙원사업이었던...그러나 긴장한 나머지 준비한 것의 반 정도밖에 보여주지(웃기지) 못했던 😭 재무제표 읽는 법에 대한 사내 강연을 진행했는데요.비전공자(그것도 회계가 싫어 경영학을 피해 경제학을 선택했던…)로서 기본적인 회계 지식이 없는 분들도 쉽게 이해하고, 회계를 배우신 분들도 이론뿐 아니라 조금 더 실용적인 정보를 얻어가고(혹은 제가 틀리면 고쳐주고,,,), 나아가 회사생활이나 투자활동에 도움이 되셨으면 하는 마음으로 강의를 준비했는데…저의 마음이 잘 전달되었는지 모르겠네요!떨림(후달달) 가득했던 첫 강연의 아쉬움을 뒤로하고, 발표 내용을 간단히 정리하여 한번 더 공유해보고자 이렇게 블로그로 남겨봅니다. + 강연 때 못다한 얘기들도 있으니 심심풀이로 읽어주세요!발표시간에 재무제표가 무엇인지 알아보고, 그 중에서도 손익계산서와 재무상태표를 상세히 살펴봤죠. 첫번째 글에는 재무정보가 무엇인지, 재무정보의 종류에는 어떤 것들이 있었는지 한번 더 정리해볼게요 🙂 재무제표는 기업의 주요 재무정보 중 하나이다.그렇다면 먼저 재무정보란?하나의 경제주체가 이해관계자의 의사결정을 돕기위해 제공하는 재무적으로 유용한 정보재무정보와 관련한 기업의 주요 이해관계자는?채권자(돈을 빌려준 사람)주주(돈을 준 사람)정부(사회간접자본 제공)‘유용한 정보’에는 무엇이 있을까?1. 재무제표(재무와 관련된 일련의 표)재무상태표(구. 대차대조표, Balance Sheet)손익계산서 (Income Statement)포괄손익계산서 (Comprehensive Income Statement)자본변동표 (Changes in Equity)현금흐름표 (Cash Flow Statement)주석 (Footnotes): 주요 회계 정책 및 방침이 설명되어있어 매우 중요한 정보임! 2. 그 외의 정보들(사진출처:삼성전자 공시 자료)Proxy Statement (주총안건 등)MD&A (경영진 분석)일반 재무제표만 봐서는 알 수 없는 사업부별 영업실적, 각종 재무 지표 등 요약업계 동향, 미래 투자 계획, 영업이익 근거, 임직원 현황 등감사보고서감사의견핵심감사사항(KAM, key audit matters or CAM, critital audit matters)내부회계관리제도 감사의견분기/반기 보고서 등IR 자료기사, 언론(브랜드 이미지, 인지도 등)이런 다양한 정보로 평가된 회사의 가치는 일반적인 재무제표에는 나오지 않지만, 인수/합병을 하게될 경우, 혹은 IPO로 기업가치를 산정할 때 등 특별한 경우 ‘영업권’의 형태로 재무적 가치로 평가될 수 있습니다. 첫 글을 마치며...(강연 때 못다했던 말!!)비교의 중요성재무정보의 유용함은 많은 경우 ‘비교’의 모습으로 옵니다.재무제표의 기본 형태만 봐도, 적으면 2기 많으면 3~4기(3~4년)의 수치를 함께 보여줍니다. 비교를 위해서요.재무제표를 통해 구할 수 있는 다양한 재무지표와 비율들이 좋은지, 나쁜지도 우리회사 것만 구해보고는 판단할 수 없어요. 각종 지표를 비교할 때 가장 중요한 것은 ‘업계의 평균’입니다.영업이익율이 마이너스면 무조건 나쁠까요? 아직 투자단계에 있는 많은 스타트업은 손실이 나고 있으니 특별히 나쁜 경우가 아닐 겁니다. 부채비율이 500%라 심각한 상황인 것 같아도, 어떤 업계에선 그게 acceptable할 수 있고(ex. 항공사) 유동비율이 250%이면 안정적인 것 같아도, 업계 평균에 따라 낮은 수준일 수도 있습니다(또는 남는 현금을 적절히 투자하지 못하는 것일 수도 있어요). 그래서 평균, 그리고 비교가 중요합니다. 주석의 중요성그런 의미에서 주석이 매우 중요한 역할을 합니다. 주석에는 재무제표상의 숫자만으로는 볼 수 없는 다양한 기준과 상황, 배경이 설명되어있습니다. 회사가 채택한 회계기준, 전기와 비교해 달라진 내용, 사업부 별 실적과 경영진이 직접 분석한 업계 동향 등 알짜배기 정보가 많이 있구요.매출원가에 포함된 원가 항목은 무엇인지, 자산의 내용연수가 몇년인지 등 재무 분석에 참고해야하는 중요한 정보들도 있습니다. 진행중인 critical한 소송, 혹은 재무상태표에 반영되지 않은 주요한 부채 등 숫자로 표현되지 못한 정보들 중 중요한 내용을 적어두기도 합니다.동일한 업계에, 비슷한 장비를 사용하는 두 회사 A, B가 있다고 가정해봅시다.A와 B의 재무상태표상 감가상각비는 동일한데, A사의 장비 내용연수는 4년인데 반해 B사는 2년이라고 해볼게요. 우선 이 내용연수 관련된 정보 자체를 주석에서 알 수 있고요. 비슷한 장비인데 내용연수가 더 짧다? 그렇다면 B사에서 장비 관리를 못해서 자주 교체가 되거나, 혹은 오히려 B사가 장비에 투자를 많이해서(자주 새 장비로 교체해서) 이익률이 좋아졌을 수도 있습니다. 이처럼 주석을 통해 재무제표 상 나타난 숫자의 다양한 원인과 결과를 분석하고, 그에 따라 A, B사를 비교해볼 수 있습니다. 감사보고서 의견이 적정이면, 투자하기 좋은 기업인가요?2000억 원 대 횡령사건이 있었던 오*템 사의 경우, 해당 사건으로 인해 감사에서 '부적정' 혹은 '거절' 의견을 받았을까요?오히려 횡령금액, 돌려받을 수 있는 금액, 받을 수 없는 '위법행위 미수금' 등을 재무제표에 충실히 반영했기 때문에 '적정' 의견을 받았습니다.이처럼 감사인의 적정의견이 해당 기업의 재무건정성을 보장하지는 않습니다(그랬으면 투자가 얼마나 쉬웠게요 ㅠㅠ)회계 기준에 맞게 재무제표를 작성했는지, 계속기업으로서의 불확실성은 없는지, 재무제표를 왜곡시킬만한 주요한 정보(ex. 큰 소송 등)가 누락되지 않았는지 등을 주로 평가하기 때문입니다.감사의견, 재무제표 상의 숫자와는 별개로 건강한 기업을 찾는 연습을 해야하는 이유랍니다!! 평소 관심있는 기업이 있었나요? 이제 우리, 네*버 검색만 하지 말고 전자공시사이트에서 주석을 한번 확인해보는 건 어떨까요? 생각보다 다양한 정보가 여러분을 기다리고 있을지도 몰라요!!

교양회계재무제표투자

FE 과제

day2실행 주소 :: https://a-yeye.github.io/Frontend/day2/day2.htmlgithub 주소 :: https://github.com/A-YEYE/Frontend/tree/main/day2<div class="menuDiv"> <button class="button" id="all">All</button> <button class="button" id="breakfast">Breakfast</button> <button class="button" id="lunch">Lunch</button> </div>const buttonElement = document.querySelector('.button'); buttonElement.addEventListener('click', (event) => { let inputClass = event.target.id; console.log(inputClass); }); ?1. button이라는 공통된 class를 가진 요소들 중 하나를 클릭했을 때 클릭된 target의 id를 가져올 줄 알았는데 id가 all인 버튼만 작동하고 breackfast, lunch는 아무 값도 반환하지 않음.=> querySelector() 메소드는 DOM에 일치하는 항목이 없으면 null을 리턴하고, 매개변수로 지정된 CSS 선택자와 일치하는 요소가 있는 경우 첫 번째 요소만을 리턴한다고 한다. 아쉬운 점- 완벽한 반응형이 아니군.....- 공통된 class를 가지고 있으니까 활용하고 싶었는데 방법을 못 찾아서 id를 이용해서 기능을 만들었는데 다른 방법이 있을까 궁금함 제이쿼리 밖에 답이 없는 걸까?  day3실행 주소 : https://a-yeye.github.io/Frontend/day3/day3.htmlgithub 주소 : https://github.com/A-YEYE/Frontend/tree/main/day3칭찬 : 처음부터 구조 잡고 들어감.아쉬운 점 :div에 내용 넣을 때 그냥 div에 넣는 게 좋은 건지 div 안에 input 박스를 넣고 투명도를 두면서 작성하는 게 좋은 건지 모르겠으나div먼저 구조 잡고 스타일 넣은 상태에서 input box 넣고 다시 스타일 주는 게 귀찮아서 남은 횟수랑 플레이어와 컴퓨터의 승점을 input type="hidden"에 넣어두고 그 값을 불러와서 div값을 변경해주는 방법으로 짰는데 이렇게 하다보니 더 복잡하게 짠 느낌..... 2. 처음에 짜둔 구조에다가 결과가 나올 때 div의 속성에 접근해서 높이나 값을 변경해줬는데 이러지말고 새로운 div를 생성해서 display를 none, block 처리했으면 더 나았을 거 같음day4실행 주소 : https://a-yeye.github.io/Frontend/day4/day4.htmlgithub 주소 : https://github.com/A-YEYE/Frontend/tree/main/day4day5-1실행 주소 :: https://a-yeye.github.io/Frontend/day5-1/day5-1.htmlgithub 주소 :: https://github.com/A-YEYE/Frontend/tree/main/day5-1day5-2실행 주소 :: https://a-yeye.github.io/Frontend/day5-2/day5-2.htmlgithub 주소 :: https://github.com/A-YEYE/Frontend/tree/main/day5-25-2에서 너무 시간을 오래 보냄.......처음에 유저가 없다고 뜨는 이유는 화면 녹화키를 눌러서 R키를 눌렀다고 인식해서...마지막에 조회했다가 해당 유저가 없습니다는 너무 오래있다가 없어지긴 함....더 잡고 싶지만 과제가 밀려서 이건 여기까지.... day6실행 주소 :: https://a-yeye.github.io/Frontend/day6/day6.htmlgithub 주소 :: https://github.com/A-YEYE/Frontend/tree/main/day6옵저버 패턴을 배우고 클릭 이벤트가 발생할 때 사용할 수 있지 않을까 싶어서 뻘짓을 하다가 여기에선 맞지 않는 다는 걸 깨닫고 원래 짜던대로 짬....비밀번호 체크할 때 정규식을 매번 남이 만들어 놓은 거 복붙해서 만들었었는데 직접 만들려니까 엄청 헤맴..let pwRule = /^(?=.*[0-9]).{6,}$/; // 1. 정규표현식 리터럴 let pwRule = "^(?=.[0-9].{6,}/)"; // 2. let regex = new RegExp(pwRule); // 3. 정규표현식 객체 생성자 2 만들어 놓고 test함수를 돌렸는데 에러가 계속 나서 한참 헤맸는데 정규표현식 객체로 만들어 줬어야 했다과제 아니었음 계속 몰랐을 듯.. day7실행 주소 :: https://a-yeye.github.io/Frontend/day7/day7.htmlgithub 주소 :: https://github.com/A-YEYE/Frontend/tree/main/day7

학생

인프런 워밍업 클럽 스터디 0기 FE 후기

후기시작 동기대학교 졸업 후 오랜 고민 끝에 취업 직무를 FE개발로 정하고 독학을 하다가, 재미있는 것을 만드는 팀 프로젝트에 어서 참여하고 싶다는 생각을 했다. 팀 프로젝트에 참여하려면 어느정도의 실력이 있어야 하는지 알지 못해 무작정 공부만 하다 보니, 속도도 느리고 비효율적임을 느꼈다. 그리고 하고 있는게 맞는 것인지 감도 못잡고 있었다. 그렇기에 프로젝트에 참여하기 전, 제대로 커리큘럼을 갖춰 누군가 이끌어주며 사람들과 함께할 수 있는 스터디를 찾아야겠다고 생각해 여러 코딩 강의 사이트 커뮤니티를 보던 중 본 인프런 워밍업 클럽 스터디 0기 모집 공고를 보게 되었다. 3주라는 짧은 시간에 필요한 정보를 배우고 비슷한 분야의 목표를 가진 사람들과 함께 달릴 수 있다는 것은 마음이 급한 나에게는 더없이 매력적인 부분이었다. 따라서 망설임 없이 참여하게 되었다. 이것은 나의 프론트엔드 분야에서의 첫 활동이고 도전이었다. 그렇기에 최선을 다하고 싶었다.3주 돌아보기1주: OT를 하고, JavaScript의 전반적인 내용을 다루었다. 강의를 전투적으로 듣고 미션 과제를 해나갔다.2주: JavaScript를 마무리하고 중간점검 Q&A를 한 뒤 리액트의 내용으로 들어갔다. 아직 제대로 이해되지 않은 부분들이 있다는 것을 인지하고 그 부분들을 공부해나갔다.3주: 리액트를 마무리하였다. 앞의 JavaScript부분에 시간을 할당하느라 더 어려운 리액트부분을 비교적 쫓기듯이 공부한 것 같아 스스로 반성했다. 공부하며 느낀 것은 Next.js같은 프레임워크가 있어서 상당히 간편하다는 점이었다.스터디 완주조건은 행사참여(OT, 중간점검), 발자국 1주에 1개(총3개이상), 자바스크립트 과제 7개중 3개, 리액트 과제 7개중 4개였다. 나는 자바스크립트 과제는 첫 주에 대부분 하였지만 리액트 과제 조건 달성을 실패할 뻔 했다. 명확하게 이해가 되지 않은 부분들도 있었으며 노트북까지 잘 따라와주지 않는 등(굉장한 렉이 걸렸다.)의 고통을 맛보았다. 하지만 다행히 과제 제출 기한을 늘려주셔서 숨이 트였고, 비록 처음의 목표였던 '모든 과제를 다 하기'는 실패하였으나 적어도 완주만은 성공하자는 의지로 조건을 달성하였다. 아쉬움이 남지만, 이번 경험을 토대로 하나로 질질 끌지 말고 해야 할 일 먼저 하는 습관을 들이기로 했다.오프라인 수료식오프라인 수료식은 판교역 근처의 건물에서 진행되었다. 처음에 건물을 착각해 아예 반대쪽의 건물을 돌아다니다가 다시 뛰어오느라 조금 지각을 하였지만 다행히 크게 놓친 부분은 없었다. 생각보다 사람들이 정말 많이 있어서 놀랐고 대부분 백엔드 분들이었다. 프론트엔드 분들은 나를 포함해서 4명이었다. 코치님도 함께 계셨는데, 영상으로만 보던 분과 한 테이블에서 식사를 하며 대화를 하는 것이 신기하였고, 테이블의 모든 분들이 반갑게 느껴졌다. 모두 친절하셨고 열심히 하시는 것이 느껴졌다. 대화를 더 나누지 못한 것이 아쉽지만 동종업계이므로 언젠가 또 뵙게 될 수 있지 않을까 생각한다.수료식은 OT, 식사타임(피자를 먹으며 대화), 코치님들의 Q&A, 우수러너 시상, 네트워킹 타임으로 구성되었다. 나는 함께 식사를 한 프론트엔드 몇 분들과 함께 우수러너로 선택되어 상품을 받았다. (오프라인으로 받은 인프런 굿즈. 그밖의 혜택은 인프콘 초대권, 1:1멘토링권 등이 있다) 인프런 굿즈의 구성품은 에코백, 컵, 뱃지, 우산이다. 전부 좋지만 특히나 작고 하찮은 뱃지가 마음에 든다. 누가 봐도 개발을 잘 할 것 같은 외모의 네모 친구다. 우수러너가 되도록 열심히 한 것이 뿌듯했으며, 우수러너로 선정해주시고, 상품 및 혜택을 주신 분들에게 감사함을 느꼈다. 네트워킹 타임에는 프론트엔드 분들 및 백엔드 분들과 여러 이야기를 나누었다. 대부분 프론트엔드 인프런 현직자 분과의 Q&A형식 비슷하게 되었고, 이것저것 질문하여 유익한 시간이 되었다. 판교에 와보고싶었는데 이번 기회에 구경하게 되어 좋았고, 이번 스터디는 꽤나 만족스러운 경험이 되었다.지금까지 열심히 지도해주시고 유익한 시간 보낼 수 있게 해주셔서 감사합니다!

Groot

[워밍업 클럽 0기 - 백엔드] 참여 후기

"인프런 워밍업 클럽 0기 - 백엔드" 참가 후기입니다.참가한 계기참가하게 된 계기는 기본기를 기르고자하는 마음 60%, 호기심 40%? 때문입니다.백엔드 프로그래머를 지망하는 만큼 자바로 실무를 하게 될 가능성이 높은데 이때까지는 이상하게 FastAPI, NestJS, ASP.NET과 같은 프레임워크를 많이 사용했었습니다.그러면서도 자바는 상당히 익숙했습니다.학교 수업을 들으며 자바로는 오히려 Client 프로그래밍을 훨씬 많이 한 것 같습니다. 학우들은 자바를 다 알아서 눈에 보이는 작업물을 보여주기에는 자바 Swing만한 것이 없었던 것 같습니다...아무튼 학교를 빠져나와서 백엔드 지망하는 분들이랑 얘기나 스터디를 하다보면 항상 예제로 나오는 것이 스프링이었습니다. DI, 컨테이너와 같은 컨셉도 소개될 때마다 예시로 등장하는 것이 스프링이고 스터디를 하다가도 다른 분이 예시로 보여주는 것도 스프링일만큼 매일 등장해서 아.. 계층 나누고 주입해주는 것은 비슷한데 자바가 훨씬 자료가 많고 추상화가 더 잘되어있구나 싶었습니다. 안 해봐서 잘은 모르지만 무엇보다 완성도가 높은 느낌이 들었습니다... 몇 년 전에 Node 진영에 ORM을 공부할 때는 자료가 너무 없어서 JPA 코드를 보면서 공부했기도 했습니다.프로젝트 끝날 때마다 Spring Boot는 해보려고 마음 먹고는 했는데 이번 기회에 입문했습니다. 기본적인 개념도 익히고 적절하게 매일 1시간 정도 과제를 고민하고 해결하며 직접 코드를 짜볼 수 있어서 좋았습니다. 다른 스터디 참여자분의 과제 코드도 볼 수 있어서 공부가 많이 된 것 같습니다.다른 프레임워크에서 넘어온만큼 보이는 것도, 예전에 제가 실수한 것도 떠올라서 강사님의 라이브 영상이나 Q&A 내용을 보면서도 많은 팁을 얻었습니다.참가하며 남긴 기록들학교가 개강하고 다른 취준 일정이랑 겹치면서 점점 시간에 쫓기고 빨리해야 했어서 마지막에 몰아서 해버렸지만 후... 아무튼 완강까지 성공했습니다.후기강의는 백엔드 개발에 입문하는 사람이 대상입니다. 그렇다 보니, Git, HTTP나 서버-클라이언트 등의 개념도 입문자를 위한 설명을 포함됩니다. 다른 MVC 프레임워크 경험한 분은 30~50% 정도는 익숙한 내용일 수도 있을 것 같습니다. 개념에 맞춰서 잘 쪼개어 있고 강의 설명이 정말 깔끔합니당.. 이런 분이 강의를 하시구나 했습니다. 저는 아무래도 시간에 쫓기며 웹 개발을 많이 했기에 여러 내용들은 점검해가는 과정이라 생각하고 열심히 수강했습니다.그래도 난이도가 낮은 경우 빠르게 보고 덮은 다음, 스스로 찾아보면서 코드를 구현해보았습니다. 매일 라디오 듣듯이 따라가며, 다른 유사 MVC 프레임워크에서 추구했던 것도 비교하며 학습했습니다. 다른 분들의 질답도 많은 공부가 되었습니다. 고민의 깊이가 다르시더라고요!!배운 것최종적으로는 자바의 역사와 주변 지식, JDSL, Lombok, 코드를 보는 관점 등 여러 키워드를 얻을 수 있어서 유익했습니다.이전에는 VSCode를 많이 썼는데 이 참에 인텔리제이의 단축키도 익숙해질 수도 있었습니다.자바에서 자주 쓰는 단축키 CheatSheet을 만들어보는 것은 어떤가요?다른 사람의 과제랑 코드를 볼 수 있는 것도 좋은 경험이었습니다. 스터디를 매우 열심히 참여하시던 분들이 많아 놀랐습니다. 그러다보니 저도 기운을 받아서 더 공부하고 찾아봤던 것 같습니다ㅎㅎ.앞으로도 파이팅해서 만들고 싶은 프로젝트를 하나 만들어보려고 합니다. To Inflearn, 최태현 지식 제공자님. 재밌고 유익한 스터디 만들어주셔서 감사합니다!

백엔드인프런인프런워밍업클럽스터디0기

crispin

[인프런 워밍업 스터디 클럽 0기_BE] 후기

3주간의 스터디가 끝이났다. 정말 많은 걸 배울 수 있었고, 새로운걸 경험해 볼 수 있었다. 1. 코치님0기 백엔트 코치님은 최태현 코치님이다. 이전 찍으셨던 몇개의 강의(사실 전부..ㅎㅎ) 를 통해 내적 친밀감이 쌓여있었는데스터디 코치님 이라는 이야기를 듣고 꼭 스터디에 참여하고 싶다는 생각이 들었다. 강의를 통해서도 정말 많은걸 알려주시려 하고, 질문도 매우 친절하게 답변해주셨었는데 스터디를 진행하면서도 코치님은 질문 뿐만 아니라특강을 통해서 정말 많은걸 알려주시려고 하는 모습이 정말 대단하다고 느껴졌다.👍 현업에서 일을 하시면서 늦은 시간까지 이어지는 질문에도 친절하게 답변해주시는 모습을 보면서 나도 저런 좋은 영향력을 주위에 끼칠수 있는 개발자가 되고 싶다는 생각이 들었다. 2. 코드리뷰스터디를 처음 시작할때 커리큘럼을 보고 미니 프로젝트를 진행할때 다른 분들과 함께 코드리뷰를 하며 진행하면 어떨까?라는 생각이 들었었다. 할까말까 고민을 정말 많이 했었는데 개인적으로도 코드 리뷰를 해보고 싶었고, 좀 더 이 스터디기간동안 많은걸 배우고 싶어 용기를 내어 함께 진행해 볼 인원을 구했었다. 정말 다행히도 5분의 스터디원 분들이 함께해보자고 하셨고, 결과적으로 너무 만족스러운 시간을 보낼 수 있었다. 내 코드를 다른 사람에게 보여준다는건 지금도 쑥쓰럽긴 하지만 그렇기 때문에 코드 한줄을 작성할때도 정말 많은 생각을 하면서 작성할 수 있었다. 그리고 그 고민했던 내용을 함께 나누면서 정말 많은걸 배울 수 있었다. 다시 한번 함께 미니프로젝트를 진행했던 백엔드 스터디원분들 에게 감사하다는 말을 전해고 싶다.🙇‍♂ 3. 인프런보통의 회사에서 하기 어려운 이런 좋은 영향력을 전파하고 만드는걸 할 수 있다는게 늘 놀랍다. 해커톤이나 개발자 컨퍼런스 후원 기업에 매우 자주 인프런이 있는걸 볼 수 있고, 직접 인프콘 이라는(한번도 가보지 못했다. 나는왜 운이 없는것인가..💧) 큰 컨퍼런스를 열기도 하고. 전반적으로 개발자 생태계에 정말 좋은영향력을 널리 퍼트리고 있는 모습을 보면 정말 대단한 기업이라는 생각이 든다. 생각은 누구나 할 수 있지만 이걸 실천할수 있다는게 참 놀랍다. 이번 스터디도 그렇고 앞으로 더 많은 좋은 영향력을 널리 퍼트리려고 다양한 시도를하고 있는 모습을 보고 있으면 언제가는 인프런에서 일을 꼭 해보고 싶다 라는 생각이 듣다.(자바 개발자 안필요 하신가요💧) 4. 0기앞으로 이 워밍업 클럽이 0기에서 멈추는것이 아니라 1기 2기 쭉쭉 더 많은 사람들에게 좋은 경험이 될 수 있도록커져갔으면 좋겠다. 다음에 더 좋은 스터디가 열린다면 주변에 적극 추천해주고, 나도 다시 한번 참여해봐야겠다. 5. 마무리좋은 기회를 만들어주신 인프런 관계자 분들에게 정말 감사드린다. 특히 스터디 운영에 대해 이런저런 공지와 궁금증을해결해주신 셰리 에게 감사의 인사를 전하고 싶다. 더 많은걸 알려주시려 했던 최태현코치님 그리고 함께 했던 모든 백엔드 스터디 원 분들에게도 다시 한번 감사하다는 인사를 전하고 싶다. 주저리주저리 말이 많아졌는데, 혹시라도 미래에 있을 1기 신청을 고민하며 이 글을 보고 있다면 당장 신청 하라고 적극 권해 주고 싶다.

웹 개발인프런인프런워밍업클럽스터디0기백엔드

[인프런 워밍업 클럽 BE] 참여 후기

이 후기글은 인프런 워밍업 클럽 0기 BE의 전체 소감문입니다.https://inf.run/Hywa 사실 후기글을 써본 경험이 거의 전무하기에, 어떻게 작성해야 하는지조차 감이 잘 잡히지 않는다. 그래서 느낀 감정들을 두서없이 그저 솔직하게 작성해볼까 한다. 참가 신청과 첫 주이번 최태현 코치님 강의는 이전에 절반만 들어놓고 반년 간 시간을 허비하며 지낸 그런 부끄러운 과거 속의 강의 중 하나였다 . 그래도 운이 좋게도 메일로 이번 워밍업 클럽 홍보 글이 날아온 것을 보았고 바로 신청했다. 나는 스스로의 실력에 확신이 차지 않으면 도전하지 못하는, 어찌보면 많이 소극적인 성격인 편이다. 그랬기에 그동안 인프런에서 팀 프로젝트를 시도해보려고 해도 개발자로서의 지식이 너무 얕은 것 같아 차마 도전해보기가 어려웠다. 이번 스터디 신청 자체가 나 나름대로의 하나의 도전이었던 셈이었다. 결과적으로는 잘한 선택이었다고 현재는 마음 깊이 생각하고 있다.디스코드에 처음 접속해 서로 인사말을 남기는데 사실 조금, 아니 상당히 당황스러웠다. 나처럼 초보자 분들이 많이 오는 것을 예상했으나 이미 현업 종사자분들도 다수 참가하시는 것을 보았다. 그때부터 발등에 불이 떨어진 기분이었다. 다른 분들의 질문의 깊이나 발자국, 과제 내용 등을 보고 압도되는 기분이었다. 😂😂그리고 대망의 첫 중간 점검 라이브 때, 일정 관리 실패로 나는 바보같이 라이브를 놓쳐버렸다. 완주 러너의 조건에 중간 점검 라이브를 모두 참가해야 한다는 조항이 있었기 때문에 일주일 만에 열심히 하겠다는 나름의 다짐이 완전히 망가졌다. 그 이후, 그리고 스터디 끝첫 라이브를 놓치고 하루는 기분이 약간 쳐지긴 했었다. 딱히 우수 러너를 노리거나 포인트를 꼭 받아야겠다! 는 아니었지만, 그래도 무언가를 완주했다는 그 성과 자체가 없어지는 것이니 뼈아픈 일이었다. (그리고 수료증을 못 받는 것도 아쉬웠고... ) 하지만 제 1의 목표는 어디까지나 학습이니 그 이후도 내 나름대로 착실히 수행했다. 완주 러너가 되지는 못하겠지만 과제도 꾸준히 제출했고 2차 점검 라이브도 모두 참석했다.이전 후기에서도 작성했듯이 미니 프로젝트 코드 리뷰도 진행해보았고 지금은 좋은 분들과 함께 사이드 프로젝트를 진행 중이다. 아직 본격적인 개발 단계 전이지만... 😊그리고 인프런과 코치님들의 배려로 완주 조건이 완화되어 무사히 완주 러너가 될 수 있었다!당연히 완주 러너가 못 될 것을 예상해 오프라인 수료식 참가 신청을 하지 않은 것은 후회하긴 했지만... 완주 러너가 된 것 만으로도 만족했다. 그리고 대망의 수료식 날, 정말 예상하지 못하게 우수 러너로 선정이 되었다. 선정해주신 코치님에게 몇 번이고 감사 인사를 드리고 싶었다. 우수 러너 혜택으로 인프콘 티켓과 코치님과의 1:1 멘토링을 얻게 되었으니 정말 너무 과분하고 감사한 보상이다. 🙇‍♀️🙇‍♀ 반성할 점과 이모저모람다 함수와 스트림 학습을 위해 새 블로그 글을 만든 것이 있는데 첫 날 이후 정말 하나도 갱신하지 못했다.😭 3/9에 SQLD 시험이 있었기에 강의, 과제, 자격증 시험 공부를 병행하면서 저 글까지 갱신하는 것은 아무래도 과한 목표였을지도 모른다. 사실 저 SQLD 시험도 진작에 공부했으면 됐겠지만... 이미 지나버렸으니 어쩔 수가 없다. 요즘은 학습한 내용을 모두 개인 노션 페이지에 정리하고 있기에 아마 추가 학습을 해도 저 블로그 글은 갱신이 안 되지 않을까 싶다.요즘은 영한님의 로드맵을 따라 쭉 강의를 듣고 있다. 사이드 프로젝트에 폐를 끼치지 않기 위해 본격적인 시작 전 최대한 기반 지식을 더 다지고 있다. 우수 러너 혜택으로 1:1 멘토링도 남아있어서 더 많은 것을 얻기 위해서라도 많은 지식을 쌓아놓아야 할 듯 하다.

백엔드스터디후기

망고123

인프런 워밍업 클럽 스터디(BE) 0기 / 3주차 발자국

일주일 간의 학습 내용에 대한 간단한 회고📕어느덧 마지막 3주차 회고 작성입니다. 인프런 워밍업 클럽 스터디 0기(BE)에 참여하면서, 어설프게 할 생각이었다면 시작조차 안 하는 것이 백배 낫다고 생각하며 진정성 있는 참여를 하기 위해 미션과 미니 프로젝트를 수행하기 위해 최선을 다하려고 끊임없는 도전과 열정을 코드로 보이기 위해 노력했습니다. 3주의 시간이 정말 짧게 느껴질 정도로, 수많은 고민과 반복적인 공부를 통해 스스로의 객관화를 가질 수 있는 매우 유익한 시간이라고 확신합니다.🔥미니 프로젝트 4단계까지 구현을 성공했습니다. 비록💡한 걸음 더! 는 마감 기간 내에 구현하지 못했지만, 처음 시작할 때의 열정과 목표를 잊지 않으면서 강의를 통해 멘토님께서 전달하고자 하는 지혜와 경험을 바탕으로 3주 동안 스스로 많이 성장한 모습에 놀라며 끊임없는 도전으로 구현 능력의 한계를 극복할 수 있게 된 특별한 경험이었습니다. 이 경험으로 취업에 성공하여 회사에 의미를 더해줄 훌륭한 개발자로 성장을 목표로 오늘도 어김없이 정진하겠습니다.일주일 동안 스스로 칭찬하고 싶은 점 미니 프로젝트를 진행하면서 정말 많은 고민을 통해 구현한 코드는 매우 의미가 있습니다. 4단계까지 무사히 마칠 수 있는 스스로에게, 한계를 넘어 벽에 부딪히는 것에 두려워하지 말고 한계를 도달하고 뛰어 넘을 수 있다는 가능성을 눈 앞에서 증명했다는 것에 감사하다고 전하고 싶습니다.아쉬웠던 점 회사 이력서를 작성하면서 4단계 💡한 걸음 더! 배포를 기간 내에 마치지 못한 것에 많은 아쉬움을 갖고 있지만, 이 또한 나의 부족함과 나태함의 결과로 승복하였습니다. 하루라도 더, 몇 시간이라도 더, 조금만 더 잠을 안자고 했더라면 충분히 할 수 있었음에도 불구하고 못한 자신에게 아쉬움을 표합니다.보완하고 싶은 점 아무리 코드를 따라 치고, 강의를 보더라도 스스로 고민을 하고 아무것도 없는 상황에서 해결하지 못한다면 의미가 없다는 것을 크게 느꼈습니다. 단기 기억 공간과 다른 모든 인지 기억에 마치 잘 알고 있다는 착각에 빠지지 않고, 철을 관철하는 정신으로 공부한 모든 내용을 체득할 것입니다.다음주에는 어떤 식으로 학습하겠다는 스스로의 목표 강의 PDF, PPT 를 통해 처음부터 끝까지 다시 복습을 진행하며, 백엔드 개발 관련 책을 구매하여 함께 병행할 예정입니다. 소중한 경험을 소홀히 하지 않고 앞으로의 부족함을 채우는 행보에 지치고 힘들며 한계에 부딪히는 순간들을 맞이하고 뛰어 넘기를 목표합니다. 학습 내용 정리미니 프로젝트 1단계API 에 따라 정상적인 동작을 수행하는지 과정을 검증하는 공간입니다. 직원과 팀 엔티티 확인하기 #4 직원과 팀의 컨트롤러, 서비스, 리포지토리 확인하기 #6커밋 메세지와 코드 확인은 위의 이슈로 이동하면 확인할 수 있습니다.0. 데이터 API팀 등록하기POST / /api/v1/team[{  "name" : "여신관리팀"}] 모든 팀 조회하기GET / /api/v1/team[  {      "name": "여신관리팀",      "manager": "김민수",      "memberCount": 2  },  {      "name": "영업1팀",      "manager": "박현준",      "memberCount": 2  },  {      "name": "전산팀",      "manager": "박준우",      "memberCount": 1  }] 직원 등록하기POST / /api/v1/members[{  "name" : "김사원",  "role" : "MEMBER",  "birthday" : "1999-11-02",  "workStartDate" : "2023-11-01",}] [{  "name" : "김사원",  "teamName" : "여신관리팀", // 직원 등록 시점에 팀 등록을 할 수 있습니다.  "role" : "MEMBER",  "birthday" : "1999-11-02",  "workStartDate" : "2023-11-01",}] 모든 직원 조회하기GET / /api/v1/members[  {      "name": "김민수",      "teamName": "여신관리팀",      "role": "MANAGER",      "birthday": "1970-11-11",      "workStartDate": "2010-02-23"  },  {      "name": "이민혁",      "teamName": "여신관리팀",      "role": "MEMBER",      "birthday": "1984-06-12",      "workStartDate": "2017-08-11"  },  {      "name": "박현준",      "teamName": "영업1팀",      "role": "MANAGER",      "birthday": "1980-09-29",      "workStartDate": "2012-02-13"  },  {      "name": "이현민",      "teamName": "영업1팀",      "role": "MEMBER",      "birthday": "1990-11-15",      "workStartDate": "2017-08-26"  },  {      "name": "박준우",      "teamName": "전산팀",      "role": "MANAGER",      "birthday": "1994-03-15",      "workStartDate": "2019-02-05"  },  {      "name": "서포터",      "teamName": null, // 팀에 포함되지 않은 직원은 null 표시합니다.      "role": "MEMBER",      "birthday": "2001-11-12",      "workStartDate": "2024-03-04"  }] 1. 테이블 생성직원과 팀 테이블은 다음과 같은 쿼리를 통해 자동으로 생성됩니다.팀 테이블직원 테이블2. 데이터 추가팀 테이블에 여신관리팀, 영업1팀, 전산팀 을 등록합니다. 직원 테이블에 다수의 직원 데이터를 등록합니다.2.1 팀 등록하기팀 등록(POST / /api/v1/team)을 통해 여신관리팀, 영업1팀, 전산팀 데이터를 팀 테이블에 저장합니다.팀 테이블여신관리팀, 영업1팀, 전산팀 데이터를 성공적으로 저장한 것을 확인합니다.2.2 직원 등록하기직원 등록(POST / /api/v1/members)을 통해 다수의 직원들을 등록합니다.직원 테이블team_id 를 통해 해당 팀에 알맞게 데이터를 성공적으로 저장합니다. 팀에 속하지 못한 경우에는 null 으로 표시됩니다.팀 테이블회원 등록과 함께 직원이 팀에 포함되면 member_count 를 증가합니다. manager 는 회원 등록 role의 MANAGER 만 가능합니다. 이미 MANAGER 가 존재하는 팀에다가 MANAGER 인 직원을 포함하려고 하면 예외가 발생합니다.3. 데이터 조회3.1 직원 조회모든 직원 조회(GET / /api/v1/members)으로 정보를 출력합니다. 팀에 속하지 못한 직원은 teamName 이 null 으로 출력됩니다.3.2 팀 조회모든 팀을 조회합니다.미니 프로젝트 2단계0. 데이터 API출근 / POST / http://localhost:8080/api/v1/commute/start?memberId=1퇴근 / POST / http://localhost:8080/api/v1/commute/end?memberId=1특정 직원의 날짜별 근무시간 조회 / GET / http://localhost:8080/api/v1/commute?id=1&date=2024-03{  "detail": [      {          "date": "2024-03-05",          "workingMinutes": 12      },      {          "date": "2024-03-04",          "workingMinutes": 600      },      {          "date": "2024-03-03",          "workingMinutes": 583      },      {          "date": "2024-03-02",          "workingMinutes": 987      }  ],  "sum": 2182} 1.테이블 미리보기출퇴근 동작 확인을 위해 미리 팀과 직원 데이터를 작성합니다.팀 테이블직원 테이블출퇴근 테이블2. 출근하기-http://localhost:8080/api/v1/commute/start?memberId=1 으로 id 가 1인 회원이 출근 처리됩니다.그림 첨부는 없지만, 회원 id 가 2, 3, 4 또한 출근으로 등록한 상태라는 점을 참고합니다.출퇴근 테이블출근에 성공한 직원은 출퇴근 테이블에 다음 그림과 같이 저장됩니다.3. 퇴근하기-http://localhost:8080/api/v1/commute/end?memberId=1 으로 id 가 1인 회원이 퇴근 처리됩니다.그림 첨부는 없지만, 회원 id 가 2, 3, 4 또한 퇴근으로 등록한 상태라는 점을 참고합니다.출퇴근 테이블퇴근에 성공한 직원은 출퇴근 테이블에 다음 그림과 같이 저장됩니다.4. id=1 직원에게 2024-03 근무한 데이터 추가하기id 가 1, 2, 3, 4 인 직원은 모두 2024-03에 한 번은 출퇴근한 상태입니다.비교를 위해 id 가 1 인 직원에게 2024-03 에 해당하는 날짜에 출퇴근 기록 데이터를 INSERT 쿼리를 수행하여 출력 데이터를 준비합니다.출퇴근 테이블5. id=1 직원의 2024-03 출퇴근 조회하기GET / http://localhost:8080/api/v1/commute?id=1&date=2024-03정상적으로 특정 직원의 날짜별 근무 시간을 조회하는 기능 을 확인할 수 있습니다.미니 프로젝트 3단계0. 데이터 API휴가 등록 / POST / /api/v1/vacation{  "id" : 2,  "date" : "2024-03-23"} 남은 휴가 개수/ GET/ /api/v1/vacation?id=114 팀마다 며칠 뒤에 휴가 등록이 가능한 일 입력 / POST / /api/v1/vacation/role{"name" : "여신관리팀","minDay" : 3 // 오늘 기준으로 몇 일 뒤에 휴가 등록 가능한 지 숫자 입력} 특정 직원의 날짜별 근무시간 조회 / GET / /api/v1/commute?id=2&date=2024-03{"detail": [{"date": "2024-03-01","workingMinutes": 670,"usingDayOff": false},{"date": "2024-03-02","workingMinutes": 720,"usingDayOff": false},... // 03 ~ 08 일 생략{"date": "2024-03-09","workingMinutes": 0,"usingDayOff": true},... // 10 ~ 30 일 생략{"date": "2024-03-31","workingMinutes": 0,"usingDayOff": false}],"sum": 1390} 1.테이블 미리보기휴가 동작 확인을 위해 데이터를 작성합니다. 참고하기 바랍니다.직원 테이블팀 테이블출퇴근 테이블휴가 테이블휴가룰 테이블1. 휴가 정책팀마다 며칠 뒤에 휴가 등록이 가능한 일 입력 / POST / /api/v1/vacation/role팀에 휴가 정책을 지정합니다. 휴가룰 테이블에서 확인이 가능합니다.휴가룰 테이블2. 휴가 등록id 가 1 인 직원에게 2024-03-10 에 휴가를 등록합니다.minDay의 3 휴가 정책에 따라서 만약 2024-03-08 이하로 입력하거나 중복된 휴가는 등록되지 않습니다.휴가 테이블3. 남은 휴가 개수남은 휴가 개수/ GET/ /api/v1/vacation?id=1남은 휴가 갯수를 숫자로 반환합니다.직원 번호 1 은 휴가 13 개를 정상적으로 반환하고 휴가를 모두 사용한 직원 번호 2는 휴가 0 개를 반환합니다.4. 특정 직원의 날짜별 근무시간 조회특정 직원의 날짜별 근무시간 조회 / GET / /api/v1/commute?id=2&date=2024-03해당 월의 모든 날짜를 휴가 유무와 출퇴근에 따라서 정상적으로 출력합니다.미니 프로젝트 4단계0. 데이터 API초과 근무 계산 / GET / /api/v1/overtime[{"id": 1,"name": "연**1","overtimeMinutes": 11100},{"id": 2,"name": "연**2","overtimeMinutes": 0}] 1.테이블 미리보기초과 근무 계산 동작 확인을 위해 데이터를 작성합니다. 참고하기 바랍니다.직원 테이블팀 테이블출퇴근 테이블2. 초과 근무 계산공공 데이터 포털로 대한민국의 공휴일과 대통령령의 대체 공휴일을 가져옵니다.요청한 년-월의 토요일, 일요일 과 공휴일 그리고 휴가일 따라서 전체 근무 일자가 몇 일인지 구합니다.요구 사항에 따라 하루 기준 8시간으로 한 달동안 법적 근로 시간을 산출하여 초과 근무 시간을 계산하여 출력합니다.그림과 같이 포스트맨의 "id" = 1의 경우 다음과 같습니다.2024-03 의 공휴일과 주말은 총 11 일입니다. 따라서 88시간 = 5280분2024-03 의 전체 평일은 총 20 일입니다. 따라서 20일 * 8시간 = 160시간 = 9600분2024-03 의 직원 (id = 1) 의 전체 근무 시간은 345 시간입니다. 따라서 345시간 * 60분 = 20,700분포스트맨의 결과 동일하게 초과 근무 시간 = 20,700 - 9600 = 11000 으로 정상적으로 출력되는 것을 확인할 수 있습니다.2024-03 의 직원 (id = 2) 는 초과 근무 시간 이 없으므로 0 으로 출력됩니다.이름은 첫 글자와 마지막 글자를 제외한 모든 글자는 * 으로 표시합니다.직원 테이블로부터 이름을 조회한 것으로 데이터베이스 안에는 정확한 이름이 존재합니다. * 는 서비스 로직에서 처리되어 연**1 처럼 응답합니다.

백엔드SpringJava

이양구

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

양성빈

[인프런 워밍업 스터디 클럽] 미니프로젝트 - step1

미니 프로젝트 - 개발 일지드디어 미니프로젝트 시작이다.프로젝트 세팅언어: JDK21프레임워크: Spring Boot 3.2.3, Spring Data JPA라이브러리: 롬복DB: mysql테스트: junit5요구사항환경 설정1. profile 분리먼저 나는 프로필을 분리하기로 하였다. 개발환경 profile과 운영환경 profile 그리고 공통적인 부분을 묶어두었다. 또한 DB의 정보는 민감한 정보이므로 환경변수에 등록해두었다.application.ymlspring: profiles: group: dev: "dev, common" prod: "prod, common" active: dev --- spring: config: activate: on-profile: dev datasource: url: "jdbc:mysql://${DB_DEV_HOST}/${DB_DEV_SCHEMA}" username: ${DB_DEV_USERNAME} password: ${DB_DEV_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update properties: hibernate: show_sql: true format_sql: true open-in-view: false logging: level: sql: trace --- spring: config: activate: on-profile: prod --- spring: config: activate: on-profile: common2. Auditing 기능 개발다음으로 나는 spring data jpa에서 제공해주는 Auditing 기능을 먼저 이용하려고 한다. 기본 엔티티를 만들기 전에 추상클래스로 공통적인 속성들을 묶어서 만들기로 하였다.BaseDateTimeEntity.java@Getter @MappedSuperclass @EntityListeners(value = AuditingEntityListener.class) public abstract class BaseDateTimeEntity { @CreatedDate @Comment("생성 날짜") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @Column(nullable = false, updatable = false) private LocalDate createdAt; @LastModifiedDate @Comment("최종 수정 날짜") @Column(nullable = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate updatedAt; }BaseEntity.java@Getter @MappedSuperclass @EntityListeners(value = AuditingEntityListener.class) public class BaseEntity extends BaseDateTimeEntity { @CreatedBy @Comment("생성한 직원") @Column(nullable = false, updatable = false) private String createdBy; @LastModifiedBy @Comment("최종 수정한 직원") @Column(nullable = false) private String updatedBy; }먼저 위의 BaseDateTimeEntity와 같이 생성 날짜와 최종 수정 날짜를 정의하였고, BaseEntity에서는 추가적으로 생성한 직원 최종 수정한 직원 부분까지 더했다. 그 이유는 요구사항에는 BaseEntity 부분이 필요가 없겠지만 나중에 추후 확장성을 위해 사용하기로 하였다. 그리고 이를 위해 Auditing 설정 파일을 작성해주었다.AuditConfig.java@Configuration @EnableJpaAuditing public class AuditConfig { @Bean public AuditorAware<String> auditorAwareProvider() { return new AuditorAwareImpl(); } }AuditorAwareImpl.javapublic class AuditorAwareImpl implements AuditorAware<String> { @Override public Optional<String> getCurrentAuditor() { return Optional.of("tester"); } }공통 예외 부분우리가 예외를 처리하다보면 커스텀하게 예외를 던져야 할 경우가 생긴다. 그리고 예외가 던져졌을 때 에러 로그가 아니라 그에 대한 커스텀 응답을 받고 싶은 경우도 있을 것이다. 이에 따라 일련의 과정을 정리해본다.먼저 예외에 마다 특정 예외에 코드가 있다고 생각을 하였다. 그에 따른 인터페이스를 이와 같이 정의하였다.public interface ExceptionCode { HttpStatus getHttpStatus(); String getCode(); String getMessage(); }그리고 해당 인터페이스를 구현한 GlobalExceptionCode enum 클래스를 개발한다.@Getter @RequiredArgsConstructor public enum GlobalExceptionCode implements ExceptionCode { INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "G-001", "Invalid Input Value"), METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "G-002", "Invalid Http Request Method"), ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "G-003", "Resource Not Found"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "F-001", "Server Error!"); private final HttpStatus httpStatus; private final String code; private final String message; }이제 커스텀 예외 응답 클래스를 개발하자. 이번엔 조금 디자인 패턴 중 정적 팩터리 메서드 패턴을 적용해보았다.@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ExceptionResponse { private String message; private HttpStatus status; private String code; private List<ValidationException> errors; private LocalDateTime timestamp; private ExceptionResponse(final ExceptionCode exceptionCode) { this.message = exceptionCode.getMessage(); this.status = exceptionCode.getHttpStatus(); this.code = exceptionCode.getCode(); this.timestamp = LocalDateTime.now(); this.errors = new ArrayList<>(); } private ExceptionResponse(final ExceptionCode errorCode, final String message) { this.message = message; this.status = errorCode.getHttpStatus(); this.code = errorCode.getCode(); this.timestamp = LocalDateTime.now(); this.errors = new ArrayList<>(); } private ExceptionResponse(final ExceptionCode errorCode, final List<ValidationException> errors) { this.message = errorCode.getMessage(); this.status = errorCode.getHttpStatus(); this.code = errorCode.getCode(); this.timestamp = LocalDateTime.now(); this.errors = errors; } public static ExceptionResponse of(final ExceptionCode errorCode) { return new ExceptionResponse(errorCode); } public static ExceptionResponse of(final ExceptionCode errorCode, final String message) { return new ExceptionResponse(errorCode, message); } public static ExceptionResponse of(final ExceptionCode code, final BindingResult bindingResult) { return new ExceptionResponse(code, ValidationException.of(bindingResult)); } public static ExceptionResponse of(final ExceptionCode errorCode, final List<ValidationException> errors) { return new ExceptionResponse(errorCode, errors); } }다음으로 우리가 정의하지 않는 validation Exception부분도 처리해줄 필요가 있었다. 그래서 아래와 같이 커스텀하게 구성을 해보았다.@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ValidationException { private String field; private String value; private String reason; private ValidationException(String field, String value, String reason) { this.field = field; this.value = value; this.reason = reason; } public static List<ValidationException> of(final String field, final String value, final String reason) { List<ValidationException> validationExceptions = new ArrayList<>(); validationExceptions.add(new ValidationException(field, value, reason)); return validationExceptions; } public static List<ValidationException> of(final BindingResult bindingResult) { final List<FieldError> validationExceptions = bindingResult.getFieldErrors(); return validationExceptions.stream() .map(error -> new ValidationException( error.getField(), error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(), error.getDefaultMessage())) .collect(Collectors.toList()); } }마지막을 ExcpetionHandler를 통해 예외처리를 해두었다. 여기서 이제 커스텀 예외가 생길때 예외 클래스를 생성 후 RuntimeException을 상속받은 후에 해당 핸들러 클래스에 적용해두면 된다.@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * Java Bean Validation 예외 핸들링 */ @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity<ExceptionResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.error("handle MethodArgumentNotValidException"); return new ResponseEntity<>(ExceptionResponse.of(INVALID_INPUT_VALUE, e.getBindingResult()), INVALID_INPUT_VALUE.getHttpStatus()); } /** * EntityNotFound 예외 핸들링 */ @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<ExceptionResponse> handleEntityNotFoundException(EntityNotFoundException e) { log.error("handle EntityNotFoundException"); return new ResponseEntity<>( ExceptionResponse.of(ENTITY_NOT_FOUND, e.getMessage()), ENTITY_NOT_FOUND.getHttpStatus()); } /** * 유효하지 않은 클라이언트의 요청 값 예외 처리 */ @ExceptionHandler(IllegalArgumentException.class) protected ResponseEntity<ExceptionResponse> handleIllegalArgumentException(IllegalArgumentException e) { log.error("handle IllegalArgumentException"); return new ResponseEntity<>( ExceptionResponse.of(INVALID_INPUT_VALUE, e.getMessage()), INVALID_INPUT_VALUE.getHttpStatus() ); } @ExceptionHandler(TeamAlreadyExistsException.class) protected ResponseEntity<ExceptionResponse> handleTeamAlreadyExistsException(TeamAlreadyExistsException e) { log.error("handle TeamAlreadyExistsException"); return new ResponseEntity<>( ExceptionResponse.of(INVALID_INPUT_VALUE, e.getMessage()), INVALID_INPUT_VALUE.getHttpStatus() ); } /** * 잘못된 HTTP Method 요청 예외 처리 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) protected ResponseEntity<ExceptionResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.error("handle HttpRequestMethodNotSupportedException"); return new ResponseEntity<>( ExceptionResponse.of(METHOD_NOT_ALLOWED), METHOD_NOT_ALLOWED.getHttpStatus() ); } /** * 잘못된 타입 변환 예외 처리 */ @ExceptionHandler(BindException.class) protected ResponseEntity<ExceptionResponse> handleBindException(BindException e) { log.error("handle BindException"); return new ResponseEntity<>( ExceptionResponse.of(INVALID_INPUT_VALUE, e.getBindingResult()), INVALID_INPUT_VALUE.getHttpStatus() ); } /** * 모든 예외를 처리 * 웬만해서 여기까지 오면 안됨 */ @ExceptionHandler(Exception.class) protected ResponseEntity<ExceptionResponse> handleException(Exception e) { log.error("handle Exception", e); return new ResponseEntity<>( ExceptionResponse.of(INTERNAL_SERVER_ERROR), INTERNAL_SERVER_ERROR.getHttpStatus() ); } }주요기능팀 등록 기능🤔 고려해볼 점1. 팀을 등록할 수 있어야 한다.2. 팀 이름이 null이거나 공란으로 요청이 갈 경우 예외처리3. 만약 이미 존재하는 팀이라면 예외를 던진다.요청 DTOspring boot starter validation을 통하여 요청 필드에 대하여 validation 처리DTO를 엔티티화 하는 로직부분을 해당 DTO안에 구현public record RegisterTeamRequestDto( @NotBlank(message = "이름은 공란일 수 없습니다.") @NotNull(message = "이름은 null일 수 없습니다.") String name ) { public Team toEntity() { return Team.builder() .name(name) .build(); } }서비스 레이어별 다른 것은 없고 insert 쿼리가 날려주는 작업으로 메서드에 트랜잭션 어노테이션을 붙여주었다.private 메서드로 해당 팀이 이미 존재하는지 확인하는 validation을 추가하였다. @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class TeamService { private final TeamRepository teamRepository; @Transactional public void registerTeam(RegistrationTeamRequestDto requestDto) { validateTeam(requestDto); Team team = requestDto.toEntity(); this.teamRepository.save(team); } /** * 팀 유효성 검사 * @param requestDto */ private void validateTeam(RegistrationTeamRequestDto requestDto) { if (this.teamRepository.existsByName(requestDto.name())) { throw new TeamAlreadyExistsException("이미 존재하는 팀 이름입니다."); } } }요청으로 온 DTO의 이름으로 유효한 팀인지 검사 후, 해당 DTO를 엔티티로 변환하고 저장시킨다.컨트롤러 레이어package me.sungbin.domain.team.controller; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import me.sungbin.domain.team.model.request.RegistrationTeamRequestDto; import me.sungbin.domain.team.model.response.TeamInfoResponseDto; import me.sungbin.domain.team.service.TeamService; import org.springframework.web.bind.annotation.*; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.domain.team.controller * @fileName : TeamController * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/team") @RequiredArgsConstructor public class TeamController { private final TeamService teamService; @PostMapping("/register") public void registerTeam(@RequestBody @Valid RegistrationTeamRequestDto requestDto) { this.teamService.registerTeam(requestDto); } }  테스트 결과성공(포스트맨)실패(이미 중복된 팀 이름)실패 (팀 이름이 공란이거나 null)테스트 코드package me.sungbin.domain.team.controller; import me.sungbin.domain.team.entity.Team; import me.sungbin.domain.team.model.request.RegistrationTeamRequestDto; import me.sungbin.domain.team.repository.TeamRepository; import me.sungbin.global.common.controller.BaseControllerTest; import me.sungbin.global.exception.GlobalExceptionCode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author : rovert * @packageName : me.sungbin.domain.team.controller * @fileName : TeamControllerTest * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ class TeamControllerTest extends BaseControllerTest { @Autowired private TeamRepository teamRepository; @BeforeEach void setup() { Team team = new Team("개발팀"); this.teamRepository.save(team); } @Test @DisplayName("팀 등록 테스트 - 실패 (팀 이름이 공란)") void register_team_test_fail_caused_by_team_name_is_empty() throws Exception { RegistrationTeamRequestDto requestDto = new RegistrationTeamRequestDto(""); this.mockMvc.perform(post("/api/team/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isNotEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("팀 등록 테스트 - 실패 (이미 존재하는 팀)") void register_team_test_fail_caused_by_already_exists_team() throws Exception { RegistrationTeamRequestDto requestDto = new RegistrationTeamRequestDto("개발팀"); this.mockMvc.perform(post("/api/team/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("팀 등록 테스트 - 성공") void register_team_test_success() throws Exception { RegistrationTeamRequestDto requestDto = new RegistrationTeamRequestDto("디자인팀"); this.mockMvc.perform(post("/api/team/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("팀 정보 조회 테스트 - 성공") void find_team_info_test_success() throws Exception { this.mockMvc.perform(get("/api/team") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); } }기본으로 사용하는 어노테이션들을 아래의 어노테이션으로 묶음package me.sungbin.global.common.annotation; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author : rovert * @packageName : me.sungbin.global.annotation * @fileName : IntegrationTest * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Transactional @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface IntegrationTest { }이 어노테이션을 BaseControllerTest라는 클래스에 선언@Disabled @IntegrationTest public class BaseControllerTest { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; }추가적으로 test 디렉터리에도 resources 디렉터리 생성 후 해당 경로에 application.yml을 생성후 테스트 profile active시켜두었다.spring: profiles: active: test --- spring: config: activate: on-profile: test datasource: url: "jdbc:h2:mem:commutedb" username: sa password: driver-class-name: org.h2.Driver jpa: properties: hibernate: show_sql: true format_sql: true open-in-view: false threads: virtual: enabled: true직원 등록 기능🤔 고려점1. 직원을 먼저 생성한다. (필수 값들은 공란일 수 없음)2. 해당 직원을 팀에 등록 시킨다. (단, 등록할 직원이 매니저인 경우 해당 팀의 매니저가 없어야 한다.)3. 등록하려는 팀이 존재해야 한다.주요 코드를 보자. 먼저 연관관계 매핑을 해야한다.package me.sungbin.domain.employee.entity; import jakarta.persistence.*; import lombok.*; import me.sungbin.domain.employee.type.Role; import me.sungbin.domain.team.entity.Team; import me.sungbin.global.common.entity.BaseDateTimeEntity; import org.hibernate.annotations.Comment; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.domain.member.entity * @fileName : Member * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Entity @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AttributeOverrides({ @AttributeOverride(name = "createdAt", column = @Column(name = "work_start_date", nullable = false, updatable = false)), @AttributeOverride(name = "updatedAt", column = @Column(name = "updated_at", nullable = false)) }) public class Employee extends BaseDateTimeEntity { @Id @Comment("직원 테이블 PK") @Column(name = "employee_id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Comment("직원 이름") @Column(name = "employee_name", nullable = false) private String name; @Comment("팀의 매니저인지 아닌지 여부") @Column(nullable = false) private boolean isManager; @Column(nullable = false) private LocalDate birthday; @Builder public Employee(String name, boolean isManager, LocalDate birthday) { this.name = name; this.isManager = isManager; this.birthday = birthday; } @ManyToOne(fetch = FetchType.LAZY) private Team team; public void updateTeam(Team team) { this.team = team; } public String getTeamName() { return this.team.getName(); } public String getRole() { return isManager ? Role.MANAGER.name() : Role.MEMBER.name(); } } package me.sungbin.domain.team.entity; import jakarta.persistence.*; import lombok.*; import me.sungbin.domain.employee.entity.Employee; import me.sungbin.global.common.entity.BaseDateTimeEntity; import org.hibernate.annotations.Comment; import java.util.ArrayList; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.domain.team.entity * @fileName : Team * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Entity @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AttributeOverrides({ @AttributeOverride(name = "createdAt", column = @Column(name = "created_at", nullable = false, updatable = false)), @AttributeOverride(name = "updatedAt", column = @Column(name = "updated_at", nullable = false)) }) public class Team extends BaseDateTimeEntity { @Id @Comment("팀 테이블 PK") @Column(name = "team_id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Comment("팀 이름") @Column(name = "team_name", nullable = false, unique = true) private String name; @OneToMany(mappedBy = "team") private List<Employee> employees = new ArrayList<>(); @Builder public Team(String name) { this.name = name; } public void addEmployee(Employee employee) { this.employees.add(employee); employee.updateTeam(this); } public String getManagerName() { return employees.stream() .filter(Employee::isManager) .map(Employee::getName) .findFirst() .orElse(null); } public boolean hasManager() { return this.employees.stream().anyMatch(Employee::isManager); } public int getEmployeeCount() { return employees != null ? employees.size() : 0; } }위와 같이 연관관계 매핑을 해준다. 여기서 Employee의 getRole부분의 메서드의 Role은 enum타입으로 아래와 같이 되어 있다.package me.sungbin.domain.employee.type; import lombok.Getter; import lombok.RequiredArgsConstructor; import me.sungbin.global.common.type.EnumType; /** * @author : rovert * @packageName : me.sungbin.domain.member.type * @fileName : Role * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @Getter @RequiredArgsConstructor public enum Role implements EnumType { MEMBER("MEMBER", "팀원"), MANAGER("MANAGER", "매니저"); private final String name; private final String description; }위의 코드를 보면 EnumType이라는 인터페이스가 있는데 그 안에는 아래와 같다.package me.sungbin.global.common.type; /** * @author : rovert * @packageName : me.sungbin.global.common.type * @fileName : EnumType * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ public interface EnumType { String name(); String getDescription(); }이렇게 한 이유는 나중의 확장성 때문에 구현을 해둔 것이다.@Transactional public void registerEmployee(RegistrationEmployeeRequestDto requestDto) { Employee employee = requestDto.toEntity(); Team team = this.teamRepository.findByName(requestDto.teamName()).orElseThrow(TeamNotFoundException::new); // 매니저가 이미 존재하는 경우 예외 발생 if (employee.isManager() && team.hasManager()) { throw new AlreadyExistsManagerException("이미 매니저가 해당 팀에 존재합니다."); } this.employeeRepository.save(employee); team.addEmployee(employee); this.teamRepository.save(team); }그리고 위와 같이 서비스 로직을 작성해준다. 해당 로직은 dto로부터 엔티티화 시키고 요청한 팀의 이름으로 팀이 존재하는지 찾는다.만약 없으면 예외를, 있다면 해당 팀에 매니저가 존재하는지 유무도 추가해두었다. 이미 있다면 예외를 없다면 해당 직원을 저장시킨다. 컨트롤러 레이어package me.sungbin.domain.employee.controller; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import me.sungbin.domain.employee.model.request.EmployeesInfoResponseDto; import me.sungbin.domain.employee.model.request.RegistrationEmployeeRequestDto; import me.sungbin.domain.employee.service.EmployeeService; import org.springframework.web.bind.annotation.*; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.domain.member.controller * @fileName : EmployeeController * @date : 3/1/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 3/1/24 rovert 최초 생성 */ @RestController @RequiredArgsConstructor @RequestMapping("/api/employee") public class EmployeeController { private final EmployeeService employeeService; @PostMapping("/register") public void registerEmployee(@RequestBody @Valid RegistrationEmployeeRequestDto requestDto) { this.employeeService.registerEmployee(requestDto); } }  테스트성공실패 (존재하는 팀이 없음)실패(이미 그 팀에 매니저가 있음)테스트코드class EmployeeControllerTest extends BaseControllerTest { @Autowired private TeamRepository teamRepository; @Autowired private EmployeeRepository employeeRepository; @BeforeEach void setup() { Team team = new Team("개발팀"); this.teamRepository.save(team); } @Test @DisplayName("직원 등록 테스트 - 실패 (잘못된 입력 값)") void register_employee_test_fail_caused_by_wrong_input() throws Exception { RegistrationEmployeeRequestDto requestDto = new RegistrationEmployeeRequestDto("", "", false, LocalDate.of(1996, 5, 22)); this.mockMvc.perform(post("/api/employee/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isNotEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("직원 등록 테스트 - 실패 (존재하지 않는 팀에 등록)") void register_employee_test_fail_caused_by_register_not_exists_team() throws Exception { RegistrationEmployeeRequestDto requestDto = new RegistrationEmployeeRequestDto("장그래", "영업팀", false, LocalDate.of(1992, 2, 22)); this.mockMvc.perform(post("/api/employee/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("message").exists()) .andExpect(jsonPath("status").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getHttpStatus().name())) .andExpect(jsonPath("code").value(GlobalExceptionCode.INVALID_INPUT_VALUE.getCode())) .andExpect(jsonPath("errors").exists()) .andExpect(jsonPath("errors").isEmpty()) .andExpect(jsonPath("timestamp").exists()); } @Test @DisplayName("직원 등록 테스트 - 성공") void register_employee_test_success() throws Exception { RegistrationEmployeeRequestDto requestDto = new RegistrationEmployeeRequestDto("양성빈", "개발팀", false, LocalDate.of(1996, 5, 22)); this.mockMvc.perform(post("/api/employee/register") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } }팀 조회 기능서비스 레이어public List<TeamInfoResponseDto> findTeamInfo() { List<Team> teams = this.teamRepository.findAll(); return teams.stream().map(TeamInfoResponseDto::new).toList(); }해당 팀들을 findAll로 select한 이후로 응답 DTO로 매핑해준다.아래는 포스트맨 테스트 결과다.이제 테스트 코드를 살펴보자.@Test @DisplayName("팀 정보 조회 테스트 - 성공") void find_team_info_test_success() throws Exception { this.mockMvc.perform(get("/api/team") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); }직원 조회 기능서비스 레이어public List<EmployeesInfoResponseDto> findEmployeesInfo() { List<Employee> employees = this.employeeRepository.findAll(); return employees.stream().map(EmployeesInfoResponseDto::new).toList(); }전체 직원을 select하여 stream 객체를 이용하여 응답 DTO와 매핑해주었다. 아래는 테스트 결과다.아래는 테스트 코드다.@Test @DisplayName("직원 정보 조회 테스트 - 성공") void find_employees_info_test_success() throws Exception { this.mockMvc.perform(get("/api/employee") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()); }회고1단계는 이제까지 우리가 배운 개념들로 충분히 개발 할 수 있는 것들이였다. 하지만 나는 여기서 더 나아가서 좀 더 예외상황을 생각해보고 더 발전시키도록 노력했다. 그리고 또한 다른 러너분들과 코드리뷰를 통해 내 코드를 리팩토링 해가면서 뭔가 실력이 점점 쌓여만 가는 것 같았다. 

백엔드인프런워밍업스터디클럽미니프로젝트

최경희

[인프런 워밍업 클럽 0기] FE 1주차

1주차 학습 요약2일차 - 자바스크립트 기초, Window 객체 및 DOM, Event자바스크립트 기초 강의에서는 Console 객체, 변수 선언 방법과 참조 범위, 호이스팅에 대한 개념, 타입 종류와 타입을 변환하는 방법, 연산, Template Literals, 반복문을 배웠으며, Window 객체 및 DOM 강의에서는 Window 객체에 대한 개념과 사용법, DOM에 대한 개념과 Document 객체를 사용한 DOM 조작법을 배웠습니다. 또한 Event 강의를 통해 여러가지 이벤트 상황에 대해 동작을 설정할 수 있는 방법과 버블링, 캡처링, 위임에 대한 개념을 배울 수 있었습니다. 3일차 - 자바스크립트 중급(1)this 키워드에 대한 개념과 사용법, bind/call/apply 메서드를 사용한 this의 참조값 변경, 삼항 연산자 사용법, 간단한 동기/비동기 개념과 이벤트 동작에 대한 내부 작동 과정, Closure에 대한 개념과 사용법, 배열/객체에 대한 구조 분해 할당 방법, 전개 연산자 사용법, 배열에 대한 Map/Filter/Reduce 메서드 사용법을 배웠습니다. 4일차 - 자바스크립트 중급(2)1일차에 배웠던 undefined와 null의 공통점과 차이점, 얕은 비교/복사 와 깊은 비교/복사에 대한 개념과 사용법, 함수 표현식과 선언문의 개념과 차이점, 즉시 실행 함수(IIFF)의 개념과 사용법, Intersection Observer 사용법, pure/impure 함수의 개념, 커링 개념과 사용법, 엄격 모드에 대한 개념과 적용법, 특징을 배웠습니다. 5일차 - OOP, 비동기OOP 강의에서는 객체 지향 프로그래밍에 대한 개념과 특징, 프로토타입에 대한 개념과 사용법, 클래스에 대한 개념과 사용법을 배웠으며, 비동기 강의에서는 callbacks, promise, async/await에 대한 개념과 사용법을 배웠습니다. 6일차 - Iterator / Generator, Design PatternIterator / Generator 강의에서는 1일차 타입 설명 때 간단하게 지나갔던 심볼에 대한 개념과 사용법, Iterator와 generator에 대한 개념과 사용법을 배웠으며, Design Pattern 강의에서는 디자인 패턴에 대한 개념과 장점 그리고 가장 자주 사용되는 패턴인 Singleton 패턴과 Factory 패턴, Mediator 패턴, Observer 패턴, Module 패턴에 대한 개념을 배웠습니다. 미션 해결 과정미션 1 : 음식 메뉴 앱각 메뉴를 클릭할 때, 메뉴에 맞는 음식 리스트를 보여준다.음식 메뉴가 아닌 투썸 플레이스 카페 메뉴로 변경했고, 메뉴 부분의 스타일과 전체적인 색상을 변경했습니다.이미지는 투썸 플레이스 카페의 홈페이지에서 이미지 주소 복사를 통해 가져왔습니다. 스타일을 수정할 때 가장 시간이 오래 걸렸으며, 현재 화면 크기에 따라 음식 아이템들의 내용이 잘리는 문제가 있어 검색을 통해 수정하고 있는 중 입니다. 미션 2 : 가위 바위 보 앱컴퓨터와 진행하는 가위 바위 보 게임입니다.전체적인 색상만 변경하고, 전체적인 스타일은 과제 예시와 비슷하게 만들었습니다.스타일을 구현하는 것이 가장 고민이 됐고 시간도 가장 오래걸렸습니다. 현재 전체적인 스타일이 과제 예시와 비슷하게 만들어져 있지만 이미지를 넣어 조금 더 보기 좋게 바꾸려고 생각 중입니다. 회고스터디를 시작하기 전에는 그래도 어느 정도 자바스크립트에 대해 알고 있다고 생각했었는데, 한 주 동안 자바스크립트에 대한 강의를 들으면서 제가 알고 있었던건 기초뿐이었다는 사실을 알게 되었습니다. 그래서 자바스크립트 강의를 들으면서 처음에는 조금 이해하기 힘든 부분도 있었고, 개념은 이해했지만 이 개념을 코드에 어떻게 적용하면 좋을지 언제 사용하면 되는지는 알기 힘들었습니다. 검색을 통해 더 알아보면서 완벽하게는 아니지만 어느 정도는 이해하고 사용할 수 있게 되었습니다. 하지만 과제는 웹에 대한 공부를 올해부터 시작했던 만큼 HTML과 CSS에 대해서도 부족한 점이 많아 이번 주에 있던 모든 과제를 완성하지 못한 것이 아쉬웠습니다. 그래도 점점 속도가 붙고 있어 과제 제출 전까지 모든 과제를 완성하는 것이 목표입니다. 역시 직접 만들어 보는 것이 배운 것을 확실하게 익힐 수 있는 시간이 된 것 같습니다.

프론트엔드

양성빈

[인프런 워밍업 스터디 클럽] 0기 두번째 발자국 (2 week)

발자국어느덧 인프런 워밍업 스터디 클럽을 시작한지도 2주째가 시작된다. 그리고 이번주 1주에 대한 회고를 시작해보려고 한다.이번주도 여러가지를 배우고 많은 경험이 된 한 주였다. 그럼 회고를 시작하겠다. 완주 및 우수러너를 위해 오늘도 달려본다.강의 요약Day 6. 스프링 컨테이너의 의미와 사용방법📖 UserController와 스프링 컨테이너상식적으로 static이 아닌 코드를 사용하려면 객체화(인스턴스화)가 필수적이다. 하지만 이전 학습의 UserController부분을 확인해보면 의아한 부분이 존재한다.private final UserService userService; public UserController(JdbcTemplate jdbcTemplate) { this.userService = new UserService(jdbcTemplate); }이렇게 UserService는 UserController 생성자 부분에서 인스턴스화를 하였지만, 정작 UserController부분은 인스턴스화를 해주지 않았지만 잘 작동하는 것을 알 수 있었다. 이로 인해 아래의 의문점이 남아진다.🙋🏻 그럼 누가 UserController부분을 인스턴스화 시켜준다는건데 누가 그런 걸 해주나요?또한 위의 코드에서 또 하나의 의문점이 남는다.🙋🏻 그리고 나는 JdbcTemplate 클래스를 따로 만져준 적이 없는데 UserController 클래스는 어떻게 이 클래스를 가져올 수 있을까요?바로 @RestController라는 어노테이션때문이다. 우리는 앞전에 @RestController라는 어노테이션이 API 진입점이라고 배웠다. 하지만 이 @RestController는 진입점의 역할과 더불어 UserController 클래스를 스프링 빈으로 등록을 시켜준다.🙋🏻 그럼 스프링 빈이 뭐에요? 빈은 영어니까 번역하면 콩인것 같은데 그럼 스프링 콩인가요?위의 질문이 나는 자연스럽게 떠올랐다. 그럼 정확히 스프링 빈이 무엇인지 알아보자.🫛 스프링 빈우리가 스프링 부트로 만든 프로젝트를 동작시키면, 우리가 만든 서버가 동작을 하는 것이다. 그러면 이 서버 내부에 거대한 컨테이너를 만들어준다. 그리고 컨테이너 안에는 빈으로 등록시킨 클래스 정보(이름, 타입)가 들어간다. 그리고 이 클래스를 인스턴스화 시켜준다. 이 때, 들어간 클래스를 스프링 빈이라고 부른다.🙋🏻 그런데 여기서 위의 코드를 보면 UserController를 인스턴스화할려면 JdbcTemplate가 필요하지 않나요?요놈은 어디서 가져오는 거에요?사실, JdbcTemplate 클래스도 빈으로 등록된 클래스이다.🙋🏻 그럼, 누가 JdbcTempalte을 인스턴스화 시켜줬어요?바로 build.gradle에 dependencies에 등록한 spring boot starter data jpa라는 것이 JdbcTemplate을 등록시켜줬다.dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' }그래서 인텔리제이로 UserController 생성자 부분을 보면 책모양으로 아이콘이 있는데 이것이 빈으로 등록되었다는 의미이다.즉, 결론을 내보면 우리가 가져온 Dependency가 JdbcTemplate을 빈으로 등록시켜준다는 의미이다.그러면 여기서 또 하나 결론이 나온다. 스프링 컨테이너 안에 우리가 작성한 스프링 빈으로 등록한 클래스는 이 컨테이너 안에 들어가게 된다. 또한 필요한 의존성이 자동 설정된다.그럼 여기서 의문점이 든다. 우리가 이전에 작성한 UserRepository 클래스와 UserService 클래스도 JdbcTemplate의 의존성이 필요하고 이 JdbcTemplate을 가져오려면 이 두개의 클래스도 빈으로 등록이 되어있어야 한다. 하지만 이 2개의 클래스는 빈으로 등록되지 않았다. 인텔리제이 화면만 봐도 책 모양 아이콘이 존재하지 않는다. 그럼 2개의 클래스를 빈으로 등록시키자! 🫛 Repository와 Service 빈 등록시키기 & Controller 클래스 변경두개의 클래스를 빈으로 등록시키는 방법은 정말 간단하다. Repository 클래스는 @Repository 어노테이션을 클래스 위에 붙여주고, Service 클래스는 @Service 어노테이션을 클래스 위에 붙여주면 빈으로 등록이 된다. 그리고 Controller부분을 수정해본다. 그럼 아래와 같이 변경될 것이다. 코드는 일부만 표기하겠다.UserController.java@RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } ... UserService.java@Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } ... UserRepository.java@Repository public class UserRepository { private final JdbcTemplate jdbcTemplate; public UserRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } ... 📚 정리그러면 한번 정리해보자. 스프링 서버가 시작이 되면 의존성에 의해 빈으로 등록된 JdbcTemplate이 스프링 컨테이너로 들어간다. 그리고 이 JdbcTemplate의 의존성을 가진 UserRepository가 빈으로 등록된다. 그러면 UserRepository를 의존하는 UserService가 빈으로 등록된다. 그리고 UserService를 의존하는 UserController가 빈으로 등록된다.그런데 아래의 의문점이 든다.🤔 아니 뭐가 좋아진거야? 그냥 new 연산자로 객체생성하면 안되는건가? 스프링 컨테이너 왜 쓰는데?이 의문점은 다음 강의에서 해소가 되었다.📖 스프링 컨테이너를 왜 사용할까?만약 아래의 요구사항이 있다고 하자.책 이름을 저장하는 API를 구현하라. 단, 이름 저장은 메모리에 저장시킨다.우리는 그럼 열심히 비즈니스 로직을 만들 것이다. 먼저 Book 객체부터 만들고, BookController, BookService, BookMemoryRepository를 만들 것이다. 그리고 BookMemoryRepository를 BookService는 아래와 같이 객체를 생성할 것이다.그런데 이렇게 열심히 만들고나니 추가 요구사항이 생겼다.public class BookService { private final BookMemoryRepository repository = new BookMemoryRepository(); } 생각해보니, 메모리가 아닌 MySQL과 같은 RDB에 저장시켜야해! 그리고 JdbcTemplate은 Repository가 바로 설정할 수 있다 하자.그러면 BookMySQLRepository를 만들고 BookService에 BookMemoryRepository가 아닌 BookMySQLRepository를 인스턴스화 해줘야 한다.public class BookService { private final BookMySQLRepository repository = new BookMySQLRepository(); }이런 과정을 하면서 우리는 불편함을 느꼈을 것이다. 우리는 repository의 기능적인 역할만 변경하였는데 서비스 코드까지 변경해야하는 경우가 생긴 것이다. 지금은 몇개 안되지만, 이 repository를 쓰는 서비스 코드가 수백개 클래스에 있다면 바로 오늘 야근을 해야하고 야근 신청서를 올려야 한다.🥲그러면 이런 야근을 피하기 위해서 repository를 변경하더라도 서비스 클래스는 변경을 안하는 방법은 없을까? 그래서 생각을 한 것이 java의 interface를 이용하는 방법이다. BookRepository라는 인터페이스를 만들고 BookMemoryRepository와 BookMySQLRepository를 구현하면 되는 것이다. 그러면 서비스 코드는 이런 식으로 변경 될 것이다.public class BookService { private final BookRepository repository = new BookMySQLRepository(); }하지만 그래도 서비스 코드는 repository 역할 변경에 다라 수정이 되긴 해야한다. 바로 new 연산자의 부분을 전부 변경해야 하기 때문이다. 또 야근 당첨이다 🥲 그러면 이걸 또 해결할 수 있는 방법은 없을까? 바로 스프링 컨테이너가 그래서 등장하였다.스프링 컨테이너가 BookService 대신 repository를 인스턴스화 해주고 그때 그때 알아서 어떤 repository 클래스를 쓸지 결정을 해줄 수 있다. 이런 방식을 제어의 역전(IoC, Inversion of Control)이라고 한다. 그리고 컨테이너가 repository 클래스를 선택해서 서브스 레이어에 넣어주는 과정의 의존성 주입(DI, Dependency Injection)라고 한다.그러면 어떤 Repository를 주입시켜줄까? 그것은 우리가 @Primary 어노테이션을 활용해 조절할 수 있다.@Primary: 우선권을 결정하는 어노테이션📖 스프링 컨테이너를 다루는 방법@Configuration: 클래스에 붙여주는 어노테이션, @Bean 어노테이션과 같이 사용@Bean: 보통은 메서드 위에 붙으며, 해당 메서드에서 반환되는 객체를 스프링 빈으로 등록시켜준다.그리고 아래의 의문사항이 든다. 그러면 우리가 이전에 @Service, @Repository 어노테이션을 붙여줬는데 이 어노테이션은 언제 사용해야할까? 위의 @Configuration + @Bean 어노테이션을 쓰면 안될까?요약하자면 다음과 같다.@Service나 @Repository 어노테이션은 개발자가 직접 만든 클래스를 빈으로 등록시키고 싶을 때 사용하며,@Configuration + @Bean 어노테이션은 외부 라이브러리나 프레임워크에서 만든 클래스를 등록시킬때 사용한다.다음으로 살펴 볼 어노테이션은 @Component 어노테이션이다.@Component: 주어진 클래스를 컴포넌트로 간주하며, 이 클래스들은 스프링 서버가 시작할 때 자동감지한다.@Component 어노테이션 덕분에 우리가 사용했던 어노테이션들이 감지가 된것이다.Service.javapackage org.springframework.stereotype; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Service { @AliasFor( annotation = Component.class ) String value() default ""; }Repository.javapackage org.springframework.stereotype; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Repository { @AliasFor( annotation = Component.class ) String value() default ""; }이렇게 각 어노테이션들의 내부구조를 보면 이렇게 @Component 어노테이션이 들어가져 있다.그럼 @Component 어노테이션은 언제 사용할까?컨트롤러, 서비스, 리포지토리가 모두 아니고 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용한다.🫛 빈 주입 받는 방법빈을 주입받는 방법은 3가지가 존재한다.생성자를 이용한 주입방법 (권장)setter와 @Autowired -> 누군가 setter를 사용하면서 오작동 가능성이 존재private final JdbcTemplate jdbcTemplate; @Autowired public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; }필드에 직접 @Autowired 사용 -> 테스트 어려움@Autowired private JdbcTemplate jdbcTemplate;마지막으로 @Qualifier 어노테이션을 알아보자. @Primary와 유사하다.스프링 빈을 사용하는 쪽, 스프링 빈을 등록하는 쪽 모두 @Qualifier를 사용할 수 있다!스프링 빈을 사용하는 쪽에서만 쓰면, 빈의 이름을 적어주어야 한다. 양쪽 모두 사용하면, @Qualifier 끼리 연결된다!@Service @Qualifier("main") public class BananaService implements FruitService { }@RestController public class UserController { private final UserService userService; private final FruitService fruitService; public UserController(UserService userService, @Qualifier("main") FruitService fruitService) { this.userService = userService; this.fruitService = fruitService; }그러면 @Qualifier와 @Primary 어노테이션중에 누가 우선순위가 높을까?사용하는 쪽에서 직접 적어준 @Qualifier가 이긴다!📚섹션3 정리클린코드가 무엇이고, 우리의 코드를 레이어 아키텍쳐로 분리도 해보며, 스프링 컨테이너가 무엇이고 스프링 빈이 무엇인지 이해를 하며 어떤 어노테이션을 통해 주입을 받고 빈으로 등록할 수 있는지 알아보았다. Day7. Spring Data JPA를 사용한 데이터베이스 조작📖 문자열 SQL을 직접 사용하는 것이 너무 어렵다!!우리는 현재 레이어드 아키텍쳐로 코드를 작성하였고, 해당 빈들을 스프링 컨테이너가 관리를 하였고 포스트맨을 통하여 API를 호출하였다. 또한 repository 레이어로 mysql과 통신을 하였다. 그런데 repository 레이어에서는 DB 쿼리를 문자열로 작성하였다. 하지만 이렇게 문자열로 작성하면 아래와 같은 문제가 있을 수 있다.문자열로 작성 시, 오타가 날 수 있는 실수가 있다. 하지만 이 실수는 컴파일 타임에 발견되지 않고 런타임에 발견되는 안 좋은 점이 있다. 그래서 어플리케이션 운영 시점에 해당 API를 사용 시, 에러를 확인할 수 있기에 엄청 치명적이다.특정 DB에 종속적이다. 만약 우리가 MySQL을 쓰다가 어느 이유로 DB를 변경하게 된다면 해당 쿼리들을 변경하는 DB 쿼리 문법에 맞게 수정해줘야한다. 마이그레이션도 일이지만 해당 쿼리를 다 고쳐야한다면 야근 당첨일 것이다. 🥲반복 작업이 많아진다. 보통 테이블당 기본적으로 CRUD 쿼리를 작성해줘야 하는데, 단순 반복작업들이 이어질 수 있다.데이터베이스 테이블과 객체의 패러다임이 다르다. 쉽게 생각해서 연관관계 매칭을 할 때 양방향 매핑을 할 때 연관관계 갖는 테이블 A는 B를 가리키고 B또한 A클래스를 가리킬 수 있지만 실제 테이블은 한쪽만 가리키게 된다. 또한 상속개념은 자바는 존재하지만 DB는 상속개념을 구현하기 매우 힘들다. 그래서 JPA라는 것이 등장하였다. JPA는 ORM의 일종인데 이 두 용어를 살펴보면 아래와 같다.JPA(Java Persistence API) : 자바 영속성 API그럼 영속성은 무엇일까? 우리는 이전에 메모리에 회원 정보를 저장하는 코드를 작성했지만 이런 코드는 서버를 재부팅하면 데이터는 날라간다. 그 이유는 RAM에 데이터가 저장되기 때문이다. 그런데 영속성은 서버가 재부팅되어도 데이터는 영구적으로 저장되는 속성을 의미한다.그리고 API는 일종의 규칙이다. 그래서 이것을 풀어써보면 아래와 같다.JPA란, 데이터를 영구적으로 보관하기 위해 자바 진영에서 정해진 규칙을 뜻한다.그러면 ORM은 무엇일까? 자바코드와 DB의 테이블을 짝 지어준다는 의미이다.📚 요약 (JPA란?)객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 자바 진영의 규칙을 뜻한다.그런데 JPA를 검색해보면 연관검색으로 Hibernate가 나온다. 이 Hibernate란 무엇일까?JPA는 쉽게 규칙이라고 하였다. 이 규칙을 구현한 구현체가 Hibernate이다. 또한 Hibernate은 내부적으로 JDBC를 사용한다. 📖 유저 테이블에 대응되는 Entity Class 만들기이제 실제로 유저 테이블과 유저 클래스를 매핑시켜보자. 이를 위해선 어노테이션 @Entity를 붙여줘야 한다.🙋🏻 Entity란?저장되고 관리되어야 하는 데이터를 의미한다.유저 테이블은 위와 같이 구성되어 있다. 먼저, id를 primary key로 설정되어 있고 auto_increment가 적용되어 있다. 이것을 자바 코드에 적용하려면 @Id와 @GeneratedValue(strategy=GenerationType.IDENTY)를 설정해줘야 한다. 그렇게 적용한 코드는 아래와 같다.@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; .... } DB의 종류마다 자동 생성 전략이 다르다!우리는 MySQL의 auto_increment를 사용했고, 이는 IDENTITY 전략과 매칭된다. JPA를 사용하기 위해 기본 생성자가 반드시 필요하다.다음으로 name 부분을 짝 지어줘야 한다.이를 위해서 @Column 어노테이션을 통해 매핑해줘야 한다.@Column(nullable = false, length = 20, name = "name") private String name;여기서 nullable = false는 이 속성은 null이 불가능하다는 의미이며, length = 20은 DB로 보면 varchar(20)을 의미한다.또한 name = "name"은 이 속성은 테이블의 name 필드와 매핑시키겠다는 의미이다.⚠ 참고참고로 name은 필드이름과 동일할 경우 생략이 가능하다.그리고 이런 nullable, length등 이런 속성을 기본으로 쓸 때 @Column 어노테이션 자체를 생략이 가능하다.이제 application.yml로 JPA 설정을 해줘야 한다. jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true dialect: org.hibernate.dialect.MySQLDialectddl-auto: 스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지create : 기존 테이블이 있다면 삭제 후 다시 생성create-drop : 스프링이 종료될 때 테이블을 모두 제거update : 객체와 테이블이 다른 부분만 변경validate : 객체와 테이블이 동일한지 확인none : 별다른 조치를 하지 않는다.show_sql: JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄 것인가format_sql: SQL을 보여줄 때 예쁘게 포맷팅 할 것인가, 여기서 예쁘게는 뭔가를 꾸미는게 아니라 우리가 쉽게 볼 수 있게 포맷팅을 해준다는 것이다.dialect: 방언(사투리), 이 옵션으로 DB를 특정하면 조금씩 다른 SQL을 수정해준다.⚠ 주의강좌에서는 방언 설정을 할 때 org.hibernate.dialect.MySQLDialect를 org.hibernate.dialect.MySQL8Dialect로 하셨다. 하지만 최근에 org.hibernate.dialect.MySQL8Dialect가 deprecated가 되었다는 warning이 발생한다. 그리고 org.hibernate.dialect.MySQLDialect로 변경하라고 써져있다.📖 Spring Data JPA를 이용해 자동으로 쿼리 날리기우리는 이제 직접 sql을 작성해주지 않고 JPA를 이용하여 유저의 생성/조회/업데이트 기능을 리팩토링할 것이다.먼저 아래와 같이 Repository 인터페이스를 만들어준다.public interface UserRepository extends JpaRepository<User, Long> { }그리고 서비스 코드에서 해당 UserRepository로 의존성 주입을 한다.다음으로 생성 부분 메서드를 만들어보자.public void saveUser(UserCreateRequest request) { this.userRepository.save(new User(request.getName(), request.getAge())); }여기서 save 메서드는 JpaRepository를 상속받은 Repository에 정의되어 있지 않지만 사용이 가능하다. 그 이유는 Spring Data JPA에서 기본으로 제공해주는 저장 로직이 담긴 로직이다. 해당 메서드를 실행하면 insert 쿼리가 날라간다.다음으로 조회 부분 메서드를 보자.public List<UserResponse> getUsers() { return this.userRepository.findAll() .stream().map(UserResponse::new) .collect(Collectors.toList()); }여기서 findAll 메서드도 기본으로 제공한다. 이 메서드의 반환은 List형태이다. 이 메서드를 실행하면 select * ~ 쿼리가 날라간다.다음으로 업데이트 기능으로 보자. 업데이트는 유저가 존재하는지 확인하고 있다면 update쿼리를 아니면 예외를 날린다.public void updateUser(UserUpdateRequest request) { User user = this.userRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new); user.updateName(request.getName()); this.userRepository.save(user); }먼저 findById라는 메서드를 호출한다. 이 메서드는 기본으로 제공해주는 메서드로 해당 메서드는 select * from user where id = ?의 쿼리를 날려준다. 이 메서드의 반환타입은 1개의 데이터를 가져오기 때문에 객체 단일 타입으로 반환된다. 여기선 User가 반환된다. 그리고 updateName이라는 메서드를 엔티티에 만들어준다. 이 메서드는 단순 setter의 역할이다. 마지막으로 setter로 속성 변경을 한 후 save로 저장을 시킨다.그럼 여기서 이렇게 메서드를 통해 쿼리 작성없이 쿼리가 날라갈 수 있는 이유는 JPA가 아닌 Spring Data JPA 때문이다.Spring Data JPA: 복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리즉, 전체적인 구조를 보면 Spring Data JPA가 JPA라는 규칙을 사용하는데 이 규칙은 Hibernate가 이 규칙을 구현했고 Hibernate는 구현할때 JDBC를 사용한다고 볼 수 있다. 📖 Spring Data JPA를 이용해 다양한 쿼리 작성하기이제 삭제 기능을 Spring Data JPA로 변경해보자. 먼저 삭제는 요청으로 들어온 유저의 이름이 존재하는지 확인하고 있다면 삭제쿼리를 날리고 아니면 예외를 날린다.public void deleteUser(String name) { User user = this.userRepository.findByName(name).orElseThrow(IllegalArgumentException::new); this.userRepository.delete(user); }여기서 나온게 findByName과 delete 메서드이다. findByName은 기본으로 제공해준 메서드가 아니고 우리가 인터페이스에 정의를 해야한다.public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByName(String name); }이런식으로 정의를 하면 이 메서드를 사용할때 select * from user where name = ? 쿼리가 나간다.다음으로 delete 메서드는 기본으로 제공해주는 메서드이다. 이 메서드를 사용하면 delete SQL이 나간다.이제 구체적으로 findByName처럼 우리가 일정 규칙에 맞게 인터페이스에 정의를 하면 쿼리들을 제공해주는데 그 규칙들을 살펴보자.find : 1건을 가져온다. 반환 타입은 객체가 될 수도 있고, Optional<타입>이 될 수도 있다.findAll : 쿼리의 결과물이 N개인 경우 사용. List<타입> 반환.exists : 쿼리 결과가 존재하는지 확인. 반환 타입은boolean count : SQL의 결과 개수를 센다. 반환 타입은 long이다.이제 By뒤에 규칙을 알아볼 텐데 By뒤에는 where 조건을 적어주는 것처럼 적어주면 된다. 조건이 여러개일 경우 And 혹은 or 조건을 통해 규칙을 정해준다.🏛 예시List<User> findAllByNameAndAge() : select * from user where name = ? and age = ?그외에 아래와 같이 다양한 조건들을 붙일 수 있다.📚 By 뒤에 조건GreaterThan : 초과GreaterThanEqual : 이상LessThan : 미만LessThanEqual : 이하Between : 사이에StartsWith : ~로 시작하는EndsWith : ~로 끝나는 Day8. 트랜잭션과 영속성 컨텍스트📖 트랜잭션 이론편트랜잭션이란 무엇일까? 트랜잭션 말만 들어봤지 이게 정확히 무슨 의미인지 알지 못했다. 트랜잭션은 아래와 같이 말한다.트랜잭션: 쪼갤 수 없는 업무의 최소 단위 = 모두 성공시키거나, 모두 실패시킨다.상황을 살펴보자.쇼핑몰이 있다고 하자. 어떤 회원이 주문을 하는 상황을 생각해보자. 주문을 하면 주문내역이 저장되고 포인트가 저장되고 결제기록이 저장될 것이다. 이 비즈니스 로직은 하나의 메서드로 묶여 있다. 그러다가 어떠한 이유로 결제기록의 비즈니스 로직에서 에러가 발생했다고 하자. 그러면 주문내역과 포인트는 있는데 결제되었다는 사실이 없을 것이다. 이런 경우 특정 비즈니스 로직에 에러가 발생할 경우 모든 SQL을 실패시켜야 할 것이다. 물론 모두 성공할 경우 성공시켜야 할 것이다. 이것을 트랜잭션이 해결해준다.DB 쿼리로 트랜잭션 시작을 알리는 쿼리는 아래와 같다.start transaction;트랜잭션 정상 종료는 아래와 같다.commit;트랜잭션 실패 처리는 아래와 같다.rollback;이 실습을 통해 알게 된 점은 트랜잭션 안에 저장/업데이트/삭제 쿼리가 발생해도 commit 전까지 반영이 안 된다는 점이다. 📖트랜잭션 적용과 영속성 컨텍스트Spring Data JPA에서 트랜잭션 적용은 @Transactional 어노테이션으로 해결할 수 있다. 이 어노테이션은 서비스 레이어의 저장/업데이트/삭제 로직에 붙일 수 있다. 조회로직에는 @Transactional(readOnly = true)로 쓸 수 있다.그리고 강좌에서 아래와 같이 말씀하셨다.⚠ 주의CheckedException은 롤백이 일어나지 않는다.하지만 이 점이 궁금해서 알아본 결과 아래와 같다.RuntimeException이든 CheckedException이든 rollback을 할지 말지는 우리가 결정할 수 있다. 바로 @Transactional의 rollbackFor이라는 옵션을 통해서다. 다만, 기본적으로는 CheckedExcpetion은 rollback을 하지 않고 RuntimeExcpetion은 rollback을 해준다. 이점을 명심하자.영속성 컨텍스트는 테이블과 매핑된 Entity 객체를 관리/보관하는 역할을 한다. 즉, 쉽게 말해서 스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.영속성 컨텍스트에는 마치 초능력자처럼 능력을 몇가지 가지고 있다.변경감지(Dirty Check): 영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다. 그래서 이전에 업데이트 로직에서 마지막에 save로직으로 저장을 했는데 @Transactional 어노테이션이 붙으면 아래와 같이 작성이 가능하다.@Transactional public void updateUser(UserUpdateRequest request) { User user = this.userRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new); user.updateName(request.getName()); }쓰기 지연: DB의 INSERT / UPDATE / DELETE SQL을 바로 날리는 것이 아니라, 트랜잭션이 commit될 때 모아서 한 번만 날린다. 이런 기능이 없다면 save 메서드가 3개가 있을 때 insert 쿼리를 일일이 3번 날리는게 아니라 일단 영속성 컨텍스트가 기억하고 한번에 날려준다.1차 캐싱: 똑같은 객체를 조회하는 로직이 있을 때 조회하는 만큼 일일이 조회쿼리를 날려주는게 아니라 처음에 영속성 컨텍스트가 해당 객체를 캐싱하고 다음 같은 객체 조회를 할때 이를 기억하고 한번의 쿼리만 날라간다.Day9. 조금 더 복잡한 기능을 API로 구성하기이번에는 실전으로 책을 생성하고 대출하고 반납하는 기능을 만들었다. 여기서 이제까지 배운 개념들을 적용했다. 물론 코치님께서 알려주시긴 하지만 나는 강좌를 멈추고 내 스스로 코드를 작성해본 다음에 코치님 설명과 비교를 했다. 여기서 대출기능을 할 때 나는 연관관계를 매핑해서 처리를 할려고 했지만 코치님께서는 일단은 대출관련 테이블을 만든 뒤에 그에 대한 엔티티, repository, service를 만드셨다. 그래서 나는 여기서 조금 깨달은 부분이 있었다. 무조건 연관관계를 짓는게 아니라 만약 실무에서 연관관계를 짓는게 불가하다면 이런 경우로 풀수도 있다는 사실을 깨달았다.미션 해결과정Day6이번 미션은 과제4에서 만들었던 Fruit관련 API를 3단분리하고, FruitRepository를 인터페이스로 만들고 해당 인터페이스를 구현한 FruitMemoryRepository와 FruitMysqlRepository를 만들어 @Primary 어노테이션을 통해 repository의 역할을 바꿔가며 해보는 과제였다.나는 먼저 기존 컨트롤러에 모여있는 비즈니스 로직을 저장, 수정, 조회기능은 repository레이어에 그리고 예외처리관련은 서비스 레이어에 분리하였다. 그리고 컨트롤러는 순수 HTTP 통신 관련만 구현해두었다. 그런 다음에 DB로직 관련 repository 클래스를 FruitMysqlRepository로 변경하고 FruitRepository 인터페이스를 생성 후 구현하고 나머지 FruitMemoryRepository를 생성하여 메모리 관련 로직을 작성해두었다. 다음 각각 클래스에 @Primary 어노테이션을 붙이고 각각 메서드에 Logback을 이용해 로그를 찍으면서 확인을 했다. 이를 통해 학습의 효과를 느낄 수 있었다. 학(강의 시청)으로 개념을 배우고 습(실습을 통한 체득)으로 체득을 함으로 좀 더 익숙하게 쓸 수 있게 되는 계기가 된 것 같다. 자세한 것은 아래 블로그를 통해 보시면 자세한 과정을 알 수 있다.https://inf.run/3EWwN피드백피드백 전까지 테스트코드도 나름 잘 작성하고 validation부분까지 잘 작성해서 나름 이번은 성공적이라고 느꼈다. 하지만 코치님께서 피드백을 주셨다. 서비스의 비즈니스 로직이 복잡할 때는 다른 내부 서비스 로직을 호출하기도 하지만 DTO와 도메인에 계산로직과 비즈니스 로직을 나눠서 넣기도 한다고 하였다. 내 코드를 보니 뭔가 DTO에도 처리할만한 부분이 있지 않았을 까 반성하게 되는 계기 된 것같다. Day7이번 미션은 과제6에서 만든 기능들을 JPA로 변경하는 부분이 있었다. 또한 다양한 쿼리메서드를 연습해볼 기회로 문제를 몇개 주셨다. 먼저 문제1에서 Spring Data JPA로 바꾸는 것은 그리 어려운 작업은 아니었던 것 같았다. 단순히 repository 인터페이스를 JpaRepository에 상속받고 엔티티를 연습했던것처럼 바꿔주면 되기 때문이다. 하지만 나는 여기서 더 나아갔다. 집계함수 부분을 Spring Data JPA로 변경할 때 좀 고민이 있었다. 집계함수를 제공해주는 쿼리메서드는 없었던 것 같았다. 그래서 집계함수를 이용하지 않고 select 쿼리를 이용해서 List<엔티티> 타입으로 반환해야하나 생각을 하던 결과 문듯 아이디어가 떠올랐다. 바로 @Query와 jpql이다. 그래서 나는 여기서 @Query 어노테이션을 이용하여 JPQL로 쿼리를 작성해보았다. 그리고 반환을 엔티티타입이 아닌 DTO로 반환해보았다. 그러니 서비스 레이어도 간단해졌다.그렇게 쉽게 바꿔서 문제1은 가볍게 해결했다. 그리고 문제2를 풀면서 다양한 쿼리 메서드를 테스트할 수 있었다. 먼저 count~로 시작하는 메서드를 만들어 count 쿼리를 작성할 수 있었다.마지막 문제3도 GreaterThanEqual, LessThanEqual의 조건을 이용하는 쿼리메서드를 작성하는 거였다.이번 미션도 테스트를 작성해보고 이번엔 진짜 잘했다고 느꼈다. JPQL을 통해 DTO로 직접 반환하는 부분까지 완벽했다고 자만했다. 하지만 피드백을 듣고 아직 많이 부족하다는 것을 느꼈다.피드백마지막 문제의 parameter GTE, LTE 부분을 enum 클래스로 관리할 수 있다고 하셨다. 이 말을 본 순간 "앗~"이라는 말이 절로 나왔다. enum을 아예 몰랐던것도 아니고 조금 반성하게 된 계기였다. 금방 과제가 끝났다고 끝까지 고민을 못해본 결과였다. Day8 ~Day8부터 미니프로젝트 과제이다. 아직은 미니프로젝트 미완성이므로, 해당 프로젝트가 단계별 완성시, 새로운 포스트로 남기겠다.회고오늘까지 나는 학습을 하면서 많은 것을 깨달았다. 물론 지식도 지식이지만 하나의 문제를 풀 때 수학처럼 다양한 방식으로 푸는 방법에 대해 깨달음을 얼었다. 무조건 좋은 방법으로 풀 수 없는 경우 우회를 해서 푸는 방식으로도 할 수 있다는 것을 알고 나 자신 스스로 반성하는 부분을 가졌다. 마음속으로 "이렇게 해서 우수러너가 될 수 있으며 원하는 기업으로 이직을 할 수 있을까?"라는 반성의 시간을 가지고 다른 열심히 하시는 러너분들을 생각해 더욱 자극을 받아서 우수러너가 되기까지 노력해보기로 생각을 하였다. 

백엔드인프런워밍업스터디클럽백엔드

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - JPA 테스트 (Day7)

미션어느덧 스터디 클럽 7일차가 되었다. 오늘은 이전에 JDBC를 이용한 서비스 로직을 JPA로 변경해보는 실습을 가졌다.그럼 이제 미션을 수행해보자.진도표 7일차와 연결됩니다우리는 JPA라는 개념을 배우고 유저 테이블에 JPA를 적용해 보았습니다. 몇 가지 문제를 통해 JPA를 연습해 봅시다! 🔥문제 1문제1의 요구사항은 과제6에서 만들었던 기능들을 JPA로 구현하라고 하셨다. 따라서 강의에서 코치님께서 보여주신 과정으로 진행해보려고 한다. step0. application.yml jpa 설정 추가spring: datasource: url: "jdbc:mysql://localhost/fruit" username: "root" password: "" driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true dialect: org.hibernate.dialect.MySQL8Dialectstep1. Fruit Entity를 JPA Entity화 하기!package me.sungbin.entity.fruit; import jakarta.persistence.*; import org.hibernate.annotations.Comment; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.entity.fruit * @fileName : Fruit * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Entity public class Fruit { @Id @Comment("Fruit 테이블의 Primary key") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; @Column(name = "warehousingDate", nullable = false) private LocalDate warehousingDate; @Column(nullable = false) private Long price; @Column(nullable = false) private boolean isSold = false; public Fruit() { } public Fruit(String name, LocalDate warehousingDate, Long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public Fruit(String name, LocalDate warehousingDate, Long price, boolean isSold) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public Fruit(Long id, String name, LocalDate warehousingDate, Long price, boolean isSold) { this.id = id; this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public Long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public Long getPrice() { return price; } public boolean isSold() { return isSold; } public void updateSoldInfo(boolean isSold) { this.isSold = isSold; } } Entity 어노테이션을 붙여서 엔티티로 만들고 기본 primary key와 auto_increment를 설정한다.그 외에, 컬럼들의 null 여부도 설정하였다.또한 warehousingDate의 필드에 컬럼 이름을 다시 넣은 이유는 mysql 쿼리가 동작할 때 warehousingDate로 컬럼이 인식이 안되고 warehousing_date로 인식을 하기 때문에 name 필드를 넣었다.step2. JpaRepository를 상속받은 인터페이스 생성기존의 FruitRepository 인터페이스를 FruitJdbcRepository로 파일명을 변경한 후, FruitRepository 클래스를 만든다.FruitJPARepository로 만들어도 상관은 없지만, 통상적으로 편하게 FruitRepository로 해주는 것이다.package me.sungbin.repository; import me.sungbin.entity.fruit.Fruit; import org.springframework.data.jpa.repository.JpaRepository; /** * @author : rovert * @packageName : me.sungbin.repository * @fileName : FruitRepository * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ public interface FruitRepository extends JpaRepository<Fruit, Long> { } step3. DTO 코드 변경package me.sungbin.dto.fruit.request; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import me.sungbin.entity.fruit.Fruit; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.request * @fileName : SaveFruitInfoRequestDto * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ public class SaveFruitInfoRequestDto { @NotBlank(message = "과일 이름이 공란일 수 없습니다.") @NotNull(message = "과일 이름이 null일 수는 없습니다.") private String name; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate warehousingDate; @Min(value = 0, message = "가격이 음수가 될 수는 없습니다.") private long price; public SaveFruitInfoRequestDto(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public Fruit toEntity() { return new Fruit(name, warehousingDate, price, false); } }과일 정보를 저장할 때, toEntity() 메서드에 Fruit 생성자의 마지막애 false를 추가하였다. 왜냐하면 DTO에서 엔티티로 변경을 할 때 판매유무를 확실히 미판매로 해두려고 하기 때문이다. step4. 서비스 로직 수정기존 FruitService를 FruitJdbcService로 변경하고 FruitService를 새로 만든다.일단 먼저 서비스 코드를 전체 보여주겠다. package me.sungbin.service; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.FruitRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author : rovert * @packageName : me.sungbin.service * @fileName : FruitService * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Service @Transactional(readOnly = true) public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } /** * 문제 1번 (과제6 문제 1) * @param requestDto */ @Transactional public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) { Fruit fruit = requestDto.toEntity(); this.fruitRepository.save(fruit); } /** * 문제 1번 (과제 6 문제 2) * @param requestDto */ @Transactional public void updateFruitInfo(UpdateFruitRequestDto requestDto) { Fruit fruit = this.fruitRepository.findById(requestDto.getId()).orElseThrow(IllegalArgumentException::new); fruit.updateSoldInfo(true); } /** * 문제 1번 (과제 6 문제 3) * @param name * @return */ public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitSalesInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } }위의 코드를 보면 나머지는 대략 이해는 되는데 getFruitInfo의 getFruitSalesInfo 메서드는 처음 볼 것이다. 우리가 배운 범위에서getFruitSalesInfo는 data jpa에서 기본으로 제공해주는 함수는 아니기 때문이다. 바로 이것은 repository에 @Query 어노테이션과 사용자 정의 JPQL 쿼리를 사용하였다. 그 이유는 집계함수로 인하여 불기파 사용하였다. 아래는 수정된 repository 코드이다.package me.sungbin.repository; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; /** * @author : rovert * @packageName : me.sungbin.repository * @fileName : FruitRepository * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Transactional(readOnly = true) public interface FruitRepository extends JpaRepository<Fruit, Long> { @Query("SELECT new me.sungbin.dto.fruit.response.GetFruitResponseDto(SUM(case when f.isSold = true then f.price else 0 end), SUM(case when f.isSold = false then f.price else 0 end)) FROM Fruit f WHERE f.name = :name") GetFruitResponseDto getFruitSalesInfo(@Param("name") String name); // 과제 6 문제 3번 } step5. 테스트이제 위의 변경된 코드를 가지고 테스트를 해서 검증해보자. fruit 테이블을 조회하면 아래처럼 비어있다고 하자.생성 테스트그리고 몇개의 데이터를 만들고 테이블에 잘 insert 되었는지 확인해보았다.수정합산 조회현재 데이터의 테이블이 아래와 같다고 하자.그러면 테스트 해보자.step6. 테스트 코드이전과 같은 아래의 테스트코드로 실행 해보았다.package me.sungbin.controller.fruit; import com.fasterxml.jackson.databind.ObjectMapper; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitControllerTest * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Transactional @SpringBootTest @AutoConfigureMockMvc class FruitControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test @DisplayName("문제 1번 통합 테스트 - 실패 (가격이 음수거나 과일 이름이 공란)") void question1_test_fail_caused_by_price_is_minus_or_fruit_name_is_empty() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("", LocalDate.of(2024, 1, 1), -1000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("문제 1번 통합 테스트 - 성공") void question1_test_success() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("파인애플", LocalDate.of(2024, 2, 2), 20000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 2번 통합 테스트 - 성공") void question2_test_success() throws Exception { UpdateFruitRequestDto requestDto = new UpdateFruitRequestDto(1); this.mockMvc.perform(put("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 3번 통합 테스트 - 성공") void question3_test_success() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/stat") .param("name", "사과")) .andDo(print()) .andExpect(status().isOk()); } }문제 2요구사항은 우리가게를 거쳐갔던 과일의 개수를 구하는 문제이다. 여기서 의도는 거쳐갔던이므로 판매가 되었던 것중의 과일의 이름을 카운트해보겠다. step0. 응답 DTO 생성 package me.sungbin.dto.fruit.response; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.response * @fileName : CountFruitNameResponseDto * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ public class CountFruitNameResponseDto { private final long count; public CountFruitNameResponseDto(long count) { this.count = count; } public long getCount() { return count; } }step1. 레파지토리에 jpa 메서드 선언package me.sungbin.repository; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; /** * @author : rovert * @packageName : me.sungbin.repository * @fileName : FruitRepository * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Transactional(readOnly = true) public interface FruitRepository extends JpaRepository<Fruit, Long> { @Query("SELECT new me.sungbin.dto.fruit.response.GetFruitResponseDto(SUM(case when f.isSold = true then f.price else 0 end), SUM(case when f.isSold = false then f.price else 0 end)) FROM Fruit f WHERE f.name = :name") GetFruitResponseDto getFruitSalesInfo(@Param("name") String name); // 과제 6 문제 3번 long countByNameAndIsSoldIsTrue(String name); } countByNameAndIsSoldIsTrue 메서드가 방금 작성한 코드이다.step2. 서비스 코드 작성package me.sungbin.service; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.CountFruitNameResponseDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.FruitRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @author : rovert * @packageName : me.sungbin.service * @fileName : FruitService * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Service @Transactional(readOnly = true) public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } /** * 문제 1번 (과제6 문제 1) * @param requestDto */ @Transactional public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) { Fruit fruit = requestDto.toEntity(); this.fruitRepository.save(fruit); } /** * 문제 1번 (과제 6 문제 2) * @param requestDto */ @Transactional public void updateFruitInfo(UpdateFruitRequestDto requestDto) { Fruit fruit = this.fruitRepository.findById(requestDto.getId()).orElseThrow(IllegalArgumentException::new); fruit.updateSoldInfo(true); this.fruitRepository.save(fruit); } /** * 문제 1번 (과제 6 문제 3) * @param name * @return */ public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitSalesInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } /** * 문제 2번 * @param name * @return */ public CountFruitNameResponseDto countFruitName(String name) { long count = this.fruitRepository.countByNameAndIsSoldIsTrue(name); return new CountFruitNameResponseDto(count); } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } } countFruitName 메서드가 내가 방금 작성한 메서드이다.step3. 컨트롤러 코드 작성package me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.CountFruitNameResponseDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.service.FruitService; import org.springframework.web.bind.annotation.*; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping public void saveFruitInfo(@RequestBody @Valid SaveFruitInfoRequestDto requestDto) { this.fruitService.saveFruitInfo(requestDto); } @PutMapping public void updateFruitInfo(@RequestBody UpdateFruitRequestDto requestDto) { this.fruitService.updateFruitInfo(requestDto); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } @GetMapping("/count") public CountFruitNameResponseDto countFruitName(@RequestParam String name) { return this.fruitService.countFruitName(name); } }countFruitName이 방금 작성한 컨트롤러 코드이다.step4. 테스트아래와 같이 DB 데이터가 있다고 하자. 그리고 포스트맨으로 테스트해보자.step5. 테스트 코드@Test @DisplayName("과제 7번 문제 2번 통합 테스트 - 실패 (파라미터 존재 X)") void lesson7_question2_fail_test_caused_by_not_exists_request_parameter() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/count")) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("과제 7번 문제 2번 통합 테스트 - 성공") void lesson7_question2_test_success() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/count") .param("name", "사과")) .andDo(print()) .andExpect(status().isOk()); }이번에는 실패 케이스와 성공 케이스 2개를 작성했으며 결과는 아래와 같다.문제 3문제 3은 아직 판매되지 않은 과일 정보 리스트 중에 특정 금액 이상 혹은 이하의 과일 목록을 받는 것이다. step0. 응답 DTO 생성package me.sungbin.dto.fruit.response; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.response * @fileName : FruitResponseDto * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ public class FruitResponseDto { private final String name; private final long price; private final LocalDate warehousingDate; public FruitResponseDto(String name, long price, LocalDate warehousingDate) { this.name = name; this.price = price; this.warehousingDate = warehousingDate; } public String getName() { return name; } public long getPrice() { return price; } public LocalDate getWarehousingDate() { return warehousingDate; } }step1. 요청 DTO 생성package me.sungbin.dto.fruit.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.request * @fileName : FruitRequestDto * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ public class FruitRequestDto { @NotBlank(message = "option은 공란일 수 없습니다.") @NotNull(message = "option은 반드시 있어야 합니다.") private final String option; private final long price; public FruitRequestDto(String option, long price) { this.option = option; this.price = price; } public String getOption() { return option; } public long getPrice() { return price; } }요청 DTO에는 spring starter validation을 추가하여 예외 처리도 해두었다.step2. Repository의 쿼리 메서드 추가쿼리 메서드 대신에 @Query를 사용하여 DTO로 반환시킬 수 있다. 과제 7의 1번처럼 말이다. 하지만 본 과제의 취지와 맞지 않은 것 같기에 과제7의 1번(문제 3번)은 @Query로 사용했으니 이번엔 안 사용하고 해보겠다.package me.sungbin.repository; import me.sungbin.dto.fruit.response.FruitResponseDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.repository * @fileName : FruitRepository * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Transactional(readOnly = true) public interface FruitRepository extends JpaRepository<Fruit, Long> { @Query("SELECT new me.sungbin.dto.fruit.response.GetFruitResponseDto(SUM(case when f.isSold = true then f.price else 0 end), SUM(case when f.isSold = false then f.price else 0 end)) FROM Fruit f WHERE f.name = :name") GetFruitResponseDto getFruitSalesInfo(@Param("name") String name); // 과제 6 문제 3번 long countByNameAndIsSoldIsTrue(String name); List<Fruit> findAllByPriceGreaterThanEqualAndIsSoldIsFalse(long price); List<Fruit> findAllByPriceLessThanEqualAndIsSoldIsFalse(long price); }step3. 서비스 코드 추가package me.sungbin.service; import me.sungbin.dto.fruit.request.FruitRequestDto; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.CountFruitNameResponseDto; import me.sungbin.dto.fruit.response.FruitResponseDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.FruitRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; /** * @author : rovert * @packageName : me.sungbin.service * @fileName : FruitService * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Service @Transactional(readOnly = true) public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } /** * 문제 1번 (과제6 문제 1) * @param requestDto */ @Transactional public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) { Fruit fruit = requestDto.toEntity(); this.fruitRepository.save(fruit); } /** * 문제 1번 (과제 6 문제 2) * @param requestDto */ @Transactional public void updateFruitInfo(UpdateFruitRequestDto requestDto) { Fruit fruit = this.fruitRepository.findById(requestDto.getId()).orElseThrow(IllegalArgumentException::new); fruit.updateSoldInfo(true); this.fruitRepository.save(fruit); } /** * 문제 1번 (과제 6 문제 3) * @param name * @return */ public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitSalesInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } /** * 문제 2번 * @param name * @return */ public CountFruitNameResponseDto countFruitName(String name) { long count = this.fruitRepository.countByNameAndIsSoldIsTrue(name); return new CountFruitNameResponseDto(count); } public List<FruitResponseDto> findSoldFruitListOfPrice(FruitRequestDto requestDto) { if (Objects.equals(requestDto.getOption(), "GTE")) { return this.fruitRepository.findAllByPriceGreaterThanEqualAndIsSoldIsFalse(requestDto.getPrice()) .stream().map(fruit -> new FruitResponseDto(fruit.getName(), fruit.getPrice(), fruit.getWarehousingDate())) .collect(Collectors.toList()); } else if (Objects.equals(requestDto.getOption(), "LTE")) { return this.fruitRepository.findAllByPriceLessThanEqualAndIsSoldIsFalse(requestDto.getPrice()) .stream().map(fruit -> new FruitResponseDto(fruit.getName(), fruit.getPrice(), fruit.getWarehousingDate())) .collect(Collectors.toList()); } else { throw new IllegalArgumentException("옵션은 GTE 혹은 LTE이여야 합니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } } 옵션이 올바르지 못할 경우 런 타임 에러 발생step4. 컨트롤러 코드 추가package me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.FruitRequestDto; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.CountFruitNameResponseDto; import me.sungbin.dto.fruit.response.FruitResponseDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.service.FruitService; import org.springframework.web.bind.annotation.*; import java.util.List; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping public void saveFruitInfo(@RequestBody @Valid SaveFruitInfoRequestDto requestDto) { this.fruitService.saveFruitInfo(requestDto); } @PutMapping public void updateFruitInfo(@RequestBody UpdateFruitRequestDto requestDto) { this.fruitService.updateFruitInfo(requestDto); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } @GetMapping("/count") public CountFruitNameResponseDto countFruitName(@RequestParam String name) { return this.fruitService.countFruitName(name); } @GetMapping("/list") public List<FruitResponseDto> findSoldFruitListOfPrice(FruitRequestDto requestDto) { return this.fruitService.findSoldFruitListOfPrice(requestDto); } }  step5. 테스트현재 DB 데이터는 아래와 같다.그럴때 테스트를 해보겠다.GTELTEstep6. 테스트 코드이제 테스트 코드를 작성해보자. 아래는 전체 테스트 코드다!package me.sungbin.controller.fruit; import com.fasterxml.jackson.databind.ObjectMapper; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitControllerTest * @date : 2/26/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/26/24 rovert 최초 생성 */ @Transactional @SpringBootTest @AutoConfigureMockMvc class FruitControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test @DisplayName("문제 1번 통합 테스트 - 실패 (가격이 음수거나 과일 이름이 공란)") void question1_test_fail_caused_by_price_is_minus_or_fruit_name_is_empty() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("", LocalDate.of(2024, 1, 1), -1000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("문제 1번 통합 테스트 - 성공") void question1_test_success() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("파인애플", LocalDate.of(2024, 2, 2), 20000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 2번 통합 테스트 - 성공") void question2_test_success() throws Exception { UpdateFruitRequestDto requestDto = new UpdateFruitRequestDto(1); this.mockMvc.perform(put("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 3번 통합 테스트 - 성공") void question3_test_success() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/stat") .param("name", "사과")) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("과제 7번 문제 2번 통합 테스트 - 실패 (파라미터 존재 X)") void lesson7_question2_fail_test_caused_by_not_exists_request_parameter() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/count")) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("과제 7번 문제 2번 통합 테스트 - 성공") void lesson7_question2_test_success() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/count") .param("name", "사과")) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("과제 7번 문제 2번 통합 테스트 - 실패 (파라미터 존재 X)") void lesson7_question3_fail_test_caused_by_not_exists_request_parameter() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/list")) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("과제 7번 문제 3번 통합 테스트 - 성공") void lesson7_question3_test_success() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/list") .param("option", "GTE") .param("price", "3000")) .andDo(print()) .andExpect(status().isOk()); } }회고JPA의 편리함을 많이 깨닫는 하루였다. 하지만 쿼리메서드를 작성할 때 조건이 엄청 길어지는 것이 내가 보기엔 단점 같다.아래의 짤이 있다. JPA도 이런 취급을 받을 날이 안 왔으면 하는 마음에서 글을 마무리하려 한다. 📚 참고https://m.blog.naver.com/PostView.naver?blogId=190208&logNo=222145961004&categoryNo=51&proxyReferer=

백엔드인프런워밍업스터디클럽JPA

학생

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

발자국이번주 강의 범위는 따라하며 배우는 자바스크립트 A-Z(섹션 1~8) 까지였다.워낙에 기록을 안하는 성격이라 어떤 식으로 해야 할지 감이 안잡히지만 이제부터라도 습관을 들이는 편이 좋겠다.강의 요약이번주 강의에는 정말 많은 내용이 담겨 있었지만,이전에 혼자 책으로 공부할 때 한 번 보고 이해가 되지 않았던 부분,중요하다고 생각하는 부분,혹은 아예 처음 듣는 내용을 위주로 요약해보았다.그 목록은 아래와 같다.호이스팅HTML DOM this자바스크립트 비동기 처리 과정IIFECurryingOOP비동기Iterator & Generator자바스크립트 디자인 패턴요약을 한다고 하긴 했는데 요약이 아니라 공부노트같긴 하다. 다음 회고때는 중요한 내용 두 개 정도로 줄일까 한다.호이스팅(hoisting)변수 선언이 스코프 내의 가장 위로 끌어올려지는 것.var, let, const 모두 호이스팅된다. 그런데 방식이 다르다.var의 호이스팅console.log(a) var a = 1출력 결과: undefinedwhy? var의 경우 선언(a = undefined) -> 할당(a = 1) 됨.let, const의 호이스팅console.log(a) let a = 1결과: 에러 뜸.why? let, const의 경우 선언 -> TDZ -> 할당(a = 1) 됨.따라서 변수 선언이 스코프의 맨 위로 올라간다 해도 출력 불가. TDZ에 걸림. HTML DOMDOM(Document Object Model): 웹 페이지를 이루는 요소들을 tree구조로 만든 객체 모델.웹 페이지 빌드 과정(CRP: Critical Rendering Path)html을 DOM으로, CSS를 CSSOM으로 만든다.DOM과 CSSOM을 결합(Render Tree 생성)Layout(페이지에 표시되는 각 요소의 크기 및 위치 계산)Paint(실제 화면에 그리기)Render Tree 생성, Layout, Paint단계는 비용이 많이 든다.이 비용을 줄이기 위해 React에서는 가상 DOM을 사용해 성능을 높임. DOM Event event의 3단계 흐름: Capturing -> Target -> Bubblingevent Capturing: 이벤트가 위에서 아래로 전달되는 것event Bubbling: 가장 깊게 중첩된 요소에 이벤트가 발생했을 때 이벤트가 위로 전달되는 것event.stopPropagation(): 중첩 중지.Event Delegation(이벤트 위임)하위 요소의 이벤트를 상위 요소에 위임하는 것.하위 요소의 이벤트를 상위에서 제어. this메소드에서 this는 해당 객체를 참조.함수에서 this는 window 객체를 참조.constructor에서 this는 빈 객체를 참조.여기서 함수와 메소드의 차이가 헷갈려 알아보니, 다음과 같았다.메소드는 함수의 일종이며, 객체의 속성이 함수인 것을 메소드라고 한다.참고: https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Functionslexical this: 화살표함수(ES6)에서 this는 항상 상위스코프의 this를 가리키게 된다.bins, call, apply함수에서 this를 쓸 때 window객체 말고 다른 객체를 참조하도록 하는 방법들call: function.call(객체, 인수1, 인수2, ...)apply: function.apply(객체, [인수1, 인수2, ...])bind: const bindFunc = function.bind(객체); bindFunc(인수) // 이 때 호출. 자바스크립트의 비동기 처리 과정event loop자바스크립트는 동기 언어. 따라서 비동기를 사용하려면 브라우저 등의 도움을 받아야 한다.브라우저 내부: 자바스크립트 엔진 + Web APIs + Callback Queue + Event Loop자바스크립트 엔진: 메모리 힙 + Call Stack메모리 힙: 변수 저장 창고Call Stack: 함수 호출 시 함수가 쌓이는 곳setTimeout과 같은 비동기 기능을 처리하는 과정 (브라우저가 처리해줌)Call Stack에 setTimeout 쌓임Web APIs에게로 setTimeout이 이동 후 setTimeout의 지정한 시간만큼 대기Callback Queue에 함수가 보내지고 대기Event Loop는 Call Stack이 비면 Callback Queue에서 먼저 온 순서대로 Call Stack에 넣어줌  IIFE(즉시 호출 함수 표현식)정의가 되자마자 즉시 실행되는 자바스크립트 함수. 주된 목적: 변수 전역 선언 회피, 다른 변수의 내부 접근 막기( // 소괄호 1: 전역선언 막고 변수접근 막기 function() {} )() // 소괄호 2: 즉시 실행 함수 생성함수 이름이 없으려면이 함수를 할당받을 변수를 지정해야 함이 함수를 즉시호출해야함(IIFE) Currying커링: f(a,b,c) => f(a)(b)(c)로 변환하는 기술. 다른 언어에도 존재함.// 매개변수가 몇개든 함수를 currying해주는 함수 function curry(func) { return function curried(...args) { if(args.length >= func.length) { return func.apply(this, args); } else { return function (...args2) { return curried.apply(this, args.concat(args2)); } } } } OOP(객체 지향 프로그래밍)특징자료 추상화(Abstraction): 필요한 정보만 표현 -> 간결상속(Inheritance): 새 클래스가 기존 클래스의 기능 가져와 사용 -> 효율다형성(Polymorphism): Overriding(재정의) 사용. 인스턴스에 따라 같은 동작에 다른 결과물.코드 재사용 가능한 이점캡슐화(Encapsulation): 클래스 안에 묶어 보호 및 쉬운 관리상속부모 클래스를 자식 클래스에서 확장.extends 키워드 사용.super(): 자식 클래스에서 부모 클래스의 생성자나 메소드 호출할 때 사용. 비동기병렬로 작업. 순서를 기다리지 않음.비동기 요청이 여러개이고 한 요청이 다른 요청의 결과에 의존할때callback함수(es5): 특정 함수에 매개변수로 전달된 함수.promise(es6): 비동기처리 결과 성공, 실패 각각 resolve, reject. .then()으로 체이닝Promise.then(resolve값).catch(reject값).finally(성공실패무관)async/await(es7): 비동기식 코드를 동기식처럼 보이게 작성.async로 함수를 감싸고 그 안에서 await를 이용해 각 요청을 대기시킴동기식 코드에서 쓰는 try catch구문을 사용 가능 Iterator & Generatoriteratornext()를 호출해서 value, done 두 개의 속성을 가지는 객체를 반환하는 객체.[Symbol.iterator]()를 이용하면 반복가능한값을 반복기로 생성 가능.generatorGenerator Function은 사용자의 요구에 따라 다른 시간 간격으로 여러 값을 반환 가능.yield: 제너레이터함수의 실행을 일시적으로 정지시킴.function* 함수이름()제너레이터: 제너레이터 함수의 반환.제너레이터.next()로 사용.generator도 value, done 속성이 담긴 객체를 반환.자바스크립트 디자인 패턴디자인 패턴: 공식화된 프로그램/시스템 디자인 문제해결 모범 사례.장점최고의 솔루션: 검증됨.재사용성: 여러문제에 적용가능.풍부한 표현력향상된 의사 소통필요없는 코드 리팩토링: 검증됨코드베이스 크기 감소: 공간 보존Singleton Pattern:인스턴스를 하나의 객체로 제한.Factory Pattern: 특수함수인 팩토리함수를 사용해 비슷한 객체 많이 만들수있음.=> 비슷한 객체를 반복적으로 생성하야 하는 경우 사용. Mediator Pattern(중재자 패턴): 객체 그룹에 대한 중앙 권한 제공.Observer Pattern: observer를 이용해 객체를 관찰.Module Pattern: 더 작은 것으로 분할.export를 이용. 미션 해결 과정(과제 총합본은 따로 작성하고 있다.) https://www.inflearn.com/blogs/6758미션1 음식 메뉴 앱포인트: 각 카테고리의 버튼을 누를 때마다 메뉴들을 보여주는 공간을 비우고 해당 카테고리의 메뉴를 채움스타일: 우선은 아는 것이 별로 없고 주어진 것부터 제대로 공부하고 싶어서 과제 예시와 최대한 비슷하게 만들었다. 추가: 미디어 쿼리를 이용해 개발자 도구가 켜진 상태의 화면크기부터는 화면에 메뉴를 1열로 표시시간 많이 든 부분: CSS, 이미지 처리(선정 및 다운로드, 크기조정) 아직 HTML/CSS 공부가 많이 필요하다는 것을 깨달았다.  사진은 pixabay의 무료 이미지를 이용했으나, 다른 분의 글을 보고 음식 메뉴에 API를 이용하는 좋은 방법이 있다는 것을 깨달았다. 그런 훌륭한 문물이 있다는 것을 이제야 상기하다니! 직접 음식 메뉴를 선정하고 설명을 지어내는 것에 신경을 쓰느라 음식 API를 이용한다는 생각을 하지 못했다. 앞으로 다루어야 할 이미지가 많아질 경우 API를 사용하자.  미션2-가위바위보 앱포인트: Math.random을 이용해 컴퓨터의 가위바위보를 구현. 판마다 게임보드에 스코어 표시스타일: background에 그라데이션 효과를 적용함. 승부 결과마다 다른 색의 글귀를 표시 시간 많이 든 부분:만들 때 헷갈렸던 부분은 재도전 버튼을 눌렀을 때 기존에 써있던 플레이어와 컴퓨터의 점수를 삭제하고 새로 표시하는 부분이었다. 결국 재도전 버튼에 붙인 retry함수에서, gameDisplay에서 생성한 class를 querySelector로 찾아가 내용물을 비우고 classList에서 class 이름을 삭제하고 각 점수를 scoreboard에서 removeChild로 지웠다. 이는 gameDisplay가 실행되면 다시 생성된다. 미션3-퀴즈 앱포인트: 버튼을 누를 때마다 next버튼 생성 및 옵션 버튼 비활성화스타일: CSS의 grid 이용해 옵션 버튼 배치. position시간 많이 든 부분: CSS. 미션4-책 리스트 나열 앱포인트: 입력 후 제출 버튼을 누르면 input value를 미리 생성해둔 ul 내부에 li형태로 추가. 경고문은 setTimeout을 이용해 3초 뒤 사라지게 함스타일: 책이 추가/삭제되거나, 입력란을 빈칸으로 두고 제출했을 때의 경고문에 따라 색 다르게 표시얘는 다음주에 CSS를 공부해서 정렬을 보기 좋게 만들어줄 것이다.(거의 기능만 구현된 못생긴 화면)조만간 수정할 부분CSS 추가알림때문에 화면이 아래로 밀리는 문제 고치기 미션5-GithubFinder 앱포인트: Github API를 사용해 데이터 가져오기스타일: 우선 단순하게(아직 미해결. API 사용법 공부 후에 완성 예정)이친구는 fetch와 RESTful API 사용하는 법을 공부해야 하기 때문에 아직 해결하지 못했다. 다음 회고 때 해결 과정이 들어갈 것이다.미션6-비밀번호 생성 앱포인트: 선택된 체크박스의 값을 넣어 length란에 입력한 숫자값을 길이로 하는 문자열을 랜덤생성해야 한다.5~70의 숫자 조건에 length가 해당하지 않으면 alert를 실행시켰다.아무런 체크박스를 선택하지 않았을 경우도 alert를 실행시켰다.생성한 비밀번호를 복사하는 copy 버튼을 눌렀을 때 클립보드에 복사하는 코드navigator.clipboard .writeText(password.value) .then(() => { alert("successfully copied"); }) .catch(() => { alert("something went wrong"); });원래는 execCommand를 썼는데 deprecated되었다고 한다.then부분은, alert를 넣을 경우 then을 쓰지 않으면 복사가 되지 않아서 stack overflow사이트에서 가져온 코드이다. https://stackoverflow.com/questions/69438702/why-does-navigator-clipboard-writetext-not-copy-text-to-clipboard-if-it-is-pro문자열, 숫자, 특수문자를 랜덤으로 섞어 조합하는 것은 까다로웠지만 Math.random과 배열을 이용해 구현했다.체크박스 중 체크된 것의 value를, 생성 버튼을 클릭할때마다 배열에 넣고, 체크가 안되어있으면 배열에서 삭제하는 코드는 거의 줄글처럼 조건이 많이 작성되었다. 굉장한 하드코딩이 되었다...회고작년 말에 진로를 FE개발쪽으로 정하고 독학을 시작하였다.FE부트캠프는 참여한 적이 없었고, 따라서 미션 하나하나가 도장깨기를 하는 느낌이었다.예상대로 매일 미션 한개씩 하는데에는 시간이 꽤 걸렸다. 그리고 부족한 점을 많이 깨달았다.CSS, API 이용, 클린코드 작성 등 여러 방면에서의 지식 및 구현 경험 부족 미션을 할 때 일단 돌아가게 만들고 머리로 정리는 하지 않는 느낌  뒤로 갈수록 시간 부족 다음주 목표CSS 공부하기REST API 공부하고 미션 마저 해결하기다음주 회고 강의 요약 부분 분량 줄이기

프론트엔드인프런워밍업클럽스터디