today_i_learned's People
today_i_learned's Issues
[CodingApple Vue] 부동산 사이트 개발
1. 개발 환경 세팅
- npm install -g @vue/cli : vue 프로젝트를 빠르게 생성할 수 있도록 도와주는 cli
- vue create 프로젝트명 으로 작업폴더 생성
- vue3로 생성
- 프로젝트명 저장소로 새로 에디터 오픈
- app.vue(메인 파일) 에서 작업 , npm run serve로 실행
- 브라우저는 vue를 해석할 수 없음. app.vue를 html로 컴파일해서 main.js가 html로 보여주고 있기 때문에 브라우저에서 확인 가능
- public : html 파일, 기타 파일 보관
2. 데이터 바인딩
- 데이터 바인딩 이유
- html에 데이터를 하드코딩 해두면 변경 어려움
- vue의 실시간 자동 렌더링을 사용하기 위해
- vue는 데이터를 변경하면 data와 관련된 html 자동 렌더링 -> 부드러운 웹앱 개발 가능, 자주 변하는 데이터들은 아래 데이터 보관함에 저장
- 자주 변경하지 않는 데이터는 데이터 보관함에 넣지 X 넣으면 자동 렌더링이 계속
- html 속성도 데이터 바인딩 가능 (style, class 등) -> 사용시 왼쪽에 ':' 붙이기
- html 속성 데이터 바인딩은 : 붙이기, 나머지는 {{ }}
3. 반복문
- v-for: "작명 in 몇회" :key="작명"
- key 필수 : 반복문 돌린 요소를 컴터가 구분하기 위해 사용
- 배열, object 넣기 가능, 데이터 갯수만큼 반복해줌
- 그럼 작명한 변수는 데이터의 자료가 됨
- 변수 작명 2개까지 가능 : 첫번째는 자료, 두번째는 index
4. 이벤트 핸들러
- v-on:click="" or @click=""
- 바닐라 : 버튼 누르면 숫자 찾아서 +1, +1된걸 html에 반영
- vue : 버튼 누르면 관련된 데이터만 +1, 관련된 html은 알아서 재렌더링됨
- 함수 생성 : methods: { 함수(){} )
- 함수 사용시 @click="함수명", js처럼 함수명()으로 호출 아님
- 함수 안에서 데이터 사용시에는 this.데이터명 // this : 내 object, 큰 object
5. 모달
- 동적인 UI 만드는 법
- UI의 현재 상태를 데이터로 저장해줌
- 데이터에 따라 UI가 어떻게 보일지 작성
- 조건식 : v-if="" -> 조건식이 참일때만 보여줌
6. 데이터 import, export
- HTML 태그 안의 속성에 데이터바인딩은 :xx
- HTML 태그 안의 내용에 데이터바인딩은 {{ xx }}
[React-Basic-Hooks] 4. Render React Element(ReactDOM / ReactDOMServer)
Render React Element(ReactDOM / ReactDOMServer)
- render 할 수 있는 곳
- 브라우저 (DOM 실제 render)
- js 환경에서 string으로 뽑아내는 방법 -> 서버(ReactDOMServer)
React Element === plain javascript object
- 개발자는 React Element를 만들고 브라우저에 그리는 것은 ReactDOM, 이게 실제로 DOM이 관리 (변경사항도 update)
- React Element은 immutable
- 한 번 element를 만들면, children or attribute 변경 불가능
- 그럼 update 어떻게? -> immutable하면 새로운 element를 만들어서 ReactDOM.render()로 전달 및 교체 (like Redux)
ReactDOM #
- render()
- containerDOM에 reactElement를 그림
- 기존에 render된 element가 있으면 update
ReactDOM.render(
reactElement, // 그릴 element
containerDOM, // 실제 그려질 DOM 전달
[callback] // render/update 후에 호출될 함수
)
function tick() {
// 한번 만들면 immutable해서 변경 불가능 -> 그럼 매번 time element를 새로 만들어주면 됨
const time = (
<div>
<h1>current time: {new Date().toLocaleTimeString()}</h1>
</div>
);
ReactDOM.render(
time,
document.querySelector('#app')
)
}
tick(); // 1초 뒤에 뜨니까 처음부터 보이도록
setInterval(tick, 1000);
// 실제로는 props를 매번 넘기거나 내부 state를 변경하지, 이렇게 매번 render() 호출하지 않음 !!!
-
코드상으로는 time element 전체를 항상 그린다고는 했지만, 실제로는 시간만 update 되고 있음
-
React에서 Virtual DOM update를 할 때, 바뀐 부분만 update하도록 diff 로직을 거침 -> 변경되는 부분만 update해서 빠르고 메모리 최적화
-
hydrate()
- render()와 기능은 동일, 단 ReactDOMServer.renderToString() 결과 재활용
- server render와 달라지는 내용을 suppressHydrationWarning={true} 통해서 1 depth 무시 가능 (안하면 warning, 가급적 사용 지양)
- 되도록 SSR(server side redering)과 CSR(client side rendering) 결과물을 맞출 것
- server/client 결과가 달라야 하는 경우 server render 값으로 우선 맞추고 CSR에서 state를 통해 변경 추천
- 이 경우 두 번 redner 되기 때문에 주의 필요
ReactDOM.hydrate(
reactElement,
containerDOM,
[callback]
)
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import App from '../defaultApp/App';
const root = document.body.querySelector('#app');
root.innerHTML = ReactDOMServer.renderToString(<App />);
/* SSR에서 온걸 CSR에서 그림
** SSR 결과를 innerHTML 했을 때 사용자가 보는 dom은 다 그려졌고,
** SSR은 서버에서 결과를 string으로 받고, 그래서 사용자 이벤트 같은 것들은 미동작.
** hydrate는 CSR에서 render를 하면서 마크업에서 이벤트를 붙여주고 동작하게끔 하는 역할
*/
setTimeout(() => {
ReactDOM.hydrate(<App />, root, () => console.log('rendered'));
}, 3000)
- unmountComponentAtNode()
- containerDOM의 Component를 unmount 시키겠다
- React를 사용하는 대부분의 SPA 환경에서는 한번 mount되면 update만 하고, 페이지를 나가야 unmount
ReactDOM.unmountComponentAtNode(containerDOM)
- findDOMNode()
- component의 실제 DOM을 가져옴
- ref로 대체 가능, functional component 사용 안됨 -> hooks에서 대체 가능
- 사용하지 말자
ReactDOM.findDOMNode(component)
- createPortal() #
- react root DOM node 외부에 React Element 그리기
- Portal 에서 다룸
ReactDOM.createPortal(child, container)
ReactDOMServer #
- ReactDOMServer 객체를 통해 컴포넌트를 정적 마크업으로 렌더링, 보통 Node 서버에서 사용
- renderToString()
- React element VirtualDOM 트리를 html string으로 뽑아내는 역할
- server rendering 후 그 결과물을 client rendering 시 재사용 할 때 사용
- 결과물이 있는 node에 ReactDOM.hydrate() 사용시 event handler만 추가하여 성능이 좋음 -> 이미 그려진 Html + CSR을 하면서 그릴 VirtualDOM mapping, 이벤트 핸들러 붙이기
- renderToStaticMarkup()
- 단순 static 마크업 생성
- react 내부적으로 사용하는 attribute 추가 안함(eg. data-reactroot)
- data-reactroot가 있으면 이 안에서 React 어플리케이션이 붙고, CSR로 동작하는구나 라고 React가 아는 힌트?
- react를 단순 template 엔진으로 사용할 때 사용
stream api
🛑 server only: UTF-8 encoding 된 byte stream을 return 한다.
- ReactDOMServer or hydrate()는 보통 프로젝트가 만들어지면 처음 한번만 사용되고 이후에는 작성할 일이 거의 없음
- hydrate()를 호출하면 그 밑에 Component가 붙는 방식이지 매번 호출할 일은 없음
Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause()
video Uncaught (in promise) 오류
Case 1
- 오토플레이 영상 클릭시 재생 중이던 오토플레이 영상을 일시중지하고 본편 영상을 재생하는 스펙
- 오토플레이 영상 정보가 충분히 불러와지기도 전에 or 재생되기도 전에 클릭시, video.pause() 코드로 인해 오류 발생
// AS-IS
this.previewVideo.addEventListener('click', event => {
this.previewPlayer.pause();
this.playVideo(true);
});
// TO-BE
this.previewPlayer.addEventListener('play', event => {
this.previewVideo.addEventListener('click', event => {
this.previewPlayer.pause();
this.playVideo(true);
});
});
- play : 영상 재생 요청이 성공했을 때 발생하는 이벤트
- play 이벤트 발생시에만 오토플레이 영상 클릭시 본편 영상 재생되도록 수정
Case 2
- Case 1과 마찬가지로 autoplay 설정된 영상 로드 후, 일시적으로 pause()하고 재생 시점이 되면 다시 play()함
- autoplay 설정된 순간에는 play()된 상태인데, 그 이후 일시적으로 pause()한 시점이 순서대로 되지 않아 iOS에서 영상이 계속 중지되어 있음
- play()시에 Promise를 반환하는데, fulfilled되야 play() or pause() 이행하고 아니면 에러 반환
- 아래와 같이 play()가 반환하는 Promise가 성공했을 때만 pause() 되도록 수정
// AS-IS
this.corePlayer.pause();
// TO-BE
const playPromise = this.corePlayer.play();
if (playPromise !== undefined) {
playPromise
.then(() => {
this.corePlayer.pause();
})
catch(error => {
console.err(error);
})
}
참고
[React JS 마스터클래스] Chapter #2 Styled Components
📌 CSS를 적용하는 다양한 방법
1. global css
import "./styles.css";
- styles.css 내부의 모든 내용이 적용한 모든 페이지에 적용됨
2. inline style
<div style={{backgroundColor: "red"}}></div>
- 가독성 떨어짐
- hover 등의 psuedo selector 불가능
3. module css
import styles from "./Movie.module.css";
<div className={styles.title}></div>
- Movie.module.css의 title class를 styles 객체의 속성 중 하나로 취급해 호출함
- 컴포넌트별로 같은 class 명을 가져도 module화 되어있기 때문에 서로 겹칠일이 없음
- className을 css에서 복붙해와야함
- className만으로 다크모드/라이트모드 구현 번거로움
- 한번에 2개 이상의 className 지정 불가능
📌 Styled Components
import styled from "styled-components";
const Father = styled.div`
width: 100px;
height: 100px;
background-color: red;
`;
<Father />
- css 코드를 css 방식 그대로 사용 가능
- div 요소 이름 대신 하나의 컴포넌트로서 생성해서 취급
- class명을 styled 컴포넌트가 알아서 랜덤하게 생성해줌
props
- 컴포넌트를 설정 변경 가능하고 확장 가능하도록 하기 위해서 컴포넌트에 데이터를 보내는 방식인 props 사용
- css 속성값에 함수로 작성
1. 설정 변경 가능한 컴포넌트
// AS-IS
// BoxOne과 BoxTwo는 background-color 속성값만 다름
const BoxOne = styled.div`
background-color: teal;
width: 100px;
height: 100px;
`;
const BoxTwo = styled.div`
background-color: tomato;
width: 100px;
height: 100px;
`;
// To-BE
const Box = styled.div`
background-color: ${props => props.bgColor};
width: 100px;
height: 100px;
`;
<Box bgColor="teal"/>
<Box bgColor="tomato"/>
- Box에 전달하는 props명과 Box 컴포넌트에서 사용하는 props의 속성명은 동일해야 함
- Styled 컴포넌트에 props를 보내면 알아서 알맞게 class명을 각각 생성함
2. 확장 가능한 컴포넌트
// AS-IS
// Box와 Circle은 border-radius 이외의 css가 모두 동일해서 중복되는 코드
const Box = styled.div`
background-color: ${props => props.bgColor};
width: 100px;
height: 100px;
`;
const Circle = styled.div`
background-color: ${props => props.bgColor};
width: 100px;
height: 100px;
border-radius: 50px;
`;
- Box가 가진 모든 속성들에 Circle만 가진 border-radius만 추가해서 확장해서 사용하고 싶다
// To-BE
const Box = styled.div`
background-color: ${props => props.bgColor};
width: 100px;
height: 100px;
`;
const Circle = styled(Box)`
border-radius: 50px;
`;
- 기존 컴포넌트의 모든 속성을 가져오고 새로운 속성까지 더해주는 확장 가능한 컴포넌트
- Box를 확장해서 Circle 컴포넌트를 생성
as
- 컴포넌트의 요소는 바꾸고 싶은데 스타일을 바꾸고 싶지 않을 때는 props 대신 as 사용
// button 요소가 아니라 모종의 이유로 a 요소지만 모든 스타일은 같도록 사용하고 싶다면 ?
const Btn = styled.button`
color: white;
background-color: tomato;
border-radius: 15px;
border: 0;
`;
// Link 컴포넌트로 확장해서 만들어도 button 요소는 그대로며 같은 스타일을 사용하고 싶은거지 확장하고 싶은게 아님
const Link = styled(Btn)``;
<Btn>Log in</Btn>
<Btn as="a" href="#">Log in</Btn>
- Btn 컴포넌트의 button 요소만 변경하기 위해서 as 사용
- as에 변경하고자 하는 요소 전달
attrs
// AS-IS
<Input required />
- Styled 컴포넌트에서 html 속성 정의 가능
- 같은 컴포넌트가 반복되어서 사용되는데, 각각에 html 속성을 동일하게 써줘야 할 때 아예 컴포넌트 정의시에 attrs로 정의 가능
// TO-BE
const Input = styled.input.attrs({ required: true })`
background-color: yellow;
`;
<Input />
<Input />
<Input />
- 모든 input 태그들이 별도로 html 속성을 각각 입력 할 필요 없이 required 속성을 가짐
애니메이션
const rotationAnimation = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const Box = styled.div`
width: 200px;
height: 200px;
background-color: tomato;
animation: ${rotationAnimation} 1s linear infinite;
`;
- keyframes를 import해주고 사용
selector
1. Styled 컴포넌트 내의 요소 select
// span 태그는 styled 컴포넌트가 아님, Box 컴포넌트 안에 위치해있을 뿐
<Box>
<span>happy</span>
</Box>
// 그럼 Box 안에서 span을 select해서 사용하면 됨
// hover과 같은 psuedo selectors는 마치 sass처럼 &로 사용 가능
const Box = styled.div`
display: flex;
width: 200px;
height: 200px;
background-color: tomato;
justify-content: center;
align-items: center;
animation: ${rotationAnimation} 1s linear infinite;
span {
font-size: 30px;
&:hover {
color: yellow;
}
}
`;
- 꼭 모든 컴포넌트를 styled 컴포넌트 처리해줄 필요는 없음
- 위처럼 styled 컴포넌트 안에서 target을 정해서 css 처리해줘도 됨
2. Styled 컴포넌트 내의 Styled 컴포넌트 select
const Text = styled.span`
font-size: 30px;
`;
const Box = styled.div`
display: flex;
width: 200px;
height: 200px;
background-color: tomato;
justify-content: center;
align-items: center;
animation: ${rotationAnimation} 1s linear infinite;
${Text} {
&:hover {
color: yellow;
}
}
`;
<Text as="p">happy</Text>
- 1번 방법처럼 하면, span 태그가 다른 태그로 변경되었을 때 css는 태그명에 의존하고 있으므로 selector도 변경된 태그로 같이 변경해줘야 함
- styled 컴포넌트 내부 요소 자체를 하나의 styled 컴포넌트로 생성하는 방법으로 적용
📌 Themes
- theme : 모든 색을 가진 object
- 나중에 색을 바꿀 때 컴포넌트를 하나하나 변경하는게 아니라 그냥 이 object의 색을 변경하면 됨
// index.js
import { ThemeProvider } from "styled-components";
// darkmode & lightmode 스위칭을 위해 속성명 동일하게 맞춤
// 그래야 theme을 사용하는 하위 styled 컴포넌트에서 모드 변경시 속성명까지 변경할 일이 없음
const darkTheme = {
textColor: "whitesmoke",
backgroundColor: "#111"
};
const lightTheme = {
textColor: "#111",
backgroundColor: "whitesmoke"
};
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={darkTheme}>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);
// App.js
const Wrapper = styled.div`
display: flex;
height: 100vh;
width: 100vw;
justify-content: center;
align-items: center;
background-color: ${props => props.theme.backgroundColor};
`;
const Title = styled.h1`
color: ${props => props.theme.textColor};
`;
- ThemeProvider로 감싸진 모든 컴포넌트에서 props로 theme 접근 가능
- App에서 사용할 때 직접적으로 theme 값을 갖고 있지 않고 theme에 대한 reference만 갖고 있음� -> 컬러값 변경시 theme을 관리하는 index에서만 변경해주면 됨
웹앱 디버깅 가이드
디버깅 가이드
가이드는 Mac OS 기준으로 작성되었습니다.
라인 앱
- 라인 통검은 웹용 프리뷰 URL을 제공한다.
디버깅 방법
이슈 종류 및 발생 환경에 따라 디버깅 방법을 달리 할 수 있다.
1. 웹 프리뷰 URL
- 웹용 프리뷰 URL에서도 재현되는 이슈라면 먼저 편하게 웹으로 디버깅한다. (먼저 Safari에서도 재현되는지 확인 필요)
- 베타 환경에 배포해서 확인하거나 Charles를 사용해도 되지만 번거롭다면
크롬 개발자도구
를 사용해서 Charles처럼 local 파일 mapping이 가능하다. Overrides
로 수정한 코드는 새로고침해도 사라지지 않으며, 미사용시에는Enable Local Overrides
체크 해제Overrides
사용시 캐시가 disabled 되므로 배포해서 최종적으로 확인해 볼 것
-
개발자도구 > Sources 탭 > Overrides > Select folder for overrides 클릭 후 오버라이딩을 위한 폴더 생성 및 지정 (액세스 요청 알림창 허용)
-
Network 탭 > 변경하고자 하는 파일 오른쪽 클릭 > Save for overrides 클릭
or Elements 탭 > 원하는 이미지 or 파일 오른쪽 클릭 > Reveal in Sources panel 클릭
-
1번에서 지정한 경로에 파일이 생성되며 파일을 수정해서 디버깅한다. 노출 화면의 우측 하단에 VConsole 버튼과 Console에서
[Local File Mapping] Test!!!
가 추가된 것을 확인 할 수 있다.
이후 크롬에서는 서버에 있는 파일을 바라보지 않고, 로컬 파일을 바라본다.
2. 앱 및 특정 단말
- 앱에서 발생하는 이벤트로 재현되는 이슈 or 특정 단말에서만 재현되는 이슈라면 앱 디버깅이 필요하다.
1. [OS 공통] 통검 - D 버튼
- 통검 appshell에서 제공하는 우측 상단
D
버튼으로console.log
,console.warn
확인 가능 (라인 웹뷰 설정시에만 보임) 장점
: 이미 디버깅용으로 개발된 기능이라 console 확인을 위해 추가적인 기능이 필요없음단점
: 너무 작은 콘솔 영역, 이미 통검 로그가 많아 테스트용 로그와 섞여 분간하기 어려움, console만 확인 가능
2. [OS 공통] Charles
- 사용중인 네트워크로 들어오는 파일을 원하는 파일로 변경해서 테스트 가능
- 간단 사용 방법
- Proxy > MacOS Proxy 설정 끔 (폰 내의 앱 요청만 보기 위해)
- Proxy > Proxy Settings : HTTP Proxy Port를 8888로 설정
- 폰과 컴퓨터를 동일한 네트워크로 연결
- 폰의 연결된 네트워크 정보 > HTTP 프록시 > 구성을 수동으로 변경 후 Help > Local IP Address를 입력, 포트번호는 8888로 입력
- 폰 > 브라우저에서 http://chls.pro/ssl 접속하여 인증서 설치
- 폰 > 설정 > 일반 > 정보 > 프로파일에서 Charles Proxy CA 인증서 활성화
- Charles에서 Proxy allow 여부 묻는 팝업 노출 > allow 허용
- 확인하고자 하는 요청의 URL을 Proxy > SSL Proxying Settings > Enable SSL Proxying 활성화 후 incldue에 URL 붙여넣기
- 원하는 파일 Local or Remote mapping 및 테스트 가능
장점
: 배포하지 않고도 편리하게 테스트 가능단점
: 과정이 번거롭기도 하고, mapping이 느려서 테스트하는데 시간이 소요됨, console은 화면에 html로 그리는식으로 확인 가능
3. [Android] Chrome Webviews 디버깅
- Remote dubugging WebViews 가이드
- 안드로이드 단말의 경우 크롬 개발자도구로 웹뷰 디버깅이 가능하다.
4. [iOS] simulator에 앱 설치 후 웹뷰 디버깅
- iOS simulator에 앱 설치 불가 -> Safari로 웹뷰 디버깅 불가
- 추가로 inspector를 붙이는 작업도 적용이 안됨 (by iOS 라인앱 개발)
5. [OS 공통] vConsole
- iOS 특정 단말에서만 발생되는 이슈 or Charles를 사용하면서 편리한 console 확인이 필요할 때 vConsole 사용이 도움 될 수 있다.
- vConsole은 웹 페이지에 가상의 개발자 콘솔을 만들어서 웹앱 디버깅을 보다 쉽게 만들어주는 툴로 스크립트를 설치해두면 console.log 확인, network 트래픽 확인, storage 정보 및 element 확인 등이 가능합니다.
// 1. npm install
$ npm install vconsole
// 2. use vconsole
import VConsole from 'vconsole';
const vConsole = new VConsole();
- 템플릿에서 vconsole 사용 스크립트를 배포해서 확인할 수는 없으므로 Charles로 로컬 파일 치환하여 확인
Log
: 기존에는 화면에 html로 노출해서 확인했던 로그들을 별도 화면으로 확인 가능, 적재된 로그들은 filter하여 원하는 결과만 모아서 확인 가능, 간단한 스크립트 실행도 가능
Network
: 간단하게 요청 정보 확인 가능, request시 전달한 data와 response 정보들을 표시해줌
Element
: 개발자도구에서 사용하던 Element 탭처럼 DOM 트리 구성 확인 가능
Storage
: 웹앱에서 사용중인 cookie, local or session 스토리지 정보 확인 가능
Node.js path 모듈
이슈
윈도우 유저가 브검 광고 개발을 할 때, _sprite_maps.scss
에서 npm start
시
@import "../sprite/_sp_app.scss";
의 경로가 상대 경로가 절대 경로로 변경되면서 에러가 발생
해결
// config/spritesmith.config.js
const getRelativePath = (path1, path2) => {
// AS-IS
const chunks1 = path.resolve(path1).split('/');
const chunks2 = path.resolve(path2).split('/');
// TO-BE
const chunks1 = path.posix.resolve(path1).split('/');
const chunks2 = path.posix.resolve(path2).split('/');
...
path
모듈은 Node.js가 실행 중인 운영 체제(Windows, POSIX)에 따라 달라진다.- Windows :
C:\Users\ano
처럼\
를 사용해 폴더 구분 - POSIX는 유닉스 기반의 maxOS, Linux과 같은 운영 체제로
/Users/ano
처럼/
를 사용해 폴더 구분
- Windows :
- 운영 체제에 따라 다른 결과가 나타나는 문제점을 막고 일관된 결과를 얻어야 한다. 브검처럼 메인이 macOS여서 POSIX 파일 경로로 작업하며 윈도우에서도 일관된 결과를 얻으려면
path
가 아닌path.posix
사용- 반대로 윈도우 파일 경로로 작업할 때 POSIX에서 일관된 결과를 얻으려면
path.win32
사용
- 반대로 윈도우 파일 경로로 작업할 때 POSIX에서 일관된 결과를 얻으려면
참고
[ReactJS로 영화 웹 서비스 만들기] Chapter #7
📌 전체 저장소
📌 To Do LIst
- commit : #
- 직접 state를 수정하지 않음, modifier 함수를 사용하고 그 함수가 대신 수정해주는 역할
- map 함수는 하나의 array에 있는 item을 내가 원하는걸로 변경하는 역할을 하고 새로운 array로 return 해줌
📌 Coin Tracker
- commit : #
- 앱 action : 페이지가 로드되면 로딩 메세지가 뜨고, 코인 정보가 나열되면 로딩 메세지는 사라지고 코인 리스트 UI 노출
- useState를 사용해서 코인 API 호출이 1번만 되도록 & loading UI가 렌더링 최초 1회만 동작하도록
- 강의 이후 챌린지 : 코인 select 후 내가 입력한 금액(달러)로 얼마의 코인을 살 수 있는지 계산하는 기능 추가
📌 Movie App
- commit : #
- 앱 action : 영화 정보를 보여주고, 연결되는 링크 제공해서 더 많은 정보 제공하는 페이지 제작
key
는 React에서만 map 안에서 컴포넌트들을 render할 때 사용함- 특정 타입의 행렬 정의시 arrayOf #
genres: PropTypes.arrayOf(PropTypes.string).isRequired
react-router
- 페이지 전환시 사용
- 이제 페이지 / route 별로 생각해보자
routes/Home.js
: 영화 전체 리스트를 보여주는 페이지routes/Detail.js
: 클릭시 영화의 상세 정보를 보여주는 페이지components/MovieApp.js
: 위의 router를 render- router은 URL을 보고 있는 컴포넌트이고 내가 보고 있는 URL에 따라 해당하는 컴포넌트를 보여줌
Router
<Router>
<Switch>
<Route path='/movie'>
<Detail />
</Route>
<Route path='/'>
<Home />
</Route>
</Switch>
</Router>
- Router 안에 들어가는건 우리가 유저에게 보여주고 싶은 것들
- url의 생김새가 다름
Browser Router
- 보통의 웹사이트처럼 생김
Hash Router
- url 뒤에 항상 #을 붙임, 그래서 보통 BrowserRouter 사용
Switch
- 한번에 하나의 Route만 렌더링되기 위해서 Switch 사용
- url 이 path에 있으면 해당 Home 컴포넌트를 렌더링해줌
- specific한 path 순서대로 사용한다
- Route(url)를 찾아서 해당 컴포넌트를 렌더링해줌
페이지 이동
- Home의 영화 정보들을 클릭하면 해당하는 Detail 컴포넌트로 보여지도록 하기 위해서는 ?
- Vanilla적인 생각이라면.. 각각의 영화 정보를 a 링크로 변경하고 클릭시
/movie
로 이동하도록 했겠지? -> 하지만 이러면 페이지 전체가 reload 되어버림, 그럼 React를 쓰는 의미가 없잖아? -> Link를 사용하자 !!!
Link
- 브라우저 새로고참 없이도 유저를 다른 페이지로 이동시켜주는 컴포넌트
Dynamic URL
- Dynamic URL ? : URL에 변수를 넣을 수 있음 (
/movie/123
) - 변수를 보낼 때는
:
를 사용하자, 아니면 해당 경로로 인식됨 /movie/:id
: /movie/123 경로로 이동/movie/id
: 말그대로 /movie/id 경로로 이동
useParams()
// in Detail 컴포넌트
const x = useParams(); // {id: '12345'}
- url에 오는 변수가 어떤 값인지 알아야 해당 변수값을 가지고 요청 등의 작업을 수행 가능 -> React Router가 url에 있는 값을 반환해주는 useParams 함수를 제공
/movie/:id
URL 이동시 Router가 Detail 컴포넌트를 렌더링해주고 있으며, Detail 컴포넌트에서 useParams 함수를 사용해서 id 값을 알 수 있음
📌 Publishing
// 1. package.json에 아래처럼 추가
"homepage": "https://{GithubID}.github.io/{저장소 코드가 반영되어있는 repo Name}"
// 2. package.json에 scripts 추가
// npm run deploy시 predeploy로 build가 실행되고 gh-pages로 build 폴더를 위에 설정한 웹사이트에 반영함
"scripts": {
"deploy": "gh-pages -d build",
"predeploy": "npm run build"
}
- github pages에 publish
gh-pages
: 결과물을 github pages에 배포할 수 있도록 해주는 패키지- 반영되는데 시간이 조금 걸림
package.json - scripts
build
: build scripts 실행시 개발한 코드의 production ready code를 생성 가능- 실행 후에 build 폴더에 압축된 최적화된 브라우저가 이해할 수 있는 js 코드가 생성됨
[React-Basic-Hooks] 10. Context
Context
React에서 data는 top-down(parent to child)로 흐른다.
Virtual DOM 자체가 트리 형태
앱 전체에서 여러군데에서 사용되는 값(global value)을 상위에서 전달하는 방법
- 여러 컴포넌트에서 공통으로 사용하는 state가 있다면 sync를 맞춰야 하니 공통 부모로 state를 올림, 부모에서 state 관리하고 자식에서 props로 받음
- 자식 컴포넌트에서 부모 컴포넌트의 state를 변경해야 하는 상황이면 state를 변경할 수 있는 함수를 자식에게 props로 전달 후 변경이 필요하면 전달받은 이 함수를 실행해서 변경
- 기능이 복잡해지고 트리가 복잡해지고 state도 형제 뿐만 아니라 아주 깊은 하단의 자식에서도 사용 및 변경해야 한다면 props를 계속 단계별로 전달해야 한다. 그럼 중간의 컴포넌트에서는 거기서는 사용하지 않지만 단순히 자식에게 전달하기 위해서 의미없이 받게 된다. 이런것들을 해결하기 위해서 Context & Redux or Mobx (상태관리를 위한 도구, Context가 최적화됨)
usage
React.createContext(defaultValue)
통해Context
생성 -> Context도 ReactComponent<Context.Provider value={contextValue}>
를 통해 context 주입- 하위(깊이 상관없이) 어느 component에서라도
useContext(Context)
통해contextValue
획득가능
Context.Provider
에 값 변경이 있을 때 useContext(Context)
사용한 모든 component는 re-render 발생
- useContext에서 listen하고 있는 컴포넌트의 변경이 생길 때 re-render
Context를 너무 빈번하게 사용하면 re-render가 자주 일어나기 때문에 최적화가 필요한데 최적화를 직접 적용하기 보다는 Redux or Mobx 사용
Global 하게 사용할 수 있는 곳에서 Context를 활용하는게 좋음
@09-context
// AS-IS
function App() {
return <Toolbar theme="green" />;
}
// 내부에서 전혀 theme를 사용하지 않고 있음
// ThemedButton을 위해 단순 전달
function Toolbar({ theme }) {
// The Toolbar component must take an extra "theme" prop
// and pass it to the ThemedButton. This can become painful
// if every single button in the app needs to know the theme
// because it would have to be passed through all components.
return (
<div>
<ThemedButton theme={theme} />
</div>
);
}
const ThemedButton = ({ theme }) => (
<button className={`button-${theme}`}>
button
</button>
)
export default App;
// TO-BE
import { createContext, useContext } from 'react';
// green으로 초기화
const ThemeContext = createContext('green');
const useThemeContext = () => {
const theme = useContext(ThemeContext) // value 값
return theme
}
function App() {
return (
<ThemeContext.Provider value="blue">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
const ThemedButton = () => {
const theme = useThemeContext()
return (
<button className={`button-${theme}`}>
button
</button>
)
}
export default App;
visibility + opacity에 transition
이슈
- 모달/팝업 열고 닫는 fadeIn/fadeOut 애니메이션 적용시 주의점? 적어봄
Animatable
Animatable
속성을 갖고 있는 해당 CSS 속성들은 animation과 transition 적용 가능
display, visibility, opacity, pointer-events
can make thing invisible | can make thing unclickable | removes from doc flow | can be transitioned | can be reversed on child | |
---|---|---|---|---|---|
opacity | yes | no | no | yes | no |
visibility | yes | yes | no | yes | yes |
display | yes | yes | yes | no | no |
pointer-events | no | yes | no | no | no |
display
는 렌더 트리에서 해당 요소를 없애버리기 때문에transition
과 같이 두 상태에서의 속성 변화를 일정 시간에 거쳐서 일어나도록 하지 못함 (시작점이 없음)
-opacity
는 숨겨져도 여전히 공간을 차지하고 있고 클릭 가능하기 때문에 이벤트 발생 가능
visibility + opacity 사용한 fadeIn/fadeOut
- opacity만으로 transition을 적용하면 팝업이 보이지 않을 때도 공간 점유 & 영역 클릭이 가능하기 때문에 이벤트가 발생함
- transition까지 사용해서 클릭 발생하지 못하도록 해야함
HTML
<button id="openModal" onclick="clickOpenModal()">OPEN MODAL</button>
<div id="modal" class="">...</div>
CSS
#modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.2);
opacity: 0;
visibility: hidden;
transition: visibility 0.2s, opacity 0.2s;
}
#modal.visible {
opacity: 1;
visibility: visible;
}
JS
function toggleModal() {
modal.classList.toggle('visible');
}
[React-Basic-Hooks] 1. React
React?
- Declarative(선언형) 프로그래밍 <-> 명령형 프로그래밍 (C, Java...)
- 목표를 명시하고 알고리즘을 명시하지 않음
- 함수형 프로그래밍 언어로 쓰이는 경우 '선언형'
- Hooks도 함수형 프로그래밍을 잘 쓰게 하기 위한 부분이 큼
- Component-Based
- SPA는 거의 Component-Based
- 재사용성 극대화
- template이 아닌 JS로 view를 그림
- Learn Once, Write Anywhere
- SSR 지원 -> 그전에는 DOM 기반으로 라이브러리가 동작했다면 Node 서버에서 React 코드가 돌아감, JS Object로 Virtual DOM 트리를 만들어 사용하니 Node에서 돌아갈 수 있고 그걸 통해서 클라이언트 말고도 SSR 지원 가능해짐
- React Native : 웹 말고도 Native 개발도 지원
// data가 전달되었을 때 이 data를 어떤 형태로 그릴거냐 (어떻게 X)
f(data) = View
What is React?
- view에만 집중
- 상태 관리를 더 하고싶다면 redux, mobx 사용
- 라우터는 react-router
- 여러가지를 조합해서 사용할 수 있는 장점이 있을 수도 있음
- JS로 view를 그리기 때문에 data 전달이나 js 표현식을 자유롭게 그릴 수 있음
- React는 framework가 아닌 view에 집중하는 라이브러리다
- React는 UI를 만든다
- SPA가 커지면서 FE로 MVC, MVVM 모델이 내려오기 시작. FE도 이제 라우팅, 비지니스 로직, view 를 세분화할 필요가 생겼고 React는 이중에서 view를 집중
URI 인코딩
encodeURIComponent(str)
- URI로 데이터를 전달하기 위해 문자열을 UTF-8로 인코딩하는 함수
- 주어진 문자열을 URI 구성요소로서 인코딩한 새로운 문자열로 반환
- 하나, 둘, 셋 또는 네 개의 연속된 이스케이프 문자로 나타냄
A-Z a-z 0-9 - _ . ! ~ * ' ( )
문자를 제외한 문자를 이스케이프함
- 상위-하위 쌍을 이루지 않은 단일 문자를 인코딩 시도하면 URIError 발생
encodeURIComponent('\uD800')
URI ?
URI(Uniform Resource Identifier)는 하나의 리소스를 가리키는 문자열
가장 흔한 URI로는 URL로, 웹 상에서의 위치로 리소스를 식별하며 프로토콜을 포함 (웹 상에 있는 자료의 id이다?)
URI가 URL보다 더 포괄적인 개념
이스케이프 ?
예를 들어, http://a.com?name=egoing&job=programmer
에서 &job=programmer
중 &
는 하나의 파리미터가 끝나고 다음 파라미터가 온다는 의미
그런데 job의 값에 &가 포함되어 버리면 job의 값을 제대로 인식할 수 없다.
그래서 이런 문제를 회피하기 위해 다음과 같이 치환
http://a.com?name=egoing&job=programmer%26blogger
그럼 시스템에서는 %26
을 &
로 해석하고 의도대로 job을 해석할 수 있게 된다.
이러한 처리를 이스케이핑(escaping)
이라고 한다.
encodeURI() vs encodeURIComponent() ?
encodeURI() : 특정 문자를 escape 할 때
encodeURIComponent() : 인터넷 주소 전체를 인코딩할 때
NetworkInformation API
NetworkInformation API #
- device가 네트워크와 통신하는데 사용한 연결에 대한 정보 제공
- 연결 유형이 변경될 경우 그에 대한 정보를 알 수 있음
navigator.connection
객체로 확인 가능
properties
NetworkInformation.type
- device가 네트워크와 통신하는데 사용한 연결 유형을
bluetooth
,cellular
,ethernet
,none
,wifi
,wimax
,other
,unknown
중 하나를 반환
NetworkInformation.effectiveType
- 연결된 네트워크 상황에 따라
slow-2g
,2g
,3g
,4g
중 하나를 반환 - 가장 최근 네트워크 통신에서의 round-trip 값과 downlink 값을 조합하여 판단
NetworkInformation.rtt
- rount-trip 추정치
NetworkInformation.downlink
- 대역폭 추정치
NetworkInformation.saveData
- 사용자가
배터리 절약 모드
를 사용하고 있는지 여부
Event Handler
onchange
- connection 객체의 change 이벤트 핸들러
// Browser Support
const connection = navigator.connection
|| navigator.mozConnection
|| navigator.webkitConnection;
function updateConnectionStatus() {
alert("Connection bandwidth: " + connection.effectiveType + " MB/s");
}
connection.addEventListener("change", updateConnectionStatus);
주의점
그 외 네이버앱의 경우 ?
- 네이버앱의 경우
naverappapi
에서 제공하는 function 호출로 네트워크 환경 체크가 가능 - 브검 가이드 참고
Safari pointer-events bug
이슈
이슈 재현 환경 : PC, iPad Safari
- display로 노출시 동영상 템플릿의 우측 상단 버튼 클릭이 안됨 -> 노출 방식을
display
에서opacity
로 변경 + 미노출일 때도 영역 차지시 클릭 방지를 위해pointer-events
로 제어 추가 - Safari에서
pointer-events
bug가 있어서 개발에서 미노출 영역touchend
이벤트 시e.preventDefault()
로 이동 막음
1. display bug ???
/* AS-IS */
.__video_active, .__video_dragging, .__video_ended, .__video_not_played {
& ~ .btn_area {
display: block;
}
}
의심 1 : z-index가 video의 다른 딤드 overlay 등으로 아래에 위치한 것은 아닐까? -> 구조상 그럴 수 없고, z-index를 9999 important로 적용해도 동일
의심2 : 클래스 조건과 무관하게 항상 버튼 노출되도록 테스트했을 때는 Safari에서 엔드 페이지로 이동 잘 됨
그렇다면 클래스 조건으로 노출 처리하는것 이외에 추가적인 제어가 필요한게 아닐까?
다른 툴바, 해상도 버튼과 함께 노출된 버튼에 커서 or 터치하면 사라짐 (__video_active 클래스가 제거되어버림)
대신 해상도 버튼에 커서 or 터치하면 버튼 그대로 노출됨 (UVP에서 개발로 추가적인 제어를 한 듯?)
그래서 버튼:hover, 버튼:focus시에도 block 유지되도록 수정했지만 동일 (PC 클릭만 적용되고 모바일 터치에는 적용 안되나 싶고, 개발로 영역 유지되도록은 테스트 안해봄)
Property | occupies space | consumes clicks |
---|---|---|
opacity: 0 | O | O |
visibility: hidden | O | X |
display: none | X | X |
display
는 none시 영역을 아예 없애버리니, 그럼 visiblity
나 opacity
방식으로 변경해봄 (UVP와 동일한 방식)
visibility는 display와 동일하게 Safari에서 클릭이 안됨
opacity
를 사용하면 미노출시에도 영역은 차지하고 클릭이 가능하기 때문에 pointer-events
로 none했다가 auto로 변경했지만 또 Safari에서 클릭 안됨 (pointer-events: none을 제거했을 때는 잘됨...)
결론
정확히 Safari에서 display bug가 있는지는 잘 모르겠음 (검색해도 단순 속성이 적용이 안된다여서 내 케이스와는 다른 듯)
display로 노출/미노출 처리는 잘 되어 보임
하지만 버튼 클릭 or 터치시 사라지는 것을 보면, display만으로는 부족하고 추가적인 제어가 필요한게 아닐까 싶음
2. pointer-events / z-index bug
pointer-events
https://css-tricks.com/almanac/properties/p/pointer-events/
pointer-events
속성은 HTML 요소들의 마우스/터치 이벤트들(CSS
:hover, :active, JS
click, tab, cursor drag etc)의 응답 조정 가능
opacity가 0이어도 영역은 차지하고 클릭도 가능하므로 pointer-events: none
으로 클릭 불가능하도록 설정하려 했으나
https://mikepk.com/2020/10/iOS-safari-scribble-bug/
Safari iOS14에서 PointerEvent bug가 있음
pointer-events css로 적용하고 있지만, 발생하는 PointerEvents 자체를 Safari에서 못잡는 이슈가 있는 듯?
z-index
opacity: 0일 때, z-index: -1로도 클릭하지 못하도록 했으나
역시나 Safari에서만 동작하지 않음
결론
개발에서 미노출 영역 touchend
이벤트 시 e.preventDefault()
로 이동 막음
하지만
영상 재생 중, 버튼이 노출될 영역을 클릭 or 터치시 버튼, 해상도 설정, 툴바를 포함한 전체적인 overlay가 노출되지 않음
미노출 영역 터치시, 영역은 차지하고 있으니 링크 터치 이벤트 자체가 발생하고 -> preventDefault()로 링크 이동을 막아버려서
아마도 플레이어의 화면 터치시 overlay 노출
과 같은 이벤트 처리를 그 이후에 처리하지 못해서 그런게 아닐까 싶다.
[React-Basic-Hooks] 3. JSX(React Element) 이해
JSX(React Element) 이해
- React element로 어떻게 변환되는지 확인 가능 #playground
- jsx는 React Element(html 형태를 한 js 객체)
const hello = <h1 className='greeting'>Hello World!</h1>;
var hello = React.createElement(
'h1',
{ className: 'greeting' },
'Hello World!'
);
태그 규칙
- 기본 html 태그는 소문자로 시작
- 사용자 정의 React Component는 대문자로 시작
- html과 달리 모든 태그는 children을 가지지 않는다면 self-closing 가능 (html 태그는 img, br 등 정해진 태그만 self-closing 가능)
- 자식 태그 가질 수 있음
- 태그명에 dot notation 사용 가능 (객체기 때문에 객체처럼 사용 가능)
표현식 사용
- js 표현식에 들어가야 하는 부분에 {} 넣고 사용 가능 (모양은 선언형이어서 html 비슷하게 작성되어 있지만 js처럼 사용 가능)
- js 표현식 자유롭게 사용 가능 (함수 호출식, 삼항 연산, 문자열, 배열 etc)
- js 변수에 담을 수 있으면 js 표현식으로 사용 가능 (for, while 등처럼 변수에 담을 수 없으면 X)
attribute
- key={value} 형태로 전달 (value는 표현식이어서 {}로 전달 가능, 단 value가 string이면 '' or "" 사용 가능, classname 경우)
- if, for, while 등의 제어는 jsx 밖에서 사용해야함
- XSS(cross-site-scripting) 기본으로 대응됨 (script를 innerHTML로 넣으면 script로 동작하는게 아니라 html 인코딩해서 동작)
- 실제 html DOM과 다른 attribute (기본으로 들어가는 key값이 변환됨) #
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}! // js 사용하는 곳은 {} 사용
</h1>
);
ReactDOM.render(
element,
document.getElementById('root')
);
주의 사항
- React 17 이전 버전 사용시, JSX 코드 scope 안에 React 선언 필요 #New JSX Trasform
- 17 버전부터는 React에 scope로 넣지 않아도 컴파일러가 자동으로 React를 Import해서 넣어줌 -> 번들 사이즈 줄어드는 장점
- 실제 DOM은 아니다.(Virtual DOM)
- DOM이 아닌 JS 객체
- JS 객체니까 JS가 실행되는 환경인 Node.js에서도 이것저것 활용 가능 -> SSR
- Single parent
02-jsx/fragment
- JSX 표현식은 반드시 1개의 부모 element를 가져야함
- 의미 없는 div로 감싸는 고전적인 방식 -> 의미 없는 태그가 사용된다는 단점
- React에서
Fragment
제공.<Fragment>...</Fragment>
대신 축약어로<>...</>
로 대체 가능
// React가 보기에는 <b>짝수</b>와 string 입니다.도 하나의 객체여서 Fragment로 감싸야함 const app = ( <> <div>안녕</div> {true && ( <> <b>짝수</b> 입니다. </> )} </> );
- white space
02-jsx/whitespace
- 표현식으로 넣은 string을 제외하고는 React element 렌더링시 모두 공백은 trim해줌 (실제로 HTML에서 그려줄 때랑 또 다를 수 있으니 확인 필요)
<div>Hello World</div> <div> Hello World </div> <div> Hello World </div> <div> Hello World </div> <div> {' Hello World! '} {'test'} </div> export default () => ( <> {whiteSpaceTest.map((element, i) => <p key={i}>{ReactDOMServer.renderToStaticMarkup(element)}</p>)} </> )
- conditional jsx
02-jsx/conditional
const elements = { hello: <h1>Hello!</h1>, bye: <h1>Good Bye!</h1>, }; function getElementByType(type) { return elements[type]; } export default () => getElementByType('hello'); // 태그를 변수에 담고, 그 변수를 태그로 사용 const AOrDiv = true ? 'a' : 'div'; export default () => <AOrDiv>안녕?</AOrDiv>; // <a>안녕?</a> // 조건 분기 처리 function EvenOdd({ number }) { const isEven = number % 2 === 0; return ( <div> {isEven ? even : odd} </div> ); } export default () => <EvenOdd number={4} />;
[React-Basic-Hooks] 7. React Hooks
React Hooks #
소개: React Conf 2018
release: React v16.8
What are Hooks
- class Component에서 functional Component로 넘어갈 수 있게 된 이유
- functional Component에서 stateful logic을 사용할 수 있게 해주는 함수 모음.
- component에서 stateful logic을 분리하여 component는 view render에만 집중할 수 있게 함.
[참고] Motivation #
- 기존 문제점
- 컴포넌트 사이에서 state 로직을 재사용하기 어려움
- render props or higher-order 컴포넌트(HOC : 컴포넌트를 다른 컴포넌트로 wrapping)같은 패턴 사용해서 이 어려움을 해결하고 있었음 -> 문법이 어려워져 보기 힘듦
- class 컴포넌트 내부에 있는 생명주기(Lifecycle) 함수들 기반으로 나뉘어져 있고, 그 내부에 관련 없는 로직이 자주 들어가게 되면서 로직이 한눈에 잘 들어오지 않음
- state 관리하는 로직이 흩어져서 관리하기 힘듦
- JS의 Class의 개념 자체가 헷갈리게 만듦(특히 this)
- 컴포넌트 사이에서 state 로직을 재사용하기 어려움
props
- top-down으로 부모 컴포넌트로부터 전달되는 값, props에 의해 view가 결정됨
- props는 수정해서는 안됨
- React 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 함
// 순수 함수 : 입력값을 바꾸지 않고 항상 동일한 입력값에 대해 동일한 결과 반환
function sum(a, b) {
return a + b;
}
// 입력값을 변경하기 때문에 순수 함수가 아님
function withdraw(account, amount) {
account.total -= amount;
}
state
- 사용자의 동작에 따른 업데이트를 위해 컴포넌트 내부에서 변경을 위해 추적하는 값을 담은 변수
📌 State Hook
- 그동안은 UI 렌더링에 관해서 알아봤다면 Hook으로 컴포넌트의 동작에 대해 더 알 수 있음
// useState : array로 반환
// state : state 값, setState : state를 update 할 수 있는 함수
const [state, setState] = useState(initialState);
// update 할 newState를 전달해서 state update
setState(newState);
setState(newState)
호출 되면 state update가 되고 component가 re-render 된다.state: any
: number, string, array, object...- 한 component에서
useState
여러번 사용해도 됨- state를 여러 개 관리 가능
React가 화면을 update하는 3가지 방법
- props 변경
- state 변경
- context 변경
@06-hooks/state
import { useState} from 'react';
function Count() {
// initialState : 0
const [count, setCount] = useState(0);
const increase = () => {
setCount(count + 1);
}
return (
<div className="set-state">
<div>Current Count: {count}</div>
<p>
<button onClick={increase}>increase</button>
</p>
</div>
)
}
Functional updates
// 값이 아니라 함수 전달
setState((previousState) => nextState)
setState()
에 update 함수를 전달할 수 있음- 한 번의 render에 여러번
setState()
하는 경우 반드시 사용해야함- setState가 여러번 발생하면 react는 마지막 setState한 값으로 override 됨
- 함수로 전달하면 체이닝으로 연결하기 때문에 이전 state 값을 기준으로 다음 state 값을 계산
- 함수로 전달하지 않으면
setState()
호출로 기존 state는 대체 됨 (not merge)
@06-hooks/functional-state-update
const [count, setCount] = useState(0);
// 1. 만약 중간에 로직이 복잡해서 여러번 setState()를 호출하게 되는 경우 (억지로 만든 예시)
// 2. 컴포넌트가 render가 될 때, state 값을 매번 가져오는데
// setCount에서의 count는 컴포넌트 내의 count state를 바라봐서 마지막 state으로만 override 됨
const increase = () => {
setCount(count + 1);
setCount(count + 1);
};
// 3. 함수가 체이닝으로 이어지기 때문에 컴포넌트 내의 state 값을 가져오는게 아니라 setCount가 알고 있는 이전 값을 가져올 수 있음
const increaseFunctional = () => {
setCount(prevCount => { prevCount + 1 });
setCount(prevCount => { prevCount + 1 });
};
// 클래스 컴포넌트의 setState 메서드와는 다르게, useState는 갱신 객체(update objects)를 자동으로 합치지는 않음
// 함수 업데이터 폼을 객체 전개 연산자와 결합함으로써 이 동작을 복제 가능
const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
- Lazy initial state
- 계산 비용이 비싼 로직을 initial render에 한 번만 호출
// Component를 맨 처음 render 할 때 1번만 실행됨
// 그 다음 setState가 호출되면 Component가 매번 호출되지만, 넘긴 함수는 memoization이 되어서 다시 실행 X
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
@06-hooks/lazy-initial-state
function someExpensiveComputation(props) {
console.log('expensive!!!!!!!!!!!');
const initialState = 0;
return initialState;
}
function LazyInitialState(props) {
// AS-IS
const initialState = someExpensiveComputation(props);
const [state, setState] = useState(initialState);
// TO-BE
const [state, setState] = useState(() => someExpensiveComputation(props));
return (
<div>
Current Count: {state}
<p>
<button onClick={() => setState(state + 1)}>
UPDATE
</button>
</p>
</div>
);
}
- state 값 변경은 Object.is 비교.
- state가 변경이 안되면 render 다시 하지 않음
- Object.is 로 object 비교는 reference 비교로 shallow 비교
- component 내부 객체/배열 생성은 매번 다른 reference를 갖기 때문에 다른 state로 인식 함 (실행될 때마다 component가 다시 불리는데 그때마다 내부 변수의 reference가 매번 달라짐)
- 이전에 불린 setState와 다음에 불린 setState에 전달된 값을 비교하게 되는데, 같은 state면 re-render 되지 않고 다르면 re-render 때문에 dom이 불필요하게 update 될 수도 있음. 주의 !!!
- 동일 state로 업데이트 되면 render 되지 않음
⚡️ Effect hook
Effect?
Side effect (함수형 컴포넌트가 render외에 하는 다른 일)
- Data fetching
- Setting up a subscription(eg. eventHandler)
- Manually changing the DOM
- timers, logging...
React's purely functional world => imperative world (명령형)
- 함수형이 아닌 명령형으로..
useEffect
// didUpdate : 컴포넌트가 업데이트 된 다음에 실행되는 함수
// dependency : 이 값이 변경이 될 때만 effect 실행해달라 (최적화 연관), 없으면 비교대상이 없으니 항상 didUpdate (지양)
useEffect(didUpdate, [dependency]);
- component render가 완료된 후에 실행
- dependency 추가하여 특정 값이 변경된 경우에만 실행하도록 설정 가능
- dependency 없는 경우 render 후에 항상 실행
- 빈 배열로 넘기면 변경 대상이 없기 때문에 component에서 딱 1번만 실행되어야 하는 경우도 useEffect에서 처리 가능
- 왜 component 안에서 호출될까?
- props, state 접근성 - 동일 scope
- props와 state를 접근하기에 같은 scope 안에 있기 때문에 함수 컴포넌트 안에서 호출하고 있음
useEffect
호출은 browser paint를 block 하지 않음(render를 먼저 하고 useEffect 실행) => 반응성 향상(UI를 먼저 렌더링하고 기능은 그 다음)- paint block 필요한 경우(컴포넌트가 render 되기 전에 반영이 되어야 하는 동기적인 경우)
useLayoutEffect
사용 (useEffect 사용시 깜빡이는 현상이 있는 경우 사용)
- paint block 필요한 경우(컴포넌트가 render 되기 전에 반영이 되어야 하는 동기적인 경우)
- 🧹cleanup effect!
- component가 unmount가 될 때, 이전에 등록해둔 effect를 cleanup
useEffect()
의 return으로 cleanup function 전달 가능- 다음
useEffect()
실행 전에 이전useEffect()
의 cleanup function 호출 됨 - component unmount 될 때도 호출 (unmount 될 때 처리되어야 하는 코드일 경우 여기서 처리)
- 관심 주제에 따라
useEffect
를 나누어서 사용 추천- 관심 주제가 여러개면 여러개의 useEffect를 사용하는 것이 유지보수에 좋음
- dependency를 가지기 때문에 굳이 실행되어야 하지 않는 로직이 다른 dependency 때문에 실행되면 안되니 분리해서 관리 추천
@06-hooks/effect
unction Example() {
console.log('1. component render start!');
const [count, setCount] = useState(0);
const [toggleCounter, setToggleCounter] = useState(true);
// render 된 다음에 useEffect로 넘긴 didUpdate 함수가 실행됨
// state가 변경되면서 전체적으로 useEffect와 render가 매번 실행됨
// dependency를 count로 전달해서 count state가 변경될 때만 실행되도록
// useEffect는 if, for 문 등에 들어가면 안되고 항상 상위에 위치해야하는 React Rule
// - 안지키면 React Hook은 항상 반드시 같은 순서로 실행이 보장되어야 한다 라는 오류가 뜸
useEffect(() => {
// if (count < 10) 일때 useEffect를 감싸는 구조 X, useEffect 안에서 조건문 처리 해야함
console.log('3. effect start!');
if (count > 10) return undefined;
// 직접 dom을 제어하는 부분이라 useEffect에서 처리
document.title = `You clicked ${count} times`;
console.log('title updated');
// 초기화 함수
// useEffect 실행하기 전에 이전 컴포넌트에서 등록된 useEffect의 clear 함수가 실행됨
return () => {
console.log('4. effect clear!');
document.title = 'clear!';
}
}, [count]);
// React Component는 Virtaul DOM만을 제어하고 실제 DOM을 제어하는 건 side effect로 판단되어 useEffect에 처리
console.log('2. Virtual DOM render!');
return (
<div>
<button onClick={() => setToggleCounter((prev) => !prev)}>
Toggle counter
</button>
{toggleCounter && (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
)}
</div>
);
}
@06-hooks/clock
const Clock = () => {
const [time, setTime] = useState(() => new Date().toLocaleTimeString());
useEffect(() => {
const timer = setTimeout(() => setTime(new Date().toLocaleTimeString()), 1000);
// clearTimeout 해주지 않으면 Clock 컴포넌트가 빠지는 경우(뒤로가기) 1초 뒤에 실행이 될 때 이미 unmount된 컴포넌트에 state를 접근하는 경우 에러 발생
return(() => {clearTimeout(timer)});
}, [time]);
render(
<div>
<h1>current time: {time}</h1>
</div>
)
};
✌️ Rules of Hooks
- Only Call Hooks at the Top Level
- if, for 문 등에 포함되면 안됨, Hook이 실행이 안될수도 있음
- Hook에 등록한 useState, useEffect가 함수형 컴포넌트 외에 Hook만을 관리하는 별도 메모리에 배열로 저장되는 구조
- 매번 render가 다시 되더라도 등록한 Hook이 유지됨
- Hook은 예외처리가 되면 안되고 top level에서 호출되어야 배열 안에서 순서가 꼬이지 않음
- Only Call Hooks from React Functions
[필수] eslint-plugin-react-hooks 추가하여 lint 도움 받기
[참고] lint 도움을 받기 위해 custom hook 들은 use
prefix를 붙인다.
💡 Building Your Own Hooks
- useState, useEffect 조합해서 재사용될 수 있는 Hook 생성 가능
custom hooks
이런 경우 사용
- stateful logic 재사용
- class형 컴포넌트에서는 HOC, render props로 지원
- component 가독성을 위해
- Hook이 길어지면 상단을 많이 차지하게 됨
@06-hooks/fetch/data-fetch
import { useState, useEffect } from 'react';
import fetch from './fetch';
import mock from './mockData';
const usersApi = 'https://jsonplaceholder.typicode.com/users';
// Custom Hook
// 배열을 반환하는 fetch일 경우 이 Hook 재사용 가능
const useArrayFetch = () => {
const [data, setData] = useState([]);
const hasData = data.length > 0;
useEffect(() => {
// useEffect는 async를 지원하지 않기 때문에 아래처럼 즉시 실행하는 형태로 사용
(async () => {
// 비동기 처리인 fetch는 useEffect에서 주로 처리
const fetchedData = await fetch(usersApi);
setData(fetchedData);
})();
}, []); // 빈 배열 넘겨서 컴포넌트가 mount될 때 1번만 호출됨
return { data, hasData };
};
const Users = () => {
// 이 Users 컴포넌트는 View render만 집중, 보통 Custom Hook은 다른 파일에 따로 보관
const { data, hasData } = useArrayFetch();
return (
<div>
{/* 사용자 친화적 UX */}
{!hasData && (<div> Loading... </div>)}
{hasData && data.map(user => {
const { id, name, email } = user;
return (
<p key={id}>{name} : <i>{email}</i></p>
)
})}
</div>
)
}
Hooks API #
Basic Hooks
- useState
const [state, setState] = useState(initialState);
setState(newState); // 컴포넌트가 re-render
- useEffect
// sideEffect가 발생하는 경우 사용
// react의 pure function에서 넘어가서 imperative 영역으로 넘어가는 경우 사용 (fetch, timer, logging, dom 제거 etc)
// class 컴포넌트의 didUpdate의 기능과 비슷
// dependency를 넣으면 각각 이전 값과 비교해서(값들은 값 비교, array나 object같은 reference를 가진 객체는 reference 비교, Object.is 비교) 바뀐 경우만 update
useEffect(didUpdate, [dependency]);
- useContext
const value = useContext(MyContext);
Additional Hooks
- useReducer
Redux
와 비슷한 형태로 사용- state가 복잡한 형태의 object 일때 사용
const [state, dispatch] = useReducer(reducer, initialArg, init);
- useCallback
- memoization, 최적화 용도로 사용
- render가 일어나면 컴포넌트가 계속 호출되니 내부에 선언된 함수가 매번 render됨 -> 그 함수의 reference가 render될 때마다 변경되는데 이 함수를 children으로 props로 전달된다면 매번 변경되니 매번 re-render 되어버림 or useEffect의 dependency로 들어갈 경우 Object.is 비교로 reference가 변경되었기 때문에 여기도 re-render 되어버림
- 이런 경우를 피하기 위해서 전달한 함수는 전달한 dependency가 변경될 때만 실행한다
// 함수를 저장
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
- useMemo
- memoization, 최적화 용도로 사용
// 값(함수의 반환값) 자체를 저장
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- useRef
- DOM 접근에 사용
- render에 영향을 주지 않는 mutable data 관리에 편리
- 컴포넌트가 re-render되어도 유지되어야 하는 값이 있어야 함 (but 그 값이 화면을 render하는 곳에는 사용되지 않는, 컴포넌트의 instance 변수?)
const refContainer = useRef(initialValue);
const value = refContainer.current
- useImperativeHandle
- 상위 component에서 전달하는 ref에 담을 값을 정한다.
forwardRef
와 같이 사용
useImperativeHandle(ref, createHandle, [deps])
- useLayoutEffect
- DOM 변경 후 paint 전에 호출 됨 (useEffect는 paint 후에 호출됨)
- browser paint를 block 하기 때문에 가능하면 사용하지 않는 것이 좋음
- 기존에 useEffect의 css 변경 로직으로 인해 render 후에 호출되는 경우 깜빡이는 경우를 방지하기 위해서 사용
useLayoutEffect(didUpdate, [dependency]);
- useDebugValue
useDebugValue(value);
Hooks Summary
- useState: Persist value between renders, trigger re-render
- render되더라도 유지되는 값
- 변경시에 화면이 변경되어야 한다면 사용
- useRef: Persist value between renders, no re-render
- render되더라도 유지되는 값
- 화면 render에 관여하지 않는 값, ref값이 변경되어도 화면이 갱신되지 않을 때 사용
- useEffect: Side effects that run after render
- render가 일어나면 일어나는 side effect
- useReducer: useState in reducer pattern
- useMemo: Memoize value between renders
- 매번 계산하기 복잡한 로직일 경우 사용
- useCallback: Persist ref equality between renders
- 함수를 memoization해서 ref를 유지해야 하는 경우 사용
When to useMemo and useCallback #
- 최적화는 비용이 큰 render를 적게하는 대신에 메모리를 좀 더 쓰는 경우가 있음
- 굳이 useMemo or useCallback을 사용하지 않아도 되는 경우를 풀어낸 글
- useState, useEffect는 결국 컴포넌트 외에 따로 메모리 영역을 잡아서 유지하는 ? 그런 구조 -> 그렇기 때문에 너무 많은 컴포넌트에서 사용되는 경우 각각의 함수 영역을 하나씩 메모리에서 차지하기 때문에 정말 필요한 경우만 사용하는게 좋다. 너무 비용이 클 때만 최적화해라
- 섣부른 성능 최적화는 오히려 성능 저하를 발생시킬 수 있기 때문에 성능 이슈가 발생했을 때 진행 추천
- Referential(성능) equality(최적화)
- Computationally expensive calculations
📝 props vs. state
React Component가 render 되는 때/시점
dom을 실제로 update하는 건 react이기 때문에 react가 언제 dom을 update하는지 그 시점을 잘 알고 있으면 좋음
- On mount : 제일 처음에 컴포넌트가 그려질 때
- props/state 변경
- context 변경 : context가 변경이 되어도 listen하고 있으면 변경이 됨
props와 state 구별
- canICalculateItFromProps() : 부모로부터 받은 props를 조합해서 나타낼 수 있는 값이냐? -> 굳이 state 사용할 필요 X
- !amIUsingItInRenderMethod() : react 컴포넌트의 return 영역인 virtual dom에 사용되는 값이냐? -> state 사용 X (ref 사용 O)
- 화면 update 해야하는데 값도 변경 되어야 하고 view에도 연관이 있냐? -> state 사용 O
[React JS 마스터클래스] Chapter #3 Typescript
📌 Why use Typescript ?
- Typescript : Javascript 베이스 언어이며 strong-typed 언어
stronly-typed 언어 : 언어가 작동하기 전에 type을 확인
// AS-IS
const plus = (a, b) => a + b;
plus(2, "hi"); // '2hi'
// TO-BE
const plus = (a:number, b:number) => a + b;
plus(2, "hi"); // 오류 발생
- Javascript는 데이터 타입을 1도 신경쓰지 않음
- 결과를 내뱉기전에 이런 결과가 나오기전에 a와 b는 number type이라는걸 Javascript에게 알려줘야해 -> Typescript
const user = {
firstName: "Youjin",
lastName: "park"
};
console.log(user.name); // undefined
- user가 갖고 있지 않은 name을 호출해서 Javascript는 그저 undefined를 반환할 뿐임, 지 할일만 함
- Typescript를 사용하면 프로그램을 돌리기도 전에 name 속성은 해당 type에 존재하지 않는다는 오류로 미리 알려줄 수 있음
- 이렇게 Typescript는 하나의 안전장치가 되어줄 수 있음
// Typescript (Before Compile)
const plus = (a:number, b:number) => a + b;
// Javascript (After Compile)
const plus = (a, b) => a + b;
- 브라우저는 Javascript 언어만을 이해 가능
- 브라우저가 Typescript를 읽기 위해서 컴파일해서 우리가 아는 normal Javascript로 변환
- 고로 Typescript의 안전장치는 코드가 동작하기
전
에 발생한다는 것 !!!
📌 Install (on CRA project)
1. typescript용 cra 생성
npx create-react-app my-app --template typescript
# or
yarn create react-app my-app --template typescript
2. 이미 javascript로 생성된 cra에 typescript 설치
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
# or
yarn add typescript @types/node @types/react @types/react-dom @types/jest
- 위처럼 typescript 설치 후,
App.js
와index.js
등 모든 js 파일 확장자를.tsx
로 변환 (React + Typescript는tsx
, Typescript는ts
)src
하위에react-app-env.d.ts
파일 생성 후 아래와 같이 작성
/// <reference types="react-scripts" />
tsconfig.json
파일 생성- Javascript 기반으로 만들어진 패키지는 Typescript용 패키지로 변경해서 설치
- Typescript 환경에서 Javascript 기반 패키지를 가져와서 사용하면 Typescript는 이 코드가 뭔지 몰라서 오류 발생
@types
: 패키지의 type definition을 알려줌 (원래 패키지를 보고 Typescript에게 해줄 설명 모음집..?)@types
네임스페이스는 DefinitelyTyped 커뮤니티의 types로부터 파생 (여기에 없다면 직접 만들어야함)@types/styled-components
는 styled-components의 type 정보를 알려주는 패키지- Typescript에게 내가 사용할 패키지의 코드가 뭐가 뭔지 알려주기 위함
📌 Typing the Props
- 컴포넌트가 필요로 하는 prop을 Typescript에게 설명하는 법
- 기존에 사용하던 propTypes는 브라우저에 콘솔로 경고 표시를 해줌, prop이 거기에 있는지 확인해주지만 코드가 실행된
후
에야 확인 가능 - Typescript로는 의도한 prop이 제대로 전달되었는지 코드가 실행되기
전
에 확인 가능
// App.tsx
import Circle from "./Circle";
function App() {
return (
<div>
<Circle bgColor="teal" />
<Circle bgColor="tomato" />
</div>
);
}
// Circle.tsx
// 예제에서는 Container와 Circle이 다른 종류의 props를 사용하게될거라는 가정 하에 같은 종류의 interface지만 2개를 각각 생성함
interface ContainerProps {
bgColor: string;
}
const Container = styled.div<ContainerProps>`
width: 100px;
height: 100px;
background-color: ${props => props.bgColor};
border-radius: 100px;
`;
interface CircleProps {
bgColor: string;
}
function Circle({ bgColor }: CircleProps) {
return <Container bgColor={bgColor} />;
}
- interface : object가 어떻게 보일지 알려주는 설명서
- Circle 컴포넌트에 bgColor props를 보냄
- Typescript : Circle이 받는 bgColor가 뭐야?!!?!?!? 나 이런거 모름~~
- 👩🏻💻 : interface로 CircleProps라는 네이밍으로(자유) bgColor의 타입을 정의해줄게
- Circle은 bgColor를 받아서 styled 컴포넌트인 Container에 보냄
- Typescript : Container는 걍 div인데 이 bgColor는 또 뭐야?!?!? 나 얘도 모름~~
- 👩🏻💻 : Container의 bgColor prop의 타입도 정의해줄게
interface PlayerShape {
name: string;
age: string;
}
const hello = (playerObj: PlayerShape) => `Hello ${playerObj.name} you are ${playerObj.age} years old.`;
hello({ name: "youjin", age: "20" });
hello({ name: "youjin", age: 20 }); // age는 string이어야함 오류
hello({ name: "youjin", age: "20", hello: 1 }); // hello는 없는 속성 오류
- interface를 정의함으로써 전달받는 playerObj 객체의 shape의 설명서를 Typescript에게 전달 -> Typescript는 코드가 실행되기전에 이 설명서를 보고 제대로 전달되는건지 확인
- 위 Circle에서는 전달받는 props 객체의 shape를 설명
📌 Optional Props
- 위에서처럼만 작성하면 모든 props가 required이기 때문에 넘겨주지 않는 경우에는 오류 발생함
// App.tsx
function App() {
return (
<div>
<Circle bgColor="teal" borderColor="red" />
<Circle bgColor="tomato" text="hi" />
</div>
);
}
// Circle.tsx
interface ContainerProps {
bgColor: string;
borderColor?: string;
}
const Container = styled.div<ContainerProps>`
width: 100px;
height: 100px;
background-color: ${props => props.bgColor};
border-radius: 100px;
border: 1px solid ${props => props.borderColor};
`;
interface CircleProps {
bgColor: string;
borderColor?: string;
text?: string;
}
function Circle({ bgColor, borderColor, text = "default text" }: CircleProps) {
return <Container bgColor={bgColor} borderColor={borderColor ?? bgColor}>{text}</Container>;
}
- borderColor를 optional한 props로 설정하고자 함 ->
borderColor?: string
- Circle이 받는 borderColor props는 optional함
- 그럼 Container가 이어받는 bgColor props도 optional하게 설정해줘야 함
- default 값도 fallback 설정 가능 ->
borderColor={borderColor ?? bgColor}
: borderColor가 없으면 bgColor로 default 설정- 여기서 bgColor는 항상 string이기 때문에 default 값으로 설정 가능
- 아니면 Circle에서 props를 받을 때 아예 default 값 설정 가능 (ES6 특성 사용)
📌 State
const [counter, setCounter] = useState(1);
- Typescript는 useState의 default값을 보고 number 타입이 들어올거라는걸 예측함
- 그래서 Typescript를 따로 사용하지않아도 이외의 타입 값이 들어오면 오류 발생
- default값이 없으면 Typescript가 추론 할 수 없어서 undefined 타입
const [counter, setCounter] = useState<number|string>(1);
setCounter(2);
setCounter("hello");
setCounter(true); // Boolean 타입 오류 발생
- state는 보통 1개의 타입으로 계속 사용되지만, 종종 여러 타입이 사용되는 경우는 위와 같이 사용 (but state 타입이 바뀌는 경우는 드뭄)
📌 Forms
function App() {
const [value, setValue] = useState("");
// 어떤 종류의 element가 이 onChange 이벤트를 발생시킬지 특정 가능
// Typescript는 이 onChange 함수가 InputElement에 의해 실행될 것을 알 수 있음
const onChange = (event: React.FormEvent<HTMLInputElement>) => {
const {
currentTarget: { value }
} = event;
// Typescript는 onChange 이벤트가 type이 text인 input에 의해서 생성되었으며
// currentTarget의 value가 string임을 알고 있음
setValue(value);
};
// event: React.{어떤 이벤트냐}<{어떤 element가 이 이벤트를 발생시키냐}>
// 타입 시스템마다 달라서 React 이외의 다른 라이브러리를 사용한다면 방식이 달라짐 참고
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Typescript에게 event가 뭔지 설명해줌으로써 preventDefault가 함수란걸 앎 (만약 event 설명이 없다면 문법 오류가 나도 어디서 나는지 모름)
console.log(value);
};
return (
<div>
<form onSubmit={onSubmit}>
<input
type="text"
value={value}
onChange={onChange}
placeholder="username"
/>
<button>Log in</button>
</form>
</div>
);
}
any
: Typesscript의 타입으로 무엇이든 될 수 있는 타입- any는 기본적으로 Javascript가 하던 어떤 타입이던 상관없어하는 행동과 동일하기 때문에 Typescript를 사용한다면 any타입을 지양하도록 노력해야 함
- 어디에 어떤 타입이 적당한지는 잘 모르니 항상 구글링해서 적당한 타입을 찾을 것 -> React SyntheticEvent 참고
📌 Themes
- https://styled-components.com/docs/api#typescript
- 이제 styled 컴포넌트 테마와 Typescript를 연결시켜 보자
1. declarations file 생성
- 앞서서 @type/styled-components를 설치했으니 이제 declarations file 생성 (ex.
***.d.ts
) - styled.d.ts 파일 생성함으로써 styled-components 내에 기존에 있던 index.d.ts 파일을 override -> 우리 테마에 사용할 타입들을 재정의하기 위해서
// src/styled.d.ts
// import original module declarations
import "styled-components";
// and extend them!
declare module "styled-components" {
export interface DefaultTheme {
textColor: string;
bgColor: string;
}
}
2. theme.ts
- 사용할 lightTheme와 darkTheme를 위에서 interface로 타입을 정의해둔 DefaultTheme를 가져와서 정의 및 export
import { DefaultTheme } from "styled-components";
// 앞서 styled.d.ts에서 생성한 DefaultTheme interface를 정의해둬서
// light와 dark 모드에서 속성을 하나라도 빠뜨리지 않고 작성 가능
export const lightTheme: DefaultTheme = {
bgColor: "white",
textColor: "black"
};
export const darkTheme: DefaultTheme = {
bgColor: "black",
textColor: "white"
};
3. App을 ThemeProvider로 감싸기
// index.ts
import { ThemeProvider } from "styled-components";
import { lightTheme } from "./theme";
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={lightTheme}>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);
4. App에서 이제 theme.ts에 정의해둔 lightTheme or darkTheme 사용
// App.ts
import styled from "styled-components";
function App() {
const H1 = styled.h1`
color: ${props => props.theme.textColor};
`;
const Container = styled.div`
background-color: ${props => props.theme.bgColor};
`;
return (
<div>
<Container>
<H1>Hello</H1>
</Container>
</div>
);
}
visibility + opacity에 transition
이슈
- 모달/팝업 열고 닫는 fadeIn/fadeOut 애니메이션 적용시 주의점? 적어봄
Animatable
Animatable
속성을 갖고 있는 해당 CSS 속성들은 animation과 transition 적용 가능
display, visibility, opacity, pointer-events
can make thing invisible | can make thing unclickable | removes from doc flow | can be transitioned | can be reversed on child | |
---|---|---|---|---|---|
opacity | yes | no | no | yes | no |
visibility | yes | yes | no | yes | yes |
display | yes | yes | yes | no | no |
pointer-events | no | yes | no | no | no |
display
는 렌더 트리에서 해당 요소를 없애버리기 때문에transition
과 같이 두 상태에서의 속성 변화를 일정 시간에 거쳐서 일어나도록 하지 못함 (시작점이 없음)
-opacity
는 숨겨져도 여전히 공간을 차지하고 있고 클릭 가능하기 때문에 이벤트 발생 가능
visibility + opacity 사용한 fadeIn/fadeOut
- opacity만으로 transition을 적용하면 팝업이 보이지 않을 때도 공간 점유 & 영역 클릭이 가능하기 때문에 이벤트가 발생함
- transition까지 사용해서 클릭 발생하지 못하도록 해야함
HTML
<button id="openModal" onclick="clickOpenModal()">OPEN MODAL</button>
<div id="modal" class="">...</div>
CSS
#modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.2);
opacity: 0;
visibility: hidden;
transition: visibility 0.2s, opacity 0.2s;
}
#modal.visible {
opacity: 1;
visibility: visible;
}
JS
function toggleModal() {
modal.classList.toggle('visible');
}
[BABEL] Note: The code generator has deoptimised the styling of as it exceeds the max of "500KB".
[BABEL] Note: The code generator has deoptimised the styling of as it exceeds the max of "500KB".
- 스토리, 오토플레이 템플릿에서 사용중이던 코어 플레이어를 LTS 버전으로 업데이트하니 코드 양이 커져서 babel 실행시 위와 같은 경고? 문구 발생, 작업에 영향을 주는 에러는 아님
- Babel의 compact 옵션은 컴파일시에 불필요한 줄바꿈과 공백 문자를 모두 생략하도록 할 수 있는데 그 max값이 500KB (과거에는 max가 100KB)
- 그래서 이 옵션을 비활성하도록
compact: false
설정
해결 방안
- bable.rc 파일 or webpack.config 파일에서 아래와 같이 compact 옵션값을 false로 수정
query: {
...
compact: false
}
[ReactJS로 영화 웹 서비스 만들기] Chapter #1 ~ #6
📌 Why React
- React는 UI를 interative하게 함
- interative한 앱에서 하는 작업들 모두가 event들을 감지하는 일이며 React는 그걸 손쉽게 해줌
- 바닐라JS 앱 : 버튼을 생성, 버튼에 id or class 추가, 이벤트 리스너 등록, 버튼 select 작업 필요
- ReactJS 앱 : 버튼을 생성하는데 뭐가 필요한지 알고 이벤트 감지의 지름길을 만들어줌
react, react-dom
- react : UI를 interative하게 만들어주는 엔진
- react-dom : react element를 html에 두는 역할의 라이브러리
📌 VanillaJS vs ReactJS
1. 처리 방식의 차이
- VanillaJS에서는 HTML을 먼저 만들고 그걸 JS로 가져와서 HTML을 수정하는 방식
- ReactJS에서는 JS로 element를 생성하고 ReactJS가 그걸 HTML로 번역해주는 방식
- ReactJS는 생성한 element(결과물인 HTML)를 업데이트할 수 있음
- 유저에게 보여질 내용을 컨트롤 가능
- on + event 이름으로 적어야 React에서 이게 이벤트인지 앎
- Vanilla에서의
addEventListener
대신 React에서 element의 property로 event 등록 가능 - 변경시 React는 이전에 렌더링한 컴포넌트와 다음에 렌더링할 컴포넌트를 비교해서 변경된 부분만 UI update
2. createElement 대신 JSX로 더 손쉽게!
- JSX : JS를 확장한 문법
- HTML과 흡사한 문법으로 element 생성 가능
- property는 마치 HTML 태그 속성처럼 작성
- createElement로 생성할 때보다 HTML처럼 직관적인 모습으로 element 생성 가능
- 대신 브라우저가 JSX를 이해할 수 있도록
Babel
을 사용해서 다시 createElement 형태로 변환 필요 #- Babel 변환 예시
- 아래 script 태그에 Babel에 넘겨준 JS 코드가 있고, head 태그 내부에 Babel이 변환시킨 브라우저가 읽을 수 있는 코드가 위치해 있음
3. 컴포넌트 안에 다른 컴포넌트 넣기
- 전체를 감싸는 Container가 Title과 Button을 JSX를 사용해서 가져오려면?
- Title과 Button을 함수화시킨다
- 호출시
Title()
처럼 호출하지 않음. JSX만의 규칙이 있음 ! <Title />
처럼 마치 HTML 태그인양 호출해줌- 컴포넌트의 첫 글자는 반드시 대문자, 소문자면 React와 JSX는 HTML 요소로 인식함
- JSX는 어플리케이션을 여러 가지 작은 요소(Title, Button...)로 나눠서 관리할 수 있도록 해주고, 우리는 그걸 합쳐서 렌더링해줌
- 함수 형태로 HTML을 재사용 할 수 있도록 도와줌
📌 state
- 예제 : https://codesandbox.io/s/state-ybd40
- state : 변경되는 데이터가 저장되는 곳
- state가 변경되면 re-render됨
- React.useState의 modifier 함수로 state를 변경하면 변경된 data를 가진 컴포넌트가 재생성되며, 변경된 부분만 UI update
state 세팅
- state 세팅에는 2가지 방법이 있음
- 직접 값을 할당 : setState(state + 1)
- 함수 할당 : setState(state => state + 1)
- 이전 단계의 state를 이용해서 현재 state를 변경하려했지만, 결과가 예상과 다르게 나올 수 있음 (다른 곳에서 동일한 state가 사용되었다던가...)
- modifier 함수에 인자로 현재 state로 다음 state를 계산하는 함수를 넣어 다음 state를 반환받는다면 예상치 못한 update가 일어나도 혼동 방지 가능
Unit Conversion
- 예제 : https://codesandbox.io/s/unit-conversion-jnf6s
for, class...
는 JS 용어로 reserved 되었기 때문에JSX
에서 사용 불가- for -> htmlFor
- class -> className
- 사용자가 input을 변화시킬 때마다
minutes(amount)
state를 update 시키고 싶다면 ? -> change 이벤트 사용 - change 이벤트와 setMinutes 함수 둘 다 필요한 이유 !
- onChange : input 변화 리스닝해서
- setMinutes : input의 변경된 value를 state에 update & UI에 re-render
- flip 버튼
- flipped state : Minutes to Hours or Hours to Minutes 여부
- minutes와 hours input에 동일한 onChange 이벤트 리스닝으로 amount state는 계속 update
- boolean 값인 flipped(초기값은 false : Minutes to Hors)과 사용자가 입력한 amount state를 조합해서 disabled와 value로 UI 노출
Unit Conversion + select unit
📌 props
Why need props ?
- 예제 : https://codesandbox.io/s/props-ohl3c
- props : 부모 컴포넌트가 자식 컴포넌트로 데이터를 보내는 방식
- props는 컴포넌트의 첫 번째이자 유일한 인자이며 두 번째 인자는 없음
- props는 객체로서
Btn({text: "confirm", color: "white"})
와 같이 전달된다 - 컴포넌트에 props로 넘겨도 바로 그 요소에 적용되지 않음, 전달받은 컴포넌트 내부에서 전달받은 props를 가지고 직접 요소에 적용해줘야 함
- 전달한 props 네이밍이 onClick이더라도 React는 그걸 자동으로 요소의 onClick 리스너에 적용해주지 않음
- 직접 내가 원하는 곳에 꽂아 써라
React Memo
- 부모의 state 변경시에 모든 자식 컴포넌트가 re-render 되어버림 (굳이 re-render 되지 않아도 되는 것까지)
- props가 변경되지 않는 선에서 컴포넌트가 re-render 될건지 여부를 결정 할 수 있음 -> React Memo
React.memo(Component)
로 Component의 Memorized 버전을 만들어서 부모의 state가 변경되었지만 props는 변경되지 않아 re-render될 필요가 없을 때 re-render되지 않도록 성능 최적화를 시켜줄 수 있음
Prop Types
- 예제 : https://codesandbox.io/s/prop-types-deuio
- React는 내가 보내는 prop의 type이 뭔지 알 턱이 없음
- 그래서 React에서
PropTypes
라는 패키지가 있는데 내가 어떤 유형의 prop을 받고 있는지 체크해줌 -> 만약 Typescript 사용중이라면 패스해도 될 듯 - https://ko.reactjs.org/docs/typechecking-with-proptypes.html
isRequired
로 해당 prop을 필수로 받을건지도 체크 가능- 내가 정해준 type에 어긋나면 warning 발생
📌 CRA
- git repo : https://github.com/beecomci/react-for-beginners/
- commit #
- create-react-app (이하 CRA)는 react 프로젝트를 만들기 위한 환경의 총 패키지를 제공
- https://ko.reactjs.org/docs/create-a-new-react-app.html
설치 방법
npx react-create-app my-app
cd my-app
npm start
CSS Module
// 1. Button.module.css
.btn { background-color: tomato; }
// 2. Button.js
import styles from './Button.module.css';
function Button() {
return <button className={styles.btn}>Confrim</button>;
}
- CRA가
Button.module.css
코드를 JS 객체 형태로 변환시켜줌 - style도 module화 가능 -> 컴포넌트끼리 스타일도 독립적으로 분리 가능
- className에 객체화된 스타일의 클래스를 가져와서 랜덤한 이름의 클래스 생성
- 모듈화할 css 파일은 중간에
module
을 붙일 것
- App 컴포넌트와 Button 컴포넌트에서 같은 title 이라는 className을 사용해도 HTML 내에서는 랜덤한 이름으로 생성되므로 서로 다른 className이 되어 중첩 문제 없음
📌 Effects
Why need Effects ?
- commit #
- React는 변화가 일어날 때마다 컴포넌트를 re-render 알아서 해줌 -> 이건 매우 편하지 !
- 그런데 어떤 경우에는 특정 코드들이 첫번째 render 될때만 실행되고 다른 state 변화에는 실행되지 않도록 하고 싶을 때가 있음
아니면 특정 데이터가 변화할 때만 실행해야 할 때도 있고...- 예시) API를 가져오는데 글자를 입력할 때마다 state가 변경될 때마다 API를 가져오고 싶지는 않잖아 ? -> 매우 비효율적임
useEffect(EffectCallback, DependencyList)
: 무언가 변화할 때 특정 코드가 실행되도록 설정 가능 / 언제 코드가 실행될지 설정 가능- EffectCallback : 실행시키고 싶은 코드
- DependencyList
- React가 지켜보는 값들을 배열로 입력, n개일 때 그 중 1개라도 변경되면 callback 실행
- 초기값 빈 배열 [] 입력시 지켜볼게 아무것도 없기 때문에 최초 1회만 callback 실행
// AS-IS : 내가 원하는건 keword가 변화할 때만 실행되었으면 하는데, 처음 1번 실행되고 input 입력 이후부터는 해당 코드가 실행되지 않음
useEffect(() => { console.log("Search for", keyword); }, []);
// TO-BE : keyword가 변화할 때 코드가 실행하고 싶다면, 두 번째 인자에 keyword 입력
useEffect(() => { console.log("Search for", keyword); }, [keyword]);
Cleanup function
- commit #
- useEffect로 컴포넌트가 언제 create 되는지 뿐만 아니라 destroy 될 때도 알 수 있음
- useEffect의 첫 번째 인자 EffectCallback 함수가 return 하는 함수는 컴포넌트가 destroy 될 때 실행됨 ->
Cleanup function
- 자주 사용하지는 않지만 특정 케이스에서 사용함
const byeFn = () => {
console.log("destroyed"); // 컴포넌트가 destroy 될 때 실행됨
};
const hiFn = () => {
console.log("created"); // Hello 컴포넌트가 create 될 때 맨 처음 1회만 실행됨
return byeFn;
};
useEffect(hiFn, []);
HTTP cookie 🍪🍪🍪
📌 Cookie
- 브랜드존 광고에서 찜하기 API를 사용하는 flow를 정리해보면서 내가 궁금했던 HTTP cookie 요청 개념에 대해 서칭해봄
🍪 쿠키란 ?
- HTTP 쿠키 (웹 쿠키, 브라우저 쿠키)는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각
- 브라우저는 그 데이터 조각들을 저장해두었다가, 동일한 서버에 재요청시 저장된 데이터를 함께 전송
- 쿠키는 두 요청이 동일한 브라우저에서 들어왔는지 아닌지를 판단해주며 이를 이용해서 사용자의 로그인 상태를 유지할 수 있음
쿠키 사용 목적
- 세션 관리
- 서버에 저장해야 할 로그인, 장바구니, 게임 스코어 등의 정보 관리
- 개인화
- 사용자 선호, 테마 등의 세팅
- 트래킹
- 사용자 행동을 기록하고 분석
웹 스토리지 API
- 과거에 쿠키를 사용하는게 클라이언트에 데이터를 저장하는 유일한 방법일 때가 있었지만, 이러면 모든 요청마다 쿠키가 함께 전송되기 때문에 성능이 떨어지는 원인이 될 수 있음
- 이젠 클라인트에 데이터를 저장하려면
웹 스토리지 API
(localStorage
,sessionStorage
)와IndexdDB
사용 추천
🍪 쿠키의 라이프타임
Session Cookie
Session Cookie
는 현재 세션이 끝날 때 삭제됨expires
나max-age
옵션이 없으면 웹 브라우저 켜져있는 동안 유효하고 끄고 다시 켜면 삭제됨- 브라우저가 현재 세션이 끝나는 시점을 정의하며, 어떤 브라우저들은 재시작할 때 세션을 복원해 세션 쿠키가 무기한 존재할 수 있도록 함
Permanent Cookie
Permanent Cookie
는expires
속성에 명시된 날짜에 삭제되거나max-age
속성에 명시된 기간 이후에 삭제됨- 웹 브라우저 껐다 켜도 유지됨
expires & max-age
// 지금으로부터 하루 후
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;
// 1시간 뒤에 쿠키가 삭제됩니다.
document.cookie = "user=John; max-age=3600";
// 만료 기간을 0으로 지정하여 쿠키를 바로 삭제함
document.cookie = "user=John; max-age=0";
expires
: 만료되는 시간 설정, 반드시 GMT 포맷으로 설정max-age
: 얼마동안 유지할 것인지 설정, 현재부터 만료일까지 시간을 초로 환산하여 값 설정
🍪 쿠키 읽기
// 통검 예시
console.log(document.cookie);
// 결과
'NNB=ALBVISLWAHPF2; MM_NEW=1; NFS=2; ASID=0a43473e0000016eefa906ac00000053; nx_open_so=1; m_loc=2972841043087ba1d84b880ce21377c31cb4ed777e8dd76f9000481112f2bb3ce4d5a870d9e5f2a263b450d433969081e13d897ce8c2677c89300dbd1872e152; NRTK=ag#20s_gr#4_ma#-2_si#2_en#2_sp#-2; EXT_V1=e3c7bc9f-765c-4bf1-9b90-89580ef3707c; NDARK=N; nso_open=1; _ga_4BKHBFKFK0=GS1.1.1614788516.2.1.1614788532.44; NSCS=1; NaverSuggestUse=use%26use; NID_JKL=DrSrBNT1aKCcGGzwdwpyKPNyiSVWtkS0JC/dPUL/OuA=; nx_ssl=2; _fbp=fb.1.1638869904485.903392222; BMR=s=1638948008184&r=https%3A%2F%2Fdev-media.naver.com%2Fpress%2F056&r2=; _ga=GA1.2.1311121557.1575903723; _ga_7VKFYR6RV1=GS1.1.1639664440.69.1.1639664451.49; page_uid=hk8ZOlprvP4sssGc55ZssssssdG-251649; NID_SES=AAABrOaPR5jNdXIbRr67JHDta8yq6dCVBqlbMDJDxhSuQFJMDYS7Kcr1h0cw2lJcjzOUb4hYWHl1IT0i/5KwpvRalAmtu0k06SkS5wDndtHGRlZJy6ntTLoONETNxXcjyDEtudJSllng9IUH1SBCTzw7w/JdVcIMDnrxkjFBFwbOWbHDVGZsnDjgoj6wX4vgEFa5SuBOXVp1M2x5DgBi8EvaSfjU954OGfdZSfSUWPNKr/qz04ZULB3Db+GtKlliGbVLiCWmIn6O6EK3aG69xq2VVheKtXxKlFv9GP6JH4ZCbK38H9jLDYg14Kn9QX9ydxnVbxdNCYnBAz0f009CXo8AzsbSbxTGpPAD9h8ur8MrOPsiHtvsjwHmbhANeUvbBDwxc74S8lJFJH2lgPaCTGWXZLJ7UIeyz5nZ5b+1yS6ZjAZOJu0Ghn8TEaUoxX0mxuHtA/qnn4M/hBOqiXYn95ZZW1r+AZUc7ixg3NbL99b4Ugwwjb5NMgWnRNtCq8fAb5ZL/FUsN6y3RrwfDMv4oZf5MCkweH5QCvQu5CWrzCpvnWJQ6Ek9aYmqDFaD7VqlJGZE3A=='
document.cookie
: 브라우저에서도 쿠키 접근 가능name=value
쌍으로 구성되어 있고, 각 쌍은;
로 구분하며 쌍 하나는 하나의 독립된 쿠키getCookie(name)
: https://ko.javascript.info/cookie#ref-2339
🍪 쿠키 쓰기
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}";
document.cookie
에 직접 값을 쓸 수 있음- 여기서 cookie는 데이터 프로퍼티가 아닌 접근자 프로퍼티여서 값을 할당하면 다른 쿠키값들은 변경되지 않고 할당한 값을 받아 해당 쿠키를 갱신
- 모든 쿠키를 덮어쓰지 않고 명시된 쿠키만 갱신
- 쿠키 name과 value에는 글자 제약이 없지만 반드시
encodeURIComponent
를 사용해서 이스케이프 처리 필요 (공백 등) - 쿠키 갱신은 동일한 도메인과 경로에서만 해야함
setCookie(name, value, options)
: https://ko.javascript.info/cookie#ref-2340
쿠키 제약 사항
encodeURIComponent
로 인코딩한name=value
쌍의 크기는 4KB를 넘길 수 없음- 도메인 하나당 저장할 수 있는 쿠키의 갯수는 20여개 정도로 한정되며 브라우저마다 조금씩 상이함
🍪 httpOnly
NID_AUT 가 httpOnly 라서 Ditto 에서 document.cookie 로 읽어올 수 없습니다.
- javascript와 관련없지만 전달하려는 쿠키가 httpOnly여서 템플릿(클라이언트)에서 읽어올 수 없었던 이슈가 있었음. 뭔가 궁금해서 정리
httpOnly
옵션은 웹서버에서Set-Cookie
헤더를 이용해 쿠키를 설정할 때 지정할 수 있는 옵션httpOnly
옵션이 설정된 쿠키는 javascript같은 클라이언트가document.cookie
로 쿠키 정보를 사용할 수 없도록 해서 쿠키를 보호함- 해커가 javascript 코드를 페이지에 삽입하고 사용자가 페이지에 접속했을 때 쿠키에 있는 인증정보를 해킹할 수 있기 때문에 미리 예방 차원
🍪 쿠키의 스코프
domain
과path
로 쿠키의 스코프를 정의- 서버에서는
set-cookie
로 응답 헤더에 추가하면 됨
- https://m.search.naver.com/search.naver?sm=mtp_hty.top&where=m&query=%EB%94%94%EC%98%AC%EA%B0%80%EB%B0%A9 이나 naver.com을 가진 도메인에서 https://apis.naver.com/imp_neo/bada-api/customer-store 경로로 API 호출시에 쿠키는 어떻게 전달되는걸까?
- https://ditto.searchad.naver.com/preview.html?creative=crtv-r3dmxh55eav5&fullwidth 경로의 경우는 ditto.searchad.naver.com 이라는 naver.com의 서브 도메인인데 이런 미리보기 경로에서도 API 호출시 쿠키가 전송되는 이유는 뭘까?
domain
// site.com에서 쿠키 설정
document.cookie = "user=Johin";
// site.com의 서브 도메인인 forum.site.com에서 user 쿠키 접근
console.log(document.cookie); // 찾을 수 없음
- 쿠키에 접근 가능한 도메인을 지정
domain
옵션에 아무 값도 넣지 않았다면 쿠키를 설정한 도메인에서만 쿠키에 접근 가능함 (site.com
에서 설정한 쿠키는other.com
에서 얻을 수 없음)- 또한 서브 도메인인
forum.site.com
에서도 쿠키 정보를 얻을 수 없는 제약 사항이 있음 -> 민감한 데이터 쿠키는 관련 페이지에서만 볼 수 있도록 안정성을 높임
- 또한 서브 도메인인
// site.com에서 서브 도메인 (*.site.com) 어디서든 쿠키 접속하게 설정 가능
document.cookie = "user=John; domain=site.com"
// 서브도메인에서 쿠키 정보 접근 가능
console.log(document.cookie); // user=John 쿠키 확인 가능
site.com
에서 쿠키를 설정할 때domain
옵션에 루트 도메인인domain=site.com
을 명시적으로 설정하면 서브 도메인에서 쿠키 접근 가능- 하위 브라우저 호환성 유지를 위해 앞에 점을 붙인
domain=.site.com
도 동일하게 적용됨
path
- 설정한 경로나 이 경로의 하위 경로에 있는 페이지만 쿠키에 접근할 수 있도록 함
- 절대 경로로 설정하며 기본값은 현재 경로
- 특별한 경우가 아니라면
path
옵션은path=/
와 같이 루트로 설정해 웹 사이트의 모든 페이지에서 쿠키에 접근 할 수 있도록 함
개발자도구에서 쿠키 확인
- 개발자도구
Application
탭에서 저장된 쿠키의 Domain과 Path 확인 가능
🍪 Credentials (자격증명)
https://oss.navercorp.com/ditto-templates/brandzone.mobile.premium/blob/master/src/js/keepAPI.js#L28
쇼핑 찜하기 API 호출시 XMLHttpRequest 객체에 xhr.withCredentials = true 설정이 있음
API 호출시 쿠키 전달에 왜 필요할까?
- Credentials (자격증명)이란 쿠키, authorizations 헤더, TLS 클라이언트 인증서를 뜻함
- Credentials를 포함한 요청은 HTTP cookies와 HTTP Authenitication 정보를 인식할 수 있도록 함
- 기본적으로 corss-site XMLHttpRequest나 Fetch 호출에서 브라우저는 자격증명을 보내지 않아서 별도로 설정 필요
- CORS - Request with Credentials
동일한 출처
- 동일한 출처라면 알아서 쿠키가 요청 헤더에 들어가게됨
- 하지만
https://m.search.naver.com/
에서https://apis.naver.com/
로 요청시 두 경로는 호스트가 다르기 때문에 동일한 출처가 아님 (메인 도메인은 같지만 서브 도메인이 다름)- 두 URL의 프로토콜, 포트, 호스트가 모두 같아야 동일한 출처라고 부를 수 있음 #
- 그럼 CORS 요청시에는 자동으로 쿠키가 요청 헤더에 들어가지 않으므로 쿠키를 전달하기 위해서 서버와 클라이언트에서는 뭘 설정해줘야 하는가?
서버에서는...
Access-Control-Allow-Credentials : true
- Request with Credential 방식이 사용될 수 있는지를 지정함
- 서버는 Response Header에 반드시 위의 값을 포함해야 함
- 클라이언트에서 아래와 같이 credentials 설정을 해서 요청을 해도 서버에 응답 헤더가 없는 경우 브라우저는 응답을 거부하고 제공하지 않음
Access-Control-Allow-Origin
헤더의 값에는 와일드카드*
가 오면 안되고https://m.naver.com
와 같이 구체적인 도메인이 와야 함 #
클라이언트에서는...
// XMLHttpRequest
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
- 기본적으로 Request without Credential
XMLHttpRequest
사용시withCredentials
속성을true
로 지정해서 Credentials 요청을 보낼 수 있음
// fetch
fetch(url, {
credentials: 'include'
})
fetch
의 경우credentials
옵션에include
,same-origin (default)
,omit
3가지가 있음include
: cross-origin 호출도 자격증명 포함same-origin (default)
: same-origin 호출만 자격증명 포함omit
: 절대 자격증명 포함하지 않음
브랜드존 예시
[React-Basic-Hooks] 9. Portal
Portal
// child : 그리고자 하는 ReactNode
// container : 어디에 그릴지 실제 DOM 전달
ReactDOM.createPortal(child: React.ReactNode, container: HTMLElement)
- react root DOM node 외부에 React Element 그리기
- 일반적인 경우 ReactDOM.render나 hydrate을 통해 dom을 그리게 되는데, 이외에 경우에 사용
modal
(페이지 이동 X, 화면 전체를 덮는),tooltip
,외부 layer
등에 사용하기 좋음 -> React가 관리하지 않는 외부의 dom일 경우 사용Portal Component
에서 발생한 event도 부모에 전달 됨. -> 일반 react child 처럼 사용 가능한 장점
@08-portal/portal
import { useState } from 'react';
import ReactDOM from 'react-dom';
// 1번째 인자 : 내가 그리고 싶은 React Element
// 2번째 인자 : 어디에 그릴지
const Modal = () => ReactDOM.createPortal(
<div>I am a modal</div>,
document.body.querySelector('#app')
)
const App = () => {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>
<div>Hello!</div>
<div>count: {count}</div>
{/* 구조상으로는 위의 영역 안에 들어가있는 것 같지만, 실제 DOM을 보면 #root 밖의 #app에 그려져 있음 */}
{/* Virtual DOM상으로는 지금 보이는 구조대로 들어가기 때문에 Modal에서 발생한 click event도 부모 div에서 catch가 가능한 점이 장점, 내부 state를 공유해서 사용 가능 */}
{/* React가 아니라면 Modal에 직접 click 이벤트를 또 걸어줘야 하는 불편함 */}
<Modal />
</div>
)
}
export default App
활용예시: codepen
- 같은 state 공유
@08-portal/windowPortal
import { useState, useEffect, useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import copyStyles from './copyStyles';
function MyWindowPortal({ children, closeWindowPortal }) {
const containerElRef = useRef(null);
// 외부 DOM이기 때문에 찾는 로직
if (!containerElRef.current) {
// STEP 1: create an empty div
containerElRef.current = document.createElement('div');
}
useEffect(() => {
const containerEl = containerElRef.current
// STEP 3: open a new browser window
const externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
// STEP 4: append the container <div> to the body of the new window
externalWindow.document.body.appendChild(containerEl);
externalWindow.document.title = 'A React portal window';
copyStyles(document, externalWindow.document); // 현재 documnet와 외부 document 스타일을 맞춤 (그냥 따로 만든 로직)
// update the state in the parent component if the user closes the new window
// 닫히기 직전에 setShowWindowPortal로 내부 state로 닫혔음을 update
externalWindow.addEventListener('beforeunload', closeWindowPortal);
// store containterEl to ref
containerElRef.current = containerEl;
// This will fire when showWindowPortal in the parent component becomes false
// So we tidy up by just closing the window
return () => {
externalWindow.close();
}
}, [closeWindowPortal])
// STEP 2: draw children to containerEl
return ReactDOM.createPortal(children, containerElRef.current);
}
function App() {
const [count, setCount] = useState(0);
const [showWindowPortal, setShowWindowPortal] = useState(false); // Portal을 보여줄지 말지
// 함수를 다시 생성할 필요가 없어서 useCallback 감싸서 사용 (아래에서 dependency로 사용)
const toggleWindowPortal = useCallback(() => {
setShowWindowPortal(prev => !prev);
}, [])
const closeWindowPortal = useCallback(() => {
setShowWindowPortal(false);
}, [])
useEffect(() => {
const interval = window.setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => {
window.clearInterval(interval);
closeWindowPortal(); // 컴포넌트가 unmount되면 Portal로 열린 Modal을 닫음
}
}, [closeWindowPortal])
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={toggleWindowPortal}>
{showWindowPortal ? 'Close the' : 'Open a'} Portal
</button>
{showWindowPortal && (
// 위에서 정의한 closeWindowPortal을 그대로 전달해서 사용 가능
// 내부 Element들 모두 children으로 전달됨
<MyWindowPortal closeWindowPortal={closeWindowPortal}>
{/* 위의 count와 같은 count 공유 사용 (외부 props, state 공유시 유용한 Portal) */}
<h1>Counter in a portal: {count}</h1>
<p>Even though I render in a different window, I share state!</p>
<button onClick={() => closeWindowPortal()}>
Close me!
</button>
</MyWindowPortal>
)}
</div>
);
}
export default App;
[React-Basic-Hooks] 6. React event handling
React event handling #
- React는 event delegation을 통해 이벤트 전달
-
React v16 이전 : 타입별 이벤트를 모두 document에 하나씩 걸고 어디서든 이벤트가 발생하면 이벤트를 찾기 위해 document까지 타고 올라가고 등록된 이벤트 발견, document에서 등록된 이벤트를 찾아서 실행시킴
-
React v17: document가 아니라 root element 위치로 바뀜, 여러개의 react 버전을 섞어서 사용하는걸 지원하기 위해 변경됨, react 외에 다른 라이브러리와 사용하는 경우 거기서 발생한 이벤트를 정지시킨다던가 하는 이슈가 있었음
-
실제 DOM element의 event handling과 비슷
-
event callback 함수에 전달되는 event 객체는 React에서 합성한 event 객체(syntheticEvent) -> 실제 native 이벤트는 아님(실제 브라우저가 발생한 이벤트)
-
실제 native event 객체에 접근하기 위해서는
e.nativeEvent
사용 (그냥 syntheticEvent 객체가 모던 브라우저에서 사용하는 이벤트 객체와 비슷하기 때문에 사용하면 됨) -
차이
- event명은 camelCase: eg)
onClick
string
이 아닌 실제function
전달- default 동작을 막기 위해서
return false
사용 불가- 대안
e.preventDefault()
@05-event-handling/preventDefault
- 대안
<!-- from --> <button onclick="activateLasers()"> Activate Lasers </button>
// to <button onClick={activateLasers}> Activate Lasers </button>
- event명은 camelCase: eg)
-
메모리 누수를 위한
addEventListener() / removeEventListener()
사용할 필요 없음- React가 mount될 때 addEventListener, unmount될 때 removeEventListener
- 실제로 각 element에 addEventListener, removeEventListener 붙는게 아니고, 로딩될 때 모든 이벤트를 root element에 붙여놓고 React가 target element에 등록된 핸들러가 있으면 동작하는 방식
-
React의 syntheticEvent #
- 거의 모든 native 이벤트를 지원
- React는 native event를 wrapping 한 syntheticEvent를 사용
- cross browser 대응을 해주기 때문에 사용이 편리
-
Event pooling #
- (React 17 이전 버전 사용시) event pooling 방식 사용 (event가 발생할 때마다 pool을 할당해서 사용하다가 event가 끝나면 null로 다시 할당하는 방식) -> 최적화의 의미가 떨어져서 17 버전부터는 제거된 방식
- (React 17 이전 버전 사용시) event가 null이 된 후에 접근하면 에러가 나니 event 유지하고 싶으면
event.persist()
호출 - React 17 버전 이후로는
event.persist()
event pooling 방식이 아니어서event.persist()
호출의미가 없음.
[React-Basic-Hooks] 8. Handle DOM in React (ref)
📝 useRef 사용은 언제?
- 실제 DOM 접근에 사용
- render에 영향을 주지 않는 mutable data 관리에 편리
- 함수형 컴포넌트에서는 render될 때마다 함수가 매번 재실행되어 각각 매번 스코프가 생김 -> 함수 내에서 변수 설정을 해놓아도 매번 render될 때마다 재할당이 됨 -> 이전 값을 유지하려면 hook - reference를 사용해서 따로 관리 필요
- 예) API 호출이 5번만 되어야한다면, 호출 와중에 update가 되어서 영향을 받지 않으려면 count를 state로 두면 변경될 때마다 매번 rendering이 되니 render에는 영향을 주지 않지만 mutable하게 관리하기 위해서 reference useRef 사용
DOM에 ref
사용하기 좋은 예
- animation 제어를 해야할 때 (대신 react-spring 추천)
- input::focus, input::text, media 선택 제어가 필요할 때(DOM API 호출을 해야할 때)
- 외부 DOM Library 사용할 때(jquery, egjs 등)
그 외에는 되도록 사용하지 않는 것이 좋다.
💡선언적(declartive)으로 생각하기
=>Dialog
component의open()
,close()
함수를 노출하기 보다는(명령형)isOpen
값(상태값)을 전달하여 제어하도록 한다. 상태에 맞는 컴포넌트를 그리도록 React에게 위임, Dialog 내에서는 isOpen의 상태값에 맞는 화면 정의가 잘 되어 있으면 Dialog가 받는 props(isOpen)으로 제어됨
DOM 라이브러리 사용시 어떻게 사용해야 할까?
- Virtual DOM에 영향을 주지 않고 직접 DOM을 제어하기 때문에 sideEffect -> useEffect 사용
- DOM을 직접 제어하기 때문에 SSR에서 처리 할 수 없음
- DOM을 직접 제어하는 것은 React, Virtual DOM을 제어하는 것은 개발자(Component 생성 etc), 개발자가 만든 Virtual DOM을 실제 DOM과 매칭시키는 것은 React의 역할, 실제 DOM을 개발자가 제어하는 경우 React가 알고 있는 DOM과 달라져서 매칭이 깨질 수가 있기에 주의해서 사용, useRef로 사용하는 것은 고립을 시키고 React가 할 일은 React에게 위임하도록 개발하는 것이 좋음
@07-refs/dom
import { useRef } from 'react';
const TextInput = () => {
// DOM 접근시 ref 사용
const textInputRef = useRef(null);
const focusInput = () => {
// 접근시 current로 접근하면 실제 dom 접근
textInputRef.current.focus();
}
return (
<div>
<input
type="text"
ref={textInputRef} // React가 실제 DOM render 후 reference를 붙여줌
/>
<button onClick={focusInput}>
Focus
</button>
</div>
);
}
forwardRef
- 상위 component의
ref
에 하위 commponent의 ref를 전달 - 되도록 사용하지 않는 것이 좋음
- 사용해야 하는 상황이 생기면 다시 설계를 고민해 보는것도 좋음
useImperativeHandle
hook과 같이 사용
@07-refs/forward
import { useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
function AutoFocusTextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focusInput();
}, []); // mount 되었을 때 1번만 실행
return <ForwaredTextInput ref={inputRef} />;
}
// forwardRef를 사용하면 부모 컴포넌트로부터 ref 전달받기 가능
function TextInput(props, ref) {
const textInput = useRef(null);
const focusInput = () => {
textInput.current.focus();
}
// focuseInput 함수를 상위 컴포넌트로 전달하겠따
useImperativeHandle(ref, () => ({ focusInput }))
return (
<div>
<input
type="text"
ref={textInput}
/>
<button onClick={focusInput}>
Focus
</button>
</div>
);
}
// forwardRef로 감싼 TextInput 컴포넌트를 부모 ForwaredTextInput에 전달
// 부모 컴포넌트는 해당 컴포넌트 사용시 ref 주입
const ForwaredTextInput = forwardRef(TextInput)
export default AutoFocusTextInput;
// 지금처럼 상위 컴포넌트에서 불편하게 제어하지 않고 TextInput의 props에 isAutoFocus flag 전달로 내부에서 flag 상태값에 따른 render를 하는 구조를 더 추천
[React-Basic-Hooks] 2. React Element & React Component
React Element
- type과 props를 가지는 JS 객체 (객체라는 개념 자체 중요)
const element = <div>Hello</div>
처럼 객체기 때문에 변수에 담을 수 있음
- jsx
- JS 문법이 아니기 때문에 브라우저에서 바로 render는 안되고, babel을 사용해서 js로 변환해야 브라우저에서 인식 가능
- jsx로 만든 element는 결국 React.createElement 형태로 변환됨
// React Element 생성
React.createElement(
'div', // type
{ className: 'greeting' }, // props
'Hello World!' // children
)
// 만든 결과
{
type: 'div',
props: {
className: 'greeting',
children: 'Hello World!'
}
}
// with jsx
// 좀 더 선언적으로 보인다는 장점
const button = (
<button className='button button-blue'>
<b>
OK!
</b>
</button>
);
// without jsx
const button = React.createElement(
'button',
{ className: 'button button-blue' },
React.createElement(
'b',
null,
'OK!'
)
);
React Component
props
는 전달 받아서 React Element를 반환하는 function or class- class에서는 React Element가 아닌 render 메소드를 반환하는 형태
- Hook이 나온 이유로는 class component를 사용해야 하는 이유가 거의 없어져서 강의에서는 함수형 component 집중
- React element tree를 encapsulate하는 역할
- 로직을 component에 담아서 재사용이 가능하도록
- 재사용되면서 조금씩 변경되어야하는 부분들은 props로 제어 가능
function Button(props) {
return (
<button className={`button button-${props.color}`}>
<b>
{props.children}
</b>
</button>
)
}
or
// 클릭했을 때 동작 같은 것들도 props로 설정 하고, 이벤트 핸들러를 붙이면 동작 가능
const Button = ({ color = 'blue', children = 'OK'}) => (
<button className={`button button-${color}`}>
<b>
{children}
</b>
</button>
)
<Button color='blue'>
OK!
</Button>
React.createElement(
Button, // 위처럼 string이 아닌 component 정의를 넘김
{ color: 'blue' },
'OK!'
);
// 만들어지는 객체
{
type: Button, // string이 아닌 객체를 가리킴
props: {
color: 'blue',
children: 'OK!'
}
}
// destructuring X
function Button(props) {
return (
<button className={`button button-${props.color}`}>
<b>
{props.children}
</b>
</button>
)
}
// destructuring O
function Button({ color = 'blue', children = 'button }) {
return (
<button className={`button button-${color}`}>
<b>
{children}
</b>
</button>
)
}
[React JS 마스터클래스] Chapter #4 CRYPTO TRACKER
📌 react-query
react-query
: 편한 방식으로 데이터를 fetch 할 수 있는 패키지
Setup
react-router-dom
&react-query
installreact-router-dom
: 어플리케이션이 URL을 가져서 각기 다른 화면을 갖게 해줌- 추가로
@types/react-router-dom
설치
react-router-dom
- 앞선 react 강의에서 다뤘던 부분
- 강의에서는 5.x.x대 버전을 사용하지만 난 6.x.x 버전 사용 중
- 6.x.x 버전 부터는
Switch
대신Routes
로 사용- 한번에 하나의 Route를 렌더링 할 수 있도록 함
📌 Styles
Reset Style
1. styled-reset
- https://www.npmjs.com/package/styled-reset
- 실질적으로 전체 문서에 적용하기에는 커스텀한 기본값을 추가하기에 어려움이 있음
2. createGlobalStyle
import { createGlobalStyle } from "styled-components";
const GlobalStyle = createGlobalStyle`
body {
color: red;
}
`;
function App() {
return (
<>
<GlobalStyle />
<Router />
</>
);
}
- styled 컴포넌트로 만든 스타일은 지정된/사용되는 요소에만 국한됨
- createGlobalStyle은 렌더링 될 때 전역 스코프에 스타일을 올려줘서 global한 style을 가지게 됨
- document의 head에 style 주입
Fragment
- 일종의 유령 컴포넌트로 기존에 React에서 1개의 element만 return 되어야 해서 사용하지도 않는 쓸데없는 빈 div를 마구잡이로 썼다면
- Fragment로 대체 가능
Font
- https://fonts.google.com/specimen/Source+Sans+Pro
- 강의에서는 위 url에서 필요한 font를 @import해서 사용
Theme
- 이제 내 앱에서 필요한 theme color 주입
📌 Coins Home
- API에서 데이러를 가져와도 똑같이 Typescript에게 뭐가 오는지 알려줘야 함
- 다른 페이지(ex. Coin Detail)에서 다시 돌아왔을 때도 Loading되는 이슈는 추후 react-query에서 수정 예정
API fetch
// 1. normal
const getCoins = async () => {
const json = await (
await fetch("https://api.coinpaprika.com/v1/coins")
).json();
};
useEffect(() => getCoins(), []);
// 2. IIFE로 굳이 getCoins 함수를 또 만들 필요 없이 즉시 실행
useEffect(() => {
(async () => {
const json = await (
await fetch("https://api.coinpaprika.com/v1/coins")
).json();
})();
}, []);
📌 Coin Detail - Route States
현재까지 개발된 상황
1. Coins Home 페이지에서 Loading 문구가 최초로 노출됨
2. 그 사이에 API request가 종료되면 코인 리스트를 가져올 수 있게 되고 Loading 문구는 사라지고 코인 리스트 UI 노출
3. 특정 코인을 클릭하면 Coin Detail 페이지로 이동 (ex. /btc)
4. 페이지 이동 후, 코인 name 제목이 여전히 특정 코인 데이터를 가져오는 동안 Loading 문구 노출됨
- Coin Detail 페이지에서 코인 name 제목은 나도 아는 데이터임에도 불구하고 API가 줄 때까지 사용자는 로딩 문구를 봐야 함 -> 좋은 UX가 아님
비하인드 더 씬 데이터
- 우리는 그동안 한 화면에서 다른 화면으로 데이터를 전송할 때 URL Parameter를 보내는 방식을 사용해 왔음
- 하지만 위와 같은 이슈가 있어서 state를 사용해보자
// Coins.tsx
// react-router-dom v6점대 이상부터 아래처럼 사용
<Link to={`/${coin.id}`} state={{ name: coin.name }}>
// Coin.tsx
// Link에서 state로 넘겼던 coin.name을 Coin에서 useLoaction을 사용해서 get
// react-router-dom v6점대 이상부터 <> generic 미지원, 아래처럼 사용 가능
interface RouteStates {
state: {
name: string;
};
}
function Coin() {
const {
state: { name }
} = useLocation() as RouteStates;
return (
<Container>
<Header>
<Title>{name}</Title> // 더이상 useParmas로 가져온 coinId가 아닌 coin의 name을 직접 뿌려줄 수 있음
</Header>
</Container>
);
}
- state는 Coins Home 페이지를 열 때 & Coin Detail 페이지로 넘어갈 때 생성됨
- state는 Coins Home 페이지에서 특정 코인을 클릭해서 이동할 때, Home에서 Detail 페이지로 보내짐
Uncaught TypeError: Cannot read properties of null (reading 'name')
- 크롬 시크릿창에서 Coin Detail 페이지로 바로 접속하면 위와 같은 오류 발생
- state가 생성되려면 Coins Home부터 열어야 그 state를 클릭시 Detail 페이지로 전송 가능
해결 방안
function Coin() {
const location = useLocation() as RouteStates; // {... state: null}
const name = location?.state?.name; // undefined 반환
return (
<Container>
<Header>
<Title>{name "Loading..."}</Title>
</Header>
{loading ? <Loader>Loading...</Loader> : null}
</Container>
);
}
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Optional_chaining
- Javascript의 Optional Chaining 연산자(?.)를 사용해서 name에 접근하기 전에 state의 상태에 따라 다르게 반환됨
- state가 있다면 name 그대로 반환, null or undefined라면 자동으로 단락되어 undefined가 반환됨
📌 Coin Detail - Data Types
- 보통 Interface는 네이밍을 맨 앞에
I
를 붙임 - API 데이터의 Interface 정의 시 미세먼지 팁
// 1. 아래 key 복사
temp1 = { API 데이터 };
Object.keys(temp1).join(); // 'id, name, symbol...'
// 2. VSCode에 붙여넣고 Command + D로 콤마 선택해서 삭제 후 엔터
// 전체 영역 선택 후, Command + Shift + i로 각 모든 커서를 받아서 공통 문자 :와 ; 입력
// 3. 아래 values의 type 복사
Object.values(temp1).map(item => typeof item).join(); // 'string, string, boolean...'
// 4. 2번처럼 콤마 선택해서 삭제 후 엔터
// 5. 2번까지 마친 key를 전체 선택 후, Comand + Shift + i로 각 모든 커서가 나오면 4번 key 붙여넣기
// 6. But array같은 경우 object로만 type 변환이 되어서 내부 속성들의 타입도 명시해줘야 함
interface ITag {
coin_counter: number;
id: string;
...
}
interface IInfoData {
id: string;
name: string;
tag: ITag[];
...
}
📌 Coin Detail - Nested Routes
- Coin.tsx에서 사용하는 API를 컴포넌트 실행 최초 1번만 받아오도록 하기 위한 useEffect의 2번째 param을 coinId로 넣어야 한다는 경고가 뜸
- hooks의 성능 향상을 위해서 경고 문구
- 사실 coinId를 넣으면 coinId가 변경될 때마다 useEffect 내의 함수가 실행되기 때문에 우리 의도와는 달라짐
- But !! 여기서 coinId는 URL로부터 오는 값이기 때문에 절대 변경될 일이 없어서 빈 배열을 넣을 때와 동일하게 컴포넌트 최초 1번만 실행됨
Nested Route
- route in route
- 탭 or 화면 내에 많은 섹션 사용시 많이 사용됨
- 탭들을 state에 넣는 대신 url로 관리해서 url로 다이렉트로 접근 가능하도록 할 때
- /btc-bitcoin/chart : 차트 탭이 보이도록
- Link를 사용해서 URL을 바꿈으로써 트리거가 되어 re-render 가능
// 1. Router.tsx
// v6점대부터 exact는 더이상 사용하지 않고 여려 라우팅을 매칭하고 싶은 경우 URL뒤에 /* 붙임
<Route path="/:coinId/*" element={<Coin />}></Route>
// 2. Coin.tsx
// v6점대부터 상대 경로 지원
<Routes>
<Route path="/price" element={<Price />}></Route>
<Route path="/chart" element={<Chart />}></Route>
</Routes>
- react-router-dom v6점대부터 위처럼 사용
- v6점대부터 달라진 점들
useMatch hook
const Tab = styled.span<{ isActice: boolean }>`
...
color: ${props =>
props.isActice ? props.theme.accentColor : props.theme.textColor};
`;
function Coin() {
const priceMatch = useMatch("/:coinId/price"); // 내가 이 url 안에 있냐
const chartMatch = useMatch("/:coinId/chart");
return (
{/* chartMatch가 null이 아님 -> /:coinId/chart url에 들어와있다면 isActice는 true */}
<Tabs>
<Tab isActice={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActice={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>
<Routes>
<Route path="/price" element={<Price />}></Route>
<Route path="/chart" element={<Chart />}></Route>
</Routes>
);
}
- 내가 특정한 URL에 있는지 여부를 알려줌
- v6점대에서는 useMatch, 이하 버전에서는 useRouteMath란 네이밍
- 내가 선택한 URL에 들어가 있다면 아래와 같은 object를, 아니라면 null 반환
📌 React Query
// index.tsx
// 초기 세팅
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>,
document.getElementById("root")
);
- https://react-query.tanstack.com/overview
- 이제야 적용하는 react-query
QueryClientProvider
안에 있는 모든걸 하위 컴포넌트에서 접근 가능함 (ThemeProvider와 같은 맥락)
Why use react-query
// api.ts
// json data의 Promise 반환하는 함수 export
// 일반적으로 API fetch 관련된 함수는 컴포넌트에서 분리함
export function fetchCoins() {
// fetch 후 response의 json return
return fetch("https://api.coinpaprika.com/v1/coins").then(response =>
response.json()
);
}
// Coins.tsx
const { isLoading, data } = useQuery<ICoin[]>("allCoins", fetchCoins);
useQuery
hook은 fetcher 함수를 호출하고 fetcher 함수가 loading 중이라면 react-query는 isLoading으로 로딩 여부를 알려줄것- fetcher 함수가 끝나면 fetcher 함수에서 반환된 json을 data에 넣음
- 앞서 했던 isLoading state + useEffect로 API 호출 방법을 이 한줄이 대체 가능한 편리한 기능 ~~~
- 앞서 했던 방법으로는 클릭해서 detail 페이지 이동 후, 되돌아오면 다시 로딩된 후 전체 목록을 받아왔다
- 이젠 로딩이 보이지 않음 왜? -> react query가 data를 캐시해서 유지하고 있기 때문 !
Devtools
// App.tsx
import { ReactQueryDevtools } from "react-query/devtools";
function App() {
return (
<>
<GlobalStyle />
<Router />
<ReactQueryDevtools initialIsOpen={true} />
</>
);
}
- react query에서 Devtools 제공
- 앱이 가진 모든 query 확인 가능, 캐시를 시각화해서 확인 할 수 있다.
- 만든
allCoins
쿼리 확인 가능, 원하는 동작 트리거 가능 -> reqct-query가 query를 array로 보고 있음 ["allCoins"]- refetch : 데이터를 다시 fetch
- reset : 쿼리 reset
Coin에 react-query 적용
// Coin.tsx
const { coinId } = useParams();
const location = useLocation() as RouteStates;
const name = location?.state?.name;
const priceMatch = useMatch("/:coinId/price"); // 내가 이 url 안에 있냐
const chartMatch = useMatch("/:coinId/chart");
// param1 : 고유한 값 coinId를 query key로 사용
// param2 : Coins에서는 인자가 필요없었으나 여기선 coinId를 알아야 하니까 (호출이 아닌 함수 자체로 넘겨줘야함 !)
// 그래서 fetchCoinInfo 함수를 호출해서 ciondId를 넣어주는 함수를 생성해서 전달
// react query는 query를 array로 봄
// 넘겨주는 key를 배열로 만들어서 같은 coinId를 사용하지 않도록 생성
const { isLoading: infoLoading, data: infoData } = useQuery<IInfoData>(
["info", coinId],
() => fetchCoinInfo(coinId!)
);
const { isLoading: tickersLoading, data: tickersData } = useQuery<IPriceData>(
["tickers", coinId],
() => fetchCoinTickers(coinId!)
);
// 2개 로딩이 끝나지 않으면 Loading 노출
const loading = infoLoading || tickersLoading;
return (
<Container>
<Header>
{/* state로부터 온 name이 있으면 -> name or 없으면 -> loading */}
{/* 근데 loading이 true면 Loading 메세지를 or false면 API로부터 온 name */}
<Title>{name ? name : loading ? "Loading..." : infoData?.name}</Title>
</Header>
{loading ? (
<Loader>Loading...</Loader>
) : ( ...
);
- Coins에 적용한 방식과 동일
- 이제 Bitcoin 클릭 후 다시 되돌아와서 다시 Bitcoin 클릭하면 Loading 보이지 않음 왜???? react-query가 data를 캐싱하고 있기 때문
📌 Price Chart
Apexchart.js
- js 차트 라이브러리
- 자세한 옵션 사용법은 Docs 참고
- https://apexcharts.com/docs/react-charts
react-helmet
- 컴포넌트로서 사용하며 컴포넌트에서 무엇을 render하던 문서의 head로 보여줌
- react-helmet 사용시에
Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code.
오류 발생 - react-helmet-async로
HelmetProvider
로Helmet
을 감싸서 사용 - title 뿐만 아니라 favicon, css 등 추가 가능
import { Helmet, HelmetProvider } from "react-helmet-async";
<HelmetProvider>
<Helmet>
<title>{name ? name : loading ? "Loading..." : infoData?.name}</title>
</Helmet>
</HelmetProvider>
[React-Basic-Hooks] 5. React Component 이해
React Component 이해 #
- 재사용성을 높여주고 기능별 단위를 만들 수 있게 해줌
- f(props) -> reactElement
Component 구현 2가지 방법
- Function Component
- props 넘겨서 react element return
// with destructuring & arrow function
const Greeting = ({ name = '아무개' }) => (
<div>
안녕하세요~ <b>{name}</b>님
</div>
);
// 사용 예시 (react component를 인스턴스화)
() => <Greeting name="유진" />;
- Class Component : Extends React.Component(or React.PureComponent)
import { Component } from 'react'
class GreetingClassComponent extends Component {
render() { // 내부 render 메소드 사용
return <div>안녕하세요~ <b>{this.props.name}</b>님</div>; // this.props : class 내부 변수로 존재
}
}
// 사용 예시
() => <GreetingClassComponent name="유진"/>;
props
- react element의 attribute를 react component에 전달하는 경우
- user-defined Component에 전달되는 jsx attribute
- single object
- 여러개 attribute는 1개의 객체로 전달됨
- props는 read-only
- Component 안에서 변경 X
- 변경하고 싶다면 copy해서 변경
- props의 기본값은 true
- value를 안넘기면 true
// string template으로 쓴 이유 -> js 표현식에 그냥 {test} 를 넣으면 값이 true, false, null, undefined 등은 React에서 노출 X const TestDefaultProp = ({ test }) => <div>{`${test}`}</div>;
- 사용자 정의 Component는 대문자로 시작
- String으로 넘길때는
""
로 바로 전달 - html unescape 됨- ""를 사용하는 경우,
"<3"
-><3
으로 html decode 되서 노출
- ""를 사용하는 경우,
...
(spread operator) 사용하여 prop 전달 가능 -> component가 커질 수 있기 때문에 spread operator만 봐서는 어떤 값인지 알 수 없으므로 사용 지양// 넘기는 props가 많을 경우에는 data 그대로 받고 Component 내부에서 객체 풀어줌 const Greeting = ({ data }) => { const { name, message = '반가워', test = 'test' } = data; return ( <div> 안녕하세요~ <b>{name}</b>님 </div> ) }; // 사용 예시 const data = { name: '아무개', message: '환영해' }; () => <Greeting data={data} />
- Array로 React element를 반환하는 경우
key
prop을 꼭 추가한다- Virtual DOM에서 변경될 때 array 변경시 일일히 모두 비교하기 어렵기 때문에 key 가 추가되어야하는 이유
key
는 해당 array 사이에서 unique 해야 함 (전체 말고 array 내에서만)- 배열 요소의 최상위 element에 key prop을 할당해야 함
- array의 index는 최후의 수단으로 key에 사용 -> 사용 지양
- key 사용 이유: diff algorithm
// key 없으면 아래와 같은 warning 발생 // Warning: Each child in a list should have a unique "key" prop. {teams.map((team) => <TeamRow key={team.name} team={team} />)}
- 태그 내부의 값은
props.children
으로 전달 #- open & closing 태그 내의 값은 props.chilren으로
All React components must act like pure functions with respect to their props.
- 순수 함수 : 동일한 param이 들어가면 결과가 항상 동일해야 함
- 같은 props가 들어가면 동일한 rendering이 되어야 함
Children
- 아래와 같은 경우 children이 될 수 있음
- String literal
- JSX Children
<MyContainer> <MyFirstComponent /> <MySecondComponent /> 안녕 </MyContainer>
- Array of React elements
- Javascript expression
- Function as children #
// 함수를 children으로 넘기고, Component에서 children을 받아서 함수 실행 // Calls the children callback numTimes to produce a repeated component function Repeat(props) { let items = []; for (let i = 0; i < props.numTimes; i++) { items.push(props.children(i)); } return <div>{items}</div>; } function ListOfTenThings() { return ( <Repeat numTimes={10}> {(index) => <div key={index}>This is item {index} in the list</div>} </Repeat> ); }
- 참고: render-prop
// hooks이 생기면서 사용은 점점 안하는 듯 <DataProvider render={data => ( <h1>Hello {data.target}</h1> )}/>
- Booleans, null, undefined 는 무시됨
- falsy 값 중 0은 render 되므로 conditional render 할 때 주의
const ConditionalChildren = () => { const showGreeting = false const list = [] // 기존 js에서는 hasList가 0이면 아무것도 노출이 안될텐데 React에서는 hasList가 0인 falsy한 값이면서 표현식 내부면 0 그대로 노출됨 // const hasList = list.length // boolean 타입으로 변경 const hasList = list.length > 0 return ( <> {showGreeting && <div>반가워요!</div>} {hasList && list.map(item => <div key={item}>{item}</div>)} </> ) };
- 값을 그대로 render 하고 싶다면 stringify 할 것 or string literal
[Typescript] Property 'value' does not exist on type 'EventTarget'.
이슈
const onChange = (event: React.FormEvent<HTMLInputElement>) => {
const {
target: { value }
} = event;
setValue(value);
};
return (
<div>
<form onSubmit={onSubmit}>
<input
type="text"
value={value}
onChange={onChange}
placeholder="username"
/>
<button>Log in</button>
</form>
</div>
);
- React Masterclass를 듣는 중, 강사님이 Typescript에서는 currentTarget을 사용하래서 왜 그런지 궁금해 찾게됨
- 실제로 event.currentTarget 대신 늘 하던대로 event.target에서 value를 가져오니 다음과 같은 오류 발생
Property 'value' does not exist on type 'EventTarget'.
target
: 실제 이벤트가 발생하는 요소currentTarget
: 이벤트가 걸린 요소
- 궁금해서 중간에 console로 정말 event.target에 value가 없나 싶어 찍어봤지만, 존재함.. 그럼 왜?
원인
- https://www.designcise.com/web/tutorial/how-to-fix-property-does-not-exist-on-type-eventtarget-typescript-error
event.target
은 기본적으로 Typscript의EventTarget
타입을 갖고 있는데,EventTarget
타입이Element
타입을 상속받지 않기 때문에 Typescript 입장에서는Element
의 id, class 등의 속성을 알아차리지 못함
그럼 왜 EventTarget 타입은 Element 타입을 상속받지 않나 ?
- 모든
EventTarget
이 HTML elements라는 보장이 없기 때문 EventTarget
은 XMLHttpRequest, FileReader, AudioNode, AudioContext 등이 될 수 있음
해결 방안
1. Typescript에게 EventTarget의 정확한 타입 설명
const onChange = (event: React.FormEvent<HTMLInputElement>) => {
const target = event.target as HTMLInputElement;
setValue(target.value);
};
- 원문에서는
HTMLElement
만 지정해줘도 된다지만, 난 정확한 타입을 지정해줘야 오류가 사라졌음
2. currentTarget
- target과 currentTarget이 동일하다면, 강사님처럼 currentTarget으로 대체 사용 가능
[React JS 마스터클래스] Chapter #5 State Management
📌 Recoil
- React에서 사용하는 state management 라이브러리
- 미니멀하고 간단해서 사용하기 쉬움
State Management Before Use Recoil
- Recoil을 사용하지 않고 global state로 관리하려면, 상위에서 하위에 해당 state를 사용하는 모든 컴포넌트에 state를 내려줘야 하는 불편함
- 현재 코드 상으로는 Toggle Mode 버튼이 Header 컴포넌트에 위치해야 함
- 하지만 global한 state 관리를 위해서 App에서 isDark state를 두어야 하고, 버튼 클릭시 모드가 변경되게 하기 위해서는
toggleDark
함수를 App -> Router -> Header로 props로 전달 해야 함 - 2단계 아래로 props를 전달해야 하는 불편함
- 심지어 Chart 컴포넌트에서 차트의 다크모드 여부를 알기 위해서는 App -> Router -> Exchange -> Chart로 무려 3단계나 isDark state를 전달해야함... 매우 비효율적
global state
global state
는 어플리케이션이 특정 value에 접근해야 할 때 컴포넌트가 어디있던지 누가 접근하고자 하는지 상관없이 쓰임- 어플리케이션 전체에서 공유되는 값
- 로그인 여부?
State Management After Use Recoil
- Recoil을 사용해서 부모가 자식에게 props로 내려주는 계층 구조 대신에 state를
어떤 곳
에 두고 컴포넌트 어디서든 접근 가능하도록 할 수 있음 - state를 수정하고 싶다면
어떤 곳
에 있는 state를 수정하면 됨
Recoil
// 1. index.tsx
import { RecoilRoot } from "recoil";
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
// 2. atoms.ts
// atom 정의
import { atom } from "recoil";
export const isDarkAtom = atom({
key: "isDark",
default: false
});
// 3. App.tsx
// atom 연결
import { useRecoilValue } from "recoil";
import { isDarkAtom } from "./routes/atoms";
const isDark = useRecoilValue(isDarkAtom);
- https://recoiljs.org/ko/
- 위에서 말한 state를 두는
어떤 곳
을 Recoil에서는Atom
이라 부름 - atom에는 내가 원하는 각기 다른 state 넣을 수 있고 특정 컴포넌트에 종속되지 않고 컴포넌트에서 value가 필요하면 직접 atom과 연결해서 받아오면 됨
- global state를 어플리케이션의 분리된 공간에서 관리하는 것이 훨씬 나은 방법 -> value가 필요한 컴포넌트만 value를 가지게 됨
NPM
FE 개발에 Node.js가 필요한 이유?
- JS 스펙의 빠른 발전에 비해 브라우저의 지원 속도는 항상 뒤쳐짐
- 아무리 편리한 스펙이 나와도 이것을 구현해 줄 수 있는 징검다리 역할인 바벨 같은 도구를 사용해야 적용 가능
- 웹펙, NPM 같은 노드 기술로 만들어진 환경에서 사용할 때 비로소 제대로 자동화된 FE 개발 환경을 만들 수 있음
LTS 버전 ?
- LTS : Long Term Support
- 일반적인 경우보다 장기간에 걸쳐 지원하도록 특별히 만들어진 SW 버전
- 보통 짝수 버전이 LTS 버전이며 안정적이고 신뢰도가 높고, 불안정할 수 있지만 최신 기능을 지원하는 것이 홀수 버전
- 서버에는 LTS 버전이 사용되고, 개발환경 구축에 사용할 때는 홀수 버전인 최신 버전을 사용
NPM
- 개발 프로젝트는 외부 라이브러리를 다운로드 받고 빌드 하는 등의 일련의 명령어를 자동화해서 프로젝트를 관리하는 도구가 존재
- NPM은 JS 기반의 프로젝트 빌드 도구인 셈
- package.json에 등록한 scripts를 이용해서 프로젝트를 실행
- 빌드, 테스트, 배포, 실행 등의 명령어를 등록 후 npm 명령어로 실행
기본적으로 가진 npm 명령어 이외에 커스텀으로 추가한 명령어는 `run`을 추가해서 실행
$ npm run build
$ npm run lint
이외의 참고 명령어
$ npm view ${모듈명} versions // 모듈의 전체 버전 확인
$ cat node_modules/${모듈명}/package.json // 모듈의 설치된 버전 확인
패키지 설치 방식
광고 업무를 하면서 어떤 프로젝트는 CDN을 사용하거나 아니면 직접 모듈 파일을 다운로드해서 넣거나, NPM을 사용해서 의존성을 관리하는 경우가 있어서 이 차이가 궁금해 서치해봄 !
CDN
- 대부분 템플릿에서 사용 중인 웹 플레이어 같은 경우는 모두 동영상 인프라팀에서 제공하는 배포 URL을 가져다가 사용함
- (정확히 말하면 cdn 방식으로 호출해서 그대로 사용하는 정석인? 방법은 아니지만 의미는 동일 !)
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
- CDN(Contents Delivery Network)으로 제공하는 라이브러리를 직접 가져오는 방식
- CDN 서버 장애로 인해 외부 라이브러리를 못가져오게 된다면, 우리 앱 서버가 정상이더라도 앱이 정상적으로 동작하지 않을 것
직접 다운로드
- 이 방식은 휴먼 에러를 발생시킬 수 있음
- 라이브러리는 계속 업데이트 될 것이고 최신 버전이 필요할 때, 매번 직접 다운로드하는 것은 귀찮은 일
- 버전에 따라 하위 호환성 여부까지 확인하려면 실수가 생길 수 있음
- 그렇다면 라이브러리를 한 곳에서 업데이트하고 하위 호환되는 안전한 버전만 다운받아 사용할 수 있다면?
NPM
- NPM을 사용해서 모듈 의존성을 관리
npm install
명령어로 외부 패키지를 프로젝트 폴더에 다운로드- package.json에 설치한 패키지 정보가 기록됨
- 빌드 후, 프로젝트 번들 js 파일에 패키지가 포함되어 빌드된 상태로 갖고 있는 것
"dependencies": {
"@egjs/flicking": "^2.5.0"
}
(~)틸트 or (^)캐럿
- 버전 번호를 관리하기 위한 규칙이 필요한데, 이 체계를
유의적 버전
이라고 함 - NPM은 유의적 버전을 따르는 전제 아래 패키지 버전을 관리함
- 유의적 버전은 Major, Minor, Patch 3가지 숫자를 조합해서 버전 관리
- 예) 16.12.3 : 주 버전이 16, 부 버전이 12, 수 버전이 3
주 버전(Major)
: 기존 버전과 호환되지 않게 변경한 경우부 버전(Minor)
: 기존 버전과 호환되면서 기능이 추가된 경우수 버전(Patch)
: 기존 버전과 호환되면서 버그를 수정한 경우
(~)틸트 방식
- 마이너 버전이 명시되어 있으면 패치 버전만 변경
- ~1.2.3 표기는 1.2.3부터 1.3.0 미만까지 포함
- 하지만 마이너 버전이 없으면 마이너 버전을 갱신
- 보통 라이브러리 정식 릴리즈 전에는 마이너 버전이 변하더라도 하위 호환성을 지키지 못하고 배포하는 경우가 빈번
- 그래서 틸트 방식은 하위 호환성을 지키지 못하는 마이너 버전으로 갱신되어 버릴 수 있는 문제 발생 가능 !!
(^)캐럿 방식
- 정식 버전에서 마이너와 패치 버전을 변경함
- ^1.2.3 표기는 1.2.3부터 2.0.0 미만까지 포함
- 정식 버전 미만인 0.x 버전은 패치만 갱신
- ^0 표기는 0.0.0부터 0.1.0 미만까지 포함
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.