모던 리액트 Deep Dive 책을 읽고 Remind할 내용들을 정리했습니다.
리액트는 단방향 바인딩만 지원
- 데이터의 흐름이 한쪽으로만 간다
- 양방향으로 바인딩되면 뷰의 변화가 컴포넌트에 영향을 미칠수도, 반대로 컴포넌트의 상태가 변경되면 뷰의 상태도 변할 수 있음
// 리액트의 경우 name이 변경되는 경우는 setName이 호출될 때 뿐이다.
// name이 변경된 이유를 찾고 싶다면 setName이 호출된 곳을 찾으면 된다.
function App {
const [name, setName] = useState('');
function onChange(e) {
setName(e.target.value);
}
JSX(JavaScript XML) 사용
강력한 커뮤니티
자바스크립트의 모든 값은 데이터 타입을 갖고 있다.
null이 가지고 있는 특별한 점 하나는 다른 원시값과 다르게 typeof(null)
확인했을 때, null 타입이 아니라 object
라는 결과가 반환된다.
빈 객체, 빈 배열은 truthy
한 값이다.
Number 타입은 -2의 53승 ~ 2의 53승 까지 다룰 수 있다.
Symbol
은 ES6에서 새롭게 추가된 타입으로, 중복되지 않은 어떠한 고유한 값을 나타내기 위해 만들어졌다. 심벌은 심벌 함수를 이용해서만 만들 수 있다.
const key = Symbol('key')
const key2 = Symbol('key')
key === key2 // false
Symbol.for('hello') === Symbol.for('hello') // true
원시 타입과 객체 타입의 가장 큰 차이점은 저장하는 방식의 차이다. 원시 타입은 불변 형태의 값으로 저장되고 이 값은 변수 할당 시점에 메모리 영역
을 차지하고 저장된다.
객체는 값을 저장하는게 아니라 참조를 저장하기 때문에 동일하게 선언했던 객체라 하더라도 저장하는 순간 다른 참조를 바라본다.
리액트에서 사용하는 동등 비교는 ==나 ===가 아닌 Object.is
이다.
요약하자면, Object.is
로 먼저 비교를 수행한 다음에 객체 간 얕은 비교를 한 번 더 수행한다.
객체 간 얕은 비교란 객체의 첫 번째 깊이에 존재하는 값만 비교한다는 것을 의미
객체의 얕은 비교까지만 구현한 이유는?
먼저, 리액트에서 사용하는 JSX Props는 객체이고, 여기에 있는 props만 일차적으로 비교하면 되기 때문
(기본적으로 리액트는 props에서 꺼내온 값을 기준으로 렌더링을 수행하기 때문에 일반적인 케이스에서는 얕은 비교로 충분할 것, 이러한 특성을 안다면 props에 또 다른 객체를 넘겨준다면 리액트 렌더링이 예상치 못하게 작동한다는 것을 알 수 있다.)
hello() // hello is not a function
let hello = function () {
console.log('hello')
}
hello()
(function (a, b) {
return a + b
})(10, 24);
// 일반적으로 이름을 붙히지 않는다
((a,b) => {
return a + b
},
)(10, 24)
// 자바스크립트는 기본적으로 함수 레벨 스코프를 따른다
if (true) {
const a = "text"
}
console.log(a) // "text"
function hello() {
let b = "text"
}
console.log(b); // b is not defined
function outerFunction() {
let x = "hello"
function innerFunction(){
console.log(x);
}
return innerFunction
}
const innerFunction = outerFunction();
innerFunction(); // "hello"
outerFunction은 innerFunction을 반환하며 실행을 종료했다. 반환한 함수에는 x라는 변수가 존재하지 않지만, 해당 함수가 선언된 어휘적 환경, 즉 outerFunction에는 x라는 변수가 존재하며 접근할 수 있다.
클로저를 사용하는 데는 비용이 든다. 클로저는 생성될 때 마다 그 선언적 환경을 기억해야 하므로 추가로 비용이 발생한다.
태스크 큐는 비동기 작업을 수행하는 큐이다. 태스트 큐와 다르게 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다.
마이크로 태스크 큐에는 대표적으로 Promise가 있다. 태스크 큐에 들어가는 setInterval, setTimeout보다 Promise가 먼저 실행된다. 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미뤄진다.
console.log(a);
setTimeout(()=>{console.log('b')},0)
Promise.resolve().then(()=>{ console.log('c')})
window.requestAnimationFrame(()=>{
console.log('d')
})
위 코드를 실행하면 a, c, d, b 순서로 출력된다. 브라우저에 렌더링 하는 작업은 마이크로 태스크 큐와 태스크 큐 사이에서 일어난다는 것을 확인할 수 있다.
바벨은 자바스크립트의 최신 문법을 다양한 브라우저에서도 일관적으로 지원할 수 있도록 코드를 트랜스파일한다.
const array = [1, 2, 3, 4, 5]
const [first, second, third, ...arrayRest] = array
// first 1
// second 2
// third 3
// arraryRest [4, 5]
// 기본값을 선언할 수도 있음, undefined일 때에만 기본값을 사용함
const array = [1, 2]
const [a = 10, b = 10, c = 10] = array
// a 1
// b 2
// c 10
const object = {
a: 1,
b: 1,
c: 1,
d: 1,
e: 1,
}
const { a, b, c, ...objectRest } = object
// a 1
// b 1
// c 1
// objectRest = {d: 1, e: 1}
useState가 객체가 아닌 배열을 반환하는 이유는 객체 구조 분해 할당은 사용하는 쪽에서 원하는 이름으로 변경하는 것이 번거롭다.
객체 전개 구문에 있어서 순서 중요, 전개 구문 이후에 값 할당이 있다면 전개 구문이 할당한 값을 덮어쓰겠지만 반대의 경우 오히려 전개 구문이 해당 값을 덮어쓰는 일이 벌어진다.
const arr = [1, 2, 3, 4, 5]
const sum = arr.reduce((result, item) => {
return result + item
}, 0)
// 15
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": true,
"target": "es5",
},
"include": ["./src/**/*"]
}
// outDir은 .ts나 .js가 만들어진 결과를 넣어두는 폴더.
// tsc는 타입스크립트를 자바스크립트로 변환하는 명령어, tsc를 사용하면 결과물이 outDir로 넘어감
// allowJs는 .js 파일을 허용할 것인지 여부
// target에는 결과물이 될 자바스크립트 버전을 지정
// include에는 트랜스파일할 자바스크립트와 타입스크립트 파일을 지정
@babel/plugin-transform-react-jsx
플러그인에 의해 jsx가 자바스크립트로 변환된다.DOM은 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다. 브라우저가 웹사이트 접근 요청을 받고 화면을 그리는 과정은 아래와 같다.
display: none
과 같이 사용자 화면에 보이지 않는 요소는 방문해 작업하지 않음. 트리 분석 하는 과정을 조금이라도 빠르게하기 위함SPA(Single Page Application)이 나오기 전에는 페이지가 변경되는 경우 다른 페이지로 가서 처음부터 HTML을 새로 받아서 다시금 렌더링 과정을 시작했다. 하지만, SPA의 경우 하나의 페이지에서 계속해서 요소의 위치를 재계산하게 된다. DOM을 관리하는 과정에서 부담해야 할 비용이 커진다. 사용자의 인터렉션에 따라 DOM의 모든 변경 사항을 추적하는 것은 개발자 입장에서 너무나 수고스러운 일이다.
개발자는 인터랙션에 모든 DOM의 변경보다는 결과적으로 만들어지는 DOM 결과물 하나만 알고 싶을 것이다. 인터랙션에 따른 DOM의 최종 결과물을 간편하게 제공하는 것은 브라우저뿐만 아니라 개발자에게도 매우 유용하다. 이런 문제점을 해결하기 위해 탄생한 것이 가상 DOM이다.
리액트에서 key는 리렌더링이 발생하는 동안 형제 요소들 사이에서 동일한 요소를 식별하는 값이다.
// 일반적인 useState
const [count, setCount] = useState(1)
// 게으른 초기화
const [count, setCount] = useState(
() => Number.parseInt(window.localStorage.getItem(cacheKey)),
)
// 1
function Component() {
useEffect(()=>{})
}
의존성 배열에 아무런 값도 넘기지 않는다면 (빈 배열 조차도 넘기지 않는다면) 의존성을 비교할 필요 없이 렌더링할 때마다 실행이 필요하다고 판단해 렌더링이 발생할 때마다 실행됨
→ 일반적으로, 컴포넌트가 렌더링됐는지 확인하기 위한 방법으로 사용됨
// 2
function Component() { console.log('a') }
useEffect는 클라이언트 사이드에서 실행되는 것을 보장해준다. 다시 말해 1번은 컴포넌트가 렌더링이 완료되고 나서 실행되고 2번은 렌더링되는 도중에 실행된다. 따라서 2번의 경우 서버 사이드 렌더링의 경우에 서버에서도 실행된다.
의존성 배열을 비교할 때 이전 값과 현재 값은 얕은 비교를 한다. Object.is를 기반으로 하는 얕은 비교를 수행한다.
useEffect의 수가 적거나 복잡성이 낮다면 첫 번째 인수로 익명 함수를 넘겨줘도 상관없지만, 코드가 복잡하고 많아질수록 무슨 일을 하는 useEffect 코드인지 파악하기 어려워진다. 이 때 useEffect의 인수를 익명 함수가 아닌 적절한 이름을 사용한 기명 함수로 바꾸자.
useEffect(
function logActiveUser() {
logging(user.id)
},
[user.id],
)
function Component(){
const inputRef = useRef()
console.log(inputRef.current) // 렌더링 전이므로 undefined
useEffect(() => {
console.log(inputRef.current) // <input type="text />
}, [inputRef])
return <input ref={inputRef} type="text />
}
원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해두고 싶다면 useRef를 사용하는것이 좋음
useState의 심화 버전이라고 볼 수 있음
function reducer() {
switch (action.type){
case 'up':
return { count: state.count + 1 }
default:
throw new Error('error');
}
}
const initialState = { count: 0 };
function App(){
const [state, dispatcher] = useReducer(reducer, initialState, init);
function handleUpButtonClick(){
dispatcher({ type: 'up' })
}
...
부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅
const Input = forwardRef((props, ref) => {
useImperativeHandle(
ref,
() => ({
alert: () => alert(props.value),
}),
[props.value],
)
return <input ref={ref} {...props} />
})
function App() {
const inputRef = useRef()
const [text, setText] = useState('');
function handleClick() {
inputRef.current.alert()
}
return (
<>
<Input ref={inputRef} value={text} />
...
모든 DOM의 변경 후에 useLayoutEffect의 콜백 함수가 동기적으로 발생
동기적으로 발생한다는 것은 useLayoutEffect의 실행이 종료될 때까지 기다린 다음 그린다는 것을 의미함, 이 점을 유의하여 웹 어플리케이션 성능에 문제가 발생하지 않도록 유의
DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용하자. (DOM 요소를 기반으로 한 애니메이션, 스크롤 위치 제어 등 화면에 반영되기 전에 하고 싶은 작업)
고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 더욱 큰 영향력을 컴포넌트에 미칠 수 있음
단순히 값을 반환하거나 부수 효과를 실행하는 사용자 정의 훅과는 다르게 컴포넌트의 결과물에 영향을 미칠 수 있는 다른 공통된 작업을 처리할 수 있음
사용자 정의 훅이 use로 시작했다면, 고차 컴포넌트는 with로 시작해야 하는 관습이 있음
고차 컴포넌트는 반드시 컴포넌트를 인수로 받게 되는데, 컴포넌트의 props를 임의로 수정, 추가, 삭제하는 일은 없어야함
여러 개의 고차 컴포넌트로 감싸면 복잡성이 커지므로 지양하고, 고차 컴포넌트는 최소한으로 사용하자
서버가 사용자에게 렌더링을 제공할 수 있을 정도의 충분한 리소스가 확보되어있을 때의 경우임, 리소스를 확보하기 어렵다면 오히려 SPA보다 느려질 수 있음
누적 레이아웃 이동이란 사용자에게 페이지를 보여준 이후에 뒤늦게 어떤 HTML 정보가 추가되거나 삭제되어 마치 화면이 덜컥 거리는 것과 같은 부정적인 사용자 경험을 말함
자바스크립트 리소스 실행은 사용자의 디바이스에서만 실행되어 사용자 디바이스 성능에 의존적임. 서버 사이드 렌더링을 수행하면 이런 부담을 서버가 덜어감.
API 호출과 인증 같이 사용자에게 노출되면 안되는 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에게 제공해 보안위협을 피할 수 있음
서버사이드에서 작성하는 코드는 브라우저 전역 객체인 window, sessionStorage 사용에 제한이 있음
SPA는 단순히 HTML, js, css 리소스를 다운로드할 수 있는 준비만 하면됨. 서버 사이드 렌더링은 사용자의 요청을 받아 렌더링을 수행할 서버가 필요.
function ChildrenComponent({ fruits }) {
useEffect(() => {
cosnole.log(fruits);
}
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>
{fruit}
</li>
))}
</ul>
)
}
function SampleComponent() {
return (
<>
<div>hello</div>
<ChildrenComponent fruits={['apple', 'banana', 'peach']} />
</>
)
}
const result = ReactDOMServer.renderToString(
React.createElement('div', { id: 'root' }, <SampleComponent />),
)
renderToString
을 사용해서 실제 브라우저가 그려야 할 HTML 결과를 만들어낸 모습이 아래와 같다.
<div id='root' data-reactroot=''>
<div>hello</div>
<ul>
<li>apple</li>
<li>banana</li>
<li>peach</li>
</ul>
</div>
눈여겨 볼 만한것은 useEffect 같은 클라이언트 사이드에서 실행되는 훅은 실행되지 않는다.
renderToString과 매우 유사하지만 루트 요소에 추가했던 data-reactroot와 같은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다. 리액트에서만 사용하는 속성을 제거하면 결과물인 HTML의 크기를 아주 약간이라도 줄일 수 있는 장점이 있음
renderToStaticMarkup은 리액트의 이벤트 리스너가 필요 없는 완전히 순수한 HTML을 만들때에만 사용함
renderToString과 결과물이 완전히 동일하지만, HTML의 결과물이 매우 큰 경우 사용한다. 스트림을 활용하여 큰 크기의 데이터를 청크 단위로 분리해 순차적으로 처리할 수 있음
renderToNodeStream에서 순수한 HTML 결과물이 필요할 때 사용
리액트 팀 또한 리액트 서버 사이드 렌더링을 직접 구현해 사용하는 것보다는 리액트 팀과 긴밀하게 협조하고 있는 Next.js 같은 프레임워크를 사용하는 것을 권장하고 있음
getServerSideProps의 props로 내려줄 수 있는 값은 JSON으로 제공할 수 있는 값으로 제한, class나 Date 등은 props로 제공할 수 없음
API 호출 시 /api/some/path
와 같이 protocol과 domain 없이 fetch 요청을 할 수 없음, 브라우저와 다르게 서버는 자신의 호스트를 유추할 수 없기 때문
어떤 조건에 따라 다른 페이지로 보내고 싶다면 redirect를 사용할 수 있음
→ 해당 페이지를 보여주기도 전에 원하는 페이지로 보내버릴 수 있어 사용자에게 훨씬 더 자연스럽게 보여줄 수 있음!
export const getServerSideProps: GetServerSideProps = async (context) => {
const { query: { id = '' },} = context
const post = await fetchPost(id.toString())
if (!post) {
redirect: {
destination: '/404'
}
}
return {
props: { post },
}
}
뷰(HTML)가 모델(javascript)를 변경할 수 있으며, 반대의 경우 모델도 뷰를 변경할 수 있음. 이는 코드 양이 많아지고 시나리오가 복잡해지면 관리가 매우 어려워짐. 페이스북 팀은 양방향이 아닌 단방향으로 데이터 흐름을 변경하는것을 제안 → Flux 패턴의 시작
리덕스는 하나의 상태 객체를 스토어에 저장해두고, 이 객체를 업데이트하는 작업을 디스패치해 업데이트를 수행함 → reducer로 트리거
하나의 상태를 바꾸고 싶어도 해야할 일이 너무 많음 → 보일러 플레이트 부담
const MemoizedComponent = memo(function() {
return <>MemoizedComponent</>
})
MemoizedComponent.displayName = "메모 컴포넌트"
크롬 개발자 도구에서 웹사이트를 제대로 디버깅하고 싶다면 시크릿 모드 또는 프라이빗 모드라 불리는 개인정보 보호 모드에서 페이지와 개발자 도구를 여는 것을 권장
→ 브라우저에 설치돼 있는 각종 확장프로그램이 전역 변수나 HTML 요소에 추가할 수 있음
메모리 탭을 열면 리액트 개발 도구의 프로파일과 비슷하게 프로파일링 작업을 거쳐야 원하는 정보를 볼 수 있음
프로파일링에 앞서 자바스크립트 인스턴스 VM을 선택함. 환경별 힙 크기를 볼 수 있는데, 실제 해당 페이지가 자바스크립트 힙을 얼마나 점유하고 있는지 나타냄. 이 크기만큼 브라우저에 부담을 줌
얕은 크기: 객체 자체가 보유하는 메모리 바이트의 크기
유지된 크기: 해당 객체 자체 뿐만 아니라 다른 부모가 존재하지 않는 모든 자식 객체들의 크기까지 더한 값
메모리 누수를 찾을 때는 얕은 크기는 작으나 유지된 크기가 큰 객체를 찾아야 함
→ 두 크기의 차이가 큰 객체는 다수의 다른 객체를 참조하고 있다는 뜻
→ 이는 해당 객체가 복잡한 참조 관계를 가지고 있다는 뜻
eslint-plugin-import
라는 패키지는 자바스크립트에서 다른 모듈을 불러오는 import와 관련된 다양한 규칙을 제공리액트 17 버전 이상을 사용하고 있따면 import React 구문을 모두 확인한 후에 제거하자
만약 일부 코드에서 특정 규칙을 임시로 제외시키고 싶다면 eslint-disable-
주석을 사용하자
// 특정 줄만 제외
console.log('hello world') // eslint-disable-line no-console
// 다음 줄 제외
// eslint-disable-next-line no-console
console.log('hello world')
// 특정 여러 줄 제외
/* eslint-disable no-console */
console.log('a')
console.log('b')
/* eslint-enable no-console */
// 파일 전체에서 제외
/* eslint-disable no-console */
console.log('c')
HTML의 특정 요소와 관련된 임의 정보를 추가할 수 있는 HTML 속성, HTML의 특정 요소에 data-로 시작하는 속성은 무엇이든 사용할 수 있음
jest.spyOn(window, 'fetch').mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(MOCK_TODO_RESPONSE),
}),
) as jest.Mock,
)
위 코드는 모든 시나리오(서버 오류 등)를 테스트 할 수 없으므로 (일일이 모킹해야함), MSW를 사용해야함
import { rest } from 'msw'
import { setupServer } from 'msw/node'
// 응답을 모킹
const MOCK_TODO_RESPONSE = {
userId: 1,
id: 1,
completed: false,
}
// 서버 생성
const server = setupServer(
rest.get('/todos', (req, res, ctx) => {
const todoId = req.params.id
return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) }))
}
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
beforeEach(() => {
render(<FetchComponent />)
})
{
"$schema" : "https://json.schemastore.org/tsconfig.json"
}
tsconfig.json을 작성하기 전에 위와 같이 JSON 최상단에 $schema키와 해당하는 값을 넣어주면 IDE에서 자동 완성이 가능해짐
이 밖에도 .eslintrc, .prettierrc와 같이 JSON 방식으로 설정하는 라이브러리가 schemastore에 해당 내용을 제공하고 있다면 더 편리하게 JSON 설정을 작성할 수 있음
파일 구조에 정답은 없고, path alias를 적용한다면 코드 내에서의 가독성 확보
요즘 대다수의 서비스가 마이크로 프론트엔드를 지향하기 때문에 프로젝트를 구축할 일이 잦아질 것
→ 템플릿 사용
먼저, 보일러 플레이트 프로젝트를 만든 다음, 깃허브에서 ‘Template repository’ 옵션을 체크해두자
다른 저장소를 생성할 때 이 템플릿을 사용할 수 있음
머지하기 전에 꼭 성공해야 하는 액션이 있다면 저장소에 브랜치 보호 규칙을 추가할 수 있음
특히, build를 실패했을 때 머지를 하지 못하게 설정할 수 있음 (build.yaml 파일 작성)
빠르게 배포할 수 있는 서비스
리액트 17버전은 16버전과 다르게 새롭게 추가된 기능이 없으며 호환성이 깨지는 변경 사항, 기존에 사용하던 코드의 수정을 피룡로 하는 변경 사항을 최소화했음
따라서 16 → 17 버전의 업그레이드는 대부분 순조롭게 함
16 버전에서는 모든 이벤트는 document에 부착되었는데, 17 버전에서는 document가 아니라 리액트 최상단 요소에 추가 됨
리액트 17부터 바벨과 협력해 import 구문 없이도 JSX를 변환할 수 있게 됨
→ 불필요한 import 구문을 삭제해 번들링 크기를 약간 줄임
16 버전까지는 동기적으로 실행돼 클린업 함수가 완료되기 전까지는 다른 작업을 방해하므로 불필요한 성능 저하로 이어졌었음
→ 17 버전에서는 화면이 완전히 업데이트된 이후에 클린업 함수가 비동적으로 실행, 즉 화면이 업데이트가 완전히 끝난 이후에 실행되도록 바뀜
리액트 18에서는 리액트 17에서 하지 못했던 다양한 기능들이 추가됨
useId는 컴포넌트별로 유니크한 값을 생성하는 새로운 훅
서버사이드와 클라이언트 간에 동일한 값이 생성되어 하이드레이션 이슈도 발생하지 않음
상태 변경으로 인해 무거운 작업이 발생하고, 이로 인해 렌더링이 가로막힐 여지가 있는 경우 사용
export default function TabContainer() {
const [isPending, startTransition] = useTransition()
const [tab, setTab] = useState<Tab>('about')
function selectTab(nextTab: Tab){
startTransition(() => {
setTab(nextTab)
})
}
return (
<>
{isPending ? ('로딩중') : (<> {tab === 'about' && <About />}{ tab === 'post' && <Post />} </>)
</>
)
}
멋진 리뷰