Debounce

이벤트가 연달아 발생할 때, 제일 처음 또는 마지막 이벤트일 때만 함수를 호출하는 방법

만약 키보드 이벤트가 발생할 때마다 API를 요청한다고 가정해보자.

“감”이라는 글자를 입력하는데 “ㄱ”, “가”, “감” 3번의 이벤트가 발생하게됩니다. 이러면 불필요한 이벤트까지 API 요청에 포함시키면 낭비이므로 이를 방지하기 위해 디바운스를 사용합니다.

1
2
3
4
5
6
7
8
9
var timer;
document.querySelector("#input").addEventListener("input", function (e) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
console.log("여기에 ajax 요청", e.target.value);
}, 200);
});
  • 주로 키보드 입력 이벤트에 사용

Throttle

마지막 이벤트가 발생한 후 일정 시간이 지나기 전에 다시 호출되지 않도록 막는 방법

만약 스크롤이벤트가 발생했을 때, 처음에 스크롤 이벤트가 발생할 때, 함수를 호출하고 몇초동안은 이벤트가 발생해도 함수를 호출시키지 않는 방법이다.

1
2
3
4
5
6
7
8
9
var timer;
document.querySelector("#input").addEventListener("input", function (e) {
if (!timer) {
timer = setTimeout(function () {
timer = null;
console.log("여기에 ajax 요청", e.target.value);
}, 200);
}
});
  • 주로 스크롤 이벤트에 사용

위와 같이 직접 구현하는 방법보다는 예외 사항을 처리하지 못할 경우도 있기때문에, _.debounce, _.throttle을 사용한다.

요즘은 토스에서도 해당 라이브러리를 지원하니 관심이 있으면 사용해보자.

토스 디바운스 라이브러리 바로가기

댓글 공유

var setTimeout Quiz

카테고리 Daily
1
2
3
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}

다음 코드의 결과는??

1
2
3
4
5
6
// 정답
5;
5;
5;
5;
5;

이유

var 키워드로 선언한 변수는 함수 레벨 스코프를 갖는다. for문 코드 블록에서는 전역 변수로 선언되었기 때문에, 변수 i 값이 갱신된다.

setTimeout 함수는 비동기 처리 방식으로 실행된다.

setTimeout 함수는 Web API로 이동하여 타이머가 만료되면 Task Queue로 이동한다.

Task Queue에서 Call Stack이 비워질 때 까지 대기한다.

대기하는 동안, 다음 for 문이 돌고 있으므로, setTimeout 함수가 Task Queue에서 실행 컨텍스트가 비워질 때 까지 계속 대기한다.

이러한 이유로 i가 5가 될 때, Call Stack이 비워지므로 그때서야 이벤트 루프에 의해서 console 창에 출력되는 i 값이 5이므로 5가 5번 찍히게 된다.

해결방법

1. 블록 레벨 스코프

1
2
3
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}

2. 즉시실행함수

1
2
3
4
5
for (var i = 0; i < 5; i++) {
(function (param) {
setTimeout(() => console.log(param), 1000);
})(i);
}

setTimeout 함수를 즉시실행함수로 감싸서 함수 레벨 스코프를 갖도록 해준 뒤 즉시실행함수의 인수로 i를 전달해주고 즉시실행함수 내부의 함수에 파라미터로 해당 인수를 어디서 참조할지 설정해주면 된다. 위 예제에서는 콘솔 로그의 인수로 파라미터를 전달해줘야 할 것이다.

댓글 공유

React Query란?

카테고리 React

React Query란?

client에서 상태관리 라이브러리를 사용하는 것은 클라이언트에서만 유효하다.

서버의 데이터를 요청하고 클라이언트의 전역 상태로 갱신하는 로직이 추가되면 클라이언트의 상태관리 라이브러리 코드가 복잡해지는 문제가 생긴다.

즉, 리액트 쿼리는 서버 데이터와 클라이언트 데이터를 구분하기 위해 사용한다.

useQuery

컴포넌트나 custom hook에서 query를 구독하기 위해서는 useQuery를 호출해야한다.

이 때, 유니크한 queryKey와 promise를 반환하는 queryFn가 있어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useQuery } from "@tanstack/react-query";

function App() {
const info = useQuery({ queryKey: ["todos"], queryFn: fetchTodoList });
}

function Todos() {
const { isLoading, isError, data, error } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodoList,
});

if (isLoading) {
return <span>Loading...</span>;
}

if (isError) {
return <span>Error: {error.message}</span>;
}

return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
  • useQuery의 반환값은 데이터를 사용하기 위한 모든 정보를 담고 있다.
  • queryKey: query caching을 queryKey를 통해서 관리한다.
    • 쿼리키는 최상위 레벨이어야 하고, string으로 구성된 배열이어야 한다.
  • queryKey에는 쿼리함수에서 사용되는 모든 변수가 포함되어야 한다.
1
2
3
4
5
6
function Todos({ todoId }) {
const result = useQuery({
queryKey: ["todos", todoId],
queryFn: () => fetchTodoById(todoId),
});
}
  • queryFn: 데이터를 resolve(분해)하거나 에러를 던지는 Promise를 반환하는 함수
    • 쿼리가 에러를 가지고 있다는 것을 결정하려면, 쿼리함수가 throw Error 또는 rejected Promise를 반환해야한다.

useMutation

query와 달리 mutation은 데이털를 생성, 갱신, 삭제하거나 서버에서 사이드 이펙트를 수행할 때, 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function App() {
const mutation = useMutation((newTodo) => {
return axios.post("/todos", newTodo);
});

return (
<div>
{mutation.isLoading ? (
"Adding todo..."
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}

{mutation.isSuccess ? <div>Todo added!</div> : null}

<button
onClick={() => {
mutation.mutate({ id: new Date(), title: "Do Laundry" });
}}
>
Create Todo
</button>
</>
)}
</div>
);
}

만약 mutation 중 error가 발생하여 data를 비우고 싶을 땐 reset을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const CreateTodo = () => {
const [title, setTitle] = useState("");
const mutation = useMutation({ mutationFn: createTodo });

const onCreateTodo = (e) => {
e.preventDefault();
mutation.mutate({ title });
};

return (
<form onSubmit={onCreateTodo}>
{mutation.error && (
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
)}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<button type="submit">Create Todo</button>
</form>
);
};

mutation의 사이드이펙트 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
useMutation({
mutationFn: addTodo,
onMutate: (variables) => {
// mutation이 발생할 때 호출

// 선택적으로 롤백 시 데이터를 포함하는 context를 반환할 수 있다.
return { id: 1 };
},
onError: (error, variables, context) => {
// 에러시 호출
console.log(`rolling back optimistic update with id ${context.id}`);
},
onSuccess: (data, variables, context) => {
// 성공시 호출
},
onSettled: (data, error, variables, context) => {
// 에러든, 성공이든 호출
},
});
  1. onMutate가 현재 mutate가 발생했을 때, 실행하는 함수이다. 위 코드에서는 롤백할 때, 사용할 데이터를 반환하고 있다.
  2. 만약 에러가 발생하면, 해당 업데이트를 롤백하는 데이터가 context에 포함되어있다.
  3. onSettled는 성공하든 실패하든 호출되는 함수다.

만약, 콜백함수에서 Promise를 반환하면, 다음 콜백이 호출되기 전에 대기한다.

1
2
3
4
5
6
7
8
9
useMutation({
mutationFn: addTodo,
onSuccess: async () => {
console.log("I'm first!");
},
onSettled: async () => {
console.log("I'm second!");
},
});
  • onSuccess가 먼저 발생하는 이유는 내부 로직때문이 아니라 단순히 코드상 위에 있기 때문이다.

댓글 공유

flex 박스 반응형 팁

카테고리 Daily

flex-grow와 flex-basis 사용하여 반응형 만들기

1
2
3
4
5
6
7
8
9
10
11
<form>
<label class="name" for="name-field">
Name:
<input id="name-field" />
</label>
<label class="email" for="email-field">
Email:
<input id="email-field" type="email" />
</label>
<button>Submit</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
form {
padding: 8px;
border: 1px solid hsl(0deg 0% 50%);

/* display 속성 */
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 8px;
}

label {
font-weight: 500;
}
input {
display: block;
width: 100%;
height: 2.5rem;
margin-top: 4px;
}
button {
height: 2.5rem;

/* display 속성 */
flex-grow: 1;
flex-basis: 70px;
}

.name {
/* display 속성 */
flex-grow: 1;
flex-basis: 120px;
}
.email {
/* display 속성 */
flex-grow: 3;
flex-basis: 170px;
}
  • flex-grow는 flex 아이템이 컨테이너 안ㄴ에서 다른 아이템들에 비해 얼마나 많은 여유공간을 차지할 것인지 결정하는 값이다.
  • 위에서는 flex-grow가 총 5이니, 1/5,1/5,3/5씩 차지하게 된다.
  • flex-basis는 flex 아이템의 초기 크기를 결정한다.
  • flex-grow가 계산되기 이전에 아이템이 어느정도 크기를 가져야하는지를 정의한다.

댓글 공유

useRef 예시

카테고리 Daily

useRef hook은 DOM에 접근하기 위해 사용한다.

useRefcurrent 프로퍼티를 포함한 객체를 반환한다.

current 프로퍼티를 포함한 객체는 컴포넌트 전체 생명 주기에서 사용될 수 있고 리렌더링을 발생시키지 않으면서 데이터를 유지할 수 있도록 한다.

즉, useRef 값은 렌더링 중에도 같은 값을 유지할 수 있다.

  • 리렌더링 없이 참조값을 갱신하는 것

문법

1
const newRefComponent = useRef(initialValue);
  • 주로 변형가능한 데이터를 리렌더링 없이 저장하기 위해 사용된다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function App() {
const [anyInput, setAnyInput] = useState(" ");
const showRender = useRef(0);
const randomInput = useRef(null);

const toggleChange = (e) => {
setAnyInput(e.target.value);
showRender.current++;
};
const focusRandomInput = () => {
randomInput.current.focus();
};

return (
<div className="App">
<input
className="TextBox"
ref={randomInput}
type="text"
value={anyInput}
onChange={toggleChange}
/>
<h3>Amount Of Renders: {showRender.current}</h3>
<button onClick={focusRandomInput}>Click To Focus On Input </button>
</div>
);
}

export default App;
  • showRender 데이터는 toggleChange 이벤트가 발생할 때마다 값이 증가하고 해당 값이 화면에 렌더링된다. 이 때, 리렌더링 없이 showRender 값을 변형할 수 있다.
  • current 프로퍼티로 DOM에 접근하여 focus를 적용할 수 있다.

댓글 공유

useReducer 예시

카테고리 React

useReducer hook은 상태관리 도구이다.

useState의 대안으로 많이 사용된다.

2개 이상의 상태를 관리하기 위해서 각각을 useState로 관리하는 것 보단 useReducer를 사용하여 action별로 상태 관리하는 것이 훨씬 단순하다.

문법

1
const [state, dispatch] = useReducer(reducer, initialState);
  • useReducer는 3개의 인자를 받을 수 있다.
  • reducer 함수, initialState(초기상태), initFunction(초기화함수, optional)

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "USER_INPUT":
return { ...state, userInput: action.payload };
case "TOGGLE_COLOR":
return { ...state, color: !state.color };
default:
throw new Error();
}
};

function App() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
userInput: "",
color: false,
});

return (
<main
className="App, App-header"
style={{ color: state.color ? "#000" : "#FF07FF" }}
>
<input
style={{ margin: "2rem" }}
type="text"
value={state.userInput}
onChange={(e) =>
dispatch({ type: "USER_INPUT", payload: e.target.value })
}
/>
<br />
<br />
<p style={{ margin: "2rem" }}>{state.count}</p>
<section style={{ margin: "2rem" }}>
<button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
<button onClick={() => dispatch({ type: "TOGGLE_COLOR" })}>
Color
</button>
</section>
<br />
<br />
<p style={{ margin: "2rem" }}>{state.userInput}</p>
</main>
);
}

export default App;
  • reducer 함수에는 초기값 stateaction에 따른 case별로 반환하는 상태값을 정의한다.
  • useReducer hook을 사용하여 state(상태), dispatch(상태변경함수)를 선언한다.
  • dispatch(상태변경함수)의 인자로 객체를 전달해주는데, type, payload 프로퍼티를 갖는 객체를 전달해준다.
  • action 객체의 type에 따라 reducer에 정의해둔 action case에 따라 반환하는 값이 달라진다.
  • 위와 같이 사용하면, useStateuseState 변경함수를 여러 개 정의하지 않고도 직관적으로 상태관리 코드를 작성할 수 있다는 장점이 있다.

댓글 공유

git 커밋 되돌리기

카테고리 git

git을 사용하다 보면 이전 커밋으로 되돌아가고 싶은 경우가 있다.

이 때 사용할 수 있는 명령어와 방법에 대해 알아보자.

되돌리기(undoing)

1
2
3
$git restore unread.md

$git restore . // 현재위치 기준 모든 파일의 변경사항 취소
  • 커밋 몇 줄을 수정하기에는 너무 많아서 최신 커밋으로 되돌아 가는 방법이다.

unstaging

1
$git reset HEAD unread.md

add한 변경사항을 working directory로 내리는 방법이다.

  • 작업한 단위 별로 add하고 커밋을 해줘야하는데, 전체파일을 다 add 해버렸으면 위 명령어로 add한 것을 취소할 수 있다.

직전에 작성한 커밋 수정

1
$git commit --amend
  • 바로 직전의 커밋만을 수정하는 방법이다.
  • 커밋창이 열리고 메세지를 수정해주면 된다.

직전에 작성한 커밋 삭제

1
$git revert --no-commit HEAD~3..
  • –no-commit을 같이 입력해줘야 한번에 삭제가 가능하다. 안그러면 1개씩 커밋을 삭제해나가야한다.
  • git commit으로 왜 삭제하였는지에 대해서도 적어줘야한다.

댓글 공유

CSS import 피하기

카테고리 Daily

CSS는 어떻게 동작하는가?

  1. CSS는 존재만으로 CSS가 파싱되기 전까지 브라우저 렌더링을 막는다.
  2. CSS는 HTML 파싱도 막는다. 스크립트가 페이지 스타일에 영향을 줄 수 있기 때문에, 브라우저가 CSS 관련 작업 중에는 작업이 완료된 후 script를 실행한다.

그러므로 이러한 상황을 피하기 위해서는 CSS를 최대한 빠르게 불러와야 하며 리소스를 최적의 순서로 불러와야 한다.

CSS import 피하기

@import는 CSS 파일의 렌더링 속도를 느리게 한다.

브라우저 렌더링 순서

  1. HTML 다운로드
  2. HTML이 CSS 요청
  3. CSS가 또 다른 @import에 있는 CSS 요청
  4. 위 단계가 끝나면 Render Tree 생성

@import 사용 시 네트워크 흐름

import css

파일 별개로 분리 시 네트워크 흐름

css split

결론

  • @import 사용 피하기
  • 파일 별개로 분리하여 관리
  • CSS 작성 시 속성을 알파벳 순서대로 작성

댓글 공유

Grid Layout 알아보기

카테고리 Daily

푸터를 만들 때, Grid를 사용하여 설계를 해보자.

1
2
3
4
5
<div class="grid">
<div class="item grid-item1">1</div>
<div class="item grid-item2">2</div>
<div class="item grid-item3">3</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.grid {
display: grid;
grid-template-columns: 100px 100px 100px;
}
.item {
height: 100px;
border: 1px solid black;
}
.grid-item1 {
background: yellow;
grid-area: 1/2/2/4;
}
.grid-item2 {
background: green;
}
.grid-item3 {
background: pink;
grid-area: 1/1/1/1;
}

Grid 레이아웃

  • 전체적인 레이아웃은 grid를 사용하고 내부의 세부적인 레이아웃은 flex를 사용한다.
  • grid 내부의 크기를 grid-area로 지정할 수 있다.
  • grid-area: 열,행
  • grid는 margin-collapse 일어나지 않는다.

댓글 공유

컨테이닝 블록

  • 요쇼의 크기, 위치를 결정하는 요인이다.
  • width, height, padding, margin 속성값과 절대적 위치(absolute, fixed)로 설정된 요소의 offset 속성값은 자신의 컨테이닝 블록으로부터 계산된다.
  • 대부분의 경우 요소의 컨테이닝 블록이 가장 가까운 블록 레벨 조상의 컨텐츠 영역이지만 예외가 있다.

컨테이닝 블록 식별

position의 속성에 따라 완전히 달라진다.

1. position 속성이 static, relative, sticky 인 경우

  • 컨테이닝 블록은 가장 가까운 조상 블록 컨테이너 또는 서식 맥락을 형성하는 조상 요소(flex,table,grid)의 컨텐츠 영역 경계를 따라 형성된다.

2. position 속성이 absolute 인 경우

  • 컨테이닝 블록은 속성값이 static이 아닌 가장 가까운 조상의 내부 여백 영역
  • 그래서 주로 조상 영역에 relative를 추가하여 조상을 기준으로 position을 조절한다.

3. position 속성이 fixed 인 경우

  • 컨테이닝 블록은 viewport, 페이지 영역이다.

예외

position 속성이 absolute, fixed 인 경우, 다음 조건을 만족하는 가장 가까운 조상 내부 영역이 컨테이닝 블록이 될 수 있다.

  1. transform이나 perspective 속성이 none이 아닐 때, transform 속성을 none으로 바꾸면 viewport 기준으로 바뀐다.

  2. will-change 속성이 transform이나 perspective 일 때, will-change는 요소의 예상되는 변화의 힌트를 브라우저에게 제공한다.

  3. filter 속성이 none이 아닐 때

  4. contain 속성이 paint 일 때

1
2
3
4
5
<body>
<div class="container">
<div class="p-absolute">1</div>
</div>
</body>

컨테이닝 블록 예시

  • container 클래스에 아무런 값을 주지 않고 있어 p-absolute 값이 static이 아닌 조상 영역을 기준으로 위치하는데, static이 아닌 조상 요소가 없어서 최상위 브라우저를 기준으로 위치해있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
body {
margin-top: 100px;
margin-left: 100px;
}
.container {
width: 500px;
height: 500px;
background: blue;
position: relative;
/* transform: rotate(0deg); */
/* perspective:0; */
/* transform: rotate(0deg); */
/* perspective: 10px; */
/* will-change: perspective; */
/* will-change: transform; */
/* filter:opacity(); */
}
.p-absolute {
width: 100px;
height: 100px;
background: yellow;
position: absolute;
top: 10px;
left: 50px;
}

컨테이닝 블록 예시2

  • 앞서 설명한 예외를 추가하면 position:relative를 추가한 것과 똑같이 동작하게 된다.
  • 해당 속성을 가진 요소를 기준으로 위치를 조절한다.

댓글 공유

loco9939

author.bio


author.job