February 12, 2022 • ☕️☕️☕️ 14 min read
빠름 빠름 빠름
이제 코드 구조를 어느 정도 이해하고 있는 것 같다. 사내에서 쓰는 약어들도 귀동냥으로 하나 둘 익혀가고. 세부적인 코드는 물론 보안사항일 것이기에 올릴 수 없겠지만, 앞으로 코딩하면서 마주하는 문제들, 그리고 그를 해결한 스토리들을 올리지 않을까 싶다.
사내 프론트엔드 팀원분들이 공유하는 TIL에서 유용한 내용들을 줍줍해서 정리하려고 한다. (나는 안 쓰는 주제에) 사실 매일매일 하는 건 내가 지키지 못할 것이라고 생각해서, 그리고 한번 지키지 못하면 또 울적해서 계속 안할 것이기에 내가 지킬 수 있는 ‘주 1회’ 공부 내용 업로드나 열심히 하려고 한다.
다른 분들과 마찬가지로, children
prop을 필수적으로 포함하고 있는지 여부에 따라서 달라진다고만 생각했다. 그리고 children
prop이 필요하지 않은 컴포넌트들도 많기 때문에, 굳이 컴포넌트의 타이핑을 해주지 않는 편을 택했다. 어쨌든, 여러 문서(예)에서도 React.FC
와 같은 타이핑을 지양하고 있으니까… 게다가 React.VFC
는 deprecated된다는 이야기가 나온지 꽤 된 것 같다.
새롭게 알게된 내용들이 있다. React에서 구현된 Component
들과 Element
들은 모두 -Component
, -Element
와 같은 네이밍을 사용하지만, 처리하는 부분에서 서로 다른 경우가 있다. 하지만 말단의 인터페이스는 동일하다.
React.FC
가 갖고 있는 children
prop은 어떻게 보면 필요한 것일 수도 있다. 결과적으로 컴포넌트는 children
이 모두 존재하기 때문이다! (JavaScript 런타임에서도 변수 슬롯이 적용되지 않았을 뿐, 변수로는 존재한다.)
그러나 문제는 TypeScript 사용 시 발생한다. TypeScript의 구조적 타이핑의 관점에서 children
은 존재하면 안 된다. TypeScript의 타입 체킹은 런타임과 분리되어 있기 때문이다. 어떤 프로퍼티를 갖고 있는가에 따라 타입이 정의되는 TypeScript의 성질상 React.FC
와 class component, pure component를 동일한 수준으로 취급할 수 없게 되었다. 그래서 이들을 호환시키는 atomic component의 입장에서 React.FC
가 가진 children
탓에 복합적인 프로그래밍이 어려워지게 됐다.
그래서 children
을 사용하지 않는 React.VFC
가 나오게 되었으나, 궁극적으로 atomic component에서 이를 제대로 활용하지 못해서 여전히 골칫거리로 남게 된다.
React.FC
와 React.VFC
라는 프레임워크를 최대한 활용하는 방법에는 장단점이 있다. 장점은 프레임워크가 달라지면 TypeScript나 트랜스파일러 등 다양한 도구에서 문법적 오류를 발견하여 에러를 내준다는 것이다. (그래서 쉽게 고칠 수 있다.) 단점은 프레임워크가 달라지면 무조건 그에 따라 변경이 필수적이라는 것이다.
가장 중요한 것은, 런타임과 타입체커 영역에 대한 분리다. 타입 체커 영역에서 불필요한 것을 왜 사용해야 하는지 논의가 이뤄질 수 있다.
tsconfig
의 compilerOptions
의 lib
에 ‘esnext’만 적어준다고 해서 es6부터 최신 문법이 다 커버되지는 않는다. 필요한 문법을 lib
에 각각 넣어줘야 한다.
Ref https://www.typescriptlang.org/tsconfig#lib
useRef
컴포넌트 밖에서 선언한 변수와 useRef
의 다른점은 리액트에서 메모리를 관리하는가 아닌가에 따른 차이가 있다. useRef
로 만들어진 변수는 리액트가 파괴되면 메모리상에서 함께 파괴된다. 다만 전역 변수는 브라우저가 종료되야 파괴된다.
메모리 오너쉽이 리액트에 있다는 것은 리액트의 렌더링 관점에서 무언가 접근할 수 있다는 것이고, 한 번만 렌더링된다는 관점에서는 리액트에서 DOM의 변경과 무관하게 static 변수(혹은 매직넘버)를 넣을 수 있다는 것이다.
또 리액트에서 메모리를 관리한다는 것은, useCallback
이나 useMemo
처럼 어딘가 메모리를 가용한다는 개념이니 성능적 향상을 위해 쓸 수도 있다.
charles은 앱과 인터넷 사이에 위치하는 web proxy 툴이다. 모든 네트워킹 요청은 Charles를 통해서 지나가고 해당 내용을 기록한다. 또한 네트워크 요청과 응답값을 살펴보는 것이 가능하고 심지어 응답 값을 변경하여 클라이언트에 적용할 수 있다.
charles proxy 프로그램을 이용하면 특정 Origin에 대한 요청을 rewrite 할 수 있다. 실제와 매우 유사한 구동환경에서 개발 및 테스트할 수 있다. 앱 내에서 웹뷰 디버깅 시 실제 앱 코드를 수정하지 않고 로컬에서 띄운 웹을 볼 때 유용하다.
charles의 Map Local 기능을 사용하면 응답 데이터를 마음대로 바꿀 수 있어서 다양한 상황에 따른 디버깅에 굉장히 유용하다.
+ USB로 모바일 디바이스를 연결했을 때 charles가 제대로 동작하게 하려면 PC와 동일한 와이파이를 사용하고, 그 와이파이에 프록시 세팅을 해줘야 한다.
Ref https://www.detroitlabs.com/blog/2018/05/01/how-to-set-up-charles-proxy-for-an-ios-simulator/ https://charlesdocsy.com/2020/05/12/rewrite-redirect-url/
@ts-ignore
옵션Ref https://365kim.tistory.com/200
MUI에는 Material Icons를 React Component로 제공하는 @mui/icons-material이라는 패키지가 있다.
svg 파일들이 일일이 React Component 파일의 형태로 되어있는 것이 아니라, svg 파일들을 읽어들이고 SvgIcon
컴포넌트로 매핑하여 빌드된다.
아이콘마다 각각 컴포넌트를 import해서 가져올 수 있다.
import AccessAlarmIcon from "@mui/icons-material/AccessAlarm";
import ThreeDRotation from "@mui/icons-material/ThreeDRotation";
Ref https://github.com/mui/material-ui/blob/master/packages/mui-icons-material/builder.js
여러 클래스에서 공유하는 구현이라면 추상 클래스를 사용하고, 클래스의 타입을 정의하고자 한다면 인터페이스를 쓰자.
html
/body
에 height: 100% | 100vh
는 지양하자min-height: 100vh
사용을 고려해보자.Ref Do not put height 100% on html, body in 2020
ErrorBoundary
가 클래스 컴포넌트로만 가능한 이유ErrorBoundary
는 getDerivedStateFromError
나 componentDidCatch
생명주기 메소드를 기반으로 구현할 수 있는데, 생명주기 메소드는 클래스 컴포넌트에서만 사용할 수 있기 때문이다.
그렇다면, 생명주기 메서드를 기반으로 구현해야 하는 이유는 무엇일까?
craco
같은 CRA 서드파티는 리액트 버전이 올라감에 따라 지원이 안 되는 경우가 있기 때문에 주의해야 한다.
git merge --no-ff
: fast-forward의 관계더라도 merge commit을 만드는 옵션
forceConsistentCasingInFileNames
: 같은 파일에 대한 일관되지 않은 참조를 허용하지 않을지 여부에 대한 설정이다. CRA + TypeScript로 프로젝트 생성 시 자동으로 true로 설정된다.
mobX의 setter를 활용했을 때, makeAutoObservable
이 action
으로 할당해주기 때문에 runInAction
없이 상태변경을 할 수 있지만, 추가 코드량이 증가하기 때문에 runInAction
을 활용하는 것이 좋을 수도 있다.
npm v7 부터는 yarn.lock을 버전 지침으로 활용한다. package-lock.json을 함께 운용하는 이유는, npm이 버전 관리를 위해 남기는 데이터가 yarn의 것보다 더 많기 때문이다.
react/jsx-curly-brace-presence
: JSX props나 children에 대해 { "text" }
와 같은 불필요한 중괄호를 쓰지 않도록 막아주는 린트 룰
react/no-array-index-key
: 리액트 컴포넌트 리스트의 key로 index를 사용하는 것을 방지해주는 린트 룰
html/body에서의 overflow: hidden
은 모든 기기에서 유효하지는 않다. (모바일 기기에서 터치로 스크롤이 되는 등)
프록시 세팅을 할 때 로컬 IP 주소를 입력해야하는데, 통신사 중 특히 KT는 IP를 수시로 줬다 뺏기 때문에 만약 배민 앱 접속 시 503이 뜬다면 일단 IP 주소가 바뀌진 않았는지를 먼저 확인해보자.
axios 에서 쿼리 파라미터를 편하게 쓰기 위해 qs
라이브러리를 활용할 수 있다.
axios.defaults.paramsSerializer = (params) => {
return qs.stringify(params);
};
css width 속성에는 max-content
와 min-content
도 있다.
react 제어 컴포넌트에서 유효성 검사 등의 이유로 value를 바꿔주면 cursor가 맨 뒤로 이동한다. 이 경우 input value 중간으로 커서를 이동시켜 값을 삭제/추가할 때 cursor가 계속 맨 뒤로 이동해 정상적으로 값을 수정할 수 없는 버그가 발생한다. ref를 통해 이전값과 현재값을 비교하고, setSelectionRange API를 사용해 커서 위치를 잡아줄 수 있다.
aos/ios/로컬 개발 환경 등에 따라 폰트가 다르게 적용될 수 있다.
webpack plugin의 동작 구조는 각 동작 파트마다 callback을 등록하여 각 단계별 처리를 하는 구조이다.
util 함수와 같이 여러 부분에서 공통적으로 사용되는 코드는 추상화 정도가 높아야 한다. 도메인-free한 공통함수를 작성하자.
무한스크롤, lottie 등에서 memory leak이 발생하지 않는지 확인하자.
props로 전달받는 on-
핸들러의 경우, 옵셔널로 하는 습관을 들이는 것이 좋다. 특정 비즈니스 코드에서는 핸들러의 존재 여부가 명확하지만, 조금 더 공통 컴포넌트를 작성하다보면 on-
핸들러의 경우 반드시 전달하지 않는 경우가 많아서 이를 예상하고 공통 컴포넌트를 작성하는 편을 권장한다.
id 프로퍼티를 가진 객체의 배열을 id 프로퍼티의 값을 키로 하는 객체로 변경하는 함수를 Object.fromEntries
를 사용하여 만들 수 있다. 기존 코드에서 계속해서 배열에서 요소롤 find 하는게 시간복잡도와 가독성 측면에서 좋지 않다고 판단하여서 하였는데, 모든 요소를 순회하는 메서드의 경우, find
를 줄여서 얻을 부하 감소와 Object.fromEntries
를 사용에 따른 부하 증가를 면밀히 따져보아야 한다. 배열의 크기가 굉장히 크고 실제로 대부분 배열의 앞부분에 찾고자 하는 값이 있을 경우에는 Object.fromEntries
에 따른 성능 감소가 더 클 수도 있다.
package.json에서 private: true
이면 패키지 배포가 refuse된다.
웹뷰가 focus되지 않고 있다면 JavaScript로 focus
를 하더라도 해당 이벤트가 발생하지 않는다. 해당 이벤트들은 그대로 쌓여 있다가 웹뷰가 focus되면 그제서야 focus
이벤트가 발생하면서 onfocus
핸들러가 호출된다.
새로운 웹뷰 또는 새탭에서 브라우저 실행할 때 현재 웹이 active가 되지 않아서 focus()
와 같은 API를 호출해도 onfocus
콜백함수가 실행되지 않는다.
CRA로 build를 하면 index.html 내 빌드 파일명이 바뀌기 때문에 캐시 무효화는 /*
로 모든 파일을 무효화할 필요 없이 /index.html만 무효화 하면 된다.
폰트에 중앙 정렬 속성을 모두 줬는데도 중앙 정렬이 제대로 되어 있지 않다면 해당 폰트 자체에 위 아래 여백이 다르게 들어가있는 건 아닌지 확인해보자.
styled-components를 사용하면 로컬 개발 환경에서는 head
태그 안에 주입해주는데, 빌드를 하고 나면(prod 환경) 별도의 JS 번들로 변환된다. 이때 따옴표가 누락되기도 하고 쌍따옴표가 되기도 하고 띄어쓰기가 사라지기도 한다.
크롬 개발자 도구의 Elements 탭 하단에 ‘Computed’ 탭에 들어가서 스크롤을 맨 아래로 내려보면 ‘Rendered Fonts’를 볼 수 있는데, 여기서 해당 컴포넌트에 있는 글자들 중 몇 글자가 어떤 폰트로 렌더링되었는지를 볼 수 있다. Roboto하고 (8 glyphs)라고 되어있으면 8글자가 Roboto 폰트로 렌더링됐다는 뜻.
이모지는 문자열에서 length 2를 가지며, 이모지에 대한 정규식은 라이브러리로 나와있다.
onfocus
의 event 객체 타입은 전역에 선언되어있는 Focus이벤트이다. React에서 FocusEvent 타입을 가져와서 사용하려할 때 충돌이 발생한다.
아이폰X 부터 상하단의 둥근 영역이 전부 디스플레이에 포함되면서, 해당 영역으로 인해 컨텐츠가 잘리는 것을 방지하기 위해 safe-area를 별도로 두고 있다. 이 safe-area 밖의 공백 영역을 위해 아이폰X 에서 사용 가능한 CSS 속성인 env()
속성과 4개의 변수가 존재한다. (iOS 11.2 이전에는 constant
, 그 이후에는 env
)
Visual Viewport API를 사용하면 가상 키보드의 활성화 여부를 감지할 수 있다. 가상 키보드가 올라가고 내려가는 것은 window.visualViewport
에서 발생하는 resize
이벤트로 감지할 수 있다.
CSS에서 동일한 font-size
를 지정해도, font-family
에 따라 글자의 높이가 서로 다르게 렌더링되는 경험이 있을 것이다. line-height
의 값을 1로 정하는 것이 왜 옳지 않은지 설명한다.
Ref https://mygumi.tistory.com/366 https://wit.nts-corp.com/2017/09/25/4903
JavaScript와 HTML, CSS 정적 리소스들을 활용하여 웹 애플리케이션을 구성하는 스택인 JAM Stack 구조에서 성능을 최적화시킬 수 있는 방법을 소개한다.
토스에서는 SSG(Static Site Generation)를 사용하고 있다. SSG는 앱을 빌드하는 시점에 미리 그려두고 이를 서빙하는 방식으로, JAM Stack에서 정적 리소스를 생성하는 용도로 사용한다. 컴파일 단계에서 미리 그릴 수 있는 부분을 최대한 그려서 사용자에게 도달하는 최초 index.html을 구성한다. Next.js 프레임워크를 사용하여 SSG를 활용하고 있다.
비동기를 제어할 때는 React의 Suspense
를 사용하는데, Next.js와 함께 사용 시 Suspense
는 서버사이드 렌더링을 지원하지 않기 때문에(알파 버전이 공개되어 있는 React18에서는 지원할 예정이다.) Suspense
를 한번 감싸서 사용해주고 있다.
import { useState, useEffect, Suspense as ReactSuspense } from "react";
export function Suspense({
fallback,
children,
}: ComponentProps<typeof ReactSuspense>) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (isMounted) {
return <ReactSuspense fallback={fallback}>{children}</ReactSuspense>;
}
return <>{fallback}</>;
}
Suspense
로 제어하고 있는 컴포넌트를 SSG로 빌드하면 fallback
이 렌더링된다. 이때 컴포넌트와 API를 가깝게 배치시켜, 데이터가 필요한 곳에서 가장 가까운 곳에서 API를 호출해주도록 한다. 즉, 컴포넌트를 작게 쪼개 각각을 Suspense
로 감싸준다.
function UserPage() {
return (
<Layout>
<h1>사용자 정보</h1>
<dl>
<dt>이름</dt>
<Suspense fallback={<dd>Loading</dd>}>
<UserName />
</Suspense>
</dl>
<h2>사용자 상세 정보</h2>
<Suspense fallback={<dd>Loading</dd>}>
<UserDetailInfo />
</Suspense>
</Layout>
);
}
UserName
과 UserDetailInfo
컴포넌트를 각각 Suspense
로 감쌌기 때문에 UserPage
에는 먼저 데이터가 준비된 컴포넌트부터 렌더링이 시작된다. 사용자는 더 이상 멍하니 흰 화면을 보고 있지 않아도 된다.
CSS-in-JS 방식의 하나인 emotion
라이브러리를 사용할 경우 dev와 prod에서 확인할 수 있는 스타일 태그의 형태가 다르다. prod에서는 스타일 태그들이 비어있다. dev에서는 DOM을 수정하는 방식을 선택하는 반면, prod에서는 CSSOM을 수정하는 방식을 선택하기 때문이다.
이는 runtime CSS-in-JS라는 emotion
의 특성과 겹치는데, styled-component
도 동일하다. prod에서도 dev와 같이 style
태그를 직접 추가한다면, 컴포넌트에서 런타임에 스타일을 수정할 때마다 style
태그가 추가되고, 그러면 DOM 트리를 다시 그리고 또 CSSOM 트리를 구축하는 과정이 반복되어 렌더링이 차단되기 때문이다.
emotion
은 DOM 트리는 수정하지 않고 CSSOM을 수정하는 방식을 선택하여 DOM 트리 파싱에 드는 시간을 줄이는 방식을 선택한 것이다. (styled-component
도 마찬가지)
🤔 JavaScript로 조작하는 UI 스타일링들은 JavaScript 런타임과 연관이 생기고, 성능 관련 문제들을 유발시킨다. 그렇다면 아예 runtime이 아닌 라이브러리를 쓰면 되지 않을까?
linaria라는 zero runtime css-in-js 라이브러리가 있기는 하다. linaria는 프로젝트 빌드 시에 CSS 파일에 스타일을 추출하고 CSS 파일을 로드하는 방식으로 동작한다. babel plugin과 webpack loader를 사용해서 빌드 시에 별도의 CSS 파일을 생성하고, 이 파일 안에서 prop이나 state 등에 의한 값들을 CSS 변수로 정의하고 CSS 변수의 값을 변경시킴으로써 동적 스타일링을 구현한다.
CSS와 JavaScript가 별도의 파일에 있기 때문에 CSS와 JavaScript를 병렬로 로드할 수 있고, CSS 구문 분석과 같은 추가 작업이 런타임에 수행될 필요가 없기 때문에 런타임 성능이 향상된다. 하지만 CSS 변수를 사용한 방식인 만큼 브라우저 호환성에 문제가 있다.
🤔 atomic css는 또 뭘까?
facebook… 아니 메타에서는 빌드 타임에 CSS를 생성해 atomic css를 JavaScript적인 방법으로 활용할 수 있는 stylex
라이브러리를 개발하여 CSS 파일 크기를 80% 줄이면서 최적화를 극대화했다.
atomic css가 갖는 특징은 다음과 같다.
className
등을 통해 스타일을 가져다 쓰는 방식Ref https://ideveloper2.dev/blog/2022-01-25—emotion으로-파악해보는-css-in-js의-이모저모/
마침 지금 듣고 있는 아이유의 unlucky에서 ‘울고 싶을지 몰라~’하는 가사가 지나갔다. 진짜 울고싶다 😫 진짜 unlucky한 인생이다. 일적으로도, 공부도 취미도 아무 문제들이 없었는데 그냥 또 불안해하던 타이밍에서 진짜 최악의 일이 터졌다. 근데 자꾸 최악이라는 표현을 쓰니까 별 거 아닌데도 최악이라고 규정지어버리는 것 같다. 누군가한테 옮은 말버릇인 것 같은데 쓰지 말아야겠다. 자꾸 스스로를 학대하면서 침잠하지 말고 벗어날 생각을 해야지 지은아.
좋은 일이 없었나. 아, 회사 사무실이 있는 롯데타워에 갔다. 38층까지 있는 사무실 건물의 38층이라니! 정말 처음 보는 뷰였다. 고소공포증이 있는 줄은 전혀 몰랐는데, 좀 무서웠다. 고작 하루였지만 팀원 분도 뵙고, 투어도 하고, 사무실이라는 공간에서 일하니 더 집중도 되는 것 같고 좋았당. 가끔씩 또 가고 싶다!