pre-render 환경에서의 반응형 웹 구현
nextjs에서 pre-render는 pre-render를 하는 시점에 따라 두가지 형태로 나뉜다. 첫번째는 static generation으로 빌드할 때 pre-render를 하는 것이다. 두번째는 server-side rendering으로 요청이 들어왔을 때 pre-render를 하는 것이다. 어떤 형태든 클라이언트에서 HTML을 만들지 않는다. 하지만 반응형 웹 구현은 클라이언트에 의존적이다. 그렇다면 pre-render 상황에서 어떻게 반응형 웹을 구현할 수 있을까? 여러가지 방법들에 대해 알아보자. 1. user-agent 이용하기 요청 헤더 중 user-agent는 다음과 같이 클라이언트의 OS, 엔진, 브라우저 정보를 포함하고 있다. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36 그리고 대부분의 모바일 기기는 user-agent에 Mobile이라는 키워드를 담아서 보내는데, 이를 이용하여 반응형 웹을 구현할 수 있다. 다음은 UAParser 라이브러리로 user-agent를 파싱하여 반응형 웹을 구현한 것이다. // Server const uaParser = require('ua-parser-js'); const userAgent = uaParser(req.headers['user-agent']); const { type } = userAgent.getDevice(); const html = ReactDOMServer.renderToString( <DeviceContext.Provider type={{ type }}> <App /> </DeviceContext.Provider>, ); // Client const App = () => ( <DeviceContext.Consumer> {({ type }) => (type === 'mobile' ? <MobileLayout /> : <DesktopLayout />)} </DeviceContext.Consumer> ); 하지만 이 방법은 user-agent로부터 구체적인 디바이스 뷰포트나 가로 모드 여부를 알 수 없다는 치명적인 단점이 있다. 그렇기 때문에 데스크톱에서 브라우저를 모바일 사이즈로 줄여서 페이지를 요청하는 경우 반응형 대응이 불가하다. 더불어서 브라우저마다 Mobile 키워드가 아닌 Mobi, IEMobile, Tablet 등의 다른 키워드를 사용하는 문제점도 존재한다. 2. 하나의 진입점에 media-query 적용 아래 App 컴포넌트는 div라는 하나의 진입점에 media-query를 적용하여 반응형 웹을 구현했다. 이 방법은 기기의 뷰포트에 상관없이 모두 동일한 DOM 트리 구조를 갖는 특징이 있는데, 만약 뷰포트에 따라 다른 DOM 트리 구조를 보여줘야 한다면 다른 방법을 선택해야 한다. .layout { width: 80vh; } @media screen and (max-width: 768px) { .layout { width: 100vh; } } const App = () => (<div className="layout">My Application</div>); 3. 여러 진입점에 media-query 적용 뷰포트에 따라 다른 DOM 트리 구조를 보여줘야 하는 경우 다음과 같이 여러 진입점에 media-query를 적용해준다. import styled from '@emotion/styled'; export default function PostDetail() { return ( <> <DesktopLayout /> <MobileLayout /> </> ); } const DesktopLayout = () => { return <DesktopLayoutWrapper>Desktop</DesktopLayoutWrapper>; }; const MobileLayout = () => { return <MobileLayoutWrapper>Mobile</MobileLayoutWrapper>; }; const DesktopLayoutWrapper = styled('div')(() => ({ display: 'none', '@media screen and (min-width: 961px)': { display: 'block', }, })); const MobileLayoutWrapper = styled('div')(() => ({ display: 'none', '@media screen and (max-width: 960px)': { display: 'block', }, })); 언뜻 보기에는 이전 방법과 큰 차이가 없어보이지만 한 가지 알고 넘어가야 하는 부분이 존재한다. pre-render된 페이지의 html 응답을 보면 아래와 같이 MobileLayout과 DesktopLayout 태그가 모두 DOM 트리에 존재한다. 이는 클라이언트 DOM 트리도 마찬가지이다. 하지만 화면을 보면 실제 MobileLayout이나 DesktopLayout 둘 중 하나만 보여지고 있다. 즉, Render 트리에는 하나만 포함된다. 문제는 보여질 필요가 없는 태그까지 DOM 트리에 포함되기 때문에 페이지 사이즈가 커지고, 더욱 더 심각한 것은 보여질 필요가 없는 태그까지 모두 마운트 된다는 것이다. 이는 사이드 이펙트로 연결될 수도 있다. 조금 더 자세히 설명하면, display: none을 하는 경우 태그는 DOM 트리에 포함되고 Render 트리에는 포함되지 않는다. DOM 트리에 포함된다는 것은 태그가 마운트 됐다는 것이고, 마운트된 태그는 useEffect를 포함한 내부의 모든 로직들을 실제로 실행한다. 이는 사이드 이펙트로 연결될 수 있다. 실제로 DesktopLayout과 MobileLayout에 콘솔을 작성하고 MobileLayout이 보여질 크기로 화면을 축소하여 페이지를 요청하면 DesktopLayout 내부의 콘솔도 찍히는 것을 확인할 수 있다. const DesktopLayout = () => { console.log('desktop layout 내부 로직'); useEffect(() => { console.log('desktop layout의 useEffect 내부 로직'); }, []); return <DesktopLayoutWrapper>Desktop</DesktopLayoutWrapper>; }; const MobileLayout = () => { console.log('mobile layout 내부 로직'); useEffect(() => { console.log('mobile layout의 useEffect 내부 로직'); }, []); return <MobileLayoutWrapper>Mobile</MobileLayoutWrapper>; }; 굳이 로직 실행에 의한 사이드 이펙트 상황이 아니더라도 문제는 발생할 수 있다. 예를들면 h1 태그의 중복 존재 문제이다. 2개의 h1 태그가 존재하고 1개의 h1 태그를 display: none 처리하는 경우, 크롤러는 display: none 처리된 h1 태그를 인식하기 때문에 이 경우 한 페이지에 h1 태그가 중복 존재하여 SEO에 영향을 줄 수 있게 된다. 4. @artsy/fresenel 이용하기 이 라이브러리는 앞선 방법과 마찬가지로 pre-render시 반응형 웹에 필요한 태그가 모두 포함된 DOM 트리를 그린다. 하지만 앞선 방법과는 다르게 보여질 필요가 없는 태그는 클라이언트 DOM 트리에 포함되지 않아 마운트로 인한 사이드 이펙트 문제가 발생하지 않는다. 코드는 다음과 같이 작성한다. import { Media, MediaContextProvider } from '@/components/Media'; const Main = () => { return ( <MediaContextProvider> <Media greaterThanOrEqual="lg"> <Desktop /> </Media> <Media lessThan="lg"> <Mobile /> </Media> </MediaContextProvider> ); }; export default Main; const Desktop = () => { return <div>Desktop</div>; }; const Mobile = () => { return <div>Mobile</div>; }; 앞선 방법과 마찬가지로 pre-render된 페이지의 html 응답을 보면 아래와 같이 MobileLayout과 DesktopLayout 태그가 모두 DOM 트리에 존재하는 것을 알 수 있다. 하지만 앞선 방법과는 다르게 클라이언트의 DOM 트리를 보면 뷰포트에 대응하는 태그만 포함된 것을 알 수 있다. 물론 마운트가 되지 않으니 내부에서 콘솔도 전혀 찍히지 않는다. const Desktop = () => { console.log('desktop layout 로직 실행'); useEffect(() => { console.log('desktop layout useEffect 내 로직 실행'); }, []); return <div>Desktop</div>; }; const Mobile = () => { console.log('mobile layout 로직 실행'); useEffect(() => { console.log('mobile layout useEffect 내 로직 실행'); }, []); return <div>Mobile</div>; }; 참고로 style을 head에 injection하는 이유는 flicker를 없애기 위함인데, html이 파싱될 때 style 태그가 먼저 파싱되게하여 flicker를 막는다는 접근이다. 그렇다면 @artsy/fresnel은 pre-render 상황의 반응형 웹 구현에 있어서 은탄환일까? 아니다. @artsy/fresnel은 다음과 같은 문제점들이 존재한다. 1.여전히 페이지 사이즈가 커지는 문제를 해결하지 못했다. 2.breakpoint로의 접근은 Media를 통해서만 가능하고, 다음과 같이 다른 컴포넌트들은 직접 접근이 불가하다. <Sans size={sm ? 2 : 3}> 3.react 18 버전 문제로 인해서 개발 환경에서 Hydration 에러가 발생한다. 이슈 참고로 반응형 웹을 구현하기 위한 라이브러리로는 @artsy/fresnel 이외에도 react-responsive와 react-media가 존재한다. 하지만 이 둘은 pre-render 상황에서 반응형 웹 구현을 깊이있게 고려하지 않았다. 먼저 react-media는 pre-render 환경에서의 반응형 웹을 user-agent 방법으로 해결하고 있다. 그리고 react-responsive는 기본적으로 pre-render하지 않는다. 서버에서 어떤 뷰포트로 그릴 것인지 device 프로퍼티로 미리 정해줄 수 있지만, 정하지 않는다면 기본적으로는 pre-render되지 않는다. 참고 문헌 Server-Rendering Responsively React 02 - SSR vs Responsive Design Two duplicated h1 tags and one hidden .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk17 { color: #808080; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }SEO 향상을 위한 h1 태그 사용법
SEO에 있어서 h1 태그는 어떤 의미를 가질까? 1. 검색 엔진이 페이지 콘텐츠를 이해하는 데 도움을 준다 구글의 John Mueller가 언급하길, 어떤 페이지의 rank가 향상되길 원한다면 이해하기 쉽도록 작성하는 것이 좋다고 한다. 그리고 검색 엔진은 독자와 같아서 h1 태그를 통해 방문한 페이지가 어떤 페이지인지 이해한다. 이 말인즉슨, h1 태그가 자세할수록 더 좋다는 의미이다. 2. UX를 향상시킨다 h1 태그를 통해서 어떤 페이지인지 한눈에 알 수 있다는 점에서 h1 태그는 UX에 도움이 된다. 그리고 UX는 SEO ranking factor다. 이미지 출처 3. 웹 접근성을 향상시킨다 웹 접근성 솔루션을 제공하는 비영리단체인 WebAIM에서는 스크린 리더를 사용하는 독자들의 60%는 heading 태그를 이용해 페이지 네비게이션을 한다는 설문 결과를 발표했다. 웹 접근성은 정량화가 어렵기 때문에 직접적인 ranking factor가 될 수 없지만, 웹 접근성이 좋은 웹사이트는 좋은 UX를 갖게 되고, UX는 웹 접근성보다 측정이 훨씬 쉽다. 그리고 W3C에서 제안하는 웹 접근성 가이드라인인 Web Content Accessibility Guidelines(WCAG)이 제시하는 기준은 SEO 규칙과 요구사항을 서술하는 Google Search Essentials과 거의 동일하다. 그렇다면 h1 태그는 어떻게 잘 작성할 수 있을까? 1. h1 태그는 페이지 타이틀로 작성한다 구글에서는 h1 태그와 같이 아티클의 내용 위의 눈에 띄는 위치에 아티클의 제목을 사용하라고 제시하고 있다. 2. Title Case를 사용한다 Title Case란 책 혹은 영화의 제목에 포함되는 단어 중 중요한 단어를 대문자로 작성하는 것이다. 대문자로 작성하는데는 여러가지 복잡한 규칙이 존재하는데 작성 사이트의 도움을 받을 수 있다. 3. h1 태그와 Title 태그를 일치시킨다 구글에서는 h1 태그와 title 태그를 일치시키라고 제시하고 있다. 하지만 h1 태그가 너무 길어질 경우에는 title 태그를 대략적으로 일치시켜도 좋다. 검색 엔진에는 title 태그 내용이 노출된다. 만약 title 태그와 h1 태그가 전혀 다르다면 title 태그 내용을 기대하고 들어온 독자들은 전혀 다른 내용의 h1 태그를 보고 속았다고 생각하게 될 것이다. 4. 중요하다고 생각되는 모든 페이지에 h1 태그를 작성한다 다만, 중요하다고 생각되지 않거나 검색 엔진에 보여질 필요가 없는 페이지라면 굳이 h1 태그를 넣을 필요는 없다. 5. 페이지당 하나의 h1 태그만 작성한다 구글 검색 엔진은 둘 이상의 h1 태그가 존재하는 상황을 고려하여 동작하기 때문에 SEO 관점에서는 이 규칙을 신경 쓰지 않아도 된다. 더불어서 HTML5에서 둘 이상의 h1 태그를 사용하면 아래 사진과 같이 위계에 맞게 heading 태그를 렌더링한다. 반면에 W3C에서는 레거시 브라우저의 경우 이러한 렌더링에 어려움이 있을 수 있기 때문에 위계에 맞는 heading 태그 사용을 추천하고 있다. 종합해보면 둘 이상의 h1 태그 사용이 SEO에 문제가 없더라도 레거시 브라우저의 렌더링 결과를 고려하여 위계에 맞게 heading 태그를 사용하는 것이 좋다. 6. 짧게 작성한다 대략 70자 이하로 작성하는 것이 좋다. 앞서 h1 태그와 title 태그를 일치시켜야 한다고 언급했다. 만약 h1 태그를 너무 길게 작성한다면 title 태그가 다음 이미지와 같이 검색 결과에서 잘릴 수 있다. h1 태그를 길게 작성하고 title 태그를 짧게 작성하는 것을 고민하는 것보다 애초에 h1 태그를 짧게 작성하는 것이 좋다. 7. heading 태그 간 위계에 맞게 스타일을 적용한다 h1 태그는 페이지에서 가장 중요한 heading이다. 그러므로 페이지에서 가장 두드러져야 한다. 너무나도 당연해 보이지만, 여러 웹 사이트들이 h1과 h2 태그 구분을 해놓지 않았다. 8. 중요한 키워드를 포함한다 h1 태그는 페이지의 주제를 가리킨다. 그러므로 중요한 키워드를 포함하는 것이 좋다. 하지만 리스트를 나열해야 하는 상황, 예를 들면 유튜브에서 더 많은 시청 기록을 끌기 위한 방법들을 소개하는 페이지의 제목이라면 “14 Proven Ways to Get More Views on Youtube”와 같이 융통성있게 변형해도 좋다. 참고 문헌 What is an H1 Tag? SEO Best Practices What Is an H1 Tag? Why It Matters & Best Practices for SEO How Much Does Google Care About Accessibility? .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }웹뷰 개발에서 겪은 이슈들
나를 포함한 웹뷰 개발 경험이 전무한 조직 내에서 웹뷰 개발을 하면서 겪은 이슈들을 리스트업 해보았다. 미리 알았더라면 조금 더 개발 시간을 단축시키고 미리 대응할 수 있지 않았을까 싶어 공유 차원에서 작성한다. 📌 뒤로가기 이슈 뒤로가기 이슈는 크게 두가지로 나뉜다. 스크롤 (앞으로 가기 포함) 디바이스 백 버튼 처리 1. 스크롤 웹뷰 환경과는 무관한 이슈지만, 처리하는데 애를 먹어 적어놨다. 본 이슈는 다이나믹 라우팅이 적용된 상세 페이지에서 발생했던 이슈로, 스크롤이 원하는 위치로 복원되지 않는 이슈다. 가령, 다른 상세 페이지로 이동했을 때 스크롤 위치가 맨 위에 있어야 하지만 맨 위에 없거나, 뒤로가기 했을 때 스크롤 위치가 복원돼야 하지만 복원되지 않는 이슈다. 주된 이유는 다음과 같다. 다이나믹 라우팅 페이지는 컴포넌트가 Unmount 되지 않는다. 스크롤 높이가 고정돼있지 않다. 1로 인해서 다른 게시글로 이동했을 때 컴포넌트가 Unmount되지 않고 컴포넌트가 받는 Prop만 계속 바뀌니까, 스크롤이 맨 위에서 시작하는게 아니라 엉뚱한 위치에서 시작하도록 만든다. 2로 인해서 상세 페이지 여러 개가 히스토리 스택에 쌓인 상태에서 뒤로가기 했을 때 storage에 저장된 스크롤 위치로 스크롤이 이동해야 하는데, 엉뚱한 위치로 스크롤이 이동한다. 이는 스크롤이 고정 높이가 아닌 상태에서 이미지 등의 컨텐츠가 다 렌더링되지 않았는데 스크롤 위치를 복원하기 때문이다. 네이버 북스의 경우, 페이지 URL을 고유하게 만든 상태에서 페이지를 벗어날 때 페이지 URL을 키로하여 스크롤 위치와, 스크롤의 높이를 기억한 다음, 뒤로가기로 다시 해당 페이지를 방문했을 때, 스크롤 높이가 storage에 저장된 스크롤 높이와 동일해졌을 때 스크롤을 복원하도록 작성돼 있다. 만약 브라우저 창 사이즈를 다르게 한 다음 뒤로가기 하는 경우, 스크롤 위치가 엉뚱한 위치로 복원되는 문제가 존재했다. (이후에 네이버 북스를 다시 방문해보니 새로운 창을 띄우는 방향으로 기획이 변경된 것을 확인했다.) 2. 디바이스 백 버튼 처리 초기에 디바이스 백 버튼이 눌렸을 때의 처리를 모두 RN에서 하고 있었다. 이로 인해서 뒤로가기시 페이지의 이동이 아닌, 모달이나 창을 닫도록 유도하기 위해서는 RN에서 이와 관련한 상태를 모두 알고 있어야하고, 결과적으로 RN 코드가 잦게 변경되어 앱을 계속 재배포 해줘야 하는 문제가 있었다. 이를 해결하기 위해서는 디바이스 백 버튼의 처리를 RN에서 하지 말고, 백 버튼이 발생했음을 웹뷰에게 넘겨주고 이후의 모든 처리를 웹뷰에게 맡기는게 좋다. 📌 AOS / IOS에 따른 이슈 프로젝트는 AOS를 먼저 구현하고 IOS를 이후에 구현하는 순서로 진행됐다. AOS 앱을 먼저 배포하고, IOS 개발에 들어가려고 하니, AOS에서 없었던 이슈가 53개가량 발견되었다. 팀원 세명이서 분배했는데, CSS 관련한 이슈의 반은 크로스 브라우징 이슈였고, 나머지 반은 mui 컴포넌트 문제였다. 프로젝트 내에서 Bottom Sheet가 사용되는데, mui에서 제공하는 Drawer의 경우 손으로 드래그 했을 때 버벅거리면서 올라간다던지, 아니면 살짝 드래그했는데 화면 밖으로 시트가 벗어난다던지 등의 앱 사용성이 떨어지는 이슈 및 버그가 존재했다. 그래서 react-spring-bottom-sheet를 도입하게 됐는데, IOS에서 이 라이브러리와 mui 컴포넌트가 CSS 적으로 충돌하는 문제가 존재했다. 충돌한다라고 함은 react-spring-bottom-sheet가 보여지고있는 경우 mui 컴포넌트가 스크롤이 안되는 등의 문제이다. 외에도 앱에서 가상 키보드가 등장할 때 mui 컴포넌트가 키보드의 등장에 반응하지 않는 문제(키보드가 위를 덮어버리는)도 존재했다. 처음 mui 도입했던 이유가 사내에 디자이너가 존재하지 않았기 때문이어서 합리적이었는데, 깃허브 이슈탭에 오픈된 버그만 258개이고, 클로즈된 버그가 4300개인 점. 그리고 프로젝트가 다양한 환경을 지원해야 한다는 점을 생각한다면 돌아봤을 때 좋은 선택은 아니었던 것 같다. 📌 위치 권한과는 다른 GPS 서비스 권한 설명에 앞서, 권한과 관련한 라이브러리는 react-native-permissions를 이용했다. AOS를 구현할 때까지만 하더라도 위치 권한의 존재만 알고 있었는데, IOS를 개발하면 GPS 서비스가 별도로 존재하는 것을 알게되었다. 이 GPS 서비스가 복병인 것은 AOS와 IOS에서 반응이 다르게 나타나기 때문이다. AOS에서는 위치 권한을 허용하는 경우, GPS가 꺼져있으면 자동으로 GPS를 키라는 요청이 등장한다. 위치 권한 허용 -> GPS 허용 순으로 동작한다. IOS에서는 GPS가 꺼져있으면 위치 권한을 요청하지 못한다. GPS 서비스 허용 -> 위치 권한 허용 순으로 동작한다. 1의 경우, 위치 권한을 허용하면 위치 권한 상태는 granted로 떨어진다. 하지만 유저의 단말에서 GPS 서비스가 꺼진 상태기 때문에 유저의 위치를 가져올 수 없어 유저 입장에서는 혼란스러울 수 있다. GPS 서비스가 꺼져있음을 알고 그에 맞게 유저에게 대응하기 위해서는 react-native-device-info를 이용하여, GPS 서비스가 키고 꺼져있음을 알 수 있다. 2의 경우, react-native-permissions 자체가 위치 권한이 unavailable을 준다는 점에서 GPS가 켜져있고 꺼져있음을 알 수 있지만, AOS 지원을 위해 이미 react-native-device-info를 깔았으므로 해당 라이브러리를 이용하여 GPS 서비스가 켜져있는지 꺼져있는지 확인하도록 통일했다. 더불어서 GPS 서비스가 꺼져 있는 경우, 아이폰 8에서 GPS 서비스를 허용하라는 모달이 무한으로 떴다 사라지기를 반복하여 앱 진입이 불가능한 이슈도 있었다. IOS 버전을 14.4에서 16버전대로 올리니 해당 이슈는 사라졌다. 📌 버전 관련 이슈 사내에서는 A 빌드, A-1 빌드, A-2 빌드, B 빌드, C 빌드, C-1 빌드와 같이 개발을 진행하게 된다.(게임 개발 프로세스를 따라간 것인데… 일반적인지는 잘 모르겠다.) 각 알파벳을 최초로 빌드하는 경우는 앱을 새로 배포하는 경우고, 이외에 A-1, A-2, C-1 빌드는 웹뷰만 수정하기 때문에 앱을 새로 배포하지 않는 개발을 한다. 그렇기 때문에 A-1 빌드시 수정할 버그들을 리스트업 할때 RN이 영향을 받는지를 잘 생각해야 한다. 가볍게 생각하지 말아야 할게, RN 코드를 수정하고 그에 맞게 웹뷰 코드를 수정하여 라이브중인 브랜치에 반영했는데, 변경된 RN 코드가 반영되지 않아서 앞서 배포한 앱이 동작하지 않을 수도 있다. 이를 해결하기 위해서 RN 코드에서 버젼 정보를 웹뷰에게 전달한다음 분기처리하는 방법도 있지만, 유지보수에 좋지 않으므로 애초에 어떤 버그를 어떤 빌드에 수정할지, 혹은 어떤 기능을 어떤 빌드에 넣을지는 잘 고민하는게 좋다. 📌 모바일 브라우저 이슈 프로젝트 곳곳에 100vh 사용했다. 이 단위는 아래와 같이 사파리나 크롬의 URL 탭 혹은 네비게이션 탭을 만났을 때, 페이지의 일부 요소가 가려지는 이슈를 발생시킨다. 조금만 리서치를 해도 해결 방법이 나올 정도로 흔한 이슈지만, 개발시에 인지하고 있었다면 vh의 남발을 미리 막을 수 있지 않았을까 싶다. .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }TCP Control이란 무엇일까?
TCP Control TCP Control과 관련된 용어 및 기술들을 설명한다 CWND(Congestion Window) AIMD와 Slow Start Fast Trasnmit과 Fast Recovery TCP Control과 TCP Avoidance의 차이 TCP Tahoe TCP Reno 참고로 여기서 4와 5는 2, 3을 조합한 알고리즘이다. CWND(Congestion Window)와 RWND(Receiver Window) TCP에서 작은 크기의 데이터를 포함하는 많은 수의 패킷 전송은 비효율적입니다. 그러므로 패킷을 한번에 보내고 응답을 하나만 받습니다. 최대한 많은 패킷을 한번에 보내는 것이 효율적이지만, 패킷 유실 가능성이 존재하므로, 적절한 송신량 결정이 중요합니다. 송신자가 한번에 보낼 수 있는 패킷의 양을 CWND라 하고, 수신자가 받을 수 있는 패킷의 양을 RWND(Receiver Window)라고 합니다. CWND와 RWND는 TCP Header에 존재합니다. 수신자는 RWND에 자신이 받을 수 있는 패킷의 크기를 기입하고, 송신자는 이를 기반으로 CWND를 정합니다. 송신량은 CWND에 제한되며, 네트워크가 수용할 상황이 아니라면, RWND보다 CWND가 훨씬 작을 수 있습니다. AIMD(Addictive Increase Multiplicative Decrease)와 Slow Start 송신자는 패킷을 전송할 때, 네트워크의 상태를 모릅니다. 그러므로 갑작스러운 데이터 전송으로 인한 부하와 혼잡을 방지하기 위해, 다음과 같은 동작을 수행합니다. 송신자는 패킷을 천천히 전송하면서 네트워크를 파악합니다. 수신자는 RWND와 함께 응답합니다. 송신자는 패킷의 양을 늘려서 전송합니다. 수신자로부터 응답을 받지 못하거나 (= Packet Loss Detection || Timeout || Retransmission TimeOut한 경우) RWND에 도달할 때까지 3을 반복합니다. 몇 가지 용어에 대해서 참고삼아 먼저 정의하고 갑니다. Timeout과 Retransmission Timeout(RTO)은 동일한 뜻으로, 패킷을 보내고 일정 시간 안에 응답을 받지 못하는 것을 의미합니다. 또한 Timeout은 시간 초과를 의미하고, Time out은 휴식 시간을 의미한다는 차이점이 있습니다. 또한 Packet Loss와 Packet Drop은 다른 의미입니다. loss는 도착지에 도착하지 못한 것이고, drop은 라우터등이 의도적으로(패킷이 DOS 어택이라고 판단하는 등) 패킷을 버린 것입니다. drop은 loss의 일종입니다. 다시 돌아와서, 3 과정에서 패킷의 양을 늘려서 전송한다고 적혀있습니다. 패킷의 양을 어떻게 증가시키느냐에 따라서 AIMD와 Slow Start 두가지로 나뉩니다. 첫번째는 AIMD(Addicitve Increase Multiplicative Decrease) 방식입니다. CWND를 1씩 증가시키고, 패킷 유실시 Slow Start Threshold(ssthresh)를 1/2 감소시킵니다. 패킷 유실시 1/2 감소하는 대상은 CWND가 아닌 ssthresh임에 주의합니다. 두번째는 Slow Start 방식입니다. AIMD는 네트워크의 수용 능력에 최대한 가깝게 사용이 가능하지만, 데이터 전송량이 매우 느리게 증가한다는 단점이 있습니다. 그래서 Slow Start가 등장했습니다. Slow Start는 CWND를 2배씩 증가시킵니다. 초기 Connection시와 패킷 유실시 Slow Start를 사용할 수 있습니다. AIMD와 Slow Start 모두 패킷 유실시 CWND를 얼마로 낮출 것인가는 상황에 따라서 달라집니다. Fast Transmit과 Fast Recovery 패킷이 중간에 유실(loss)되어 순서대로 도착하지 못하는 경우 어떻게 될까요? 만약 송신자가 패킷 1, 2, 3, 4, 5, 6 순서대로 보내는데, 3번 패킷이 유실된 경우, 수신자는 ACK 1, 2, , 2, 2, 2를 보내게 됩니다. 즉, 수신자는 마지막으로 받은 패킷을 가리키는 ACK를 계속해서 보냅니다. 여기서 두 번째로 보내는 ACK 2, 즉, ‘중복된’ ACK를 Duplicated ACK라고 합니다. 송신자는 이 Duplicated ACK를 통해서, 수신자가 순서대로 데이터를 못받고 있다는 것, 즉, 앞의 패킷이 유실되었음을 알게됩니다. 혹시나 패킷 유실이 아닌, 지연으로 인해서 늦게 도착할수도 있기 때문에, 송신자는 Retransmit 하기전에 Duplicated ACK를 3번 기다립니다. 아래 사진과 같이 말이죠. 이미지 출처 위 사진에서 놓치지 말아야할 것은, 3번 패킷을 재전송한 후에, 6번 패킷까지 잘 받았다는 ACK 6을 전달한다는 것 입니다. Fast Reransmit이 어떤 효과가 있는지 그래프를 통해서 한번 알아보겠습니다. 아래 나오는 그래프는 가볍게 보시길 바랍니다. 이미지 출처 그래프 해석에 있어서 필요한 정보만 나열하면 다음과 같습니다 상위 그래프는 Fast Retransmit이 적용되지 않음, 하위 그래프는 Fast Retransmit이 적용됨 상위의 Dot은 Timeout이 발생한 시점 상위의 Hash 마크는 패킷 전송을 의미 파란색 라인은 CWND를 의미합니다. 수직선은 패킷 유실이 발생한 시점 하위 그래프를 보면, 상위 그래프에 비해, Timeout 대기 시간이 짧고, Timeout 대기 시간에도 패킷을 전송하는 것을 확인할 수 있습니다. 우리는 여기서 패킷 유실과 Timeout의 관계를 짚고 넘어가야 합니다. 상위 그래프의 적색 원을 보면, 패킷 유실이 발생하면 일정 시간 후 Timeout이 발생합니다. 아래 그래프의 적색 원을 보면, 패킷 유실이 발생하면 Timeout 없이 Retransmisstion이 발생합니다. Timeout이 발생하면 패킷 유실이 발생한게 맞지만, 패킷 유실이 발생하면 Timeout이 발생하는 것은 아닙니다. Fast Retrasnmit에서 설명했듯이, 패킷 유실이 일어났음에도 수신자로부터 응답을 받을 수 있었습니다. 그리고 이 차이를 통해서 Timeout이 발생하는 경우가 Fast Retransmit이 발생하는 경우보다 네트워크 상황이 안좋다는 것을 알 수 있습니다. 여기까지가 Fast Retransmit에 관한 이야기고, Fast Retransmit의 성능을 조금더 향상시키기 위해서 Fast Recovery가 등장합니다. 이는 Slow Start Phase를 건너 뛰는 것 입니다. 아래 Congestion Control 알고리즘 중 하나인 TCP Reno가 좋은 예시입니다. 이미지 출처 즉, CWND 1부터 Slow Start를 적용하는게 아니라, Fast Retransmit으로 패킷 유실을 감지하면 CWND의 절반부터 Addictive Increase를 하는 것 입니다. 결국 Slow Start는 Connection의 시작 단계와 Timeout이 발생했을 때만 사용하게 되는것이죠. TCP Control과 TCP Avoidance의 차이 Congestion Control과 Congestion Avoidance 용어를 동일하게 사용해도 되는지, 그리고 Congestion Control에 대해 이야기하면서 등장하는 Flow Control은 무엇인지 알아봅시다. 먼저 Congestion Control과 Congestion Avoidance가 동일한가? 결론부터 말하자면 둘은 전혀 다릅니다. 위키피디아에서는 ‘TCP 혼잡 회피 알고리즘은 혼잡 제어 알고리즘의 기반이다’라고만 설명할 뿐, 둘을 구분해서 쓰고 있지 않습니다. 이뿐만 아니라 여러 곳에서 그렇습니다. 동일하게 취급해도 되나보네 싶지만, 아래와 같이 종종 등장하는 혼잡 제어 알고리즘을 보면, 동일하게 취급하면 안될것 같습니다. 🙄 이미지 출처 이미지 출처 인터넷에 Congestion Control과 Congestion Avoidance로 검색하면 흔히 나오는 그래프 두장입니다. 위 그래프는 Congestion Control과 Congestion Avoidance를 명백하게 구분지어 놓았고, 아래 그래프는 Slow Start와 Congestion Avoidnace로 구분지어 놓았습니다. 보통 아래와 같이 Phase 명을 Slow Start와 Congestion Avoidance로 명명하고, Congestion Avoidance Phase에 Linear하게 증가하는 그래프가 더 많이 보입니다. 혼란이 가중되던 중, Congestion Control과 Congestion Avoidance 의미를 다르게 규정하는 논문을 발견했습니다. (“CONGESTION AVOIDANCE IN COMPUTER NETWORKS WITH A CONNECTIONLESS NETWORK LAYER PART I: CONCEPTS, GOALS AND METHODOLOGY”, Ra j Jain, K. K. Ramakrishnan Digital Equipment Corporation) 논문에서 설명하는 내용은 아래와 같습니다. 먼저 위 그래프를 이해해야 합니다. Load(네트워크 부하)가 낮을 때는 Throughput이 Linear하게 증가합니다. 그러다가 Load가 네트워크 Capacity보다 커지는 경우, Throughput이 0이 됩니다. 이때를 혼잡 붕괴(Congestion collapse)라고 합니다. 그래프의 수직선을 보겠습니다. Throughput이 급격하게 떨어지기 시작하는 지점을 Cliff라고하고, Throughput이 천천히 증가하기 시작하는 지점을 Knee라고 합니다. Knee 근방에서 트래픽을 사용하는 전략을 Congestion Avoidance라고 하고, Cliff를 넘지않게 트래픽을 사용하는 전략을 Congestion Control이라고 합니다. 다음과 같이 Response Time과 Load의 관계로도 볼 수 있습니다. Congestion Avoidance는 유저가 Response Time에 심각하게 영향을 주지 않을만큼 트래픽을 사용하는 전략입니다. 우연히 영향을 받게 되는 경우, Congestion Control을 통해서 Cliff의 왼쪽구간에서 동작하게 만들어 줍니다. Congestion Control은 ‘회복하는 과정’과 같고, Congestion Avoidance는 ‘예방하는 과정’과 같다고합니다. 다시 종합해보면, Congestion Avoidance는 Congestion Collapse가 발생하지 않는 선에서 트래픽을 최대한 사용하는 알고리즘이고, Congestion Control은 Congestion Collapse가 발생하는 상황을 막는 알고리즘입니다. 아래 추후에 언급하는 TCP Reno를 통해서 대입해보면, 어느정도 들어맞는 것 같습니다. TCP Tahoe 참고로 Tahoe는 USA의 한 호수입니다. 이 기술이 이 호수 근처에서 발명되어 TCP Tahoe라는 이름이 붙게되었습니다. 이후에 등장하는 Reno는 도시 이름입니다. TCP Tahoe는 아래와 같이 구성됩니다 TCP Tahoe = Slow Start + AIMD + Fast Transmit 동작은 아래와 같습니다. 이미지 출처 Slow Start Phase Slow Start Threshold(ssthresh)에 도달하기 전까지 slow start 알고리즘이 적용됩니다. 그래프에는 안나와있지만 초기 ssthresh는 infinite입니다. 이후, 패킷 유실에 따라서 ssthresh 값을 조정합니다. ssthresh에 도달하면, AIMD 알고리즘이 적용됩니다. AIMD Phase AIMD에서 이야기했던 대로, CWND를 1씩 증가시키다가, Timeout이 발생하면 CWND를 1로 초기화시키고, ssthresh를 50% 감소시킵니다. CWND가 50% 감소하는게 아니라, ssthresh가 50%감소하는 것에 다시 주의합니다. 패킷 유실은 RTO나 Fast Retrasnmit에 의해 감지되며, 둘 모두 CWND를 1로 만들고, ssthresh를 이전 CWND의 1/2로 만듭니다. TCP Reno TCP Tahoe에 Fast Recovery가 추가된 전략입니다. TCP Reno = TCP Tahoe + Fast Recovery 이미지출처 패킷 유실을 어떻게 발견했느냐에 따라서 동작이 달라집니다. 패킷 유실을 Fast Transmit을 통해서 발견하게 되는 경우, Fast Recovery를 통해서 CWND와 ssthresh를 이전 CWND의 1/2 수준으로 줄입니다. 패킷 유실을 RTO를 통해서 발견하게 되는 경우, CWND를 1로, ssthresh를 이전 CWND의 1/2수준으로 줄입니다. 이미지 출처 📚 참고 문헌 packet drop vs packet loss TCP congestion control from wikipedia TCP Congestion Control from systemapproach End-to-end principle What Is TCP Slow Start WHAT IS CWND AND RWND? TCP Tahoe and TCP Reno TCP RTOs: Retransmission Timeouts & Application Performance Degradation “IT 엔지니어를 위한 네트워크 입문” 고재성, 이상훈 지음 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }메타프로그래밍이란?
메타프로그래밍이란? Proxy와 Reflect에 대한 공부를 하던 도중 메타 프로그래밍이라는 단어가 등장하는데 이해가 안돼서 정리했다. 메타프로그래밍 정의에 앞서서, 메타프로그래밍은 언어 특성이 아니며 별다른 스탠다드도 존재하지 않기 때문에 사용하는 언어와 사람에 따라서 다르게 해석될 수 있음을 전제한다. (여러 문서를 읽어본바 정의가 조금씩은 다르나 큰 틀은 벗어나지 않는 것 같다. 🧐) 그러므로 정의에 관해서 애써 기억할 필요없고, 다만 컨셉에 대해서는 이해해 놓을 필요가 있다. 위키피디아에서는 메타프로그래밍을 다음과 같이 정의한다. 메타프로그래밍은 프로그래밍 기술로, 다른 프로그램을 데이터로 취급하여 분석, 생성, 변형등의 조작을 하는 어떤 프로그램을 작성하는 것 대부분의 문서가 이 정의로 시작을 하는데, 이 정의만을 놓고보면 다음 코드가 왜 메타프로그래밍인지 이해가 잘안간다. function coerce(value) { if (typeof value === 'string') { return parseInt(value); } else if (typeof value === 'boolean') { return value === true ? 1 : 0; } else if (value instanceof Employee) { return value.salary; } else { return value; } } console.log(1 + coerce(true)); // 2 console.log(1 + coerce(3)); // 4 console.log(1 + coerce('20 items')); // 21 console.log(1 + coerce(new Employee('Ross', 100))); // 101 위 코드에서 어떤 프로그램은 무엇이고 다른 프로그램은 무엇일까? 이해를 돕기 위해서 메타프로그래밍을 다시 정의하면 다음과 같이 정의할 수 있다. 메타프로그래밍은 프로그래밍 기술로, 다른 코드를 데이터로 취급하여 분석, 생성, 변형등의 조작을 하는 코드를 작성하는 것을 의미한다. 이는 런타임에 기존 코드가 동작에 맞게 자기 자신을 변형하는 것을 포함한다. 이를 위해서 자바스크립트에서는 Proxy나 Reflect를 이용할 수 있다. 이러한 정의를 바탕으로 앞선 코드를 이해해보면 “corece라는 함수가 들어오는 코드(여기서는 value 인자로, 런타임에는 유저가 입력한 무언가가 될수 있을 것 같다.)에 따라서 typeof를 통해서 분석한 뒤 알맞은 동작을 수행하고, 기존 코드(log되는 결과)가 변형될 수 있겠구나”정도로 이해할 수 있을 것 같다. 참고로 이 문서를 다른 분께 공유드리면서 “메타”의 정의에 대해서 말씀해 주셨는데, 쉽게 말해서 “A에 대한 A”라고 이해하면 된다. 실제로 메타 데이터 용어를 위키피디아에서는 다음과 같이 정의하고 있다. 메타데이터(metadata)는 데이터(data)에 대한 데이터이다. 다시 메타프로그래밍으로 돌아와서, 메타프로그래밍은 크게 다음 두가지 능력을 갖고 있다. 프로그램 코드를 생성하는 능력(Code Generation) 프로그램이 자기 자신을 조작하거나 다른 프로그램을 조작할 수 있는 능력(Reflection 혹은 Reflective Programming) 그리고 Reflection은 다시 다음 세가지로 분류할 수 있다. introspection(분석) intercession(중재) self-modification(자기 수정) 각각에 대해서 알아보자. 우선 코드를 생성하는 코드로는 eval을 예로들수 있다. string으로 작성된 자바스크립트 코드는 런타임에 실제 자바스크립트 코드가 생성되어 실행된다. eval(` function sayHello() { console.log("Hello World"); } `); // sayHello라는 함수가 이미 정의돼 있는 것 처럼 호출이 가능하다. sayHello(); 그리고 분석(introspection)과 관련한 코드로는 ES6 이전에는 typeof, instanceof, Object.* 등을 이용할 수 있고, ES6 이후부터는 introspection을 위한 Reflect API가 도입되었다. 다음 코드에서 instanceof는 특정 함수의 인스턴스인지 확인함으로써 introspection을 수행하고있다. function Pet(name) { this.name = name; } const pet = new Pet('Bubbles'); console.log(pet instanceof Pet); console.log(pet instanceof Object); 조정(intercession)은 기본 동작을 재정의하는 것이다. 원본(target)을 수정하지 말아야 한다는 전제가 존재한다. ES6부터 Proxy를 이용해서 가능하며, ES5에서는 getter와 setter를 이용해서 비슷하게 구현 가능하지만, 원본이 수정된다는 점에서 intercession으로 보기 어렵다. var target = { name: 'Ross', salary: 200 }; var targetWithProxy = new Proxy(target, { get: function (target, prop) { return prop === 'salary' ? target[prop] + 100 : null; }, }); console.log('proxy:', targetWithProxy.salary); // proxy: 300 console.log('target:', target.salary); // target: 200 Proxy는 두번째 인자에 정의된 핸들러 객체를 전달할 수 있다. 핸들러 객체 내부에는 동작을 가로채는 get과 set과 같은 trap이 정의될 수 있다. targetWithProxy.salary에 접근할 때, trap 함수인 get 함수가 기존 프로퍼티에 + 100을 더하여 읽기 동작이 수행되도록 읽기 동작을 재정의하고 있다. self-modification은 프로그램이 자기 자신을 수정할 수 있는 것이다. intercession과는 다르게 원본이 변경된다. var blog = { name: 'freeCodeCamp', modifySelf: function (key, value) { blog[key] = value; }, }; blog.modifySelf('author', 'Tapas'); 여기까지 메타프로그래밍에 대해서 알아보았다. 다시 한 번 언급하지만 메타프로그래밍은 “프로그래밍 언어 특징”이나 “표준화된 것”으로 묘사될 수 없고, “수용력(Capacity)“에 가깝다. Go와 같은 몇몇 프로그래밍 언어는 메타프로그래밍을 완전히 지원하지 않고 일부만 지원한다. 📚 참고문헌 A brief introduction to Metaprogramming in JavaScript Metaprograaming with Proxies Comprehensive Guide To Metaprogramming in Javascript Exploring Metaprogramming, Proxying And Reflection In JavaScript Reflect API는 왜 도입됐을까? ES6에 도입된 Reflect는 introspection을 위한 메서드들을 제공한다. 하지만 이는 ES5에 이미 Object와 Function 객체에 존재했던 메서드들이다. 이미 메서드들이 존재하는데 Reflect API를 도입한 이유가 뭘까? 그 이유는 다음과 같다. 1. All in one namespace ES6 이전에는 Reflection과 관련한 기능들이 하나의 네임스페이스 안에 존재하지 않았다. ES6부터는 Reflection과 관련한 기능들이 Reflect API 내에 존재하게된다. 또한 Object 처럼 생성자로 호출이 불가능하고, 함수로의 호출이 불가능(non-callable)하며, 메서드들은 모두 정적 메서드들이다. 우리는 연산을 위해서 흔히 Math 객체를 사용하는데, Math 객체 역시 생성자로 호출이 불가능하고, 함수로의 호출이 불가능하며, 메서드들이 모두 정적 메서드들이다. 2. Simple to use 사용하기가 쉽다. Object 객체에 존재하는 introspection과 관련한 메서드들은 동작이 실패하는 경우 예외를 발생시킨다. 개발자 입장에서는 예외를 처리하기보다는 Boolean 결과를 처리하는게 편하다. 예를들면 Object에 존재하는 defineProperty는 다음과 같이 사용해야 한다. try { Object.defineProperty(obj, name, desc); } catch(e) { // handle the exceptionl } 하지만 Reflect API를 사용하는 경우, 다음과 같이 사용이 가능해진다. if(Reflect.defineProperty(obj, name, desc)) { // success } else { // failure } 3. 신뢰성 있는 apply() 메서드의 사용 ES5에서 함수를 this value와 함께 호출하기 위해서 보통 다음과 같이 사용했다. Function.prototype.apply.call(func, obj, arr); // or func.apply(obj, arr); 하지만 이러한 접근은 func 함수 내에 apply라는 메서드가 존재하는 경우가 있을 수 있기 때문에 신뢰성이 떨어진다. Reflect는 apply 메서드를 제공함으로써 이러한 문제를 해결한다. Reflect.apply(func, obj, arr); 📚 참고문헌 What is Metaprogramming in JavaScript? In English, please .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }slug란 무엇이고 SEO에 어떻게 영향을 줄까?
목차는 다음과 같다. slug란 무엇일까? slug가 seo 랭킹에 왜 중요할까? seo에 좋은 slug를 작성하는 방법은 무엇일까? 1. slug란 무엇일까? slug란 url에서 마지막 backslash(’/’) 뒤에 오는 문자열로, 이 문자열을 통해서 현재 보여지는 페이지가 어떤 페이지인지 식별할 수 있고, 유저와 검색 엔진에게 현재 보여지는 페이지가 어떤 정보를 담고있는지를 알려준다. 2. slug는 SEO 랭킹에 왜 중요할까? 한번쯤 랜덤한 문자와 숫자로 구성된, 읽을 수 없는 URL을 본적이 있지 않은가? 이러한 URL들은 신뢰도가 떨어지고, 혼란을 야기함으로써 유저들은 링크를 공유하지 않게된다. URL이 깔끔하고 깨끗하다면 적어도 링크 공유에 의지가 있는 유저들을 떠나보내진 않았을 것이다. 이는 비단 유저뿐만이 아니라 검색 엔진도 마찬가지다. 검색 엔진 알고리즘 역시도 깔끔하고, 깨끗하고, 데이터를 이해하기에 최적화된 컨텐츠가 필요하다. 잘 작성된 slug는 사이트를 방문하는 유저 뿐만 아니라 검색 엔진이 페이지를 이해하는데 도움을 준다. 그렇다면 seo에 최적화된 slug를 작성하는 법은 무엇일까? 이후에 언급될 방법들을 보면 알겠지만, 대부분이 slug의 가독성을 향상시키는 방법들이다. 3. seo에 좋은 slug를 작성하는 방법은 무엇일까? (1) 최대한 짧게 만들기 이 이야기는 slug를 포함한 URL관점에서 해석해야 한다. 그러니까 URL이 짧으면 짧을수록 좋다. 이는 SEO 뿐만이 아니라 다음과 같은 장점도 누릴 수 있다. 유저가 기억하기 쉬워진다. SERP에서 짤리지 않게된다. 첫번째는 자명하고, 두번째는 SERP가 무엇인지에 대한 이해가 필요하다. SERP는 Search Engine Results Pages의 줄임말로, 구글과 같은 검색 엔진을 통해서 유저에게 보여지는 페이지들을 의미한다. SERP는 지리적 위치, 검색 기록등을 포함한 다양한 인자를 고려하여 보여지기 때문에 유니크하다. 그리고 SERP가 짤리게 된다면 아래 이미지와 같은 결과가 나오게된다. 이미지출처 slug를 짧게 만들기 위한 두가지 팁은 다음과 같다 중요 키워드만 남긴다. 기존 slug가 SEO-Experiments-That-Changes-SEO-Forever이라면, SEO-Experiments 정도로 키워드만 남길 수 있게된다. function words(‘a’, ‘of’, ‘the’)나 verb(‘are’, ‘have’ 등)를 제외한다. 오해하지 말아야 할것은, slug를 가능한 선에서 짧게 만들자는 것이지, 내부에 어떤 컨텐츠가 있는지도 알수 없을 정도로 짧게 만들자는 것은 아니다. (2) 안전한 문자, 예약되지 않은 문자 사용하기 안전하지 않은 문자나 예약된 문자를 사용하는 경우, 가독성이 떨어질 뿐만아니라 웹 크롤러의 접근을 막을수도 있다. 이미지출처 (3) hyphen 사용하기 만약 키워드를 구분하기 위해서 공백을 넣는다면, 브라우저는 이를 ‘%20’으로 변형하게하여 어색한 URL을 만든다. 이를 방지하기 위해서 우리는 다음 두가지 선택지를 가질 수 있다. hyphen (-) underscore (_) 이 중에서 hyphen을 사용하자. 구글 검색 엔진은 hyphen 사용을 권하고 있다. 또한 컴퓨터나 웹크롤러는 hyphen을 공백으로 인식하여 which-creates-something-that-looks-like-this를 whichcreatessomethingthatlookslikethis로 해석하지만, underscore는 이렇게 해석되는게 불가능하다. hyphen이 언급된 김에 한가지 더 짚고 넘어가자면, 도메인 이름에 hyphen을 넣지 않는 것이 좋다. 이유는 유저가 기억하기 어렵고, 구두로 말할때도 헷갈릴 수 있기 때문이다. 도메인 이름에 hyphen이 들어가면 SEO에 불리하다는 이야기도 있지만 이는 본 아티클에서는 미신이라고 언급하고 있다. 다만 hyphen을 통해서 URL이 길어지기 때문에 SEO에 불리할 수는 있다. (4) 키워드만 포함하고, 키워드 수식어 붙여주기 키워드만 포함하는 이유는 URL을 짧게 만들자는 취지 뿐만 아니라, 컨텐츠를 업데이트하기 쉽게 만들려는 의도도 존재한다. 만약 기존 slug가 컨텐츠에 대한 너무 많은 정보를 포함한다면, 해당 컨텐츠가 조금만 수정돼도 slug가 영향을 받게 될것이다. 또한 키워드 수식어를 붙여주는 것 역시도 좋다. 키워드 수식어는 slug에 추가적인 정보를 제공하는 단어로, “best”, “guide”, “checklist”, “review”등이 있는데 이는 SEO에 도움을 줄수 있다. 다만, 여기서도 컨텐츠의 정보를 변경하기 힘들게 만드는 “guide”, “checklist”등 보다는 “best”, “boost”등의 키워드를 사용하는 것이 좋다. (5) 제목과 일치시키기 제목과 일치시킴으로써 페이지가 어떤 컨텐츠를 포함하고 있는지를 드러내는 것이다. 물론 slug와 제목이 무조건 일치할 필요는 없다. 가령 ‘Everything You Need to Know About Content Marketing’의 slug는 ‘everything-about-content-marketing’정도가 될수있다. (6) 날짜를 포함한 숫자 삭제하기 slug에 숫자가 들어가는 경우, 항상 제목과 slug를 일치시켜야 한다는 번거로움이 존재한다. 기존 slug가 다음과 같이 작성돼있다고 가정해보자. 23-SEO-Expriments 만약 여기서 컨텐츠 내용이 수정되어 21개의 SEO Experiments가 된다면 slug 역시도 수정해주어야한다. 만약에 이를 까먹게 된다면 아래 이미지와 같이 SERP에도 잘못된 정보가 표시될 것이다. 이미지출처 만약 날짜가 들어간다면, 유저 입장에서 컨텐츠가 옛날 컨텐츠처럼 보일수도 있다. 이 글을 읽고 계시는 독자분들은 ‘valuable-seo-lessons-2012’를 봤을 때 어떤 느낌이 드는지 궁금하다. 나는 만약 이 slug를 보게된다면 굳이 이 게시글에 들어가려고 하지 않을 것 같다. (7) 소문자 사용하기 대부분의 모던 웹 서버는 URL에 대해서 case-insenstive, 즉, 대소문자를 구분하지 않는다. 하지만 모든 웹 서버가 그런것은 아니기 때문에, 조심하자는 차원에서 “do or die”전략으로 소문자만 쓰자는 것이다. 만약 대문자와 소문자를 섞어서 사용한다면 404 페이지 에러나 페이지 중복 문제가 발생할 수 있다. 📚 참고문헌 What is a URL Slug & How to Use Them Successfully in Your SEO Strategy? Best practices for SEO-friendly URLs URL Slugs: How to Create SEO-Friendly URLs (10 Easy Steps) Dash or Underscore in URL? Here’s How It’s Affecting Your SEO .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }storage 안전하게 사용하기
본인이 프로그래머스 데브코스를 수강할 때 바닐라 자바스크립트 멘토님께서 localStorage를 다음과 같이 사용하셨다. const storage = window.localStorage; const getItem = (key, fallbackValue) => { try { const res = storage.getItem(key); return res ? JSON.parse(res) : fallbackValue; } catch (e) { console.error(e.message); return fallbackValue; } }; export default { getItem, setItem, }; 위 코드의 getItem 내부에서 왜 try ~ catch 문을 써야하는지다. 당시에 워낙 수업 따라가기도 힘들었어서 제대로 짚고 넘어가지 못한 부분이었는데, 하필 강사님이 계신 회사에 면접을 보러갔을 때 강사님께서 왜 저렇게 작성한건지 알고 있냐고 물어보셨었다. 그 당시에 강사님께서 이유를 알려주셨었는데, 이번에 데브매칭 시험을 공부하면서 이유를 까먹어 다시 찾아보았다. 우선 강사님이 알려주신 이유와 별개로 한가지 이유가 더 존재하는데 다음과 같다. JSON.parse시 발생할 수 있는 에러 localStorage가 지원되지 않는 환경에서 발생하는 에러 1은 다음과 같다. storage에 저장할 때는 JSON.stringify를 이용해서 저장하는데 어떤 경유로 { a : 1 이라는 객체가 저장됐다고 가정해보자. 그리고 이 객체를 다시 가져와서 JSON.parse 하려고 할때 다음과 같은 에러가 발생하게 된다. 에러가 의미하는대로, 두번째 프로퍼티(at position2)가 와야하는데 생략이 되었거나, 두번째 프로퍼티가 오지는 않는데 }를 기입하지 않아서 발생하는 에러다. 만약 아래와 같이 try ~ catch를 사용하지 않고 코드를 작성하면 프로그램이 멈출수도 있기 때문에 catch 문에서 에러를 포착하여 fallbackValue를 내놓는 것이다. const res = JSON.parse(localStorage.getItem(2)); // 에러 발생 console.log(res); // 실행되지 않음 2는 다음과 같다. 우선 Can i use?를 통해 localStorage와 sessionStorage가 어떤 브라우저에서 지원되지 않는지 찾아봤다. localStorage는 다음과 같다. ; sessionStorage는 다음과 같다. ; 그렇다면 지원이 안되는 상황에서 코드가 작동하도록 하기 위해서는 어떻게 작성해야 할까? How to Use LocalStorage Safely에서는 다음과 같이 사용하는 것을 제시하고 있다. function isSupportLS() { try { localStorage.setItem('_ranger-test-key', 'hi'); localStorage.getItem('_ranger-test-key'); localStorage.removeItem('_ranger-test-key'); return true; } catch (e) { return false; } } class Memory { constructor() { this.cache = {}; } setItem(cacheKey, data) { this.cache[cacheKey] = data; } getItem(cacheKey) { return this.cache[cacheKey]; } removeItem(cacheKey) { this.cache[cacheKey] = undefined; } } export const storage = isSupportLS() ? window.localStorage : new Memory(); 그러니까 isSupportLS 함수를 실행하여 에러가 발생하면 브라우저 저장소가 아닌 웹사이트 내부의 Memory에 저장하는 방법을 제시하고 있다. 하지만 Javascript Try Catch for Localstorage Detection에서 isSupportLS 보다 더 간단하게, 그리고 Edge case까지 고려하여 작성하는 방법을 제시하고 있다. function supports_html5_storage() { try { return 'localStorage' in window && window['localStorage'] !== null; } catch (e) { return false; } } 해당 게시글을 들어가면 알겠지만, 위 함수에서 try ~ catch 문을 사용하는 이유는 오래된 Firefox는 쿠키 사용을 꺼놨을 때 예외가 발생할 수 있는 버그가 있다고 한다. 그러므로 코드를 다음과 같이 완성할 수 있게된다. function supports_html5_storage() { try { return 'localStorage' in window && window['localStorage'] !== null; } catch (e) { return false; } } class Memory { constructor() { this.cache = {}; } setItem(cacheKey, data) { this.cache[cacheKey] = data; } getItem(cacheKey) { return this.cache[cacheKey]; } removeItem(cacheKey) { this.cache[cacheKey] = undefined; } } export const storage = supports_html5_storage() ? window.localStorage : new Memory(); .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk8 { color: #CE9178; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }equality 방법 비교하기
본인은 지금까지 어떤 두 객체를 비교하기 위해서 JSON.stringify를 써왔다. 그러다가 lodash라는 라이브러리에서 isEqual을 제공한다는 것을 알고 있었는데, 문득 JSON.stringify 해주면 끝나는 것을 왜 별도 라이브러리를 설치하면서까지 객체를 비교해야하나 싶어서 리서치하여 정리했다. JSON.stringify의 문제점 JSON.stringify가 갖는 문제점은 다음 코드들처럼 예상과 다른 로깅 결과로부터 알수있다. console.log(JSON.stringify({ a:1, b:2 }) === JSON.stringify({ b:2, a:1 })); // false console.log(JSON.stringify(NaN) === JSON.stringify((null))) // true 더불어서 다음 코드와 같이, 순환 참조하는 객체에 대해서 콘솔을 찍어보면 “Uncaught ReferenceError: Cannot access ‘a’ before initialization” 에러가 발생한다. const a = { b: a, } console.log(JSON.stringify(a)); // Uncaught ReferenceError: Cannot access 'a' before initialization === Equality, Shallow Eqaulity, Deep Equality 비교 두 객체의 동일함을 판단하기위한 방법으로 JSON.stringify를 이용한 방법, Shallow Equality를 이용한 방법, Deep Eqaulity를 이용한 방법이 존재한다. JSON.stringify는 객체를 JSON으로 만들어서 비교하는 방법이다. 그렇다면 Shallow Equality와 Deep Equality는 무엇일까? 다음 예시들을 통해서 알아보자. const user1 = { name: "John", address: { line1: "55 Green Park Road", line2: "Purple Valley" } }; const user2 = user1; console.log(user1 === user2); // true console.log(shallowEqual(user1, user2)); // true console.log(deepEqual(user1, user2)); // true 위 코드에서 ===는 reference equality를 기반으로 동작한다. user1과 user2는 동일한 주소를 가리키기 때문에 true를 출력한다. 만약 다음과 같이 user2에 객체를 만들어서 할당한다면 false가 출력되게된다. const user2 = { name: "John", address: user1.address, } console.log(user1 === user2); // false console.log(shallowEqual(user1, user2)); // true console.log(deepEqual(user1, user2)); // true 그럼에도 불구하고 shallowEqual은 여전히 true를 출력해낸다. shallowEqual은 프로퍼티 하나하나에 대해서 ===을 적용하기 때문에, user2의 address reference가 동일함으로 true를 출력하는 것이다. 만약 다음과 같이 user2의 address 프로퍼티에 새로운 객체를 만들어서 값을 할당한다면, 결과는 false가 나오게 될것이다. const user2 = { name: "John", address: { line1: "55 Green Park Road", line2: "Purple Valley" } } console.log(user1 === user2); // false console.log(shallowEqual(user1, user2)); // false console.log(deepEqual(user1, user2)); // true 지금까지 user2를 다양하게 수정했음에도 불구하고 deepEqual은 계속해서 true를 출력하고 있다. 이유는 deep Equal이 프로퍼티 내부를 전부 비교하기 때문이다. Shallow Equality와 Deep Equality는 어떻게 만들 수 있을까? 자바스크립트에서는 비교를 위해서 ==, ===연산자와 Object.is 메서드를 이용해서 비교를 한다. 그렇다면 두 변수를 깊게(deeply) 비교하기 위해서는 어떻게 해야할까? == 연산자는 값을 비교하기에 앞서 동일한 타입을 갖도록 변환한 후 비교를 진행하는, 굉장히 느슨한 비교(loose eqaulity operator)연산자다. ===연산자는 ==연산자와는 다르게 타입 변환 과정 없이 비교를 진행하는 엄격한 비교(strict equaility operator)연산자다. 하지만 ===연산자도 다음과 같은 허점이 존재한다. console.log(+0 === -0); // true console.log(NaN === NaN); // false Object.is는 대부분의 경우 ===연산자와 동일하게 동작하지만, 앞선 두 케이스와는 반대되는, 올바른 결과를 내놓는다. console.log(Object.is(+0, -0)); // false console.log(Object.is(NaN, NaN)); // true 다만 이것이 Object.is가 ===보다 더 엄격하게 비교한다는 것은 아니다. 상황에 따라서 둘중 하나를 선택하면 된다. Deep Eqaul은 여러가지 edge case를 고려해야 하기 때문에 성능이 느릴 수 있다. 그래서 React에서는 상태 변화 비교를 위해 Shallow Equal을 이용한다. Shallow Equal은 다음과 같이 구현될 수 있다. import is from './objectIs'; import hasOwnProperty from './hasOwnProperty'; function shallowEqual(objA: mixed, objB: mixed): boolean { // P1 if (is(objA, objB)) { return true; } // P2 if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); // P3 if (keysA.length !== keysB.length) { return false; } // P4 for (let i = 0; i < keysA.length; i++) { const currentKey = keysA[i]; if ( !hasOwnProperty.call(objB, currentKey) || !is(objA[currentKey], objB[currentKey]) ) { return false; } } return true; }; P1은 === 연산으로 비교하여 동일하면 true를 return한다 P2는 이후 로직을 실행시키기 위해 객체가 아닌 경우를 return한다 P3은 키들의 개수가 다른 경우 false를 return한다 P4는 본격적으로 프로퍼티를 하나하나 비교하며 값이 객체인 경우 재귀적으로 비교한다. 그리고 Deep Equal은 다음과 같이 구현된다. const deepEqual = (objA, objB, map = new WeakMap()) => { // P1 if (Object.is(objA, objB)) return true; // P2 if (objA instanceof Date && objB instanceof Date) { return objA.getTime() === objB.getTime(); } if (objA instanceof RegExp && objB instanceof RegExp) { return objA.toString() === objB.toString(); } // P3 if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } // P4 if (map.get(objA) === objB) return true; map.set(objA, objB); // P5 const keysA = Reflect.ownKeys(objA); const keysB = Reflect.ownKeys(objB); // P6 if (keysA.length !== keysB.length) { return false; } // P7 for (let i = 0; i < keysA.length; i++) { if ( !Reflect.has(objB, keysA[i]) || !deepEqual(objA[keysA[i]], objB[keysA[i]], map) ) { return false; } } return true; }; P1, P3, P6, P7은 Shallow Eqaul과 동일하다. P2에서 Date와 RegExp의 경우의 비교를 진행한다. P4에서 순환 참조인 경우 true를 반환한다. P5에서는 Shallow Equal과는 다르게 Object.keys로 키를 얻는게 아니라, Reflect.ownKeys로 키들을 얻는다. Reflect와 WeakMap에 대해서는 설명을 건너뛰겠다. 궁금하다면 Reflect와 WeakMap을 참고하자 📚 참고문헌 JavaScript deep object comparison - JSON.stringify vs deepEqual Is it fine to use JSON.stringify for deep comparisons and cloning? How to Get a Perfect Deep Equal in JavaScript? .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk15 { color: #C586C0; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }usePrevious를 조금 더 깊게 이해해보자
usePrevious 훅을 검색해보면 보통 아래와 같이 작성된다. // usePrevious.ts export default function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } 그리고 아래와 같이 import하여 사용할 수 있다. // App.tsx function App() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); useEffect(() => { console.log(count, prevCount); }, [count, prevCount]); return ( <div> <h1> Now: {count} <br /> Before: {prevCount} </h1> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } 코드 출처 App 컴포넌트 내의 useEffect 내부에서 count와 prevCount에 대해서 콘솔을 찍어보면 prevCount는 count의 항상 이전 값을 보여준다. 이와 관련하여 이해가 안가는 부분이 존재했다. 내가 생각한 것은 다음과 같다. useEffect 내부의 로직은 렌더링이 발생한 후에 실행된다. 그러므로 ref.current가 바뀌는 것도 렌더링이 된 후이다. 더불어서 App 컴포넌트의 useEffect 내 prevCount는 이미 값이 바뀌어 있으므로 콘솔을 찍었을 때 count와 prevCount가 같은 값을 가져야 한다. 라는게 내 생각이었다. 하지만 그렇지 않다. 답은 다음 할당문에 존재했다. const prevCount = usePrevious(count); useEffect 내에서 count와 prevCount가 동일한 값을 갖지 않는 이유는, 이전 값을 접근할 때 ref.current를 통해서 접근하는게 아니라 prevCount를 통해서 접근하기 때문이다. ref.current값이 바뀌기 전에는 count의 이전 값을 가지고 있다. 이 값을 우선 prevCount에 할당하고, 렌더링이 끝나면 ref.current를 count의 최신값에 업데이트함으로써 useEffect 내에서 서로 다른 값을 가질 수 있는 것이다. usePrevious 강화하기 앞선 usePrevious 훅은 한 가지 문제점을 안고있다. 다음과 같이 App 코드가 작성돼 있다고 가정해보자 function App() { const [count, setCount] = useState(0); const [_, forceRerender] = useState({}); const prevCount = usePrevious(count); useEffect(() => { console.log(count, prevCount); }, [count, prevCount]); return ( <div> <button onClick={() => forceRerender({})}>force rerender</button> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } Increment 버튼을 세번 누르면 useEffect 내의 count와 prevCount는 어떤 값이 찍힐까? 각각 3과 2가 찍힐 것이다. 이 상태에서 만약 force rerender 버튼을 누르면 useEffect 내에서는 어떤 값이 찍힐까? 3과 3이 찍힌다. count 상태가 변하지 않았음에도 불구하고, prevCount가 count값과 동일해지는 것이다. 이러한 문제를 해결하기 위해서 usePrevious를 다음과 같이 수정할 수 있다. export const usePreviousPersistent = <TValue extends unknown>( value: TValue ) => { // P1 const ref = useRef<{ value: TValue; prev: TValue | null }>({ value: value, prev: null }); // P2 const current = ref.current.value; // P3 if (value !== current) { ref.current = { value: value, prev: current }; } // P4 return ref.current.prev; }; 주석을 따라서 설명하면 다음과 같다. (P1) 기존의 usePrevious 훅과는 다르게 ref 내에 이전 값을 저장하는게 아니라, 이전 값과 현재 값을 프로퍼티로 갖는 객체를 저장한다. usePrevious는 항상 prev 프로퍼티 값을 반환한다. (P2) ref가 저장하는 객체의 현재 값인 value를 curent에 할당한다. (P3) 인자로 전달되는 value와 current(ref가 기존에 기억하고 있던 value)가 다르다면, ref가 관찰하는 상태가 업데이트 된 것이므로, ref가 기존에 기억하고 있던 value는 이전 값이 되므로 prev 프로퍼티에 할당하고, 새로 기억해야 하는 value는 value 프로퍼티에 할당한다. 만약 객체를 비교해야 하는 경우 deep equality를 사용해야 하지만 글쓴이는 라이브러리에 따라서 속도가 느릴 수 있기 때문에 별로 선호하지 않는다고 한다. 그래서 matcher 함수를 전달하는 다음 방식을 제안하고 있다. export const usePreviousPersistentWithMatcher = <TValue extends unknown>( value: TValue, isEqualFunc: (prev: TValue, next: TValue) => boolean ) => { const ref = useRef<{ value: TValue; prev: TValue | null }>({ value: value, prev: null }); const current = ref.current.value; if (isEqualFunc ? !isEqualFunc(current, value) : value !== current) { ref.current = { value: value, prev: current }; } return ref.current.prev; }; 📚 참고문헌 Implementing advanced usePrevious hook with React useRef .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }2월 2주차
📌 웹뷰 개발을 처음하면서 맞닥뜨린 문제 Google Store에 1.0.0 버전을 12월 말에 배포하고, 1.0.1을 준비하던 중 문제에 부딪혔다. 나는 1.0.1 버전에서 권한 승인 기능과 관련한 고도화 작업을 진행했는데, 1.0.0 버전에 대한 고려를 미쳐하지 못했다. react 코드만 바뀌면 상관이 없는데 react-native 코드도 바뀌어서, 기존에 1.0.0 버전의 react-native 코드가 적용된 앱은 사용이 불가능해진 것. 쉽게말하면 react를 1.0.1 버전으로 업그레이드 하면 기존의 앱과 새롭게 배포될 앱 모두 react 1.0.1 버전이 적용되는데, 기존 앱의 경우 react-native 버전이 1.0.0이고, 신규 앱의 경우 react-native 버전이 1.0.1인 상황에서 react 1.0.1버전은 react-native 1.0.1에 의존하기 때문에 기존 사용자는 앱이 켜지지 않는 문제가 발생했다. 절대 발생하면 안되는 문제를 발생시켰지만, 너무나도 귀중한 경험을 한것 같다. 📌 기확자와 처음 협업하면서 맞닥뜨린 문제 기획자분과 본격적으로 일하기 시작한지 2주가 됐다. 이미 한분과 일은 게속해왔지만, 그분이 기획하신 업무를 받아서 일을 진행하진 않았어서 사실 기획자와 협업한다는 느낌이 많이 없었다. 이번에 같이 일하면서 두가지를 느꼈는데, 그중 첫번째는 행위를 나열한다는 것이다. 예를들면 “A한 상황에서 B를 클릭하면 C가 되게해야 한다”의 형식으로 티켓(기획)을 만들어주신다. 물론 행위가 달라질 수 있기 때문에, A, B, C를 바꿔서 제시해주신다. 처음에는 기획자분들께서 일을 잘하시고 똑똑하시니까 이를 믿고 개발에 바로 돌입했는데, 코드를 작성하다보니 계속 예외 행위에 부딪혔다. 심지어는 기획자분께서 제시해준 행위 하나는 발생할 수 없는 행위… 예를들면 안돼야 하지만, 돼야하는 경우가 존재하는. 그래서 개발 중간에 vscode를 끄고 usecase부터 작성했다. usecase를 작성하니 (1) 기획적인 부분에서 검토가 부족했던 부분을 발견할 수 있었고, (2) 행위들을 몇 가지 케이스로 분류가 가능했고, (3) 분류하고나니 머릿속에 어떻게 로직을 작성해야 하는지 쉽게 그려졌다. 이를 깨닫고 항상 티켓에 대한 작업을 들어가기 전에는 usecase부터 작성한 후에 티켓 작업에 들어가고 있다. 여담으로, usecase를 작성할때 코테 준비했던게 많은 도움이 되는 것 같다. 📌 ssr에서의 bfcache 그리고 뒤로가기 ssr 환경에서는 bfcache가 동작하지 않는다고 한다. bfcache가 동작하려면 기본적으로 img, font와 같은 asset들을 클라이언트에서 들고 있을 수 있는 csr만 가능하고, ssr은 페이지를 다 그려서 내려주기 때문에 불가능하다고 한다. 추가적으로 프로젝트를 진행하면서 bfcache의 존재로 인해 이전 페이지의 정보가 유지돼야 한다는 주장이 있었는데, bfcache는 이전 페이지의 정보를 들고 있다가 보여주기 때문에 페이지가 보여지는 속도가 빠른 장점이 있지만, 반대로 이전 페이지의 정보가 최신화되지 못한다는 단점이 있다. 그러니까 이전 페이지의 정보가 유지돼야 한다는 것은 어떤 표준이나 규약같은게 아니라 단점으로 언급된다는 것이다. 더불어서 nextjs에서 뒤로가기 했을 때 스크롤을 복원하는 설정을 제공하고 있긴 하나, next 13.0.7버전 이상부터 정상적인 동작을 보장하고 있다. 📌 금주 읽은 아티클 1. Scrollend, a new JavaScript event scrollend 이벤트가 나온다고 한다. 프로젝트의 디테일 페이지를 react-native에서 react로 마이그레이션 할때, react-native에 존재하는 onScrollEnd 이벤트가 react에 존재하지 않아서 굉장히 힘들게 구현했는데, 기쁜 소식이 들려와서 좋다. 아직 지원되는 브라우저가 많지 않아서 않아서 당장 사용은 어려울듯 ㅜ.ㅜ 2. useLayoutEffect와 관련해서 React Hooks — When to Use useLayoutEffect Instead of useEffect React useEffect v/s useLayoutEffect Hook: Key differences useEffect는 부수적인 효과(api 요청, 이벤트 리스너 등록, 상태 변화에 따른 특정 액션 수행 등)를 만든다는 표현을 또 처음 알게됐다. 더불어서 useEffect 내부의 return 문은 오직 컴포넌트가 unmount될때만 실행되는 줄 알았는데 의존성 배열 요소가 바뀌어도 실행되는 것을 처음 알았다. useLayoutEffect는 당연스럽게도 너무 헤비한 작업을 처리하면 browser에 화면이 늦게 보인다고하고, 사용 예시로는 부모 요소의 size를 기반으로 자식 요소의 size를 결정해야할 때 유용하다고 한다. 또한 react 18 버전에서는 useEffect의 동작이 조금 바뀌었다고 하는데, 앞선 두개의 문서만으로는 이해가 되질 않아서 chatGPT에게 물어보니 import React, { useState, useEffect } from 'react' function Example() { const [count, setCount] = useState(0) useEffect(() => { console.log('useEffect triggered') document.title = `Count: ${count}` }, [count]) return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ) } 유저 상호작용에는 discrete input과 continuous input 두가지가 있는데, discrete input의 경우는 마우스 클릭, 키보드 입력, 화면 터치 등이 있고, continuous input은 스크롤링, 마우스 이동 등이 존재한다. 만약 앞선 사진의 코드 처럼 discrete input에 의해서 상태가 변경되어 useEffect 내부 코드가 실행되는 경우, react 18 버전에서는 브라우저가 paint 하기 전에 useEffect 내부 코드가 즉각 실행된다고 한다. 그러므로 앞선 코드도 button을 discrete input하여 count가 바뀌므로 document.title이 paint되기전에 실행된다고 한다. 3. 타입스크립트에서 전문가처럼 에러 처리하기 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk17 { color: #808080; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }nextjs의 dynamic route 페이지에서의 스크롤 복원
웹뷰 프로젝트를 진행하면서, dynamic routing 페이지에서 뒤로가기시 스크롤 복원 로직을 구현하면서 겪은 시행착오와 문제점들을 해결한 과정을 공유합니다. 목차는 다음과 같습니다. 📌 1. file based routing과 dynamic routing에 관한 이해 📌 2. dynamic routing 페이지의 특성 📌 3. 용어 정의 📌 4. nextjs에서 제공되는 route와 관련한 메서드들의 실행과 렌더링 및 페이지 URL 변화 📌 5. 코드 레벨에서의 뒤로가기 구현 📌 6. 네이버 책방의 스크롤 처리 스크롤 저장 스크롤 복원 어떤 글을 작성을 완료하고 뒤로가기 했을 때 글 작성 페이지로 돌아가지 않게 하는 방법 📌 1. file based routing과 dynamic routing이란? nextjs는 file based routing이며 dynamic routing 기능을 제공합니다. file based routing이라 함은 nextjs에서 고유하게 제공하는 pages 폴더 하위의 폴더 및 파일들이 URL로 인식됨을 의미합니다. 가령 아래와 같은 폴더 구조가 있다고 가정합니다. pages ㄴ item ㄴ list.tsx ㄴ [id].tsx 위와 같은 폴더 및 파일 구조를 갖는 경우, https://example.com/item/list으로 접근이 가능하고, 파일명에 대괄호가 들어가 있는 것을 통해서 dynamic routing 처리가 되어있는 것을 알 수 있습니다. 그러므로 https://exmaple.com/item/1 혹은 http://example.com/item/23 등, item 하위 경로의 아무 숫자로든 접근이 가능해집니다. 📌 2. dynamic routing 페이지의 특성 dynamic routing 페이지의 특성 중 하나는, 페이지 내에 존재하는 버튼 요소등을 통해서 동일한 dynamic routing 경로에 접근하는 경우, 그러니까 item/1 -> item/2 -> item/3 -> … -> item/100 과 같이 이동하는 경우, [id].tsx 컴포넌트는 unmount되지 않고 계속 사용됩니다. 다만, 리렌더링이 계속 발생할 뿐입니다. 이로부터 파생되는 다음 두가지 특성이 존재합니다. 1. 이전 상태가 계속 유지됩니다. item/1에서 머무르다가 item/2에 도달했을 때 값을 useState 내부 값을 초기화한 상태에서 시작하는게 아니라, item/1에 사용하던 상태와 컴포넌트가 그대로 남아있습니다. 그리고 다음과 같은 문제를 야기합니다. 서로 다른 item 페이지에 방문할 때마다, useEffect 내부에서 어떤 로직이 실행되길 기대한다면, dependency에 페이지 변화에 관한 변수를 넣어주어야 합니다. (추측) 스크롤이 복원되려는 것과 컨텐츠 내용물이 달라짐으로써 스크롤 높이가 달라지는 문제로 스크롤이 정상적인 위치로 복원되지 않는 문제가 있습니다. item/1이 item/2에 비해서 스토리 컨텐츠의 스크롤 가능 높이가 매우 길다고 가정할 때, item/2에서 item/1로 뒤로가기 하는 경우, 렌더링하는 과정과 스크롤을 복원하려는 로직의 충돌(?)로 인해 스크롤이 정상적인 위치로 복원되지 않습니다. 2. 화면을 아얘 처음부터 그리지 않기 때문에 화면이 깜빡이지 않습니다. 만약 이러한 특성을 원하지 않는다면, Component에 다음과 같이 key를 할당하여 remount 시켜 상태를 초기화할수 있습니다. import { useRouter } from 'next/router'; import { getItem } from 'apis'; const ItemPage = () => { const { query } = useRouter(); const id = +query.id; const { data, isLoading, error } = useQuery(['item', id], getItem); if (isLoading || error) { return <></>; } return <Item key={id} />; }; 📌 3. 용어 정의 페이지 네비게이션은 다음 네가지 방법으로 일어날 수 있습니다. 페이지 내에서 다음 페이지로 넘어가는 요소와의 상호작용 (push) 뒤로가기 (backspace, 주소탭 옆의 뒤로가기 클릭) 앞으로가기 (forward, 주소탭 옆의 앞으로가기 클릭) 주소탭에 URL 직접 입력 첫번째는 단순 router.push를 하는 일반적인 상황이므로 제외하고, 나머지 케이스들 중에서 뒤로가기 케이스에 대해서만 로직을 작성했습니다. 📌 4. nextjs에서 제공되는 route와 관련한 이벤트들의 실행과 렌더링 및 페이지 URL 변화 nextjs에서 route와 관련한 이벤트 핸들러(beforePopState, routeChangeStart 등)들을 제공합니다. 스크롤 복구 로직을 구현하면서 이벤트 핸들러 내부에서의 상태 업데이트와 언제 어떻게 렌더링이 발생하여 실질적인 페이지 이동은 언제 일어나는지에 대한 이해가 필요했습니다. 관련한 코드는 Codesandbox에 작성해두었습니다. 앞선 Codesandbox 코드들을 렌더링 및 페이지 URL 변화 관점에서 시각화 해보면 다음과 같습니다. 먼저 push state 상황이므로 beforePopState 이벤트 핸들러는 실행되지 않습니다. 처음 렌더링(R1)은 routeChangeStart 이벤트 핸들러 내부의 상태 업데이트에 의해서 발생하고, 두번째 렌더링(R2)는 beforeNavigationStart 이벤트 핸들러에 의해 발생되고, 리렌더링이 끝나면 URL이 바뀌면서 초기 렌더링(R3)이 진행됩니다. 초기 렌더링이 끝나면 routeChangeComplete에 의해서 네번째 렌더링(R4)가 발생합니다. rotueChangeStart 이벤트 핸들러나 beforeNavigationStart 이벤트 핸들러에서 상태를 업데이트하면 URL이 바뀌기전에 업데이트된 사항을 변경하고 다음 URL로 넘어가게 됩니다. chatGPT는 ‘route가 끝나면 유저가 사용 가능한 페이지가 보여져있다고’고 설명합니다. 만약 초기 렌더링 단계의 존재를 알지 못한다면 상황에 따라서 이 문장이 조금 혼란스러울 수 있습니다. 예를들면 isRouting이라는 boolean 상태가 있다고 가정해봅시다. 그리고 routeChangeStart시 isRouting true로 만들고, routeChangeComplete시 isRouting을 false로 만들어봅니다. console.log(유저가 보는 URL, isRouting)을 로깅하면 item/2, false가 찍히는 경우가 있을텐데, 초기 렌더링이 존재함을 알지 못한다면 ‘라우팅도 안끝났는데 왜 URL은 바뀌어있지?‘라는 생각이 들수 있습니다. 아래 뒤로가기 상황은 beforePopState만 추가되고 모두 동일합니다. 참고로 beforePopState는 뒤로가기(back)뿐만 아니라 앞으로가기(forward)시에도 트리거됩니다. 📌 5. 코드 레벨에서의 뒤로가기 구현 isBeforePopStateEventTriggered isCurrentPageVisitedByBackspace 우선 뒤로가기와 관련한 위 두가지 변수를 먼저 설명하겠습니다. beforePopState 이벤트 핸들러가 실행된 후 routeChangeStart 이벤트 핸들러가 실행됩니다. 뒤로가기가 발생했는지, 안발생했는지를 알기 위해서는 beforePopState가 실행됐는지 안실행됐는지를 알수 있어야합니다. 그래서 isBeforePopStateEventTriggered 변수와 업데이트 함수를 만들었습니다. 뒤로가기로직은 많은 페이지에서 사용될 것으로 예상됩니다. 그렇기 때문에 앱이 꺼지기 전까지 unmount되지 않는 _app 컴포넌트 내에 useBackSpace훅 내에서 이벤트 리스너를 등록합니다. 뒤에서 설명하겠지만 실제 사용하는 페이지에서(예를들면 스크롤 복원이 필요한 페이지) isBeforePopStateTriggered 변수가 필요하기 때문에 export 해주었습니다. export let isBeforePopStateTriggered = false; export let updateIsBeforePopStateTriggered = (newValue: Boolean) => (isBeforePopStateTriggered = newValue); 또한 단순히 flag의 역할을 위해서 사용되어 굳이 리렌더링을 유발하는 상태로 관리할 필요가 없어서 일반 변수로 선언해주었으며, react가 관리하는 상태와 햇갈릴 수 있을 것 같아서 set이라는 prefix 대신 update prefix를 붙여주었습니다. 앞선 사진속 로직을 이어서 설명하면 다음과 같습니다. isBeforePopStateEventTriggered의 초기값을 항상 false로 만들고, beforePopState 이벤트 핸들러가 실행되면 true로 만듭니다. 그러면 routeChangeStart가 실행됐을 때 이 값이 true라면 뒤로가기가 발생한 상황이고, 그렇지 않다면 뒤로가기가 발생하지 않은 상태가 되게 됩니다. 그리고 isBeforePopStateEventTriggered는 routeChangeStart 이벤트 핸들러 내에서만 사용이 되는데, 만약 isBeforePopStateEventTriggered가 true라면 isCurrentPageVisitedByBackspace는 true가 되고 아니라면 false가 되게 됩니다. 1. 스크롤 저장 스크롤 저장은 (1) push state시 혹은 (2) history stack상 맨 마지막에 존재하는 페이지에서 뒤로가기시(앞으로가기가 불가능한 페이지)에서 스크롤을 저장해야 합니다. (2)를 고려하지 않는다면, 뒤로가기 했다가 다시 앞으로가기시에 저장된 스크롤 위치가 없어서 정상적인 스크롤 복구가 이루어지지 않습니다. 저는 앞으로가기가 있는 웹의 조건이 아닌 앱의 조건만을 고려하여 구현하였으므로 (1)과 (2)를 모두 처리하는 로직은 주석처리했습니다. const handleChangeRouteStart = () => { // (1)과 (2)처리 // if (isBeforePopStateEventTriggered && isCurrentPageVisitedByBackspace) { // return // } // (1)만 처리 if (isBeforePopStateEventTriggered) { return; } if (scrollElementRef.current) { setScrollPosition([ ...prevScrollPosition, { id, scroll: scrollElementRef.current.scrollTop, }, ]); } }; events.on('routeChangeStart', handleChangeRouteStart); 보시다시피 스크롤 저장은 routeChangeStart 이벤트 핸들러 내에서 발생하게 되는데, isBeforePopStateEventTriggered가 true인 경우 뒤로가기 상황이고, false인 경우 push 상황이라고 생각할 수 있습니다. (1)과 (2)를 처리하는 코드는 왜 isBeforePopStateEventTriggered && isCurrentPageVisitedByBackspace인 이유는, isCurrentPageVisitedByBackspace의 상태 업데이트와 뒤로가기시 스크롤을 저장하는 로직이 모두 routeChangeStart에서 발생하기 때문입니다. 맨 앞 페이지에서 뒤로가기를 눌러도 상태가 업데이트 되기 전에 isCurrentPageVisitedByBackspace에 접근하므로 맨 앞 페이지는 스크롤이 저장되게 되는거죠. 2. 스크롤 복구 스크롤 복구는 뒤로가기가 발생했을 때만 트리거돼야 하므로 다음과 같이 isCurrentPageVisitedByBackspace가 true인 경우에만 스크롤이 복구되도록 작성해줍니다. useEffect(() => { if (isCurrentPageVisitedByBackspace) { const { scroll } = scrollPositions.pop(); setScrollPositions(scrollPositions); scrollElementRef.current.scrollTo(scroll); } }, [isCurrentPageVisitedByBackspace]); 3. 어떤 글을 작성을 완료하고 뒤로가기 했을 때 글 작성 페이지로 돌아가지 않게 하는 방법 문제 상황은 다음과 같습니다. 작성 페이지로 접근할 수 있는 페이지에서 작성 페이지로 접근하고, 두번의 네비게이션 후 작성을 완료하게 되면 작성된 글의 id를 전달받고 새로운 페이지로 넘어가게 됩니다. 그런데 여기서 뒤로가기를 했을 때 작성 페이지 1과 2를 보고싶지 않습니다. 원하는 상황은 다음과 같이 history stack이 정리되었으면 좋겠습니다. 단순히 go(-n)을 처리하자니 다시 앞으로가기하는 경우 작성 페이지1과 2를 방문할 수 있었고, history stack에서 강제로 pop하는 메서드는 존재하지 않았습니다. 그래서 workaround한 해결방법으로 접근했습니다. onSuccess: ({ createdItemId }) => { setCreatedItemId(createdItemId); window.history.go(-2); }; 우선 작성하여 생성된 게시글의 id를 전역상태로 두고, history.go(-2)해줍니다. 그리고 _app 아래의 useBackSpace 훅 안에 다음과 같은 로직을 작성해줍니다. useEffect(() => { if ( !isCurrentPageWritePage(router) && isNewlyCreatedItemExist(createdItemId) ) { router.push(`/item/${createdItemId}`); } }, []); 현재 페이지가 글 작성 페이지가 아니고 새로 작성된 게시글이 있는 경우에만, 새로 작성된 게시글의 페이지로 이동시킵니다. 이 로직이 실행되는 것은 오로지 (1) 글이 새로 작성되고, (2) history.go(-2)가 발생했을 때 뿐일 것 입니다. const handleRouteChangeComplete = () => { if ( isCurrentPageNewlyCreatedPage(router) && isNewlyCreatedItemExist(createdItemId) ) { setCreatedItemId(null); } }; 그리고 페이지 이동이 끝나게 되면 createdItemId를 다시 null로 초기화하기 위해 routeChangeComplete 이벤트 핸들러에 위와 같이 로직을 작성합니다. 이렇게 작성하면 글 작성을 하고 뒤로가기를 했을 때 다시 작성 페이지로 돌아가지 않게됩니다. history stack이 원하던 상태로 된것 입니다. 다만 여기에는 한가지 문제점이 있습니다. go(-2)를 하여 어떤 페이지로 이동했을 때, 이 페이지가 잠깐동안 보여진다는 것 입니다. 이를 해결하기 위해서, return <>{!isNewlyCreatedItemExist(createdItemId) && <Component />}</>; 글이 생성됐으면 해당 페이지로 이동하기 전까지는 잠깐동안 페이지를 보여주지 않도록하여 마무리합니다. 📌 6. 네이버 책방의 스크롤 처리 제가 고민하는 다음 세가지 문제에 대해서 SSR 환경에서 동작하는 네이버 책방은 어떻게 처리했을까 확인해보았습니다. 1. 앞선 2-1에서 언급했던 스크롤이 복원되려는 것과 컨텐츠 내용물이 달라짐으로써 스크롤 높이가 달라지는 문제로 스크롤이 정상적인 위치로 복원되지 않는 문제 네이버 책방은 페이지 이동시 스크롤 위치 정보 뿐만 아니라, 스크롤 가능한 길이(scroll bar의 높이) 역시도 저장합니다. 아마 이 상태를 저장하는 이유는 컨텐츠들이 다 로드돼서 스크롤 가능한 길이가 저장된 상태와 일치했을 때 스크롤을 복구하려는 이유에서 저장한게 아닐까 싶습니다. 2. 페이지 1 -> 페이지 2 -> 다른 도메인 -> 페이지 1에 도달했을 때 스크롤 복원 여부 이상적으로는 페이지 1에 도달할때는 뒤로가기나 앞으로가기를 통해서 방문한게 아니니까 맨 위에 있어야 하는데, 스크롤 복원이 일어납니다. 3. 페이지 1 -> 페이지 2 -> 페이지 3 🟢 -> 페이지 2 -> 페이지 3와 같이 history stack이 쌓였을 때 🟢에서 뒤로가기와 앞으로가기를 구분하지 않으면 어떠한 스크롤 위치를 복구해야 할지 모름 페이지 3에서 beforePopState가 발생하면 앞으로 가기를 해서 페이지2에 방문하거나 뒤로가기를 해서 페이지2에 방문하거나 두 가지중 하나일 것 입니다. 그러므로 뒤로가기와 앞으로가기를 구분해서 적절하게 스크롤 위치를 복원해야 한다고 생각했는데요. 네이버 책방은 스크롤을 저장할 때 페이지 URL을 남기는데, URL에는 NaPm이라는 파라미터가 남게됩니다. 이 파라미터는 동일한 페이지를 방문해도 값이 달라집니다. 아마 이 값을 고유한 id값처럼 사용하여 굳이 뒤로가기와 앞으로가기를 구분하지 않고도 페이지 복원이 가능한 것 같습니다. .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk17 { color: #808080; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }1월 2주차
📌 vscode 윈도우가 켜졌을 때 파일명 바꾸기 vscode 내에서 특정 파일의 이름을 바꾸어야 할때가 많다. 더욱이 우리 프로젝트는 nx 모노 레포를 사용해서 폴더의 depth가 깊다(apps -> project -> src -> components …🤷). 이때 sidebar를 통해서 수정하는게 여간 번거로운게 아니다. 키보드를 벗어나는 것도 귀찮다. File Utils extension을 이용하면 아래 이미지처럼 커멘드 팔레트를 통해서 간단하게 이동할 수 있다. 📌 branch 명 바꾸기 본인은 vscode 터미널을 이용해서 명령어를 통해 git 관련한 작업을 진행한다. 이때 브랜치 이름이 permission-request-check와 같이 길게되면 종종 명령어를 치다가 오타가 나곤한다. 복사하는 방법이 없나 하다가, 앞선 File Utils와 마찬가지로 Git Lens에서 제공하는 기능을 이용하면 쉽게 브랜치 이름을 복사할 수 있다. 📌 권한 요청 구현 현재 앱은 사용하려면 위치, 앨범에 관한 유저의 권한 승인이 필요하다. 한가지 까다로운 상황은, 유저가 앱을 사용하다가 앱을 종료하지 않은채 앱의 설정으로가서 권한을 변경하고 앱으로 돌아왔을 때 앱 내의 권한 상태를 변경해야한다. 앱이 foreground에 있는지 background에 있는지를 구하는 방법은 쉽다. react-native에서 제공하는 AppState를 이용하면 된다. 아래 이미지와 같이 주석으로 설명도 달려있다. 그래서 아래 코드와 같이 AppState가 background에서 active가 되었을 때 권한 상태를 바꿔주는 코드를 작성할 수 있다. const handleReceiveMessageFromWeb = async (권한 종류) => { const res = await request(권한 종류); // 유저에게 권한 요청창이 등장함 setPermissionStatus(res); }; App.addEventListener('message', handleReceiveMessageFromWeb); // 앱이 웹으로부터 권한 요청 관련한 통신을 listen함 useEffect(() => { const handleAppStateChange = async(nextAppState) => { if (appState.match(/inactive|background/) && nextAppState === 'active') { const res = await check('LOCATION') // 🙋 앱의 권한 상태 변경 setPermissionStatus(res); } } AppState.addEventListener('change', handleAppStateChange) return () => { AppState.removeEventListener('change', handleAppStateChange) } }, [appState]) 앱 상태와 관련한 이야기는 여기까지하고, 현재 앱은 권한이 필요한 기능에 접근할 때 권한 요청이 들어간다. 가령 내 위치로 가기 버튼을 눌렀을 때 위치 권한을 요청하여 유저의 화면에 아래와 같은 권한 설정창이 등장한다. 이미지 출처 권한 요청이 실행되기 위해서 웹뷰에서 앱으로 권한 요청에 관한 통신이 일어난다. 그리고 권한 요청은 앞선 코드의 handleReceiveMessageFromWeb 내의 request 코드가 실행될때 등장한다. 문제는 이 요청이 보여지는 순간, 앞선 코드의 handleAppStateChange가 실행된다. 분명 앱에 존재하는데, 권한 요청이 뜨면 background 상태로 들어가는 것 처럼 행동하는 것이다. chatGPT에 다시 물어보니 아래와 같은 답변을 얻을 수 있었다. 결국 handleReceiveMessageFromWeb도 실행되고, handleAppStateChange도 실행돼서 setPermissionStatus가 두번 실행된다. 코드가 실행되도 정상적으로 동작하면 상관이 없을 수도 있는데, 유저가 요청에 대해서 ‘거부 및 다시보지 않음’을 선택하는 경우에, handleAppStateChange에서 실행된 check가 App의 상태를 blocked가 아닌 denied로 check한다. 승인이나 거부에 대해서는 granted와 denied가 정상적으로 check하는데, blocked만 상태를 denied로 check한다. -_-… 원인을 찾지 못해서 우선은 권한 요청 창이 떠있는 경우 handleAppStateChange가 실행되지 않도록 아래와 같이 코드를 수정했다. const handleReceiveMessageFromWeb = async (권한 종류) => { setIsPermissionRequestNotificationOpen(true); const res = await request(권한 종류); // 유저에게 권한 요청창이 등장함 setPermissionStatus(res); setIsPermissionRequestNotificationOpen(false); }; App.addEventListener('message', handleReceiveMessageFromWeb); // 앱이 웹으로부터 권한 요청 관련한 통신을 listen함 useEffect(() => { const handleAppStateChange = async(nextAppState) => { if (appState.match(/inactive|background/) && nextAppState === 'active' && !isPermissionRequestNotificationOpen) { const res = await check('LOCATION') // 🙋 앱의 권한 상태 변경 setPermissionStatus(res); } } AppState.addEventListener('change', handleAppStateChange) return () => { AppState.removeEventListener('change', handleAppStateChange) } }, [appState, isPermissionRequestNotificationOpen]) 📌 Typescript 에러 아래 코드가 왜 에러가 발생하는지 모르겠다. -_-; const callbackFunction = ( fn: ((oneArgument: number) => void) | (() => void), ) => { if (fn.length === 1) { fn(1) return } if (fn.length === 0) { fn() // 🤷 error that you must pass one parameter return } } 리서치해도, ChatGPT에게 물어봐도 답이 안나와서 stackoverflow에서 답변을 받았다. 0개 혹은 1개의 파라미터가 들어올 수 있는 경우에는 아래와 같이 타입을 작성하는 것이 타당하고, const callbackFunction = (fn: (oneArgument?: number) => void) => { // implementation } 2개 이상의 파라미터가 들어올 수 있는 경우에는 아래와 같이 코드를 작성하는 것이 타당하다고 한다. const callbackFunction = (fn: (...args: any[]) => void) => { // implementation } 📌 함수 오버로드 (Function Overloading) Function Overloading는 서로 다른 파라미터 타입이나 서로 다른 파라미터 개수를 갖는 동일한 이름의 함수를 여러개 만드는 것이다. 참고로 Method Overloading은 서로 다른 파라미터 개수를 갖는 동일한 이름의 method를 여러개 만드는 것이다. Javascript는 아래 코드처럼 (1) 파라미터 타입을 기재하지 않고, (2) 파라미터 개수를 확인하지 않기 때문에 다른 언어들과 같은 방식으로 Function Overloading 구현이 불가하다. function foo(bar) {} foo('Superman', 'Batman') foo() 아래와 같이 Javascript에서도 Overloading 닮은 모양으로 구현이 가능하지만, (1) 코드가 장황해지고 (2) 유지보수가 어려워진다. function concatString(s1, s2, s3) { if (arguments.length > 3) { throw new Error('signature not supported') } let s = s1 if (s2 && typeof s2 === 'string') { s += `, ${s2}` } if (s3 && typeof s3 === 'string') { s += `, ${s3}` } return s } 앞서 작성한 코드는 Typescript를 만나고 아래와 같이 작성이 가능해진다. function concatString(s1: string, s2?: string, s3?: string) { let s = s1 if (s2) { s += `, ${s2}` } if (s3) { s += `, ${s3}` } return s } // ❎ now this works concatString('one') concatString('one', 'two') concatString('one', 'two', 'three') // ❌ we will get compile errors if we try to do concatString('one', true) concatString('one', 'two', 'three', 'four') Overload Signature와 Implementation Signature 두개가 존재하며, 하나의 함수는 여러 개의 Overload Signature와 한개의 Implementation Signaure가 존재한다. 참고로 Signature은 function부터 return type까지를 의미하며, Overloading Signature은 함수의 body가 없지만, Implementation Signature은 함수의 body가 존재한다. Implementation Signature은 반드시 Overload Signature가 호환할 수 있어야 한다. 아래 코드를 보면 알수있다. // Overload Signature function greet(person: string): string; function greet(person: string[]): string; // Implementation Signature function greet(person: unknown): unknown { if (typeof person === 'string') { return `Hello, ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `Hello, ${name}!`); } throw new Error('Unable to greet'); } Function Overloading의 장점은 관련한 기능들을 한곳에 모을 수 있어 유지보수가 향상된다. 기능이 조금 차이가 난다고해서 이름이 다른 함수를 여러개 만들 필요가 없다. 단점은 Overload Signature가 많아질수록 Implementation Signature가 복잡하고 이해하기 어려워진다. 참고로 아래와 같이 선택적 파라미터로 처리할 수 있는 상황에 Function Overloading을 적용하진 말자 // Not recommended function myFunc(): string; function myFunc(param1: string): string; function myFunc(param1: string, param2: string): string; function myFunc(...args: string[]): string { // implementation... } // OK function myFunc(param1?: string, param2?: string): string { // implementation... } 사용시 한가지 주의해야하는 것은 타입스크립트는 첫 번째로 만족되는 Overload Signature를 함수를 호출할 때 처음으로 매칭되는 Overload Signature를 호출하기 때문에 순서가 중요하다. function fn(x: unknown): unknown; function fn(x: number[]): number; function fn(x:any) { return x; } //call the function with a number array and expect a number back var x = fn([1,2,3]); // x is unknown 위 코드를 다음과 같이 수정하면 error가 사라진다. function fn(x: number[]): number; function fn(x: unknown): unknown; function fn(x:any) { return x; } var x = fn([1,2,3]); // x: number Mastering Function Overloading in TypeScript A Simple Explanation of Function Overloading in TypeScript Function Overloading / Method Overloading TypeScript 📌 namespace, module, declare, Ambient Code namespace와 module의 역할은 동일하게 관련한 코드들을 그룹핑하여 이름이 충돌하거나 전역 스코프가 오염되는 것을 막는 것이다. 유일한 차이점은 namespace는 여러 파일에서 확장될 수 있고, module은 하나의 파일 안에서만 정의 된다는 것입니다. 또한 최신 Typescript에서 namespace는 deprecated되어, module의 사용이 권장됩니다. module MyModule { export function doSomething() { console.log("I'm doing something!"); } export class MyClass { // class code here } } // usage import { MyModule } from "./myModule"; MyModule.doSomething(); // myNamespace.ts namespace MyNamespace { export function doSomething() { console.log("I'm doing something!"); } } // myNamespace2.ts namespace MyNamespace { export class MyClass { // class code here } } import * as MyNamespace from "./myNamespace"; MyNamespace.doSomething(); const myClass = new MyNamespace.MyClass(); declare은 타입스크립트 컴파일러에게 “이 변수나 모듈은 어딘가에 존재하고, 타입은 이거야”라고 말하는 것이다. 예를 들어보자. Typescript React App에 Webpcack Hot Middleware가 존재한다. 하지만 Webpack Hot Middleware는 순수 Javascript로 작성됐다. 그래서 타입스크립트가 체크할 수 있는 타입 선언이 존재하지 않는다. Webpack Hot Middleware는 module이 존재하고 내부에 hot이라는 프로퍼티가 존재한다. 만약 module.hot에 접근하려고하면 타입스크립트는 “property ‘hot’ does not exist”라는 에러를 만들어낸다. 이때 우리는 declare 키워드를 사용하여 에러 제거가 가능하다. declare let module: any 만약 여기서 declare 키워드를 지운다면 “‘module’ already exists”라는 에러가 발생하게 될것이다. 위 코드만 보면 declare가 어떤 코드를 생성하는 것 처럼 보이지만, 어떠한 변수나 모듈도 생성하지 않는다. 다른 예로, 아래 코드는 순수하게 javascript로 쓰여진 jQuery의 변수 중 일부를 declare를 통해서 사용하는 것이다. declare let $: any; $(document).ready(() => { console.log("jQuery is ready!"); }); Ambient 코드는 애플리케이션에 존재하지 않고 외부 라이브러리에서 제공해주는 코드를 의미한다. declare를 사용하는 것은 변수나 모듈이 ambient고 자바스크립트 코드를 생성하지 말라고 타입스크립트 컴파일러에게 말하는 것과 같다. chatGPT Purpose of declare keyword in TypeScript .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk5 { color: #D16969; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }11월 4주차
📌 금주는 없던 런칭 일정이 뿅하고 생겨나서 정신없이 구현에만 몰두했다. 왠만해선 코드 퀄리티를 챙기려고 하는데, 기획, 디자인, 개발이 모두 병렬적으로 진행되고 있어서 코드 퀄리티도 챙길수가 없는 상태이다. 현재 상황에 대해서 많은 아쉬움이 드는데, 글로 따로 남겨놔야겠다. 런칭 전까지는 리서치한 내용이 다소 줄어들 것 같다. 🫠 📌 알고있는 것과 모르고 있는 것은 다르고, 제대로 아는 것과 아는 것은 또 다르다. 이전 회사에서 사수분이 해주었던 말씀이다. 금주에는 내가 css적으로 한번도 구현해보지 못했던 페이지의 동작을 구현해야 했다. 이 동작은 특정 ButtonGroup이 화면에 Footer처럼 fixed된 상태로 존재하다가, 스크롤을 내리면 특정 위치에서는 fixed가 아닌 relative처럼 동작하는 것이었다. 알 사람은 알겠지만 position: sticky를 이용하면 해결되는 문제다.처음에 기획 미팅을 할때, 이 동작에 대해서 구현이 어려울 것 같아 고민이 많았는데, FE 팀장님께서는 너무 간단하게 구현하기 쉽다고 말씀해주셨다. 자리에 돌아와서 구현 방향에 대해서 고민할 때 sticky가 생각이났다. 사용해 본적은 없지만 일전에 fixed와 absolute에 대해서 리서치할 일이 있었을 때 알게되어 ‘이런 것도 있구나’하고 넘어갔던게 문제 해결에 도움이 됐다. 사수분의 말이 떠올랐던 경험이었다. .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }DP(Dynamic Programming)과 Greedy 알고리즘
📌 Dynamic Programming 다이나믹 프로그래밍은 (1) 어떤 문제를 하위 문제로 쪼갤 수 있고, (2) 어떤 문제의 하위 문제가 겹치고, (3) optimal substructure 특성을 가지고 있을 때 도움이되는 기술이다. 앞서 말한 세가지 조건이 만족될 때, 하위 문제의 해답을 저장하고 필요할때 재사용함으로써 CPU의 연산량을 줄여 효율성을 향상시킬 수 있다. 만약 어떤 문제 A를 구성하는 하위 문제들이 최적의 해답으로 해결됐을 때, A 문제 역시도 최적의 해답으로 해결된다면, A 문제는 optimal substructure 하다고 말할 수 있다. wikipeida 피보나치 수열을 예로 들어보자. 피보나치 수열은 0, 1로 시작하며 다음에 오는 숫자는 이전 두개의 숫자의 합이 되는 수열을 말한다. 다섯 자리까지 구하는 경우 아래와 같이 구할 수 있다. F(0) = 0 F(1) = 1 F(2) = F(1) + F(0) F(3) = F(2) + F(1) F(4) = F(3) + F(2) 이를 통해서 ‘하위 문제의 해답을 저장하고 필요할때 재사용함’을 알수있다. 그리고 이러한 기술을 memoization이라고 한다. 사실 다이나믹 프로그래밍 구현에는 memoization과 tabulation이라는 두가지 기술이 존재한다. memoization을 이용한 다이나믹 프로그래밍은 top-down 방식으로 볼수 있다. 이미 모든 하위 문제의 해답을 계산했다고 ‘가정’하며, 해답을 구하는 순서에 대해 관심갖지 않고 단순히 재귀 함수(recursion)를 호출할 뿐이다. 보통 a(n) = a(n-1) + a(n-2)의 재귀적인 구조로, 코드가 직관적인 장점이 있지만, 메모리 스택이 쌓인다는 단점이 있다. 반면 Tabulation은 bottom-up 방식이다. Tabulation은 도표 작성이라는 뜻으로, 이를 이용한 다이나믹 프로그래밍을 table-filling 알고리즘이라고 부르기도 한다. memoization과는 다르게 해답을 구하는 순서를 미리 정해야한다. 예를들면 피보나치에서 a(0)과 a(1)을 먼저 구하여 저장하고(table-filling), 순차(iteration)적으로 a(i) = a(i-1) + a(i-2)를 구한다. 메모리 스택에 대한 걱정이 없고, 함수 호출에 대한 overhead가 없기 때문에 성능적으로 유리하지만, 순서를 미리 정해야하는 단점이 있다. 다이나믹 프로그래밍과 함께 많이 언급되는 것이 탐욕 알고리즘(Greedy Algorithm)이다. 두개의 차이점에 대해서 알아보아야 하는데, 이는 탐욕 알고리즘 목차에서 알아보도록 하자. programize when to use bottom-up DP and when to use top-down DP What is the difference between bottom-up and top-down? 📌 Greedy Algorithm 매순간 주어지는 정보로만 최적의 해답을 선택한다. 이 선택이 전체적인 관점에서 최적의 해답이 되지 못할 수 있다. 예를들면, 청첩장을 받은 사람이 결혼식장에 가려고 하는데, 늦잠을 자버렸다고 하자. 식장까지 이동 수단의 선택지가 전철과 택시가 있다고 했을 때 최적의 해답은 택시를 타는 것이 된다. 택시를 타고 이동하는데, 마라톤 행사로 인해서 5블럭까지 직진을 할수 없는 상황에 부딪혔다. 만약 마라톤 행사에 대한 정보를 고려했다면 전철을 타고 늦지 않을 수 있었다. 또다른 예는 아래 트리의 루트로부터 비용이 가장 많은 노드를 거쳐서 이동하는 경우를 생각해보자. 20부터 시작해서 2와 3을 선택해야 할때, 3이 비용이 더크니 3을 선택하고, 최종적으로 1을 선택해서 총 비용이 24가 나온다. 만약 2를 선택하고 10을 선택했다면 32라는 비용을 얻을 수 있게된다. Dynamic Programming과 Greedy Algorithm은 모두 최적화 문제를 해결하는데 사용된다. 중요한 차이점은, Dynamic Programming은 항상 최적의 해답을 보장하지만, Greedy Algorithm은 항상 최적의 해답을 보장하지 못한다. 추가적으로 Dynamic Programming은 이전에 구해놓은 해답을 이용하기 때문에 메모리가 필요하지만, Greedy Algorithm은 매순간 해답을 선택하기 때문에 상대적으로 메모리가 필요하지 않다. programize Difference Between Greedy Method and Dynamic Programming .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }11월 3주차
📌 Union Find 아래 코드의 union 로직은 time limit exceeded가 발생할 확률이 크다. 시간 복잡도에 있어서 병목이 발생할 수 있는 부분은 union된 노드가 만드는 트리가 Imbalanced 돼있을 때이다. 즉, 리스트처럼 0 - 1 - 2 - 3 - 4 - 5로 연결돼서, tree의 height이 노드의 개수와 같아지면 트리는 매우 Imbalanced한 상태로 볼수있다. 0 - 1 - 2 - 3 - 4 - 5와 같이 union 돼있을 때, this.find(5)가 실행되면, height만큼 탐색을 해야한다. 이를 해결하기 위해서, 아래와 같이 특정 root 노드가 얼마나 많은 노드들과 결합되어 있는지를 알수 있는 count 배열을 만든다. 그리고 2개 이상의 노드가 존재하는 트리를 union 해야할 때 노드가 적은 트리를 노드가 많은 트리에 결합하는 규칙을 적용한다. 이유는 아래 그림처럼 노드가 많은 트리를 노드가 적은 트리에 결합하는 경우, height이 1개 더 증가하기 때문이다. Union Find Algorithm: Brief but Comprehensive Guide 📌 next/image 컴포넌트 width 100% 만들기 next/image(이하 이미지 컴포넌트)는 CLS 방지를 위해서 width와 height을 기입해야한다. 만약 어떤 이미지 컴포넌트를 부모 컴포넌트 내에 꽉 차게 만들고 싶다면, layout은 fill prop을 전달하면 이미지가 부모 컴포넌트에 꽉 차게된다. 단, 부모 컴포넌트 역시도 width와 height을 기입해주어야 한다. 우리가 width 100%를 적용하고 height이 그에 따라서 늘어나길 기대할 수 없다는 말이다. 이러한 next/image 성질에 대해서 이미 여러 개발자들이 불편을 토로해 왔다. 이를 해결하기 위해서는 Image 컴포넌트를 감싸는 Wrapper 컴포넌트에 다음과 같은 스타일을 적용한다. '& > span': { position: 'unset !important' }, '& > span > img': { objectFit: 'contain', width: '100% !important', position: 'relative !important', height: 'unset !important' } Wrapper 컴포넌트의 경우 여러 태그들이 올수 있기 때문에(div, button 등), template 태그로 컴포넌트를 만들었고, 사용하는 쪽에서 as 프로퍼티를 통해서 태그를 수정하여 사용했다. // ./baseComponents.js export const NextImageWrapperTemplate = styled('template')(() => ({ '& > span': { position: 'unset !important', }, '& > span > img': { objectFit: 'contain', width: '100% !important', position: 'relative !important', height: 'unset !important', }, })) // index.js import NextImageWrapperTemplate from './baseComponents' import Image from 'next/image' const ComponentFunction = () => { return ( <NextImageWrapperTemplate as="button"> <Image /> </NextImageWrapperTemplate> ) } 📌 Norminal Typing, Structural Typing, Duck Typing 이 글은 Structural, Nominal, and Duck typing을 오,의역 했다. Norminal Typing, Structural Typing, Duck Typing은 타입 시스템이 타입 호환성을 비교하는 서로 다른 방법들이다. 주어진 두개의 타입이 (1) 서로 호환되거나 (2) 하나의 타입이 다른 타입의 부모 타입인 경우 타입 호환성을 얻을 수 있다. 타입 호환성 = 두개의 타입이 서로 호환 || 하나의 타입이 다른 타입의 부모 타입 T1 타입이 T2 타입과 호환 가능하다고 할때, T2 타입이 T1 타입과 호환 가능함을 의미하지는 않는다. 예를들면, Integer 데이터를 Decimal 데이터로 바꾸는 것은 가능하지만, Decimal 데이터를 Integer 데이터로 바꾸는 것은 불가능하다. (Decimal 타입이 무엇인지 이해하려면 깊게 들어가야 하므로, 여기서는 단순히 Decimal이 Integer의 자식 타입 정도로만 이해하고 넘어가자.) Norminal Typing은 타입 호환성을 비교하기 위해서 ‘이름’을 사용하고, Structural Typing은 타입 호환성을 비교하기 위해서 ‘구조’를 사용한다. Duck Typing은 사용에 기반을 둔 ‘구조’를 사용하며(당장 이해가 어렵지만 뒤에 예제를 보면 무슨 말인지 이해가 될 것이다.), 동적 타입 언어에서 보통 발견할 수 있다. 1. Norminal Typing Norminal Typing을 사용하는 타입 시스템에서는 타입 호환성을 위해서 다음 두가지중 하나가 만족되어야 한다. 타입 이름이 매칭되어야 함 (동일한 클래스로부터 만들어진 객체들이라고 표현하는게 이해가 더 빠른 것 같다. 🤔) 하나의 타입이 다른 타입의 부모 타입이어야 함 (상속 관계에 있는 클래스로부터 만들어진 객체들이라고 표현하는게 이해가 더 빠른 것 같다. 🤔) 먼저 1의 상황에 대해서 이야기해보자. 아래는 C# 예제다. Dog와 Cat 클래스가 name 프로퍼티와 makeNoise 메서드를 가지고 있다고 가정하자. 그리고 다음과 같이 객체를 생성하고, Dog cat = new Dog('Mars'); Cat cat = new Cat('Venus'); 다음 메서드에 위 두가지 객체를 각각 전달한다고 할때, static public void makeNoise(Dog obj) { obj.makeNoise(); } Dog 인스턴스에 대해선 동작을 하지만 Cat 인스턴스에 대해서는 동작하지 않는다. 설령 두 인스턴스의 모양이 동일하더라도, C#은 둘을 호환 가능하다고 취급하지 않는 것이다. 이렇듯 Nominal Typing은 서로 다른 타입으로부터 생성된 객체는 호환이 불가능하다. 앞서 2에서 이야기한 상속 관계가 아니라면 말이다. 2. Norminal Typing with Subtypes A를 상속받는 B가 존재한다고 할때, B 를 A 로 호환하는 것은 가능하지만, A 를 B 로 호환하는 것은 불가능하다. 앞선 예제에 이어서, 다음과 같이 BullDog이 Dog를 상속받는다고 가정해보자. public class BullDog:Dog { public String breedName; public BullDog(string name, string breedName) : base(name) { this.breedName=breedName; } } 이제 BullDog 타입은 Dog의 대체 타입으로 사용 가능하기 때문에, makeNoise 메서드의 인자로 전달할 수 있게된다. static public void makeNoise(BullDog obj) { obj.makeNoise(); } 만약 다음과 같이 makeNoise의 파라미터 타입을 Dog로 바꾼다면, BullDog 인자를 전달하면 동작하지 않게된다. Dog dog = new Dog("Mars"); makeNoise(dog); //error CS1503: Argument 1: cannot convert from 'Dog' to 'BullDog' 3. Structural Typing Structural Typing에서는 타입의 이름보다 타입의 형태가 더 중요하다. 그리고 Typescript는 Structural Typing 시스템이다. 앞선 예제를 Typescript로 옮겨보자. Dog와 Cat 클래스 모두 다음과 같이 name 프로퍼티와 makeNoise 메서드가 존재한다고 할때, class Dog { constructor(public name: string) {} makeNoise() { console.log('Woof') } } class Cat { constructor(public name: string) {} makeNoise() { console.log('Miau') } } makeNoise 함수의 인자로 두개 모두 전달이 가능하다. let dog = new Dog('Mars') let cat = new Cat('Venus') function makeNoise(obj: Dog) { obj.makeNoise() } makeNoise(dog) //Ok makeNoise(cat) //Ok 만약 Person 클래스가 makeNoise 메서드를 갖고 있고, name 프로퍼티를 갖고 있지 않다면, 앞선 makeNoise 함수에 전달이 불가능하게 된다. class Person { constructor(public firstName: string, public lastName:string) { } makeNoise() { console.log('Helloooo') } } let person= new Person("Jon","Snow") makeNoise(person) //Error //Argument of type 'Person' is not assignable to parameter of type 'Dog'. // Property 'name' is missing in type 'Person' but required in type 'Dog'. 만약 Person 클래스에 name 프로퍼티가 있고, 더불어서 address 프로퍼티가 존재해도 makeNoise의 인자로 전달이 가능하다. class Person { constructor(public name:string,public address: string) { } makeNoise() { console.log('Helloooo') } } let person= new Person("Jon Snow","North") makeNoise(person) 4. Duck Typing Duck Typing에서 이름과 형태는 중요하지 않다. Duck Typing은 어떤 동작을 위해 필요한 메서드와 프로퍼티가 존재해야 하며, 보통 javascript와 같은 동적 타입 언어에서 찾아볼 수 있다. “어떤 것이 오리처럼 걷고, 오리처럼 수영하고, 오리처럼 꽥꽥거리면 그것은 아마도 오리일 것이다”라는 것에서 Duck Typing이 유래했다. 만약 어떤 객체가 어떤 행동을 하길 기대하고, 그 객체가 그 행동을 한다면 개발자는 이 객체를 사용할 수 있다. 이 객체의 모양과 타입은 전혀 중요하지않다. 아래는 person과 backAccount 두가지 객체를 포함하고 있는 자바스크립트 예제이다. 그들은 동일한 타입을 공유하지도 않고, 동일한 구조를 갖고있지도 않다. 다만 동일하게 someFn 메서드를 갖고 있을 때, invokeSomeFn이라는 someFn 메서드를 호출하는 함수의 인자로 각각의 객체를 문제 없이 전달할 수 있다. let person = { name: 'Jon', someFn: function () { console.log('hello ' + this.name + ' king in the north') }, } let bankAccount = { accountNo: '100', someFn: function () { console.log('Please deppost some money') }, } invokeSomeFn = function (obj) { obj.someFn() } invokeSomeFn(person) invokeSomeFn(bankAccount) 5. Norminal vs Structural vs Duck Norminal Typing이 가장 덜 유연하고, Duck Typing이 가장 유연하며, Structural Typing은 그 사이에 있습니다. 타입이 덜 유연할수록 타입을 명시적으로든 암시적으로든 지정해주어야하기 때문에, 더 많은 코드가 요구되고 덜 유연할 수 있지만, 가독성이 좋아지고, 에러가 덜 발생하며, 버그를 쉽게 찾아 수정할 수 있습니다. 📌 Lighthouse 측정 관련 web vital 측정을 위해 Lighthouse를 사용했는데, 아래 사진과 같이(당시에는 아래 사진보다 더 심하게 측정값이 달라졌었다.) 측정할 때마다 METRIC이 크게 달라지는 문제를 만났었다. 이렇게되면 성능 개선에 대한 Before와 After 측정이 어려웠는데, 다른 팀원들에게 물어보니 나처럼 결과값이 크게 변동하는 사람이 없었다. 어디서 이런 차이점이 발생할까하고 곰곰히 생각해보다가, lighthouse는 제어되는 환경에서 테스트를 진행한다고 햇는데, 내가 상황을 제어하여 테스트했는지에 대한 의문이 들었다. 그래서 lighthouse 측정을 실행하고 탭을 이리저리 바꾸거나(측정 대상이 되는 탭을 background로 돌림), 새로운 윈도우를 하나 띄어서 여러 탭을 만들고 유튜브 동영상에 들어가는 등 부가적인 작업을 돌리니 결과값이 달라지는 것을 확인했다. 어찌보면 너무나도 중요하고 당연한 부분인데 간과했던 것 같다. 😥 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk17 { color: #808080; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }11월 2주차
📌 이미지의 offsetTop 값 가져오기와 debounce를 이용한 최적화 상세 페이지는 텍스트 n개와 이미지 m개가 하나의 그룹을 이루어서 총 x개의 그룹이 보여진다. 그리고 각 그룹에 매칭되는 버튼들이 존재했는데, 특정 버튼을 누르면 버튼에 매칭되는 그룹으로 스크롤이 자동으로 이동해야 했다. 그래서 각 그룹의 offsetTop을 구해야했는데 이 offsetTop의 값이 이상하게 계산됐다. 참고로 offsetTop을 계산한다는 말은 scrollTo를 이용한다는 말인데, scrollIntoView를 이용하지 못하는 이유는 스크롤이 어떤 그룹의 위치에 있느냐에 따라서도 이 버튼의 상태에 영향을 줄수 있기 때문이다. 버튼이 스크롤 위치에 영향을 줄때 scrollIntoView를 이용하고, 스크롤이 버튼에 영향을 줄때 intersection observer를 이용하는 방법도 존재하지만, intersection observer 사용이 너무 복잡해지고 원래 사용 목적과도 어긋나기도했다. offsetTop이 있으면 스크롤이 버튼에 영향을 줄수도 있고, 버튼이 스크롤에 영향을 줄수도 있어서 로직 복잡도도 낮아진다고 생각했다. 다시 본론으로 돌아와서, 처음 페이지에 진입할때 가져오는 offsetTop 값과 페이지에 진입한 후 내부 로직이 다시 실행될 때의 offsetTop값이 달랐다. 여기서 내부 로직을 다시 실행시킨다함은(표현이 맞는지 모르겠다 -_-) 페이지에 진입한 후에 offsetTop을 구하는 useEffect 내부에 console.log(‘test’)를 작성하고 저장하는 것이다. 뭐가 문제일까해서 팀장님께 여쭈어봤더니 이미지가 렌더링되기 전에 offsetTop이 계산돼서 그렇다는 것을 알게되었다. next/image에서 onLoadingComplete 프로퍼티를 이용하면 해결할 수 있을 것 ‘같았다’. 구현하면서 아뿔사 했던게, onLoadingComplete를 이용하는 경우, next/image가 제공하는 lazy loading을 사용할 수 없게된다. lazy loading을 사용하는 경우, 실제 view가 보여지기 전까지 onLoadingComplete이 호출되지 않기때문이다. 일단은 next/image의 loading 프로퍼티를 eager로 만들어서 lazy loading이 안되게 구현하고, 모든 이미지들에 대해서 onLoadingComplete 프로퍼티에 offsetTop을 업데이트하는 함수를 전달했다. 여기에 성능적인 부분을 고려하여 업데이트 함수가 debounce되도록 했다. (이미지1이 onLoadingComplete이 되더라도 이미지2, 이미지3 onLoadingComplete가 호출되면 이미지1의 onLoadingComplete 로직 실행을 폐기한다.) 📌 next/image 최적화 이미지를 가져올때 query로 image transformation을 하지 않고, 고정된 크기에 따라서 이미지를 가져오는 URL path가 달라진다. next/image는, static image에 대해서 deviceSizes에 따라서 resize된 이미지를 만들어서 .next에 저장하는데, 앞서 말한 상황에서는 resize되지 않은, 동일한 크기를 갖는 이미지를 만들어서 .next에 저장한다. 불필요한 파일이 저장되는 것이다. nextjs 문서를 뒤져봐도 특정 상황에서만 deviceSizes를 가져오는 방법은 제시되어 있지 않아서, sizes 프로퍼티를 설정해서 모든 media condition에서 하나의 size만 갖도록 만들어서 이러한 문제를 해결했다. 📌 상세 페이지 리펙토링 현재 회사에서 맡고 있는 상세 페이지는 굉장히 어려운 페이지다. 어렵다고 느끼는 이유는 첫번째로는 유저가 인터랙션할수 있는 요소가 상당히 많다. 두번째로는 하나의 스크롤 핸들러 함수가 해야만하는 일이 너무 많다. 상세 페이지에는 구글 맵이 존재하고, 구글 맵 안에는 마커가 존재하는데, 마커를 누르면 스크롤 위치가 바뀌어야 하고, 스크롤 위치에 따라서 마커 위치가 바뀌어야한다. 스크롤 위치에 따라서 맨 위로가기 버튼이 보여지거나 숨겨져야하고, 스크롤 위치에 따라서 메인 이미지 대신 구글 맵으로 opacity가 바뀌어야한다. 댓글 영역으로 가기 버튼을 누르면 스크롤 위치가 바뀌어야하고, 이전과는 다르게 스크롤 위치에 따라서 마커 위치가 바뀌지 않아야한다. 복잡하다는 이야기는 여기까지하고, 리펙토링에 대해서 이야기해보면 크게 두가지 리펙토링을 했다. 1. 컴포넌트 나누기 처음 상세 페이지를 마주했을 때, 관련한 로직이 모두 하나의 컴포넌트 안에 담겨있었다. 내가 가장 먼저 한 일은 하나의 컴포넌트를 여러 개의 컴포넌트로 쪼개는 일이었다. 컴포넌트를 노드로 하는 트리 구조를 UI를 기반으로 그리고 다른 컴포넌트에 영향을 주는 상태 변화를 직선으로 표현했다. UI를 기반으로 책임을 나누고, 다른 컴포넌트에 영향을 주는 컴포넌트와 영향을 받는 컴포넌트를 최대한 가까이 배치했다. 2. 상태 관리 라이브러리에게 책임 넘기기 로직은 처음에 페이지 컴포넌트 내부에 모두 두었다. 상태 관리 라이브러리가 없었던 상태에서, 페이지 컴포넌트 내에 스크롤 핸들러 함수가 등록되는 wrapper 태그가 존재해서 어쩔 수 없었다. 그 결과 가독성이나 상태추적이 매우 어려운 상태였다. 연차가 있으신 팀장님께 여쭈었더니 로직을 Hook으로 뭉치라고 말씀해주셨다. 거대해진 Hook 두개가 만들어졌고, 상황은 크게 개선되지 않았다. 상태 관리 라이브러리가 도입되면서 이야기가 달라졌다. 사실 나는 상태 관리 라이브러리를 제대로 써본적이없다. vue 프레임워크를 사용할때 vuex를 써보기만했지(당시에도 다들 한번쯤 경험해봤듯이, 무지성으로 사용했다.) 리액트에서는 한번도 써본적이 없다. 그래서 상태 관리 라이브러리를 도입하는게 망설여졌다. 동일한 코드를 네달정도봐서 그런가 ‘현재도 괜찮은데 이걸 도입한다고해서 가독성이 개선되고 상태 추적이 쉬워질까?‘에 대한 의문도 있었다. 그림이 잘 안그려졌다. 팀장님께 여쭤봐도, 상태 관리 라이브러리 도입은 결국 또 다른 상태 추적 비용만 만들 수 있다고 말씀해주셨다. 그래서 머릿속에서는 잘 안그려지니 Before와 After를 만들기로 했다. 영향을 주는 컴포넌트와 영향을 받는 컴포넌트가 자기 자신이거나 혹은 부모 자식관계가 아니면 상태 관리 라이브러리에게 책임을 넘겼다. After를 만들고 나서도 이게 개선이 된 코드인가에 대한 의문이 존재했는데, 팀장님이나 팀원으로부터 코드가 개선되었다라는 말을 듣고 나서야 확신하게 되었다. 상태 관리 라이브러리를 유의미하게 쓴 것은 이번이 처음인 것 같다. 이러한 리펙토링 과정에서 느낀 것은, 동일한 코드를 오랫동안 보고있으면 개선 가능성에 대해서 부정적인 시각이 형성될 수 있는데, 이럴때는 Before와 After를 짜는게 가장 좋다는 것이다. 📌 네이티브앱 vs 웹앱 vs 하이브리드앱 vs 크로스플랫폼 앱 vs 프로그래시브 웹앱 네이티브 앱의 장점은 성능과 관련해서 특정 플랫폼(AOS or IOS)이 요구하는 기준을 만족해야 하기 때문에 하이브리드 앱이나 웹앱에 비해서 빠르고, 특정 플랫폼이 갖는 기능(모바일 API)을 사용할 수 있다. 또한 하이브리드 앱에 비해서 앱 스토어에 좀 더 잘 노출된다는 장점이 있다. 하지만 플랫폼에 맞는 앱 개발자를 뽑아야하고, 플랫폼에 맞는 Codebase도 두개가 존재해야 한다는 점에서 리소스가 많이 들고, 앱스토어에 배포되기 때문에 업데이트와 관련한 별도의 승인 과정이 있어서 업데이트 사항 반영이 느리다. 웹앱은 모바일 웹보다 좀 더 모바일 최적화 되어있다고한다(참고로 모바일 웹과 모바일 웹앱은 구분이 모호하다고 한다 🙄). 경우 플랫폼에 맞는 앱 개발자를 뽑거나, Codebase도 두개가 존재할 필요가 없다는 점에서 리소스가 적게 들고, 앱스토어에 배포되지 않기 때문에 업데이트와 관련한 별도의 승인 과정이 없어서 업데이트 사항을 빠르게 반영할 수 있다. 또한 브라우저를 통해서 접근이 가능하다. 하지만 특정 플랫폼의 기능을 사용할 수 없고, 네이티브앱에 비해서 느린 단점이 존재한다. 하이브리드앱은 네이티브 앱과 웹앱의 장점을 합쳐놓았다. 겉은 앱으로 감싸져있고, 내부는 웹뷰를 통해서 브라우저를 보여주는 방식이다. 웹앱처럼 플랫폼에 맞는 앱 개발자를 뽑거나 Codebase가 두개 존재할 필요가 없고, 네이티브 앱처럼 특정 플랫폼 API에 접근이 가능하다는 장점이 존재한다. 브라우저의 성능에 영향을 받으면서 네이티브앱에 비해 느리고, 웹과 앱에 대한 프로그래밍 언어적인 이해가 필요하다는 점에서 러닝커브가 높다는 단점이 존재한다. 크로스 플랫폼 앱은 하나의 언어와 한번의 개발로 IOS와 AOS 플랫폼에서 동작하는 앱을 만드는 것이다. 프로그래시브 웹앱은 모바일 웹의 한계를 브라우저의 발전을 통해서 끌어올린다. 일반적인 모바일 웹은 PWA가 되면서 ‘홈 화면에 추가’를 하면 주소창 없이 네이티브나 하이브리드 앱인것처럼 동작한다. 또한 웹의 지정된 내용들은 핸드폰에 저장되면서 통신이 안돼도 일반 웹처럼 공룡(구글 공룡게임)이 나타나는게 아니라, 웹의 오프라인 기능들이 화면에 나타난다. 안드로이드에서는 푸시 알람, 스토어 출시도 가능하다고 한다. 😮 리액트 네이티브에서 웹뷰를 띄어서 보여준다면, 크로스 플랫폼 앱이자 하이브리드 앱이라고 보면 된다고한다. https://www.upwork.com/resources/native-hybrid-web-app-differences https://www.youtube.com/watch?v=NMdnzvPsGu8 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }11월 1주차
📌 CRUD, Business Logic, Business Rule CRUD는 애플리케이션이 할수 있는 Create, Read, Update, Delete 작업을 의미하고, Business Logic은 Business Rule을 코드로 옮긴 것 입니다. Business Rule은 애플리케이션이 어떻게 동작해야 하는지를 정의하고, CRUD에 대한 제약을 만들어냅니다. Bug Tracker 애플리케이션은 버그를 Create, Read, Update, Delete한다는 점에서 CRUD 앱으로 해석될 수 있지만, 실제 내부에서는 단순한 CRUD가 발생하지 않습니다. 개발자는 특정 버그에 대해서 verified된 것인지 closed된 것인지를 표시할 수 없습니다. 오직 QA 소속만이 할수 있습니다. 프로젝트 매니저는 버그를 delete할 수 없습니다. 버그에 ‘테스트 예정’이라는 마킹을 남기기 위해서는 해당 버그에 대해서 적어도 하나의 commit이 남아있어야 합니다. closed된 버그만이 reopen 상태로 바뀔 수 있습니다. 개발자와 QA 소속 인원만이 프로젝트에 존재하는 버그를 확인할 수 있습니다. 위에 적어놓은 항목들이 Busienss Rule들입니다. 카카오톡 채팅방으로 따진다면 방장만이 사람들을 강퇴할 수 있음, 메세지를 삭제할 수 있음, 확성기를 쓸수 있음등에 대한 제약들이 Business Rule에 해당하지 않을까 싶다. What really is the “business logic”? 📌 zustand와 jotai zustand에서 jotai로 변경했다. 다른 특성들을 다 차치하고, boilerplate 코드가 적어져서 너무 편하다. zustand에서는 setter 함수(tslint 설정에 따라 다르겠지만 우리 팀에서는 setter 함수에 대한 타입까지 선언해줘야 했다.) 를 항상 적어줬는데, jotai에서는 그럴 필요가 없다. 📌 react 18이 outdated한 transient update 제거하는 특성 오픈소스 컨트리뷰션에서 만난 팀원들에게 react 18 shallow dive라는 이름으로 react 18버전에 대해서 발표를 했다. 발표하면서 멘토님으로부터 다음과 같은 질문을 받았다. 버튼을 클릭해서 counter의 숫자를 올린다고 할때, outdated한 transient update는 제거되니까 counter 계산이 건너뛰어질 수 있나요? 생각하지도 못했던 질문이었다. ㅇㅅaㅇ;;; 그래서 테스트를 위해서 다음과 같이 코드를 작성했다. 버튼을 연속으로 네번 누르면 결과가 어떻게 나올까? ; callback 함수로 업데이트하는 경우에는 사용자의 빠른 인터랙션이 어떤 연산과정과 얽히는 경우 이전 계산은 날라가버려서 숫자 1이 찍히는 것을 알수있다 (뭐 당연한 것 같기도 하고 -_-). 다만 callback 함수를 통해서 업데이트하는 경우, 어찌됐건 이전 계산 결과를 바탕으로 연산을 진행하기 때문에 숫자 4가 찍히는 것을 알수있다. 📌 jotai와 provider-less mode jotai의 경우 provider mode와 provider-less 모드가 존재한다. app 컴포넌트를 provider로 감싸면 provider 모드고 감싸지 않으면 provider-less 모드가 되는데, provider를 사용할 때 발생하는 이점은 다음과 같다. 디버깅의 목적 provider-less 모드에서는 디버깅시 리액트 컴포넌트 트리 전체에 대한 atom을 보여주지만, 만약 특정 subtree를 provider로 감싸면, 해당 provider에 존재하는 atom들만 디버깅이 가능하다고 한다. (링크) SSR시에 특정 atom이 초기값을 갖게하기 2번의 대안으로 useHydrateAtoms를 이용하는 방법도 존재하는데, Provider를 이용하는 것에 비해서 훨씬 더 깔끔하다. 공식 문서에서는 SSR 사용시에 Provider를 감싸주어야 한다고 한다. 이유는 앱이 여러 개의 instance를 가질 수 있기 때문에, Provider를 제공함으로써 각각의 인스턴스가 각각의 상태를 가짐으로써 default store에 이전에 존재하던 값이 영향받지 않도록 하기 위함이라고 한다. 공식문서, github issue 잘 이해가 되지 않는다. 🤯 📌 next/image가 해결해주는 것 부적절한 이미지 사이즈 무엇을 설정해야 next/image가 이 부분을 커버할 수 있는지 아직 이해하지 못했다. 주요 키워드로 sizes / srcset / deviceSizes / ImageSizes가 있는 듯 하다. 이미지 형식 브라우저가 WebP 형식을 지원한다면, jpg, png 파일은 자동으로 WebP 포맷으로 변환된다. 실제로 네트워크창을 열어보면 jpg 이미지 형식을 webp으로 불러오는 것을 확인할 수 있다. 너무 빠른 로드 next/image는 기본적으로 lazy-loading한다 로드 우선 순위 priority 프로퍼티 설정을 통해서 LCP 개선을 이끌어낼 수 있다. priority 관련한 내용은 아래에 별도로 기술해놓았다. CLS (Culmulative Layout Shift) placeholder를 통해서 CLS를 방지한다. placeholder는 빈 영역 또는 blur 이미지(로컬 이미지의 경우 build 타임에 base64로 인코딩한 blur 이미지 자동 생성, 리모트 이미지의 경우, build 타임에 이미지 파일 접근이 불가능하므로 base64로 인코딩된 data url 지정 필요)를 적용한다. 또한 리모트 이미지에 대해서 width와 height을 요구하는 것 역시도 CLS를 방지하기 위함이다. (layout이 fill로 적용되는 경우, width와 height을 기입하지 않아도 되는데, 그 이유는 부모의 width와 height을 따라가기 때문이다) 참고로 로컬 이미지의 경우(static import), build 타임에 width, height 계산 및 base64인코딩 후 blur 이미지 처리까지 되지만, 리모트 이미지의 경우(dynamic image), build 타임에 이미지 접근이 불가능하므로, width, height 기입 및 blur 이미지의 경우, base64로 인코딩된 data url을 지정해줘야 한다. 위 내용은 An Overview: Next.js Image Component And Its Powerful Capabilities와 Next/Image를 활용한 이미지 최적화를 참고했다. 📌 dynamic import vs static import nextjs를 읽다보면 static import와 dynamic image에 관한 이야기가 나오는데, dynamic image는 리모트의 이미지를 가져오는 것이고, static import는 로컬 이미지를 가져오는 것을 의미하는 듯 하다. 링크 📌 Image 컴포넌트 priority 프로퍼티 LCP(Largest Contentful Paint) 요소가 되는 이미지에는 priority 프로퍼티를 추가해야한다. priorty 프로퍼티 통해서 높은 우선순위와 함께 preload 된다. 스크롤 없이(above the fold, 아무런 행동을 하지 않은 기본 화면) 보여지는 이미지에만 priorty 프로퍼티를 적용해야한다. nextjs 공식문서 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }10월 4주차
📌 기획자님 이해시키기 새로 들어오신 기획자분께서 다음과 같이 질문을 해주셨다. 코드를 짜는게 AOS, IOS에 상관없이 구현이 되는거에요? 뭘 하구있냐면, 버튼쪽 구현하는걸 보고 있는데, 보니까 버튼 구현할 때 파라미터로 컬러값 넣으면 안드로이드는 버튼 백그라운드가 바뀌고 IOS는 텍스트 컬러값이 바뀌더라고요? 그래서 혹시 저희가 개발하는거를 IOS 빌드할 때 그대로 입히나(?)하는 비개발자의 질문…입니다. 이해가 되시나요? react-native의 특정 코드가 실행되는 플랫폼에 의존하는 것에 대해서 여쭤보신 것 같다(웹뷰로 구현하기 때문에 react-native에 대해 딥하게 들어가진 않아서 구체적으로 어떤 코드가 어떻게 영향을 받는진 잘 모르겠지만). 설명하는 것을 좋아하는 나에게 있어서 이 질문은 소소한 행복으로 다가왔다.🤩 어떻게 해야 쉽게 이해시킬 수 있을까 고민하다가 나온 답변은 아래와 같았다. 리액트 네이티브 + 리액트 두개를 이용해서 개발하고 있는데요. 리액트 네이티브만 사용해서 개발한다면 OO님이 말씀하신대로 컬러값을 넣었을 때 IOS나 AOS에 따라서 버튼 색깔이 적용되는 모습이 다를거에요. 그런데 리액트 네이티브와 리액트 두개를 이용하는 이유는, 이렇게 플랫폼에 따라서 다르게 취급해야 하는 코드를 작성하는게 너무 귀찮고 번거로우니까 그런거에요. 리액트 네이티브가 하는 역할은 우리가 만든 프로젝트를 앱을 통해서 보여줄 수 있게 만드는 역할밖에 없어요. 마치 어떤 영화 카세트 테이프를 넣느냐에 따라서 티비에 나오는 영화가 달라질 수 있잖아요? 여기서 티비가 리액트 네이티브고, 카세트 테이프가 리액트인거에요. 웹 화면에 대한 정보를 실제로 담고 있는 녀석이 카세트 테이프가 되는거죠. 그리고 단순한 웹 화면은 AOS와 IOS에 따라서 달라지지 않기 때문에, 앞서 말한 리액트 네이티브의 취약점을 보완할 수 있게 되는거에요. 물론 여기에는 카세트 테이프를 앱이 아닌 웹으로도 바로 접근할수 있다는 점은 생략됐지만,,, 나중에 천천히 알려주는 걸로 🫠 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }10월 3주차
📌 소프트웨어에서 resolve의 의미는 대체 무엇일까? 수능 영어를 공부할 때 이 녀석의 뜻은 분명 ‘해결하다’였다. 그런데 개발 분야를 공부하면서 관련 영어 문서에 이녀석의 단어를 ‘해결하다’로 해석하려고 하면 잘 해석되지 않는다. 이번에 GraphQL에서 argument를 공부하면서 또 마주쳤다. These are passed into the server-side execution of this field, and affect how it’s resolved resolve의 뜻을 “해결하다”라고 생각하고 위 문장을 해석해보면, argument들은 해당 필드의 서버 실행시 전달되며, 이것이 어떻게 해결될지에 영향을 끼칩니다. 정도인데, 해석이 깔끔하지 않다. 대체 뭘 해결한다는거지 ㅡ,.ㅡ 🤯 이에 대한 궁금증을 What does ‘Resolving’ mean in programming를 통해서 어느정도 해소할 수 있었다. resolve는 의존성 주입(resolve an implementation to an interface), 패키지 관리자(resolve packages dependencies), 웹(resolve a hostname)등 다양한 곳에서 사용되고 있다. 그렇다면 resolve의 의미와 통하면서 쉬운 단어이는 Convert나 Transform, Get을 두고 resolve가 많이 쓰여지는 것일까? 라는 질문의 글이다. 답변은 다음과 같다. (오의역 매우 주의 🙇🏻♂️) resolve a hostname은 hostname과 정보를 연결해주는 repository에 대한 질의가 포함되어 있다. repository 없이는 이 정보를 알수가 없다. google.come의 경우 특정한 IP로 resolve되지만, 이 IP는 할당될 당시에 우연히 할당된 숫자일 뿐이다. 이 IP는 현재와는 전혀 다른 숫자를 가지고 있었을 수도 있고, 쿼리나 IP 등록과 관련된 모든 정보를 저장하고 있지 않는 이상 이러한 google.com에서 IP로의 변환이 불가능하다. 유사하게 resolve pacakge dependencies는 설치되지 않은 패키지를 설치하는 것을 요구한다. 하지만 설치되지 않은 부분의 이름을 알기에는 충분하지 않고, 실제로는 설치되지 않은 패키지가 갖고 있는 내용물을 얻어야 하는데, 내용물의 이름으로부터 어떤 컨텐츠인지 예측할수가 없다. 예를들면, QMail은 메일 프로그램처럼 들리지만, 설치 없이는 이 프로그램의 특성을 이름으로부터 예측할 수 없다는 것이다. 왜냐하면 이름은 컴파일된 프로그램에 비해서 너무나도 적은 정보를 전달하기 때문이다. 그러므로 문맥이 필요없는 단순 변형 상황에서 “resolve”라는 단어를 쓸 수가 없다. “resolve”는 추가적인 정보가 필요한 상황에서 사용하려고 예약된 언어이다. 그러니까… resolve는 변환과 획득의 의미가 맞지만, 단순 변환이 아닌 hostname에서 ip로의 변환, 단순 획득이 아닌 package를 설치하는 상황처럼, 문맥이 필요한 상황에서만 resolve를 쓰는 것 같다. 📌 nx에 husky와 lint-staged 셋업하기 nx를 팀장님께서 셋업해주시고, 이어서 내가 husky와 lint-staged를 셋업하기로 했다. 근데 토나온다 🤯. husky, lint-staged, nx 어디서도 각 라이브러리를 동시에 쓸때 셋업하는 방법에 대한 가이드가 나오지 않는다. 다행히 nx 레포 issue에 nx에서의 husky와 lint-staged 셋업에 관한 내용이 파편처럼 흩어져 있다. 그중에 #869 issue가 도움이 됐다. 하지만 이게 끝이 아니다. 위 링크대로 셋업을 하면 commit시 린팅하는 시간이 굉장히 오래 걸리는 문제에 부딪히게 된다. 여기서 첫번째 동영상이 문제 상황이다. 원인이 support-color와 colorette가 tty.isatty(1)을 실행하는데, 이게 git hook으로 실행하면 false가 나오서 실행이 안되고, lint-staged를 직접 실행하면 true가 나와서 실행이 된다고 한다. 어찌됐건 이 문제에 대한 해결법도 존재했다. 다만 내 프로젝트에서는 아래와 같은 콘솔과 함께 동작하지 않는다. 어이x… .husky/pre-commit: line 4: /dev/tty: No such device or address 결국 exec 1> /dev/tty의 정체를 해석하기 위해서 linux 명령어도 찾아보고 이래저래 삽질하다가, Why “No such device or address” when open /dev/tty in the first process?에서 영감을 얻어 tty를 console로 바꾸니 정상적으로 동작하는 것을 확인했다. (무친…) 원인이 프로세스를 위한 제어 터미널을 갖기 위해서는 real terminal을 가져야 하는데, /dev/tty는 real terminal이 아니다…라고 하는데, 시간상 더 딥하게 들어가진 못했다. 🏳️gg 나랑 동일한 문제를 겪고 계신분들을 위해서 영어로 답변을 달아놓긴 했는데, 과연 따봉이 달릴지 리버스 따봉이 달릴지 ㅋ.ㅋ; 📌 nx에 husky와 lint-staged 셋업하기 (이어서) 이제 husky와 lint-staged가 잘 동작하는 것 확인했다. commit 잘 되겠지? 하는 순간 다음 에러에 또 부딪힌다. (하늘이시어… 🤯) Failed to load plugin ‘react-native’ declared in ‘apps\project\react-native-multiple-image-picker-main\package.json » @react-native-community/eslint-config’: Cannot find module ‘eslint-plugin-react-native’ 요 문제는 eslint-plugin-react-native를 설치함으로써 해결했고, 그 다음으로 등장하는 다음 에러는(이제는 에러가 안나오는게 이상한 수준) Failed to load plugin ‘flowtype’ declared in ‘apps\project\react-native-multiple-image-picker-main\package.json » @react-native-community/eslint-config#overrides[0]’: Package subpath ‘./lib/rules/no-unused-expressions’ is not defined by “exports” in D:\Source\Naranggo\node_modules\eslint\package.json 여기에서는 버전 문제라고 말하지만, 버전 문제가 아니었다. 문제의 정확한 원인을 모르겠어서 이리저리 뒤져보다가, react-native-multiple-image-picker-main 경로의 package.json에 eslintConfig 설정이 root:true와 함께 설정되어 있는 것을 발견했다. 이전까지의 지식으로는 하위 폴더에 eslint 설정이 존재하는 경우, 적용 우선 순위에 문제가 있을 뿐 이와 같은 에러가 발생하지 않는 것으로 알고있었는데, 왠지 찝찝해서 eslintConfig를 삭제하니까 정상적으로 동작했다. 라이브러리라서 직접 건들일 일도 없기 때문에 그냥 최상위 eslint 파일에 의존하는 것으로… 📌 The ref value ‘intersectionObserverRef.current’ will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy ‘intersectionObserverRef.current’ to a variable inside the effect, and use that variable in the cleanup function. intersectionObserver 훅을 만들던 중 아래와 같은 경고에 부딪혔다. 처음부터 위와 같은 코드를 작성했던 것은 아니다. 처음 코드는 원래 아래와 같았다. const useIntersectionObserver = (callback, options) => { const intersectionObserverRef = useRef(null) useEffect(() => { intersectionObserverRef.current = new IntersectionObserver( callback, options, ) return () => intersectionObserverRef.current?.disconnect() }, [callback, options]) return intersectionObserverRef.current } 위에 인자로 전달되는 callback과 options는 따로 메모된게 아니라 useIntersectionObserver 호출시에 생성되어 전달되고 있다. 그렇기 때문에 리렌더링이 발생하여 callback과 options가 다시 만들어지면 useEffect 내부 로직이 다시 돌아가는 문제가 발생한다. 그래서 앞선 경고 상황처럼, useRef를 초기화할때 다음과 같이 intersectionObserver 인스턴스를 만들려고 했다. const useIntersectionObserver = (callback, options) => { const intersectionObserverRef = useRef( new IntersectionObserver(callback, options), ) useEffect(() => { return () => intersectionObserverRef.current?.disconnect() }, []) return intersectionObserverRef.current } 그리고 발생한 경고인데, 경고의 의미가 잘 납득되지 않아서 리서치해봤다. ref가 가리키는 대상이 DOM 요소와 관련이 없다면 이 경고를 크게 신경쓸 필요 없다. 경고가 의미하는 바는, ref가 가리키는 대상이 DOM 요소라면 useEffect 내부의 변수에 저장하고 사용하라는 의미이다. 왜냐하면 뒷처리 함수가 실행될 때 컴포넌트의 변화가 있을 수 있기 때문이다. codesandbox를 보면, 1초 전에는 첫번째 useEffect 내부의 mount1이 잘 출력되는 것을 볼수 있으나, 1초 후에 Box가 unmount 됐을 때는 ref 요소를 찾을 수 없게된다. 하지만 두번째 useEffect처럼, 내부의 변수에 저장하고 사용한다면, 정상적으로 DOM 요소가 나오는 것을 알수 있다. 📌 Cannot assign to ‘current’ because it is read-only property 다음과 같은 코드에서 에러가 발생한다. const ComponentFunction = () => { const stringRef = useRef<string>(null); useEffect(() => { ref.current = 'hello'; }, []) } 이유는 null로 ref를 초기화할때, 제네릭타입으로 null 타입을 전달하지 않아서, 불변의 ref 객체가 만들어졌기 때문이라고 한다. ref 타입과 관련해서는 별도의 문서로 정리해야겠다. 🤔 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk8 { color: #CE9178; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }10월 2주차
📌 nest 공부 시작 저번 주 토요일에 기숙사를 들어갔다. 덕분에 출퇴근 시간이 1시간에서 5분으로 단축됐다. 책상 앞에 앉아 있을 수 있는 시간이 늘어났는데, 뭘 하면 좋을까 하다가 회사에서 백엔드 인력도 부족해서 프로젝트 진행이 안되기도 하고, 이전에 사놓고 안들어놓은 nestjs 강의도 있어서 해당 강의를 수강하며 백엔드에 shallow dive 해보려고 한다. 📌 listens on port 3000 for connections express 공식 문서에서 listens on port 3000 for connections 라는 문장을 봤다. 낯이 익어서 언제 봤지 했는데, TCP 통신의 3 way handshake에서 본것 같다. 벌써부터 지식이 확장되는 느낌이 든다. 끌끌,,, 📌 command + J 터미널 여는 단축키를 몰라서 맨날 command palette 열고, View Toggle 터미널의 약자인 vt를 입력해서 띄우곤 했는데, 오늘 nest 강의에서 command + J를 통해 들어갈 수 있다는 것을 배웠다. 이로써 생산성이 조금 더 향상되었다. 끌끌,,, 📌 Strong and Weak Typing 타입스크립트를 공부하면서 Strong Type과 Weak Type이라는 단어를 심심치 않게 마주친게 기억나서 리서치했다. 이 두 용어 자체는 굉장히 모호해서, 다음 네가지로 해석될 수 있다고 한다. strong은 static을 의미하기도 하지만, 대부분의 사람들은 static의 정의에 동의하기 때문에 static을 사용하는게 좋습니다. strong은 데이터 타입간에 암묵적인 형변환이 발생하지 않음을 의미합니다. 예를들면, 자바스크립트는 Weak Typing으로 “a” - 1을 허용합니다. 하지만 대부분의 언어의 경우, 1 - 1.1과 같은 정수에서 소수로의 변환과 같이, 암묵적 형변환의 수준이 정해져 있습니다. strong은 허용되는 형변환과 허용되지 않는 형변환의 기준을 나누는데, strong, 그러니까 이 기준에는 명확한 기준이 없으며 개개인에 따라서 다릅니다. strong은 언어의 타입 규칙을 어길 수 없음을 의미하기도 합니다. strong은 memory-safe함을 의미합니다. C가 좋은 예입니다. 만약 xs가 4개의 숫자를 저장하는 배열이라고 해도, C는 xs[5]나 xs[1000]을 허용합니다. 위처럼 뭐 다양하게 사용되는 것 같은데, 타입스크립트에서 마주치는 strong과 weak의 쓰임은 2번이 아닐까 싶다. Strong and Weak Typing 📌 Life Cycle Script 아래 script에서 npm run start를 했을 때, prestart와 build가 먼저 실행된다고 한다. { "scripts": { "prestart": "npm run build", "build" "tsc", "start": "node dist/app.js" } } prestart가 먼저 실행되면 npm run build가 실행되기 때문에 tsc가 실행되는 것 인정. 그런데 prestart가 왜 먼저 실행될까? 하던 의문이 들던차에, Life Cycle Script의 존재에 대해서 알게되었다. 이름 그대로 start가 실행되기 전에 prestart가 실행되는 것인데, git life cycle과 비슷하게 바라보면 될듯하다. @___@ 📌 tsc-watch package nest강의에 셋업에 대한 추가적인 설명이 없어서 리서치했다. tsc-watch는 타입스크립트를 위한 nodemon이라고한다. nodemon은 소스 코드에 어떠한 변경이 발생했을 때 자동으로 서버를 다시 시작하는 유틸리티를 의미한다고 한다. tsc-watch는 타입스크립트 컴파일러를 –watch 플래그와 함께 실행하고, 추가적으로 컴파일의 상태(성공, 실패와 같은)에 따른 COMMAND 실행도 가능하게 한다고 한다. { "scripts": { "start:dev": "tsc-watch --onSuccess \"node dist/app.js\"", } } 위 명령어는 타입스크립트 컴파일이 성공했을 때, node dist/app.js 명령어를 실행하는 스크립트로 볼수있다. 📌 Signleton 패턴 오픈소스 컨트리뷰션을 하면서, commit에 따른 code diff view 코드를 구현하기 위해서 git lens 코드를 리버스 엔지니어링한 경험이 있다. 그때 당시에 어떤 클래스가 자기 자신의 인스턴스를 자기 자신의 정적 메서드에서 호출하는 코드를 본적이 있었다. 촉으로 ‘이거 패턴이다! 패턴같은데!!’라고 내적 호기심의 폭발과 함께 개발자 톡방에 물어봤지만 읽씹당했다🤮. 당시에 “언젠가 너 또 만나겠지?” 하고 있었는데, 이번에 nest 공부하면서 만나게 됐다. 얼마나 반가운지 ㅋㅋㅋㅋ 기념으로 creational에 정리해 두었다. .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk6 { color: #D7BA7D; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }creational
본 글은 refactoring.guru, sbcode, patterns.dev의 내용을 참고, 번역했습니다. 📌 Singleton 하나의 클래스가 하나의 인스턴스만 갖는 것을 보장하고, 이 인스턴스를 전역에서 접근할 수 있도록 합니다. Singleton 패턴은 다음 두가지 문제를 해결해줍니다. 첫번째는 하나의 클래스가 하나의 인스턴스만 갖도록 합니다. 왜 클래스가 가질 수 있는 인스턴스의 개수를 제한하려는 것일까요? 이는 공용으로 사용되는 자원의 접근을 제한하기 위함입니다. 가령 데이터 베이스를 연다거나, 로깅 컴포넌트를 사용하는 등이 좋은 예가 될수 있습니다. 구현 원리는 처음 생성자를 호출할 때는 새로운 인스턴스를 만들어주고, 이후에 생성자를 호출할 때는 기존의 인스턴스를 리턴해주는 방식입니다. guru에서는 일반적인 생성자를 호출해서 Singleton 클래스를 만들 수 없다고 말하지만, sbcode에서는 생성자를 호출해서 Signleton 클래스를 구현하고 있습니다. 두번째는 전역에서 접근 가능하다는 점입니다. 앞서 설명한 내용과 동일합니다. 처음에 생성자를 호출할 때는 새로운 인스턴스를 만들고, 이후에는 기존의 인스턴스를 리턴하는 방식이, 전역 어디서든 첫 인스턴스에 접근 가능하다는 것입니다. 전역 변수처럼요. 하지만 전역 변수와는 다르게 overwrite의 문제가 존재하지 않습니다. UML 다이어그램은 아래와 같습니다. 앞서 이야기 했던 것 처럼, guru에는 생성자를 호출해서 Singleton 클래스를 만들 수 없다고 이야기하지만, sbcode에서는 생성자를 호출해서 Singleton 클래스를 만들고 있습니다. 각각의 사이트가 어떤 코드를 제시했는지 확인해볼까요? 먼저 guru 입니다. class Singleton { private static instance: Singleton private constructor() {} public static getInstance(): Singleton { if(!Singleton.instance) { Singleton.instance = new Singleton() } return Singleton.instance; } } function clientCode() { const s1 = Singleton.getInstance() const s2 = Singleton.getInstance() if(s1 === s2) { console.log('Singleton works') } else { console.log('Singleton fails') } } clientCode() 정적 메서드를 만들어서 호출해야지만 instance를 만들거나 얻을 수 있습니다. 다음은 sbcode 입니다. class Singleton { static Instance: Singleton id: number constuctor(id: number) { this.id = id if (Singleton.instance) { return Singleton.instance } Singleton.instance = this } } const OBJECT1 = new Singleton(1) const OBJECT2 = new Singleton(2) console.log(OBJECT1 === OBJECT2) // true console.log(OBJECT1.id) // 1 console.log(OBJECT2.id) // 2 둘 다 Singleton 패턴을 잘 구현하고 있다. 지극히 개인적으로는 guru의 경우 new 키워드를 호출하는게 아니라 getInstance를 호출함으로써 new 호출을 하는, Singleton 클래스가 아닌 클래스들과 구분이 된다는 장점이 있지만, constructor에 private이라는 타입 시스템의 접근 수준 제한자가 사용됐기 때문에, 런타임에 Singleton 클래스의 역할을 잃을 가능성이 존재하는 것 같다. 후자의 경우, 이러한 문제점은 없지만 new 키워드를 사용해야 한다는 단점이 있다. 추가적으로, 위 두 코드를 통해서 왜 전역 객체처럼 overwrite 상황이 안펼쳐지는지 알수있다. 장점은 앞서 말한 것 처럼 (1) 하나의 클래스가 하나의 인스턴스만 갖는 것을 보장하고, (2) 전역에서 접근 가능하다. 단점은, (1) 단일 책임 원칙이 위배되고, (2) 멀티 스레드 환경에서는 Singleton 인스턴스가 여러개 생성될 수 있으며, (3) 프로그램을 구성하는 컴포넌트가 서로를 너무 잘 알게되고, (4) 단위 테스트가 어려워진다. 단점에 대해서는 배경 지식이 요구되어 추후 더 정리하려고 한다.🤮 Singleton 패턴의 사례는 게임 경기와 리더 보드를 생각하면 좋을 듯 하다. 각각의 게임들은 각각의 클래스로부터 인스턴스를 만들지만, 각 게임들은 하나의 리더 보드를 공유한다. 구체적인 UML과 코드는 sbcode를 참고하도록 하자. .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk3 { color: #6A9955; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }mapped type 마스터하기
📌 타입스크립트에서 mapped type은 왜 사용될까요? mapped type은 어떤 타입을 기반으로 타입을 선언해야 할때 유용합니다. // 현재 유저의 설정 값 type AppConfig = { username: string, layout: string, }; // 현재 유저가 설정 값 변경을 허용 했는지 여부 type AppPermissions = { changeUsername: boolean, changeLayout: boolean, }; 위 예제의 문제는 AppConfig와 AppPermissions간에는 AppConfig에 새로운 필드가 추가되면, AppPermissions에도 새로운 필드가 추가돼야하는 암묵적인 관계가 형성되어 있습니다. 이 둘의 관계를 프로그래머가 숙지하고 있으면서 필드가 추가될 때 양쪽을 직접 업데이트 하는 것 보다, 타입 시스템이 이 관계를 알고 있어서 알아서 업데이트 해주는 방향이 더 낫습니다. mapped type의 구체적인 개념에 대해서는 아래에서 더 알아보기로 하고, 위 예제를 mapped type을 이용해서 수정하면 아래와 같아집니다. type AppConfig = { username: string, layout: string, } type AppPermissions = { [Property in keyof AppConfig as `change${Capicalize<Property>}`]: boolean; } 우리는 Property와 keyof 연산자 사이의 in을 통해 mapped type이 사용되었음을 알수 있습니다. 위 코드에서는 타입 시스템이 AppConfig와 AppPermissions의 관계를 관리하기 때문에, AppConfig에 새로운 필드가 추가될 때마다 개발자가 직접 AppPermissions에 추가해줄 필요가 없어졌습니다. 📌 mapped type의 코어 개념 mapped type의 코어 개념에는, map, indexed access type, index signature, union type, keyof type operator 등이 있습니다. 해당 내용을 따로 기술하진 않겠습니다. 📌 mapped type의 사용 예제와 해석 사용 예제를 이해하기 전에 mapped type의 기본 구조에 대해서 한가지만 알고 갑시다. [P in keyof T]: T[P]; 위 코드에서 P는 유니온 타입 keyof T를 구성하는 string literal type을 나타냅니다. 그리고 string literal type P는 T[P] 타입을 갖습니다. 이러한 이해를 바탕으로 다음과 같이 전자기기의 manufacturer와 price에 대한 정보를 갖는 타입이 있다고 가정합시다. type Device = { manufacturer: string, price: string, }; 그리고 각 Device의 프로퍼티는 인간이 읽을 수 있는 데이터의 형태로 변환돼야 한다고 가정해봅시다. 그리고 당연히 그에 따른 타입 역시도 필요하게 되는데, 이때 mapped type을 이용할 수 있습니다. type DeviceFormatter = { [key in keyof Device as `format${Capitalize<Key>}`]: (value: Device[key]) => string; } 참고로, 문서에 설명은 안되어 있지만 Capitalize<Key>의 타입 정의는 다음과 같지 않을까 싶습니다. type Capitalize<Key> = (word: Key) => string; 어찌됐건 앞선 DeviceFormatter의 코드를 쪼개어 해석해 봅시다. Key in keyof Device는 keyof 타입 연산자를 이용해서 Device 타입의 키들로 구성된 union 타입을 만들어냅니다. 그리고 이를 index signature 안에 넣어서 Device의 모든 프로퍼티를 순회하며 DeviceFormatter의 프로퍼티에 매핑시킵니다(Device 프로퍼티 타입을 이용해서 DeviceFormatter의 프로퍼티 타입을 만드는 것 입니다). format${Capitalize<key>}는 프로퍼티 이름을 x에서 formatX로 변경하기 위해서 key remapping과 template literal type을 사용한 것입니다. 여기서 key remapping은 mapped type을 사용할 때, as를 이용해서 키를 다시 매핑시키는 것을 의미합니다. template literal type은 자바스크립트에서 사용하던 template literal과 동일합니다. 기존의 문자열과 데이터를 이용해서 새로운 문자열을 만드는 것인데, 이를 타입을 위해서 사용할 뿐입니다. 결과적으로 DeviceFormatter가 만들어내는 타입은 다음과 같습니다. type Device = { manufacturer: string, price: string, }; type DeviceFormatter = { formatManufacturer: (value: string) => string, formatPrice: (value: number) => string, }; 만약 Device에 releaseYear 필드를 개발자가 추가한다면, DeviceFormatter 필드는 타입 시스템이 추가할 것입니다. type Device = { manufacturer: string, price: number, releaseYear: number, }; type DeviceFormatter = { formatManufacturer: (value: string) => string, formatPrice: (value: number) => string, formatReleaseYear: (value: number) => string, }; 📌 제네릭 타입을 이용해서 재사용 가능한 mapped type 만들기 앞선 Device에 이어서 다음과 같은 Accessory에 대한 타입 정보도 만들어야 한다고 가정해 봅시다. type Accessory = { color: string, size: number, }; 그리고 앞선 Device처럼 Accessory의 프로퍼티를 기반으로 한 새로운 객체를 만들어야 한다고하면, 다음과 같이 구현할 수 있을 것 입니다. type AccessoryFormatter = { [Key in keyof Accessory as `format${Capitalize<Key>}`]: (value: Accessory[Key]) => string; }; 앞선 DeviceFormatter와의 차이점은 오직 참조 대상이 Device에서 Accessory로 바뀌었다는 것 입니다. 우리는 DeviceFormatter와 AccessoryFormatter라는 중복된 코드를 작성하는 것이 아닌, 제네릭 타입을 이용해서 DRY한 코드를 작성할 수 있습니다. type Formatter<T> = { [Key in keyof T as `format${Capitalize<Key & string>}`]: (value: T[Key]) => string; } 그리고 DeviceFormatter와 AccessoryFormater는 다음과 같이 정의할 수 있습니다. type DeviceFormatter = Formatter<Device>; type AccessoryFormatter = Formatter<Accessory>; 📚 참고문헌 Mastering mapped types in TypeScript mapped types in TypeScript .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }Jamstack
본 글은 What is Jamstack의 내용을 참고, 번역했습니다. 📌 Jamstack에 대한 간단한 소개 Jamstack은 웹 개발을 위한 프레임워크나 기술이 아닌 아키텍쳐를 의미하며 빠르고, 안전하고, 확장성있는 웹사이트 개발이 가능하도록 합니다. 핵심적인 원리는 pre-rendering과 decoupling이며, 주된 목표는 서버의 로드를 가능한한 클라이언트로 옮기는 것입니다. 📌 Pre-rendering 요청이 들어왔을 때 마크업을 만드는 것이 아닌, 빌드 과정에서 마크업을 미리 만들어 놓는 것을 의미합니다. 이 덕분에 CDN으로부터 사이트를 전달받을 수 있게되어, 서버 운용의 비용과 복잡성, 위험성을 줄일 수 있습니다. Gatsby, Hugo, Jekyll과 같은 유명한 정적 사이트 생성 툴들이 존재하기 때문에, 많은 개발자들은 이미 Jamstack 개발자가 되기 위한 도구에 익숙합니다. 📌 CMS(Contnent Management Systme)란? decoupling에 대해서 설명하기 전에, CMS에 대한 이해가 선행되어야 합니다. CMS는 유저가 기술에 대한 이해나 코드 작성 없이 웹 사이트를 생성, 배포, 관리할 수 있는 웹사이트입니다. CMS는 Tranditional CMS와 Headless CMS 둘로 나뉩니다. Traditional CMS는 하나의 bucket안에 사이트 생성에 필요한 모든 구성 요소(데이터 베이스, 컨텐츠, 프론트엔드 템플릿 등)가 다 들어있습니다. 크게보면 frontend와 backend가 같이 있는, monolithic(모든 것이 하나에 다 들어가 있는)한 구조로, 각 구성 요소들이 강하게 coupling 되어 있어서, 성능이나 기능 구현에 있어서 제약이 있었습니다. 그리고 Traditional CMS에서 frontend(head)를 분리시킨 것이 Headless CMS입니다. frontend agnostic합니다. Traditional CMS에서는 content가 같은 bucket 안에 있기 때문에 바로 접근이 가능하지만, Headless CMS는 그렇지 않습니다. CMS 밖에 있는 frontend framework는 API를 이용해서 CMS의 content에 접근이 가능합니다. Headless CMS는 onmichannel 하다는 장점이 있습니다. omni는 ‘모든’을 의미하는 접두사로, omnichannel은 모든 채널 정도로 읽으면 될듯합니다. Headless CMS를 사용하는 경우, 컨텐츠가 어디서, 어떻게 보여질지 걱정하지 않아도 됩니다. 웹사이트, 모바일폰, 스마트와치, AR/VR 등, Headless CMS를 사용한다면 컨텐츠를 전달할 수 있는 채널은 무궁무진 합니다. 📌 Jamstack vs 고전적인 CMS Jamstack의 경우 앞서 설명한 것처럼 고전적인 CMS가 갖는 문제를 해결하기 위해서 bucket안의 구성 요소들을 최대한 분리(decoupling)했습니다. bucket 안의 구성 요소들을 최대한 분리하여 의존성을 낮추고, 개발자로 하여금 기술 선택에 있어서 자유를 주었습니다. 기술 선택에 있어서 Headless CMS도 하나의 선택지가 될수 있는 것이죠. 또한 CMS와는 다르게, 유저는 서버에서 만들어진 마크업이 아닌 CDN으로부터 캐싱된 마크업을 보게됩니다. 📌 웹 개발 히스토리와 자바스크립트의 역할 웹 개발 히스토리와 자바스크립트의 역할을 이해하면 Jamstack을 이해하는데 도움이 됩니다. Jamstack은 오늘날에는 인기가 많지만, 10년 전까지만해도 인기가 없었습니다. 초창기 웹사이트들은 단순히 정적인 HTML을 보여주는 것에 불과했습니다. 하지만 인터넷과 이를 둘러싼 기술들이 발전하면서 웹사이트는 더 많은 일을 할수있게 되었고, 이러한 변화로 유저는 더이상 정적인 HTML이 아닌 맞춤형 컨텐츠를 담은 HTML을 받을 수 있게 되었습니다. 다만 여기에는 한가지 문제가 존재했습니다. 맞춤형 컨텐츠를 만들기 위해서 서버의 로드가 자연스럽게 커지게 되었고, 자연스럽게 기존의 정적인 HTML보다 화면에 보여지기까지 더 오랜 시간이 요구되어 사용자 경험이 떨어지게 되었습니다. 불행하게도 당시 이를 해결할 방법이 마땅히 존재하지 않았지만, 자바스크립트의 등장 및 발전과 함께 이를 해결할 수 있게 되었습니다. 자바스크립트를 이용해서 페이지가 로드된 이후에 페이지를 동적으로 수정할수 있게 되었습니다. 브라우저는 더 이상 문서 뷰어가 아닌, 웹사이트 컨텐츠가 어떻게 보여지고 동작할지를 결정하는 복잡한 작업들을 처리할수 있게 되었습니다. 이러한 발전은 모든 웹사이트에 엄청난 혜택을 가지고 왔는데, 가장 큰 혜택은 서버의 로드를 클라이언트로 옮길 수 있게 된 것입니다. 📌 Jamstack을 사용하는 이유 고전적인 웹사이트 개발 방법 대신 Jamstack을 사용하는 이유는 서버의 부하를 줄이기 위해서입니다. 이로인해서 얻게되는 이득은 다음과 같습니다. — 빠른 성능 CDN을 통해서 pre-built된 마크업과 asset을 전달합니다. — 안정성 서버나 데이터베이스의 취약성에 대한 걱정이 없습니다. — 낮은 가격 정적 파일의 호스팅 비용은 싸거나 혹은 무료입니다. — 더 나은 개발자 경험 monolithic architecture에 묶일 필요 없이, 프론트 엔드 개발자는 프론트 엔드에만 집중할 수 있습니다. — 확장성 방문자가 많아지는 경우, CDN을 통해서 해소가 가능합니다. 📌 Jamstack에서 Javascript Jamstack에서 자바스크립트는 컨텐츠를 보여주고 사용자 경험을 향상시키는 역할을 합니다. 그리고 J가 자바스크립트를 의미하지만, 꼭 자바스크립트일 필요는 없습니다. 선호에 따라서 Ruby, Python, Go와 같은 언어를 사용해도 좋습니다. 📌 Jamstack에서 API 초기 API는 웹 개발의 과정과 작업 흐름에 맞게 발전해왔습니다. 이 말은 즉, API가 대부분 서버 사이드에서 이용되었다는 의미입니다. 하지만 자바스크립트의 발전과 함께, 브라우저에서 자바스크립트로 실행될 수 있는 웹 API가 만들어지기 시작했습니다. 이것은 Jamstack 아키텍쳐가 만들어질 수 있는 큰 원동력 가운데 하나였습니다. 웹 API와 함께 서버가 수행하던 무거운 작업들이 모두 클라이언트 사이드로 옮겨갔고, API가 점점 더 많은 일을 할수 있게되면서 API를 microservice로 이용하려는 움직임이 일어났습니다. microservice는 다른 서비스에 의존하지 않고 특정한 기능을 수행하는 작은 코드조각들로, 각 microservice 들은 독립적으로 작업되지만 결국 매끄럽게 통합 및 연결되어 특정 웹 기능들 전달하는 서비스들의 아키텍쳐를 만들어냅니다. microservice에 관한 내용은 마이크로서비스 아키텍처. 그것이 뭣이 중헌디?을 참고하면 좋을 듯합니다. 📌 Jamstack에서 Markup HTML도 오랫동안 존재했지만, HTML의 주요한 역할은 여전히 컨텐츠와 뼈대를 화면에 보여주는 것이었습니다. 이것은 Jamstack 사이트에서도 달라지지 않았습니다. 다만 HTML이 서빙되는 방식이 달라졌습니다. 클라이언트의 모든 요청에 맞게 서버에서 HTML을 만들어 내는 것 대신, 캐싱된 HTML에 의존하기 시작했습니다. 정적 사이트 생성기와 같은 빌드 툴이 마크업을 미리 만들어서 CDN에 전달하기 때문에, 서버는 더이상 클라이언트 요청에 대해서 실시간으로 작업할 필요가 없어졌고, 오직 content나 asset에 변경이 있을때만 작업하도록 바뀌었습니다. 📚 참고 문헌 Jamstack Realizing the Potential of the API in Jamstack Traditional CMS vs Headless CMS (1) Traditional CMS vs Headless CMS (2) Traditional CMS vs Headless CMS (3) .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }10월 1주차
📌 쓸만한 extension 발견 monorepo 폴더에서 사용하면 괜찮을 것 같은 extension을 발견했다. monorepo는 root 폴더에서 npm i를 한다음 특정 패키지로 이동해야 할때 command + O를 눌러서 경로를 찾아 이동한다. 그런데 ‘Open Folder Context Menus for VS Code’라는 extension을 사용하면 아래와 같이 폴더에서 오른쪽 클릭을 했을 때 새로운 workbench에서 열것이냐(Open New Workbench Here), 아니면 현재 workbench가 해당 폴더로 이동할 것이냐(Reopen Workbench Here)에 대한 선택을 할수있게되어, 경로를 일일히 찾아들어가지 않아도 된다. 📌 4200 포트 대체 정체가… 🤮 최근에 Nx를 이용해서 monorepo를 셋업했다. Nx에서 nextjs의 로컬 서버 기본 포트 번호가 4200번이다. 그리고 네이버 로그인 API를 발급받는데, 서비스 URL과 Callback URL 설정시 포트번호가 4200번이면 아래와 같은 화면이 보여지게된다. 구글링하면 URL을수정하라고 하는데, 문제가 URL의 포트 번호라는 것을 알기까지는 상당히 오랜 시간이 걸렸다. 그니까 4200 포트 번호를 3000 포트 번호로 바꾸게 되면 정상적인 화면이 보여지게 된다. 대체 무슨 상황인지 🤯 📌 Y2K Bug UTC와 GMT에 대한 공부를 하다가 Y2K Bug에 대해서 궁금해서 찾아봤다. 밀레니엄 버그라고도 불리우는데, 년도에 대한 정보를 저장할때 맨 뒤 두자리만 저장하여 발생하는 버그를 의미한다고 한다. 가령 1962년이라고 한다면 년도에 대해서 62라는 숫자만 저장하게 되는데, 이 경우, 2000년이 됐을 때 컴퓨터는 1900년으로 해석하게 된다는 의미이다 -_-; 하필 2000년이 다가오고 있을 때 이 문제에 대해서 인식하기 시작했다고 하는데, 별거 아닌 것 같아 보이지만, 은행, 운송, 발전소등과 같이 하루 혹은 년 단위로 동작하는 프로그래밍 된 작업들은 큰 문제가 된다고 한다. 문제 해결을 위해서 기존 2자리의 데이터를 모두 4자리로 확장했다고 한다. Y2K Bug 📌 Time Stamp, Unix Time, Epoch Time 이 역시도 UTC와 GMT에 대해서 공부하다가 궁금하여 조사하게 되었다. 1. Time Stamp 파일이나 로그에 등록된 시간으로, 파일이나 로그가 생성, 삭제, 수정, 전송된 시간이 기록된다. 2. Unix Time 특정 날짜(Epoch Time) 이후부터 카운팅한 초의 합(total second)를 의미한다. 일반적인 시간 형식(YYYY-MM-DD hh:mm:ss 등)이 아닌, 단순한 single number를 저장함으로써 저장 공간을 효율적으로 사용할 수 있게된다. 현대의 경우 차이가 크지 않을 수 있지만, 저장 공간이 훨씬 작은 1960년 후반 유닉스를 생각해본다면 이야기가 달라진다. 천재가 아닌 이상 머릿속으로 초의 합을 일반적인 시간 형식으로 바꾸는 것은 불가능하다는 단점이 있지만, Wed, 21 Oct 2015 07:28:00 GMT와 같은 일반적인 시간 형식과 비교했을 때, 두개의 Unix Time Stamp가 있을 때 시간적인 선후를 구분하기 쉽고, 두 Time Stamp간 차이를 구하는 것 역시도 쉽다는 장점이 있다. 3. Epoch Time Unix Time이 특정 날짜 이후부터 카운팅한 초의 합이라고 설명했다. 여기서 특정 날짜는 언제일까? 바로 1970년 1월 1일 00시 00분 00초 UTC 시간을 의미하며 이를 Epoch Time 혹은 Unix Epoch라고 부른다. 참고로 Epoch는 (중요한 사건, 변화들이 일어난) 시대를 의미한다. 프로그래머들이 1970년 1월 1일 00시 00분 00초 UTC를 Epoch Time으로 선정한 이유는 Unix Time이 만들어진 날짜와 가장 가까운 날짜라고 한다. Time Stamp What Is Unix Time and When Was the Unix Epoch? .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }Web History (1)
본 글은 A “Brief” History of the Web의 내용을 번역했습니다. 📌 첫 웹 서버, 웹 브라우저, 웹 페이지 1990년 후반에 버너스리가 HTML로 작성된 세계 최초 웹페이지를 배포했습니다. 아래는 세계 최초 웹 서버입니다. 당시 웹 서버는 네트워크에 연결된 컴퓨터로, 웹 서버는 자신의 IP에 접근하는 브라우저에게 자신의 파일시스템 일부를 노출합니다. 그리고 웹 브라우저는 파일시스템에서 HTML로 작성된 문서를 다운받아서 보여줍니다. 이 당시에도 네트워크 프로토콜은 HTTP였습니다. 📌 Dynamic Web의 시작인 Common Gate Interface 당시에 조금 더 똑똑한 웹페이제 대한 요구가 있었습니다. 웹 서버도 똑같은 컴퓨터인데, 웹 서버라고 프로그램을 실행하지 못할 이유가 있나?라는 질문과 함께요. CGI는 웹 서버로 하여금 서버가 단순히 HTML 페이지를 반환하는 것이 아니라, 프로그램을 실행 가능케 만들었습니다. 초기 CGI는 C로 작성된 코드만을 실행하다가, 이후에 Perl, Ruby와 같은 다른 언어에 대한 실행도 가능해졌습니다. 이미지 출처 📌 Templating 서버 사이드 스크립팅 이전에, 웹 사이트는 오직 읽기만 가능했습니다. 그리고 웹 페이지에 방문하면 작성자에 의해서 업데이트 되지 않는 이상 항상 같은 컨텐츠가 보여졌습니다. 또한, 하나의 웹 사이트에 유사한 스타일을 가진 페이지가 여러개 존재한다면, 어찌됐건 공유하는 스타일을 사용하는 것이 아닌, 각 페이지별로 스타일이 쓰여져 있어서 스타일이 업데이트되면 각 페이지를 업데이트 해주어야 했습니다. Templating은 이 문제에 대한 해결책이 되었습니다. Templating을 통해서 페이지의 일부를 재사용하거나, for문이나 if문을 통한 HTML 코드 작성이 가능했습니다. 그리고 CGI Script는 Template Engine이 해석해주었습니다. Template Engine이 서버에서 브라우저로 html을 내려주기 전에, CGI Script를 해석하여 HTML에 대한 전처리를 진행했습니다. 서버에 실행 모델이 존재할 수 있게 되면서, 서버 사이드 스크립트를 데이터 베이스에 연결하여 Templating을 하므로써 웹 페이지는 조금 더 다이나믹 해질 수 있었습니다. 서버 사이드 스크립트가 데이터 베이스로부터 데이터를 가져와서 페이지에 데이터를 표현하기 위한 template 문법을 적용하는 것이지요. 이로 인해서 페이지의 수정 없이 데이터만 바꿈으로서 페이지가 조금 더 다이나믹 해지는 것입니다. 이것이 interactive web의 시작이었습니다. 📚 참고 문헌 What is CGI? .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }9월 5주차
📌 처음으로 기획자분과 같이 일하게된 소감 기획자분이 월요일에 들어오셨다. 개발중인 앱을 사용하고 후기를 남겨주셨는데, 읽고나서 느낀점은 기본적으로 글을 잘쓰신다. 서비스를 글로 잘 풀어낸다. 평론가 같다는 느낌이 들었다. 그동안 내가 개발을 하면서 어렴풋하게 갖고 있던 생각들을 글로 표현했다면 아마 이렇게 작성하지 않았을까 싶다. 그만큼 많이 공감됐다. 그리고 지금까지의 개발은 항해사 없이 노만 젓는 느낌이었다면, 이제는 든든한 항해사가 들어온 느낌이었다. 📌 Date 함수 사용하기 githru에서 날짜로 commit 내역들을 필터링을 했을 때 , 특정 commit들이 보여지지 않는 문제가 있었다. 원인은 두가지였다. 첫번째 원인은 toISOString의 사용이었다. 다음과 같이 new Date의 인자로 한국 시간을 보내도, toISOString을 호출하는 경우 utc가 출력된다. console.log( new Date('Fri Sep 09 2022 18:33:02 GMT+0900 (Korean Standard Time)'), ) // Fri Sep 09 2022 18:33:02 GMT+0900 (한국 표준시) console.log( new Date( 'Fri Sep 09 2022 18:33:02 GMT+0900 (Korean Standard Time)', ).toISOString(), ) // 2022-09-09T09:33:02.000Z 두번째로 filtering 하는 로직의 문제였는데, 다음과 같이 new Date의 인자로 YYYY-MM-DD를 전달하게 되는 경우, 오전 9시의 GMT가 출력된다. console.log(new Date('2022-09-13')) // Tue Sep 13 2022 09:00:00 GMT+0900 (한국 표준시) 위와 같은 YYYY-MM-DD를 전달하면서 날짜를 비교해야하는 로직의 경우, 다음과 같이 적절한 시간을 같이 전달해주면 해결된다. const fromDate = '2022-04-28' const toDate = '2022-09-06' if ( new Date(targetDate) >= new Date(`${fromDate} 00:00:00`) && new Date(targetDate) <= new Date(`${toDate} 23:59:59`) ) { // ... } 그런데 서버로부터 utc 시간만 받아와서 사용하다가 GMT 시간을 다루게 된 것은 처음인데, 차이점좀 조사해서 코드에 녹여야겠다. 🤮 📌 useEffect 걷어내기 문득 githru 코드를 리펙토링 하다가 다음과 같은 구조의 코드를 보게됐다. const ComponentFunction = () => { const [a, setA] = useState('a'); const [b, setB] = useState('b'); const [c, setC] = useState('c'); const handleClickButton = () => setA('aa'); useEffect(() => { setB('bb'); setC('cc'); }, [a]) return ( ... ) } click로직과 useEffect로직이 떨어져 있어서 상태 추적이 어려운 코드다. 더불어서 setA가 호출되면서 리렌더링이 한번 발생하고, setB와 setC가 호출되면서 리렌더링이 한번 발생해서, 총 두번의 리렌더링이 발생한다. 만약 이를 다음과 같이 수정하면, 상태 추적도 쉬워지고 리렌더링도 한번만 발생하게 된다. const ComponentFunction = () => { const [a, setA] = useState('a') const [b, setB] = useState('b') const [c, setC] = useState('c') const handleClickButton = () => { setA('aa'); setB('bb'); setC('cc'); } return ( ... ) } 📌 husky, lint-staged, nx 도입 금주에 마크업 관련 작업을 다 끝냈다. 이제 다음 할 업무를 고민하던 도중, 일전에 develop 브랜치에 에러가 존재하는 코드가 섞여 들어간 히스토리가 있어서 팀장님께 husky, lint-staged 도입을 말씀드렸다. 도입 과정이 조금 어려웠던 부분은 정상적이지 않은 repository 구조였다. repository가 아래와 같이 monorepo를 구현하려다가 만 구조인데, .git web ㄴ package.json ㄴ ... webview ㄴ package.json ㄴ ... 다행히 이러한 multipackage 폴더 구조에서 husky를 셋업하는 가이드를 찾을 수 있었다. 그리고 nextjs 가이드 문서를 보고 lint staged를 같이 셋업한 결과 stackoverflow에서 말하는 문제에 부딪히게 되었다. 동작은 하는데, 빌드가 오래걸리고 재귀적으로 hook이 호출되는 느낌 🤯 현재 문제를 해결해도 monorepo를 셋업하면 결국에는 husky와 lint-staged를 다시 셋업해야할 것 같아서, 우선은 monorepo 셋업 후 다시 진행하려고 한다. 📌 금주 읽은 문서 1. Jamstack Gatsby를 사용하다가 Jamstack이라는 아키텍쳐를 보게되었다. 프로그래머스 데브코스를 수강할 때도 Jamstack이라는 단어를 들었던 기억이 있어서 리서치를 하고 있는데, 너무 두리뭉실하다. 명쾌하게 정리가 안되는 느낌. 조사하다보니 Jamstack의 비교대상으로 CMS가 나와서 추가적으로 관련 문서를 읽어보았다. Jamstack 공식문서 What is Jamstack? Traditional CMS vs Headless CMS (1) Traditional CMS vs Headless CMS (2) Traditional CMS vs Headless CMS (3) 2. SOLID 요새 오며가며 JAVA 객체지향 디자인 패턴 UML과 GoF 디자인 패턴 핵심 10가지로 배우는 책을 재미있게 읽고 있는데, SOLID를 프론트엔드에서도 적용이 가능한가 싶어서 리서치하다가 읽게되었다. Can you apply SOLID principles to your React applications ? .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk15 { color: #C586C0; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }React에 대해 알아보자
📌 객체 지향의 UI 프로그래밍 (object-oriented UI programming) 리액트 입문자라면 이전까지는 클래스 컴포넌트를 이용해서 작업해왔을 것입니다. 예를들면 클래스를 이용해서 각각의 프로퍼티와 상태를 갖고 있는 Button 인스턴스들을 만들어서 화면에 보여주는 것처럼요. 이것이 고전적인 객체 지향의 UI 프로그래밍이었습니다. 이러한 방식은 자식 컴포넌트 인스턴스 생성과 제거가 전적으로 개발자에게 달려있었습니다. Form 컴포넌트내에 Button 컴포넌트를 렌더링하기를 원한다면, Button 컴포넌트 인스턴스를 만들고 수동으로 Button 컴포넌트 인스턴스를 업데이트 해야했습니다. 아래와 같이 말이죠. class Form extends TraditionalObjectOrientedView { render() { const { isSubmitted, buttonText } = this.attrs if (!isSubmitted && !this.button) { // 폼이 제출되지 않았다면 버튼을 만듭니다. this.button = new Button({ children: buttonText, color: 'blue', }) this.el.appendChild(this.button.el) } if (this.button) { // 버튼이 보여지면 텍스트를 업데이트합니다. this.button.attrs.children = buttonText this.button.render() } if (isSubmitted && this.button) { // 폼이 제출되면 버튼을 제거합니다. this.el.removeChild(this.button.el); this.button.destroy(); } if(isSubmitted && !this.message) s{ // 폼이 제출되면ㄴ 성공 메세지를 보여줍니다. this.message = new Message({ text: 'Success' }) this.el.appendChild(this.message.el)s } } } 각 컴포넌트 인스턴스(this.button)는 자신의 DOM 노드에 대한 참조와 자식 컴포넌트들의 인스턴스에 대한 참조를 유지하고 있고, 개발자는 적절한 흐름에 맞게 이들을 생성, 업데이트, 제거해야 했습니다. 컴포넌트가 가질 수 있는 상태에 따라서 코드 라인 수는 제곱으로 증가했고, 부모 컴포넌트가 자식 컴포넌트에 직접적으로 접근이 가능해지면서 컴포넌트간 결속력이 커지는 구조였습니다. 그렇다면 리액트는 어떻게 이 문제를 해결하였을까요? 📌 React Element 리액트에서는 이 문제를 React Element가 해결해줍니다. React Element는 DOM Element와는 다른 일반 자바스크립트 객체(plain javascript object)로 Component Instance와 DOM Node를 묘사하는 객체입니다. 상태가 없고 불변하며, 다음 프로퍼티들을 갖고 있습니다. { $$typeof: Symbol.for('react'), key: key, ref: ref, _owner: owner, type: type, props: props, } key, ref, type, props는 눈에 익겠지만 $$typeof와 _owner는 눈에 익지 않습니다. 이는 아래에서 설명하겠습니다. React Element는 instance가 아닙니다. 그렇기 때문에 this.button.render와 같이 메서드 호출도 불가능하며, 단순히 화면에 어떻게 보여지길 원하는지 리액트에게 알리는 수단일 뿐입니다. 📌 React Element의 생성 React Element는 createElement를 통해서 생성되며, createElement로 전달되는 인자는 다음과 같습니다. createElement(type, { props }, ...children); createElement를 호출하지않고 일반 자바스크립트 객체를 직접 작성하는 것도 가능합니다. 아래 reactElementA와 reactElementB는 동일합니다. const reactElementA = createElement('div', { id: 'div-id' }, 'div-text'); const reactElementB = { type: 'div', props: { id: 'div-id', children: 'div-text', }, }; 공식 문서에서도 그렇고 다른 가이드 문서에서도 본것 같아서 위와 같이 적어놨는데, 여기서 React Element에는 $$typeof와 _owner 필드도 존재하기 때문에, React Element 객체를 직접 작성하여 생성하는 것은 불가능하다고 합니다. createElement를 사용하거나 이후에 이야기할 JSX를 사용해야만 생성이 가능한 것 같습니다. 많이 사용하는 JSX문 역시도, 바벨에 의해서 createElement를 호출하도록 트랜스파일링되어 React Element가 생성됩니다. 다음 코드들 처럼 말이죠. // JSX 표현 class ComponentOne extends React.Component { render() { return <p>Hello!</p> } } // 위 JSX문은 바벨에 의해서 다음과 같이 트랜스파일링 된다. class ComponentOne extends React.Component { render() { return createElement('p', {}, 'Hello!') } } // JSX 표현 function ComponentThree() { return ( <div> <ComponentOne /> <ComponentTwo /> </div> ) } // 위 JSX문은 바벨에 의해서 다음과 같이 트랜스파일링 된다. function ComponentThree() { return ( createElement( 'div', { }, createElement(ComponentOne, { }); createElement(ComponentTwo, { }); ) } React Element는 DOM node를 묘사하듯이, 다음과 같이 Component도 묘사할 수 있습니다. const reactElement = { type: Button, props: { color: 'blue', children: 'OK', }, }; 그리고 하나의 React Element Tree 안에 DOM node를 묘사하는 React Element와 Component를 묘사하는 React Element가 섞여 존재할 수 있습니다. const DeleteAccount = () => ({ type: 'div', props: { children: [{ type: 'p', props: { children: 'Are you sure?' } }, { type: DangerButton, props: { children: 'Yep' } }, { type: Button, props: { color: 'blue', children: 'Cancel' } }] }); 위 React Element를 JSX로는 다음과 같이 표현합니다. const DeleteAccount = () => ( <div> <p>Are you sure?</p> <DangerButton>Yep</DangerButton> <Button color="blue">Cancel</Button> </div> ); 리액트는 이러한 React Element 구조를 통해서(JSX문으로 작성됐지만 결국에는 React Element 객체로 표현될테니까요) is-a 관계와 has-a 관계를 모두 표현함으로써 컴포넌트간 결속력을 떨어뜨립니다. Button은 몇가지 프로퍼티를 갖고 있는 button 입니다 DangerButton은 몇가지 프로퍼티를 갖고 있는 Button 입니다. DeleteAccount는 div 내에 p, DangerButton, Button이 존재합니다. 📌 React Element와 React Component 위 이미지에서 적색 박스를 React Component라 부르고, 청색 박스를 React Element라고 부릅니다.(물론 지금은 JSX로 작성되어 있지만, 결국 바벨에 의해서 React Element로 트랜스파일링되죠.) 앞서 React Element가 무엇인지 정의하긴했지만, 공식문서에 나온 다음 정의를 빌어서 다시 정의해보면, “A ReactElement is a light, stateless, immutable, virtual representation of a DOM Element.” DOM Element의 표현인데, 가볍고, 상태가없고, 불변이고, 가상(plain javascript object)의 표현입니다. 반면에 React Component는 React Element에서 상태가 추가된 것입니다. 📌 React Node와 JSX.Element React Node는 React Element를 포함하며, React가 렌더링할수 있는 무엇이든 포함됩니다. DefinitelyTyped에 정의된 다음 React Node 타입 정의를 보면 이해가 갈 것입니다. /** * @deprecated - This type is not relevant when using React. Inline the type instead to make the intent clear. */ type ReactText = string | number; /** * @deprecated - This type is not relevant when using React. Inline the type instead to make the intent clear. */ type ReactChild = ReactElement | string | number; /** * @deprecated Use either `ReactNode[]` if you need an array or `Iterable<ReactNode>` if its passed to a host component. */ interface ReactNodeArray extends ReadonlyArray<ReactNode> {} type ReactFragment = Iterable<ReactNode>; type ReactNode = | ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined; 참고로 React Node는 클래스 컴포넌트의 return 메서드 반환 타입이기도 합니다. 반면에 함수형 컴포넌트의 return 메서드 반환 타입은 React Element입니다. 히스토리가 있는데, 커멘트가 너무 길어서 패스했습니다. 그리고 React Element의 props와 type에 대한 제네릭 타입이 any이면 JSX.Element가 됩니다. 다양한 library가 JSX를 각자의 방식대로 구현하기 위해서 존재한다고 하네요. interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> { type: T; props: P; key: Key | null; } 📌 컴포넌트의 React Element Tree 캡슐화 앞서 객체 지향 UI의 Form을 리액트로 구현하면 다음과 같습니다. const Form = ({ isSubmitted, buttonText }) => { if (isSubmitted) { return { type: Message, props: { text: 'Success!', }, }; } return { type: Button, props: { children: buttonText, color: 'blue', }, }; }; 위 Form 컴포넌트 함수를 보듯이, 컴포넌트는 기본적으로 React Element Tree를 리턴합니다. 개발자는 Form 컴포넌트를 사용할 때 내부 DOM이 어떤식으로 구성되어 있는지 알 필요가 없습니다. 그리고 리액트는 다음과 같이 type이 컴포넌트 함수인 React Element를 만나게 되면, { type: Button, props: { color: 'blue', children: 'OK', } } 리액트는 컴포넌트에게 어떤 React Element를 렌더링할 것인지 물어봅니다. 그리고 Button 컴포넌트는 다음과 같은 React Element를 return 합니다. { type: 'button', props: { className: 'button button-blue', children: { type: 'b', props: { children: 'OK!', } } } } 리액트는 이렇게 묻고 답하는 과정을 계속 반복하며 하나의 React Element Tree를 만들어냅니다. 📌 React Element에 존재하는 $$typeof 와 _owner 이해하기 1. $$typeof $$typeof를 이야기하려면 먼저 보안에 관한 이야기를 해야합니다. 서버의 보안에 빈틈이 존재해서 유저가 임의로 작성한 React Element를 JSON 객체 형태로 서버에 저장하게(최적화를 위해서 React Element 객체를 직접 작성하기도 했다고 합니다.)되었다고 가정합시다. 만약 이 객체가 아래와 같은 클라이언트 코드에 도달하게 되면 문제가 발생하게 됩니다. let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* put your exploit here */', }, }, // ... } let message = { text: expectedTextButGotJSON } <p>{message.text}</p> React 0.13 버전까지만 해도 이러한 XSS 공격에 취약했는데, React 0.14 버전부터는 $$typeof 태그를 통해서 이 문제를 해결했습니다. 기본적으로 React Element마다 $$typeof가 존재하는데, Symbol은 JSON 안에 넣을 수 없기 때문에 리액트는 element.$$typeof를 통해서 element의 유효성을 확인합니다. 만약 브라우저에서 Symbol을 지원하지 않는 경우에는 이러한 보호가 이루어질 수 없습니다. 어찌됐건 일관성을 위해서 $$typeof 필드는 여전히 존재하는데, 이때 값으로 0xeac7이 할당됩니다. 0xeac7인 이유는 모양이 React와 닮아서입니다. 참고로 리액트를 포함한 모던 라이브러리는 기본적으로 텍스트 컨텐츠에 대해서 이스케이프 처리를 지원하기 때문에, message.text 내에 <나 >처럼 위험한 문자가 있는 경우 이스케이프 처리가 된다고합니다. 2. _owner 다음과 같은 코드가 있습니다. const MyContainer = props => <MyChild value={props.value} />; 이를 통해서 다음을 알수 있습니다. MyContainer는 MyChild의 owner입니다. MyChild는 MyContainer의 ownee입니다. 이렇듯, DOM 관계를 나타내듯 부모/자식 관계로 이야기하지 않습니다. 이번에는 다음과 같이 MyChild 컴포넌가 div 태그로 래핑된 상황을 생각해봅시다. const MyContainer = props => ( <div> <MyChild value={props.value} /> </div> ); MyContainer는 MyChild의 부모가 아니라 owner입니다. React Chrome Developer Tools를 이용해서 본다면, 다음과 같이 보여집니다. MyChild의 owner는 div가 아닌 MyContainer 입니다. MyContainer는 어떠한 owner도 갖고 있지 않습니다. span의 owner는 MyChild 입니다. owner에 대해서 정리해보면, 다음과 같습니다. owner는 React Element입니다. ownee는 무엇이든 될수 있습니다.(React Element 혹은 순수한 HTML 태그) 특정 node의 owner는 조상중에서 node 자신을 render하거나 prop을 전달하는 요소입니다. 📚 참고 문헌 리액트 공식문서 Difference between React Component and React Element Instance In React When to use JSX.Element vs ReactNode vs ReactElement? $$typeof Understand the concepts of ownership and children in ReactJS Why Do React Elements Have a $$typeof Property? The difference between Virtual DOM and DOM .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk17 { color: #808080; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }9월 4주차
📌 git pull, fetch 차이 git rebase에 대해서 공부하던 중 git pull과 git fetch, git remote update의 차이점이 궁금해져서 리서치했다. git pull은 git fetch와 git merge가 합쳐진 명령어다. git fetch는 원격 브랜치의 변경 사항을 로컬의 .git 폴더(local repository)로 가져오지만, 로컬 브랜치의 코드(workspace)에 적용하지 않는다. 이를 통해서 원격 브랜치에서 가져온 변경 사항을 로컬 브랜치의 코드에 반영하기 전에 리뷰할 수 있는 기회가 생기고, merge로 인해서 발생하는 충돌을 막을 수도 있다. 그림으로 보면 아래와 같다. stackoverflow에서 설명하기로 git fetch를 이용하는 과정은 다음 cli들을 통해서 이루어진다. git fetch git diff master origin/master 확인 및 수정이 끝났다면, git pull을 통해서 업데이트 가능하다. 📌 git switch, checkout 차이 pull, fetch, remote update의 차이에 이어서, checkout과 switch의 차이가 궁금해져서 추가적으로 리서치했다. stackoverflow과 stackoverflow를 참고했다. checkout는 switch와 restore라는 두가지 기능을 동시에 갖고 있다. 이는 사람들로 하여금 사용 목적에 많은 혼란을 일으켰는데, git 2.23에서부터 checkout이 갖고 있는 기능을 분리한 switch와 restore가 도입되었다. 새 버전에 맞게 git checkout 대신 switch/restore 사용하기에서는 아래와 같이 기능이 분리되었음을 설명한다. checkout: Switch branches or restore working tree files switch: Switch branches restore: Restore working tree files checkout은 git checkout 으로 사용하는 경우, unstaged의 변경 사항을 제거할 수 있다. 이는 git restore 과 동일하다. git checkout 으로 사용하는 경우, 해당 branchname으로 switch가 가능하다. 이는 git switch 과 동일하다. 📌 CI/CD, devOps 용어 살펴보기 이번에 오픈소스 기여를 하면서 pr에서 ci 에러와 마주쳤다. ci/cd 및 devOps에 대한 개념이 없고, 시간도 없던터라 러프하게 찾아보았다. 우선 ci/cd에서 ci는 빌드, 테스트, 통합의 과정이고, cd는 content delivery랑 content deployment로 나뉠 수 있는데, 전자는 배포전에 중단될 수 있는 여지가 있으며, 후자는 배포까지 중단없이 이어진다고 한다. devOps는, dev(개발)과 ops(운영)간의 원활한 커뮤니케이션 및 협업을 장려하여 애플리케이션 개발의 품질과 속도를 향상시키고, 제품의 릴리즈 주기를 단축시킴으로서 고객 만족을 향상시킨다고 한다. 지금까지 한번쯤 들어보고, 또 행해본 스크럼, 칸반, 애자일 모두 DevOps 방법들이고, jira, github, slack이 모두 DevOps 툴체인의 일부라고. 📌 코드 리펙토링을 진행하면서 코드 리펙토링을 진행하면서 두가지 리펙토링을 진행했다. 첫번째로는 다음과 같은 유형의 코드다. const targetData = getTargetData() otherFunction(targetData) 함수 이름에 이미 목적어가 있는데 굳이 식별자를 한번 거쳐서 전달하고 있었다. 그래서 다음과 같이 리펙토링했다. otherFunction(getTargetData()) 리펙토링을 하며 유인동 저자의 ‘함수혐 자바스크립트 프로그래밍’책을 다시금 펴볼 기회가 생겼다. 값 대신 함수로, for와 if 대신 고차 함수와 보조 함수로, 연산자 대신 함수로, 함수 합성 등 앞서 설명한 함수적 기법들을 사용하면 코드도 간결해지고 함수명을 통해 로직을 더 명확히 전달할 수 있어 읽기 좋은 코드가 된다. 짧고 읽기 좋은 코드도 중요한 가치이지만 좀 더 고상한 이점이 있다. 인자 선언이나 변수 선언이 적어진다는 점이다. 코드에 인자와 변수가 등장하지 않고 함수의 내부가 보이지 않는다는 것은 새로운 상황도 생기지 않는다는 말이다. 새로운 상황이 생기지 않는다는 것은 개발자가 예측하지 못할 상황이 없다는 말이다. 두번째로는 커스텀 훅에서 리턴해주는 함수에 대한 이야기이다. 개인 취향 및 컨벤션 차이가 있는 부분이다. 개인적으로 이벤트 핸들러 함수 이름을 handle + 동사 + 목적어로 짓는데, 커스텀 훅에서 만약 이벤트 핸들러의 역할을 할수 있는 함수를 반환한다면, handle 접두사 없이 곧바로 동사로 시작하도록 변경했다. 개인 취향 및 컨벤션 차이일수도 있는데, 개인적으로 훅이 이벤트 핸들러 안에서 다른 여러 로직과 함께 실행되는 경우를 많이 마주쳤다. 그래서 다음과 같이 이벤트 핸들러 함수 안에 handle이 하나 더 추가되는 것이 어색하다고 느껴졌다. const ComponentFunction = () => { const [handleChangeButtonDisplay] = useBtnDisplay() const handleClickBtn = () => { handleChangeButtonDisplay() fn1() fn2() } return <Button onClick={handleClickBtn}>ButtonName</Button> } 다음과 같이 바꾸는 편이 좀 더 깔끔해 보인다. const ComponentFunction = () => { const [changeBtnDisplay] = useBtnDisplay() const handleClickBtn = () => { changeBtnDisplay() fn1() fn2() } return <Button onClick={handleClickBtn}>ButtonName</Button> } 📌 isNil 함수 수정하기 8월 4일에 isNil 함수를 다음과 같이 작성했었다. const isNil = <T>(param: T | Nil): param is T => param !== undefined || param !== null isNil(value)의 결과가 undefined임에도 불구하고 if문을 통과하는 것을 보고, 문제점을 발견하여 수정했다. const isNil = <T>(param: T | Nil): param is T => parm !== undefined && param !== null 📌 expand와 collapse 역할을 하는 버튼의 이름을 무엇일까 아래와 같이 collapse와 expand의 역할을 하는 버튼을 만들어야 할 일이 있었다. 버튼 요소 안에서 collapse와 expand 아이콘을 분기하는 로직을 짜는데, 이 버튼의 이름을 무엇으로 만들어야 할지 고민이었다. const ComponentFunction = () => { return <Button>{isExpanded ? <ExpandIcon /> : <CollapseIcon />}</Button> 마이크로소프트의 윈도우 디자인 가이드에서는 이를 Expander로 지어, 이를 따르기로 결정했다. 📌 Git Graph 소스코드를 뜯어보며 githru 오픈소스를 기여하면서, 파일을 클릭했을 때 해당 파일의 commit diff를 보여주는 기능을 구현하려 했다. commit diff에 대한 api 키워드가 잡히지 않아서 Git Graph 소스코드를 참고하려고 했는데, 아래와 같은 구조의 함수를 마주쳤다. 이전에 데브코스를 수강할 때 강사님께서 localStorage.getItem(key)를 래핑하여 localStorage.getItem(key, defaultValue)와 같이 사용 가능하도록 함수를 사용한적이 있는데, vscode api는 기본적으로 이 기능이 내장되어 있었다. 과거에 아무 생각 없이 썼던 함수, 약간 레퍼런스 없는 함수를 이렇게 api로 제공하고 있는 것을 보고 재미(?)를 느꼈다. 마땅한 표현이 떠오르지 않는데, 점과 점을 잇는데 기분이 좋은? 📌 금주 읽은 문서 회사에서 내가 담당하는 페이지중 하나가 (개인적으로) 난이도가 매우 높다. 그리고 일전에 React 상태 관리 라이브러리(1)를 작성하면서 상태가 복잡한 경우 state machine을 사용하라는 것에 대한 글귀를 본 기억이 있어서, 이 페이지의 복잡성을 해소시켜줄 열쇠가 될수도 있을 것 같아서 찾아보았다. State Machines in React How to Use Finite State Machines in React 이번주에는 로버트 C. 마틴의 클린 아키텍처 책에서 파서드 패턴이 언급되어 관련한 문서를 찾아보았다. Facade Design Pattern Facade Design Pattern 2 Facade Design Pattern 3 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk17 { color: #808080; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }structural
본 글은 refactoring.guru, sbcode, patterns.dev의 내용을 참고, 번역했습니다. 📌 Proxy 원본 객체에 대한 인터페이스(혹은 substitute 혹은 placeholder)로 원본 객체로 위장하여 원본 객체에 대한 접근을 제어합니다. Mongkey Patching, Object Augmentation, Surrogate 라고도 불립니다. 참고로 Monkey Patching이란, 런타임에 기존 코드를 수정하는 일 없이 기존 코드에 기능을 추가하거나, 기존 코드의 기능을 변형하거나, 기존 코드의 기능을 제거하는 테크닉을 의미합니다. 간단한 예시를 하나 들어보겠습니다. 우리는 console.log를 디버깅을 위해서 사용하거나 변수가 어떤 값을 갖고 있는지 알기 위해서 사용하곤 합니다. 모든 것이 잘 동작하지만, 여기에 더해서 console.log가 호출될 때 날짜와 시간이 찍히길 원할 수도 있습니다. 그리고 이는 mongkey patching 기법을 이용해서 아래와 같이 구현이 가능합니다. var log = console.log console.log = function () { log.apply(console, [new Date().toString()].concat(arguments)) } 기존의 console.log는 log 변수에 저장함으로써 변경이 일어나지 않고, console의 log 프로퍼티를 오버라이드 하고 날짜와 arguments를 log 변수에 전달하면서 console.log에 기능을 추가했습니다. 다시 proxy로 돌아와서, 그렇다면 원본 객체에 대한 접근을 제어하는 이유가 무엇일까요? 이는 다음 proxy 타입들을 보면 알수 있습니다. Virtual Proxy Lazy Initialization을 하는 proxy로, Lazy Initialization은 당장 필요하지 않은 무거운 원본 서비스 객체가 시스템상의 자원을 차지하면서 존재하기 보다는, 정말 필요할 때까지 객체의 생성이나 초기화를 미루는 것입니다. Remote Proxy 원본 서비스 객체가 원격 서버에 존재할 때, 네트워크를 통해서 요청을 보내기 위한 자질구레한 작업들을 수행하는 proxy입니다. Protection Proxy 특정 클라이언트만 원본 서비스 객체에 접근할 수 있도록 할때 사용하는 proxy로, 만약 원본 서비스 객체가 OS의 일부분으로 매우 중요하고, 여러 개의 클라이언트가(악의적인 클라이언트가 포함되어 있을 수 있는) 이에 접근하려고 할때 사용할 수 있습니다. Logging Proxy 원본 서비스 객체에 요청을 전달하기 전에 log를 남깁니다. 이를 통해서 요청에 대한 history를 남길 수 있습니다. Caching Proxy 클라이언트 요청 결과의 일부를 캐싱하고 lifecycle을 관리하는 proxy입니다. Smart Reference 원본 서비스 객체를 이용하는 클라이언트가 없는 경우에, 해당 원본 서비스 객체가 차지하고 있는 자원을 해제할 수 있습니다. proxy는 원본 서비스 객체를 참조하고 있는 클라이언트를 계속 추적합니다. 만약 참조하는 클라이언트가 없는 경우, 생성된 원본 서비스 객체에 할당된 자원을 제거합니다. proxy는 현실 세계의 신용 카드에 비유할 수 있습니다. 신용 카드는 은행 계좌에 대한 proxy가 될수 있고, 현금에 대한 proxy가 될수도 있습니다. 소비자 입장에서는 돈뭉치를 들고다니며 지불하지 않아도 편하고, 가계 주인 입장에서는 지불받은 금액이 곧바로 계좌로 가기때문에 행여 강도에 의해서 잃어버리거나 관리할 필요가 없어져서 편합니다. (관계가 좀 햇갈리는데, 크게 클라이언트, proxy, 서비스 객체가 존재한다면, 클라이언트는 카드를 받은 가게 사장님이되고, proxy는 카드 그 자체이며, 서비스 객체는 은행 계좌로 해석하면 되지 않을까 싶습니다. 💶) UML 다이어그램은 아래와 같습니다. 원본 서비스 객체(Service)의 타입 인터페이스인 Service Interface를 선언합니다. proxy는 자기 자신을 원본 서비스 객체로 위장하기 위해서 이 인터페이스를 무조건적으로 따라야 합니다. 그리고 클라이언트는 원본 서비스 객체나 proxy 모두와 동작이 가능합니다. 코드로 본다면 아래와 같습니다. interface Subject { request(): void; } class RealSubject implements Subject { public request(): void { console.log('RealSubject: Handling request'); } } class Proxy implements Subject { private realSubject: RealSubject; constructor(realSubject: RealSubject) { this.realSubject = realSubject; } public request(): void { if(this.checkAccess()) { this.realSubject.request(); this.logAccess(); } } private checkAccess(): boolean { console.log('Proxy: Checking access prior to firing a real request.'); } private logAccess(): void { console.log('Proxy: Logging the time of request.'); } } function clientCode(subject: Subject) { subject.request(); } console.log('Client: Executing the client code with a real subject'); const realSubject = new RealSubject(); clientCode(realSubject); console.log('Client: Executing the same client code with a proxy'); const proxy = new Proxy(realSubject); clientCode(proxy); proxy의 장점으로는 (1) 클라이언트가 원본 서비스 객체에 대해 알 필요 없이 제어가 가능하며, (2) 라이프 사이클도 관리할 수 있습니다. (3) 프록시는 원본 서비스 객체가 준비가 안된 상태라도 작업이 가능하며, (4) 원본 서비스 객체나 클라이언트를 변경하지 않고 새로운 proxy 도입이 가능하기 때문에 개방/폐쇄 원칙을 지킬 수 있습니다. 단점으로는 (1) 수많은 새로운 클래스들을 도입해야 하기때문에 복잡해질 수 있고, (2) 응답이 지연될 수 있습니다. 📌 Facade 라이브러리나 프레임워크에 대한 간단하고 편리한 인터페이스를 제공합니다. 1. Problem and Solution 매우 복잡한 SubSystem(라이브러리나 프레임워크)에 속한 광범위한 객체 집합들과 함께 동작해야하는 코드를 작성한다고 상상해보세요. 보통 이러한 객체들은 초기 내용을 설정하고, 의존성을 추적하고, 올바른 순서대로 메서드가 실행돼도록 해야합니다. 결과적으로는 당신의 클래스 내 비지니스 로직들이 라이브러리 클래스의 구현 내용들과 강하게 결합되면서 이해하기 어렵고 유지보수하기 힘든 코드가 만들어지게 됩니다. Facade는 SubSystem이 갖고 있는 수많은 기능을 제공하는 것이 아니라 몇몇의 기능만 제공하며, 복잡한 SubSystem 대한 간단한 인터페이스를 제공하는 클래스입니다. 현실 세계에서 전화 주문을 생각하면 쉽습니다. 주문을 위해서는 주문, 지불, 배달에 대한 과정(SubSystem)을 직접 밟아야 하지만, 전화 상담원(Facade)을 통해서 이러한 과정을 쉽게 처리할 수 있습니다. 2. Pros and Cons 복잡한 하위 시스템의 코드를 분리할 수 있다는 장점이 있지만, god object가 될수도 있다는 단점도 존재합니다. 3. Code class SubSystemA { // 내부가 매우 복잡하다고 가정한다. method() { return 1 } } class SubSystemB { // 내부가 매우 복잡하다고 가정한다. method(value) { return value } } class SubSystemC { // 내부가 매우 복잡하다고 가정한다. method(value) { return value + 2 } } class Facade { subSystemClassA() { return new SubSystemClassA().method() } subSystemClassB(value) { return new SubSystemClassB().method(value) } subSystemClassC(value) { return new SubSystemClassC().method(value) } operation(valueB, valueC) { return ( subSystemClassA().method() + subSystemClassB().method(valueB) + subSystemClassC().method(valueC) ) } } // 하위 시스템을 다이렉트로 사용하는 경우 console.log( new SubSystemClassA().method() + new SubSystemClassB().method(2) + new SubSystemClassC().method(3), ) // 8 // 간단한 Facade를 통해서 하위 시스템을 사용하는 경우 const facade = new Facade() console.log(facade.operation(2, 3)) // 8 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }9월 3주차
📌 Mui Select 컴포넌트는 Semantic하지 않다 mui Select 컴포넌트를 사용하려고 하는데, mui Select 컴포넌트의 demo 코드에 있는 id, label, labelId가 문서를 봐서는 무엇을 의미하는지 잘 이해가 안됐다. 개발자 도구를 통해서 보려고 했는데, Select 컴포넌트가 전혀 Semantic 하게 만들어지지 않았다는 것을 알게되었다. select 태그는 사용되지도 않았을 뿐더러, 같이 사용되는 label 역시도 웹 접근성을 위해 사용되는 for 속성이 없었다. 일전에 웹 접근성을 준수하면서 select 태그를 커스터마이징하는데 고통의 시간을 보낸 경험이 있는데, Mui는 웹 접근성 부분을 포기한 것 같다. 📌 TwoOptionsModal 공용 컴포넌트 만들기 아래와 같이 두개의 선택지가 있는 모달이 여기저기서 사용된다고 해보자 처음에 본인은 아래와 같이, TwoOptionsModal이라는, Mui 컴포넌트를 감싸는 공용 컴포넌트를 만들었다. import { Modal } from '@mui/material' interface TwoOptionsModalProps { isModalOpen: boolean; leftBtnName: string; rightBtnName: string; children: ReactNode; onCloseModal: () => void; onClickLeftBtn?: () => void; onClickRightBtn?: () => void; } const TwoOptionsModal = ({ isModalOpen, leftBtnName, rightBtnName, onCloseModal, onClickLeftBtn, onClickRightBtn, children, }: TwoOptionsModalProps) => { return ( <Modal open={isModalOpen} onClose={onCloseModal}> <ModalContentWrapper ref={modalWrapperRef}> <ChildrenWrapper>{children}</ChildrenWrapper> <BtnWrapper> <ModalBtn onClick={onClickLeftBtn}>{leftBtnName}</ModalBtn> <ModalBtn onClick={onClickRightBtn}>{rightBtnName}</ModalBtn> </BtnWrapper> </ModalContentWrapper> </Modal> ) } 문제는, 다른 페이지에서 TwoOptionsModal를 만들 때, 이를 컴포넌트화 해야하는 경우 아래와 같이 매번 interface를 만들고, TwoOptionsModal에 prop을 전달해주어야 하는 단점이 있었다. import TwoOptionsModal from '@components/TwoOptionsMdoal' interface DeleteConfirmModalProps { isModalOpen: boolean; leftBtnName: string; rightBtnName: string; onCloseModal: () => void; onClickLeftBtn: () => void; onClickRightBtn: () => void; } const DeleteConfirmModal = ({ isModalOpen, leftBtnName, rightBtnName, onCloseModal, onClickLeftBtn, onClickRightBtn, }: DeleteConfirmModalProps) => { return ( <TwoOptionsModal isModalOpen={isModalOpen} leftBtnName={leftBtnName} rightBtnName={rightBtnName} onCloseModal={onCloseModal} onClickLeftBtn={onClickLeftBtn} onClickRightBtn={onClickRightBtn} > { ... } </TwoOptionsModal> ) } 금주에 HOC 아티클을 읽고 아이디를 얻어, 아래와 같은 방향으로 컴포넌트를 수정했다. const withTwoOptionsModal = Component => { return ({ isModalOpen, leftBtnName, rightBtnName, onCloseModal, onClickLeftBtn, onClickRightBtn, }: TwoOptionsModalProps) => ( <TwoOptionsModal isModalOpen={isModalOpen} leftBtnName={leftBtnName} rightBtnName={rightBtnName} onCloseModal={onCloseModal} onClickLeftBtn={onClickLeftBtn} onClickRightBtn={onClickRightBtn} > <Component /> </TwoOptionsModal> ) } import withTwoOptionsModal from '@components/TwoOptionsMdoal' const DeleteConfirmModal = () => { return { ... } // 컴포넌트 구현 내용 } export default withTwoOptionsModal(DeleteConfirmModal); 📌 git의 case insensitive 금일 컴포넌트를 만들면서, 파일 이름을 실수로 파스칼 케이스가 아닌 카멜 케이스로 PR을 올렸다. 다시 파스칼 케이스로 바꿔 push하려고 했는데, git이 파일 이름 변화를 추적하지 못하는 문제에 부딪혔다. 리서치 해보니 git은 기본적으로 case insensitive하기 때문에, 소문자 대문자 변경은 추적하지 못한다고 한다. 지금까지 파일 이름 변경해도 이런적이 없었는데 왜 갑자기?라는 생각이 들던차에 오롯이 대소문자 변경은 이번이 처음인 것을 깨달았다. 어찌됐건 stackoverflow에서 제시하는 방법은 다음 세가지가 존재했다. git rm oldName _oldName && git rm _oldName newName git config core.ignorecase false git rm -r –cached . && git add . 1번의 경우, 경로를 전부 입력해야하고, 중간 이름(_oldName)을 거쳐야 한다는 번거로움이 있다. 2번의 경우, git이 case sensitive하도록 설정하는 것이다. 하만 OS가 case insensitive할때, git을 case sensitive하게 만드는 것은 side effect 가 발생할 수 있다고 한다. 가령 MAC에서 a라는 파일 이름을 A로 바꾸어서 push하면, repo에는 a라는 파일과 A라는 파일이 동시에 존재한다는 것이다. 이외에도 여러가지 현상이 존재하는 것 같아서 이 방법도 패스했다. 3번의 경우, 경로를 입력하거나, 중간 이름을 거쳐야 한다는 번거로움도 없고, side effect에 대한 언급도 없기 때문에 이 방법을 선택하게 되었다. 📌 git rm -r –cached file 이란? stackoverflow 의 답변을 가져왔다. rm은 remove를 의미한다. git rm은 대상 파일을 index에서도 지우고, working tree에서도 지우지만, git rm –cached는 index에서만 지우고, working tree에는 파일을 남겨둔다. 그리고 r은 recursive를 의미한다. 그러므로 3번 방법이 의도하는 바는 git rm -r –cached가 의도하는 바는, 현재 프로젝트에서 존재하는 file들 unstaged 상태로 만들고, git add .를 통해서 다시 staged 상태로 만들어서 대소문자가 바뀐 파일을 git이 추적하게 만드는 것이다. 참고로 index나 working tree에 대한 설명은 여기 에 잘 나와있다. 📌 금주 읽은 문서 HOC 패턴에 관해서 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk17 { color: #808080; } .default-dark .mtk3 { color: #6A9955; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }tsconfig --downlevelIteration
본 글은 Downlevel Iteration for ES3/ES5 in TypeScript의 일부를 번역해놓은 글 입니다. TypeScript 2.3 버전에는 tsconfig의 target이 es3와 es5일때, es6의 이터레이션 프로토콜 지원을 위한 downlevelIteration 플래그가 도입되었습니다. 본 내용에 들어가기에 앞서서 tsconfig.json compilerOptions의 target과 lib 옵션에 대해서 먼저 알아야 합니다. 📌 compilerOptions — target 모던 브라우저의 경우 es6 문법을 대부분 지원하지만, 더 옛날 환경에서의 동작이나, 더 최신 환경에서의 동작을 보장해야 할때도 있습니다. target에 ES 버전을 설정함으로써 타입스크립트 코드를 해당 버전의 자바스크립트 코드로 트랜스파일 가능합니다. 예를들면 es6 문법 중 하나인 화살표 함수를 function 키워드를 사용한 es5 이하의 문법으로 변환할 수 있습니다. 참고로 target의 기본값은 es3입니다. target을 바꾸면 다음에 설명할 lib 설정의 기본값이 바뀝니다. target을 es5로 설정하면 lib에는 ‘dom’과 ‘es5’가 기본으로 설정됩니다. target과 lib을 동시에 설정함으로서 디테일한 설정이 가능하지만, 편의상 target만 설정해도 좋습니다. node 개발자의 경우, 관련된 커뮤니티에서 특정 플랫폼과 버전에 따른 tsconfig 설정을 미리 만들어 놓았습니다. target의 설정값중 하나인 ESNext의 경우, 현재 설치된 타입스크립트 버전에서 지원할 수 있는 가장 최신 버전의 ES를 의미합니다. 이 설정은 타입스크립트 버전에 의존하기 때문에 유의해야 합니다. 📌 compilerOptions — lib 타입스크립트는 기본적으로 빌트인 객체(Math) 혹은 호스트 객체(document)에 대한 타입 정의들을 포함하고 있습니다. 또한 target에 매칭되는 비교적 최신 기능들, 가령 target이 es6 이상이면 Map등에 대한 타입 정의 역시도 갖고 있습니다. 이러한 선언들은 d.ts 선언 파일들에 담겨있는데, lib을 설정하므로써 필요한 타입 정의 파일들을 선택적으로 고를 수 있습니다. 선택적으로 골라야 하는 상황은 아래와 같습니다. 프로젝트가 브라우저에서 동작하지 않기 때문에 DOM 타입이 필요 없을 수 있습니다. 특정 버전의 문법 중 일부가 필요하지만, 해당 버전의 문법 전체가 필요하지 않을 수 있습니다. 높은 버전의 문법 중 일부에 대해서 폴리필이 존재할 수 있습니다. 📌 target과 lib의 차이점 target과 lib에 대한 공식 문서 설명이 빈약한 것 같아 TypeScript lib vs target: What’s the difference? 를 추가적으로 번역했습니다. target 옵션에 ‘es5’를 설정한다는 것은 다음 두가지 의미를 갖습니다. 첫번째로, 타입스크립트 코드에 es5에서 지원되지 않는 자바스크립트 문법이 존재한다면, 컴파일시 이를 es5의 자바스크립트 문법으로 트랜스파일링 한다는 것입니다. 가령, 다음과 같은 화살표 함수는 const add = (a: number, b: number) => a + b 컴파일시 다음과 같이 트랜스파일 됩니다. var add = function (a, b) { return a + b } 두번째로, es5에서 지원되지 않는 API 사용이 불가능해집니다. 타입스크립트는 폴리필을 제공하지 않기 때문에, es6 이상에서만 지원되는 Promise 사용이 불가능합니다. 아래 코드에 대해서는 return Promise.resolve(value) 다음과 같은 에러를 발생합니다. // 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later. 타입스크립트는 트랜스파일 기능은 존재하지만, Promise나 Map, array.find에 대한 폴리필을 제공하지 않습니다. 바벨과는 다르게요 그러므로 Promise를 es5에서 사용하기 위해서 개발자는 Promise 폴리필을 추가해야 합니다. 하지만 폴리필을 추가해도 타입스크립트는 이를 알지 못해서 계속 에러를 발생시킵니다. 그렇기 때문에 lib 옵션이 도입되었습니다. lib 옵션 설정을 통해서 타입스크립트에게 런타임에 사용할 자바스크립트 API를 알려줄 수 있습니다. 여기서는 Promise API를 사용하기 때문에, lib을 [’dom’, ‘es5’, ‘es2015.promise’]를 추가하여 관련한 타입 선언을 포함시킬 수 있습니다. downlevelIteration에 대해서 알아보기 전에 알아야 할 내용은 전부 다루었습니다. 이제부터 downlevelIteration에 대해서 알아봅시다. 📌 for…of 문을 이용해서 배열 순회하기 tsconfig를 다음과 같이 설정해 놓았습니다. { compilerOptions: { target: 'es5', } } 그리고 index.ts파일에 다음과 같이 es6 문법인 for… of 문으로 배열을 순회하며 로깅하는 코드가 존재합니다. const numbers = [4, 8, 15, 16, 23, 42] for (const number of numbers) { console.log(number) } 이 코드를 컴파일 없이 바로 실행했을 때 다음과 같이 출력되는 것을 확인할 수 있습니다. $ node index.ts 4 8 15 16 23 42 그러면 이제 index.ts를 index.js로 컴파일해봅니다. $ tsc -p . 만들어진 자바스크립트 코드를 보면, for…of 문이 index 기반의 for 문으로 바뀐 것을 확인할 수 있습니다. var numbers = [4, 8, 15, 16, 23, 42] for (var _i = 0, numbers_1 = numbers; _i < numbers_1.length; _i++) { var number = numbers_1[_i] console.log(number) } 이 코드를 실행하면 아래와 같이 잘 동작하는 것을 확인할 수 있습니다. $ node index.js 4 8 15 16 23 42 node index.ts나 node index.js나 실행 결과가 동일합니다. 이는 타입스크립트 컴파일이 코드의 동작에 아무런 영향도 끼치지 않는다는 것을 의미합니다. 📌 for…of 문을 이용해서 문자열 순회하기 이번에는 배열이 아닌 문자열을 순회해봅시다. const text = 'Booh! 👻' for (const char of text) { console.log(char) } 컴파일 없이 바로 코드를 실행해보면 아래와 같은 결과가 나오는 것을 알수 있습니다. $ node index.ts B o o h ! 👻 이제 index.ts를 index.js로 컴파일하면 아래와 같이 코드가 만들어지는 것을 알수 있습니다. var text = 'Booh! 👻' for (var _i = 0, text_1 = text; _i < text_1.length; _i++) { var char = text_1[_i] console.log(char) } 하지만 이를 실행했을 때 코드 동작은 전혀 달라집니다. $ node index.js B o o h ! � � 유령 이모지의 code point 는 U+1F47B 입니다. 좀 더 정확히 말하면, U+D83D와 U+DC7B의 두개의 code unit으로 구성되어 있습니다. 문자열의 특정 index에 접근하면 code point가 아닌 code unit을 return 받게 됩니다. (이 부분에 대한 내용은 별도 문서에서 다루겠습니다.) index.ts와 index.js의 동작이 다른 이유는, 문자열 이터레이션 프로토콜의 경우, code point를 순회하지만, for 문은 ghost 이모지를 code unit으로 쪼개어 순회하기 때문입니다. 이는 단순히 문자열 length 프로퍼티에 접근하는 것과 문자열 스프레딩 결과물에 의해 생성된 값을 담은 배열의 length 프로퍼티에 접근한 결과를 보면 납득할 수 있습니다. const ghostEmoji = '\u{1F47B}' console.log(ghostEmoji.length) // 2 console.log([...ghostEmoji].length) // 1 요약하자면, for…of 문이 ES3 혹은 ES5를 타게팅할 때 항상 올바르게 동작하는 것은 아닙니다. 이것이 —downlevelIteration 등장한 이유입니다. 📌 downlevelIteration의 등장 이번에는 tsconfig의 compilerOptions에 downlevelIteration을 추가하여, 앞선 index.ts를 index.js로 다시 컴파일 해봅시다. var __values = (this && this.__values) || function (o) { var m = typeof Symbol === 'function' && o[Symbol.iterator], i = 0 if (m) return m.call(o) return { next: function () { if (o && i >= o.length) o = void 0 return { value: o && o[i++], done: !o } }, } } var text = 'Booh! 👻' try { for ( var text_1 = __values(text), text_1_1 = text_1.next(); !text_1_1.done; text_1_1 = text_1.next() ) { var char = text_1_1.value console.log(char) } } catch (e_1_1) { e_1 = { error: e_1_1 } } finally { try { if (text_1_1 && !text_1_1.done && (_a = text_1.return)) _a.call(text_1) } finally { if (e_1) throw e_1.error } } var e_1, _a 보시다시피, index 기반의 for 문이 아닌, 이터레이션 프로토콜을 좀더 알맞게 구현한, 훨씬 더 정교한 코드가 만들어졌습니다. 만들어진 코드를 조금 살펴보면, __values 보조 함수는 [Symbol.iterator] 메서드를 호출하고, 찾을 수 없다면 이터레이터를 직접 만듭니다. for 문은 code unit을 순회하는 것이 아닌, done이 true가 될때까지 next 메서드를 호출합니다. ECMAScript 스펙에 맞게 이터레이션 프로토콜을 구현하기 위해 try / catch / finally 문도 추가됩니다. 이 코드를 실행하게 되면 코드가 정상적으로 동작하는 것을 알수 있습니다. $ node index.js B o o h ! 👻 📌 es6 Collection을 downlevelIteration 하기 ES6에서 Map과 Set의 새로운 컬렉션이 추가되었습니다. 여기서는 for of 문법으로 어떻게 Map을 순회하는지 보려고 합니다. const digits = new Map([ [0, 'zero'], [1, 'one'], [2, 'two'], [3, 'three'], [4, 'four'], [5, 'five'], [6, 'six'], [7, 'seven'], [8, 'eight'], [9, 'nine'], ]) for (const [digit, name] of digits) { console.log(`${digit} -> ${name}`) } 위 코드는 아래와 같이 정상적으로 작동합니다. $ node index.ts 0 -> zero 1 -> one 2 -> two 3 -> three 4 -> four 5 -> five 6 -> six 7 -> seven 8 -> eight 9 -> nine 하지만 타입스크립트 컴파일러는 Map을 찾을 수 없다는 에러를 발생합니다. Map 컬렉션을 지원하지 않는 es5를 타게팅하기 때문입니다. 만약 Map에 대한 폴리필을 제공한다고 할때, 이 코드가 정상적으로 컴파일되게 하려면 어떻게 해야할까요? 해결책은 lib 옵션에 ‘es2015.collection’와 ‘es2015.iterable’ 값을 넣어주는 것 입니다. 이는 타입스크립트에게 ES5로 타게팅을 하되, es6의 컬렉션과 이터러블을 지원할 것임을 설정하는 것 입니다. lib 옵션을 설정하는 순간, 타게팅을 es5로 설정했을 때 lib의 기본값은 적용되지 않기때문에, ‘dom’과 ‘es5’ 역시도 추가해주어야 합니다. 결과적으로 tsconfig.json은 아래와 같이 설정됩니다. { "compilerOptions": { "target": "es5", "downlevelIteration": true, "lib": [ "dom", "es5", "es2015.collection", "es2015.iterable" ] } } 컴파일을 하면 다음과 같은 코드가 만들어집니다. var __values = (this && this.__values) || function (o) { var m = typeof Symbol === 'function' && o[Symbol.iterator], i = 0 if (m) return m.call(o) return { next: function () { if (o && i >= o.length) o = void 0 return { value: o && o[i++], done: !o } }, } } var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === 'function' && o[Symbol.iterator] if (!m) return o var i = m.call(o), r, ar = [], e try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value) } catch (error) { e = { error: error } } finally { try { if (r && !r.done && (m = i['return'])) m.call(i) } finally { if (e) throw e.error } } return ar } var digits = new Map([ [0, 'zero'], [1, 'one'], [2, 'two'], [3, 'three'], [4, 'four'], [5, 'five'], [6, 'six'], [7, 'seven'], [8, 'eight'], [9, 'nine'], ]) try { for ( var digits_1 = __values(digits), digits_1_1 = digits_1.next(); !digits_1_1.done; digits_1_1 = digits_1.next() ) { var _a = __read(digits_1_1.value, 2), digit = _a[0], name_1 = _a[1] console.log(digit + ' -> ' + name_1) } } catch (e_1_1) { e_1 = { error: e_1_1 } } finally { try { if (digits_1_1 && !digits_1_1.done && (_b = digits_1.return)) _b.call(digits_1) } finally { if (e_1) throw e_1.error } } var e_1, _b 한가지 주목해야 하는 것은, 이번에는 __read 변수까지 생성되어, 코드 사이즈가 크게 증가한다는 것입니다. 📌 —importHelpers와 tslib npm 패키지로 코드 사이즈 줄이기 위를 보면 알겠지만 __values, __read 보조 함수가 추가되는 것을 알수있습니다. 타입스크립트 프로젝트에서 만약 여러 파일들을 컴파일하는 경우, 코드 사이즈는 더욱 더 방대하게 늘어날 것입니다. 보통 프로젝트에서 번들러를 사용하는데, 번들 결과물 역시도 이러한 보조 함수때문에 불필요하게 커지게됩니다. 이 문제에 대한 해결법은 —importHelpers 컴파일러 옵션과 tslib npm 패키지를 사용하는 것입니다. —importHelpers는 타입스크립트 컴파일러가 모든 보조함수를 tslib에서 가져오도록 합니다. 웹팩은 단순히 npm 패키지를 한번만 기술하므로서, 코드 중복을 방지할 수 있게 됩니다. 지금까지 설명한 내용을 코드로 보면 아래와 같습니다. 우선 테스트할 코드를 다음과 같이 작성해줍니다. const digits = new Map([ [0, 'zero'], [1, 'one'], [2, 'two'], [3, 'three'], [4, 'four'], [5, 'five'], [6, 'six'], [7, 'seven'], [8, 'eight'], [9, 'nine'], ]) export function printDigits() { for (const [digit, name] of digits) { console.log(`${digit} -> ${name}`) } } 컴파일러 옵션을 다음과 같이 수정해줍니다. { "compilerOptions": { "target": "es5", "downlevelIteration": true, "importHelpers": true, "lib": [ "dom", "es5", "es2015.collection", "es2015.iterable" ] } } 컴파일을 했을 때 결과물은 다음과 같이 만들어집니다. 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) var tslib_1 = require('tslib') var digits = new Map([ [0, 'zero'], [1, 'one'], [2, 'two'], [3, 'three'], [4, 'four'], [5, 'five'], [6, 'six'], [7, 'seven'], [8, 'eight'], [9, 'nine'], ]) function printDigits() { try { for ( var digits_1 = tslib_1.__values(digits), digits_1_1 = digits_1.next(); !digits_1_1.done; digits_1_1 = digits_1.next() ) { var _a = tslib_1.__read(digits_1_1.value, 2), digit = _a[0], name_1 = _a[1] console.log(digit + ' -> ' + name_1) } } catch (e_1_1) { e_1 = { error: e_1_1 } } finally { try { if (digits_1_1 && !digits_1_1.done && (_b = digits_1.return)) _b.call(digits_1) } finally { if (e_1) throw e_1.error } } var e_1, _b } exports.printDigits = printDigits 같은 파일 안에 보조 함수들이 정의되지 않고, 대신 코드 시작부터 tslib 패키지를 가져오는 것을 알수 있습니다. 📌 결론 타입스크립트의 downlevelIteration 프로토콜은 es6의 이터레이션 프로토콜이 이용된 문법을 es5로 트랜스파일링할때 조금 더 정확하게 트랜스파일링 할수 있도록 하는 옵션입니다. 옵션을 키게되면 코드 사이즈가 전체적으로 커지게 되지만, –ImportHelpers와 tslib npm 패키지를 이용해서 코드 사이즈를 줄여줄 수 있습니다. 📚 참고문헌 타입 스크립트 공식문서, downlevelIteraton 타입 스크립트 공식문서, tsconfig 설정 .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk6 { color: #D7BA7D; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }9월 2주차
📌 XYProblem stackoverflow를 검색하다가 XYProblem이라는 단어를 배웠다. 위키피디아 정의는 다음과 같다. The XY problem is a communication problem encountered in help desk, technical support, software engineering, or customer service situations where the question is about an end user’s attempted solution (Y) rather than the root problem itself (X) 뭐 안내 데스크나, 소프트웨어 개발, 고객 응대할시 발생하는 커뮤니케이션 문제인 것은 알겠는데, 잘 와닿지 않아서 PerlMonks 라는 커뮤니티 사이트에서 언급된 내용을 가져와봤다. Someone asks how to do Y when they really want to do X. They ask how to do Y because they believe it is the best way to accomplish X. People trying to help go through many iterations of “try this”, followed by “that won’t work because of”. That is, depending on the circumstances, other solutions may be the way to go. 그러니까, X(root problem)에 대한 언급 없이 Y(solution)만을 요구하는 문제라고 한다. 이게 문제가 되는 이유는 물어보는 쪽이나 도와주려는 쪽이나 시간적으로, 에너지적으로 많은 낭비가 발생하기 때문이라고 한다. stackexchange 에서 XYProblems가 발생하는 상황을 제시해주었다. 유저는 X하기를 원한다. 유저는 X하는 방벙블 모르지만, 왠지 Y가 해결책이 될수 있다고 생각한다. 하지만 유저는 Y에 대한 해결책 역시도 모른다. 유저는 Y에 대해서 도움을 요청한다. 다른 사람들은 Y의 해결에 도움을 주려고 했으나, Y라는 문제를 해결하려 하는 것이 이상해보입니다. 여러 소통과 시간 낭비 끝에, 유저가 X를 해결하기를 원하고 Y는 X의 올바른 해결책이 아님을 깨닫습니다. 뭔가 내가 회사 생활 처음 했을때나, 주변 신입분들이 이 상황으로 인해서 많이 혼이났던 것 같은… 📌 Cherry Pick의 사용 금일 처음으로 cherry pick을 사용할만한 상황을 마주쳤다. base가 develop인 B 브랜치를 작업하는 도중에, PR이 올라간 A 브랜치 작업 결과물이 필요했다. B 브랜치 작업에 들어가기 전에 A 브랜치 작업물이 필요한 것을 알았으면 B 브랜치를 A 브랜치로부터 땄을텐데, 이를 모르고 작업했다. 그래서 cherry pick을 통해서 A브랜치의 작업 결과물을 가져왔다. ` 추가적으로, PR이 올라간 A 브랜치 작업은 작업 내용이 ‘마커에 이미지가 보여지지 않는 버그 수정’이었다. 하나의 commit으로 버그 수정을 끝낼 수 있었기 때문에 commit 메세지를 ‘버그 수정’이라고 적었다. 왜냐하면 PR의 목적은 제목부터 드러나기 때문에, commit이 하나인 경우, 제목을 ‘버그 수정’이라고 지어놔도 리뷰어 입장에서는 commit이 무엇을 하는지 알수 있기 때문이다. 헌데 다른 브랜치에서 다른 작업을 할 때 ‘버그 수정’이라는 commit을 cherry pick하게 되면 리뷰어 입장에서 무슨 버그를 수정했는지 모르기 때문에 별로 좋은 네이밍이 아님을 깨달았다. .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; }9월 1주차
📌 styled 컴포넌트를 이용해서 스타일 상속받기 style을 상속받아야 하는 상황은 다음 두가지가 존재한다. 스타일 속성이 고정되어 있고, 내용물만 바뀌는 경우 일부 스타일 속성만 고정되어 있고, 스타일 속성이 추가되며 내용물도 바뀌는 경우 1번은 다음 네이버 메일함의 아이콘 리스트가 좋은 예시가 될수 있다. 내용물은 다르지만, 아이콘들은 사이즈나 색깔에 있어서 공통의 스타일을 갖고 있다.(흰색이 적용된 메일함 아이콘은 우선은 논의에서 제외하자) 이때 styled component에서 제공하는 ‘as’ prop이 매우 유용하다. as를 활용하는 방법은 아래와 같다. import { styled } from 'styled-component' import { Mail, Calendar, Cloud } from 'icon-library' const ButtonOfMail = ({ buttonType, isSelected }) => { const buttonTypeToIcon = { mail: <Mail />, calendar: <Calendar />, cloud: <Cloud />, } return ( <Button> <IconButton as={buttonTypeToIcon(buttonType)} isSelected /> </Button> ) } const IconButton = styled('template')(({ isSelected }) => ({ fontSize: '1rem', color: isSelected ? 'white' : 'dark', })) 2번은 ContentEditable을 사용하는 경우를 예로 들수 있다. 특정 요소를 contentEditable로 만들었을 때, 이 요소가 기본적으로 갖게되는 스타일 속성이 사라지고, placeholder를 기본적으로 보여주기를 바랬다. 어떤 요소를 contentEidtable로 만들었을 때, 기본적으로 아래 스타일 속성이 적용되었으면 했다. '&:empty:before': { content: 'attr(placeholder)', display: 'block', fontSize: '1rem', color: 'red' }, '&:focus': { outline: 'none' } 이를 위해서 CommonStyles라는 파일 내에 기본적으로 스타일이 적용되어 있는 컴포넌트들을 만들었다. import { styled } from '@mui/material' export const ContentEditable = styled('div')(({ theme }) => ({ '&:empty:before': { content: 'attr(placeholder)', display: 'block', fontSize: '1rem', color: theme.palette.custom.grey3, }, '&:focus': { outline: 'none', }, })) contentEditable 요소가 필요한 곳에서는 다음과 같이 사용할 수 있다. const ReportContentEditable = styled(ContentEditable)(() => ({ width: '100%', height: '40vh', padding: '0.3rem', margin: '1rem 0', fontSize: '1rem', border: '1px solid', borderRadius: '6px', overflowY: 'scroll', })) .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk17 { color: #808080; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }React와 상태 관리 라이브러리
본 글은 React State Management Libraries and How to Choose의 일부를 번역해놓은 글 입니다. 📌 State 웹에서 버튼을 클릭하면 사이드바가 사라지거나, 메세지를 보내면 채팅창에 나타나는 등 웹은 이벤트에 반응합니다. 이벤트가 발생할 때, 웹은 이벤트에 반응하기 위해서 업데이트가 일어나는데, 이러한 업데이트를 우리는 웹의 상태가 변한다라고 표현합니다. 그리고 사이드바가 사라지는 것 처럼 화면의 모습이 바뀌게 되죠. 웹 개발자들은 ‘사이드바가 보여질지 안보여질지의 결정’, ‘채팅창 내의 메세지’들 등을 전부 상태로 보게됩니다. 그리고 이 상태들의 실체는 결국 데이터입니다. 웹 어딘가에 isSidebarOpen 데이터가 true 혹은 false로 되어있고, chatMessages 배열 데이터가 존재하는 것입니다. 앞서 말한 웹 어딘가는 단순히 컴포넌트 내부의 상태가 될 수 있고 혹은 이번 주제에서 다룰 상태 관리 라이브러리의 store에 존재할수도 있습니다. 📌 State Management 앞서 말했듯이 상태는 웹 어딘가에 저장되어 있습니다. 그리고 State Management는 이 상태를 어떻게 저장하고, 어떻게 변화시킬 것인가를 의미합니다. 저장 장소에 관해서는 useState, useReducer를 호출한 컴포넌트 내부가 되거나, Redux, Mobx, Recoil, Zustand 각각의 store가 될수 있습니다. (본문에서는 window에 저장하는 방법도 이야기하고 있지만, 일반적인 상황도 아닌 것 같고, 마주하지도 못해서 window 관련 내용은 생략하겠습니다) 📌 데이터를 변화시키고 화면을 다시 그리는 것 앞에서 이벤트가 발생하면 상태가 변하고, 상태가 바뀌었기 때문에 화면이 바뀐다고 말했습니다. 하지만 이벤트가 발생한다고 해서 상태가 변하는 것은 아니며, 상태가 변한다고해서 화면이 바뀌는 것은 아닙니다. (1) 이벤트와 상태 변화를 연결시켜야 하고, (2) 상태 변화를 리액트에게 알려야 합니다. 이후 리액트는 (3) 리렌더링을 발생하여 변화한 상태에 맞게 화면을 다시 그려줍니다. 인식하지 못했겠지만 개발자 여러분들은 이 행위를 자연스럽게 해왔습니다. 이벤트 핸들러 내부에서 state setter를 호출하는 것이 (1)에 해당되고, state setter를 호출함으로서 (2), (3)이 자연스럽게 이루어집니다. (2)를 보면 알겠지만 리액트는 이름과는 다르게 다른 프레임워크(Angular, Svelete, Vue)처럼 ‘reactive’하지 않습니다. 이는 리액트가 ‘단방향 데이터 바인딩(one way data binding)’이기 때문입니다. 위 내용을 이해했다면, 아래 버튼을 누를 때마다 count는 분명 증가하는 것을 console을 통해서 확인할 수 있지만, 화면에는 계속 0이 표시되는 이유를 이해할 것입니다. function App() { let count = 0; return ( <> <button onClick={() => { count += 1; console.log(count); }} > CountUp </button> <div>{count}</div> </> ); } state setter의 경우, useState, useReducer, this.setState 이거나 redux, mobx, recoil이 각각의 방식으로 상태 변화를 react에게 알릴 것 입니다. 📌 Data Binding 데이터를 View와 연결하는 것을 의미하며, 데이터의 흐름 방향에 따라서 (1) 단방향 데이터 바인딩(One-way data binding)과 (2) 양방향 데이터 바인딩(Two-way data binding) 두가지로 나뉩니다. 이름에서 느껴지듯, 데이터가 한쪽 방향으로밖에 못 흐른다면 단방향 데이터 바인딩 이라고 하고, 이 경우 데이터가 변해야만 UI가 변합니다. 데이터가 양쪽 방향으로 모두 흐를 수 있다면 양방향 데이터 바인딩이라고 하고, 단방향 데이터 바인딩과는 다르게 UI가 변해도 내부 데이터가 변할 수 있습니다. 📌 useState useState는 단일 값을 저장할 수 있습니다. 만약 단일 값으로 여러 데이터를 갖고 있는 객체를 저장하려고 한다면, 가급적 쪼개는 것이 좋습니다. useState는 3개 혹은 5개를 초과하는 경우, 앱의 변경사항을 예측하거나 추적하기 어렵게 만들 수 있다는 문제점이 있습니다. 특히 이 상태들이 서로에게 의존한다면 더더욱 그렇습니다. 만약 의존성이 복잡하다면, state machine을 고려해보는 것도 좋습니다. 📌 useReducer useReducer의 경우, 한 곳에서 action에 따라서 상태를 업데이트 시킬 수 있는 기능을 제공합니다. useState와 마찬가지로 오직 하나의 값을 저장할 수 있지만, 보통 여러 값을 갖는 객체를 저장하여, 해당 객체를 좀 더 관리하기 쉽게 만들어줍니다. useReducer 용례와 관련한 구체적인 내용은 여기를 참고하는 것을 추천드립니다. 📌 ContextAPI 다음으로 만나게되는 문제는 prop driling 입니다. 리액트 컴포넌트 트리에서, 하나의 컴포넌트가 상태를 가지고 있고, 해당 컴포넌트보다 5 레벨 밑에 있는 컴포넌트가 해당 상태에 접근하려고 할때를 생각해봅니다. 이때 상태를 prop으로서 수동적으로 drill down 해주어야 합니다. 여기서 prop은 property의 줄임말로, 부모 컴포넌트에서 자식 컴포넌트에게 넘겨주는 데이터입니다. 이 문제를 해결하기 위한 가장 쉬운 방법은 React에서 제공하는 ContextAPI를 이용하는 것입니다. 사용법은 아래와 같습니다. // 1. Context를 생성하여 export 합니다. export const MyDataContext = React.createContext(); // 2. 컴포넌트 내에서 drill down할 data를 다음과 같이 넘겨줄 수 있습니다. const TheComponentsWithState = () => { const [state, setState] = useState('whatever'); return ( <MyDataContext.Provider value={state}> <ComponentThatNeedsData /> </MyDataContext.Provider> ); }; // 3. TheComponentsWithState 내부의 subcomponent들은, 다음과 같이 데이터를 꺼내어 사용할 수 있습니다. const ComponentThatNeedsData = () => { const data = useContext(MyDataContext); { ... } } 이러한 간결함에도 불구하고, ContextAPI는 사용 방법에 의존하는 한 가지 중요한 단점이 있습니다. useContext를 호출하는 모든 컴포넌트는 Provider의 value prop이 변할 때 리렌더링이 발생한다는 점입니다. 만약 value prop이 50개의 상태를 가지고 있는데, 이 상태 중 하나만 변경되더라도 useContext를 호출하는 모든 컴포넌트가 리렌더링 되어야 합니다. 이러한 단점을 피하기 위해서, 여러 개의 ContextAPI를 생성하고 연관된 데이터 끼리 묶어놓거나 혹은 라이브러리를 찾게 됩니다. ContextAPI를 사용하면서 놓칠 수 있는 또 다른 문제점은, 아래 코드처럼 새로 생성되는 객체를 넘기는 것입니다. 놓치기 쉬운 문제죠. const TheComponentsWithState = () => { const [state, setState] = useState('whatever'); return ( <MyDataContext.Provider value={{ state, setState, }} > <ComponentThatNeedsData /> </MyDataContext.Provider> ); }; 문제는 TheComponentsWith가 리렌더링 될때마다 state와 state setter를 감싸주는 객체가 새로 생성된다는 것 입니다. 여기까지 이야기를 하고 보면, ContextAPI는 사실 State Management보다는 단순히 상태를 전달하는 역할을 하고 있음을 알 수 있습니다. 맞습니다. 상태는 어딘가에 존재하고, ContextAPI는 단순히 이 상태를 전달해주는 역할에 불과합니다. 📚 참고 문헌 Difference Between One-way and Two-way Databinding in Angular .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk17 { color: #808080; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .mtk8 { color: #CE9178; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }eslint와 prettier
📌 ESLint란? Eslint는 ES와 Lint가 합쳐진 단어입니다. ES는 ECMAScript로 아마도 잘 알고 계시지만, Lint는 처음 봤을수도 있는데요. wikipedia에서는 다음과 같이 정의합니다. 린트(lint) 또는 린터(linter)는 소스 코드 를 분석하여 프로그램 오류, 버그, 스타일 오류, 의심스러운 구조체에 표시(flag)를 달아놓기 위한 도구들을 가리킨다. 느낌이 올듯 말듯… 구글에 Lint를 검색해봅니다. 위와 같이 lint roller가 나오는 것을 확인할 수 있습니다. 오래된 스웨터들을 보면 옷에 삐져나온 보프라기를 때낼 때 사용하는 것이죠. 어떻게 좀 느낌이 오시나요? Lint를 간단히 정의하면 코드 개선을 돕는 도구 입니다. 그리고 ESLint는 정적 코드 분석기(Static Code Analyzer)로, 특정한 코드 스타일을 따르지 않는 코드와 문법적인 에러를 찾아주고, 부분적으로 수정까지 해주는 도구 입니다. 정적 코드 분석이란 프로그램 실행 없이 소프트웨어를 분석하는 것을 의미합니다. 📌 코드 형식 규칙과 코드 품질 규칙 먼저 Linting(코드 개선)에는 크게 두 가지 범주가 존재합니다. 첫 번째는 코드 형식 규칙이고, 두 번째는 코드 품질 규칙입니다. 코드 형식 규칙은 코드의 형식에 관한 규칙입니다. 들여쓰기에 tab이나 space의 혼용을 막는 ESLint의 ‘no-mixed-spaces-and-tabs’ 규칙이 그 예입니다. 코드 품질 규칙은 코드 품질에 관한 규칙으로, 버그를 찾거나 예방할 수 있는 규칙들입니다. 가령 ESLint의 ‘no-implict-globals’는 전역 스코프의 변수 선언을 금지함으로써 변수의 충돌을 예방하는 역할을 하죠. ESLint는 코드 형식 규칙과 코드 품질 규칙 모두 다루지만, Prettier는 코드 형식 규칙만 다룹니다. 그러면 ESLint만 쓰면 되지 왜 Prettier까지 같이 쓰는걸까요? 이유는 Prettier가 ESLint보다 코드 형식 규칙을 더 잘 적용하기 때문입니다. 조금 더 자세한 내용은 아래에서 다루겠습니다. 📌 번외로 EditorConfig란? 코드의 일관성을 위해서 ESLint, Prettier 뿐만 아니라 EditorConfig 역시도 많이 쓰입니다. EditorConfig는 코드 형식 규칙이나 코드 품질 규칙에 관여하지 않습니다. EditorConfig는 팀 내에 여러 IDE 툴을 사용하는 경우에도 코드 스타일 통일이 가능하게 만들어줍니다 📌 Prettier가 ESLint보다 코드 형식 규칙을 어떻게 더 잘 지킬까? 아래와 같이 예시 코드를 하나 준비했습니다. function printUser(firstName, lastName, number, street, code, city, country) { console.log( `${firstName} ${lastName} lives at ${number}, ${street}, ${code} in ${city}, ${country}`, ) } printUser( 'John', 'Doe', 48, '998 Primrose Lane', 53718, 'Madison', 'United States of America', ) ESLint를 다음과 같이 설정합니다. { "extends": ["eslint:recommended"], "env": { "es6": true, "node": true }, "rules": { "max-len": ["error", {"code": 80}], "indent": ["error", 2] } } 위 코드에 적용된 규칙들은 다음과 같습니다. console 문 허용 금지 ( eslint에서 추천하는 규칙들에 포함되어 있습니다. = eslint:recommended ) 코드 최대 문자열 길이 80 들여쓰기는 2칸 그리고 맨 처음 코드를 실행하면, 다음과 같은 에러가 발생합니다. 코드 최대 문자열 길이가 80을 넘었고, console 문이 존재하며, 들여쓰기가 제대로 안되어있다고 error를 보여주고 있습니다. ESLint에서 제공하는 에러 수정 플래그(–fix)와 함께 ESLint를 실행했을 때는 다음과 같은 결과가 나오게 됩니다. ESLint가 max-len과 console문 에러는 수정하지 못했지만, 들여쓰기 에러는 부분적으로 수정한 것을 확인할 수 있습니다. 들여쓰기 2칸, 코드 최대 문자열 길이 80의 규칙을 설정한 Prettier를 실행하면 다음과 같이 코드가 자동 변환하는 것을 확인할 수 있습니다. function printUser(firstName, lastName, number, street, code, city, country) { console.log( `${firstName} ${lastName} lives at ${number}, ${street}, ${code} in ${city}, ${country}`, ) } printUser( 'John', 'Doe', 48, '998 Primrose Lane', 53718, 'Madison', 'United States of America', ) ESLint는 하지 못하는 max-len 수정이 가능해지는 것이죠. 하지만 Prettier는 ESLint처럼 코드 품질에 영향을 줄 수 있는 코드들(console.log)에 대해서 어떠한 경고도 보여주지 않습니다. 그렇기 때문에 코드 형식과 코드 품질 둘 다 잡는 가장 좋은 방법은 ESLint와 Prettier를 동시에 사용하는 것임을 알 수 있습니다. 만약 ESLint와 Prettier, EditorConfig 중 하나만을 사용해야 한다면, 이것은 전적으로 사용자의 선택에 달려있습니다. 하지만 명심하세요. 앞에서 보았듯이 Prettier는 코드 형식 규칙만을 지킬 뿐, 품질 규칙은 제공하지 않습니다. 그렇기 때문에 Prettier를 먼저 고려하기 보다는, ESLint를 먼저 고려하는 것을 추천 드립니다 📌 ESLint와 Prettier의 충돌 ESLint와 Prettier에는 아래와 같이 규칙이 충돌하는 부분이 존재합니다. 우리는 이 충돌을 막아야 합니다. 그렇지 않으면 저장할 때마다 issue에 올라온 무한 르프에 빠지게 됩니다. 이를 해결하기 위해서 Prettier는 코드 형식 규칙만을, ESlint는 코드 품질 규칙만을 다루게 환경을 구성합니다. 물론 겹치는 것 중에 어느 한쪽으로 분류하기 애매한 것들도 존재하지만, 너무 세세한 것 까지는 고려하지 않아도 괜찮습니다. 우리의 관심사는 오직 Prettier와 ESLint가 충돌 없이 하나의 규칙만 다루는 것 입니다. 아래 그림과 같이 말이죠. 📌 ESLint와 Prettier의 충돌을 막기위한 방법 결론부터 말하면, ESLint와 Prettier 이외에 다음 두 가지 라이브러리가 더 필요합니다. eslint-config-prettier eslint-plugin-prettier ESLint와 Prettier가 공존하려면, ESLint에서 Prettier와 충돌이 발생하는 규칙들을 모두 무력화 시키면 됩니다. 이 역할을 1번 라이브러리가 수행해 주는 것이죠. eslint-config-prettier 설치 후 ESLint를 다음과 같이 설정합니다. { "extends": ["eslint:recommended", "prettier"], "env": { "es6": true, "node": true } } 중요한 것은 extends 배열의 나중 요소가, 왼쪽 요소의 설정 내용 중 곂치는 부분을 덮어쓰기 때문에, prettier에게 코드 형식 규칙 적용을 100% 위임하려면, 배열의 마지막 항목에 prettier를 기입해야 합니다. 1번에 대한 설정은 여기까지입니다. 추가적으로, 코드 형식 규칙 적용 및 코드 품질 규칙 적용을 위해 ESLint와 Prettier를 각각 실행하는 것은 비효율적입니다. 이는 2번 라이브러리를 통해서 한 번의 실행으로 ESLint와 Prettier가 적용되게 설정이 가능합니다. eslint-plugin-prettier 설치 후 다음과 같이 추가적으로 설정해줍니다. { "extends": ["eslint:recommended", "prettier"], "env": { "es6": true, "node": true }, "rules": { "prettier/prettier": "error" }, "plugins": [ "prettier" ] } 📌 ESLint와 Prettier에서 설정할 수 있는 규칙은 무엇이 있을까? ESLint와 Prettier의 차이점에 대해서 지금까지 설명해왔는데, 그러면 실제 각각의 라이브러리에서 적용 가능한 규칙이 무엇이 있는지 대략적으로 알아봅시다. ESLint의 경우, ESLint 공식 문서에서 추천하는 설정 규칙들을 몇 개 보자면, // comma-dangle: [2, "never"] 설정시 // Error가 발생하는 경우, var foo = { bar: 'baz', qux: 'quux', /*error Unexpected trailing comma.*/ } var arr = [1, 2], /*error Unexpected trailing comma.*/ foo({ bar: 'baz', qux: 'quux', /*error Unexpected trailing comma.*/ }) // Error가 발생하지 않는 경우, var foo = { bar: 'baz', qux: 'quux' } var arr = [1, 2] foo({ bar: 'baz', qux: 'quux' }) // eslint no-dupe-args: 2 설정시 function foo(a, b, a) { /*error Duplicate param 'a'.*/ console.log('which a is it?', a) } // eslint no-extra-semi: 2 설정시 // Error가 발생하는 경우 var x = 5 /*error Unnecessary semicolon.*/ function foo() { // code } /*error Unnecessary semicolon.*/ // Error가 발생하지 않는 경우 var x = 5 var foo = function () { // code } module.exports = { trailingComma: 'all', // trailingComma는 후행쉼표라고 불립니다. // all을 하는 경우, 객체의 마지막 요소 뒤에 comma를 삽입합니다. // const obj = { // a:1, // b:2, // } // none을 하는 경우, comma가 사라집니다. // 후행쉼표에 대해서는 아래에서 추가적으로 설명할 내용이 있습니다. bracketSpacing: true, // true인 경우, 중괄호 사이에 스페이스를 부여합니다. // { foo: bar } // false인 경우, 중괄호 사이에 스페이스를 제거합니다. // {for: bar} arrowParens: 'always', // 'always'인 경우, 항상 parenthesis를 포함합니다. // (x) => x; // 'avoid'인 경우, 가능하다면 parenthesis를 제거합니다. // x => x; } ※ trailingComma에 대해서 (Trailing comma after last line in object) 버전 관리 툴에 의해 관리되는 코드(version controlled code)라면, trailingComma를 가급적 삽입합니다. 이는 가짜 변경점(spurious difference)을 막기 위해서인데요. 만약 trailingComma: ‘none’인 상태에서 위 obj에 새로운 프로퍼티를 추가하는 경우, 두개의 라인이 변경되었다고 판단하기 때문입니다. // 새로운 프로퍼티 추가 전 const obj = { a: 1, b: 2, } // 새로운 프로퍼티 추가 후 const obj = { a: 1, b: 2, // 변경된 Line 1 c: 3, // 변경된 Line 2 } 📚 참고문헌 린트(ESLint)와 프리티어(Prettier)로 협업 환경 세팅하기 Why You Should Use ESLint, Prettier & EditorConfig What Is a Linter? Here’s a Definition and Quick-Start Guide Set up ESlint, Prettier & EditorConfig without conflicts It this the correct way of extending eslint rules? .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk8 { color: #CE9178; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk3 { color: #6A9955; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }8월 4주차
📌 요소의 프로퍼티 가져오기 요소의 높이(clientHeight)나 스크롤 내 위치(offsetTop)에 접근해야만 하는 로직이 존재했다. 리액트에서 useRef로 DOM 요소의 높이 정보에 접근하기 전에, current의 초기값으로 null을 할당하기 때문에 null 체크도 해주어야 했다. 본인은 non null로 처리를 했는데, 팀장님께서 가급적 javascript로 처리를 하라고 피드백주셨다. (개인적으로 이럴때 만큼은 non null을 써도 되지 않나 싶은…) 그러면 useEffect(() => { const elementHeight = elementRef.current!.clientHeight setElementHeight(elementHeight) }, []) 이랬던 코드가 useEffect(() => { const elementHeight = elementRef.current?.clientHeight if (elementHeight && elementHeight !== 0) { setElementHeight(elementHeight) } }, []) null 체크로 인해서 이렇게 길어지게 된다. 길어보이지 않을 수 있지만, 본인이 하고있는 프로젝트에서는 요소 5개의 clientHeight과 offsetTop을 구해야하기 떄문에 코드가 매우매우 길어지게된다. 그래서 코드 가독성을 조금이라도 개선시키고자 isNil을 추가했다. useEffect(() => { const elementHeight = elementRef.current?.clientHeight if (isNil(elementHeight)) { setElementHeight(elementHeight) } }, []) 위 코드는 원래같았으면 elementHeight에 에러가 발생해야하지만, isNil에 사용자 정의 타입 가드를 사용했기 때문에 에러가 발생하지 않는다. 코드로 본다면 다음과 같다. type isNil = undefined | null const isNil = <T>(param: T | isNil): param is T => param !== undefined || param !== null 📌 곪아 터져버린 타입 폴더 페이지별로 *.d.ts로 타입 파일을 만들어 개발해왔다. 언젠가 타입 충돌이 일어날거라고 예상은 했었는데 오늘 터져버렸다. 동료가 개발하면서 선언한 타입이 내가 선언한 타입과 충돌이 일어난 것이다. husky도 없던터라 merge하기전에 문제를 잡지도 못했다. 어떻게 하면 이러한 상황을 예방할 수 있었을까 생각해봤는데, 현재 폴더 구조에서 100% 예방하는 것은 불가능했다. 물론 새로운 타입을 선언할 때마다, 왠지 충돌이 날것 같은 이름을 지을 때마다 한번쯤 확인할 수는 있지만 명확한 기준도 없고 이는 너무 피곤한 일이다. 다만 조금이라도 이러한 상황에 처하지 않기 위해서 FE 팀장님께 타입 폴더를 다음 세가지로 분류하는 것을 제안드렸다. libraries pages apis 그리고 어느 폴더에도 속하지 않는 common.d.ts를 두는 것이다. 여기에는 window 객체에 프로퍼티를 추가했을 때 필요한 타입들이 존재한다. libraries 폴더의 경우, 현재 족히 5개 이상의 페이지에서 사용중인 구글 맵 라이브러리 혹은 mui 관련 타입을 관리하고, pages 폴더의 경우 클라이언트 상태 혹은 받아온 api에서 재가공된 타입들을 관리한다. 마지막으로 apis는 서버에서 내려주는 순수한 api 관련 타입들을 관리한다. 이렇게 구분한다고해서 타입 충돌을 100% 막을 수있는 것은 아니지만, 빈도는 낮출 수 있으리라 생각되고 FE 팀장님 역시도 흔쾌히 받아들여 주셨다. 📌 Property does not exist on type ‘never’ ts(2339) 최근 작업을 하면서 위 에러를 자주 마주쳤다. 기술 부재로 인해서 매번 타입 단언을 통해서 해결하며 넘어갔는데, 왜 특정 변수를 never 타입으로 인식하는지 궁금해서 찾아보았다. 우선 위 에러가 발생하는 코드는 두가지가 존재하는데, 그중 하나를 stackoverflow 에서 가져왔다. interface Foo { a: 1 } let instance: Foo | null = null const mutate = () => (instance = { a: 1, }) if (!instance) { console.log(null) } else { console.log(instance.a) // 🔥 에러 발생 지점 } 첫 번째 예시는 왜 never 타입에 property가 존재하지 않는다고 표시하고 있을까? never 타입은 명시도 안했는데 어디서 튀어나온걸까? 타입스크립트 핸드북과 앞서 언급한 stackoverflow 답변들에서 해답을 얻을 수 있었다. The never type represents th e type of values that never occur. For instance, never is the return type for a function expression or an arrow function expression that always throws an exception or one that never returns. Variables also acquire the type never when narrowed by any type guards that can never be true. bold처리된 내용을 해석해보면, never타입은 절대로 존재할 수 없는 값에 대한 타입이다. (…) 사실일 수 없는 타입 가드(else 문은 절대로 실행되지 않는다.)에 의해서 타입이 좁혀진 경우, 변수는 never타입을 갖게 된다. else 문은 절대로 실행되지 않는다(사실일 수 없다). 그렇기 때문에 내부 변수가 never 타입을 갖게된다 정도로 해석이 가능한듯 싶다. 아래 두번째 예시가 동일한 에러를 발생하는 이유를 이 관점에서 똑같이 해석할 수 있다. const instance = { bar: null, } console.log(instance.bar?.foo) // 🔥 Property 'foo' does not exist on type 'never' instance.bar는 절대로 사실일 수 없는데 foo 프로퍼티에 접근하려고 하니, never에는 foo 프로퍼티가 존재하지 않는다 정도로 해석이 가능할듯 싶다. .grvsc-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; padding-top: 1rem; padding-top: var(--grvsc-padding-top, var(--grvsc-padding-v, 1rem)); padding-bottom: 1rem; padding-bottom: var(--grvsc-padding-bottom, var(--grvsc-padding-v, 1rem)); border-radius: 8px; border-radius: var(--grvsc-border-radius, 8px); font-feature-settings: normal; line-height: 1.4; } .grvsc-code { display: table; } .grvsc-line { display: table-row; box-sizing: border-box; width: 100%; position: relative; } .grvsc-line > * { position: relative; } .grvsc-gutter-pad { display: table-cell; padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } .grvsc-gutter { display: table-cell; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter::before { content: attr(data-content); } .grvsc-source { display: table-cell; padding-left: 1.5rem; padding-left: var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)); padding-right: 1.5rem; padding-right: var(--grvsc-padding-right, var(--grvsc-padding-h, 1.5rem)); } .grvsc-source:empty::after { content: ' '; -webkit-user-select: none; -moz-user-select: none; user-select: none; } .grvsc-gutter + .grvsc-source { padding-left: 0.75rem; padding-left: calc(var(--grvsc-padding-left, var(--grvsc-padding-h, 1.5rem)) / 2); } /* Line transformer styles */ .grvsc-has-line-highlighting > .grvsc-code > .grvsc-line::before { content: ' '; position: absolute; width: 100%; } .grvsc-line-diff-add::before { background-color: var(--grvsc-line-diff-add-background-color, rgba(0, 255, 60, 0.2)); } .grvsc-line-diff-del::before { background-color: var(--grvsc-line-diff-del-background-color, rgba(255, 0, 20, 0.2)); } .grvsc-line-number { padding: 0 2px; text-align: right; opacity: 0.7; } .default-dark { background-color: #1E1E1E; color: #D4D4D4; } .default-dark .mtk11 { color: #DCDCAA; } .default-dark .mtk1 { color: #D4D4D4; } .default-dark .mtk4 { color: #569CD6; } .default-dark .mtk12 { color: #9CDCFE; } .default-dark .mtk15 { color: #C586C0; } .default-dark .mtk7 { color: #B5CEA8; } .default-dark .mtk10 { color: #4EC9B0; } .default-dark .mtk3 { color: #6A9955; } .default-dark .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); }