sangmin802 / sangmin802.github.io

📌 개인 기술블로그
https://sangmin802.github.io/
MIT License
2 stars 0 forks source link
gatsby

SangMin 개발이야기

템플릿 제작자 : Jbee
템플릿 : [gatsby-starter-bee](https://github.com/JaeYeo pHan/gatsby-starter-bee)
너무나도 멋진 베이스 만들어주셔서 감사합니다.

포스트로 작성하기에는 부정확하거나 단순한 소스들

React.createElement, jsx

React 진행과정

jsx

Hook의 등장

class가 아닌 함수형 컴포넌트에 React의 state와 생명주기 기능을 연동시켜주는 함수이다.

useState

배포된 모듈들의 선언형 프로그래밍을 위한 useContext 응용

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는 반환하지 않는 사용자정의 컴포넌트를 모듈로 만들어서 위의 문제를 대신해주도록 한것 같다. 또한, pathrender 될 컴포넌트와 같이 핵심적인 요소만을 사용자가 지정하도록 하여, 부가적인 로직을 작성할 필요 없이 선언형의 컴포넌트만을 작성하여도 되도록 해준게 아닐까?

Suspense

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 조차도 되지 않음

이처럼 필요한 데이터가 존재하지 않을 때, 해당 컴포넌트 자체의 렌더링을 일시 중단시킬 수 있음

ErrorBoundary

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 에서 렌더 이전 상태값에 할당시킬 수 있음

Promise와 callback 차이

React는 라이브러리? 프레임워크?

왜 타입스크립트?

history.replace vs history.go

비동기를 통한 성능개선

비동기를 통한 경험 개선

resolve의 호출시기

비동기를 사용하여 진행중인 이벤트를 일시 중단하고, 다른 환경에서 resolve를 호출하여 중단된 이벤트를 이어서 진행할 수 있음

브라우저 작동 과정

react flux? 디자인패턴?

virtual dom, 메모이제이션과 diff

why parse block

document.write와 같이 html을 즉시 생성하는 경우가 있어서(그 순서를 보장하기 위해?)

Promise 실행함수와 try catch 범위

new Promise의 인자로 전달되는 콜백함수는 하나의 실행함수로서 프로미스의 실행, 거절을 의미하는 두가지 인자를 받는 함수이다. 이 실행함수는 Promise 객체가 반환되기 이전에 먼저 실행이 된다.

대게, 어떠한 비동기로 진행된 작업이 완료되고 내부에서 resolve 혹은 reject를 호출하여 이후의 로직을 마이크로태스크큐에 넘겨 이벤트루프를 통해 콜스택에 넘겨지도록 한다.

await이 없다면 try catch로 잡을 수 없는 이유는, Promise 내부에서 발생한 throw는 하나의 reject와 동일하게 작동되어 Promise가 거절되었다는 것은, 해당 에러는 이미 비동기로서 try catch의 영향권을 벗어난 상태이기 때문이다.

렌더링 파이프라인

브라우저를 렌더링하는 과정들 파싱, 렌더트리 구축, 레이아웃 형성, 페인트, 컴포지트단계 (컴포지터 스레드 작업)을 하나의 렌더링 파이프라인이라고 한다.

css-in-js

  1. 클래스명에대한 고민을 줄일 수 있음

  2. 현재의 자바스크립트 환경에 쉽게 접근할 수 있고, 동적으로 생성된 값에 자유롭게 스타일링을 할 수 있음

    • 동적인 스타일을 위해 어쩔수 없이 인라인스타일을 사용해야하는 상황이 생긴다.

      여기서 인라인스타일이 왜 문제가 있을까라는 생각을 해보았는데, reflow가 발생하여 레이아웃 계산을 다시 할 때 인라인으로 작성되는 스타일은 여러 스타일이 합쳐져있는 클래스를 추가하는것으로 한번에 부여하여 일괄 계산하는것이 아니라 스타일을 하나씩 하나씩 반영하여 각각 계산하기 때문에 reflow의 비용이 비교적 더 크다는것 같다.

  3. 별도의 스타일 파일을 관리할 필요가 없음

    • 근데 생각해보면 css-in-js를 사용하더라도 스타일을 별도의 파일로 관리하며 import 해 사용하긴 함..
  4. 결국 의존하는 새로운 모듈이 추가되는 것

  5. css-in-css를 통해 완성되어있는 클래스를 즉시 반영하는것과, 자바스크립트로 된 새로운 스타일을 읽고 계산하여 반영하는것에 차이가 있다고 함

    • 어쨌든 결국 스타일을 적용하기 위해 스크립트를 한번 더 읽고 계산하고 해석해야 하는 점

TDD - Test-driven development

별다른 프레임워크가 아닌 테스트 기반 개발 기법을 의미한다. A라는 기능을 개발한다고 하였을 때, 실제 코드에 즉시 개발을 하는것이 아닌 다른 순서로 진행이 된다.

  1. 테스트코드에 A 기능에대한 테스트 작성
  2. 테스트 실행
  3. 테스트 오류

    아직 기능에 대한 작성이 되지 않았기 때문에 당연한 오류

  4. 딱 테스트를 통과할 수준의 코드를 작성
  5. 테스트 실행
  6. 테스트 성공
  7. B 기능 개발을 위해 1번부터 다시 진행

장점

  1. 구현하고자 하는 기능의 특징 및 핵심적인 부분만을 고려할 수 있게 됨

    테스트를 성공시키기 위한 최소한의 코드를 작성하게되어, 불필요한 코드들은 생성되지 않을것 같음.

  2. 직접 이벤트를 호출하는등의 테스트를 통해 사용자 입장에서 코드를 작성할 수 있게 됨.

  3. 개발을 완료한 기능에 대해 테스트코드를 작성하지 않더라도 로컬환경에서 실행시켜 직접확인하는틍의 테스트는 필수적이다. 하지만, 클릭 과 같은 이벤트등을 테스트하고자 할 때, 다시 테스트하기위해 원래대로 돌아와야 하는 등 부수적인 작업들이 생길 수 있는데, 테스트코드는 그럴 필요가 없다.

주의점

이전에 연습해본다고 작성했던 테스트코드들의 몇몇은 잘못된, 실패한 테스트코드인것 같음.

  1. 컴포넌트의 내부가 리팩토링이 될 때, 테스트코드가 붕괴됨
  2. 사용자의 이벤트와 관계없이 그냥 첫 렌더링이 잘 되는지? 클릭했을 때 호출이 되는지? 프레임워크나 자바스크립트가 당연히 해주는 작업들을 테스트함.

    의미없이 단위테스트의 규모가 커졌었음.

아무래도 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에서 함수형 컴포넌트 특징

  1. 일급함수의 역할로 함수내부에서 함수를 반환하는 고차함수가 가능하고, 함수자체를 인자로 보낼 수 도 있다.
  2. 외부의 데이터는 절대 변하지 않는 불변성을 갖는다
    • 속성으로 전달된 값들은 절대로 직접 변경할 수 없으며, 새롭게 복사된? 값 만을 업데이트할 수 있다.
  3. 함수 내부로 전달되는 값을 사용하여 함수 외부의 컨텍스트에 영향을 주지 않는 순수한 함수이다.

    외부의 변하지 않는 상수를 참조하여 새로운 값을 반환하는것은 괜찮다는것 같음

    • 이 점에 있어서, 다소 혼동되는점이 있는데 React 에서는 useEffect와 같이 공문에서 대놓고 설명하기를 사이드이펙트를 관리하기 위한 hook이라고 설명하고 있고, 해당 함수 외부의 컨텍스트에 접근하는 역할을 수행한다. 오로지 return만 수행하는 순수함수와 다르게 useEffect를 통해 DOM조작을 하거나 외부 컨텍스트에 접근하는등의 행위가 가능해졌다.. 여기서 React의 함수형 컴포넌트는 함수형 프로그래밍이 아니란것이 보여짐
  4. 내부의 상태를 가질 수 없다.
    • 하나의 목적을 수행하기 위한 여러개의 상태(속성), 메소드들을 하나로 캡슐화 하여 상태를 변화시킬 수 있는 객체지향과 다르게 함수형은 함수 내부에 상태를 가질수 없고, 외부의 상태에 변화를 주어서도 안된다. 내부의 상태를 가질 수 없다는것이 결과를 반환한기 위한 함수가 호출될 때 마다 상태가 초기화되는 때문이 아닐까 싶다.
    • 이 또한 의문인 점이, useState를 통해 고유한 상태를 가질 수 있게 되었다. hook의 동작 원리를 고려해보았을 때, 함수 외부의 컨텍스트에서 공문에서 소개된 객체 자료구조에서 순서대로 저장되어있는 상태값을 업데이트하는점이 순수함수의 사이드이펙트나, 상태값을 가질 수 없다는 특징과는 다른 모습을 보여줌.

requestAnimationFrame

대부분의 디스플레이는 초당 60번 (이를 60fps) 화면을 새로고친다고 한다.

브라우저또한 다르지 않다고 하는데 이 횟수가 사용자에게 있어 자연스러운 화면을 보여주는 방법이라고 한다.

하지만, 만약 요소에 할당한 애니메이션이 그 1/60초 내에 모두 해결되지 않으면 페이지가 혹은 애니메이션이 버벅거리는것처럼 보여진다.

렌더링작업과 자바스크립트의 실행은 모두 렌더링프로세스의 메인스레드에서 작동되기 때문에, 긴 시간이 걸리는 자바스크립트 애니메이션이 실행된다면 위의 초당 60번의 게산이 중단될 수 있다고 한다.

requestAnimationFrame은 이런 자바스크립트 작업을 작은 덩어리로 나누어서 모든 프레임에 실행되도록 도와준다고 한다.

60fps를 유지하는 화면의 새로고침 작업이 1/60초 이상의 자바스크립트 작업에 중단되어 대기하지 않도록 나누는것 같음

setInterval이나 setTimeout과 같은 반복적인 애니메이션을 사용하고자 할 때 진행되는 콜백함수가 프레임의 종료시에 시작될 수 있어서 누락될 수 도 있다고 한다.

requesetAnimationFrame을 사용하면 프레임 시작 시에 실행되도록 보장해준다고 함.

requesetAnimationFrame 또한 비동기로 작업이 처리되지만, 매크로태스크 큐가 아닌 별도의 애니메이션 큐에 저장되어 스크립트의 진행상황에 영향을 받지않는것이 핵심인것 같다.

Vue, React

Vue의 특징을 비교적 잘 알고있는 React와 비교하여 생각해봄

MVVM

VueMVVM디자인 패턴을 갖고있고, Model, View, ViewModel의 약어라고 함. ModelView는 다른 디자인패턴과 동일하고, ViewModel의 경우 Vue Instance로 구성되는 View에서 사용되는 Model의 데이터들을 View에 넘겨주는 역할을 하고, View와 관련된 로직들을 관리하기도 함.

Controller같은건가?

Vue에서 templateview Vue instance에서 datamodel, View instance의 나머지가 ViewModel인듯 함.

useEffect, watch

ReacthookuseEffect와 같이 특정 상태의 변화를 감지하고 이후의 사이드이펙트 작업을 처리하는것은 watch가 대신할 수 있을것 같음. 다만 배열형식이 아닌 단일 상태값을 key로 하여 작동되는듯 함.

JSX

React에서는 HTMLJS가 함께 작성되어 하나의 createElement의 역할을 수행하는 JSX를 사용하지만, Vue는 하나의 파일에 HTML, CSS, JS가 모두 사용됨.

template, css, script

ReactDOM.render, vue mount

Vue Cli를 사용하게될 경우, new Vue로 하나의 인스턴스가 생성되고 그 내부를 구성하는 형식인듯 함. 이후의 컴포넌트단위는 export되는 단일 객체로 진행

단 하나의 인스턴스

new Vue({
  render: h => h(App),
}).$mount('#app')
ReactDOM.render(
  <App />
  document.getElementById('root')
)

ReactReactDOM.render과 유사하게 작동되는듯 함.

slot, children

React의 경우 자식 요소들을 그대로 작성해, 자식요소들을 갖는 컴포넌트의 children속성을 사용하여 유연한 컴포넌트를 만들 수 있음 Vue의 경우, slot이라는 태그를 사용하여 특정 slotname속성을 부여하여 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>
      `,
    },
  },
})

vuev-for디렉티브를 통해 React에서의 .map처럼 여러개의 자식요소를 생성할 수 있는데, 이 떄의 상황에서도 v-slot을 사용하여 유연하게 설계하도록 도움을 주었음. 특이했던점은, 배열을 속성으로 전달한것까지는 당연한데, 반복하여 조회하는 배열 내 단일 요소를 마치 상위컴포넌트의 slot에 매칭된 요소로 전달해주는 느낌처럼 작동됨.

아마 template 태그의 내용이 매칭된 slot과 대체되면서 속성을 공유한다는게 아닐까 라는 생각?

잡동사니