이 글에 나온 내용이 CSS가 안먹는 등 문제가 많아서 리덕스 적용하여 재개하였음!
//pages/Header.tsx
const Header: React.FC = () => {
return (
<div>
<h1>개쩌는 투두앱</h1>
</div>
);
};
export default Header;
React.FC타입은 리액트 컴포넌트 타입을 의미한다.
//pages/_app.tsx
import { AppProps } from "next/app";
import GlobalStyle from "../styles/GlobalStyle";
import Header from "./Header";
const app = ({ Component, pageProps }: AppProps) => {
return (
<>
<GlobalStyle />
<Header />
<Component {...pageProps} />
</>
);
};
export default app;
Next.js는 AppProps이라는 별도의 타입을 지원한다. 프롭스는 :AppProps로 타입을 지정하고, 위와 같이 헤더를 넣는다.
Next js 구동 방식과 getInitailProps를 참조하여 나머지 파일들에 대한 설명을 보자. 요약하자면 Next는 최초로 _app.js
, _document.js
를 실행한다. 서버환경에서 실행되므로 window객체는 없다는 점에 유의하자. _app.js
가 컴포넌트의 레이아웃을 구성하여 HTML의 body를 만든다. 이후 _document.js
가 html의 body가 어디에 들어가면 좋을지를 구성한다. _document.js
는 어디까지나 회면을 구성하므로 어플리케이션 로직은 _app.js
를 활용해야 한다.
getInitialProps은 (9.3이후에는 getStaticProps, getStaticPaths, getServerSideProps으로 분화됨) 간단하게 말하면 FetchData 로직이다. React에서 useEffect(()=>{},[])
로 마운트 된 후 실행하거나, Vue에서 onCreate, onMount와 같은 라이프 라이클 훅을 서버사이드에서 처리하여 렌더링한다고 보면 된다. 만일 모든 페이지에서 동일한 정보가 필요하다면 _app.js
getInitialProps을 사용하여 pageProps로 넘겨주면 ㄷ된다. 유의할 점은 한 페이지는 하나의 getInitialProps만 동작한다. _app.js
에서 getInitialProps을 사용했다면 그 하부 페이지는 별도의 데이터를 Fetch하지 않음을 의미한다.
아무리 해도 styled-componets가 적용되지 않아서
yarn create next-app --example with-styled-components with-styled-components-app
를 실행해서 예제를 받았다.
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^13.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^5.3.6"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "18.0.10",
"@types/styled-components": "^5.1.26",
"typescript": "4.9.4",
"eslint": "^8.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "13.1.1",
"eslint-config-prettier": "^8.6.0"
},
"eslintConfig": {
"extends": [
"next/core-web-vitals",
"airbnb",
"airbnb-typescript",
"prettier"
]
}
}
//components\TodoList.tsx
const TodoList: React.FC = () => {
return (
<div>
<h1>TodoList</h1>
</div>
);
};
export default TodoList;
//pages\index.tsx
import { NextPage } from "next";
import TodoList from "../components/TodoList";
const index: NextPage = () => {
return <TodoList />;
};
export default index;
//types\todo.d.ts
export type TodoType = {
id: number;
text: string;
color: "RED" | "ORANGE" | "YELLOW";
checked: boolean;
};
지정한 타입으로 Todo 데이터를 index.tsx에 만들자
import { NextPage } from "next";
import TodoList from "../components/TodoList";
import { TodoType } from "../types/todo";
const todos: TodoType[] = [
{ id: 1, text: "마트 가서 장보기", color: "RED", checked: false },
{ id: 2, text: "수학 숙제하기", color: "ORANGE", checked: true },
{ id: 3, text: "투두리스트 만들기", color: "YELLOW", checked: false },
{
id: 4,
text: "마트가서 투두리스트 만드는 숙제하기",
color: "RED",
checked: false,
},
];
const index: NextPage = () => {
return <TodoList todos={todos} />;
};
export default index;
이때 '정의된 어트리뷰트가 아니다'라며 타입스크립트가 에러를 띄운다.
타입스크립트 인터페이스를 만들자.
//components\TodoList.tsx
import { TodoType } from "../types/todo";
interface IProps {
todos: TodoType[];
}
const TodoList: React.FC<IProps> = () => {
return (
<div>
<h1>TodoList</h1>
</div>
);
};
export default TodoList;
TodoType을 import하여 이를 IProps라는 인터페스로 만들고, React.FC의 제너릭에 IProps라는 타입을 넣어주었다.
이제 index.tsx에서도 IProps라는 어트리뷰트임을 잘 인식한다.
리스트의 갯수를 세자.
아래는 switch-case와 플래그를 이용하여 total을 구하는 패턴이다.
//components\TodoList.tsx
const todoNums = useCallback(() => {
const data = {
total: 0,
RED: 0,
ORANGE: 0,
YELLOW: 0,
};
todos.forEach((todo) => {
let hasColor = true;
switch (todo.color) {
case "RED":
data.RED += 1;
break;
case "ORANGE":
data.ORANGE += 1;
break;
case "YELLOW":
data.YELLOW += 1;
break;
default:
hasColor = false;
}
if (hasColor) data.total += 1;
});
return data;
}, [todos]);
아래는 책에서 소개된 새로운 colors가 발견되면 문자열을 키로하는 새로운 값을 선언하는 패턴이다. 대괄호 표기법을 사용하면 표현식을 키값으로 가진 프로퍼티를 조회할 수 있다.
const todoNums = useMemo(() => {
const data: ObjectIndexType = { total: 0 };
todos.forEach((todo)=>{
const value = data[todo.color];
if(!value){
data[todo.color] = 1;
} else {
data[todo.color] += 1;
}
data.total += 1;
})
return data
}, [todos]);
console.log를 찍으면 { total: 4, RED: 2, ORANGE: 1, YELLOW: 1 }
로 정상 출력 된다. (참고로 콘솔은 터미널에 출력된다.)
Object.keys()로 객체의 키값을 배열로 얻어 다양하게 써보자
import { useMemo } from "react";
import { TodoType } from "../types/todo";
interface IProps {
todos: TodoType[];
}
type ObjectIndexType = {
[key: string]: number | undefined;
};
const TodoList: React.FC<IProps> = ({ todos }) => {
const todoNums = useMemo(() => {
const data: ObjectIndexType = {};
todos.forEach((todo) => {
const value = data[todo.color];
if (!value) {
data[todo.color] = 1;
} else {
data[todo.color] += 1;
}
});
return data;
}, [todos]);
return (
<div>
<h1>TodoList</h1>
<p>남은 TODO {todos.length}개</p>
<div>
{Object.keys(todoNums).map((el, index) => (
<p style={{ color: `${el}` }} key={index}>
{todoNums[el]}개
</p>
))}
</div>
</div>
);
};
export default TodoList;
리액트에서 여러개의 태그를 순회하며 렌더링 할 때에는 map메서드를 사용한다.(for문은 사용이 안된다 ㅠㅠ)
Object.keys(object)는 키값을 배열로 반환한다. 이를 이용하여 key값을 인라인 스타일로도 넣고, 갯수를 넣는 용도로도 썼다.