~ 2024.5.29
사용자 인터페이스를 만들기 위한 javascript의 라이브러리이다.
interface란 사용자가 웹이나 어플리케이션을 봤을 때 실제로 보이는 영역으로 UserInerface UI를 말한다.
리액트는 상호작용interaction이 많은 웹 앱을 만들기위해 주로 사용되며, 비슷하게 웹 앱을 만드는 다른 Tool인 Vue 혹은 Angular와 많이 비교한다.
react는 라이브러리이고, vue·angular는 프레임워크다.
간단하게, 프레임워크는 어떠한 앱을 만들기 위해 필요한 대부분의 틀을 제공하는 것이고 라이브러리는 특정 기능을 모듈화 한 것이다.
프레임워크는 작성된 소스코드를 호출하고, 소스코드는 기능 구현을 위해 라이브러리를 호출한다. 즉 프레임워크에는 라이브러리가 포함될 수도 있다.
테스트, 상태관리, 페이지이동 등을 위한 여러 라이브러리가 프레임워크 내에 포함될 수 있다.
처음에 리액트는 UserInerface를 만들기 위해 사용된다고 했다. 전에 nodejs를 공부하면서 MVC pattern을 공부했다. 리액트는 여기서 View를 담당하는 것이다.
즉, 눈에 보이는 측면만을 만들기 때문에 프레임워크가 아닌 라이브러리다.
리액트만으로는 UI밖에 만들 수 없기 때문에, 그외의 상태관리나 빌드, 라우팅(페이지이동)은 다른 라이브러리들의 도움을 받아야한다.
react로 project를 생성하고 싶다면 우선 node.js와 npm이 설치되어 있어야 한다.
리액트앱의 런타임은 브라우저여서 node.js와 직접적인 연관은 없지만, 주요 도구인 라이브러리들이 node.js를 사용하기 때문이다.
바벨, 웹펙, 리액트라우터돔 등은 npm의 registry에서 받아와서 설치해야 사용 할 수 있다.
https://www.codenary.co.kr/techstack/detail/reactjs
위 주소를 방문해보면, 현재 리액트를 이용하여 제공되고 있는 서비스들을 볼 수 있다.
국내만이 아니라 해외사이트들도 마찬가지다. 많은 서비스에서 리액트를 사용하고 있다.
추가로 아래에서 언급할 컴포넌트와 가상돔도 리액트의 장점이다.
리액트를 사용하는 이유중의 하나는 컴포넌트일 것이다.
리액트는 여러 컴포넌트를 이용해서 웹 앱을 개발하는 Component Driven Devlopment(CDD)컴포넌트 기반 개발 라이브러리이다.
컴포넌트란 앱을 이루는 최소단위를 말하며, 리액트는 화면을 독립적이고 재사용 가능한 컴포넌트로 구성한다.
하나의 페이지를 리액트로 만든다면, 여러개의 컴포넌트가 모여서 하나의 페이지를 이루게된다.
컴포넌트는 재사용성이 좋다. 만약 위 페이지에서 사용한 스토리 컴포넌트를 다른 페이지에서도 사용하고싶다면, 그냥 그 컴포넌트를 그대로 가져다가 사용하면 된다.
리액트의 주요 특징 중 하나는 가상돔을 사용한다는 것이다.
왜 리액트는 가상돔을 사용하는가를 알려면 우선 브라우저가 렌더링 하는 과정을 알아야한다.
웹 페이지의 빌드 과정
브라우저가 서버에게 페이지에 대한 HTML을 응답받고 화면에 표시할 때는 여러 단계를 거친다. 이미지출처
우선 렌더엔진이 문서를 읽어들이고 그것들을 파싱하여 어떤 내용을 페이지에 렌더링해야할지 결정하고 DOMtree와 CSSOM을 만든다.
브라우저는 DOM과 CSSOM를 결합하고, 화면에 보이는 모든 컨텐츠와 스타일 정보를 모두 포함하는 최종 렌더링 트리 Rendering tree를 출력한다.
이후에 Layout단계에서 브라우저는 페이지에 표시되는 각 요소의 크기와 위치를 계산하고, 실제로 화면에 그린다 Paint.
그런데 만약 어떤 인터렉션에 의해서 DOM에 변화가 발생하면 그때마다 Render tree는 모두 재생성된다.
즉, 모든 요소들의 스타일을 다시 계산하고 재배치하고 다시 그려야한다.
만약 상호작용이 아주 많은 웹이라면 작은 변화로 인해 불필요하게 많은 과정을 거치게 되니 성능과 비용이 매우 낭비된다.
이러한 문제로 인해 나오게 된것이 virtual DOM이다.

가상돔이란 실제 DOM을 메모리에 복사한다고 생각하면 된다.
데이터가 변하면 리액트는 가상돔에 렌더링한다.
이후 이전에 있던 가상돔과 비교하여 바뀐 부분을 찾는 Diffing과정을 거쳐 Diff를 만들고,
바뀐 부분만 실제 DOM에 적용Patch하여 재조정 reconciliation한다.
이렇게 가상돔이 존재하면, 변한 요소가 30개가 될지라도 실제 DOM에는 한번에 묶어서 적용 할 수 있다
빠른것이 꼭 성능이 좋은것은 아니다? with chat GPT
프로그램이 빠르게 실행된다고 해서 그것이 항상 성능이 좋은 것은 아니에요. 성능을 평가할 때에는 다양한 요소를 고려해야 합니다.
정확성: 프로그램이 올바른 결과를 내는가가 가장 중요합니다. 빠르게 실행되더라도 잘못된 결과를 내면 그것은 좋은 성능이 아닙니다.
자원 사용량: 프로그램이 실행되는 동안 사용하는 자원(메모리, CPU 등)을 고려해야 합니다. 빠른 프로그램이 많은 자원을 사용한다면, 그것은 효율적이지 않을 수 있습니다.
확장성: 프로그램이 크기가 커지거나 작업 부하가 증가할 때도 성능이 유지되는가를 고려해야 합니다. 일부 프로그램은 작은 입력에 대해 빠르지만 큰 입력에 대해서는 성능이 급격히 떨어지는 경우가 있습니다.
유지보수성: 코드가 복잡하고 이해하기 어렵다면 나중에 추가적인 개발이나 유지보수가 어려울 수 있습니다. 따라서 코드의 가독성과 유지보수성도 성능을 평가할 때 중요한 요소입니다.
따라서 프로그램의 성능을 평가할 때에는 단순히 실행 시간만을 고려하는 것이 아니라 다양한 요소를 고려해야 합니다.
npx create-react-app 폴더명 , 폴더명에 ./입력하면 현재폴더에 실행리액트앱에는 앞으로 배우게될 webpack, babel과 같은 라이브러리가 필요하다.
그래서 예전의 리액트앱은 그것을 일일이 설치해야 했지만, 위의 명령어를 입력하면 이제 npm이 알아서 설치하고 필요한 설정을 해준다.
웹팩은 오픈소스 자바스크립트 모듈 번들러(..)로, 여러개로 나뉘어져있는 파일들을 하나의 자바스크립트 코드로 압축하고 최적화하는 라이브러리다.

위 이미지처럼 여러 파일들을 압축하여 최적화 할 수 있기 때문에 로딩에 대한 네트워크 비용을 줄일 수 있고, 그렇기 때문에 모듈단위로 개발하여도 무리가없다.
모듈단위로 개발하면 가독성과 유지보수에도 이점이 생긴다.
바벨은 최신 자바스크립트 문법을 지원하지 않는 브라우저를 위해 사용되는 라이브러리로, 새로운 문법을 구형 브라우저에서도 돌릴 수 있게 변환시켜주는 라이브러리다.
// Bable input : ES6(ES2015) arrow function
[1, 2, 3].map(n) => n+1;
// Bable output : ES5 equivalent
[1, 2, 3].map(function(){
return n+1;
})
중요한건 npx create react app을하면 위 친구들을 따로 설치와 설정을 안해줘도 알아서 해준다.
터미널에 명령어를 치고 엔터를 누르면 패키지가 실행되면서 필요한 파일들이 설치된다.
npx?
npx는 노드 패키지의 실행을 도와주는 도구이다.
즉,npx create-react-app이란 create-react-app을 설치하는 것이 아니라 npm registry에 있는 create-react-app을 한번 내 폴더에 실행시켜 주는것이다.
그러면 create-react-app이 필요한 파일들을 내 폴더에 설치한다.
이렇게 동작하는 이유는 정말 맨처음 한번 설치하고나면 create-react-app파일은 필요가 없기 때문이다.
또 실행하면 설치하는 방식으로 동작하면 따로 최신버전을 받을 필요 없이 registry에 있는 최신 버전으로 앱을 생성할 수 있다.
반면에 npm은 노드패키지를 설치하는것을 도와주는 도구이다.
vite?!
(출처 : 블로그1, 블로그2)
후에 vite를 배울거지만, 최근에는 리액트프로젝트를 생성할 때 CRA(create-react-app)가 아닌 vite를 사용한다고해서 가볍게 보도록 하자.
vite는 Evan You가 개발한 빠른 웹 개발 빌드 도구이며 최신 버전의 JavaScript 모듈 시스템으로, Just-In-Time(JIT) 컴파일링 방식을 사용하여 개발자들이 더욱 빠르게 개발을 진행할 수 있도록 지원한다.
1. npm create vite@latest
2. 생성 폴더명 입력
3. 원하는 framework 선택 -> js or ts 선택
4. 아래 순서대로 실행실제로 실행해보니까 깜짝놀랄만큼 순식간에 설치된다. CRA는 정말 오래걸렸었다.
이유는 번들링의 차이라고 한다. CRA는 Webpack을 사용하는데, 개발환경에서 도중에도 계속 번들링을 진행하기 때문에 서버시작과 리로딩 시간이 오래걸린다.
그에반해 vite는 Rollup을 사용하는데, 개발환경에서는 번들링을 진행하지 않기 때문에 서버시작과 리로딩이 빠르다.
다만 Webpack은 다양한 기능을 제공해서 그것이 필요하면 CRA를 사용해야겠지만, 공식문서에서도 그정도로 복잡한 과정이라면 다른 라이브러리를 사용하는 것을 추천할정도로 Vite는 유용하다.
실제로 설치를 해보면, CRA로 생성한 리액트 프로젝트는 node_modules가 다 설치되어 있고 리액트에 필요한 많은 모듈이 설치되어있다.
반면에 vite는 설치부터 매우 빠른데 nodemodules를 설치하지 않기때문에 시작할 때 npm install을 해야한다. 설치를 해도 react와 react-dom만 설치한다.
심지어 npm run dev하면 화면도 안켜준다 ㅋㅋ
이 외에도 비트는 jsx파일확장자를 사용하고, index.html이 public폴더 밖에 있는 등 여러 차이가 있지만 동작은 같다.
+) 나중에 추가로 배운건데, vite는 비트~라고 읽는다. 공식문서에서도 적혀있을정도!
또파일을 편집할 때 전체 번들을 다시 빌드 하는 것이 아닌, 다른 페이지에 영향을 주지 않고 일부 변경된 모듈 자체만 교체해서 빠르게 화면에 반영되도록 하는 HMR을 vite에서도 수행하는데, 이게 ESM을 통해서 된다고한다. (ecmascript module) 이게 무슨의미냐면, ESM은 node수업때 배웠는데 브라우저 환경에서 사용하는 자바스크립트의 모듈이다.
즉, HMR을 브라우저가 담당하게 해서 번들링을 브라우저가 하게 하니까 성능 부담이 줄어든다. 분업을 하기 때문에 로딩 속도가 더 빨라 질 수 있다 이말이야!
또cra는 typecript를 쓰려면 추가 라이브러리를 설치해줘야하는데, vite는 기본설정이 되어있다. (어렵게 적혀있기로는 esbuild(go로 쓰여져 있어서 빠르다)를 이용해서 transpiling하기 때문에 훨씬 속도가 빠르다고한다. 다만 타입 checking은 안되는데, 이미 에디터 내에 다른 것들이 타입체킹을 충분히 해주기 때문이라고 한다.)
설정을 다 마쳤다면 package.json의 script대로 npm run start를 해주면 된다.
start는 실행(실무용) / text는 test이다.
build는 만든 앱을 실제로 배포하기전에 사용한다.

리액트 프로젝트 설치를 마치고나면, 폴더에 위와 같은 파일들이 생성된다.
/public/index.html
public폴더에 있는 index.html은 이름이 수정되면 안되는 페이지 템플릿이다.
관행적으로, 사용자가 사이트에 방문하여 페이지를 요청하면 별도의 지정이 되어 있지 않은 경우에는 index.html을 화면에 띄운다.
www.abc.com은 실제로는 www.abc.com/index.html인것이다.
리액트 프로젝트의 index.html은 이런 모습이다. 내용을 보면 public 경로에 있던 이미지와 json파일이 사용되고 있다.
index.html에는 public폴더내에 있는 정적 파일들만 사용 할 수 있다.
/src/index.js
이름이 수정되면 안되는 두번재 파일은 src 폴더의 index.js이다.
코드를 보면 index.html에 있던 div#root를 선택해서 화면을 그리고 있는걸 볼 수 있다. 즉 index.js는 index.html에 화면을 그리는 js파일이다.
이렇게 src폴더에 js파일과 css파일 등 대부분의 리액트 소스코드를 넣어서 사용하면 된다.
webpack은 해당 폴더의 파일들만 번들링하여 최적화 할 수 있다.

화면을 시작하면 처음에 이런 모습이다.
콘솔을 살펴보면 div#root 안에 본문이 작성되어있는 것을 볼 수 있다.
index.js를 보면 App.js를 화면에 그리도록 코드가 입력되어있다.
src폴더내 App.js를 가보면 드디어 화면에 그려진것과 같은 코드가 적혀있는걸 확인 할 수 있다.
single page application
App.js파일의 소스코드를 변경하면 새로고침없이 바로 화면에 변경된 부분이 적용된다.
원래 전통적인 어플리케이션은 멀티 페이지 어플리케이션이다. SSR의 경우 MPA를 사용한다.
즉, 여러개의 페이지를 옮겨다니며 페이지를 띄우고 싶으면 여러개의 html파일을 만들어서 링크를 타고 새로고침하며 옮겨야했다. 이 때 화면이 하얗게 변하는 플리커Flicker현상이 발생한다. 화면을 싹 비우고 새롭게 그려야 하기 때문이다. -> 사용자 불편
하지만 SPA에서는 살펴본것처럼 하나의 index.html파일만 존재하고, 그 안의 div#root를 수정하면 화면이 바뀌는것처럼 보이게 할 수 있다.

SPA에서 서버는 일단 비어있는 HTML을 제공한다. 이것이 아까 살펴본 index.html이다. 이후에 리액트가 dom을 조작하여 div#root에 화면을 그리는 것이다.
즉, 자바스크립트에서 했던것처럼 일부 변경이 된 부분만 골라와서 DOM을 조작해 화면을 바꿀 수 있다. 이것이 CSR이다.
SPA는 필요한 데이터만 요청하고 업데이트하므로 서버 부하가 줄어들고, 전체 페이지를 새로 로드 할 필요가 없으므로 서버에 대한 요청도 줄어든다.
CSR의 단점은 반대로 플리커현상은 없지만 처음에 사용자가 브라우저에 접속했을 때 화면이 랜더링 될 때까지 하얀화면을 보고 있어야한다. 빈 index.html을 받기 때문이다.
(반대로 생각해보면 이래서 index.html파일이 따로 존재하고 그 div를 잡아오는 index.js가 있고 실제로 파일이 그려지는 App.js가 있었던 것이다.)
이렇게 처음에 서버로부터 빈 html 파일을 받기 때문에 검색엔진 입장에서 정보를 얻지 못해서 SEO에 좋지 못하다고 하는것이고, 반대로 SSR의 경우 처음부터 완성된 html을 받기 때문에 검색엔진이 얻어 갈 정보가 많은것이다.
꼭 둘중에 하나의 방식을 선택해야 하는 것은 아니고, 장단점에 따라서 둘을 섞어서 사용 할 수 도 있다 -> 그것이 리액트의 라이브러리인 Next.js이다. next.js는 ssr이지만 일부 csr페이지도 만들 수 있다.
즉 리액트는 화면의 일부 변경점이 생기면 그 때 화면을 업데이트한다. 이러한 환경을 선언적 프로그래밍이라고 한다.
나중에 더 자세히 배우겠지만 리액트에는 state=상태가 존재한다.
이 상태가 화면의 변경점이 된다.
상태가 변경되면 리액트는 알아서 변경된 상태를 화면에 그린다.
바닐라 자바스크립트에서 작성한것 처럼 일일이 getElementById해서 DOM을 가져오고, 내용을 작성하고, append하는 명령적 프로그래밍의 형태가 아니다.
명령적프로그래밍이 원하는 결과를 얻기위해 어떻게 해야하는지 일일이 작성해야 한다면, 선언적 프로그래밍은 결과를 명시하면 리액트가 알아서 DOM을 조작하여 화면을 렌더링한다.
즉 리액트로 원하는 결과를 얻으려면 어떻게How가 아니라 무엇What을 결과로 얻고자 하는지를 작성해주면 된다.
아직 느낌이 오지 않을 수도 있지만, state와 props를 이용해서 코드를 작성하고 결과물을보면 바닐라js에서 DOM을 조작하여 화면을 그렸던것과 확실하게 다르다는것을 알 수 있다.
SPA가 가능한이유는 HTML5의 History API 때문이다. 리액트에서 라우터기능을 사용할 때 필요한 react-router-dom이 HistoryAPI를 사용하기 때문에 SPA가 가능한것이다.
출처 : 블로그
History API는 브라우저가 관리하는 세션 히스토리(session history), 즉 페이지 방문 이력을 제어하기 위한 웹 표준 API다.
말이 좀 어렵지만, 페이지 이동 경로에대한 기록을 가지고 있다가 사용자가 웹브라우저에서 <-, ->버튼을 누르면 해당 경로로 이동 할 수 있게 해준다는 소리다.
그런 기본적인 동작을 하는것이 History.back(), History.forword()이다.
실제로 콘솔에서 해당 코드를 실행해보면 페이지가 전, 후로 이동하는걸 볼 수 있다.
여기서 SPA와 연관되는것은 History API의 pushState() 또는 replaceState() 메서드이다.
이것을 사용하면 페이지 리로딩 없이 URL만 갱신하기 때문에 페이지는 리액트로 원하는대로 그릴 수 있는것이다.
중요한 부분은 “뒤로 가기”나 “앞으로 가기” 버튼은 보통 브라우저의 주소 표시줄 좌측에 위치하기때문에 리액트나 자바스크립트로 통제 할 수 있는 범위 밖에 존재한다는 것이다.
여기에 사용되는것이 HistoryApi의 PopState 이벤트이다. HistoryApi는 브라우저의 api이기 때문에 브라우저에서 발생하는 이벤트를 감지 할 수 있는 것이다.
이 이벤트는 사용자가 브라우저에서 뒤로 가기나 앞으로 가기를 할 때 window 전역 객체에서 발생함으로, 이 이벤트에 pushState() 또는 replaceState()를 걸고, 자바스크립트로 원하는 화면을 그릴 수 있는 것이다.
블로그 글에 의하면 pushState() 또는 replaceState()는 사용법이 굉장히 악랄하다고 한다.
코드를 살펴보면 인자를 3개씩 전달해야하고, 상태를 따로 전달해야하는 등 복잡한데
중요한것은 react-router-dom을 사용하면 이런 복잡한 과정 없이 위와 같은 과정이 일어나게 해서 새로고침없이 url만바뀌고 그 url에 따른 화면을 그리는 작업을 할 수 있게 된다는 것이다!
앞에서 '컴포넌트란 앱을 이루는 최소단위를 말하며, 리액트는 화면을 독립적이고 재사용 가능한 컴포넌트로 구성한다' 라고 했다.
리액트에는 컴포넌트를 생성하는 두가지 방법이 있다.
일단 state와 props는 그냥 프로퍼티나 변수라고 생각하고 이해해보자.
클래스형 컴포넌트
class MyComponent extends Component{
constructor(props){
super(props)
this.state = {상태명 : 상태값}
}
conponentMethod(){
this.setState({상태명 : 새로운상태값})
}
render(){
return(표시할요소, 컴포넌트)
}
}
클래스형은 자바스크립트에서 했던 class문법과 동일하다.
Component라는 리액트에 내장되어있는 부모요소를 상속받아 컴포넌트를 생성한다.
그렇기 때문에 super(props)를 사용해서 props를 등록해야 this.state를 사용 할 수 있다.(부모클래스에 지정이 안되어있으면 값을 불러 올 수 없고 자식요소에서도 사용 할 수 없다.)
그러나 state와 props가 분명하면
class MyComponent extends Component{
state = {상태이름 : 상태값}
conponentMethod(){
this.setState({상태명 : 새로운상태값})
}
render(){
return(표시할요소, 컴포넌트)
}
}
이런식으로 생략해서 작성 할 수 있다.
함수형 컴포넌트
function MyComponent(){
const [상태, 상태변경함수] = useState(상태값)
const componentMethod = ()=>{
상태변경함수(새로운 상태값)
}
return(표시할요소, 컴포넌트)
}
예전에는 class컴포넌트 밖에 없었지만 최신 리액트에서는 함수형 컴포넌트를 사용한다. class컴포넌트를 쉽게 사용 할 수 있도록 만들어졌다.
그러나 리액트가 이용되기 시작한것 자체가 오래되지 않은만큼 아직 레거시파일에는 클래스컴포넌트가 남아있을 수 있으니 알아두어야 한다.
컴포넌트를 다른파일에서 사용하려면 export default 접두사를 이용해야 한다.
export default function Introduce() {
return (
// JSX 코드
);
}
function Introduce() {
return (
// JSX 코드
);
}
export default Introduce;
불러 올때는 import! 해서 중첩하여 사용가능.
import Introduce from './Introduce';
export default function MyApp() {
return (
<div>
<h1> 오즈코딩스쿨에 오신 여러분 ! 환영합니다 </h1>
<Introduce /> {/* Introduce 컴포넌트 중첩 */}
</div>
);
}
import { Component } from 'react';
export default class App extends Component {
render() {
return (
<main className="main-container">
<div className="sub-container">
<h1>장바구니</h1>
<div>
{/*expense form*/}
</div>
<div>
{/*expense list*/}
</div>
<div>
<p>총합계:</p>
</div>
</div>
</main>
);
}
}
아직 관리할 state가 없는 클래스 컴포넌트를 생성해보았다.
코드를 보면, javscript 파일인데도 html문법인 태그가 사용된걸 볼 수 있다. vite에서는 심지어 확장자명도 jsx였다.
jsx란 React에서 사용되는 javascript 확장 문법으로, HTML과 유사한 형태로 js코드를 작성 할 수 있다.
즉, 형태는 HTML이지만 언어는 Javscript이다.
원래 리액트에서 태그를 이용해 화면을 띄우려면
const myelement = React.createElement('hi', {}, 'I don not use JSX')
와 같이 작성하고,
ReactDom.render(myelement, document.getElementById('root');
이렇게 render해줘야 하지만 jsx를 사용하면 첫 예시처럼 하면 된다.
이것은 babel덕분에 가능하다.
홈페이지에가보면 어떻게 변환되는지 볼 수 있다.
규칙
retern문 안에서만 HTML형태로 작성한다.
두 줄 이상일 경우 소괄호로 묶는다.
하나의 최상위 요소 안에 존재해야한다.
retern(
<div></div>
<div></div>
)
retern(
<>
<div></div>
<div></div>
</>
)
위 코드처럼 작성하면 안된다. <>는 Fragments의 축약형이다. 리액트에서 태그를 감싸는데만 사용할 의미없는 빈 태그를 위해 사용된다. 참고 브라우저상의 HTML 트리 구조에서 흔적을 남기지 않고 그룹화해 준다.
JSX는 HTML처럼 보이지만 내부적으로는 JavaScript 객체로 변환됨으로 하나의 배열로 감싸지 않은 함수에서는 두 개의 객체를 반환할 수 없다. (함수는 두개의 값을 반환 할 수 없다는 말이다.)
중괄호를 사용해서 js코드를 작성한다.
return(
<div>{`안녕하세요 ${name}입니다.`}</div>
)
jsx내에서 javscript문법이 사용될 때는 위처럼 {}내에 작성한다.
class가 아닌 className을 사용한다.
모든 태그를 닫아야 한다.
리액트에서 요소의 list를 나열 할 때는 고유의 key를 넣어줘야한다. 키는 리액트가 변경, 추가 또는 제거된 항목을 식별할 때 필요하다.
변경점이 생기면 리액트에서는 가상돔에 우선 적용한다고 했다. 리스트의 경우에는 어떤 리스트이고 어떤 순서인지 key를 통해 인식한다.
왼쪽처럼 기존의 리스트 뒤에 새로운 리스트가 생길때는 상관없지만, 기존의 리스트 보다 앞 혹은 중간에 리스트가 추가되면 리액트는 첫 리스트가 바뀌었으니 완전히 새로운 값으로 인식하고 모든 자식 엘리먼트를 새롭게 그리려고한다. 바뀐것은 <li>3</li>뿐인데도!
어떤것이 바뀐 부분인지 인식하지 못하기 때문이다. 그래서 key값이 필요하다. key값을 알고있으면 어떤 key가 추가되고 배치되었는지 알 수 있기 때문이다.
주의할점은 index를 key로 사용하는것은 추천하지 않는다! (전에 localstorage에서 index를 key로 사용했더니 배열의 길이가 수정 될 때마다 index가 겹쳤던 현상과 같다.)
앞에서 선언적 프로그래밍을 공부할 때, "리액트는 화면의 일부 변경점이 생기면 그 때 화면을 업데이트한다."라고 했다.
이때 변경되는 부분이 바로 state다. state는 특정 컴포넌트 안에서 사용하는 변경가능한 데이터이다. 즉 리액트는 state가 변경되면 화면을 리랜더링한다.
클래스형 컴포넌트와 함수형컴포넌트에서 적혀있던 state가 바로 이 state이다.
클래스형 컴포넌트에서는 상태를 객체의 형태로 주고, 변경하고 싶으면 this.setState()를 사용한다.
함수형 컴포넌트에서는 useState()를 사용하여 상태를 관리한다. 구조분해할당으로 배열에 상태와 상태변경함수를 담아준다.

주의할점은 state를 직접 변경해서는 화면을 새로 그리지 않는다.
반드시 상태변경함수를 사용하여 state를 변경해야 화면을 리랜더링한다.
그러면 왜 setter 함수를 사용해야만 업데이트될까? 컴포넌트가 화면에 그려지려면 render() 되어야 하는데, setter함수가 내부적으로 render()를 호출하기 때문이다.
추가로 setter함수는 '비동기'로 동작한다. 가끔 setter함수로 값을 변경시키고 변경값을 바로 console에 찍어보면 변경하기 이전의 값이 찍히는 경우가있다. 이것이 setter함수가 비동기적으로 동작한다는 증거다. 라이브 수업에서도 알려주셨었는데 (앞에서 한번 적었다.)상태가 바뀔 때 마다 화면이 리렌더링 되는것은 어떻게보면 비효율적일 수도 있기 때문에, 리액트는 내부적으로 상태를 묶어서 변경하도록 되어있고 그것을 batching이라고한다. 원래는 비동기에는 batching이 적용이 안됐지만 18버전부터는 배칭이 비동기에도 적용되기 때문에 setter함수에도 적용이 된다.
useState는 React에서 상태 관리를 위해 사용하는 Hook 중 하나이다.
Hook은 기본적으로 함수이지만, 일반 함수와는 달리 함수 컴포넌트에 추가적인 기능을 제공하는 역할을 수행한다.
const [state, setState] = useState(initialState);
매개변수
initialState는 state 초기 설정값이다. 어떤 유형의 값이든 지정할 수 있다.
반환 값
useState는 두 개의 값을 가진 배열을 반환한다.
첫 번째로는 상태값인 state를 반환하고, 두 번째로는 상태 값을 업데이트하는 함수를 반환한다.
사용 시 주의 사항
useState 로 관리되는 상태를 직접 수정하면 안된다.
상태 값을 업데이트하려면 반드시 set 함수 (상단 예제의 경우 : setName)를 사용하여 업데이트해야 한다.
setState(state+1)
과 같은 방식으로 사용해야하는데, 위 예제처럼 state를 직접 변경하는게 아니라 함수형으로 업데이트 하는 방법이 있다.
setState((prev)=>{prev+1})
prev에는 기존 상태의 값이 담긴다.
반드시 컴포넌트 최상단 층 위에서만 호출해야 한다.
다음과 같은 useState()는 허용되지 않는다.
function App() {
if(num === 1) {
const [number, setNumber] = useState(0);
}
}
function App() {
while (num < 9) {
const [number, setNumber] = useState(0);
}
}
React 함수형 컴포넌트 내부에서만 불러야 한다.
앞서 학습한 클래스형 컴포넌트의 경우 hook이 없어도 잘 동작하기 때문에, hook을 사용할 수 없다.
useState를 사용하는 경우 import문을 사용하여 호출되어야 한다.
import { useState } from 'react'; // 중괄호로 감싸 호출해야한다.
상태 변경이 동시에 여러번 일어나는 경우 혹은 여러 상태가 한번에 변경 되는 경우 묶어서 한번에 처리되는 배칭 Batching이 일어난다. 상태변경과 렌더링이 한번에 처리된다.
-> react 17버전에서는 비동기시에는 batching 되지 않았지만, 18버전부터는 모두 적용되서 상태관리를 할 때 덜 신경써도 된다고 한다.
import { Component } from 'react';
import ExpenseForm from './components/ExpenseForm';
import ExpenseList from './components/ExpenseList';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
expenses: [
{ id: 1, charge: '콜라', amount: 2000 },
{ id: 2, charge: '빵', amount: 1000 },
{ id: 3, charge: '맥북', amount: 20000 },
],
};
}
render() {
return (
<main className="main-container">
<div className="sub-container">
<h1>장바구니</h1>
<div>
<ExpenseForm></ExpenseForm>
</div>
<div>
<ExpenseList
initialExpenses={this.state.expenses}
handleDelete={this.handleDelete}
></ExpenseList>
</div>
<div>
<p>총합계:</p>
</div>
</div>
</main>
);
}
}
jsx에서의 예시에서 사용한 코드인데, 컴포넌트 안에는 이렇게 다른 컴포넌트를 하위요소로 사용 할 수 있다.
현재 상위컴포넌트인 App에서 하위컴포넌트인 ExpenseList로 state를 전달하고 싶을 때 사용하는것이 props이다. state가 자식요소에게 전달되면 props가 된다. (그래서 class에서도 super(props)한것. 자식입장에서 부모에게 보낸거니까!)
props는 부모 컴포넌트에게 전달받은 데이터를 말한다. 자식컴포넌트는 부모컴포넌트의 state를 직접적으로 변경 할 수 없다.
React에서 props는 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 정보의 집합입니다. 이 정보를 이용해서 각 컴포넌트는 서로 영향을 주지 않고 독립적으로 작동할 수 있습니다.
<자식컴포넌트 props명 = {props}/>
와 같이 작성하면 된다.
위 예시에서도 initialExpenses = this.state.expense로 상태를 하위요소의 props 로 전달하고있다.
자식요소에서는
이렇게 받아쓰면 된다.
export default class ExpenseList extends Component {
render() {
return (
<>
<ul className="list"></ul>
{this.props.initialExpenses.map((expense) => {
return (
<ExpenseItem
key={expense.id}
expense={expense}
handleDelete={this.props.handleDelete}
></ExpenseItem>
);
})}
<button className="btn">목록지우기</button>
</>
);
}
}
App.js의 자식컴포넌트인 ExpenseList.js에서 props를 사용하고 있다.
아까 props는 직접적으로 변경 할 수 없다고 했다. 그래서 ExpenseList컴포넌트는 setState를 props로 받아(handleDelete={this.handleDelete}) 사용하고있다.
과제에서 당황한점!
export default class App extends Component { render() { // 사용자 정보 정의 (자유롭게 수정하여 결과를 확인해보세요.) const = user = { name: '김오즈', age: '3', email: 'ozcoding@example.com', profileImageUrl: 'https://t4.ftcdn.net/jpg/06/25/57/19/240_F_625571921_tUlFUGv94qKWk7UNhoVNwQ9B9Aq0TLuv.jpg', job: '프론트엔드 개발자', blog: 'https://velog.io/', github: 'https://github.com/', }; return ( <div className="parent-container"> <h1 className="title">Portfolio</h1> {/* user 객체를 ProfileCard 컴포넌트에 직접 전달 */} <ProfileCard user={user} /> </div> ); } }여기서 user={this.user}가 아니다! user는 class함수내에 정의된 state가 아니라 render()함수 내에 정의된 객체일뿐이다! 그래서 this.user라고 하지않고 그냥 user라고 전달해야한다.
만약 상황에따라서 유저명을 바꾸거나 하고싶었으면 state로 관리해야 한다.
const App = () => {
const [expenses, setExpenses] = useState([
{ id: 1, charge: '콜라', amount: 2000 },
{ id: 2, charge: '빵', amount: 1000 },
{ id: 3, charge: '맥북', amount: 20000 },
]);
const handleDelete = (id) => {
const newExpenses = expenses.filter((v) => {
return v.id !== id;
});
setExpenses(newExpenses);
};
return (
<main className="main-container">
<div className="sub-container">
<h1>장바구니</h1>
<div>
{/*expense form*/}
<ExpenseForm></ExpenseForm>
</div>
<div>
{/*expense list*/}
<ExpenseList
initialExpenses={expenses}
handleDelete={handleDelete}
></ExpenseList>
</div>
<div>
<p>총합계:</p>
</div>
</div>
</main>
);
};
export default App;
const ExpenseList = ({ initialExpenses, handleDelete }) => {
return (
<>
<ul className="list"></ul>
{initialExpenses.map((expense) => {
return (
<ExpenseItem
key={expense.id}
expense={expense}
handleDelete={handleDelete}
></ExpenseItem>
);
})}
<button className="btn">목록지우기</button>
</>
);
};
export default ExpenseList;
app.js로 부터 받은 props를 구조분해할당하여 받고 있다.
React Hooks은 class가 아닌 함수형 컴포넌트에서 React의 상태(state)와 생명주기(lifecycle) 기능을 사용할 수 있게 해준다.
vsReact Hooks가 없을때는 그랬다! react hooks가 생긴 후로 함수형 컴포넌트에서도 class의 기능을 사용 할 수 있다.

리액트의 생명주기는 컴포넌트가 생성될 때(마운트, 화면에 처음 그려질 때), 업데이트될 때(상태가 변해서 다시 그려질 때), 그리고 제거될 때(화면에서 사라질 때)의 일련의 단계를 메서드를 사용하여 제어하고 관리하는 것을 말한다.
class형 컴포넌트
클래스형 컴포넌트가 화면에 표시될 때 실제로 보이는 것은 해당 클래스의 인스턴스이다.
여기서 상태가 변하면 인스턴스 내부의 값이 변하고 인스턴스가 화면을 리렌더링한다. render()함수가 재실행 되는것이다.
화면에서 사라져야 할 때 인스턴스가 언마운트되고, 다시 그려야할 때는 새로운 인스턴스를 생성해서 화면을 그린다.

class형 컴포넌트에서는 각 상황마다 처리해주는 메소드를 하나씩 만들어서 처리한다.
이미지를 보면, 생성될 때와 업데이트 할 때 모두 render()함수를 실행하며, 각 상황에 맞는 함수로 해당 상황에 원하는 동작을 만들 수 있다.
funtional 컴포넌트
반면에 함수형 컴포넌트는 새로운 객체를 만드는 것이 아니라 해당 함수를 호출하여 화면을 그린다.
상태가 변하면 클래스와 다르게 기존 함수가 종료(언마운트)되고 변한 상태를 포함한 새로운 함수가 실행된다.
즉, 상태가 변할 때 마다 새로운 함수 호출로 완전히 대체되면서 리렌더링된다.

기존의 함수형컴포넌트는 생명주기에 맞는 동작을 할 수 없어서 간결하더라도 class를 사용했는데, react Hooks가 업데이트 되면서 함수형컴포넌트도 이것을 할 수 있게 됐다.
그것이 useEffect이다.
react Hooks로 데이터를 받아오고, 컴포넌트의 시작에서 api를 호출하는 등 많은 부분을 할 수 있게 됐다.
useEffect(()=>{},[여기!]) 여기 부분 = 두번째 인자에 값을 넣어주면 값이 변할 때 마다 해당 함수를 실행 할 수 있음.등등!
또다른 큰 장점은 HOC를 대체 할 수 있다는 것이다.
HOC는 화면에서 재사용 가능한 로직만을 분리해서 component로 만들고, 불가능한 부분은 parameter로 받아서 처리하는 방법이다.

사용 방법은 대략 위와 같다. 분리해야할 컴포넌트를 따로 만들어서, 자식요소에게 랜더시키고, 자식요소를 부모요소로 감싸고, 자식요소는 그것을 받아서 사용한다.
위의 방식은 Wrapper가 너무 많아지면 데이터 흐름을 파악하기가 힘들어진다는 것이다.
하지만 리액트훅은 위와 같이 하면 된다. 공통되는 부분을 함수로 만들고 리턴하면, 다른 컴포넌트에서는 그냥 그 값을 받아와서 사용하면 된다. 불필요한 wrapper가 생기지 않는다.
이렇게 동작하는것 역시 babel의 힘이다.
useEffect()는 React에서 부수 효과(side effects)를 다루기 위해 사용되는 Hook으로, 컴포넌트가 렌더링될 때마다 특정작업을 수행할 수 있도록 설정할 수 있다.
부수 효과란 컴포넌트의 렌더링 결과에 영향을 주지 않으면서 컴포넌트의 생명주기에 따라 수행해야 하는 작업들을 말한다.(예를들어, 데이터 가져오기, 구독설정, 수동으로 DOM조작 등)
useEffect는 React 컴포넌트가 화면에 나타난 후 이제 무엇을 할까?라고 컴포넌트에게 물어보는 것과 같다.
useEffect(()=>{
//부수효과를 수행하는 코드
},[])
useEffect함수는 첫번째 인자로 함수를 받고, 두번째 인자로 배열을 받는다. 함수는 useEffect가 호출되면 실행될 코드고, 배열은 useEffect가 호출되는 조건을 정의해준다. 의존성 배열(dependency array)이라고하며 이 배열에 명시된 값들이 변경될 때만 함수가 실행된다.
즉, 어떻게 값을 받느냐에 따라서 life-cycle의 어떤 부분에서 실행될지를 정할 수 있다.
useEffect(()=>{
첫번째인자 : 함수
})그러나 사실 함수형 컴포넌트는 상태가 변할 때 마다 리랜더링 됨으로 어떤 상황에의해 발생하는 useEffect인지 알 수 없기 때문에 잘 사용하지 않는 방식이다.
2. 빈 배열을 인자로 주면, 컴포넌트가 처음 랜더링 됐을 때(마운트)만 한번 호출된다.
useEffect(()=>{
첫번째인자 : 함수
},[])
useEffect(()=>{
첫번째인자 : 함수
},[상태1, 상태2])useEffect(()=>{
첫번째인자 : 함수
return ()=>{리턴되는함수}
})이것은 보통 cleanup함수라고 부른다. 예를들어 컴포넌트가 호출 될때 타이머(setinterval)를 실행한다던가, addEventListener를 이용해서 이벤트를 호출하는 두개의 경우는 모두 이벤트의 실행이 종료되면 그것을 정리해줘야한다. (clearinterval, removeEventListener)
그렇지 않을 경우 만약 윈도우와 같은 전역 객체에 이벤트가 걸려있다면, 컴포넌트가 재생성 될 때마다 이벤트가 중복 될 수 있기 때문이다.
주로 서버에서 어떤 값을 받아와서 화면을 그릴 때 정말 많이 사용함으로 반드시 익혀놔야 한다.
예를들어, 페이지가 렌더링 될 때 서버에서 데이터를 받아와서 상태로 사용하는 아래 코드가 있다.
const [list, setList] = useState([]);
fetch('data/list.json')
.then((res) => res.json())
.then((data) => {
console.log('리스트 받아옴');
setList(data);
});
순간 랜더링이 3000번 이상 발생한다. 데이터를 받아 온 후 state가 변하고, 앱이 랜더링 되면서 또 fetch가 실행되어 무한반복된다.
const [list, setList] = useState([]);
useEffect(() => {
fetch('data/list.json')
.then((res) => res.json())
.then((data) => {
console.log('리스트 받아옴');
setList(data);
});
},[]);
이렇게 useEffect의 두번째 인자로 빈배열을 주면 처음 랜더링 될 때만 데이터 통신이 발생하게 된다.
두번인이유!
이 예제에서 데이터가 두 번 로드되는 것을 보셨을 겁니다. 이는 React의 StrictMode기능 때문입니다.
StrictMode는 개발 모드에서 컴포넌트의 렌더링을 두 번 실행하여, 부주의한 사이드 이펙트를 포착하고 앱의 견고성을 높이기 위한 도구입니다. 이러한 이중 렌더링은 오직 개발 환경에서만 발생하며, 프로덕션 빌드에서는 발생하지 않습니다. 따라서 실제 사용자 환경에서는 성능에 영향을 주지 않습니다.
StrictMode를 사용하는 주된 이유는 다음과 같습니다
1. 부적절한 사이드 이펙트를 미리 감지하고 수정할 수 있도록 돕습니다.
2. 레거시 코드의 문제를 식별하고, 미래에 React 업데이트에 더 잘 적응할 수 있도록 준비합니다.
3. 개발 과정에서의 실수를 최소화하고, 코드 품질을 개선합니다.
이 기능은<StrictMode>태그를 사용하여 적용할 수 있으며, 개발 중 버그와 문제를 식별하는 데 매우 유용합니다. 만약 이중 렌더링으로 인해 혼란을 겪고 있다면, 프로덕션 환경에서는 이 기능이 비활성화됨을 기억하시기 바랍니다.
useState와 useEffect는 컴포넌트의 최상위에서만 사용 할 수 있다 -> 즉, 반복문이나 조건문 내에서는 사용 할 수 없다.
여기서 주의할점은 controlled component이다

만약 input에 텍스트가 입력 될 때마다 상태가 변경되는 컴포넌트라면, 그럴 때마다 함수는 재실행되어 화면을 그린다.
그것이 필요한 경우라면 모르지만, 그냥 완성된 값이 필요하다면 이러한 동작은 매우 불필요하다.
그럴 때 사용하는것이 useRef이다.
useRef()는 DOM주소를 직접 가져와서 리렌더링 되어도 초기화되지 않는 변수가 필요할 때 사용한다.
사용방법은 이렇다. useRef를 초기값을 null을 줘서 만들고, 적용하고 싶은 부분(입력할 때마다 상태가 변하는 부분)에 ref이름으로 속성값을 준다.
이렇게 콘솔에 찍어보면 document.querySelect('input')한것처럼 해당 DOM과 직접 연결된것을 볼 수 있다.

위 상황처럼 input의 값을 받아서 요청을 보내는 때에는 input의 결과만 필요하기 때문에 useRef를 쓰기에 적절하다.
useRef는 변수관리 / 특정 DOM을 선택하는 두가지 이유로 사용 할 수 있다.
ref객체를 만들어서 특정 컴포넌트나 태그에 속성으로 넣어주면, 객체의 current의 값에 할당된다.
위 예시의 경우에는 modal창을 닫을 때 이벤트가 발생한 영역이 ref의 값과 다를 경우에 modal이 닫히도록 했다.
ref객체를 생성 할 때 초기값을 주면 바로 current의 값이 된다. 이걸로 변수를 관리 할 수 있다.
그러면 이때까지 배운 변수 관리방법은 let.const와 같은 키워드 사용 / state / useref 이렇게 세가지가된다.
첫번째로 키워드를 사용하는 방법은, 리액트에서 state가 변경되면서 함수가 리랜더링 될 때마다 초기화 된다.
state는 값이 변하면 컴포넌트를 리렌더링하고, 리렌더링되어도 값이 변하지 않는다.
ref는 컴포넌트가 리렌더링 되어도 값이 초기화 되지 않고 값이 변한다고 컴포넌트를 리렌더링 하지 않는다.
( 구분을 잘해야한다 컴포넌트 리렌더링의 경우다. 새로고침은 완전히 무조건 전부다 초기화된다. )
그래서 렌더링 수를 확인하는 경우 사용 할 수 있다.
추가로, ref는 리액트에서 예약어라서 props로 사용 할 경우 오류가 난다.

그럴경우에는 그냥 props 이름을 ref가 아니라 다른 값으로 사용하던가 forward Ref를 사용하면 된다.
이렇게 컴포넌트를 감싸면, 해당 컴포넌트는 props와 ref를 분리해서 받게되고 ref가 props에 해당하지 않기 때문에 오류가 나지 않는다.
리액트에서 함수형 컴포넌트는 상태가 바뀔 때 마다 새롭게 생성된다고 공부했다. 즉, 최적화 함수는 함수형컴포넌트가 이렇게 함수이기 때문에 생겨난것이다.
함수형 컴포넌트의 state가 변경되면서 내부의 함수들도 모두 선언과 초기화와 값이 할당되는 과정을 반복하게되고, 불필요한 성능낭비가 생기게 된다.
이러한 비효율성을 막고 내용이 바뀔 필요가 없다면 저장해서 재사용하기 위해 최적화 함수들이 만들어졌다.
+ useEffect와 살짝 햇갈렸는데, useEffect는 외존성 배열에 따라서 인자함수를 실행해주는거고, useCallback이나 useMemo는 의존성배열에 따라서 해당 함수의 메모리 주소를 해당함수에 똑같이 할당할지, 아니면 새로운 메모리 주소를 할당할지를 정하는것!
투두리스트 앱이 하나 있다고 치고, 구성은 아래와 같다.
각 컴포넌트마다 console.log('렌더링됨')을 넣어놓고 앱을 실행했다.
그러면 결과는 위와같다. 이제 input에 글자를 쓸 때 마다 결과를 보면,
이렇게 모든 컴포넌트가 리렌더링 되고 있는걸 볼 수 있다.
App 컴포넌트와 form 컴포넌트는 input을 state로 받고 있기 때문에 리렌더링 되는것이 맞지만, lists와 list는 input state를 받고 있지 않은데도 App컴포넌트의 자식요소이기 때문에 같이 리랜더링 되고있다.
앱이 커질수록 이와같은 방식의 렌더링은 성능에 영향을 미친다.
리액트에서 제공하는 React.memo라는 api를 사용하여 리렌더링 하고 싶지 않은 컴포넌트를 감싸주면 input이 변경되어도 해당 컴포넌터는 리렌더링 되지 않는다.
를 이용하면 현재 크롬에서 사용중인 리액트 앱의 성능과 컴포넌트 등 많은 정보를 볼 수 있다.
위 이미지를 보면 현재 앱의 구성 컴포넌트와 state까지 볼 수 있다.
위 이미지는 profiler기능이다. 파란 버튼을 누르고 text를 입력해보면 현재 어떤앱이 얼만큼 성능을 차지하는지 볼 수 있다.
React.memo추가하고나면 확실히 성능이 개선된 모습을 볼 수 있다.
그러면 React.memo를 컴포넌트마다 더 추가하면 좋은거 아니냐? 라고 생각 할 수 있는데, 그것이 더 유리했다면 애초에 리액트의 기본 기능이었을 것이다.
따로 빼놓은 이유는 memo를 사용하는 것 자체로 props가 이전과 같은지 아닌지를 비교하는 내부 계산이 들어가기 때문이다. 만약 state가 바뀔 때마다 호출되는 form과 같은 컴포넌트가 React.memo로 설정되어 있다면 그때마다 계산작업이 추가되면서 오히려 성능이 나빠 질 수 있다.
즉, React.memo는 state가 자주 바뀌지 않을 때만 사용하는게 좋다.
생명주기를 공부하면서 리액트의 funtional 컴포넌트는 상태가 바뀔 때 마다 함수를 다시 만든다고 했다.
이렇게 App.js내부에 여러 함수가 있고, state가 바뀔 때 마다 App.js가 함수가 다시 만들어지면 내부에 존재하는 다른 함수들도 그 때 마다 재생성된다.
만약 그 함수가 내부의 다른 컴포넌트로 전달되고 있으면, 해당 하위 컴포넌트는 React.memo로 감싸져있고 state가 변경되지 않아도, props로 받은 함수가 재생성되면서 리렌더링 되어버린다.
이렇게 자주 바뀌는 state와 직접적인 연관이 없을 때 해당 함수를 useCallback으로 감싸주면, 함수는 조건에 해당 할 때만 재생성되고, 그렇지 않으면 이전 렌더링애서 생성된 함수와 같은 함수 인스턴스를 사용 할 수 있다.
useCallback은 두번째 인자로 의존성 배열을 받는다.
위처럼 빈 배열이면 조건이 없으니까 맨처음 렌더링 될 때만 만들어진다. 만약에 배열안에 상태를 넣어주면 그 상태가 변할 때만 해당 함수를 다시 생성한다.
useEffect와 비슷한데, useEffect는 '의존성배열에 따라서 그 함수를 실행한다'이고 useCallback은 '의존성배열의 상태에따라 그 함수를 새로 만든다' 이다.
마찬가지로 최상위에서만 사용 할 수 있어서 조건문이나 반복문 내에서 사용 할 수 없다.
이렇게 useCallback과 같이 함수를 한번 만들고 기억해두는것을
이라고 한다.
메모이제이션은 비용이 많이 드는 함수 호출의 결과를 저장하고 동일한 입력이 발생 할 때 캐시된 결과를 반환하여 컴퓨터 프로글매의 속도를 높이즌데 사용되는 최적화 기술이다.
useMemo는 useCallback과는 또 다르게 함수가 아니라 함수가 return하는 값을 기억해둔다. 일반적으로 렌더링 시에 계산 비용이 많이 드는 작업을 피하고자 사용한다.
만약 위 컴포넌트에 있는 compute()가 엄청나게 복잡한 연산을 하는 함수라면 함수가 호출 될 때마다 오랜 시간이 걸리게 된다.
그래서 useMemo를 사용해서 값을 기억했다가 값이 변하지 않으면 재생성하지 않도록 할 수 있다.
인자인 a,b가 변하지 않으면 값이 바뀌지 않는다.
마찬가지로, 조건문과 반복문에서 사용 할 수 없다.
lazy는 컴포넌트 로딩을 연기해주는 api이다.
라우팅이 설정되어있는 위와같은 앱이 있다. utils폴더의 index.js에는 공통적으로 사용할 함수를 작성해둔다.
import { sum } from '../utils/index';
const Home = () => {
return (
<div>
<h1>Home</h1>
<button
onClick={() => {
alert(sum(1, 1));
}}
>
+
</button>
</div>
);
};
해당 페이지는 Home에서 +버튼을 누르면 받은 값을 연산해서 알림창에 띄운다.
콘솔창에서는 이렇게 현재 페이지의 구성을 볼 수 있다.
import React from 'react';
const Home = () => {
return (
<div>
<h1>Home</h1>
<button
onClick={() => {
import('../utils/index').then((module) => {
alert(module.sum(1, 1));
});
}}
>
+
</button>
</div>
);
};
export default Home;
코드를 이렇게 수정하면
처음 페이지가 로딩 됐을 때는 유틸폴더의 index.js가 다운로드 되어있지 않아서 보이지 않는다.
컴포넌트도 마찬가지로 

바로 로딩하고 싶지 않은 컴포넌트는 위와같이 lazy 모듈을 사용해서 import하면 바로 다운로드 되지 않고 해당 페이지에 접속할 때 해당 페이지컴포넌트를 받아온다.
suspense는 그렇게 컴포넌트를 받아 오는 동안에 보여질 임시 페이지를 설정하는 부분이다. (하지 않으면 오류)
매우 찰나인데, 이렇게 콘솔 네트워크에서 slow를 주면 천천히 살펴 볼 수 있다. 신기
그런데 이것역시 그럼 다 이렇게 하면 되는거아니야?! 라고 생각 할 수 있지만, 해당 컴포넌트나 함수가 너무 커서 로딩속도에 영향을 줄 때 의미가 있는것이지 앱의 사이즈가 작으면 오히려 추가과정이 필요해져서 로딩속도를 더 느리게 만들 수 있다.
+ 실시간 세션을 듣다가 보니까 보통 useCallback이나 useMemo에 잘못된 의존형배열을 전달해서 아예 먹통이 되는 경우가 많은데, 그럴 때 상태변경함수 setState에서 직접 값을 변경하는 방식이 아니라 함수형 업데이트를 하면 상태변경함수에 전달하는 값이 고정된 state의 값이 아니라 이전의 값이라는 형태이기 때문에 잘 적용이 된다.
그래서 하고싶은말은 무작정 그렇게 작성하라는게 아니라, 범용성이 더 좋아보였다는 것이고 최적화 함수는 제대로 써야한다..
그리고 최적화는 일단 구현단계를 끝마치고 나서 다시 보는게 좋을것 같다고 말씀하셨다. 어려운 작업이 맞기때문에 지금은 개념을 먼저 알고 나중에 적용해보도록 하자.
이때까지 상태관리를 하는 방법은 useState만 사용했고, component간에 정보 전달은 state를 props로 전달하는 방식이었다.
종속되는 컴포넌트가 하나일 때는 이런 방식이 간단하지만, 아주 깊은 자식요소에게 props를 전달해야 할때는 이런 방식은 매우 불편하다.
예를들어 위 이미지에서 4, 8번 자식이 같은 state를 공유해야하면, App컴포넌트에서 state를 만들어야하고 값을 쓰지 않는 1, 3번 자식들도 state를 만들어야 하는 등 매우 불필요하고 번거롭다.
위와 같이 드릴질 하는것처럼 props를 계속 내려주는것을 props Drilling이라고 한다. props Drilling자체는 개발하다보면 나타날 수 있는 자연스러운 현상으로 상태를 체계적으로 설계하면 useState만으로도 규모있는 애플리케이션을 만들 수 있다고 이론적으로는 알려져있다.
이런 문제를 해결하기 위해 많은 상태관리라이브러리가 만들어져있는데, 리액트에서는 기본적으로 Context를 제공하고, 외부 라이브러리로는 Redux, Redux toolkit, recoil, MobX, Zustand, Jotai 등등이 있다.
현재 날짜 (24.5.24) 기준으로는 recoil은 업데이트가 안된지 오래됐고, mobx의 포지션은 약간 애매하고, Redux의 사용량이 압도적으로 많고 그 외에는 zustand나 jotai등이 쉽고 간편함으로 배우는것을 추천한다.

일단 기본적으로 리액트에서 제공하는 React Context를 공부한다.
컨텍스트는 모든 수준에 수동으로 props를 전달하지 않고도 데이터를 전달 할 수 있는 방법을 제공한다. 즉 전역으로 데이터를 관리하여 필요할때 컴포넌트에 제공한다.
주로 전역상태, 테마, 서비스 사용자 설정 등에서 이용된다.
state를 전역으로 context에 설정하면, 자식컴포넌트는 부모가 아니라 context에서 바로 뽑아와서 사용 할 수 있다.
const AppContext = createContext();
전역으로 사용할 객체를 만든다. 이 객체는 일종의 구독기능이다.
context가 변하면 그걸 구독하고있는 자식요소들도 변경되어야 하고 그런 요소들을 consumer라고 하며, context를 provider라고 한다.
즉 자식요소들이 구독을 하려면 위 객체를 자신의 컴포넌트에서 호출해야한다.
<AppContext.Provider value={{}}>
{children}
</AppContext.Provider>
만들어진 객체가 제공하는 Provider기능을 사용하여 자식요소를 감싼다. value에 전달할 값을 넣는다. Provider가 전역상태와 자식요소를 연결한다.
변경사항이 있으면 Object.is와 동일한 알고리즘을 사용하여 새 값과 이전값을 비교한다.
Object.is는 인자로 받은 값 두개를 비교한다. === 연산자와 다른점은 ===연산자는 -0과 +0을 동일값으로 치지만 Object.is는 false처리한다.
자식요소가 만들어진 context를 사용하기 위해서 useContext를 사용한다.
const value = useContext(AppContext)
useContext는 createContext에 의해 만들어진 context객체를 인수로 받으면, 현재 등록되어있는 context value를 반환한다.
만약 상위요소에 여러개의 context가 존재하면 가장 가깝게일치하는 context의 값을 받아온다.
실습에서는
이렇게 따로 파일을 만들어서 관리했다.
이렇게 App.jsx를 감싸는 형태로 제공하면 전역에서 해당 값을 사용 할 수 있다. 그런데 그러면 value를 main.jsx에서 만들어야하는데 그러면 코드가 더러워지기때문에
이렇게 따로 파일에서 만들어서 exports하여 제공 할 수 있다.
이렇게 사용해도 동일하다.
main.jsx에 적용한 모습.
다른 컴포넌트에서는 이렇게 받아와서 사용 할 수 있다.
위 예제에서 값을 제공할 때 useReducer를 사용했다.
이때까지 상태관리는 모두 useState를 이용했는데, 조건에 따라서 상태를 관리하는 등 좀더 복잡한 상태관리를 처리 할 수 있다.
useReducer는 React에서 제공하는 훅 중 하나로, 복잡한 상태 관리를 할 때 사용된다. 주로 상태와 상태를 업데이트하는 함수를 결합하여 관리하는 데 사용된다. Redux와 유사한 개념이지만, 더 간단하고 로컬 상태에만 적용된다. (리덕스는 비슷하게 동작하는 상태관리라이브러리이다.)

이것이 상태값으로 쓰인 예시이다.
useReducer는 인자로 초기상태와 상태를 변경할 때 사용될 함수를 받는다.
형태는 useState와 같다. initialState가 첫 state가 되고, state를 변경하는 setter가 dispatch이다. (이름은 달라도 상관없으나 통상적으로 사용하는듯 했다.)
dispatch는 AppReducer 함수에 정의된대로 initialState를 관리하여 state에 저장한다.
즉 AppReducer는 상태업데이트 로직을 정의하는 순수함수이다.
예제에서 사용된 AppReducer는 이렇게 되어있다.
이 함수는 매개변수로 state와 action을 받아서 각 호출시 동일한 입력에 대해 같은 출력을 반환해야하고, 위의 예시와 같이 새로운 상태를 반환하도록 한다. ( 불변성 )
state는 initialState가 된다.
action은 다른 컴포넌트가 dispatch를 호출 해서 사용 할 때 dispatch('여기') << 여기부분에 값을 {type : 'add', payload : 1}이런식으로 주게되면, 그 값이 action객체가 된다. 이것을 이용하여 type에 맞는 case를 동작시켜 리턴을 만들고 initialState를 변경시킨다.
useReducer역시 상태이기 때문에 상태가 변하면 컴포넌트를 리렌더링한다.
예제에서는 최종적으로 이런식으로 value를 작성했고,
자식요소는 위에서 말한것처럼 이런식으로 값을 받아와 사용하거나
이런식으로 값을 받아와 수정할 수 있다.
다른 훅과 마찬가지로 최상위레벨에서만 사용 할 수 있다.
reducer는 pure function이기 때문에 인자를 변경하거나, 부수효과를 발생시키거나, Date.now()나 Math.random()과 같이 non-pure funtion을 내부에서 호출해서는 안된다.
자바스크립트 애플리케이션을 위한 상태관리 라이브러리이다.
리덕스는 store로 전역적으로 값을 관리하여 컴포넌트들이 바로 접근해서 사용 할 수 있도록 한다. 리덕스의 데이터 흐름은 한 방향으로 흐른다.
이렇게 단방향으로 데이터의 흐름이 전달되는것을 Flux라고한다.
자바스크립트 딥다이브 스터디 중 flux관련해서 배운부분이 있어서 짧게 추가한다.
이렇게 코드를 작성하는 방식이 정해져있는걸 디자인패턴이라고한다.
nodejs배울 때 했던 MVC도 디자인패턴중에 하나다. Flux가 나온 이유는 MVC와 같은 양방향 디자인패턴 같은경우 흐름을 파악하기가 힘들기 때문이다. 서로 상호작용을 하기 때문에 디버깅 할 때 문제가 어디서 발생한건지를 파악하기가 힘들다.
그러나 단방향으로 진행되는 Flux패턴으로 작성할 경우 데이터 흐름을 파악 할 수 있기때문에 디버깅할 때 매우 큰 차이를 보인다.
리덕스 사용 예시
어떤 은행앱에 출금과 입금 기능이 있다.
처음에 10달러가 입금되면 입금버튼이 눌려질거고 이벤트가 발생하면 10달러를 입금한다는 데이터를 리듀서에게 보낸다. 리듀서에서 리턴을 한 값이 스토어에 있는 0원을 10달러로 변경시키고, 스토어의 값이 10으로 변하면 값을 구독하고 있는 컴포넌트들이 리렌더링된다.
이벤트가 발생해서 누군가 10달러를 입금한다는 정보를 담고 있는 객체가 action객체이다.
그리고 정보를 담고있는 action객체를 reducer에게 전달하는것이 dispatch이다. action이 dispatch돼서 reducer로 전달되면, reducer는 action에 담긴 정보를 토대로 어떤 값을 리턴하고, store에 있는 state를 업데이트한다.
사실 최근에는 리덕스툴킷을 사용하는데, 리덕스를 먼저 익혀야 내부 동작을 알아보기 더 쉬움으로 리덕스를 먼저 공부한다.
리듀서를 만든다. 위 코드에서 아무런 설정이 되어있지 않을 때 초기값을 보여주도록 default를 return state로 수정해야한다.
createStore를 통해 store를 생성하고 store의 값으로 생성한 리듀서를 넣어주면 된다.
App 컴포넌트가 store를 구독하게 만들려면
App 컴포넌트를 함수형태로 만들어 실행하고, .subscribe메소드의 인자로 넣어주면 된다.
그리고 props 형태로 store값을 .getState()메소드를 통해서 가져와서 전달하고, 자식 컴포넌트에서 받아서 사용 하면 된다.
그런데 위와 같은 형태로 적용할거면 사실 useState로 state를 관리하는것과 다를게 없어서 나중에 수정할 것.
리듀서가 여러개일때는 이렇게 하나의 리듀서로 합치고 그것을 store의 값으로 사용하면 된다.
앞에서 언급한것처럼 props의 형태로 전달하던 것을 react-redux에서 제공하는 Provider를 사용해서 App컴포넌트를 감싸주면 하위요소들에게 store에 접근하도록 해 줄 수 있다. 아까 위에서 막 함수형태로 만들고, 실행하고, props로 전달하고 하던 기능을 대신해주는것! Provider의 props로 만들어준 store만 전달하면 된다.

그리고 자식요소에서는 그냥 store의 상태와 dispatch를 위와 같이 받아와서 사용하면된다 ㅇㅅㅇ!!
이렇게 쓰면됨! 아주간단하고 편하다 굿
액션을 디스패치해서 리듀서에 도달하는 하기전에 사전에 지정된 작업을 실행할 수 있게 해주는 중간자가 미들웨어이다.

비동기통신, 로깅작업 등을 한다. 미들웨어를 만드는 방법은 위와 같다.
미들웨어는 여러개를 만들어서 등록할 수 있고, 다음 미들웨어로 넘어가려면 next()함수를 마지막에 호출해야한다.
미들웨어를 등록하고 store에 추가하면 된다.
store에 등록된 state를 변경할 때 마다 로그가 뜬다.
미들웨어에서 비동기작업을 할 때 많이 사용되는 방법이 redux-thunk이다. 즉 미들웨어중에 하나다.
thunk는 일부 지연된 작업을 수행하는 코드 조각을 의미하는 프로그래밍 용어로 지금 당장 일부 로직을 실행하는 대신 나중에 작업을 수행하는데 사용 할 수 있는 함수 본문이나 코드조각이다.
위 코드는 jsonplaceholder에서 dummy user data를 받아와서 store에 저장한다. 그런데 컴포넌트상에 저장해두지 않고 action폴더를 따로 만들어서 관리한다. 이 파일에는 앞으로 데이터를 삭제하거나 수정거나 추가하는 등의 작업이 더 추가될것이다.
이렇게 필요한 기능들을 모아서 정리해두는것을 중앙집중식 접근방식이라고 한다.
그런데 위 코드는 동작을 하지 않는다. 왜냐하면 store에 접근하기 위한 useDispatch나 useSelector는 컴포넌트 내에서만 사용 할 수 있기 때문에 위와같이 일반 함수에서는 사용 할 수가 없다.
이럴 때 thunk를 사용한다.
일단 설치 : npm install redux-thunk
이렇게 기존에 함수를 dispatch와 getState를 인자로 받는 함수를 리턴하도록 수정한다.
그리고 미들웨어로 등록하고,
호출할 때 그냥 함수를 import해서 호출하는게 아니라 dispatch를 사용해서 호출하면 끝이다. 그러면 이제 컴포넌트가 아닌 함수에서도 useDispatch와 useSelect를 사용 할 수 있다.
사실 이부분이 라이브강의에서는 thunk덕분에 가능한거라고 말하셨다. 원래 dispatch는 action과 같은 객체만 값으로 받을 수 있어서 위처럼 함수를 인자로 전달 할 수가 없는데 thunk를 사용해서 미들웨어에 등록하면 그것을 가능하게 해준다.
보통 이런식으로 비동기 통신에 사용함
라이브강의에서는 버튼을 누르면 1초후에 1이 증가하는 버튼을 만들었다.

그러면 thunk를 미들웨어에 등록하고 미들웨어를 스토어에 등록해주기만해도 dispatch내부에서 함수를 사용 할 수 있음.
https://www.youtube.com/watch?v=9wrHxqI6zuM
Redux 자체는 상태관리를 위해 만들터진 라이브러리이다.
그러나 베이직한 상태로는 리액트에 적용하기 힘들어서 React Redux가 만들어졌고, Redux를 사용하는데 기억해야 할 것과 설정 할 것이 많아서 간편하게 사용할 수 있도록 Redux toolkit이 만들어졌다.
npx create-react-app ./ --template reduxnpm install @reduxjs/toolkit리덕스툴킷을 사용할때는 리덕스툴킷과 리액트리덕스만 설치하면 된다.
기존의 리덕스는 스토어를 만들고, 제공할 Provider로 하위컴포넌트를 감싸고, 리듀서를 만들고, 스토어에 초기값과 리듀서를 등록하고, useSelector와 useDispatch를 사용한다.
기존의 리덕스가 여러가지 reducer를 넣어서 하나의 store를 만들었다면, redux toolkit은 slice라는 기능별로 작은 store를 만들고 그것을 하나의 store로 합친다. (이것은 redux가 해준다.)
액션과 리듀서를 따로 만들어야했었는데 slice를 사용하면 따로 만들지 않아도 된다. 액션타입, 액션생성자, 리듀서의 역할을 한번에 한다.
createSlice()로 slice 생성
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
up: (state, action) => {
state.value += action.payload;
},
},
});
slice에는 name, initialState, reducers가 포함되어야 한다. 이러면 작은 스토어인 slice가 생성된다.
redux toolkit은 자체적으로 immer라이브러리를 적용하기 때문에 reducers안에서 상태의 불변성을 지켜주지 않아도 된다.
createSlice를 이용하면 자체적으로 내부의 reducers들을 reducer로 반환한다.
configureStore()로 store 생성
const store = configure({
reducer:{
counter : counterSlice.reducer
}
})
configureStore는 객체 하나를 인수로 받는다. 하나로 합쳐진 reducer를 등록해준다.
또 여러개의 리듀서를 만들때 플레인 리덕스처럼 combineReducer를 사용하지 않고 그냥 reducer에 추가해주면 된다.
상태를 받아올 땐 useSelector()
const count = useSelector((state) => {
console.log(state);
return state.counter.value;
});
state에 아까 추가한 counter가 추가되어있는 모습. 초기값은 value에 들어가있다.
action을 전달 할 땐 useDispatch()
const dispatch = useDispatch();
const handleClick = () => {
//dispatch({type : 'counter/up', payload:1});
dispatch(counterSlice.actions.up(1));
}
정석적이고 익숙한 방법은 윗줄의 type과 payload를 일일이 기입하는 방법이다. type은 slice의 name과 reducers가 마치 경로처럼 만들어진 것이고, payload를 추가해주면되는데 그것을 간단하게 아랫줄처럼 표현해도 된다.
슬라이스를 만들면 내부에 actioncreator가 존재하고 그것이 action객체를 생성해주기때문에 저렇게 작성이 가능한것이다.
슬라이스 내부에 등록한 reducers에 name이 prefix되어 counter/up 이렇게 type이 생성된다.

dispatch(counterSlice.actions.up(1)); 이런 형태로 전달했어도 제대로 전달 된 모습이다.
이 과정에서 redux-toolkit의 createAction이 동작해서 action을 알아서 생성해서 전달하는 것이다. 즉 createAction으로 action을 만드는 작업이 필요하다.
추가로 위처럼 dispatch안에 counterSlice.actions.up 이렇게 치는것도 간소화 하려면 아래처럼
이런 형태로 export시키면
이렇게 import해서 사용 할 수 있다. 이게 어떤 원리냐면
아까도 말했지만 counterslice.action을 하면 createSlice를 하는것과 같이 action이 action creator로 만들어져서 위의 이미지처럼 담기게 된다. 그래서 우리는 dispatch(up(payload))만 해도 위처럼 동작하고 있는 것이다.
기본 사용법은 이렇게 끝이고, 새로 배워서 어색하고 힘든거지 코드만 보면 간결하고 익숙해지면 더 쉽게 만들 수 있을 것 같다. 그리고 위에서 못적었는데 당연히 Provider로 하위컴포넌트를 감싸고 store도 속성으로 등록해줘야한다.
위와 같이 간소화하여 만들 수도 있지만 redux toolkit에서는 잠깐 언급한 createAction처럼 action이나 reducer를 명시적으로 만들 수 있는 api를 제공한다.
createAction() : 강의에서는 예시를 들면서 typescript를 사용했다. redux-toolkit에서는 createAction의 인수로 action의 type만 전달하면 해당 type을 가지는 action creator 함수를 생성하고 생성된 함수에 인수를 넣어주면 그 값이 payload의 값으로 들어가게 된다.
createReducer() :
기존의 리듀서는 왼쪽처럼 조건문을 사용하여 작성했다. redux-toolkit에서는 createReducer를 사용해서 만들 수 있다. 인자로 초기상태와 action을 처리할 콜백함수를 받는다.
이 때 콜백함수를 작성하는 방법이 빌더콜백builder-callback / 맵 객체map-object표기법 두가지가 있다. 타입스크립트와의 호환성 때문에 빌더콜백이 보통 더 선호된다고 한다.
빌더 콜백에는 위 3가지 메소드가 있다. addCase는 액션의 타입을 확인하고 리듀서를 실행한다.
addMatcher는 특정한 패턴을 만들고 그것이 일치하면 리듀서를 실행한다.
위 예시에서는 /pending으로 끝나는 type은 addmatcher의 리듀서를 실행한다.
addDefaultCase는 위의 두 경우가 모두 실행되지 않았을 때 실행된다. switch문으로 작성했을 때 default와 같다.
map-object 표기법을 사용하는 방법은 위와같다. 빌더콜백에서는 빌더 함수 하나를 인수로 넣고 그 안에서 빌더의 메서드를 사용했다면 map에서는 다 따로 구분해서 인수로 넣는다. 결국 동작 방식은 똑같다.
prepare 콜백함수로 action contents 커스터마이징
아까 액션은 이런식으로 생성 할 수 있다고 했다. 그런데 action객체안에 어떤 값들을 넣어서 생성하고 싶다면
이렇게 createAction의 두번째 인자로 prepare 콜백함수를 넣어서 위와 같이 payload 부분에 넣어주고싶은 부분을 작성해주면된다.
그러면 addTodo를 사용해서 action을 만들어보면 텍스트만 기입했지만 액션에 다른 값들이 추가된것이 보인다.
slice로 간단하게 만들때도 이렇게 prepare를 추가 할 수 있다.
extraReducers
slice 내부에서도 extrReducer를 사용 할 수 있다. createSlice의 내부 프로퍼티 중 하나이다. createSlice가 내부에서 생성하는 action type 외에 다른 action type에 응답 할 때 사용된다. 즉 외부 액션을 참조하기 위한것으로 slice.action에서 생성된 액션을 가지지 않는다.
여기서 외부란 slice 내부에서가 아니라 createAction을 사용해서 따로 만들어준 action과 같은 경우를 말한다.
createAsyncThunk : createAction의 비동기 버전이다.
createAction이 두번째 인자로 prepare함수를 받았다면 createAsyncThunk는 promise를 반환하는 콜백함수를 받아서 위와같이 action type을 만든다.
첫번째 인자인 텍스트 뒤에는 앞에서 말한것처럼 팬딩 풀필드 리젝트가 상태에따라 추가될것이고, 그 뒤에 promise를 반환하는 콜백함수가 들어가게 된다.
응답이 오면 res.data는 action의 payload로 들어가게 된다. 그래서 위에서 dispatch(fetchUserById(123))에 123은 받아온 userId가 payload로 들어간 모습이다.
이걸 slice나 reducer에서 처리할 때 아까 말한것처럼 외부 action으로 만든거니까 extraReducers에서 처리하면 된다.
사용은 다른 것들과 마찬가지로 dispatch로 사용하면 된다.
2번째 인자인 promise를 반환하는 콜백함수 위와 같은 인자를 받을 수 있고 인자에 따라서 다르게 처리된다.
thunkAPI부분에 추가하면 된다.
위 예시중 rejectWithValue를 사용하여 에러처리를 해준 모습이다. 에러가 payload로 들어가게 된다.
그 값을 이렇게 state.error에 담아서 사용 할 수 있다.
비동기 thunk를 실행 중 취소 할 수 있다.
이런 thunk가 있다.
해당 컴포넌트가 마운트 될 때마다 비동기적으로 값을 10씩 증가시키는데, abort()를 사용하면 그것을 취소 할 수 있다. 위 코드에서는 언마운트하면 바로 취소한다.
리덕스 도구에서 보면 reject된것을 볼 수 있다.
그러나 데이터 통신의 경우 thunk를 취소해도 한번 요청이 가면 요청 자체가 취소되지 않는다. 요청을 취소하려면
이렇게 thunk를 만들 때 AbortController를 사용하면 된다. 그리고 콜백함수에 아까 말한것처럼 옵션으로 signal을 넣어야 한다. 앞의 _ 는 원래 fetchuserAsync의 인수가 들어가는 곳이지만 지금은 딱히 없어서 밑줄을 넣었다.
이제
언마운트할 때마다 thunk도 취소되고 요청도 취소된다.
추가로 이후에 redux toolkit을 사용해서 todolist를 만드는 실습 중에
이런식으로 reducers를 만들었다. 이게 처음엔 이해가 안됐는데 불변성을 유지하지 않는게 이해가 안되는게 아니라 todo를 수정하는데 state가 수정된다는게 이해가 안됐다. find는 얕은복사를 해서 state와 같은 참조값을 todo에 넣는거기 때문에 todo가 가리키는 것은 state와 같은 객체를 가리키고 있게 되고 그래서 todo를 수정하면 state도 수정된다.
라이브강의에서는
thunk를 이렇게 간단하게 만드는 법도 알려주셨다. 리덕스툴킷은 createAsyncThunk()를 제공해서 따로 react-thunk를 설치하지 않아도 된다. 그냥 위 코드처럼 사용해서 만들고
이렇게 dispatch내부에서 사용하면 된다. dispatch는 원래 객체만 받을 수 있지만 thunk가 함수를 받을 수 있게 해주니까 createAsyncThunk로 만든 함수도 들어갈 수 있는 것 같다.
const handleDelete = (id) => {
const newExpenses = expenses.filter((v) => {
return v.id !== id;
});
setExpenses(newExpenses);
};
const handleSubmit = (e) => {
e.preventDefault();
if (charge !== '' && amount > 0) {
const newExpense = { id: crypto.randomUUID(), charge, amount };
const newExpenses = [...expenses, newExpense];
setExpenses(newExpenses);
} else {
console.error('error');
}
};
이 코드는 리스트를 담고있는 배열을 삭제하거나 추가한다.
그런데 잘 보면 filter, map메소드를 사용해서 새로운 배열을 만들고 그 배열로 화면을 그리고있다. 왜 이렇게 해야할까?
불변성이란 값이나 상태를 변경할수 없는 것을 의미한다.
자바스크립트의 데이터타입에는 원시타입과 참조타입이있다.

원시타입은 고정된 크기로 call stack의 메모리공간에 저장되고, 참조타입은 데이터 크기가 정해지지 않고 heap에 저장된다. 그리고 힙의 메모리 주소를 콜스택에 저장해둔다.
어떤 변수에 담긴 원시데이터를 변경하면, value가 변하는 것이 아니라, 새로운 메모리공간에 원시데이터를 담고 변수는 새로운 공간의 주소를 담게된다. 반면에 참조데이터는 수정하면 heap에 있는 value를 변경한다.
자바스크립트 딥다이브 스터디를 하면서 해당부분을 추가로 알게되어서 작성한다. 왜 자바스크립트는 참조타입은 메모리 주소를 콜스택에 저장하고 메모리에 값을 변경할까? 이건 참조데이터가 굉장히 무겁기 때문이라고한다. 배열이나 객체같은 참조데이터는 안에 담을수있는 데이터의 종류가 많기 때문에 그만큼 큰 공간을 차지하기때문에 원시데이터타입처럼 새로운 공간을 할당하는게 비효율적이기 때문이다.
즉 참조데이터는 불변성이 지켜지지 않는다.
React에서는 특히 불변성이 중요하다. 리액트의 상태는 불변해야 하기 때문이다. 리액트는 상태가 변경될 때마다 컴포넌트가 리렌더링두ㅚ고, 새로운 가상 돔 트리를 구성하는 방식으로 동작하기 때문에 상태의 불변성을 유지해야 이전 상태간의 ㅊ차이를 명확하게 비교 할 수 있다. 그렇지 않으변 변경사항을 올바르게 감지하지 못할 수 있다.
그렇기 때문에 리액트에서 참조타입의 값을 바꿀때는 새로운배열이나 객체를 만들어서 메모리에 저장하고, 그 메모리 주소를 변수가 참조하도록 작업해야한다.
즉, 원본데이터를 직접 변경하는 splice, push등은 지양하고, 새로운 배열을 만드는 spread operator 전개연산자, map, filter, slice, reduce등을 지향해야한다.
만약 객체 내부에 객체가 중첩되어 있는 형태라면 깊은 복사를 통해서 복제해야한다. JSON.stringify()와 JSON.parse()를 사용하는 방법이 있지만 내부에 함수가 존재할경우 null이 되는 현상이 발생 할 수 있다. 그래서 lodash나 ramda와 같은 외부 라이브러리를 사용 할 수 있다.
+ 보통 공부할 때 다루는 자료는
const [state, setState] = useState([
{id:1, name:'보람', age:'비밀'},
{id:2, name:'람보', age:'비밀'},
])
이런식으로 참조값 내에 참조값이 존재하는 경우가 많다. 그러나 사실 위에서 언급한 방식들은 깊은복사가 아니기 때문에
[...기존객체, 새로운객체] 이런식으로 새로운 배열을 만들어내도 그 안에 든 객체는 여전히 동일한 주소를 가지고 있게된다. 평소에는 이 점을 주의해서 사용하면 되는데,
데이터 통신의 경우에는 좀 다르다.
데이터 통신에서 사용되는 값은 배열처럼 생겼지만 사실 JSON이니까 모두 text이다.
그래서 깊은 복사가 되기 때문에 객체끼리 비교하면 받아온 값이 다르다고 판단 할 수 있다. 참조 주소가 변하기 때문이다.
그런데 원시자료형은 깊은복사가 되어도 주소가 아니라 값 자체를 비교한다. 즉 위 이미지에서 id의 값인 1은 1이랑 비교하면 같은값이라고 해준다.
이런식으로 데이터를 받아와서 값을 비교할 때는 객체 자체를 상태의 비교값으로 사용하기보다 그 안에 들어있는 원시자료형을 사용하는게 좋다.
이런식으로 원래는 {el}이렇게 객체 자체를 전달했지만 spread 연산자를 사용해서 객체 내부의 값을 풀어서 전달하면 된다.
그럼 그 값을 props로 받는 컴포넌트에서는 id, content로 직접 받게 되는것이다.
객체를 직접 내려주다가 객체에 든 값을 내려주는 것이다.
css를 작성하는 가장 기본적인 방법은 .css파일에 그냥 작성하는것이다.
웹페이지 스타일링의 근본이고, 초보자도 배우기 쉬우며 별도의 설치과정이나 설정이 필요없고 브라우저가 직접 해석함으로 처리과정이 없어서 빠르게 로드된다.
그러나 연관된 코드끼리 구분을 짓기 힘들고, 중복되는 코드를 재사용 할 수 없으며, 유지 보수가 힘들다는 단점이있다.
이러한 단점을 보완하기위해 나온 여러가지 방법이 있고, 최근에 주로 사용되는것으로 sass(scss), styled-components, tailwind가 있다.
+ css작성중 자기 자신을 가리키고 싶으면 &을 사용하면된다.
이렇게하면 section의 자식요소인 img를 선택하는것이 된다.

Syntactically Awesome Style Sheets 문법적으로 짱 멋진 스타일 시트 라는 뜻이다.
scss는 Sassy CSS의 줄임말로 마찬가지로 멋진 CSS라는 의미이고 SASS 3버전에서 등장하여 호환성과 가독성을 높여준다. (즉 SASS의 버전 중 하나라는 소리다.)
sass는 css 코드 중첩, css 코드 변수화, css속성 재사용 등 여러가지 기능을 제공한다.
사용방법은 이렇다.
scss는 중괄호를 사용하고, sass는 들여쓰기를 사용한다. 사용하는 언어에 따라 선호도가 달라 질 수 있다. (파이썬과같이 들여쓰기가 기본인경우 sass가 더 익숙할 수 있다.)

변수화
$를 붙여서 변수화하여 원하는 곳에서 사용 할 수 있다.
중첩
자식요소를 중괄호 내부에 포함시켜 가시성이 좋아진다.
mixin
mixin으로 만들고 사용하고자 하는곳에서 include로 호출하면 반복되는 코드를 재사용 할 수 있게 된다.
함수처럼 넣고자 하는 값을 다르게 줄 수 있다.
장점 : 코드 재사용성 향상, 가독성 향상, 유지보수 easy
단점 : css대비추가학습 필요, 변환과정이 필요하다보니 기본 css보다 살짝 느림.

Css-in-Js : js파일 안에서 CSS를 처리 할 수 있게 해주는 대표적인 라이브러리이다.
리액트는 컴포넌트 기반 개발언어로 js코드 안에 html(사실은 jsx)이 들어있는 형태이다. 그러나 css는 여전히 따로 작성해야했고 css도 한번에 작성하기 위해 만들어진것이 styled-components이다.
설치 : npm install styled-components or npm install styled-components@latest
기본 사용법
const 컴포넌트이름 = styled.태그종류`
CSS속성 : 속성값;
`
재사용법
const 컴포넌트이름 = styled(만들어져있는 컴포넌트명)`
추가할CSS속성 : 속성값;
`
styled-components는 기본적으로 sass를 제공해서 중첩기능을 사용 할 수 있다.
위 이미지는 StyleHeader 컴포넌트 내의 button을 중첩하여 스타일링 하고있다.
공식홈페이지를 보면, 위와 같은 예제가 있다.
Title이라는 컴포넌트를 styled를 쓰면 styled-component를 이용해서 생성한다.
h1태그이며,``안에 스타일을 정의한다. 위 이미지가 기초사용방법이다.
컴포넌트의 props에 따라 디자인을 줄 수 있다. 위 이미지는 primary의 상태에따른 색깔을 삼항조건연산자로 지정한다.

컴포넌트를 확장시킬 수 있다. 토마토버튼은 버튼의 스타일속성을 모두 가지고 있다. 별도로 추가할 사항만 입력하면 된다.
return (
<NavWrapper $show={show}>
<Logo>
<img
src="/images/apple-logo.png"
alt="logo"
onClick={() => {
window.location.href = '/';
}}
/>
</Logo>
</NavWrapper>
);
};
const NavWrapper = styled.nav`
position: fixed;
top: 0;
left: 0;
right: 0;
height: 70px;
background-color: ${(props) =>
props.$show === 'true' ? '#000000' : '#000000'};
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 36px;
letter-spacing: 16px;
z-index: 3;
`;
props로 전달할 때 $를 붙였다. 없어도 제대로 되긴 하는데, 이 블로그를 보면 나와있는데, 콘솔창에 경고문이 뜸.
props를 className과 같은 컴포넌트의 속성값으로 DOM이 받아들이고 있다고 구분을 위해서는 $를 붙여라라고 경고하는것.
실제로 $를 붙이지 않으면 개발자도구에서 속성으로 인지해서 DOM에서 보인다.
이렇게!
하지만 $를 붙이면 props로 인식해서 DOM에서 보이지 않음.
장점 : css를 컴포넌트화 한다. css와 js의 상호작용이 쉬워지며 class이름을 자동으로 생성해준다.(스타일과 연관된 방식으로)
단점 : css대비 추가 학습이 필요하며, class이름이 사람이 보기에 분명하지 않고 js파일의 크기가 커지게된다.

가장 대표적인 utility-first css 프레임 워크이다. 이미 작성된 css코드를 클래스명으로 넣어주면 스타일이 적용함으로써 css 작성의 고민을 줄여준다.
사용할 프레임워크에 따라서 달라진다. vite를 사용해보자.
터미널에 입력한다. tailwind를 위한 추가 모듈이 많은것을 볼 수 있고, package도 따로 설치해야한다.
어떤 경로에 tailwind를 사용하는지 config파일에 추가해줘야한다. 위 내용은 index.html과 src폴더 내부에 있는 js, ts, jsx, tsx파일에 tailwind를 적용한다는 뜻이다.
이미 완성된 css를 받아와서 사용하기 위해 index.css에 추가한다.이렇게 설정과정은 매우 길다..
사용방법 역시 tailwind 사이트에 매우 잘 나와있다. 검색을 할 수도 있고 만약 display속성을 사용하고 싶으면 위 이미지에서 block을 가져와서 class명으로 넣으면 된다.
이런식으로 버튼의 배경색을 넣고싶으면 해당 내용을 찾아서 classname으로 넣으면 된다.
만약 세부적인 값을 주고싶다면, 클래스명의 수치값 부분에 []를 사용해서 그 안에 값을 넣으면 된다.
장점 : css코드를 직접 작성하지 않아도 되기 때문에 개발속도가 빨라지고 체계적이고 일관된 디자인을 사용 할 수 있다.
단점 : 초기 설정이 복잡하고, 배워야하며, 코드가 지저분해진다. 또 사용하지 않는 css설정까지 모두 저장해놓고 사용하는것이기 때문에 css의 크기가 커진다.(사용하지 않는 설정인 경우 제거하는 방법이 있다고 한다.)
tailwind를 사용하는 대표적인 예시로 githup사이트가 있다.
깃헙에서 개발자도구를 보면 class명이 익숙하다. tailwind로 작성했기 때문이다.
위 이미지처럼 class를 나타내는 이름을 가장 앞에 두고, 그 뒤에 tailwind 클래스명을 넣는 등의 주로 사용되는 작성법이 있다.
각각 css 작성법들은 모두 장단점이 존재하고, 보통은 본인이 속한 단체에서 사용하는 방법을 따르겠지만 왜 그것을 이용하는지에 대한 이해를 하는것이 중요하다.
the movie db api에서 데이터를 받아올 때 axios를 사용했다.
import React, { useEffect, useState } from 'react';
import './Banner.css';
import axios from '../api/axios';
import requests from '../api/requests';
const Banner = () => {
const [movie, setMovie] = useState(null);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
// 현재 상영중인 영화 정보(간략한 여러 정보)
const response = await axios.get(requests.fetchNowPlaying);
// 여러 영화 중 하나의 영화 데이터 받아오기
const movieId =
response.data.results[
Math.floor(Math.random() * response.data.results.length)
].id;
const { data: movieDetail } = await axios.get(`movie/${movieId}`, {
params: { append_to_response: 'videos' },
});
setMovie(movieDetail);
};
if (!movie) {
return <div>loading...</div>;
} else {
return <>Banner</>;
}
};
export default Banner;
화면이 로딩된순간 데이터통신을 해서 받아와야 함으로 useEffect의 두번째인자에 빈배열을 준다.
기억해둬야겠다고 생각했던 부분은 const response = await axios.get(requests.fetchNowPlaying); 이부분이다.
원래는 괄호안에 https:// 어쩌구저쩌구 하고 값을 넣어줘야 하는데, requests를 대신 넣어줬다.
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://api.themoviedb.org/3',
params: {
api_key: '1740a25fa10f613fd3f0006305bb82c8',
language: 'ko-KR',
},
});
export default instance;
const requests = {
fetchNowPlaying: 'movie/now_playing',
fetchTrending: '/trending/all/week',
fetchTopRated: '/movie/top-rated',
fetchActionMovies: '/discover/movie?with_genres=28',
fetchComedyMovies: '/discover/movie?with_genres=35',
fetchHorrorMovies: '/discover/movie?with_genres=27',
fetchRomanceMovies: '/discover/movie?with_genres=10749',
fetchDocumentaries: '/discover/movie?with_genres=99',
};
export default requests;
그 전에 이렇게 2개의 모듈을 만들어뒀기 때문이다.
axios.create()메소드로 새로운 axios 인스턴스를 생성해주면, instance에는 기본적으로 입력해준 url과 params가 들어있기 때문에 해당 인스턴스를 사용 할때 따로 기입하지 않은것이다.
그리고 default로 export했기 때문에 import axios from '../api/axios'; 이렇게 instance라는 이름이 아니라 axios라고 받아오면 해당 모듈에서 만들어져서 기본 설정이 되어있는 axios를 사용해서 요청을 보낼 수 있게 된다.
원래는 다운받은 모듈을 import할 때는 import axios from 'axios'이렇게 한다!
그리고 내부 url역시 requests에서 원하는 값을 받아와서 사용하면 된다.
위 코드는 the movie db api에서 현재 상영중인 영화의 목록을 받아오고, 그 중에서 랜덤한 하나의 영화의 정보를 화면에 띄워준다.
+ const [movie, setMovie] = useState(null);의 초기값을 null로 준 이유는, 만약에 초기값이 빈객체면, data도 객체라서 둘을 비교할때 객체의 키 값이 존재하는지를 비교하는 메소드를 사용해야해서 비교적 번거로워질 수 있다. 그냥 null로 하면 값이 존재하냐 존재하지 않느냐만 비교하면 된다.
+ iframe 사용하기
return (
<>
<Container>
<HomeContainer>
<Iframe
src={`https://www.youtube.com/embed/
${movie.videos.results[0].key}?controls=0&autoplay=1&mute=1`}
></Iframe>
</HomeContainer>
</Container>
<button
onClick={() => {
setIsClicked(false);
}}
>
X
</button>
</>
);
}
};
export default Banner;
const Iframe = styled.iframe`
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.65;
border: none;
`;
ifram은 HTML inline frame의 약자이다.
다른 HTML 페이지를 현재 페이지에 포함시키는 중첩된브라우저로, iframe을 사용하면 해당 웹 페이지 안에 어떠한 제한 없이 다른 페이지를 불러와서 삽입 할 수 있다.
유튜브에서 공유-> 더보기를 누르면 이렇게 경로를 보여준다. 복사해서 넣으면 실제로 내 웹페이지에서 해당 유튜브를 볼 수 있다.
위 코드에서는 src에
받아온 응답에서 해당하는 key를 넣어준것이다.
그리고 추가로 설정을 할 수 있다.
참고 : https://evergiver97.tistory.com/entry/iframe
controls=0은 영상에 재생이나 소리설정같은 컨트롤을 없앤다.
autoplay=1은 자동재생한다.
mute=1은 영상이 시작될때 소리볼륨을 0으로 만든다.
잘 적용된모습 ~_~
React-router-dom은 라우팅을 관리하는 라이브러리이다. 라우팅은 사용자가 URL을 변경하면 해당하는 화면을 보여주는 것을 의미한다. (요청 주소에 맞는 화면을 보여주는 것)
또 웹 앱에서 동적 라우팅을 구현 할 수 있게 된다.
설치 : npm i react-router-dom
설정
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
index.js파일의 App컴포넌트를 BrowserRouter로 감싼다.
BrowserRouter를 사용하여 App 컴포넌트를 감싸는 구조는 애플리케이션의 최상위 레벨에서 라우팅 컨텍스트를 제공하기 위해서이다. 이 컨텍스트는 애플리케이션의 하위 컴포넌트들이 라우팅 정보를 접근하고 사용할 수 있도록 한다.
BrowserRouter는 history api를 이용해서 경로를 탐색할 수 있게 돕는다.
위에서 한번 언급했지만, 리액트는 SPA이기 때문에 하나의 index.html템플릿 파일만을 가지고, 그 안에 있는 div.root를 조작하여 화면을 그린다.
이 때 history api를 이용해서 url을 바꾸고, DOM을 조작하여 root에 원하는 컴포넌트를 그리도록 한다.
사용
import { Route, Routes } from "react-router-dom";
<Routes>
<Route path="/profile" element={<Profile />} />
<Route path="/board" element={<Board />} />
</Routes>
Routes컴포넌트는 앱에서 생성될 모든 개별 경로에 대한 컨테이너 / 상위 역할을 한다. 자식인 Route중 매칭되는 첫번째 Route를 렌더링 해준다.
Route컴포넌트는 path와 element라는 props를 가지며 단일 경로를 만드는데 사용된다. 해당하는 경로에서 어떠한 컴포넌트 요소를 렌더링 할지 결정한다. 이 때 경로가 /이면 앱이 처음 로드될 때 가장 먼저 렌더링된다.
<Link>import { Link } from "react-router-dom";
<Link to='주소값'>
html의 a태그와 유사하게 동작하는 컴포넌트이다. Link 컴포넌트는 페이지를 새로고침 없이 주소만 변경한다.
-> a태그를 사용하지 않는 이유 : a태그를 사용하면 페이지 전체가 새로 로딩되고 화면 깜빡임이 필수적으로 발생해 이는 사용자 경험을 떨어리고, SPA라는 react와 맞지 않는다.
to속성에 이동할 주소값을 적으면 된다. 앱 구성 요소에 나열된 경로 이름을 생성했기 때문에 링크를 클릭하면 경로를 살펴보고 해당 경로 이름으로된 구성 요소를 랜더링한다.
콘솔상에는 a태그라고 뜬다.
: 리액트의 가장 강력한 기능 중 하나로, 레이아웃을 복잡하게 작성할 필요를 덜어준다.
function App() {
return (
<Routes>
<Route path="/" element={<LayOut />}>
<Route index element={<LoginPage />}></Route>
<Route path="main" element={<MainPage />}>
<Route path="profile"></Route>
</Route>
<Route path=":movieId" element={<DetailPage />}></Route>
<Route path="search" element={<SearchPage />}></Route>
</Route>
</Routes>
);
}
const LayOut = () => {
return (
<>
<Nav></Nav>
<Outlet />
</>
);
};
위 코드는 라우트컴포넌트 안에 라우트 컴포넌트가 존재하는 형태이다.
Layout은 말그대로 header나 footer와 같은 어느 화면에서나 공동으로 보여야 할 레이아웃을 표시한다. 앱에 접속했을 때 가장 먼저 나타나게 된다.
<outlet/>컴포넌트는 layout의 자식으로 포함된 컴포넌트들이 표시될 위치를 결정한다.
index속성을 주면 해당 앱에 들어갔을 때 처음 보여주는 컴포넌트가 된다. 그 레이아웃이 layout컴포넌트이다.
<MainPage />의 경로는 / 경로 내부에 위치함으로 /main이 되고, 그 안에 존재하는 라우터의 경로는 /main/profile이 된다.
: 해당하는 경로로 이동시킨다. React Router에서 제공되는 Hook 중 하나로 Link와 다른점은 함수 호출을 통해 페이지를 이동시킬 때 사용되기 때문에, 특정 조건을 충족할 경우에 페이지를 이동하게 할 수 있다.



위 코드는, input에 값이 바뀌면 값을 받아와서 바로 searchPage 컴포넌트로 이동시킨다.
양수를 넣으면 값만큼 앞으로, 음수를 넣으면 값만큼 뒤로 이동한다.
: 현재 위치 객체를 반환한다. 현재 위치가 변경될 때 마다 일부 부수효과를 수행하려는 경우에 유용하다.


위에서 이어서 searchpage 컴포넌트에서 useLocation을 사용해서 현재 위치 객체를 반환한 모습이다.
위 params에서 spiderman만을 가져와서 데이터요청에 사용하고 싶을 때 useSearchParams를 사용 할 수 있다.
URLSearchParams는 현재 URL의 쿼리 파라미터를 URLSearchParams 객체로 반환하고, 쿼리 파라미터를 업데이트할 수 있는 함수를 제공한다. 주로 검색 기능, 필터링, 페이징 등과 같은 작업을 수행할 때 유용하다.


위 코드는 useSearchParams를 사용하여 키와 값 형태로 만들어진 query에, useSearchParams의 메소드인 .get()을 사용해서 값만 가져온 모습이다.
: 주소에 사용된 쿼리 파라미터 값을 담은 객체 생성.




받아온 영화의 이미지를 누르면 id를 경로에 넣고 이동시키도록했다. 그러면 라우터에 등록된대로 detailpage로 이동된다. detailpage에서 useParams를 확인해보면 잘 들어간 모습이다.
여러 값을 가져 올 수 있음
useParmas는 URL 경로 매개변수를 읽는 데 사용된다.
위 예시에서 :movidId로 매개변수를 주었기 때문에 {movidId : '888'}과 같은 객체가 만들어졌다. 보통 : 다음에 작성된다.
useSearchParams는 URL 쿼리 문자열을 읽고 조작하는 데 사용된다. 쿼리 문자열은 ?로 시작하며, 키-값 쌍으로 이루어진다. 예를 들어, /search?query=react에서 query=react가 쿼리 파라미터이다.
그럼 물음표는 뭔데?
https://www.beusable.net/blog/?p=4507

위 블로그 글에 의하면 파라미터는 쿼리스트링이라고 부르며 key, vlaue의 형태로 이루어진다, 물음표 뒤에 나열되고 &구호로 구별하여 여러개 존재 할 수 있다.
쿼리는 질문이라는 뜻임으로 쿼리스트링은 url에서 물음표 뒤 모든 문자열을 의미한다.
그리고 get으로 요청 보낼 때 get변수가 된다. db에 해당 변수로 데이터를 요청한다.
아까 위에서 예로 들면 q가 키이고 spiderman이 값이되어 해당 정보를 요청하는 get메소드 요청을 db에 보낸것이다.
그러니까 정리하자면 useParams는 말그대로 페이지 라우팅을 만들 때 설정한 경로의 일부분을 키 값으로 저장하여 가져올 수 있게 하는거고,
useSearchParams는 url에서 쿼리스트링을 키값 형태로 가져와서 사용 할 수 있는 것.
useLocation과 usesearchparams의 차이는 이렇다. usesearchparams이 쿼리스트링의 값만 깨끗하게 반환한다.
: <Routes>와 기능적으로 동일하다. 객체를 사용하여 <Route>를 생성하는 방법이다.
리액트에 내재되어있는 위와 같은 hook외에도 반복되는 기능을 만들 고 싶은 경우 hook을 만들어서 사용 할 수 있다.

보통 이렇게 hooks 폴더를 만들어서 따로 관리한다.
import { useState, useEffect } from 'react'
export default function useFetchData(url) {
const [value, setValue] = useState([]);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => setValue(data))
}, [url]) // 의존성 배열에 url이 변경되는 경우에만 로직을 실행하도록 설정
return value; // 변경된 value 값을 return
}
import { useState, useEffect } from 'react'
import useFetchData from './hooks/useFetchData';
export default App() {
const user = useFetchData('http://ozcoding/12345');
return (
<>
{/* user 데이터 */}
</>
);
}
이렇게하면 hook이 사용되는 컴포넌트가 깔끔해지고, 재사용하기 좋아진다.
import { useState, useEffect } from 'react';
export const useDebounce = (value, delay) => {
const [deBounceValue, setDeBounceValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDeBounceValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return deBounceValue;
};
vod에서는 이런 예제를 사용했다.
const useQuery = () => {
return new URLSearchParams(useLocation().search);
};
let query = useQuery();
const searchTerm = query.get('q');
const deBouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (deBouncedSearchTerm) {
fetchSearchMovie(deBouncedSearchTerm);
}
}, [deBouncedSearchTerm]);
const fetchSearchMovie = async (searchTerm) => {
try {
const response = await axios.get(
`/search/multi?include_adult=false&query=${searchTerm}`,
);
console.log(response);
setSearchResults(response.data.results);
} catch (error) {
console.error(error);
}
};
위 코드는 주소에서 query를 받아와서 데이터 요청을 보내는데, 이 전에 설정을 input이 변할 때 마다 query가 변하도록 했고, 그에따라서 query가 입력 될 때마다 데이터 통신을 했다.
그렇게 하지 말고, useDebounce라는 훅을 만들어서 위 컴포넌트가 실행되면 해당 함수가 실행이 되는데 input에 값이 입력 되어서 query를 받아오면 useDebounce에 값이 들어가고, 500ms가 지나기 전에 새 값이 들어오면 함수가 재실행되니까 useDebounce도 다시 실행되고 하는식으로 500ms가 지나야 value를 반환하고 그렇게 반환이 되어야 데이터통신을 시작할 수 있도록 했다.
아마도 hook 사용법을 익히기위해서 일부러 좀 어렵게 하신거같다..
인줄 알았는데 이게 알고보니 되게 유명한 방식들이더라.
연속적으로 발생하는 함수나 이벤트를 최적화를 통해 성능을 향상시키기 위해서 묶어서 처리하는 방식이다.


디바운스는 연속적인 동작이 끝나고 나면 한번에 묶어서 처리해도 상관없는 작업을 처리할 때 사용하는 전략으로 검색 자동 완성 기능 등이 있다.
Throttle은 중간에 끊기지 않는 상호작용이 필요 할 때 사용되며 마우스이동이나 스크롤 이벤트 등이 있다.
디바운스는 일정 시간동안 새로운 이벤트가 발생하면 계속해서 새로운 타이머를 만들다가 타이머의 시간이 모두 흘러도 새로운 이벤트가 발생하지 않으면 그 때 값을 반환한다.
위 코드는 사용자가 input에 값을 입력하면 그것이 params에 추가가되고, 3초 이내에 input에 새로 값이 추가되지 않으면 그때 state를 추가하여서 화면에 값을 그린다. 만약 3초가 지나지 않았는데 다시 params가 추가되면, 기존에 브라우저에서 진행되던 셋타임아웃은 제거되어야 함으로 useEffect내부의 return에서 clearTimeout으로 지워야 한다.
그러려면~ 위코드가 잘못됐다. 의존성 배열에 param을 넣어야한다.
Throttle은 좀 더 개념부터가 어렵다.
처음 이벤트가 발생한 시점을 Date.now()를 사용해서 값을 기록해둔다. (useRef사용)
그리고 정해진 시간이 지나서 이벤트가 처리되면 그 시간을 기점으로 다시 일정시간을 측정한다.


컴포넌트가 생성된 시점과 이벤트가 실제로 동작한 시점을 뺀다.
그 주기가 1초마다이고 싶다면 위와같이 하면 된다. 1초 혹은 1초보다 커질때마다 상태를 업데이트하라.
settimeout의 두번째 인자는 위와같이 계산한다. 이벤트 시작 시간에서 현재시각을 뺀 만큼을 기준시간에서 빼야 실행시간이다.
왜냐하면 1초마다 이벤트가 실행되어야 하고 첫 실행시점이 0초고 이벤트가 실제로 발생한게 0.6초라면, 0.4초후에 이벤트가 실행이 되어야 하기 때문이다.
갱장히 복잡한데 일단 개념만 이해해두면, 잘 만들어져있는 코드들이 많아서 사용하면 편리하다고 한다.
이런 앱이 있다고 치고, 목록에는 영화이미지가 약 20개가있다. 버튼이나 스크롤로 다음 영화 이미지 목록을 볼 수도 있지만, 손가락으로 스마트폰을 터치하는것처럼 마우스로 클릭해서 다음페이지로 넘길 수 있다.
이런 기능을 쉽게 만들어주는게 swiper라이브러리이다.
공식홈페이지에 나와있는 그대로 적용하면 간단하게 만들 수 있다.
사용할 페이지에 import를 모두 가져오고, 전체를 swiper로 감싼다. 그리고 적용할 개별 요소를 siperslider로 감싼다.
<Swiper
// install Swiper modules
modules={[Navigation, Pagination, Scrollbar, A11y]}
breakpoints={{
1378: {
slidesPerView: 6,
slidesPerGroup: 6,
},
998: {
slidesPerView: 5,
slidesPerGroup: 5,
},
665: {
slidesPerView: 4,
slidesPerGroup: 4,
},
0: {
slidesPerView: 3,
slidesPerGroup: 3,
},
}}
navigation
pagination={{ clickable: true }}
scrollbar={{ draggable: true }}
onSwiper={(swiper) => console.log(swiper)}
onSlideChange={() => console.log('slide change')}
>
<div className="slider">
<div id={id} className="row__posters">
{movies.map((movie) => {
return (
<SwiperSlide key={movie.id}>
<Wrap>
<img
className="row__poster"
src={`https://image.tmdb.org/t/p/original${movie.backdrop_path}`}
alt={movie.name}
onClick={() => {
handleClick(movie);
}}
></img>
</Wrap>
</SwiperSlide>
);
})}
</div>
</div>
</Swiper>
그러면 끝인데, swiper에는 기본적으로 css가 모두 설정이 되어있다.
만약 개별적으로 css를 수정시키고 싶으면,
개발자 도구를 보면 요소의 class명을 알 수 있으니 css파일에서 수정 할 수 있다.
주의할점은, css의 우선순위이다. 전에 공부한적이 있는데 인라인 방식이 아니라면 나중에 적용한 방식이 우선적용되는데, 보통 파일에서 css파일 import는 최상단에서하고 그 아래에서 컴포넌트를 만들기 때문에 컴포넌트에 적용되어있는 css가 이경우 먼저 적용이 된다.
그래서 css파일에서 적용하고 싶으면 !important를 붙여야한다.
breakpoints는 media 쿼리이다. 각 너비마다 몇개의 아이템을 보여주고, 몇개씩 이동 할지 결정한다.
: 부모 컴포넌트의 DOM계층 구조 바깥에 있는 DOM노드로 자식을 렌더링 하는 방식.
ReactDOM.createPortal(child, container)
첫번째 인자 child에는 엘리먼트, 문자열 혹은 fragment같은 렌더링 가능한 react의 자식, 두번째 인자는 DOM 엘리먼트

모달창은 zindex가 1000인데도 zindex2의 div보다 뒤에있음.
그것은 모달창이 포함된 div의 zindex가 1이기 때문.
div#root가 있는 index.html파일에서 동일한 레벨의 div를 하나 만들어주고,
.createPortal로 감싸고 두번째 인자로 만들어준 동일레벨div를 주면 해당 div로 이동한것처럼 여겨져서
이렇게 모달이 적용된다.
특이한것은 DOM상에서는 다른 div의 아래로 옮겨졌어도, 기존의 zindex1의 부모요소로 이벤트버블링이 된다는 것이다. 리액트트리상에는 존재하기 때문이다.
firebase는 모바일 및 웹 애플리케이션을 만들기위해 google에서 개발한 플랫폼으로, 앱을 만들 때 필요한 인증 / 데이터베이스 / 스토리지 / 푸시 알람 / 배포 등의 필요한 부분을 제공한다.
firebase 사이트로 이동하여 회원가입, 로그인 -> go to console 클릭
프로젝트 생성 ( 나는 실습에서는 분석을 뺐다. ) -> 웹 선택
npm install firebase 
생성이 완료되면 이렇게 설정값을 화면에 띄워준다. 
파일을 하나 만들어서 저장하고 export해준다.
-> 파일 마지막에 export default app;
이것은 firebase를 사용해서 인증을 요청할 때
이런식으로 내 정보를 담아서 인증객체를 만들어야하고, 그때에 사용 된다. 이것을 통해서 firebase에 요청을 보내야 한다.
사용
우선 인증에 사용할 로그인방법을 설정해줘야한다.
홈페이지를 보면 한글번역도 잘되어있고 원하는 사용법을 쓰면된다.
필요에따라 원하는 방식을 선택해서 적용하면 된다.
나는 실습에서 버튼을 누르면 구글에서 제공하는 팝업창을 띄워서 로그인 하는 방법을 사용하기 위해서 위 이미지의 방식을 사용했다.



넣어주면됨.


매우 간단쓰 로그인 후에 user정보가 존재하면 main으로, 아니면 login페이지로 이동되게함
그냥 signout쓰면됨 ㅇㅅㅇ;;; 위 코드에서 사용된 userData는 받아온 user정보를 화면에 띄우기 위해 사용했기 때문에 비워줌.배포! 파이어베이스를 사용하면, 깃헙에 코드를 올릴때마다 빌드해서 배포를 바로바로 해줄 수 있음.
그럼 이런 창이 뜸. 여기서 이제 원하는 옵션들을 고르면됨. 나같은경우는 호스팅을 원했고, 깃허브와 함꼐 사용하는것을 고름. 원하는옵션에가서 스페이스 한번!!(쭉눌렀다가 다시해야했음) -> 엔터
-> 사용중인 레파지토리 이름과 사용자이름 입력
이렇게 설정파일들이 생성되고
깃허브로가서 action가보면 열라 바쁘게 배포하고있음.
firebase에서 내 앱 들어가보면 도메인이있고, 들어가보면 잘 동작함.멀 잘못 건들였는지.. 첨에 글로벌 설치할 때 안되서
export PATH=$(npm config get prefix)/bin:$PATH 이걸 입력해야했음..
메모용 ) https://velog.io/@ljk4268/firebase-firebase-회원가입-구현하기 파이어 베이스 이메일 / 비밀번호 인증시 참고한 주소



이런 오류가 뜬다. 왜그런가 했더니 수정할때마다 npm run build와 firebase deploy를 다시 해줘야한다. 그리고 진짜 더 중요한건 웹페이지에서 새로고침을 한번 해야 적용이 된다는거다; 왜이렇게 동작하는 지는 잘 모르겠지만.. 웹 페이지에 바로 반영이 되지 않나보다. vercel에서는 그냥 배포하면 잘 적용 되는걸로봐서는 vercel을 이용하자 ㅇㅅㅇ;;정규 표현식을 사용 할 때 한글은 모음과 자음이 결합된 형식이라 매우 힘든데, 이것을 해주는 라이브러리가 있다.


사용법은 위와같이 간단하다. 정규표현식을 사용하고 싶은 부분에 메소드를 사용하면 되고, 정규표현식의 값을 비교할 때는 match를 사용한다.
테스트 주도 개발 Test Driven Development란 실제 코드를 작성하기 전에 테스트 코드를 먼저 작성해보는 것을 말한다.
이후에 만든 컴포넌트가 테스트코드를 pass할 수 있어야 한다.
예를들어 이런 테스트 코드가있다. 의미를 정확하게 몰라도 대충 보면 counter라는 id를 가진 dom요소를 스크린에서 가져와서 해당 요소의 textcontent가 0인지를 확인하는 거다.
실제로 컴포넌트를 작성하고 npm run test로 실행해보면
이렇게 test가 통과했다고 알려준다. 이것이 TDD다.
이렇게 기능이이 잘 되어있는지를 확인하는것이 행동 주도 테스트이다. 다른 테스트 방식으로는 구현 주도 테스트가있다. 사용자의 입장에서 볼 때 해당 컴포넌트가 p태그인지 a태그인지는 중요하지않고 그 안에 글씨만 정확하게 적혀 있으면된다.
반대로 구현주도테스트는 태그와 같은 구현들이 의도대로 되어있느냐를 테스트한다. 만약에 p태그로 작성해야하는데 a태그로 작성하면 테스트를 통과하지 못한다.
이렇게 test작업을 하려면 React Testing Library와 Jest 라이브러리가 필요하다.
원래 DOM에는 test를 위한 DOM testing library가 있고, 리액트 구성 요소를 test하려면 React Testing Library를 사용해서 DOM testing library위에 구축하는것이다.
그러나 보통 CRA로 애플리케이션을 만들면, 이미 포함되어 있다.
CRA로 만든 애플리케이션에 App.test.js파일이 있는 이유도 테스트를 위해서이다.
React Testing Library가 컴포넌트를 렌더링 해주면 jest가 그것을 테스트 하는 방식이다.
Jest는 facebook에서 만들어진 테스팅 프레임 워크이다. jest는 파일명에 test가 있거나 폴더명을 tests로 하면 해당 파일을 test한다.
아까 위에서 언급한 예제가 jest 파일 구조이다.
describe으로 여러 관련 테스트를 그룹화하여 블록으로 만들 수 도 있다. it은 test와 같다.
test블록 내부에는 expect와 matcher가 있다. dom요소가 matcher에 적혀있는데로 동작하는지를 확인한다.
랜더함수는 랜더링할 컴포넌트를 넣어주면 된다.
jest내부에서 dom요소를 가져올 때 사용한 getByTestId가 쿼리함수다.
쿼리란 페이지에서 요소를 찾기위해 테스트라이브러리가 제공하는 방법으로 get, find, query 3가지의 유형이 있다.
이 셋의 차이점은 요소가 발견되지 않으면 쿼리에서 오류를 발생시키는지, promise를 반환하고 다시 시도하는지의 여부다.
경우에 맞는 쿼리함수를 사용해야 한다.
테스팅 할 때 matcher를 알맞게 썻는지 확신이 들지 않을 수 있다. 혹은 코드의 형식이나 자바스크립트 문법등이 올바르지 못할 때도 있는데 이러한 부분을 도와주는 모듈이 eslint이다.
자바스크립트를 쓰는 가이드라인을 제시하고 문법에 오류가 나면 알려주는 역할 등 주로 문법 오류를 잡아주면서 formatter의 역할도 한다.
이렇게 어떤 오류가 있는지 알려준다.
프로젝트 내에 npm을 설치해서 사용하면 해당 프로젝트를 사용하는 팀원들과 모듈을 공유해서 사용하는거고, vscode와 같은 환경에서 익스텐션으로 사용하면 개인으로만 적용된다.

npm으로 설치 후에 CRA환경일경우 위와같이 파일을 추가하고 내용을 입력해야한다. 그리고 package.json에 있는
이부분은 지워줘야 충돌이 나지 않는다.
(주의할점은 강의영상이 지금 버전과 호환이 안되어서 나는 강의대로 입력했더니 제대로 동작하지 않았다. 위와 같이 수정하니까 그제서야됐다. 즉 버전마다 작성법이 달라 질 수 있다.)
이렇게 두가지를 설치해야 한다.
그러고나면 이렇게 테스트 코드를 작성했더니 빨간 밑줄이 그어진다. textcontent를 확인하려면 toBe도 동작하긴 하지만 toHaveTextContent가 더 적절하다는것이다.
코맨트대로 수정하면 밑줄이 사라진다.
이렇게 테스트를 작성하고 강의에서는 aws s3로 배포까지 했다. 그런데 그냥 하지 않고 github 액션을 이용해서 우리가 깃헙에 푸시를 하면 test를 마치고 테스트가 통과하면 deploy(배포)되도록 할 수 있다.
깃헙 레파지토리에서 액션 선택 노드js선택
그러면 이런 파일이 나온다. 원하는 동작대로 작성하면 된다. push를 하면 그 아래 jobs에 적힌대로 동작한다는 뜻이다.
버전도 하나 선택해서 입력하고,
어떻게 동작할지도 적어준다.
aws s3는 정적인 앱을 배포하는데 사용된다고 한다. 추후에 더 공부하면 좋을 것 같다.
aws에 가입하고 -> 콘솔로 이동 -> s3 검색 -> 버킷만들기 -> 지역 서울 -> 이름 적고 버킷 생성 -> 세부 정보 보기 -> 속성 -> 정적웹사이트호스팅 -> 편집 -> 활성화 -> 저장
그러면 이렇게 사이트를 하나 만들어준다.
들어가보면 현재 403 에러가 난다. 접근 권한을 설정해줘야한다.
권한탭으로 이동 -> 퍼블릭 액세스 차단 편집 모드 끄기 -> 저장
버킷정책 ->
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::react-action-test-bucket-aws/*"
]
}
]
}
리소스 부분에 자신의 버킷 이름을 복사해서 넣으면 된다. https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/example-bucket-policies.html 여기 가보면 정책을 필요에 따라서 다르게 넣는 예시를 많이 보여준다.
그렇게 하고 다시 주소에 들어가보면 이제 404에러가뜬다. 우리가 배포만하고 파일을 올리지 않아서 그렇다.
다시 아까 작성하던 깃헙액션부분으로 가서 추가로 입력해주자.
https://github.com/awact/s3-action
위 주소로 들어가보면 s3에 배포하고자 할 때 어떤 옵션을 추가해야하는지가 나와있다.
나의 액션에도 추가해준다.
이렇게 한다고 끝이 아니고, 추가로 권한을 만들어야한다.
이게 IAM 이라고 하는데 솔직히 이해는 잘 모르겠다! 일반적으로 aws에 가입하면 root사용자라고 한다. 모든 권한을 다 가지고 있어서 보안에 유리하지 않은? 사용자라고 한다.
그래서 특정한 권한을 부여받는 IAM사용자로 내 계정을 만들어야한다. 그래야 aws리소스에 대한 엑세스를 부여 받을 수 있다고 한다. ( 걍 한마디로 인증을 해야 이거 사용할 수 있게 해준다는 소리 같다 )
IAM검색 -> 엑세스 관리 -> 사용자생성 -> 이름넣고 -> 그 아래에 엑세스키 어쩌구 만들수 있도록 한다고 선택 -> 직접 정책 연결 -> s3 검색 ->
필요한 권한 선택 -> 만듦
밖으로 나와서 만들어진 사용자를 클릭해서 액세스 키를 만들어주면 액세스키와 시크릿액세스 키를 만들어주는데 그걸 무조건 저장해놔야한다. 아니면 사용자를 다시 만들어야한다.
그리고 이걸
이 부분에 추가해야하는데 그냥 하는게 아니고 깃허브에서 세팅 -< 시크릿 -> 액션 -> 뉴 리파지토리 시크릿
해서 이런식으로 전에 노드js할 때 dotenv 작성한것처럼 비밀스럽게 name에 해당하는 secret은 몬지 넣어준다.
이것까지 다 하고나면 작성하던 action node.js 파일을 커밋해주면된다.
그러면 이제 내파일들까지 올리면 이렇게 깃헙이 아까 action node.js에 작성된것처럼 하나하나 실행을해서 aws s3로 배포해준다. 위 이미지에선 test까지 하고있는걸 확인 할 수 있다.
완료되고 나서 버킷을 확인해보면 이렇게 파일들이 잘 받아와져 있다.
이제 아까 그 만들어준 주소에 들어가면
로딩도 잘 되고 기능도 잘 된다!
5.7 리액트 수업이 시작됐다 ~_~ node가 정말 어려웠어서.. 벗어나니까 뭔가 아무것도 안했는데도 벌써 재미있다. 오늘은 거의 선수지식만 배우는데도 하루가 걸렸다. 뭐 이렇게 알아야 할게 많지 싶으면서도 이거 만든사람은 천잰가 어떻게 이런 생각을했지? 싶었다. 화면의 일부만 바꾸고싶으니까 화면전체를 js로 그려 일부만 변경하고, 주소는 historyAPI로 변경하자 << 정말 창의적인 발상이다..
5.8 뭔가 눈에 보이는걸 만드는건 즐겁다.
5.13 useCallback..useMemo..react.memo.. 이것들은 뭔가 없어도 똑바로 돌아가는데 있으면 성능을 더 좋게 만들어주는.. 어려운 것들이다.. 적용해보라는 과제가 있어서 해봤는데, 이게 정답이라는게 있나? 하는 생각이 드는 과제였다.. 실제로 어떻게 적용해야 할지도 되게 애매했고 다른분들의 답을 봐도 모두 달랐다. 조교님께 여쭤보니 성능을 위한 부분은 일단 구현이 익숙해지고 나면 고려해야 할 부분이라 물론 개념을 알면 좋으니 이해만 하고 있다가 좀 더 나중에 제대로 적용해보는게 좋을것 같다고 답해주셨다.
5.16 리액트를 하다보면.. 리액트 구현을 하는것도 어렵고.. css도 어렵다.. 대체 css 가 왜 그렇게 되는지 모르겠다..ㅎ.. 하고싶은대로 구현하려면 정말 연습을 많이 해야 할것같다고 느낀다.
5.21 firebase 설치하다가 기절할뻔했다. 일단 너~~~무 오래걸려서 이게 멈춘줄알고 몇번을 재시작했고, 컴퓨터 설정을 잘못만졌는지 global설치는 되었는데 npm이 가져오질 못해서 .. 정말 힘들었다.. 결국 해결은 했는데.. 뿌듯함보다는 잘 모르니까 이런 문제가 생기는구나 하고 후회 많이했다.. 알고 써야지..
5.28 강의가..참..아쉽다.. 많은 개념을 알려주려 해서일까..정돈되지 않고 계속 넘어가고 쌓으려하니 많은 가닥들이 무너지는 것 같다는 느낌을 요즘 받는다. 그래도 최대한 복습하고 추가학습하려고 노력하고 있다. 곧 리액트가 끝나는데 정말 이렇게 끝나도 되나 하는 불안감이 있다. 어차피 공부에 끝이 없다는걸 알면서도 그런 기분이 든다..~ 조금지쳤나 싶기도 하다. 아무생각없이 하자 나자신아 의미는 부여하니까 만들어지는거야..~!..
5.29 tdd를 오늘 배웠는데 사실 알면 좋고 할 줄 알아야 하는 부분도 맞겠지만 지금 이걸 완전히 알아야하나? 하는 의문이 들어서 사실 완전히 이해되지 않더라도 그냥 그렇구나 하면서 조금 넘겼다. 다만 TDD라는게 있다는건 알았는데 이렇게 해야하는거구나 신기하긴 했다.
그리고 드디어 오늘로 한달내내 배웠던 리액트가 끝이났다.. 아직 next.js가 남긴 했지만 ㅎ
사실 완벽하고자 하는게 욕심이고 완벽 할 수도 없겠지만 아쉬운 부분이 많다.
앞으로 프로젝트를 만들어가면서 쌓아야 하겠지만 이대로 정말 끝내도 되나? 하는 불안감이 계속 남는다.
딥다이브 스터디를 하다가 알게된건데 react-query도 사실 되게 중요한거라고한다. 그래서 리액트 로드맵들을 좀 찾아봤는데 리액트쿼리 학습을 추천하느것도 있고 아예 언급이 없는 경우도 있었다. 조금 살펴보니 이것도 최적화를 위한 부분인 것 같아서 정규 과정이 끝나고 스스로 프로젝트를 만들어나갈때 반드시!! 적용해보아야겠다고 생각했다.
바로 다음주부터는 미니프로젝트를 시작하는데.. 걱정이 많이 된다.. 한달 내내 이론을 흡수하는데만 전념해왔는데 실제로 적용시킬 때 배운것들이 얼마나생각날까 싶다 ㅜㅜ.. 큭큭.. 그래도 최대한 정리를 열심히 해왔으니까.. 지금까지 작성해온것들 열심히 보면서 만들어봐야지 5월도 고생했다 나자신