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; }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)); }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)); }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; }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; }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; }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)); }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)); }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)); }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)); }