next.js와 emotion을 통해 api를 가져와 퓨처라마 홈페이지를 만드는 일련의 과정을 통해 새로운 언어를 학습하기 위한 프로젝트
src
├── components
│ ├── DataList.tsx
│ ├── Error.tsx
│ ├── Loading.tsx
│ ├── cast
│ │ └── CastList.tsx
│ ├── characters
│ │ └── CharList.tsx
│ ├── episodes
│ │ ├── EpiList.tsx
│ │ └── Episode.tsx
│ ├── index.tsx
│ ├── info
│ │ └── InfoList.tsx
│ ├── inventory
│ │ └── InvenList.tsx
│ ├── layouts
│ │ ├── Layouts.tsx
│ │ ├── Navigation.tsx
│ │ └── index.ts
│ └── questions
│ ├── QuestList.tsx
│ └── QuestQna.tsx
├── constants
│ └── index.ts
├── hooks
│ └── useData.ts
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── hello.ts
│ ├── cast.tsx
│ ├── characters.tsx
│ ├── episodes.tsx
│ ├── index.tsx
│ ├── info.tsx
│ ├── inventory.tsx
│ └── questions.tsx
├── styles
│ ├── Home.module.css
│ └── globals.css
├── types
│ ├── Cast.ts
│ ├── Characters.ts
│ ├── Episodes.ts
│ ├── Info.ts
│ ├── Inventory.ts
│ ├── Questions.ts
│ └── index.ts
└── utils
└── fetcher.ts
api를 가져와 사용하는 흐름에 대해 한번 더 배울 수 있는 계기가 되었다. 이전 와인/맥주 페이지의 경우 와인과 맥주를 종류별로 불러올 때, 가져오는 데이터의 형식이 같아 아주 간단하게 페이지를 이동할 수 있었다. 하지만 이번 api의 경우 카테고리별로 다른 데이터 포멧을 가지고 있어, 어떤 식으로 데이터를 가져오는게 효율적인 방식일까 고민하게 되었다.
├── constants
│ └── index.ts
├── hooks
│ └── useData.ts
└── utils
└── fetcher.ts
일단 전체적으로 어떻게 코드가 진행되는 지 살펴보면, constant/index.ts
에 endpoint에 해당하는 api 주소값을 넣어준다. 이 api에서 데이터를 가져오기 위한 fetcher.ts
를 utils
폴더에 만들어주고, useSWR
을 사용해 원하는 path마다 api endpoint에서 fetcher
함수가 실행되도록 하는 useData
라는 이름의 커스텀 훅을 만들어준다. 이렇게 세 단계를 거치면 우린 useData
함수에 path값을 전달해 원하는 데이터를 받을 수 있게된다.
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── hello.ts
│ ├── cast.tsx
│ ├── characters.tsx
│ ├── episodes.tsx
│ ├── index.tsx
│ ├── info.tsx
│ ├── inventory.tsx
│ └── questions.tsx
페이지를 살펴보면 가장 위에 _app.tsx
파일을 확인할 수 있다. 이 파일은 Next.js에서 페이지를 초기화 하기 위해 사용하는 App 컴포넌트
를 덮어씌운 파일로 다음의 다양한 기능들을 수행할 수 있다.
import "../styles/globals.css"
import type { AppProps } from "next/app"
import { Layout } from "../components/layouts"
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Layout>
<Component {...pageProps} />
</Layout>
</>
)
}
export default MyApp
MyApp은 *Component라는 Props를 갖는다. 이 component props는 활성화된 page를 나타내고 있어 만약 루트가 바뀐다면 component가 나타내고 있는 props도 변경된다. 또, pageProps 라는 props는 data fetching method 중 하나에 의해 페이지에 미리 로드되는 초기 프롭스를 의미한다. 이 부분은 data fetching method를 공부해야 무슨 소리인지 이해할 수 있을 것 같다..
다음 게시글로 작성하면 링크를 추가하도록 하겠다.
💥 _app.js 사용시 주의할 점
- 만약
_app.js
를 처음 만든다면development server
를 재시작해야 적용됨.- App에 커스텀
getInitialProps
를 추가한다면static generation
없이automatic static opimization
불가- 만약
getInitialProps
를 추가한다면, 반드시import App from "next/app"
을 불러와서getInitialProps
안에App.getInitialProps(appContext)
를 호출해 리턴값과 리턴 객체를 병합해야함.- App은 최근
getStaticProps
나getServerSideProps
와 같은Data Fetching methods
사용 불가
page폴더 안에 생성된 페이지들은 Navigation
을 통해 서로 이동할 수 있도록 해주었다. 이 때 path에 대한 데이터를 constant/index.ts
에 저장하고 저장된 데이터를 map으로 순회해 각각의 path값과 네비게이션 이름을 리턴하도록 했다.
<Navlists>
{ROUTES.map((routeObject: ROUTE) => {
return (
<Navlist key={routeObject.ID}>
<Link href={routeObject.PATH}>
<a>{routeObject.LABEL}</a>
</Link>
</Navlist>
)
})}
</Navlists>
네비게이션을 모든 페이지에서 보이게 하기 위해 네비게이션을 Layout
이라는 컴포넌트에 넣고 layout의 children props가 네비게이션의 하단에 나타나도록 만들었다. 이 Layout을 _app.tsx
에서 넣어 모든 페이지 로딩마다 호출될 수 있도록 했다.
각각의 페이지들은 모두 api에서 불러온 데이터들로 구성되어있다. 데이터를 불러오고 체크하는 부분은 모두 같지만 데이터 타입이 달라 어떻게 모듈화를 하면 좋을지 고민을 하게 되었고, 공통적으로 데이터를 받아와서 에러체킹을 하는 부분을 컴포넌트로 만들어 dataname에 따라 해당하는 페이지의 컴포넌트로 연결되게 하였다.
function DataList({ dataname }: DataName): JSX.Element {
const { data, error } = useData(dataname)
if (error) return <Error />
if (!data) return <Loading />
const SITE: SiteType = {
cast: <CastList data={data} />,
characters: <CharList data={data} />,
episodes: <EpiList data={data} />,
info: <InfoList data={data} />,
inventory: <InvenList data={data} />,
questions: <QuestList data={data} />,
}
return (
<main>
<Header>{dataname}</Header>
<FlexCenter>{SITE[dataname]}</FlexCenter>
</main>
)
}
이 페이지를 만들면서 동적라우팅에 대한 질문을 굉장히 많이 받았다 나도 모르는데... 간단히 공부한 내용으론 지금 진행중인 블로그를 만드는 프로젝트에서 사용자에 따라 각자의 내 블로그 페이지를 보여줄 때 사용되어야 할 것 같아 공부를 해두어야겠다. (다음 포스팅으로 돌아오겠다는 소리)
(emotion/styled 사용)
기존 스타일드 컴포넌트 사용시 props를 넘겨 조건에 따른 스타일을 적용하는게 굉장히 편리했다. 근데 이번 프로젝트에서 typescript를 사용하면서 props를 넘겨주는 방식에 타입을 설정해주어야하는 단계가 추가되어 전보다 단계가 복잡해졌다.
type logo = {
background: string
}
우선 넘겨주려는 props의 타입을 지정해 준 뒤,
const logoimg = (props: logo) => css`
background-image: url(${props.background});
background-position: center center;
background-size: contain;
background-repeat: no-repeat;
`
해당 프롭스를 사용하는 스타일을 생성해준다.
이렇게 생성된 스타일을 사용하는 컴포넌트를 만들어주면 원하는대로 프롭스가 적용이 된다.
const LogoImg = styled.a`
${logoimg}
display: inline-block;
width: 160px;
height: 100px;
`
텍스트에 그라데이션을 넣고 싶어 관련 정보를 찾아봐았다. 검색한 페이지에서 syntax를 따라해 붙여넣었는데, 예상하던 모양과 다른 모양이 나왔다.
//syntax
h1 {
font-size: 72px;
background: -webkit-linear-gradient(#eee, #333);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
내가 원하던 모양은 상하가 아니라 좌우로 흘러가는 모양이라 방향값을 정하기 위해 linear-gradient에서 사용했던 것처럼 to right
를 입력해보았다.
그랬더니 아예 모양이 사라져서 각도로도 입력해보았다.
background: -webkit-linear-gradient( 180deg, rgba(165, 170, 218, 1) 0%, rgba(52, 174, 172, 1) 100% );
각도로 입력했더니 제대로 입력되는 것을 확인할 수 있었다.
그럼 방향 키워드로 입력이 안되는 것일까 싶어 그냥 right
만 작성해서 넣어봤더니 제대로 동작하는 것을 확인 할 수 있었다.
Quetion에서 문제를 풀고 문제에 대한 답을 저장하는 과정에 대해 많은 고민을 했다. 초기엔 dictionary를 사용해 dictionary 내 dictionary의 형태로 사용하고 싶었다. 문제의 index를 키로 잡고 선택한 문제의 답(string)과 일치 여부(boolean)를 값으로 넣어 순서에 관계없이 문제를 풀어도 값이 저장되고, 기존의 값을 다시 확인할 수 있도록 만들고 싶었으나 useState로 dictionary in dictionary 값을 갱신하는데 한계가 있었다.
그래서 문제의 갯수만큼의 빈 배열을 만들어 문제의 인덱스에 맞게 사용자가 선택한 답을 저장하고, 제출 버튼을 누를 시 답과 비교하도록 코드를 작성하였다. useState를 사용해 재렌더링이 되어도 값이 유지되도록 하였으며, 스코어 값이 변경될 때마다 재렌더링이 되는 것을 막기위해 useRef를 사용해주었다.
const score = useRef<number>(0)
const [showScore, setShowScore] = useState<boolean>(true)
const dataLen = data.length
const [answers, setAnswers] = useState<Array<string>>(
new Array(dataLen).fill("")
)
const [qnum, setQnum] = useState<number>(1)
// 변경된 정답 재할당
const Clicked = (id: number, value: string) => {
answers[id - 1] = value
setAnswers([...answers])
}
info 데이터를 살펴보면 객체가 배열안에 들어있는 형태였다.
[
"yearsAired": "1999–2013",
"creators": [
{
"name": "David X. Cohen",
"url": "http://www.imdb.com/name/nm0169326"
},
{
"name": "Matt Groening",
"url": "http://www.imdb.com/name/nm0004981"
}
],
"id": 1
}
]
그래서 다음과 같은 데이터를 어떻게 타입선언 해주어야하나 찾아본 결과 두가지 방식으로 타입 선언이 가능하다는 것을 알게되었다.
Array<type>
사용하기~타입들의 배열이다 라는 것을 알려주기 위해 Array<{ name : string, url : string }> 으로 표현할 수 있다. 만약 내부의 타입을 더 간단하게 표현하고 싶다면
interface Creator = {
name : string,
url : string
}
export interface Info {
synopsis: string
yearsAired: string
creators: Array<Creator>
id: number
}
이렇게 표현할 수도 있다.
type[]
사용하기위에서 정의한 타입을 가지고 표현하자면
export interface Info {
synopsis: string
yearsAired: string
creators: Creator[]
id: number
}
이렇게도 사용 가능하다
항목별로 분류될 수 있는 필터링 기능을 추가했다.
select 박스를 새로 디자인해 원하는 에피소드를 선택할 수 있도록 했다.
너무 멋져요! 이 글을 참고해서 프로젝트 관련 글을 작성해야겠어요!