프로토타입(prototype)이란 한글로 원래의 형태 또는 전형적인 예라는 뜻을 가지고 있습니다.
javascript는 프로토타입 기반의 언어입니다.
그래서 javascript에서 프로토타입 개념을 이해하는 것은 매우 중요합니다.
클래스 기반언어 에서는 클래스를 먼저 선언하고 새로운 객체(인스턴스)를 생성합니다.
하지만 javascript에서는 프로토타입(prototpe)을 원형으로 삼고 이를 이용하여 객체를 생성합니다.
var jade = {
name: 'jade'
}
jade.toString() // '[object Object]'
function Person(name) {
this.name = name
}
var wade = new Person('wade')
wade.name // 'wade'
wade.hasOwnProperty('name') // true
위의 코드는 javascript에서 객체를 선언한 예시입니다.
하나는 객체 리터럴 방식을 사용하였고 하나는 생성자 함수를 사용하였습니다.
jade 변수와 wade 변수에서는 각각 toString, hasOwnProperty 메서드를 선언해주지 않았습니다.
하지만 사용 결과는 에러를 반환하지 않고 정상적인 결과를 반환하였습니다.
왜 이런 동작이 가능한 걸까요??
javascript는 프로토타입 기반의 언어이고 프로토타입 체인이 이루어져 해당 메소드들을 사용할 수 있었기 때문입니다.
javascript에서 변수는 호출된 프로퍼티가 존재하지 않으면 자신의 프로토타입 객체에 접근하여 해당 메서드가 있는지 탐색합니다.
있으면 사용, 없으면 다음 프로토타입 객체에 접근하여 메서드를 탐색 및 사용하게 됩니다.
이것을 프로토타입 체인이라 합니다.
여기서 나오는 프로토타입 객체는 자신의 원형이 되는 객체, 부모 객체라 이해하였습니다.
위 그림을 이해하면 프로토타입의 동작 흐름을 이해하는데 큰 도움이 될 수 있습니다.
위 그림과 아래의 코드를 같이 살펴보겠습니다.
function Person(name) {
this.name = name
}
var logan = new Person('logan')
console.log(Person.prototype) // { constructor: f }
console.log(Person.prototype.constructor) // f Person
console.log(logan.constructor) // f Person
위의 코드를 더 위의 그림에 도식화한 내용입니다.
이제 상세히 설명하겠습니다.
Person은 생성자 함수 입니다(javascript에서는 보통 파스칼 케이스로 선언)
logan은 new Person을 통해 생성된 객체 입니다.
Person 생성자 함수에서 prototype 속성을 통해 Person의 원형 객체에 접근할 수 있습니다.
logan에서는 __proto__를 통해 자신의 원형객체에 접근할 수 있습니다.
constructor는 새롭게 생성된 인스턴스에서 자신의 생성자 함수에 접근할 수 있는 수단입니다.
아직 한가지 동작은 설명이 되지 않았습니다.
logan.constructor의 동작입니다.
logan에는 constructor 속성이 존재하지 않습니다. 따라서 동작을 하지 않아야만 할것 같습니다.
하지만 동작합니다.
logan.__proto__.constructor이 호출되었기 때문입니다. 이때 프로토타입 체인이 등장하게 됩니다.
우선 logan 객체에서 constructor가 존재하는지 찾습니다.
없는 것을 확인했고 __proto__를 통해 logan의 프로토타입에 접근하여 constructor를 찾았고 이를 호출했습니다.
function Person(name) {
this.name = name
this.getName = function() {
console.log(this.name)
}
}
var logan = new Person('logan')
logan.getName()
이런식으로 메소드를 생성자 함수 내에서 정의해주면 새로 생성된 객체는 getName 메소드를 호출하게 됩니다.
이 생성자 함수를 이용하여 무수히 많은 객체를 생성한다고 가정하겠습니다.
그러면 객체마다 getName이라는 메서드에 메모리 영역을 할당해야 합니다.
이러한 메모리 영역을 프로토타입을 이용하면 세이브 할 수 있습니다
function Person(name) {
this.name = name
}
Person.prototype.getName = function () {
console.log(this.name)
}
var logan = new Person('logan')
logan.getName()
위와 같이 프로토타입 체인을 이용하면 Person 생성자 함수가 무수히 많은 객체를 생성해도
생성된 객체 내에 getName 메소드가 존재하지 않으므로 __proto__를 통해 getName메소드에 존재합니다.
그러면 메모리 영역을 객체마다 할당하지 않아도 같은 결과를 가져올 수 있습니다.
프로토 타입 체인 -> 프로퍼티를 가져오기 위해 자신의 프로토타입에 프로퍼티가 있는지 찾기위한 과정
자식이 차키가 없으면 부모의 차키를 찾고 그 다음엔 조부모의 차키를 찾는다.
차키를 찾지 못하면 그 차는 사용할수가 없다.
스코프 체인 -> 실행컨텍스트에서 변수를 찾기 위해 스코프체인 프로퍼티를 참조하여 변수를 찾기위한 과정
react-query의 공식 홈페이지에서도 서버의 상태를 위한 라이브러리로 설명되어있습니다.
* React Query는 서버 상태 라이브러리입니다. 서버와 클라이언트의 비동기적인 동작을 관리하기에 적합합니다. * Redux, MobX, Zustand, etc 는 클라이언트 상태 라이브러리 입니다. 비동기 데이터를 저장할수는 있지만 React Query에 비해서는 비효율적입니다.
이러한 점을 염두에 두고 React Query는 클라이언트 상태에 위치한 캐시데이터를 관리하기 위한 boiler plate코드와 관련 로직들을 코드 몇 줄 만으로 대체한다는 것입니다!
Redux에서 서버의 api요청 응답을 저장해두고 있을때는 아래의 사항들이 걸렸습니다.
1. 너무 많은 boiler plate 코드
2. 언제 응답되었는지를 알수 없는 데이터 로드 시기
3. 전역 클라이언트 상태와 같이 관리되어지는 서버 상태
그래서 React Query를 도입했었고 axios, fetch등과 같이 비동기 요청을 보내주는 도구들과 함께 효율적으로 사용했었습니다.
아래에서는 간단한 기본 개념과 쓰임을 알아보려 합니다.
먼저 stale에 대하여 알아보려 합니다.
브라우저에서는 header의 cache-control를 통해 네트워크 요청의 캐시를 관리합니다.
캐시를 이용하는 목적은 동일한 요청이라면 서버로의 요청이 아니라 메모리에서 가져와 더 빠른시간내에 응답을 하기 위해서 사용합니다.
cache-control의 값에는 max-age, no store, no cache 등이 들어갈 수 있지만 stale-while-revalidate라는 값 또한 들어갈 수 있습니다. (stale은 신선하지 않은 이라는 뜻입니다.)
예시를 들어보겠습니다. cache-control: "max-age= 60, stale-while-revalidate= 60" 으로 응답헤더에 포함되어 있다면
1~60초에 요청이 들어오면 캐시는 신선하므로 바로 가져가 사용합니다.
61~120초 요청에 들어오면 캐시는 stale 상태 신선하지 않은 상태가 됩니다. 그래서 우선 캐시된 값을 내보내고 서버로 값을 refetch, 즉 fresh 한 상태로 바꾸기 위해 요청을 보내고 응답을 받아오면 캐시된 값을 교체해 줍니다.
react-query에서 기본 stale time은 0입니다. cache-time의 default는 5분 입니다.
이 설정을 바꾸고 싶다면 개별 요청에서만 옵션을 변경해주어 보내주거나 queryClient에서 설정해주면 모든 요청에 기본 요청으로 설정됩니다.
stale 쿼리들은 아래와 같은 상황에서는 백그라운드 상태에서 자동으로 요청됩니다.
New instances of the query mount(잘 모르겠습니다...)
window가 다시 focus 되었을때
network가 재연결 되었을때
query가 설정한 retch 간격에 따라
그외 기본 사항들
inactive 상태인 쿼리들은 5분 이후에 garbage collect 됩니다.
요청에 실패할 경우 3번 까지 요청을 보내고 모두 실패하였을시 화면에 표시합니다. 이때 요청간 delay를 설정할 수 있습니다.
기본적으로 Json 형식의 쿼리 결과는 데이터가 실제로 변경되었는지 여부를 감지하기 위해 구조적으로 공유되어 집니다. 실제로 변경되지 않은 경우 데이터 참조는 변경되지 않은 상태로 유지되어 useMemo및 useCallback과 관련하여 값 안정화에 더 도움이 됩니다.
QueryClient Provider
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
function App(){
return (
<QueryClientProvider client={queryClient}>
<Component />
</QueryClientProvider>
)
}
만약 useQuery를 복수개 요청하였고 모든 데이터의 유효성이 검증되어야 한다면 코드는 복잡해질수 있습니다.
Suspense를 사용하여 loading 상태를 대체할 수 있고 ErrorBoundary를 사용하여 error 상태를 대체할 수 있습니다.
react-query에서는 errorResetBoundary를 제공해주고 있습니다.
react-error-boundary 라이브러리와 같이 사용하면 errorBoundary 역할을 할수 있습니다.
저는 typescript에서 react 18, react-dom18을 사용하여 suspense, react-query를 사용하려 하였으나 아직 @types/react-query내부에서 참조하고 있는 react가 17을 기반으로 하고 있어 그런지 typescript 적용에는 실패하였습니다. 추후에 react js 에서 시도해보고 안되면 다른 방법을 찾아볼 예정입니다.
복수의 useQuery 사용
한 컴포넌트에서 여러 요청이 복수로 parallel하게 수행되어져야 할 경우 아래와 같이 작성할 수 있습니다.
function App () {
// The following queries will execute in parallel
const usersQuery = useQuery('users', fetchUsers)
const teamsQuery = useQuery('teams', fetchTeams)
const projectsQuery = useQuery('projects', fetchProjects)
...
}
parallel하게 수행되어 져야 하는 요청이 복수개일 경우 useQueries의 사용을 고려할 수 있습니다.
a요청이 수행되어 지고 a요청의 결과를 가지고 b 요청이 수행되어 져야 할 경우를 의존적 쿼리라 칭하겠습니다.
이런 의존적 쿼리가 필요할 경우 enabled 옵션을 사용하면 편리합니다.
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', userId],
getProjectsByUser,
{
// The query will not execute until the userId exists
enabled: !!userId,
}
)
// isIdle will be `true` until `enabled` is true and the query begins to fetch.
// It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)
이 외에 useInfiniteQuery등과 옵션중 keepPreviousData등 편리한 옵션이 많아서 매우 유용합니다!!
useMutation
get 요청을 보낼때는 useQuery를 사용합니다.
post, delete, patch, put 과 같은 요청을 이용할때는 useMutation 훅을 사용합니다.
useQuery에 비해 적은 옵션을 사용합니다.
DevTool
ReactQuery에서는 DevTool을 제공합니다.
키와 데이터 값을 같이 볼수 있고
fresh, fetching, stale 등 데이터의 상태를 눈으로 볼수 있고
refetch, invalidate, reset, remove 등의 동작을 수행할 수 있습니다.
함수의 감춰진 프로퍼티 중 [[scope]]는 스코프 체인이 아니라 현재 참조하고 있는 스코프를 의미합니다.
function test1() {
function test2() {
function test3() {
var x = 1;
function test4() {
console.log("test4", x)
}
console.dir(test4)
test4();
}
console.dir(test3)
test3();
}
console.dir(test2)
test2();
}
console.dir(test1)
test1();
var name = 'name'
function hi() {
var name = 'same'
console.log(name) // 'same'
sayWord('hello') // 'hello name'
}
function sayWord(word) {
console.log(word, name)
}
hi()
아래는 위에서 사용한 함수를 실행컨텍스트의 스코프 체인을 이용하는 상황을 같이 서술한 것입니다.
var name = 'name'
function hi() {
var name = 'same'
console.log(name) // 'same' // name은 hi 컨텍스트의 변수객체 내에 존재하므로 가져와 사용합니다.
sayWord('hello') // 'hello name'
// sayWord는 hi컨텍스트의 변수 객체에 존재하지 않으므로 상위 스코프인 전역 컨텍스트의 변수객체를 참조합니다.
}
function sayWord(word) {
console.log(word, name)
// name은 sayWord의 변수 객체에 존재하지 않으므로 상위 스코프인 전역 컨텍스트의 변수객체를 참조합니다.
}
hi()
javascript에서 실행 컨텍스트 내부에 사용하고자 하는 변수가 존재하지 않는다면 상위 스코프의 변수객체를 참조합니다.
최상위 스코프까지 변수를 탐색하고 있으면 return, 찾지 못하면 referenceError를 반환합니다.
javascript에서는 createObjectURL을 통해 Blob 객체를 나타낼 수 있습니다.
URL.createObjectURL 메소드를 사용하면 생성된 window의 document에서만 접근 가능한 url이 생성됩니다.
이를 통해 image를 입력받았을때 화면에 보여줄 수 있는 임시 url을 생성할 수 있습니다.
Blob(Binary large Object) 객체는 javascript에서 이미지, 영상, 멀티미디어 데이터를 다룰때 사용합니다. createObjectUrl에 입력 가능한 (File, Blob, MediaSource)중 File은 Blob에 기반한 인터페이스 입니다.
input에 type을 file로 주면 입력받은 파일에 input.files 메소드를 통해 접근할 수 있습니다. 아래의 embedded 예제에서 보면 확인할 수 있습니다.
이 url은 다른 window에서는 접근이 되지 않습니다.
URL.revokeObjectURL을 통해 createObjectURL을 통해 생성된 Url을 폐기 시킬수 있습니다
폐기시키지 않으면 javascript engine에서는 생성된 url을 계속 사용하는 것으로 판단하여 GC가 동작하지 않게 됩니다.