🌈 구현 기간
기간: 2022.08.22 ~ 2022.08.29🌈 배포
https://hwjs-portfolio.vercel.app/🌈 용도
컴퓨터공학과 4학년이 되고 취업하기 위해 내 프로젝트와 자기소개를 하는 간단한 웹 포트폴리오를 제작하였다.🌈 그 외_
처음 사용하는 기술 스택인Next.js
+Typescript
+Recoil
+React-query
를 사용, 해당 기술을 학습하는데 목적을 두었다.
전역 상태 라이브러리인 Recoil
을 사용하여 다크모드 State를 관리, 커스텀 훅(useDarkMode)을 구현하였다.
import { useRecoilState } from 'recoil';
import { themeState } from '@/atom/atom.theme';
export const useDarkMode = (): [string, () => void] => {
const [theme, setTheme] = useRecoilState(themeState);
const toggleTheme = () => {
if (theme === 'light') {
setTheme('dark');
} else {
setTheme('light');
}
};
return [theme, toggleTheme];
};
Recoil에서 다크모드 테마를 dark
, light
로 설정, selector함수를 통해서 dark
, light
에 해당하는 테마를 선택하도록 구현하였다.
import { atom, selector } from 'recoil';
import { v1 } from 'uuid';
import { lightTheme, darkTheme } from '@/stylesheets/theme';
export const themeState = atom({
key: `themeState${v1()}`,
default: 'light',
});
export const themeSelector = selector({
key: `themeSelector${v1()}`,
get: ({ get }) => {
const theme = get(themeState);
return theme === 'light' ? lightTheme : darkTheme;
},
});
Expectation Violation: Duplication atom key "themeState". This is a FATAL ERROR in production. But it is safe to ignore this warning if it occurred becaused of hot module replacement.
Recoil의 atom은 항상 고유한 key값을 가져야 하는데 그렇지 못해서 발생하는 에러라고 한다. 이는 Next.js + Recoil에서 많이 발생하는 에러라고 한다.
uuid
라는 난수 생성 라이브러리로 atom의 고유 key 값에 생성한 난수를 추가하여 에러를 해결하였다.
.env
파일에 notiontoken
과database_id
정보저장
NEXT_PUBLIC_NOTION_TOKEN=...
NEXT_PUBLIC_NOTION_DATABASE_ID=...
{Next_Public_...}
을 붙여주면 브라우저 영역에서도 해당 데이터 값을 인식하게 된다.
- useQuery를 이용한 API 데이터 받아오는 함수 구현
import axios from 'axios';
import { TOKEN, DATABASE_ID } from '@/config/index';
import { ProjectAPI, ProjectResult } from '@/types/axios.types';
const options = {
method: 'POST',
url: `https://api.notion.com/v1/databases/${DATABASE_ID as string}/query`,
headers: {
Accept: 'application/json',
'Notion-Version': '2022-02-22',
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN as string}`,
},
data: { page_size: 100 },
};
// notion에서 db 정보 가져오기
export const getNotionApi = async () => {
return axios.request<ProjectAPI<ProjectResult>>(options).then((res) => res.data);
};
- Project 컴포넌트에 해당 데이터 렌더링하기
getStaticProps
를 통해 API 데이터를 미리 가져오고 그 데이터로 정적 사이트를 구현한다. JS파일 가져오기 및 Hydrate과정이 이루어진다....
const Project: NextPage = () => {
const [showChild, setShowChild] = useState(false);
const project = useQuery<ProjectAPI<ProjectResult>>(['project'], async () => getNotionApi());
// 클라이언트 측 하이드레이션이 표시될 때까지 대기
useEffect(() => {
setShowChild(true);
}, []);
if (!showChild) {
// 처음 자리표시자 UI를 표시할 수 있다.
return null;
}
return (
<>
<HelmetProvider>
<Helmet>
<meta lang="ko" />
<meta charSet="utf-8" />
<meta name="description" content="이 문서는 준승의 프로젝트 소개 페이지입니다." />
<meta name="author" content="황준승" />
<title>준승's potofolio</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Rajdhani:700" />
</Helmet>
</HelmetProvider>
<ProjectList pj={project} />
</>
);
};
export default Project;
export const getStaticProps: GetStaticProps = async () => {
const queryClient = new QueryClient();
await queryClient.prefetchQuery<ProjectAPI<ProjectResult>>(['project'], async () => getNotionApi());
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
Error: Hydrate failed because the initial UI does not match what was rendered on the server.
공식문서에 의하면 사전렌더링된 React tree(SSR/SSG)와 브라우저에서 첫 번째 렌더링 중에 렌더링된 React 트리 간 차이가 있을 때 발생한다고 한다.
예)
function MyComponent() {
// window 객체에 따라 값이 달라진다.
// 값이 정확히 뭔지 모른다.
const color = typeof window !== 'undefined' ? 'red' : 'blue'
// 이는 서버사이드 렌더링(window 객체 X)과 초기 렌더링(window 객체 O)할 때 값이 달라질 수 있다.
return <h1 className={`title ${color}`}>Hello World!</h1>
}
따라서 초기렌더링과정과 서버사이드 렌더링할 때의 state값을 모두 동일하게 맞추어줘야한다.
공식문서에 따르면 useEffect
문으로 클라이언트 측 하이드레이션이 표시될 때까지 대기한 다음 렌더링을 한다면 초기 렌더링 페이지와 서버 사이드렌더링 페이지를 일치시킬 수 있다.
- 컴포넌트 재사용성 고려
여러 컴포넌트에 쓰일 Button
, Link
, Blind
컴포넌트 등을 구현
다양한 스타일링 적용을 위해 props 속성에 따른 스타일링 세분화
import { css } from 'styled-components';
import { shadow } from '@/stylesheets/utils';
import { ButtonStyleProps } from '@/types/common.types';
export const ButtonStyle = css<ButtonStyleProps>`
width: max-content;
outline: none;
border: none;
background-color: inherit;
white-space: nowrap;
font-weight: 600;
padding: 0.5rem;
padding-bottom: 0.4rem;
border-radius: 2px;
cursor: pointer;
transition: .2s all;
&:active {
transform: translateY(3px);
}
${({ variant, colorScheme }) => (variant === 'solid') && css`
background-color: ${colorScheme};
color: white;
&:hover {
background-color: white;
color: ${colorScheme}};
${shadow(1)};
}
`}
${({ variant, colorScheme }) => (variant === 'outline' || variant === 'link') && css`
color: ${colorScheme};
&:hover {
background: ${colorScheme};
color: white;
${shadow(1)};
}
`}
`;
getStaticProps
vsgetServerProps
getStaticProps
: Static Static Generation 으로 빌드할 때 데이터를 불러온다.
getServerProps
: Page가 요청받을 때 마다 호출되어 pre-rendering한다.
해당 프로젝트에서는 한번의 API 호출을 함으로 getStaticProps
를 선택하여 정적 페이지를 만들었습니다.
자세한 내용은 후에 블로그 글에 남기도록 하겠습니다.
- type 선언 방법(type vs interface)
typescript에서는 type을 선언하는 두 가지 방식이 있습니다.
저는 유니온 연산자와 병합 연산자를 사용할 수 있는 type 선언 방식
을 선호합니다.
자세한 내용은 후에 블로그 글에 남기도록 하겠습니다.
- typescript 재사용성 고려하기
Typescript는 구조적 타이핑이다.
선언한 타입에 대해서 재사용성을 최대한 고려하여 구현하도록 노력하였다.
Typesciprt의 구조적 타이핑에 대해서는 후에 블로그 글에 대해 자세히 다루도록 하겠습니다.
- type-guard
예기치 못한 데이터 타입(null | undefined)에 대해 타입가드를 구현하는 식으로 구현하였다.
비록 1주차에서는 test code를 구현하지 못했지만 해당 프로젝트에서 어떻게 쓸까에 대해서 고민을 많이 하였다.
React-test-Library는 행위 주도 테스트로 사용자가 애플리케이션을 이용하는 관점에서 테스트 코드를 작성한다.
저 같은 경우 프로젝트 초반에 헤더 스타일링, 푸터 스타일링하는 부분에서는 테스트 코드를 작성해보았지만 굳이 작성을 해야하나라는 느낌을 받았다. (실제로 작성하고 필요없다고 느껴 모두 삭제함)
따라서 이벤트 발생 시 state가 어떻게 변화하는 지에 대한 테스트 코드 위주로 작성하는 것이 좋겠다고 판단하였다. (2주차 때 API 테스트 코드를 작성해볼 생각입니다.)