템플릿 제작자 : Jbee
템플릿 : [gatsby-starter-bee](https://github.com/JaeYeo pHan/gatsby-starter-bee)
너무나도 멋진 베이스 만들어주셔서 감사합니다.
React의 UI를 구성하는 요소를 js 내부에 포함시켜 하나의 컴포넌트라고 불리는 jsx를 사용함. babel을 통해 React.createElement로 컴파일 한다고 함
ReactDOM.render는 단 한번만 호출이 되며, ReactDOM은 해당 엘리먼트와 자식 엘리먼트들에서 변경이 되는 부분만을 업데이트 한다.
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
)
ReactDOM.render(element, document.getElementById('root'))
}
setInterval(tick, 1000)
위의 메소드를 setInterval로 반복하여 실행하여 전체의 UI를 render 시켜도 오로지 h2의 텍스트만 변경된다.
ReactDOM.render에 사용되는 엘리먼트는 <Welcome />
과 같은 사용자 정의 컴포넌트로도 가능함
<div>{word}</div>
와 같은 React 엘리먼트(jsx)를 받는다.<Welcome/>
과 같은 사용자 정의 컴포넌트로 대체될 수 있으며, 이러한 컴포넌트들은 하나 혹은 여러개의 엘리먼트 또는 또 다른 사용자 정의 컴포넌트를 return 한다.<div> -> <span>
에는 해당 엘리먼트와 자식들을 모두 버리고 새롭게 생성함. 이 때문에, unmount, mount가 다시 호출되는 것이러한 특징 때문에, React는 단방향식 데이터 흐름이라고 한다.
<Component id={1} />
등등들은 React.createElement(엘리먼트, 속성, 자식들) 으로 컴파일 되어 작동된다class가 아닌 함수형 컴포넌트에 React의 state와 생명주기 기능을 연동시켜주는 함수이다.
React
에서 전역상태를 관리할 수 있는 hook
인데, 모듈을 배포할 때 사용된 방식이 인상깊어서 기록에 남김.
K?????F????
생성된 context
의 범위를 결정하는 Provider
내부에 위치하는 자식 컴포넌트들이 오로지 null
만을 반환하도록 설정되어있음
이 컴포넌트들은 ReactElement
를 반환하는것이 아니라 하나의 함수로서 호출이되었을 때, 전역상태에 어떠한 상태값을 할당하는 역할로만 수행됨.
생각해보니 그러한 방법이 React-Router
와 같이 자주 사용하는 모듈에서도 갖고있는 특징 같음.
return (
<Router>
<Route path="/" component={Home} />
<Route path="/something" component={Something} />
</Router>
)
실제로 ReactElement
를 생성하는 아니라 Router Provider
가 관리하는 전역 상태의 상태값을 업데이트 하는 로직들이 내부에 포함되어있는 사용자정의 컴포넌트
전역상태의 최신상태를 알 기 위해, 해당 상태값을 사용하는 컴포넌트들은 모두 Provider
내부에 위치해야 하는 특성을 고려하였을 때,
만약, 이를 컴포넌트 형식이 아니라 hook
형식으로 모듈을 제공했더라면, 사용하는 사람은 Provider
역할을 하는 Router
내부에, 전역상태를 업데이트시켜주는 로직을 스스로 만들고 그것들을 갖고있는 컴포넌트들을 별도로 생성해야 했을 것이다.
여러곳에 해당 모듈의
hook
들이 뿌려질수도 있고, 그 역할을 수행하기 위한 컴포넌트들이 더 생성될 수 도 있는 상황
그러한것을 대신해주기 위해 이러한 로직을 내재하고있지만, ReactElement
는 반환하지 않는 사용자정의 컴포넌트를 모듈로 만들어서 위의 문제를 대신해주도록 한것 같다. 또한, path
나 render 될 컴포넌트
와 같이 핵심적인 요소만을 사용자가 지정하도록 하여, 부가적인 로직을 작성할 필요 없이 선언형의 컴포넌트만을 작성하여도 되도록 해준게 아닐까?
Suspense는 React에서 사용되고있는 컴포넌트가 사용하고있는 데이터가 아직 준비되지 않았다는것을 React에 알려줄 수 있는 방법.
React-query와 같이 비동기과정에서 loading과같은 상태를 잡을수 있는데, 이 때의 비동기작업들은 일반 Promise를 반환하는것이 아니라 상태, 결과, 실제 promise나 해당 promise의 상태를 변수로서 클로저로 기억하여 값을 반환하는 함수를 반환하는것 같음
promise의 resolve, reject 상태에 따라 클로저로서 참조중인 상태변수도 업데이트시킴
이 때, 만약 상태가 처음의 pending이라면 throw를 통해 중단시키고 해당 promise를 에러의 값으로 전달하여 Suspense가 포착하여 fallback 컴포넌트를 반환하는것 같음
실제로, Suspense로 감싼 컴포넌트 내부에서 Promise자체를 throw를 통해 에러의 값으로 전달하면 error가 발생하는것이 아닌, Suspense에 지정한 fallback이 반환되는것을 확인할 수 있음
데이터를 받아오는 컴포넌트 내부에서 throw를 호출하여 해당 컴포넌트를 중지시키는게 핵심인것 같음. 또한 그 throw 자체를 Suspense가 감지하여 fallback을 반환하는것도
unmount가 아닌 중지임 mount 조차도 되지 않음
일종의 비동기 작업처럼 중지하고 이후로 미루는것 같음
모든 컴포넌트의 렌더링이 종료되면 중지되어있던 컴포넌트의 상위에 있던 Suspense에 전달된 fallback을 찾고 그것을 렌더링함
흔히 ...loading 같은것들
import React from 'react'
let resolve = null
const promise = new Promise(res => {
resolve = res
}).then(() => {
data = 'success data'
})
function async() {
if (!data) throw promise
return data
}
export default function App() {
return (
<div className="App">
<React.Suspense
fallback={
<button
onClick={() => {
resolve?.()
}}
>
resolve!
</button>
}
>
<InnerApp />
</React.Suspense>
</div>
)
}
function InnerApp() {
const data = async()
return <>{data}</>
}
codeSandbox에서 정말정말로 간단하게 구현해봄
이처럼 필요한 데이터가 존재하지 않을 때, 해당 컴포넌트 자체의 렌더링을 일시 중단시킬 수 있음
React의 컴포넌트 기반 선언형 프로그래밍에서 render 과정 중 발생한 에러를 핸들링할 수 있는 컴포넌트
내부의 getDerivedStateFromError
생명주기를 갖고있는 컴포넌트 하위에 존재하는 컴포넌트 내에서 발생한 에러를 포착하고, 중단시킨 다음 지정한 fallback
을 렌더링 함
mount가 안된다는 소리
try catch
와 같은 방법으로 에러를 핸들링할 수 없나?
앞서 말했던 것처럼, ReactElement
를 생성하는데 선언형의 프로그래밍 방식을 사용할 때 try catch
로는 에러를 핸들링할 수 없음
return
을 통해 렌더링 할 때, ReactElement
를 반환하는 사용자 정의 컴포넌트들 사이사이에 try catch
구문을 사용할 수가 없음
try catch
는 내부의 명령형 프로그래밍에 대한 에러를 핸들링 하기 위함이라고 함
사용자 정의 컴포넌트 내부에서
try catch
로 다른것을return
하는것은 되는것 같긴 함.
ErrorBoundary
는 사용자의 이벤트(사실 이벤트도 비동기니깐), 비동기작업 등 현재 환경에서 벗어나게 된다면 에러를 핸들링할 수 없다고 함.
function App() {
const [handleContinue, handleMount, handleFetch, handleCancel] = useAsync()
return (
<div className="App">
<button onClick={handleMount}>Mount</button>
<button onClick={handleFetch}>Fetch</button>
<button onClick={handleContinue}>Continue</button>
<button onClick={handleCancel}>Cancel</button>
<InnerApp />
</div>
)
}
function InnerApp() {
const [render, setRender] = useState(false)
throw new Error()
return <div>innerApp</div>
}
export default App
위와같은 상황에서는 InnerApp
의 렌더링 과정에서 발상한 에러로 중단되었을 때, 상위 App
의 렌더링까지도 되지 않게됨.
하지만
function App() {
const [handleContinue, handleMount, handleFetch, handleCancel] = useAsync()
return (
<div className="App">
<button onClick={handleMount}>Mount</button>
<button onClick={handleFetch}>Fetch</button>
<button onClick={handleContinue}>Continue</button>
<button onClick={handleCancel}>Cancel</button>
<ErrorBoundary>
<InnerApp />
</ErrorBoundary>
</div>
)
}
function InnerApp() {
const [render, setRender] = useState(false)
throw new Error()
return <div>innerApp</div>
}
export default App
위와같이 ErorrBoundary
로 하위의 컴포넌트에서 렌더링중 발생하는 에러에 대해 미리 포착을 하고 그에 맞는 컴포넌트를 지정해준다면, App
의 다른 ReactElement
들은 정상적으로 렌더링 되고, ErrorBoundary
내부의 ReactElement
들만 지정한 에러 전용 렌더링이 진행됨
에러가 발생하였을 때 전체 UI
가 깨지는것이 아닌 에러가 발생한 일부분만 대체하도록 도와주는것이 큰 것 같음
React Query
와 같은 모듈을 사용할 때에는, 비동기 작업이여도 에러를 잘 잡는데?React Query
등의 모듈을 사용하여 비동기작업을 관리할 때, Promise
객체를 즉시 반환하는것이 아닌 여러 상태를 클로저로서 참조하는 함수를 반환하는게 아닐까 싶음
따라서, 데이터의 요청, 에러, 성공을 하나의 값으로 기억하고 업데이트될 때마다 일반 함수 내부에서 throw
를 시키기 때문에 ErrorBoundary
가 에러를 잡을수 있는것 아닐까?
throw
될 때 전달하는 값을 객체로 하면 ErrorBoundary
의 유연함을 향상시킬 수 있음
각각의 에러에 따라 다른 컴포넌트를 렌더링 하고자 할 때, 그에 맞는
Fallback
컴포넌트를 전달하여getDerivedStateFromError
에서 렌더 이전 상태값에 할당시킬 수 있음
원래대로라면, 처음 데이터를 비동기로 받아오고, 받아온 데이터를 기반으로 새롭게 요청을 하는 과정에 있어서, 사용자가 첫 화면을 보는데 너무 오랜시간이 걸린다고 파악되어
첫 데이터가 수신된다면, 화면을 그리고 이후의 데이터는 별도의 비동기 작업으로 진행하여 데이터를 받아오도록 처리
비동기를 사용하여 진행중인 이벤트를 일시 중단하고, 다른 환경에서 resolve를 호출하여 중단된 이벤트를 이어서 진행할 수 있음
<div> -> <span>
에는 해당 엘리먼트와 자식들을 모두 버리고 새롭게 생성함. 이 때문에, unmount, mount가 다시 호출되는 것물론, diff 알고리즘이 매우 효율적이라 큰 문제는 안되지만, 우리가 React 내부에서 작성한 로직들이 다시 실행된다는것은 얘기가 다름
document.write와 같이 html을 즉시 생성하는 경우가 있어서(그 순서를 보장하기 위해?)
new Promise의 인자로 전달되는 콜백함수는 하나의 실행함수로서 프로미스의 실행, 거절을 의미하는 두가지 인자를 받는 함수이다. 이 실행함수는 Promise 객체가 반환되기 이전에 먼저 실행이 된다.
대게, 어떠한 비동기로 진행된 작업이 완료되고 내부에서 resolve 혹은 reject를 호출하여 이후의 로직을 마이크로태스크큐에 넘겨 이벤트루프를 통해 콜스택에 넘겨지도록 한다.
await이 없다면 try catch로 잡을 수 없는 이유는, Promise 내부에서 발생한 throw는 하나의 reject와 동일하게 작동되어 Promise가 거절되었다는 것은, 해당 에러는 이미 비동기로서 try catch의 영향권을 벗어난 상태이기 때문이다.
'암시적 try..catch'
가 존재하고 있고, 스스로 에러를 잡고, 에러를 거부상태의 프라미스로 변경시킨다.
(throw error -> reject)
setTimout이라는 별도의 비동기 환경에서 발상한 에러이기 때문에 내부에 try catch로 감싸고 catch 시 Promise의 reject를 호출해줘야 한다.
브라우저를 렌더링하는 과정들 파싱, 렌더트리 구축, 레이아웃 형성, 페인트, 컴포지트단계 (컴포지터 스레드 작업)을 하나의 렌더링 파이프라인이라고 한다.
클래스명에대한 고민을 줄일 수 있음
현재의 자바스크립트 환경에 쉽게 접근할 수 있고, 동적으로 생성된 값에 자유롭게 스타일링을 할 수 있음
여기서 인라인스타일이 왜 문제가 있을까라는 생각을 해보았는데,
reflow
가 발생하여 레이아웃 계산을 다시 할 때 인라인으로 작성되는 스타일은 여러 스타일이 합쳐져있는 클래스를 추가하는것으로 한번에 부여하여 일괄 계산하는것이 아니라 스타일을 하나씩 하나씩 반영하여 각각 계산하기 때문에reflow
의 비용이 비교적 더 크다는것 같다.
별도의 스타일 파일을 관리할 필요가 없음
결국 의존하는 새로운 모듈이 추가되는 것
css-in-css를 통해 완성되어있는 클래스를 즉시 반영하는것과, 자바스크립트로 된 새로운 스타일을 읽고 계산하여 반영하는것에 차이가 있다고 함
별다른 프레임워크가 아닌 테스트 기반 개발 기법을 의미한다. A라는 기능을 개발한다고 하였을 때, 실제 코드에 즉시 개발을 하는것이 아닌 다른 순서로 진행이 된다.
아직 기능에 대한 작성이 되지 않았기 때문에 당연한 오류
구현하고자 하는 기능의 특징 및 핵심적인 부분만을 고려할 수 있게 됨
테스트를 성공시키기 위한 최소한의 코드를 작성하게되어, 불필요한 코드들은 생성되지 않을것 같음.
직접 이벤트를 호출하는등의 테스트를 통해 사용자 입장에서 코드를 작성할 수 있게 됨.
개발을 완료한 기능에 대해 테스트코드를 작성하지 않더라도 로컬환경에서 실행시켜 직접확인하는틍의 테스트는 필수적이다. 하지만, 클릭 과 같은 이벤트등을 테스트하고자 할 때, 다시 테스트하기위해 원래대로 돌아와야 하는 등 부수적인 작업들이 생길 수 있는데, 테스트코드는 그럴 필요가 없다.
이전에 연습해본다고 작성했던 테스트코드들의 몇몇은 잘못된, 실패한 테스트코드인것 같음.
의미없이 단위테스트의 규모가 커졌었음.
아무래도 1번과 같이 리팩토링을 하였을 때, 테스트코드가 깨진다는점은 치명적이였고 이는 어떤 기능을 담당하는 컴포넌트 자체를 테스트하는것이 아닌 내부의 작은 기능들 자체를 테스트하였기 때문이였던것 같다.
테스트를 작성하는 목표를 생각하고 다시작성해봐야할것 같다. 사용자의 입장에서 어떠한 행위(이벤트)를 하였을 때 개발자가 의도한 대로 작동되는지를 확인하자
사용자의 입장에서 동적인 요소만(사용자의 이벤트 혹은 반복적인 변화 등등)을 테스트하고, 요소요소에 집중하기보다는 결과가 참인지를 확인하기
import React from "react";
import { render } from "utils/test";
import TimerContainer from "./timerList";
import List from "components/list/list";
const initialData = [
{
name: "무릉도원",
src: "/img/island/island_04.png",
time: ["00:00", "06:00", "12:00", "18:00"],
endTime: "18:00",
lv: 400,
position: "대항해",
contType: "ISLAND",
},
{
name: "기에나",
contType: "CO_OCEAN",
lv: "-",
src: "/img/ocean/ocean_01.png",
position: ["아르데타인", "베른", "애니츠"],
endPosition: "애니츠",
time: ["00:00", "12:00", "18:00"],
endTime: "18:00",
},
];
const expectData = [
{
name: "무릉도원",
src: "/img/island/island_04.png",
time: ["18:00"],
endTime: "18:00",
lv: 400,
position: "대항해",
contType: "ISLAND",
},
{
name: "기에나",
contType: "CO_OCEAN",
lv: "-",
src: "/img/ocean/ocean_01.png",
position: ["애니츠"],
endPosition: "애니츠",
time: ["18:00"],
endTime: "18:00",
},
];
// 호이스팅됨
jest.mock("components/list/list.tsx", () => (props: any) => (
<div data-testid={JSON.stringify(props.data)} />
));
describe("TimerContainer", () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("컨텐츠에서 종료되지 않은 시간대만 유지 및 빠른순으로 정렬", () => {
const notification = jest.fn();
// mock 함수가 되기 전에 date 캐싱
const mockDateObject = new Date(2021, 1, 1, 17);
jest
.spyOn(global, "Date")
.mockImplementation(() => (mockDateObject as unknown) as string);
const { getByTestId } = render(
<TimerContainer data={initialData} notification={notification} />
);
expect(getByTestId(JSON.stringify(expectData))).toBeTruthy();
});
it("clear", () => {});
});
늘 동일한 결과를 확인하기 위해 테스트 대상 내부에서 사용되는 컴포넌트, 혹은 변화가 잦은 값을 반환하는 메소드들은 mocking
하였다.
테스트를 진행하고, 사용자의 시선에서 결과물이 원하는 값과 동일한지 true
참인지만을 확인하였다.
외부의 변하지 않는 상수를 참조하여 새로운 값을 반환하는것은 괜찮다는것 같음
- 이 점에 있어서, 다소 혼동되는점이 있는데
React
에서는useEffect
와 같이 공문에서 대놓고 설명하기를 사이드이펙트를 관리하기 위한hook
이라고 설명하고 있고, 해당 함수 외부의 컨텍스트에 접근하는 역할을 수행한다. 오로지return
만 수행하는 순수함수와 다르게useEffect
를 통해DOM
조작을 하거나 외부 컨텍스트에 접근하는등의 행위가 가능해졌다.. 여기서 React의 함수형 컴포넌트는 함수형 프로그래밍이 아니란것이 보여짐
useState
를 통해 고유한 상태를 가질 수 있게 되었다. hook
의 동작 원리를 고려해보았을 때, 함수 외부의 컨텍스트에서 공문에서 소개된 객체 자료구조에서 순서대로 저장되어있는 상태값을 업데이트하는점이 순수함수의 사이드이펙트나, 상태값을 가질 수 없다는 특징과는 다른 모습을 보여줌.대부분의 디스플레이는 초당 60번 (이를 60fps) 화면을 새로고친다고 한다.
브라우저또한 다르지 않다고 하는데 이 횟수가 사용자에게 있어 자연스러운 화면을 보여주는 방법이라고 한다.
하지만, 만약 요소에 할당한 애니메이션이 그 1/60초 내에 모두 해결되지 않으면 페이지가 혹은 애니메이션이 버벅거리는것처럼 보여진다.
렌더링작업과 자바스크립트의 실행은 모두 렌더링프로세스의 메인스레드에서 작동되기 때문에, 긴 시간이 걸리는 자바스크립트 애니메이션이 실행된다면 위의 초당 60번의 게산이 중단될 수 있다고 한다.
requestAnimationFrame
은 이런 자바스크립트 작업을 작은 덩어리로 나누어서 모든 프레임에 실행되도록 도와준다고 한다.
60fps를 유지하는 화면의 새로고침 작업이 1/60초 이상의 자바스크립트 작업에 중단되어 대기하지 않도록 나누는것 같음
setInterval
이나 setTimeout
과 같은 반복적인 애니메이션을 사용하고자 할 때 진행되는 콜백함수가 프레임의 종료시에 시작될 수 있어서 누락될 수 도 있다고 한다.
requesetAnimationFrame
을 사용하면 프레임 시작 시에 실행되도록 보장해준다고 함.
requesetAnimationFrame
또한 비동기로 작업이 처리되지만, 매크로태스크 큐가 아닌 별도의 애니메이션 큐에 저장되어 스크립트의 진행상황에 영향을 받지않는것이 핵심인것 같다.
Vue
의 특징을 비교적 잘 알고있는 React
와 비교하여 생각해봄
Vue
는 MVVM
디자인 패턴을 갖고있고, Model
, View
, ViewModel
의 약어라고 함. Model
과 View
는 다른 디자인패턴과 동일하고, ViewModel
의 경우 Vue Instance
로 구성되는 View
에서 사용되는 Model
의 데이터들을 View
에 넘겨주는 역할을 하고, View
와 관련된 로직들을 관리하기도 함.
Controller
같은건가?
Vue
에서 template
이 view
Vue instance
에서 data
가 model
, View instance
의 나머지가 ViewModel
인듯 함.
React
의 hook
중 useEffect
와 같이 특정 상태의 변화를 감지하고 이후의 사이드이펙트 작업을 처리하는것은 watch
가 대신할 수 있을것 같음. 다만 배열형식이 아닌 단일 상태값을 key
로 하여 작동되는듯 함.
React
에서는 HTML
과 JS
가 함께 작성되어 하나의 createElement
의 역할을 수행하는 JSX
를 사용하지만, Vue
는 하나의 파일에 HTML
, CSS
, JS
가 모두 사용됨.
template
,css
,script
Vue Cli
를 사용하게될 경우, new Vue
로 하나의 인스턴스가 생성되고 그 내부를 구성하는 형식인듯 함. 이후의 컴포넌트단위는 export
되는 단일 객체로 진행
단 하나의 인스턴스
new Vue({
render: h => h(App),
}).$mount('#app')
ReactDOM.render(
<App />
document.getElementById('root')
)
React
의 ReactDOM.render
과 유사하게 작동되는듯 함.
React
의 경우 자식 요소들을 그대로 작성해, 자식요소들을 갖는 컴포넌트의 children
속성을 사용하여 유연한 컴포넌트를 만들 수 있음
Vue
의 경우, slot
이라는 태그를 사용하여 특정 slot
에 name
속성을 부여하여 1대1 매칭을 해줘서 유연한 컴포넌트로 만드는것 같음.
<template #main>
<p>메인 컨텐츠</p>
<p>메인 컨텐츠</p>
<p>메인 컨텐츠</p>
</template>
components: {
"my-component": {
template: `
<slot name="main">메인컨텐츠1이 없습니다</slot>
`,
},
},
괜찮은 점은, slot
에 매칭되는 요소가 없다면 대체되는 내용을 지정해줄 수 있었음.
먄약, p태그의 메인컨텐츠 3개가 없다면 메인컨텐츠1이 없습니다가 렌더됨
<div id="slot3">
<my-list :items="arr">
<template #item="{item : {id, text}}">
<li>{{id}}, {{text}}</li>
</template>
</my-list>
</div>
new Vue({
el: '#slot3',
data: {
arr: [
{ id: 1, text: 'banana' },
{ id: 2, text: 'apple' },
{ id: 3, text: 'grapes' },
],
},
components: {
'my-list': {
props: ['items'],
template: `
<ul>
<slot
name="item"
v-for="item in items"
:item="item"
>
</slot>
</ul>
`,
},
},
})
vue
는 v-for
디렉티브를 통해 React
에서의 .map
처럼 여러개의 자식요소를 생성할 수 있는데, 이 떄의 상황에서도 v-slot
을 사용하여 유연하게 설계하도록 도움을 주었음.
특이했던점은, 배열을 속성으로 전달한것까지는 당연한데, 반복하여 조회하는 배열 내 단일 요소를 마치 상위컴포넌트의 slot
에 매칭된 요소로 전달해주는 느낌처럼 작동됨.
아마 template 태그의 내용이 매칭된 slot과 대체되면서 속성을 공유한다는게 아닐까 라는 생각?
expression
)은 변수할당, *
+
와 같은 수식들을 의미한다.statement
)은 프로그래밍에서 실행 가능한 최소의 독립적인 조각을 의미한다.es6
-> es5
와 같이 하나의 언어를 다른 수준으로 변환하는것을 트랜스파일100
~500
상태코드큰 차이로
interface
는 동일한 키워드로 두번 선언되었을 때, 병합을 하지만type alias
는 그렇지 않음. 되도록interface
사용을 권장하고 배열과 같은 구조의 갯수와 위치에 맞는 타입을 고정하는 튜플과 같은 경우에type alias
를 사용