본문 바로가기
📚 Diary/회고

2022 - 끄적임

by 파크park 2022. 7. 19.

이력서 작성

코드스쿼드 과정 중 코드 리뷰를 빰빰에게 받았었는데, 빰빰에게 이력서 를 받아서 모의면접을 진행해볼 수 있는 기회를 주셔서 이력서를 작성했다. 사실 이전에 사용하던 Notion 으로 만든 이력서가 있었는데 코드스쿼드 이전에 작성했던 것이라 프로젝트 관련한 내용이 거의 없다싶이 하고, 프론트엔드 관련된 기술이 거의 없어서 수정해나가는 작업을 했다.

 

구글에 '개발자 이력서' 라고 작성하면 가장 먼저 나오는 워니 님의 글과, EO 에서 만든 컨텐츠도 참고해보았을 때, 

결과와 지표로 설명이 되는 것이 이상적이겠지만, 사용자가 없는 토이프로젝트에서는 가장 어려운 부분이라고 생각이 들었다.

 

프로젝트의 기획서에 존재하지 않지만 스스로, 혹은 팀원과 상의하여 사용자 경험 측면에서 더 좋은 방향으로 기능 구현을 진행해나간 적이 많아서 사용자 경험을 향상시키기 위해 어떤 고민을 했는지를 중점적으로 작성했다. 

 

Test 코드 공부..!

이 부분에 대해서 사용자가 없는 토이프로젝트가 측정할 수 있는 실질적인 지표는 리액트 Profiler 를 통한 렌더링 속도, 횟수 측정, 데이터 통신부분, Chrome Lighthouse 등이 떠올랐었다.

 

의도적으로 수치를 확인하기 위해 issue-tracker 프로젝트에서 Component 를 Memoziation 하여 리렌더링 최적화 하는 부분이라던지, Fetch 를 위한 라이브러리 사용을 하지 않는 등 1, 2주차에서는 최대한 빠르게 사이트를 완성하고 3주차에 테스트와 리팩터링을 거칠 예정이었다.

 

테스트코드에 대한 학습이 부족하기도 했고, 로그인 관련한 이슈가 계속 생겼기 때문에 3주차에 계획대로 진행하지 못하고 리팩터링을 진행하지 못했는데, 해당 부분을 진행해보기 위해서 React 에서의 TDD 는 어떻게 하는지? 를 공부해보고 있다.

 

오늘 공부한 내용중 신선했던 부분은 React 의 Component 는 View 의 역할만 하도록 하고, 비즈니스 로직을 별도로 분리하여 해당 비즈니스 로직만 Unit-test 할 수있는 MVP 패턴을 사용하는 방법이었다. 나는 useReducer 와 dispatch 를 사용하는 패턴이 익숙했는데, 패턴에 크게 상관없이 비즈니스로직과 컴포넌트를 분리해서 테스팅한다는 것에 힌트가 되었다.

 

Hook 을 테스트 하는 방법으로 hook 테스팅 라이브러리를 사용할 수도 있지만, 비즈니스 로직 자체가 리액트와 커플링되는 것이 바람직 한지 고민도 되었다. 비즈니스로직이 분리된다면 테스트성이 좋아지고, 로직이 완전히 분리되기 때문에 React 든, Vue 든, VanilaJS 이든간에 사용될 수 있어 역할이 명확해진다는 것이 좋았다.

 

일단 저번에 NPM 에 배포했던 React Calendar 의 테스트코드를 작성하는 것을 목표로 공부해볼 생각이다.

 

테스트 관련 읽어볼 글들

- https://kentcdodds.com/blog/common-mistakes-with-react-testing-library

- https://velog.io/@velopert/tdd-with-react-testing-library

- https://jbee.io/react/testing-0-react-testing-intro/

 

 

React Test

React 를 테스트하는 툴에 대해서 Enzyme 와 React Testing Library (RTL) 의 차이를 알아보고, 내부구현사항에 의존하는 테스트(Enzyme)는 테스트성이 떨어진다고 생각하여  RTL 을 선택했다.

 

React-TS-boilerplate 를 기반으로 생성한 레포지토리에서 testing 을 위한 라이브러리들을 추가해나갔다.

 

가장 먼저 test 의 기본 틀인 jest 를 다운받고, 자동완성과 타입지정을 위해 @types/jest 를 다운받았다.

이외 사용하면서 오류가 생기거나 필요하다고 판단되는 라이브러리만 설치 추가할 생각으로 진행했다.

 

1.  Path alias (절대경로)

가장 먼저 alias path 에 이슈가 생겼다. (절대경로 지정)

프로젝트에서 절대경로를 사용하는 것을 선호하는데, jest 가 테스트를 하면서 해당 절대경로를 불러오지 못하는 문제였다.

루트경로에 jest.config.js 를 만들어 절대경로관련 옵션을 추가해 해결하였다.

 

module.exports = {
  moduleNameMapper: {
    '^@components(.*)$': '<rootDir>/src/components$1',
    '^@presenters(.*)$': '<rootDir>/src/presenters$1',
  },
};

 

이제 사용하고자하는 폴더경로가 생기면 tsconfig.json 과 webpack.config.js 와 jest.config.js 세 곳에서 수정+추가를 해주어야 하는데 너무 x 1000 번거러울 것 같다. 이전에 tsconfig.json 과 webpack.config.js 둘 에서만 사용할 때에도 불편했는데 하나가 더 추가되면 더더더 귀찮을 듯 했다.

 

하나의 path alias 파일을 통해서 injection 할 수 있는 방법을 찾아봐야 겠다.

 

2. jsx tsx 확장자명? render 함수 내 jsx 문법 사용 이슈

보통의 테스트코드 예제는 .ts 로 파일명을 짓고, import React 와 babel-jest 를 사용하여 jsx 문법을 사용할 수 있도록 하는 것으로 보였다. .ts 로 파일명을 지으니 render 함수가 jsx 문법을 이해하지못해 오류를 내뿜었고, 나는 그냥 테스트파일의 확장자명을 .tsx 로 수정하여 jsx 문법을 사용하겠다고 하고 사용했다. 테스트파일은 .jsx .tsx 확장자 를 사용하면 안되는지 찾아보니 그런 이슈는 없는 걸로 확인했다.

 

다음으로 생긴 이슈는 jsdom 관련한 이슈였다.

test 파일에서 @testing-library/react 에서 제공하는 render 함수를 사용하니 document 를 찾지 못하는 문제였다.

해당 문제는 jest-environment-jsdom 를 다운받고, jest.config.js 에 testEnvironment 속성으로 jest-environment-jsdom 을 지정하여 해결하였다.

 

// package.json
  "devDependencies": {
    ...
    "jest-environment-jsdom": "^28.1.3",
    ...
  }
 
// jest.config.js

module.exports = {
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@components(.*)$': '<rootDir>/src/components$1',
    '^@presenters(.*)$': '<rootDir>/src/presenters$1',
  },
};

 

3. @testing-library/user-event 버전 이슈 (@13 -> @14)

다음 문제는 userEvent 를 동작시키는 데 생겼다.

 

fireEvent 를 사용하려다가, fireEvent 보다 실제 사용자가 event 를 호출하는 것 처럼 사용되는 것이 userEvent 라고 하여 userEvent 를 사용하고자 했다.

 

그런데 아무리 userEvent.click 메서드를 실행해도 toHaveBeenCalledTimes 가 0 으로 찍혔다.

혹시나 싶어 fireEvent 를 실행해보니, 해당 이벤트는 잘 실행되었었다.

 

찾아보니 내가 참고한 코드는 userEvent@13 의 코드였고, 공식문서에서 userEvent@14 로 업데이트 된 이후의 코드를 설명하고 있었다. 이전에는 userEvent.click 으로 바로 사용했지만, @14 버전부터는 user 를 setting 하고, 해당 user 인스턴스를 바탕으로 이벤트를 실행시키면 되었다.

 

describe('Todo Add Form', () => {
  let onTodoAdd;
  let input;
  let button;
  let user;

  beforeEach(() => {
    onTodoAdd = jest.fn();
    user = userEvent.setup();
    render(<TodoAddForm onTodoAdd={onTodoAdd} />);
    input = screen.getByPlaceholderText('할일 입력');
    button = screen.getByRole('button');
  });

  it('if doesn`t exist input current, it is not called', async () => {
    await user.click(button);
    expect(onTodoAdd).toHaveBeenCalledTimes(0);
  });

  it('load todo add form', async () => {
    await user.type(input, 'new todo');
    await user.click(button);

    expect(onTodoAdd).toHaveBeenCalledTimes(1);
    expect(onTodoAdd).toHaveBeenCalledWith('new todo');
  });
});

 

 

Swagger

프로젝트 하나를 기획해보는 과정에서 백엔드를 어떻게 구성할 것인지에 대해서 먼저 데이터베이스 모델을 설정해보았다.

이후 백엔드와 협업하며 아쉬웠던 점을 보완할 수 있는 프로젝트가 되었으면 좋겠어서 무엇이 아쉬웠나 되돌아보았을 때

 

1. API 를 간단히 테스트해볼 수 없음 (mock api 의 부재)

2. API 명세가 수시로 바뀜 (바뀌는건 어쩔 수 없다 하여도, 문서를 통해 공유하여 문서에 반영되지 않는 경우가 있음)

 

등이 있었다. 그래서 백엔드를 이번에 설계해보면서 실제 Production 환경에서 쓰이는 서버와 간단히 테스트해볼 수 있는 서버 (heroku 등으로 배포) 를 별도로 두어 설계를 해볼생각이다.

 

코드스쿼드 과정 중 프론트끼리만 프로젝트를 진행한 적이 몇 번있는데, BB 와 프로젝트할 때 백엔드 부분을 맡아 진행했던 적이 있다.

원래는 JSON server 를 사용해 간단히 할 생각이었지만.. api 가 다양하게 생기고 router 를 분리하고싶은 욕심이 생겨 백엔드 서버를 구축했었다.

 

그 때 어떻게 효율적으로 API 문서를 전달할 수 있을지? 고민을 했었는데, 고민끝에 Swagger UI 를 도입해 사용했었다. 직접 postman 이나 insomnia 같은 API platform 을 사용해 테스트해보는 것 처럼 웹에서 간단히 API 들을 테스트해볼 수 있어서 좋았던 기억이 있다.

 

 

 

이때 Swagger 를 써보면서, Swagger + express 의 조합이 공식문서에 의해 소개되진 않고있어 세팅하는데 꽤 애먹었던 기억이 있다. 

그래서 본격적으로 백엔드 서버를 구축해보기 전 이때의 삽질기억을 떠올려서 Swagger 작성과 관련한 글을 작성해보려 한다.

 

React Batching

Jamie 의 React Batching 관련한 글을 흥미롭게 읽었다.

이 역시 BB 와 프로젝트를 할 때 Custom 한 MyReact 를 (peact.js) 만들면서 실제 setState 의 동작방식이 비동기로만 동작한다고 이해하고 사용했었는데, 왜 비동기인지? setState 가 여러번 불렸을 때 왜 한번만 불리는 것 같은지? 에 대해 읽기 좋게 써주셔서 술술 읽었다.

 

글을 읽으면서 궁금했던 두 가지가 있었다.

1. batching 을 하는 기준 시간이 16ms 라는 점

2. setState -> 16ms 이상의 시간이 걸리는 로직 -> setState 라면 batching 이 안되는가? 

 

1번은 Jamie 가 답변을 해주셨는데, 사용자가 보통 60fps 를 사용하니 1000(ms) / 60 = 16.66ms 이므로 16ms 의 간격으로 batching 하는 것이라는 거였는데 충분히 끄덕여지는 이유였다.

 

2번은 간단히 테스트해볼 때에는 알 수 없었다. 이벤트루프와 관련한 코드의 실행순서를 좀 더 고민해봐야 왜 그런지 이해하여 테스트해볼 수 있는건가 싶긴하다, 관련해서 어떤 처리를 하는지 실제 코드를 보고싶다는 생각을 했다.

reflow

DOM 요소의 기하학적 속성이 변경될때, 브라우저 사이즈가 변할때, 스타일시트가 로딩되었을때 발생하는 변화들을 다시 계산 해주는 작업이다. 

DOM Tree 와 CSSOM Tree 가 결합된 Render Tree 가 화면에 어떻게 배치되어야 하는지 위치를 계산하는 Layout 단계로 돌아가 다시 Paint 작업을 하게 된다.

 

무한 스크롤을 구현하면서 스크롤 이벤트 시 사용하는 getBoundingClientRect 메서드가 reflow 를 일으킨다고 대략적으로 알고 있었는데, 오터가 구현한 코드를 보니 getBoundingClientRect 메서드 말고 scrollHeight 를 사용해 해결한 것을 보았다.

scrollHeight 는 reflow 가 안일어나나? 찾아보니 getBoundingClientRect 말고도 reflow 를 일으키는 다양한 이유 (+ hover..) 들이 있었다. 

Input 의 focus 같은 것은 필수불가결한 느낌이 있어서 어떻게 바꿔야할지도 고민해보았는데 결국 최소화(minimizing) 하는 것이 최선이라는 생각이 들었다.

aliases path

잘 사용하고 있는 React-ts-boilerplate 에 Jest 를 추가하여 개선했다.

jest 를 사용하다보니 jest 역시 절대경로에 문제가 있었는데, 별도의 config 를 설정해주면 되었다.

그런데 그렇게되면 하나의 path 가 추가될 때 마다 tsconfig.json, webpack.config.js, jest.config.js 세 곳에 모두 path 를 추가해줘야 하니 여간 귀찮은 일이 아니었다.

 

그래서 aliases.js 라는 파일을 별도로 만들어서 webpack 과 jest 에서 사용할 수 있도록 모듈화하여 aliases 내부에서 한 곳에 추가하면 webpack 과 jest 는 따로 수정할 필요가 없도록 개선했다.

 

왜 tsconfig 는 .js 확장자를 지원하지 않는걸까? 나랑 똑같은 생각을 하고 있는 사람들이 많나보다.. 

 compilerOptions.path dynamically is needed ... !!!!!!!!!!!!!

찾아보니 storybook 도 해당 설정이 필요할 것 같은데, 진즉에 해놓길 잘했다는 생각이 든다.

 

로그인 로직 작성중

백엔드 구성을 하면서, github login 로직을 작성하고 있는데 이게 프론트를 통한 callback 을 테스트해보기가 가능한가? 싶었다.

그래서 해당 로그인 로직이 정상적으로 동작하는지 알 수가 없었어서,, 프론트 작업도 조금씩 시작해보려 한다.

프론트는 애초에 빠른 사용을 위해 mui 를 사용할 까 하다가, 나중에 혹시라도 다른 분들이 작업해주실 수도 있지 않을까 싶어서 확장성을 위해 사용하지 않는 것으로 결정했다. mui 스럽게 레고처럼 컴포넌트를 만들어 보고,, 이 기회에 storybook 도 공부해볼까 싶다.

 

Storybook 도입

- 깃헙 로그인을 테스트하기 위하여 프론트 프로젝트를 boilerplate 를 통해 생성하였는데, 기술적인 도전을 시도하고 싶었던 것 중 하나인 storybook 을 도입하고 시작하고자 마음먹었다. storybook 을 도입하는데 햄디 글이 가장 도움이 되었다. 

햄디 글과의 차이점은 .storybook 의 설정파일인 main.js 에서 webpack 의 builder 가 webpack5 이냐 @storybook/builder-webpack5 이냐의 단순한 차이가 있었는데 둘 다 같은걸 불러오는 것 같다.

 

Webpack 5
Storybook builds your project with Webpack 4 by default. If your project uses Webpack 5, you can opt into the Webpack 5 builder by installing the required dependencies (i.e., @storybook/builder-webpack5, @storybook/manager-webpack5) and update your Storybook configuration as follows:

https://storybook.js.org/docs/react/builders/webpack

 

단 하나 아쉬운점은 npm dependency 관련 충돌이 많이 나는 것 같다.

npm audit fix 를 실행했다가 eslint 관련 다양한 설정 모듈들이 날아가서 다시 설치를 해줬었다.

webpack 4 가 default 라고 되어있고, react 관련해서도 createRoot 가 아닌 render 로 앱을 실행하는 것을 보면 react v17 이 default 이지 않을까 추측된다. 그래서 현재 쓰고있는 react-ts-boilerplate 에 적용시키는 것은 일단 보류하려고 한다.

 

Alias 추가 개선

Storybook 을 도입하면서 storybook 도 절대경로를 따로 설정해줘야 했지만 기존에 썼던 webpack 의 alias 를 사용하면 됐어서 별도의 파싱을 할 필요는 없었다.

 

그런데 tsconfig.json 은 내부에 외부에서 import 한 값을 사용할 수없으니 tsconfig.json 과 aliases.js 두 파일을 수정해야 했다.

생각해보니 json 은 js 에서 import 해와서 쓸 수 있으니까 tsconfig.path.json 으로 별도로 분리하고 aliases.js 에서 tsconfig.path.json 을 import 해 사용하면 되겠다는 생각이 들어서 해당 생각으로 개선을 시작했다.

 

결론은 잘 사용된다. 이젠 경로를 수정할 일이 있을 때 tsconfig.path.json 만 수정하면 된다.

// aliases.js

const path = require('path');
const tsconfigPath = require('./tsconfig.path.json');

const aliases = Object.entries(tsconfigPath.compilerOptions.paths).map(
  ([key, value]) => {
    // "@components/*" -> "@components"
    const k = key.slice(0, key.length - 2);
    // "src/components/*" -> "src/components"
    const v = value[0].slice(0, value[0].length - 2);
    return [k, v];
  },
);

const webpackAliases = Object.fromEntries(
  aliases.map(([key, value]) => [key, path.resolve(__dirname, value)]),
);

const jestAliases = Object.fromEntries(
  aliases.map(([key, value]) => [`^${key}(.*)$`, `<rootDir>/${value}$1`]),
);

module.exports = {
  webpack: webpackAliases,
  jest: jestAliases,
};
// tsconfig.path.json

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@components/*": ["src/components/*"],
      "@presenters/*": ["src/presenters/*"],
      "@theme/*": ["src/theme/*"]
    }
  }
}
// tsconfig.json

{
  "compilerOptions": {
    "outDir": "./build",
    "target": "ESNext",
    "module": "commonjs",
    "moduleResolution": "Node",
    "declaration": true,
    "jsx": "react-jsx",
    "allowJs": true,
    "strict": true,
    "noImplicitAny": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src", "webpack"],
  "exclude": ["node_modules"],
  "extends": "./tsconfig.path.json" // <- extends 로 추가
}

 

로그인 구현중..

오늘 프론트단에서 로그인을 위한 버튼을 만들어서 백엔드서버를 켜고 몇가지 테스트를 해봤다.

일단 백엔드에서 Redis 를 써보는 건 처음인데, Redis 에 jwt token 을 저장하기 위한 부분도 막히고, Github OAuth Login 을 통해서 받은 token (access-token)을 저장할지, 아니면 User 의 정보를 최초로 한번만 가져오고 해당 User 의 정보를 통해 데이터베이스에서 해당하는 유저를 로그인 시키는 방향으로 할 지 고민이 된다. 어플리케이션에서 Github User 의 Resource 를 요청할 일이 없다고 판단이 되기때문에 후자로 진행할 듯 싶다.

 

프로젝트의 진행이 더뎌진 이유는 Storybook 을 도입하면서 무엇을, 어디까지 Storybook 을 통해 테스팅하고 어떤 단위로 컴포넌트를 만들어야 할 지에 대한 고민이 생겼기 때문이다. 그래서 화요일에는 Storybook 과 관련한 컨퍼런스를 참고하기도 하고, 컴포넌트 설계에 대한 한재엽님의 글을 참고하기도 했다.

 

컴포넌트를 어떻게 설계할 것인지에 대한 정답은 정해져있다라기 보다는 컴포넌트를 작성하는 패턴에 대해서 이유를 가지고 사용하는 것이 중요하겠다는 생각이 들었다.  그래서 일단은 데이터를 다루는 로직에 대한 부분은 되도록 hook 에서 다루는 방향으로 진행해보려한다.

 

VSC Snippet

프로젝트를 시작하면 다양한 파일들을 많이 만들어야 하는데, 보통 비슷한 형식의 코드를 작성하는 경우가 많다.
특히나 스토리북은 지정된 템플릿을 복+붙 해와서 사용하기 때문에 이와 관련된 Snippet 을 만들어 사용하는 것이 훨씬 편리하겠다는 생각이 들어서 Snippet 을 만들어봤다. 글 링크
필요한 게 더 생기거나 한다면 해당 글에 올린 json 을 수정해나가면서 보완할 생각이다.

Github API

github api 에서 user 로 접근하면 email 과 관련된 정보를 얻을 수 있다는 명세에 따라서 그대로 사용했는데 null 이 나왔다.
왜 null 이 나오는지 이유를 찾아보니 유저가 email 을 공개할 것인지 아닌지를 설정하는 부분이 있어서 만약 사용자가 email 에 대한 접근을 public 하게 두지 않았다면 별도로 email 을 요청하는 API 를 작성해야 한다.

  const userRequest = await axios.get(`${githubApiUrl}/user`, {
    headers: {
      Authorization: `token ${access_token}`,
    },
  });

  const { email, avatar_url, name } = userRequest.data;
  console.log(email); // null

 

github application 에서 permission 관련 설정도 변경해야한다.

  const { access_token } = tokenRequest.data;
  const {
    data: { avatar_url, name },
  } = await axios.get<{ avatar_url: string; name: string }>(
    `${githubApiUrl}/user`,
    {
      headers: {
        Authorization: `token ${access_token}`,
      },
    }
  );


  const { data: emailData } = await axios.get(`${githubApiUrl}/user/emails`, {
    headers: {
      Authorization: `token ${access_token}`,
    },
  });


  const { email } = emailData.find(
    (emailInfo: { primary: boolean; verified: boolean }) => {
      const isPrimary = emailInfo.primary === true;
      const isVerified = emailInfo.verified === true;
      return isPrimary && isVerified;
    }
  );

Webpack v5 Asset Module 

기존에 쓰이던 file-loader, url-loader, raw-loader 등을 대체할 수 있다. 

type 으로 사용하고자 하는 유형을 선택하면 되는데, asset 만 입력하면 webpack 의 기본 조건(8kb) 에 맞춰 계산하여 크기가 낮은 것은 inline, 높은 것은 resource 로 처리한다.

 

msw 사용 관련

msw (0.45.0) 를 오늘(22.08.30) 다운받아서 사용하는데, 뜬금없는 에러가 발생했다.

 

 

msw 는 이전에 dependency 로 graphql 을 설치받아 사용하고 있었는데, 0.45 버전 부터 devDependency 로 옮겨 사용하고있어 문제가 생겼다.

 

문제가 생긴 이유는 해당 Pull Request 때문인데, PR 에서는 15 버전의 graphql 도 사용하기 위해 graphql-js 를 peer dependency 로 사용해야 한다고 이야기한다. 16의 버전을 사용하도록 강제하면 안된다는 것이다.

 

단일 버전을 사용하도록 강제하는 경우, 만약 다른 버전의 graphql 을 사용하게 되면 두 가지 버전의 graphql 이 존재할 수 있다.

중복된 graphql 모듈을 서로 다르기 때문에 동시에 사용할 수 없도록 에러를 발생시킨다고 한다. Error 관련 코드 

 

이와 같은 상황을 의도적으로 발생시키자면, 아래와 같다.

 

일부러 msw 의 0.44.2 버전의 패키지의 의존성을 보면, graphql 이 ^16.3.0 을 의존하는 것을 볼 수 있다.

 

graphql 의 버전은 ^16.3.0 이 다운로드 될 예정이므로, >=16.3.0  < 17.0 인 graphql 이 다운로드 될 것이다.

그런데 만약 나의 프로젝트가 graphql 15 버전을 사용한다면, msw 를 사용할 수 없을 것이다.

PR 은 이를 위해 15 혹은 16 버전을 peer 로 하도록 요청했다.

추가적으로 peerDependencyMeta 라는 옵션을 통해 peer dependency 인 graphql 을 다운받지 않았어도, 경고메세지가 생기지 않게 하였다.

 

 

 graphql 이 msw 에 의해 다운받아져서, 그것이 번들링의 일부가 되기를 꺼린 것으로 보인다.

사용자가 15 버전을 다운받아 사용할지, 16 버전을 다운받아 사용할 지 msw 개발자는 알 방법이 없으므로 특정 graphql 의 버전을 다운받아 포함시키기보다는 사용자가 의도적으로 graphql 을 다운받아 사용하길 바라는 듯 하다.

 

To be absolutely clear: graphql is an optional peer dependency of MSW:
Optional, because graphql is not required for MSW to execute; Peer, because we have no intention of bundling graphql as a part of MSW. That is also a bad idea since GraphQL versions mismatches would be horrible.

 

그렇다고 해서 사용자가 graphql 을 쓰지도 않는데 다운받게 하는게 맞는건가? 싶기도 하다.

당장에 필요하다면 0.44.x 버전을 쓰거나, graphql 을 다운받아 사용하면 되긴 하지만, 패키지 구경을 좀 더 해볼까 싶다.

 

Dropdown click away

dropdown 의 클릭 이벤트 중, 드롭다운 이외의 영역을 클릭 시 isOpen state 가 변경되도록 하는 로직을 만들었다.

CSS 의 가상요소를 활용했는데, Open State 를 변경하기위한 별도의 태그를 만들기 보다 해당 방법이 의도하는 바를 알기 쉽다고 생각하여 선택했다.

 

const Dropdown = ({ children }) => {
  const dropdownReducer = useReducer(reducer, initState);
  const [state, dispatch] = dropdownReducer;
  return (
    <DropdownContext.Provider value={dropdownReducer}>
      <div
        css={css`
          position: relative;
          &::before {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            content: '';
            z-index: 90;
            display: ${state.isOpen ? 'block' : 'none'};
          }
        `}
        onClick={() => {
          if (state.isOpen) {
            dispatch({ type: 'TOGGLE_OPEN' });
          }
        }}
      >
        {children}
      </div>
    </DropdownContext.Provider>
  );
};

trigger 의 위치를 기반으로 position 을 잡기위해 만들었던 div 태그에 ::before 가상요소를 만들어 Dropdown 의 상태를 Toggle 하도록 하였다.

 

font import...

이전에 font import 를 index.html 에서 해결했던 경험이 있다.

styled components 에서 새로운 컴포넌트가 특정 이벤트에 의해서 렌더링되면 새로운 클래스를 넣기 위해 style 태그를 변경하는데, 

styled components 에서 font 를 import 하면 해당 과정에서 폰트를 다시 불러오는 현상 때문이었다.

 

그런데 해당 해결법이 배포시 문제가 된다는걸 다시 깨달았다.

build 를 하면 webpack 이 하나의 js 로 번들링 해주는데, 폰트를 public 폴더 하위에 존재하게 두면 배포를 위한 빌드에서 폰트를 포함시키는게 곤란했다.

여러모로 생각해보니 그냥 하나의 js 로 번들링 시키는게 낫겠다 싶어 app.css 를 만들어 import 하여 쓰는걸로 다시 변경했다.

 

@font-face {
  font-family: Roboto;
  font-style: normal;
  font-weight: 400;
  src: url('./assets/fonts/noto-sans-kr-regular.woff2') format('woff2');
}
@font-face {
  font-family: NotoSansKR;
  font-style: normal;
  font-weight: 400;
  src: url('./assets/fonts/roboto-regular.woff2') format('woff2');
}

 

import './app.css';
...

const App: React.FC = () => {

  return (
    <ThemeProvider theme={isDarkMode ? darkTheme : lightTheme}>
      <GlobalStyle />
      <Routes />
    </ThemeProvider>
  );
};

export default App;