아트집담당 역할왜 Next.js를 사용했나요? ★왜 TypeScript를 사용했나요? ★왜 Recoil을 사용했나요? ★Redux 및 Context API에 대해서 설명해주세요왜 Emotion을 사용했나요?왜 Vercel을 사용했나요?유저의 인증을 어떻게 처리했나요? 이 프로젝트에서 아쉬웠던 점은 무엇인가요? 쿠키를 사용한 이유가 있나요? SWR을 사용하신 이유가 있을까요? React-query가 아닌 SWR을 사용한 이유? 트러블 슈팅 경험 (기술)상태 함께두기에 대해서 설명해주세요★기획 의도 이 프로젝트를 수행하던 중에 겪었던 어려움 (협업)테스팅을 도입한 이유와 내용 ★디바운스와 스로틀링의 차이점 아토믹 디자인 패턴 브랜치는 어떻게 구성되나요? PR과 코드 리뷰는 어떤 방식으로 했나요? 협업 툴은 무엇을 사용했나요? 초록집사캐싱과 프리패칭 scroll 방식이 아니라 IntersectionObserver를 사용하신 이유가 있나요? 인증 관리는 어떤 식으로 하셨나요? 이 프로젝트에서 아쉬웠던 점은? 북레스트컴포넌트 기반의 SPAMVP 디자인 패턴데이터를 어디에 저장하나요? Webpack과 Babel에 대해서 설명해주세요 History API를 활용한 동적 라우팅
아트집
담당 역할
저는 프론트엔드 개발 리드를 하였고,
담당 업무는 후기를 작성할 수 있는 페이지를 개발하였고,
마이 페이지 및 프로필 수정 페이지와 같이
유저와 관련된 페이지를 주로 개발했습니다.
왜 Next.js를 사용했나요? ★
전시회 정보 플랫폼이라는 점에서 SEO가 매우 중요.
Next.js를 통해 SSR 및 SSG를 사용할 수 있다고 생각했기 때문.
전시회 및 후기 관련 페이지에는 SSR을 사용하였다.
단, 이들 페이지가 정적인 페이지는 아니었기 때문에 SSG를 사용하지는 못하였다.
(좋아요 개수 반영 등)
(SSG는 거의 변화가 없는 페이지. 배포 시에만 재생성됨)
한편, 나의 경우는 마이 페이지를 구현했는데
이 페이지는 유저의 사적인 정보가 있는 페이지이기 때문에
SEO로 노출할 필요가 없다고 판단.
굳이 SSR을 사용하지 않고 CSR을 사용하였고,
페이지의 전환을 빠르게 할 수 있었다.
그 외 프레임워크라는 특성에서 얻을 수 있는 협업의 용이성 때문이다.
(폴더 구조가 갖춰져 있음. 편리한 라우팅 기능)
이미지 최적화 등의 기타 옵션 등
Next.js를 사용하면서 어렵지는 않았나요?
Next.js가 익숙하지 않았기 때문에 개발 도중 시행착오.
대표적인 것은 인증 문제.
초기에는 토큰을 로컬 스토리지에 저장하여 관리했는데,
Next.js 서버에서 로컬 스토리지에 접근할 수 없으므로 SSR 시 문제가 발생하였다.
결국 로컬 스토리지가 아닌 쿠키에 토큰을 저장하는 것으로 뒤늦게 바꾸어야만 했다.
그 외에도, react-hydration-error가 종종 발생해서 골치를 썩였다.
이것은 Next.js에 의해 pre-rendering된 리액트 트리와
브라우저의 리액트 트리가 불일치해서 발생하는 에러였는데,
생전 처음 보는 에러였기 때문에 개발 도중에 상당히 머리가 아팠던 것 같다.
기술의 사용법을 익히는 것도 중요하지만,
그 기술의 작동 방식에 대한 근본적이고 정확한 이해가
선행되어야 함을 느꼈습니다.
왜 TypeScript를 사용했나요? ★
자바스크립트는 동적 타입 언어이기 때문에 불안정하다는 단점.
이를 보완하기 위해 TypeScript를 도입하여 사용했습니다.
컴파일 이전단계에서 타입 에러를 잡을 수 있게 됨으로써
디버깅 시간을 크게 단축할 수 있었다.
(cannot read property of undefined 에러)
컴포넌트의 props에도 타입을 정의함으로써 코드를
한층 견고하고 안정적으로 만들 수 있었다.
또한, 백엔드 API 명세서의 필드값을
Interface로 미리 선언하고 사용함으로써
TypeScript가 지원하는 타입 추론,
코드 자동완성과 같은 편리한 기능들을 이용할 수 있었다.
한편, 타입스크립트의 한계도 어렴풋이 느꼈던 것 같다.
Object.keys 메소드를 사용 시, 항상 string 배열로 타입 추론이 판정된다.
(내가 원했던 것은 해당 객체의 key 타입으로 구성된 배열)
이 경우는 어쩔 수 없이 타입 단언을 사용해야 했다.
또한, 생소한 타입인 경우에는 타입 추론이 올바르게 안 되는 경우도 있었다.
예를 들어, Ant Design이라는 UI 라이브러리를 사용했었는데요,
이 라이브러리에 고유한 타입은 타입 추론이 잘 되지 않는 경우도 있었습니다.
TypeScript를 사용함으로써 코드의 가독성이 높아졌느냐?
반반.
코드가 다소 길어진다는 단점도 있었지만,
오히려 타입을 명시함으로써 코드의 의미가 더 명확해지기도 했기 때문이다.
왜 Recoil을 사용했나요? ★
Recoil은 2020년 페이스북에서 만든 새로운 상태 관리 라이브러리.
Recoil은 스토어 선언이 간편하고, 리액트 문법과 친화적이라는 강점.
Redux는 안정적이고 훌륭하지만,
기본적인 store 구성을 위해 많은 코드를 작성해야만 한다.
반면에 Recoil은 스토어 선언이 아주 간편하고,
리액트 훅을 사용하는 것과 거의 유사하게
Recoil의 상태를 사용하거나 갱신할 수 있다는 점이 큰 메리트.
개발된지 얼마되지 않아서 레퍼런스가 적다는 단점은 있으나,
프로젝트의 규모가 그리 큰 편이 아니고
로그인한 유저의 상태를 관리하는 용도라면 충분히 가능할 것이라고 판단.
특히, 새로운 기술을 도입해 사용해보고 싶다는 팀원들의 의견을 수용해서
Recoil을 사용하는 것으로 결정했습니다.
[사용해보니 어땠나요?]
스토어를 선언하고 각 컴포넌트에서 사용하는 것이 아주 편리했다.
또한, recoil에는 effect라는 기능이 있어서
atom이 외부의 다른 값에 의존하게 만들 수 있다.
예를 들어, 우리는 유저의 정보를 저장하는 userAtom이라는
전역 상태를 만들었는데, 이 userAtom이 쿠키의 값을 구독하게 만들었다.
왜냐하면 쿠키에 인증 토큰이 들어있기 때문이다.
쿠키에 들어있는 인증 토큰이 변경되면(로그아웃, 새로고침 등을 이유로)
userAtom의 값이 자동으로 변경되도록 처리하였다.
이 기능이 상당히 유용하다고 느꼈다.
로그인을 하면, 전달받는 유저의 정보로 userAtom의 값을 갱신해준다.
[Recoil의 문법에 대해서 설명해 주실 수 있나요?]
[atom과 selector에 대해서 설명해주세요]
[이것을 어떻게 프로젝트에 적용했는지에 대해 설명해주세요]
그런데 한편으로, Recoil을 사용하면서 우려되고 아쉬웠던 점도 있었다.
스토어를 선언하고 사용하는 것이 너무 쉽기 때문에
전역 상태 관리가 방만해질 가능성이 있다고 느꼈다.
전역 상태가 많으면 버그를 낳을 수 있고 추적하기 어렵기 때문에
지양하는 것이 필요하다.
Recoil의 간편한 문법이 자칫하면 이런 문제를 낳을 수 있지 않을까라고 생각.
처음에는 Recoil을 사용하여 서버 데이터도 캐싱해서 관리하려고 했었는데
이것이 조금 까다롭다는 것을 알게 되었다.
그래서 Recoil은 유저의 데이터를 관리하는 것에 제한적으로 사용하고
서버 데이터의 캐싱은 SWR이라는 라이브러리를 사용하는 것으로
역할을 분리하였다.
Recoil에는 기본적인 상태의 단위인 atom과
그로부터 파생된 데이터를 관리할 수 있는 selector라는 것이 있다.
selector는 함수이고, 이를 통해 비동기 작업을 할 수 있는데
이때 사용하는 atom에 자동으로 의존성이 걸리게 된다.
즉, atom의 값이 동일하면 내부적으로
반환값을 메모이제이션 하고 있어 캐싱된 값을 반환한다.
그런데 리서치를 통해 알아보니,
이 과정이 다소 까다롭다는 것,
특히 캐싱처리를 위해 atom값을 업데이트하는 것이 조금 번거롭다는 것을 알게되었다.
Recoil 상태의 기본 단위는 atom이다.
atom에는 key라는 고유한 값이 부여된다.
Selector는 atom으로부터 파생된 데이터를
선언하고 관리할 수 있다. 프로젝트에서는 Selector를 특별히 사용하지 않았다.
로그인한 유저의 데이터를 userAtom으로 관리
Redux 및 Context API에 대해서 설명해주세요
그 외에도 대안이 두 가지.
Context API, Redux
각각의 장단점을 분석
Context API는 별도의 라이브러리 설치 없이 바로 사용할 수 있다는 장점.
그러나 렌더링 최적화가 되어 있지 않다.
어떤 컨텍스트에서 업데이트가 발생하면
해당 컨텍스트 프로바이더의 하위 트리 전체가 리렌더링된다.
(컨텍스트를 분리하거나 메모이제이션을 통한 최적화가 필요)
컨텍스트를 추가할 때마다 프로바이더로 매번 감싸줘야하므로
프로바이더 헬을 야기할 수 있다.
Redux는 가장 안정적인 상태 관리 라이브러리.
스토어의 상태를 구독하는 최상위 컴포넌트만 렌더링할 수 있으며,
액션을 디스패치할 때마다 기록이 남으므로 에러를 찾기 쉽다.
타임머신 기능을 사용할 수 있다는 점.
아주 작은 기능이여도 액션, 리듀서등을 만들어놔야하므로 코드량이 늘어난다.
물론 Redux-toolkit을 쓰면 간편하게 사용할 수 있고,
다른 미들웨어 라이브러리를 사용한다면 비동기 데이터도 효과적으로 관리 가능.
전역적으로 관리해야 할 상태들이 무엇인지에 관해 팀원들과 논의
- 현재 로그인한 유저의 정보를 저장하는 용도
(아이디, 닉네임, 프로필 사진 등)
- 확실하지는 않지만…
어떤 컴포넌트에서는 props drilling을 피하는 용도로도 사용될 수 있음
리코일의 문법과 사용
atom, selector
왜 Emotion을 사용했나요?
CSS in JS
CSS 로직을 별도의 파일로 분리해서 사용하기보다는,
컴포넌트 단위로 스타일링을 하고 싶었고,
무엇보다 자바스크립트를 사용해 동적으로
스타일을 제어할 수 있다는 점이 큰 메리트라고 판단하여 선택.
특히, 컴포넌트의 props나 state에 따라서
스타일을 동적으로 제어하는 것이 무척 편리하였다)
vs Styled-component
Styled-component와 Emotion은 거의 유사하다.
기능에 큰 차이가 없고, 둘 다 SASS 문법을 사용하는 것도 비슷.
용량이나 속도도 거의 비슷하다.
그럼에도 Emotion을 선택했던 이유는 두 가지.
첫째, 팀에서 Emotion에 능숙한 팀원들이 더 많았다.
이것이 가장 큰 이유.
둘째, SSR을 기본적으로 지원
Emotion은 SSR에서 별도의 설정 없이도 동작한다.
반면에, Styled-component는 별도의 설정이 필요하다.
저희는 Next.js를 사용했기 때문에 이것이 특히 중요했다.
※ Emotion에서 제공해주는
css 속성을 사용하면 이를 클래스로 변환해준다.
왜 Vercel을 사용했나요?
Next.js가 Vercel에서 만들었기 때문에 안정적일 것이라는 기대.
Vercel bot이 자동으로 배포 preview를 보여준다는 장점. (물론 Nextlify도 그렇지만)
유저의 인증을 어떻게 처리했나요?
- 왜 쿠키를 사용했나요?
2. 새로고침 유지를 어떻게 했나요?
이 프로젝트에서 아쉬웠던 점은 무엇인가요?
기술적으로 가장 아쉬움이 남는 부분은 ‘보안’입니다.
쿠키를 사용해서 액세스 토큰과 리프레시 토큰을 한꺼번에 저장했는데,
보안 측면에서 아쉬웠던 부분이 있었습니다.
보통은 서버에서 응답 헤더에 set-cookie를 설정함으로써
API를 요청할 때 토큰을 자동 전송하는 방식을 많이 사용합니다.
그런데 저희는 그런 방식이 아니라,
클라이언트에서 토큰을 쿠키에 저장하고
API 요청 시에 토큰을 꺼내 헤더에 직접 담는 방식을 사용했습니다.
이러한 방식은 XSS(크로스 사이트 스크립팅) 공격에 취약합니다.
이를 막기 위해서는 httpOnly 설정을 해야 하는데,
저희의 방식으로는 브라우저에서 쿠키에 반드시 접근해야하므로,
이것을 설정할 수 없었습니다.
그로 인해, 보안에서 가장 중요하다고 할 수 있는 XSS 공격에
취약해졌다는 점이 가장 큰 아쉬움으로 남습니다.
만일 이것을 개선한다면, 다른 방식으로 접근했을 것 같습니다.
먼저 리프레시 토큰과 액세스 토큰의 저장 위치를 분리합니다.
리프레시 토큰을 httpOnly 쿠키로 설정하고,
액세스 토큰은 자바스크립트 전역 변수에 담아 사용했을 것 같습니다.
그리고 새로고침이 발생하면 리프레시 토큰을 보내어
액세스 토큰을 재발급받는 방식을 취합니다.
헤더에는 리프레시 토큰만이 담기므로 CSRF 공격을 막을 수 있습니다.
또한, httpOnly 쿠키를 설정했으므로 XSS로부터 상대적으로 안전합니다.
보안에는 완전한 정답이 없다고 하지만,
이러한 방법이 상대적으로 더 안전한 방법이라고 생각합니다.
(리프레시 토큰이 CSRF에 의해 사용된다 하더라도
공격자는 액세스 토큰을 알 수 없다.
CSRF는 피해자의 컴퓨터를 제어할 수 있는 것이 아니기 때문)
(Same-site 정책으로 CSRF를 커버할 수는 있으나, 완전한 상태는 아니다)
저희 프론트도, 백엔드도
쿠키와 인증, 보안에 대한 이해가 부족했기 때문이라고 생각합니다.
프로젝트 당시에는 기능을 구현하는 것이 우선했기 때문에,
보안과 같은 안정성은 상대적으로 덜 고려했다고 생각합니다.
쿠키를 사용한 이유가 있나요?
세 가지 대안.
쿠키, 로컬 스토리지, 세션.
로컬 스토리지는 SSR이 적용되었을 때 사용하지 못하므로 제외.
세션을 활용한 방법은 조금 복잡하고, 이전에 사용해 본 적이 없었기 때문에
쿠키를 사용하여 토큰을 저장하였다.
액세스 토큰은 30분,
리프레시 토큰은 2주로 설정
SWR을 사용하신 이유가 있을까요?
SWR을 통해 얻을 수 있던 이점은 크게 네 가지.
첫 번째는 CSR에서 데이터 패칭 로직을 단순화할 수 있다는 점 (선언형)
두 번째는 캐싱을 통해 페이지를 빠르게 전환.
세 번째는 Props drilling을 어느정도 피할 수 있다는 점.
네 번째는 서버와의 동기화를 높일 수 있다는 점.
React-query가 아닌 SWR을 사용한 이유?
둘은 유사한 목적과 원리를 가진 라이브러리.
서버의 데이터를 가져오는 것을 쉽게 해주고
캐싱을 사용하며, 동기화를 높여준다는 점에서 유사하다.
React-query는 사용해보지 않아서 잘 모르겠습니다.
Next.js의 개발팀이 SWR을 만들었으므로, 호환성에 문제가 없을 것이라는 점.
데이터를 패칭(GET)하는 로직에 주로 사용할 예정이었기 때문에
SWR만으로도 충분하다고 판단하였다.
트러블 슈팅 경험 (기술)
- Form 컴포넌트 렌더링 문제
후기를 작성하고 수정할 수 있는 Form을 구현.
Form에서 전체 입력 필드의 상태를 관리하다보니
전체 리렌더링이 과도하게 일어나면서 성능 문제가 발생.
Performance 탭을 사용해서 검사를 해 보니,
유저가 키를 10번 빠르게 눌렀을 때,
약 400ms의 Total Blocking Time이 발생.
Frame time은 약 90ms 정도로 측정됨.
첫 번째는 시도는 메모이제이션.
React.memo를 사용하면 지금 당장은 간단히 해결할 수 있지만,
장기적으로 봤을 때(폼의 규모가 커지고 기능을 추가하는 등)
이상적인 방식이 아니라고 생각.
코드의 복잡성이 증가해서 개발자가 신경써야할 것이 많아지기 때문.
props의 변경 여부를 계속 신경써야 하므로 개발자의 부담이 증가.
useCallback이나 useMemo를 사용하게 되면
코드가 길어지고 신경써야할 것이 점점 많아진다.
또한 리액트는 메모이제이션한 값을 따로 저장해야 하고,
props의 변경을 매번 체크해야하므로 비용이 발생.
메모이제이션보다 덜 복잡하고, 지속적일 수 있는 방식을 고민하고 리서치.
특히, 기존의 Form 관련 라이브러리들이
이 문제를 어떻게 해결하였는가를 리서치.
Formik, React-hook-form 등 해당 라이브러리들의
구현 원리를 이해해서 나의 코드에 반영해보고 싶었다.
가장 인상깊었던 것은 React-hook-form입니다.
잘 알고 계시겠지만,
React-hook-form은 비제어 컴포넌트 방식을 사용한다.
Form이 각 입력 필드의 상태를 관리하거나 제어하지 않고
필요한 순간에만 각 입력 필드의 값을 가져온다.
그렇게 되면, 리렌더링의 범위를 획기적으로 낮추어 성능을 개선.
한편, 이때 알게 된 것이 ‘상태 함께두기’라는 기법이다.
리액트에서의 상태(state)를 위로 끌어올리는 것이 아니라,
밑으로 내리라는 것.
상태를 그것이 사용되는 컴포넌트에 최대한 가깝게 위치시키라는 것.
“코드를 최대한 그것과 연관있는 곳에 배치시켜라”
“상태(state)를 그것이 사용되는 컴포넌트에 가깝게 위치시켜라”
상태 함께두기에 대해서 설명해주세요★
“리액트에서 컴포넌트의 상태를
그것이 사용되는 컴포넌트에 최대한 가깝게 위치시킴으로써”
리렌더링의 범위를 줄여 성능을 개선하는 기법이다.
리액트에서는 보통 상태(state)를 상위 컴포넌트로 끌어올려서 관리.
만일 하위 컴포넌트에서 해당 상태가 필요하다면 props로 전달한다.
이러한 단방향 패턴이 리액트의 핵심이고,
구현에 있어 편리하기 때문에 권장되는 방식이다.
일반적으론 큰 문제가 되지 않지만,
때로는 리렌더링으로 인한 성능 문제를 낳을 수도 있다.
리액트에서는 상위 컴포넌트가 리렌더링이 되면,
하위 컴포넌트 전부가 리렌더링이 되므로.
저는 후기 작성 Form을 구현하면서 이 문제를 경험하였다.
Form과 각 입력 필드가 있고,
Form에서 각 입력 필드의 상태값, 에러값을 모두 관리했는데
그러다보니 폼 전체가 리렌더링이 되면서 성능 문제가 발생.
이를 해결하려면 보통 메모이제이션을 사용하지만,
저는 메모이제이션이 마냥 최선의 방식은 아니라고 생각합니다.
대신에 상태 함께두기 기법을 적용하여,
각 입력 필드에서 자신의 상태를 직접 관리하도록 위임시켰음.
(여기서는 입력값과 유효성 검사를 통한 에러값을 의미)
이렇게 되면 리렌더링의 범위가 줄어들어 성능을 안정화.
한편, Form은 비제어 컴포넌트 방식이 되어
각 입력 필드의 값을 직접 관리하지는 않지만,
폼을 제출할 때는 그 값을 반드시 가져와야만 했다.
저는 여기서 useImperativeHandle 훅을 사용하였다.
부모 컴포넌트에서 자식 컴포넌트의 상태를 참조할 수 있는 훅.
결과적으로, 폼의 기능도 정상적으로 구현할 수 있었고
리렌더링의 범위를 줄여 성능을 안정화할 수 있었다.
한편 이것은 주관적이지만, 메모이제이션을 사용할 때보다
코드가 더 간결해지고,
무엇보다 각 컴포넌트의 관심사와 역할이 분리되고 명확해져서
구조가 효율적으로 개선되었다고 생각합니다.
한편, 이 과정을 통해 깨달은 것이 있다.
제가 사용했던 상태함께두기
그리고 부모가 자식의 state를 ref로 참조하는 방식은
리액트에서 일반적으로 사용되는 방식은 아닙니다.
정론과는 조금 거리가 있는 방식이라고 할까요.
하지만 저는 때로는 그렇게 기존의 틀에서 벗어나서
생각하고 접근하는 것이 필요하고,
그것을 통해 문제를 해결할 수 있다는 것을 배웠습니다.
제가 어떤 프레임워크에서 개발을 하고 있다 하더라도,
항상 그 프레임워크의 틀과 규칙에 매몰되기보다는,
때로는 그 틀을 벗어나서 사고하고 문제에 접근할 수 있다는 것,
그래야만 한다는 것을 배울 수 있었습니다.
이것이 제가 배울 수 있었던 교훈이라고 생각합니다.
한편, 조금 더 고민을 해야 하는 문제들도 있다.
유효성 검사를 할 때 다른 필드의 값을 참조해야 하는 경우이다.
예를 들어, 비밀번호와 비밀번호 확인.
이런 경우는 어떻게 구현할 수 있을까?
컨텍스트 API나 상태 관리 라이브러리를 사용해
해당 입력 필드들만 리렌더링이 발생하도록 구조를 고민해야 할 것 같다.
입력 필드가 동적으로 추가되는 폼은 어떨까?
이것을 구현하려면 어떻게 해야 할까?
React Hook Form 리서치
Form 관련된 라이브러리들은 이 문제를 어떻게 해결하였는가?
그 구현 원리를 이해하고 나의 코드에 적용해보고 싶었다.
기획 의도
전시회를 조회할 수 있는 서비스는 많지만,
유저들이 자신의 후기를 자유롭게 공유할 수 있는 플랫폼은
많지 않다는 것에 대해 팀원들의 의견이 모아져
프로젝트의 주제로 결정하게 되었습니다.
그리고 한편으로, 전시회의 공공데이터를 활용할 수 있으며
전시회, 후기, 유저 등 도메인이 명확하게 나뉘는 것도 개발상의 이점이 있다고 판단.
이 프로젝트를 수행하던 중에 겪었던 어려움 (협업)
테스팅을 도입한 이유와 내용 ★
Form 컴포넌트를 리팩토링 자주 하면서
기능이 제대로 동작하는지 매번 수작업으로 체크를 해야 했다.
이것이 너무나 번거롭게 느껴졌다.
이때 처음으로, 자동화된 테스팅의 필요성을 느낌.
그래서 Jest와 React-Testing-Library를 활용하여
Form의 기능을 체크하는 테스트 로직을 작성했습니다.
그러나 솔직히 말씀드리면, 많은 것을 테스트하지는 못했습니다.
사용자가 입력한 값이 제대로 반영되는지,
유효성 검사가 제대로 동작하는지, 오류 메시지가 올바르게 표시되는지
폼이 제출되었을 때, 각 입력 필드의 값을 올바르게 제출하는가 등을 테스트
처음에는 실제 API를 사용해서 테스트 로직을 짜려고 했는데,
이 API를 mocking해서 사용하는 방법을 알아보고 적용하고 있습니다.
왜냐하면, 실제 API를 사용하는 것은
컴포넌트 테스팅의 목적과 어긋난다고 생각하기 때문이다.
즉, 나는 컴포넌트의 기능을 테스트하고 싶은 것이지,
API가 정상적으로 동작하는가를 테스트하고 싶은 것이 아니다.
1) 테스트의 주요 개념 및 문법
- 단위 테스트(컴포넌트 테스트)
프론트엔드에서는 파일구조를 컨테이너/컴포넌트로 나눌 때
컴포넌트에 해당되는 파일에 테스트를 작성하면 유닛테스트로 봅니다.
- 통합 테스트
통합테스트는 여러 모듈끼리 연결이 있는 코드를 테스트합니다.
프론트엔드에서는 파일구조를 컨테이너/컴포넌트로 나눌 때
컨테이너에 해당하는, 즉 페이지 자체를 테스트하는 코드를 짜면
여기에 해당된다 볼 수 있습니다.
※ unit과 integration은 실제 테스트 코드를 작성할 때 영역이 겹치기도 한다.
- E2E 테스트
Cypress, Selenium 같은 툴로 실제 사용자처럼
개발 환경으로 구성된 사이트에 들어가서
테스트를 하는 코드를 작성하면 e2e라고 말할 수 있습니다.
- get 동기적, 타겟을 찾을 수 없으면 오류
- find 비동기적, 타겟을 찾을 때까지 최대 5초를 기다림 Promise를 리턴한다.
- query 동기적, 타겟을 찾을 수 없으면 null
2) 테스트 커버리지는 몇인가요?
구문(Statements), 분기(Branches), 함수(Functions), 줄(Lines)
테스트 커버리지는 현재 약 80%
그 까닭은 폼에서 이미지를 업로드하는 필드가 있는데,
해당 필드에 대한 테스트는 아직 작성 중이기 때문이다.
이미지 업로드는 Ant design을 사용해서 구현했는데,
그것 때문인지 파일의 업로드를 테스트하는 것이 조금 까다롭다.
현재 알아보고 있는 중이다.
디바운스와 스로틀링의 차이점
아토믹 디자인 패턴
Atoms, Molecules, Organisms, Templates
Organism은 Molecule보다 더 복잡하고
서비스에서 표현될 수 있는 명확한 영역과 특정 컨텍스트를 갖는다.
더 구체적으로 표현되는 반면에, 상대적으로 재사용성이 낮아진다.
브랜치는 어떻게 구성되나요?
크게는 main, develop, feature 브랜치
feature 외에도 refactor, hotfix, chore 등 작업의 종류에 맞게 할당.
먼저 이슈를 만들고, 그 이슈의 번호에 따라 feature 브랜치를 만든다.
ex) feature/#213
작업이 완료되면 feature 브랜치를 원격 푸시한 후,
develop 브랜치로 PR을 날린다.
PR에서 코드 리뷰를 진행하며,
동료들의 approve가 2명 이상이 되어야 merge 될 수 있도록 하였다.
(급한 이슈인 경우 2명이 되지 않아도 merge가 가능하지만,
프로젝트 기간에는 가능한 준수하도록 하였다)
PR과 코드 리뷰는 어떤 방식으로 했나요?
작업이 어떤 종류이냐에 따라 조금 달라진다.
기능을 구현하거나 버그를 고친 PR의 경우,
해당 브랜치로 직접 들어가거나
vercel에서 제공해주는 preview를 통해서 동작을 체크.
그 외 일반적인 PR의 경우,
먼저 폴더 구조를 통해 전반적인 수정 사항을 이해하고
동료가 수정한 코드를 읽으면서,
작성한 의도를 파악하기 위해 노력하는 편이다.
도중에 궁금한 것이 있거나, 조언해주고 싶은 것이 있으면
코멘트를 통해서 리뷰를 남긴다.
특히, 팀의 코드 컨벤션과 어긋나는 것이 있으면 꼭 지적.
전반적으로 큰 문제가 없다면 approve를 해준다.
특히, 프로젝트 초기에 코드 리뷰에 열의를 쏟았다.
코드의 컨벤션을 통일시키는 것이 필요하다고 생각했기 때문.
(Prettier와 ESLint도 사용)
네이밍 규칙
- 인터페이스의 prefix에 I를 붙이지 않을 것
- 핸들러 함수 handle + 컴포넌트 + 이벤트 ex) handleButtonClick
- 인라인 스타일을 사용하는 것을 지양할 것
- 문자열, 숫자는 항상 상수로 사용할 것
- Import / Interface(type) / main / styled / export
협업 툴은 무엇을 사용했나요?
노션과 github의 Projects를 사용하여 스크럼을 관리.
github의 Discussions를 사용하여 문서화.
초록집사
캐싱과 프리패칭
문제의 배경이 무엇이었는지
어떻게 해결했는지
어떤 한계를 느꼈는지
API를 호출해 서버에서 데이터를 불러오는 시간이 조금 오래 걸리는 편이었다.
약 1.2s 가량 소요되었다.
더군다나 해당 API가 사용되는 페이지는
유저의 화면 전환이 자주 발생하는 페이지였다.
이 경우 스켈레톤이나 스피너를 통해 placeholder를 둠으로써 보완하지만,
다른 방식으로 보완할 수는 없을까라는 생각이 들었고,
SWR의 캐싱을 도입해 사용해보았다.
로컬 캐시를 사용함으로써, 페이지 전환 시 즉시 렌더링을 할 수 있었다.
1) 깜빡임 문제
로컬 캐시 직접 제어.
깜빡임 현상을 해결하기 위해 mutate를 사용하여 로컬 캐시 제어.
구현은 하였으나, 업데이트 하는 로직이 조금 복잡하였다.
깜빡임 현상이 용인할 수 있는 수준이라면, 그냥 두어도 괜찮다고 생각한다.
2) 첫 페이지 진입 시에는 캐싱 불가
SWR이 제공하는 프리패칭 기능을 사용해 보았다.
유저가 게시물의 이미지에 마우스를 호버하면,
해당 게시물의 데이터를 미리 불러오는 방식이다.
이를 통해 해결은 할 수 있었지만, 잠재적 문제가 있다는 생각이 들었다.
불필요한 API 요청을 과도하게 발생시켜
서버에 부하를 야기할 수 있는 잠재적 문제.
기존처럼 스켈레톤 및 스피너 UI를 사용하는 방식이
좀 더 안전하고 비용이 적다고 할 수 있으며,
프리패칭은 꼭 필요한 경우에만 조심스럽게 사용하는 것이 필요함을 느꼈다.
잘 사용하면 보약이지만, 잘못 사용하면 독약이 될 수 있다.
신중하게 사용하는 것이 반드시 필요하다고 느꼈다.
scroll 방식이 아니라 IntersectionObserver를 사용하신 이유가 있나요?
스크롤 이벤트는 불필요한 호출이 너무 많이 일어나고,
동기적으로 실행되어 메인 스레드에 악영향을 준다.
스로틀을 사용해 과도한 호출을 제한할 필요가 있다.
(특정 요소의 값을 읽기 위해 getBoundingClientRect 함수를
사용하면 리플로우 현상이 발생할 수도 있다)
반면에 IntersectionObserver는 비동기적으로 실행되므로,
메인 스레드에 영향을 주지 않으면서 변경사항을 관찰할 수 있다.
인증 관리는 어떤 식으로 하셨나요?
토큰은 액세스 토큰만 존재하였고, 리프레시는 없었음.
토큰을 로컬 스토리지에 넣어 저장하였고,
로컬 스토리지에서 토큰을 꺼내 인증 여부를 판단하거나,
인증이 필요한 API 호출 시에 사용하였다.
그런데 지금 돌이켜본다면, 조금 아쉬운 측면이 있다.
먼저 로컬 스토리지에서 매번 값을 꺼내오기 보다는
유저의 로그인 여부를 판단하는 isLoggedIn 등의 값을
전역 변수로 두어 인증 여부를 판단하는 방식이 더 좋았을 것 같다.
(로컬 스토리지에서 값을 꺼내오는데도 비용이 있으므로)
또한, 아무래도 XSS에 취약할 수 밖에 없다는 점.
리액트가 기본적으로 입력값을 이스케이프 처리함으로써
XSS를 막아주기는 하지만, 그럼에도 불구하고 여전히 취약점은 존재.
(dangerouslySetInnerHTML, a 태그의 href 속성을 통한 XSS 등)
보안에 정답은 없다고는 하지만, 조금은 아쉬웠던 부분이다.
(HTML 문자열의 고유 기능을 제거하고 단순 문자열로써 처리)
액세스 토큰과 리프레시 토큰으로 나누고,
마감기한을 설정했으면 더 좋지 않았을까?
이 프로젝트에서 아쉬웠던 점은?
기관에서 제공해 준 백엔드 API를 사용했는데
조율할 수 있는 상황이 아니었다.
태그 검색 시,
백엔드 API의 한계를 극복했던 경험.
북레스트
컴포넌트 기반의 SPA
좀 더 자세히 설명해주세요.
MVP 디자인 패턴
MVP는 Model, View, Presenter의 약자 입니다.
하나의 애플리케이션을 구성할 때 그 구성 요소를 세 가지의 역할로 구분한 패턴.
Model은 데이터 및 이러한 데이터의 가공을 관리하는 컴포넌트입니다.
View는 화면에 보여지는 인터페이스 요소입니다.
Presenter는 View와 1:1의 관계를 갖습니다.
사용자의 입력이 View를 통해서 들어오면
Model에게 요청하여 데이터를 받은 뒤, View에 다시 전달합니다.
사용자의 입력이 View를 통해서 들어오면
Model을 가공하여 View에 다시 전달해 준다.
Presenter와 View는 1:1의 관계이다.
View와 Model의 의존성이 없다.
View와 Presenter의 의존성이 높다.
- 왜 MVP 디자인 패턴을 적용하셨나요?
프로젝트에서 구조를 체계적으로 정립하는 것의 중요성을 느꼈기 때문입니다.
어떤 파일 또는 컴포넌트의 역할을 효과적으로 분리할 필요가 있다고 느꼈습니다.
예전에도 자바스크립트로 간단한 프로젝트를 한 적이 있었는데요….
기능들을 하나 하나 추가함에 따라 코드의 양이 늘어났고,
이것을 모듈로, 파일로 분리해야 하는데
어떤 기준으로 분리하면 좋을지 난감했습니다.
그래서 리서치를 통해 MVC 디자인 패턴이라는 것을 알게 되었고,
이를 적용하여 프로젝트를 만들게 되었습니다.
1) 목표했던 바를 얻었나요? 무엇을 느꼈나요?
네, 특히 기능을 개발할 때 보다는
버그를 수정하거나 리팩토링 단계에서 아주 유용하다고 느꼈습니다.
(화면에서 문제가 발생하면 뷰, 데이터에서 문제가 발생하면 모델을 확인하면 되므로)
구조가 체계화되고 각 파일들의 역할과 관심사가 분리됨에 따라
코드에 대한 이해가 높아졌고, 작업이 한결 편리해졌습니다.
한편, 그 엄격함 때문에 다소 번거롭게 느껴지기도 했다.
간단한 기능을 추가하는데도 여러개의 파일을 왔다갔다 해야했기 때문이다.
이것에서 조금 불편하기도 했지만 그럼에도 불구하고,
구조가 체계화되고 명확해짐으로써 오는 이점이 훨씬 컸기 때문에
좋은 선택이었다고 생각합니다.
그런데 다시 돌이켜보면,
당시에는 학습과 경험이 부족해서 잘 몰랐지만….
제가 구현했던 것은 MVC 패턴보다는 MVP 패턴에 좀 더 가깝다는 생각이 듭니다.
(Presenter)
MVC 패턴은 사용자의 입력이 컨트롤러로 바로 들어오고,
컨트롤러는 뷰를 단지 선택할 뿐, 직접 업데이트하지 않는다.
모델이 뷰를 직접 업데이트한다.
반면에, MVP 패턴은 사용자의 입력이 뷰를 통해서 프레젠터로 들어오고,
프레젠터는 모델을 업데이트한 뒤, 이 결과를 바탕으로 뷰를 업데이트한다.
내가 구현한 것은 후자에 가깝다.
당시에는 학습과 경험이 부족해서 잘 몰랐지만….
MVP와 MVC 패턴의 차이, 각각 설명
MVC는 Model, View, Controller의 약자 입니다.
하나의 애플리케이션을 구성할 때 그 구성 요소를 세 가지의 역할로 구분한 패턴.
Model은 데이터 및 이러한 데이터의 가공을 관리하는 컴포넌트입니다.
View는 화면에 보여지는 인터페이스 요소입니다.
Controller는 데이터와 인터페이스를 중재하는 역할을 합니다.
뷰로부터 변경 내용을 통지 받으면 이를 각 구성 요소에게 통지합니다.
사용자가 어플리케이션을 조작하여 발생하는 변경 이벤트들을 처리하는 역할도 수행합니다.
2) MVC 디자인 패턴은 보통 백엔드에서 하는데…?
프론트엔드라고 못할 것은 없다고 생각합니다.
- MVC 외에 다른 디자인 패턴을 알고 계신가요?
MVP, MVVM, FLUX 패턴에 대해서 알고 있습니다.
데이터를 어디에 저장하나요?
네 로컬 스토리지를 사용해 데이터를 저장하고 있습니다.
그렇지만 서버에 저장하는 방식보다
안정적이지 못하다는 점에서 한계가 있다고 생각합니다.
온전한 서비스가 되기 위해서는
프론트엔드만으로는 충분하지 않고, 백엔드가 동반되어야 함을 느꼈습니다.
최근에는 백엔드 API와 서버를 쉽게 구축할 수 있도록 도와주는
STRAPI라는 툴을 공부하고 있는데요,
이것을 사용하여 추후 리팩토링할 계획을 가지고 있습니다.
Webpack과 Babel에 대해서 설명해주세요
History API를 활용한 동적 라우팅
pushState, replaceState로 url의 history 스택을 수정.
그리고 변경된 url을 리딩하여 그에 맞게 페이지를 새롭게 갱신.
뒤로가기 등 popstate 이벤트 발생 시에도
변경된 url을 리딩하여 페이지를 새롭게 그려준다.
동적 라우팅은 url에 id값 등 path variable이 동적으로 달라질 수 있는 것.
일반 라우팅과 유사하고, 이것은 정규 표현식을 사용하여 처리.