프로필사진


2022.03.02

2022년 2월

반응형

1. 들어가며

한 달 동안 했던 것들을 정리한다는 목표를 정하니 매일 작업하면서 작성하면 좋을 것들을 메모하는 습관이 생겼습니다. 이러한 변화는 좋다는 생각이 듭니다.

 

서울특별시 광진구

2. 이번 달에는 무엇을 했는가?

A. 여행

 이번 달은 우연한 기회로 여행을 다녀오게 되었습니다. 업무에서 벗어나 서울에 위치한 두 곳의 호텔에서 지내게 되었습니다. 나름 호캉스를 즐기고 뻥 뚫린 전망을 보면서 쉴 수 있었습니다. 룸 서비스를 시켰는데 치킨이 맛있어서 사진을 찍어봤습니다. 다음에 다시 이 호텔에 갈 일이 생긴다면 시켜먹어야겠다는 생각했습니다. 

B. 개인 프로젝트

  이번 달 개인 프로젝트는 블로그에 적용할 스킨 만들기를 해봤습니다. 기본으로 제공하는 스킨들은 뭔가 아쉽다는 생각에 직접 만들기로 했습니다. 이전부터 조금씩 블로그 스킨 작업을 해봤었는데 워낙 많은 기능을 생각하려다 보니 끝내지 못한 경우가 발생해서 2월 한 달간 집중해서 처음 정한 기능까지만 완성을 목표로 작업을 했습니다. 

 

  작업을 진행하면서 몇 가지 느낀 점이 있습니다. 티스토리는 강력한 도구를 제공하고 있으면서도 많은 허점이 있고 문서가 부족하다는 생각이 들었습니다. 예를 들면, 하단 이미지처럼 각 글의 하단에 댓글의 개수를 담고 싶었지만, 리스트에서는 제공하는 댓글 수는 글에서는 제공하는 API가 없다는 점이 아쉬웠습니다. 

  이번 한 달 내에 개인 시간을 투자해서 모든 기능을 구현하는 것은 불가능하다고 판단을 했고 최소한 기능을 결정해서 만드는 방향으로 선택했습니다. 이번 작업에서는 리스트, 글, 메뉴까지만 만드는 것을 목표로 삼았습니다. 간단하게 블로그 스킨을 만든 과정에 대해 정리해 봤습니다.

 

Step 1. 간단하게 디자인하기

  간단하게 레이아웃을 PPT로 만들고 넣고 싶은 항목들을 여러 사이트나 블로그에서 혹은 기본으로 제공하는 스킨에서 가져와 내용을 채웁니다. 이 과정에서 티스토리 API에서 지원 여부도 확인합니다. (이 부분에서 글 하단에 댓글 수를 표시할 수 있다고 착각하는 실수를 했습니다...)

 

Step 2. 티스토리에서 기본으로 제공하는 스킨 다운로드

  티스토리에서 기본으로 제공하는 스킨들은 티스토리 API 기반으로 만들어졌기 때문에 충분한 예제로 사용할 수 있을 거라 판단했습니다. 필요한 기능이 있는 5개의 스킨을 다운로드하여 하나의 폴더에 담았습니다.

 

Step 3. 화면을 구성하고 스크립트를 작성

  "Step 1"에서 구성했던 내용을 바탕으로 화면을 구성하고 js를 작성합니다. 테스트를 위해 "manage > 꾸미기 > 스킨 편집 > html 편집"을 통해서 html, css를 복붙 하는 방법을 채택했습니다. (참 쉽죠?)

 

출처 : https://mandugun.tistory.com/8

C. 회사생활

  연말평가를 통해서 동료의 조언을 듣고 나니 약간의 목표와 더 열심히 해야겠다고 생각했습니다. 이번 달에는 작업해야 할 것들이 좀 많아서 개인 프로젝트를 작업할 여력이 없었지만, 3월에는 어느 정도 여유가 있을 것 같아 미뤘던 사내 프로젝트를 진행하려고 합니다.

D. 도서

  웹 어셈블리 도서를 선물 받아서 오랜만에 책을 읽기 시작했습니다. 아직 1/4 정도 읽었지만 대략 어떤 느낌인지는 알겠지만 깊게 들어가니 하나도 무슨 소리인지 모르겠다고 생각했습니다. 아무래도 아직 프로그래밍 언어가 동작하는 방식에 대한 지식이 부족한 것으로 생각합니다. 두어 번 더 읽어보고 이번 기회에 이해하는 방향으로 가야겠습니다. 관련해서 이해되고 정리가 된다면 글을 써보는 것도 좋겠다는 생각했습니다.

E. 프로젝트 경험 - 함수 실행 시 종료될 때까지 재요청을 무시하기 2

  2022.02.01 - [일기장] - 2022년 1월 에서 작성된 내용 중 "E. 프로젝트 경험 - 함수 실행 시 종료될 때까지 재요청을 무시하기" 부분에 이어서 추가된 사항이 있었습니다. 

import { useEffect, useRef, useState } from 'react';

export default function useFunc(func, delay = 1) {
  const [flag, setFlag] = useState(true);
  const idRef = useRef();

  useEffect(() => () => clearTimeout(idRef.current), []);

  return async () => {
    if (flag) {
      return;
    }
    setFlag(false);
    const beforeUrl = window.location.href;
    try {
      await func();
    } catch (e) {
      throw e;
    } finally {
      idRef.current = setTimeout(() => {
        if (beforeUrl !== window.location.href) {
          return;
        }
        setFlag(true);
      }, delay);
    }
  };
}

  위 코드는 이전에 작업했던 코드를 그대로 가져왔습니다. 이후 여러 테스트를 진행하다가 페이지 이동이 아닌, 같은 페이지에서 컴포넌트 갱신이 발생하면서 unmount 된 이후에 API응답이 왔을 경우 setFlag가 호출되면서 memory leak 경고가 발생하는 버그를 찾았습니다. 

 

  이 부분을 보안하고자 idRef에 약간의 트릭을 추가했습니다. unmount 될 때, finally에서 unmount 되었는지 알 수 있는 방법이 필요했고, idRef값의 기본값이 undefined라는 것에 힌트를 얻었습니다. idRef 자체가 값이 비어있다면 undefined이고 unmount 되었을 경우 null을 가지게 함으로써 finally에서 구분이 가능하도록 했습니다.

import { useEffect, useRef, useState } from 'react';

export default function useFunc(func, delay = 1) {
  const [flag, setFlag] = useState(true);
  const idRef = useRef();

  useEffect(() => () => {
    clearTimeout(idRef.current);
    idRef.current = null;
  }, []);

  return async () => {
    if (flag) {
      return;
    }
    setFlag(false);
    try {
      await func();
    } catch (e) {
      throw e;
    } finally {
      if(idRef.current === null){
        return;
      }
      idRef.current = setTimeout(() => setFlag(true), delay);
    }
  };
}

  finally에서 if문을 추가함으로써 setTimeout이 실행하기 전 unmount가 되었을 경우를 대응할 수 있게 되었습니다. 기존에는 URL 변경되었을 경우 요청을 방지하는 코드를 추가했으나 이는 페이지 이동이 발생했을 경우에만 유효했지만 이번에 적용한 방법으로 URL 변경뿐만 아니라 컴포넌트 갱신에도 적용할 수 있게 되었습니다. 

F. 프로젝트 경험 - 전역 에러 핸들링 하기

  프로젝트에서 React를 사용하고 있었고 전역으로 발생하는 에러는 componentDidCatch와 getDerivedStateFromError를 사용해서 처리하고 있었습니다. Redux와 함께 조합해서 사용할 땐 문제가 없었지만, (지금 생각해보면 있었을 것 같지만) react-query를 사용하면서 API에러가 발생했을 경우 componentDidCatch와 getDerivedStateFromError으로는 잡을 수 없다는 것을 깨닫게 되었습니다. 해당 이슈를 해결하기 위해 리서치를 하다가 아래 글을 보게 되었습니다.

https://eddiewould.com/2021/28/28/handling-rejected-promises-error-boundary-react/

 

Handling rejected promises in React with an error boundary

Treat unhandled promise rejections (exceptions in async code) consistently with regular synchronous exceptions in React.

eddiewould.com

  요약하자면, componentDidCatch와 getDerivedStateFromError는 비동기 과정에서 발생한 에러는 잡을 수 없습니다. 해당 이슈를 해결하기 위해 componentDidMount, componentWillUnmount를 사용해서 window객체의 unhandledrejection 이벤트를 추가해 해결할 수 있었습니다. 하지만 글을 쓰다가 추가적으로 리서치를 진행하니, 해당 이벤트는 IE에서 지원하지 않는다는 이야기가 있어서 아무래도 다른 대안을 찾아야 할 것 같습니다.

G. 프로젝트 경험 - ErrorBoundary와 함께 ReduxSaga와 같은 Provider를 사용할 때 순서

  ErrorBoundary와 함께 ReduxSaga와 같이 Provider를 사용하는 라이브러리를 사용할 때, 순서가 중요하다는 점을 모르고 있다가 우연히 console의 경고를 보게 되었습니다. Saga의 에러를 ErrorBoundary에서 캐치해야겠다는 생각으로 ErrorBoundary 하위 컴포넌트로 Provider를 위치시킨 상태로 진행하고 있었고 에러가 발생했을 때 ErrorBoundary가 캐치함과 동시에 경고를 발생하고 있어서 Provider의 하위 컴포넌트로 ErrorBoundary를 위치하는 형태로 개선했습니다.

H. 프로젝트 경험 - history 뒤로 가기 시 상태 전달하기

  사용자가 특정 페이지에서 뒤로 가기를 했을 때 주의나 안내를 해야 하는 기능이 필요했습니다. 처음 생각했던 방법은 "뒤로 가기"에 힌트를 얻어 history 객체를 사용하는 방법을 도입했습니다.

 

  history 객체는 stack의 내용을 볼 순 없지만, 페이지 이동이 발생하면 push, 뒤로 가면 pop이 발생하므로, pop이 발생되어 특정 페이지 이전 페이지에 도달하게 된다면, 주의나 안내를 표시하는 형태로 작업을 진행했습니다. 하지만 우리의 iPhone Safari에서는 예상했던 것처럼 pop이 발생하지만 그 직후 새로고침이 발생하면서 history가 초기화되는 현상이 발생했습니다. 즉, 잠깐 주의나 안내가 표시되지만 사용자가 인식하기도 전에 바로 사라져 버리는 이슈가 발생했습니다. 다음 대책으로 useState를 고려했지만 새로고침이 발생한다면 동일하게 초기화될 것으로 판단했고 URL Query로 flag 값을 추가하는 방법도 생각했지만 페이지를 pop 했다가 replace 처리했다가 push 하는 과정이 필요했고 특정 페이지의 다음 페이지로 이동한 다음 뒤로 가기 했을 때도 pop 했다가 replace 처리 후 push 해야 하는 복잡한 과정이 더 추가되기 때문에 좋은 사용자 경험을 줄 수 없을 것이라는 판단 했습니다.  

 

  마지막으로 생각한 방법은 localStorage를 사용하는 방법을 도입했고 특정 페이지 접근 시 localStorage에 flag값을 저장하고 특정 페이지 이전 페이지에서는 localStorage 값 여부에 따라 주의나 안내를 표시하는 형태로 분기 처리를 진행했습니다.

I. 프로젝트 경험 - og tag의 canonicalUrl

  프로젝트에서 SSI를 사용하고 있었으나 일부 페이지에서 예상했던 og tag를 가져오지 않는 이슈가 발생했습니다. 이는 중복 캐싱 방지를 위한 canonicalUrl 값이 있었기 때문으로 판단되었습니다. canonicalUrl 값은 해당 링크의 og tag와 같은 값을 가진다는 의미로 처음에는 해당 값에 대해 몰랐다가 정상적으로 응답이 오는 og tag와 문제가 발생하는 og tag를 비교하면서 알 수 있었습니다. 이후에는 정상적인 값을 바라볼 수 있도록 수정하여 이슈를 해결했습니다.

J. 프로젝트 경험 - 삼성 브라우저의 다크 모드

  페이지에서는 다크 모드를 지원하지 않지만, 삼성 브라우저의 경우 자체 로직을 통해 강제 다크 모드를 제공하고 있습니다. 이는 치명적인 버그가 있는데 일부 이미지의 경우 자체 로직으로 다크 모드가 적용되지 않아 UI가 깨지는 현상이 발생합니다. 이러한 버그는 작업하고 있던 페이지에서도 발생했는데 다크 모드가 활성화되었는지 여부를 프론트에서 확인할 수 있는 방법이 없다 보니 노운 이슈로 가기로 했습니다. 물론 사용자가 브라우저 설정에서 해당 이슈를 해결할 수 있지만, 이 서비스 하나 때문에 사용자에게 강요하는 것도 문제가 있을 거라는 판단 했습니다.

k. 프로젝트 경험 - CaseSensitivePathsPlugin 이슈

  Storybook development mode로 실행했을 때에는 문제가 없었지만 build를 했을 때에 오류가 발생했습니다. 분명 어디서 많이 본 에러인데 매번 까먹고 찾아보는 것 같아서 오래 기억하고자 작성하게 되었습니다. 프로젝트의 전체적인 폴더 구조 개편이 있었는데 우리의 Git 친구(혹은 OS)가 파일의 대소문자를 구분하지 않아 발생하는 이슈로 전체적으로 제거하고 다시 PULL 하는 형태로 이슈를 해결했습니다. 수작업으로 파일명을 수정하는 형태로 해결이 가능하지만, 실수하거나 놓치는 부분이 있을 수 있으므로 PULL 하는 형태가 가장 깔끔하고 좋은 것 같습니다.

ERROR in ./stories/test.stories.js
Module not found: Error: [CaseSensitivePathsPlugin] `/project/front/src/pages/Test.jsx` does not match the corresponding path on disk `pages`.
 @ ./stories/test.stories.js 165:0-55 168:13-22 226:91-100 283:91-100
 @ ./stories sync ^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.js)$
 @ ./generated-stories-entry.js
 @ multi ./node_modules/@storybook/core-client/dist/esm/globals/polyfills.js ./node_modules/@storybook/core-client/dist/esm/globals/globals.js (webpack)-hot-middleware/client.js?reload=true&quiet=false&noInfo=undefined ./storybook-init-framework-entry.js ./node_modules/@storybook/addon-docs/dist/esm/frameworks/common/config.js-generated-config-entry.js ./node_modules/@storybook/addon-docs/dist/esm/frameworks/react/config.js-generated-config-entry.js ./node_modules/@storybook/react/dist/esm/client/preview/config-generated-config-entry.js ./node_modules/@storybook/addon-backgrounds/dist/esm/preset/addDecorator.js-generated-config-entry.js ./node_modules/@storybook/addon-backgrounds/dist/esm/preset/addParameter.js-generated-config-entry.js ./node_modules/@storybook/addon-measure/dist/esm/preset/addDecorator.js-generated-config-entry.js ./node_modules/@storybook/addon-outline/dist/esm/preset/addDecorator.js-generated-config-entry.js ./node_modules/@storybook/addon-actions/dist/esm/preset/addDecorator.js-generated-config-entry.js ./node_modules/@storybook/addon-actions/dist/esm/preset/addArgs.js-generated-config-entry.js ./.storybook/preview.js-generated-config-entry.js ./generated-stories-entry.js

3. 마무리

  이번 달에도 다양한 시도를 해볼 수 있는 시간을 가질 수 있어서 좋았습니다. 삼성 브라우저의 다크 모드에 대해서도 이전에 글을 읽어본 적이 있어서 나름 빠르게 대응할 수 있었고 canonicalUrl의 경우에는 처음 알게 되어서 새로운 지식을 얻어갈 수 있었습니다. history 객체에 대해서는 좀 더 알아볼 필요가 있을 것 같고 전역 에러 핸들링에 대해서는 좀 더 리서치를 해야겠습니다. 이번 글도 2월 말일에 딱 올리려고 했는데 다른 일정 때문에 3월에 올리게 되었습니다. 말일에 몰아서 작성하기보다는 중간중간 내용을 작성해야 할 필요성도 느꼈습니다. 제목만 적으니.. 어떤 내용이었는지 생각이 떠오르지 않았습니다... 3월에서도 더 많이 배우고 더 시도하고 좋은 결과가 있었으면 좋겠습니다. 

반응형

'일기장' 카테고리의 다른 글

2022년 3월  (0) 2022.04.02
2022년 1월  (0) 2022.02.01
2021년 회고  (0) 2022.01.23
다른 '일기장'의 최근 글