본문으로 건너뛰기

"web" 태그로 연결된 11개 게시물개의 게시물이 있습니다.

모든 태그 보기

왜 React는 Server Component를 만들게 되었을까?

· 약 19분
Hyunmo Ahn
Front End Engineer @ Line+

Purpose

React 18이 2022년에 출시되었고, Nextjs App router가 2023년에 출시되어 이미 케케묵은 조사이지만, nextjs page router와 app router를 쓰며 어떤 점이 달라졌고, 왜 달라졌는지 이유를 찾던 중 app router를 왜 사용하게 되었는지 이유를 찾게 된 글이 있다. 그 글을 읽고 이해한 바를 정리하고자 한다.

Story

글을 읽기 전에 다음과 같은 상황을 가정해보자.

새로운 회사로 이직을 하기 위해 Frontend 개발자 면접자리에서 다음과 같은 질문을 받았다고 하자.
아니면, 새로운 프로젝트 혹은 기존 프로젝트에서 nextjs app router를 사용하자고 팀원들을 설득을 하는 자리이거나,
혼자서 nextjs의 page router와 app router 중 어떤 기술이 적합한지 고민하고 있는 상황이라고 하자.

단순 app router가 새롭게 나온 기술이고, Frontend 개발자로써 app router를 사용해보자는 답안도 충분히 설득력이 있을 수 있다. 하지만, 아래와 같은 질문의 답을 이해하고 있다면, 조금 더 설득력 있는 답변을 할 수 있을 것이다.

Question
  • Q. SSR(Server Side Rendering)과 CSR(Client Side Rendering)의 차이점은 무엇인가?
  • Q. 기존 방식 (nextjs page router)에서 어떤 한계가 nextjs app router 방식을 만들게 되었을까?
  • Q. React에서 Suspense를 사용하는 이유는 무엇일까?

위 질문의 답을 알고 있다면, 본문을 읽지 않아도 무방할 것이고 조금 더 신뢰도가 있는 글을 원한다면 React 18 Architecture 소개 글을 보아도 좋을 것이다. 본문을 읽지 않고 답만 알고 싶다면 Result를 먼저 살펴보자.

다음 본문은 Architecture 글을 읽고, 이해한 바를 필자의 의견을 섞어 정리한 글이다.

Local Storage 용량 초과

· 약 5분
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

프로젝트에서 LocalStorage를 사용했을 때 용량을 고려한 적이 있나요?

localStorage의 용도에 따라 다르겠지만, 대부분의 경우는 용량을 고려하지 않고 사용하게 됩니다. 하지만, 데이터의 성격이 계속해서 쌓이는 구조의 데이터를 저장한다면 데이터는 localStorage의 용량을 초과할 수 있습니다.

초과하면 과연 어떻게 동작할까요?

정답은 Throw 에러 입니다.

Figure 1Local Storage Exceed Error

Solution

위 에러가 발생하면, setStorage 구문은 throw로 인해 종료가 되고 에러 전파를 진행하게 됩니다.

프로젝트에 에러 전파를 잘 처리하고 있다면, 에러 메세지와 함께 에러 로그가 수집 될 것이고 전파를 잘 처리 하지 않았다면 프로젝트가 멈출 수 있습니다.

이러한 상황을 방지하기 위해서는 try-catch 구문을 사용해서 에러를 잡아내고, 에러를 처리하는 로직을 추가해야 합니다.

const setStorage = (key, value) => {
try {
window.localStorage.setItem(key, value);
} catch (e) {
// Stop Throw Error
console.error('Local Storage Exceed Error', e);
}
}

물론 에러가 발생했다는 것은 LocalStorage의 용량을 초과했다는 것이므로 앞으로 사용자는 계속해서 Local Storage Exceed Error가 발생하고, 데이터를 저장하지 못할 것 입니다.

따라서 에러처리 뿐 아니라 데이터를 지워주는 작업까지 추가해주어야합니다.

const setStorage = (key, value) => {
try {
window.localStorage.setItem(key, value);
} catch (e) {
// Stop Throw Error
console.error('Local Storage Exceed Error', e);
window.localStorage.clear();
}
}

React의 이벤트 전파

· 약 13분
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

React 이벤트 핸들러 방식과 vanilla Javascript의 이벤트 핸들러 방식을 혼용해서 사용할 때 이벤트 전파가 의도한대로 동작하지 않을 수 있다. 예를 들면 아래와 같이 button1, button2 구조에서 button2를 클릭했을 때 button1의 이벤트 핸들러도 실행되는 문제가 발생한다.

const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});

const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};

return (
<button id='button1'>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);
// When button2 is clicked
button2 clicked
button1 clicked

Solution

React에서 이벤트 전파는 vanilla Javascript에서 사용하는 이벤트 전파와 다르다. React에서 이벤트 전파를 delegation 방식으로 처리하기 때문이다. (comment)

React에서는 (React 17 이후로) 이벤트 리스너를 rootDOM에 등록해서 사용한다. 따라서 React끼리의 event listener는 생각한 대로 DOM 구조에 따라 이벤트가 전파가 전달되지만 vanilla Javascript의 이벤트 전파는 생각대로 동작하지 않는다.

가능하다면 두가지 이벤트를 섞어서 쓰지 않는 방향이 좋겠지만, 3rd party library를 사용하거나 제어하지 못하는 부분의 이벤트는 제어하기 힘들기 때문에 동작 방식에 맞춰 이벤트 전파를 막아야한다.

// Unify event listener to vanilla Javascript
const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});

const button2El = document.getElementById('button2');
button2El.addEventListener('click', (e) => {
e.stopPropagation();
console.log('button2 clicked');
});

return (
<button id='button1'>
<button id='button2'>Click me</button>
</button>
);

// Or
// Unify event listener to React
const handleClick1 = (e) => {
console.log('button1 clicked');
};

const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};

return (
<button id='button1' onClick={handleClick1}>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);

Nextjs에서 server action은 순차적으로 실행된다

· 약 8분
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

nextjs에서 server action은 client 환경에서도 server를 통해 함수를 호출하기 위한 방법으로 사용된다. 이 때, server action은 순차적으로 실행되어 여러 함수를 호출하더라도 하나씩 실행되므로 사용에 유의해야한다.

Behavior

간략하게 요약 된 예시를 보자. 예시는 두가지 함수가 존재한다.

'use server';
import { format } from 'date-fns';

export const callServerFn = async (count)=> {
return new Promise((resolve) => setTimeout(() => {
const time = format(new Date(), 'HH:mm:SS.sss');
console.log(`Server Call #${count}: `, time);
resolve(count)
}, 1000));
}

callServerFn 함수는 응답에 1초가 걸리는 함수이고, 'use server'를 사용하며 server action으로 사용된다. 해당 로직은 next server 에서 이루어진다. 서버 응답 타이밍을 확인하기 위해 log를 찍고있다.

// client.tsx
'use client';

export const CallButton = () => {
const handleClick = async () => {
const result = await Promise.all([
callServer(1),
callServer(2),
callServer(3),
])
}

return (<button onClick={handleClick}>Hello World</button>);
}

CallButton 컴포넌트는 버튼을 클릭하면 callServer 함수를 3번 호출하고, 모든 함수가 완료되면 결과를 반환한다.

Server Call #1:  01:23:22.010
Server Call #2: 01:23:24.011
Server Call #3: 01:23:25.012

버튼을 눌렀을 때 분명 3개의 호출이 동시에 이루어졌지만, log 결과는 하나씩 순차적으로 1초에 한번씩 호출되는 것을 확인 할 수 있다.

CSS로 line-spacing을 구현하는 방법

· 약 4분
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

CSS에서는 줄 간격을 조정하는데 line-height를 사용한다. 하지만 내가 경험했던 프로젝트에서 line-spacing을 지원해야하는 요구사항이 있었다. 이에 대해 CSS로 line-spacing을 구현하는 방법에 대해서 설명하고자 한다.

Line Spacing

Line Gap

Line Gap

Line Gap

Line Spacing (5px)

Line Height

Line Gap

Line Gap

Line Gap

Line Height (fontSize + 5px)

Cheat Sheet

여백이 위, 아래로 나눠서 적용되는 line-height와 달리, line-spacing은 줄 사이에만 여백을 적용한다. 이 때, line-height와의 차이는 줄의 처음과 끝의 여백이 들어가지 않는다는 점이다.

이를 이용해서 margin을 이용하여 line-spacing을 구현할 수 있다.

const lineHeight = 20; // origin line-height
const lineSpacing = 10;
const margin = - (lineSpacing / 2);

const LineSpacingText = () => {
return (
<p style={{
marginTop: `${margin}px`,
marginBottom: `${margin}px`,
lineHeight: `${lineHeight + lineSpacing}px`,
}}>
Hello, World!
</p>
);
};

Playground

Line Spacing 0px

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.

Line Spacing (0px)

Line Height (0px + fontSize)

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.

Line Height (0px + fontSize)

nested component를 피해야 하는 이유

· 약 5분
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

프로젝트를 진행하면서 불필요하게 re-render가 발생하는 문제를 만났다.

단순히 render를 하는 것 뿐 아니라 매 render시마다 DOM을 새롭게 다시 그리고 있던 것이 문제였는데, 원인은 nested component로 사용한 코드였다.

따라서 이 글에서는 nested component를 사용하는 것을 피해야 하는 이유를 설명하고자 한다.

Cheat Sheet

// Bad
const List = ({ hasWrapper, borderStyle, children }) => {
const Border = () => {
return <p style={borderStyle}>{children}</p>;
}

if (hasWrapper) {
return (
<Wrapper>
<Border />
</Wrapper>
);
}

return <Border/>;
};
// Good
const List = ({ hasWrapper, borderStyle, children }) => {
if (hasWrapper) {
return (
<Wrapper>
<Border borderStyle={borderStyle}>
{children}
</Border>
</Wrapper>
);
}

return (
<Border borderStyle={borderStyle}>
{children}
</Border>
);
};

const Border = ({ borderStyle, children }) => {
return <p style={borderStyle}>{children}</p>;
}

위와 같이 Border의 코드를 재사용하고, List의 props를 재사용하기 위해 nested component를 사용하게 될 수 있다. 하지만 이런 경우 children은 불필요하게 매 render시마다 새롭게 DOM이 그려지게 된다.

callback function props로 인한 re-render를 피하는 법

· 약 11분
Hyunmo Ahn
Front End Engineer @ Line+

React를 사용하다 보면 function을 component props로 넘길 때 re-render를 피하기 위해 reference 관리에 주의해야 한다. 대부분 useCallback을 사용해서 불필요한 re-render를 피하게 되지만 좋은 대안을 발견하게 되어 이 글을 쓰게 되었다.

영감을 받은 코드는 radix-ui/primitivesuseCallbackRef 이며 이 글에서는 useCallbackRef를 쓰는 케이스와 동작을 이야기 할 예정이다.

아래의 코드는 앞으로 설명 할 useCallbackRef 코드이다. 과연 어떤 상황에서 사용 할 것 같은가?

// useCallbackRef.js
import { useRef, useEffect, useMemo } from 'react';

export function useCallbackRef(callback) {
const callbackRef = useRef(callback);

useEffect(() => {
callbackRef.current = callback;
});

return useMemo(() => ((...args) => callbackRef.current?.(...args)), []);
}

Front-end에서 OAS generator를 어떻게 쓰면 좋을까?

· 약 34분
Hyunmo Ahn
Front End Engineer @ Line+

최근 프로젝트에 OAS-generator를 도입해서 사용하고 있다. 사용하기로 결정할 때 고민이나 확인이 필요한 점이 많았고, 적용 이후 좋은 점도 많은 것 같아서 OAS-Generator 사용 경험에 대해 글을 쓰려한다.

아마 OAS Generator가 무엇인지 궁금한 분들, 알고는 있지만 도입에 고민이 되시는 분들, 이미 사용하고 있지만 잘 사용하고 있는지 망설여지는 분들이 이 글을 읽고 좋은 모티브나 경험을 배워갔으면 좋겠다.

이 글에서 말하고 있는 건 다음과 같다.

  • OAS-generator 가 어떤 것인지.
  • OAS-generator 를 사용했을 때의 장단점.
  • OAS-generator 를 어떻게 사용해야하는지.
    • 설정
    • 커스텀 템플릿
  • 최적화
사전지식
  • Rest API로 Front-end 개발을 진행해 본 경험
  • Mustache 문법을 읽을 수 있는 지식 (Optional)
    • 글을 이해하는데는 필요없지만 실제로 사용한다면 꼭 알아야한다.

immer 내부 살펴보기

· 약 45분
Hyunmo Ahn
Front End Engineer @ Line+

이 글은 기본적으로 immer에 대해서 알아보는 시간을 가진다. 만약 immer를 잘 모르는 분들은 아래 챕터를 먼저 읽어보는 것을 권장한다.

무엇이 궁금할까?

Question

Q1. immer는 객체 mutable하게 바꾸는 방식을 어떻게 immutable한 방식으로 바꾸어주고 있을까?

immer는 mutable하게 변경하는 객체 built-in method를 사용하더라도 immutable하게 데이터를 반환해주는 기능을 한다. 이 기능이 내부적으로 어떤 방식으로 이루어지는지 알아본다.

아래 코드 예시는 immer 공식 문서에 존재하는 basic example을 가져온 것이다.

import produce from 'immer';

const baseState = [
{
title: "Learn TypeScript",
done: true,
},
{
title: "Try Immer",
done: false,
},
]

const nextState = produce(baseState, (draft) => {
draft.push({ title: "Tweet about It" });
draft[1].done = true;
})

console.log(baseState === nextState) // false
console.log(nextState)
/*
[
{
title: "Learn TypeScript",
done: true,
},
{
title: "Learn TypeScript",
done: true,
},
{
title: "Tweet about It",
},
]
*/
Question

Q2. immer는 어떻게 structural sharing을 사용하는걸까?

*structural sharing: 객체를 copy할 때 변경되지 않은 객체는 reference를 동일하게 사용하는 방식.

객체를 immutable하게 업데이트 한다는 것은 기존 객체를 새로운 객체로 복사한다는 것이다. 즉, 복사에 비용이 발생한다. immer는 객체를 복사할 때, 변경되지 않은 reference는 재사용하는 structural sharing 방식을 사용해서 객체를 복사한다. immer에서는 어떤방식을 사용해서 structural sharing을 사용하고 있는지 알아본다.

Question

Q3. immer에서는 produce함수 내에서 draft를 직접 업데이트하는 방식이 아니라 return을 통해서 데이터를 업데이트하는 경우가 있는데, 이런 경우에 로직이 다른지?

immer를 사용할 때, 위에서 제시한 mutable한 객체 변경 방식이 아닌, 새로운 객체를 리턴하는 경우가 있다. 이는 immer와는 무관하게 immutable하게 Javascript에서 객체를 반환해주는 방식과 동일하다. immer에서는 이러한 방식을 공식적으로 허용하고 있고 두 방식, mutable하게 객체를 변경하는 방식과 immutable하게 객체를 변경시키는 방식 모두 혼용해서 쓰는 개발자도 많을 것 이다. 이러한 방식 차이는 immer에서 어떤 로직차이를 발생시키는지 알아본다.

// mutable method
const nextState = produce(baseState, (draft) => {
draft.push({ title: "Tweet about It" });
draft[1].done = true;
})

// immutable method
const nextState = produce(baseState, (draft) => {
return {
...baseState,
{ ...baseState[1], done: true },
{ title: "Tweet about It" },
}
})
사전지식
  • immer 혹은 redux-toolkit을 사용해 본 경험
  • Proxy에 대한 이해 (optional)

redux-devtools의 time-travel-debugging 톺아보기

· 약 42분
Hyunmo Ahn
Front End Engineer @ Line+

목적

redux를 이용해서 웹 어플리케이션 개발을 진행해 본 사람이라면 redux-devtools를 이용해서 time-travel 디버깅을 사용해본 경험이 있을 것이다.

redux-devtools가 어떤 것인지 헷갈리시는 분이 있다면 아래 영상을 참고하기 바란다.

만약 redux-devtools를 사용해 본 경험이 없다면 이번 article을 이해하기 어려울 수 있다.

redux-devtools는 redux를 사용한 web application의 redux정보(action, reducer, state)를 기록하고, 특정 시점의 reducer로 rollback하고, 특정 action을 없었던 일로 할 수 있다. 하지만, redux-devtools를 사용하지 않고 web application 내부에 비슷한 동작을 구현을 하려 한다면 간단한 일이 아니다. 예를들어, A버튼을 눌렀을 때 이제껏 발생시켰던 action을 없었던 일로 한다거나, Submit버튼을 누르기 전 이탈 한다면 해당 페이지에서 발생시켰던 모든 action을 rollback한다거나 하는 로직말이다.

redux-devtools는 버튼으로 손쉽게 제공하는 동작을 직접 로직을 구현한다고 하면 방법을 모르겠다. redux-devtools는 어떻게 이러한 일들을 가능하게 할까?

이 article에서는

  • redux-devtools 에서 호출되는 action과 reducer를 logging하는 방법.
  • redux-devtools 에서 특정 action이 dispatch 된 시점으로 jump하는 방법.
  • redux-devtools 에서 특정 action이 동작하지 않은 것 처럼 skip시키는 방법.

에 대해서 확인해 볼 것 이다.

사전지식
Caution
  • 이 문서는 browser extension에 대한 내용은 다루지 않습니다.
  • redux-devtools의 core에 대해서 다루고 있으며 browser extension에 대한 지식이 없어도 이해가 가능합니다.
  • redux-devtools-extensions와 관련하여 browser extension에 대한 내용을 원하는 분들은 이 글에서 다루는 내용과 맞지 않을 수 있습니다.