create-next-app으로 Next.js 애플리케이션을 처음 만들면 생기는 디렉토리 구조
next-js-app
node_modules/
package.json
pages/
public/
styles/
node_modules/ : Next.js 프로젝트의 의존성 패키지를 설치하는 디렉토리
pages/ : 웹 애플리케이션의 페이지 파일을 저장하고 라우팅 시스템을 만드는 디렉토리
public/ : 컴파일된 CSS 및 자바스크립트 파일, 이미지, 아이콘 등 정적 자원을 저장하고 제공하는 디렉토리
styles/ : 스타일링 포맷(CSS, SASS, LESS 등)과 관계없이 스타일링 모듈을 저장하는 디렉토리
아토믹 디자인 원칙
에 따라 각 컴포넌트를 서로 다른 수준의 디렉토리에 둔다
$ mkdir components && cd components
$ mkdir atoms
$ mkdir molecules
$ mkdir organisms
$ mkdir templates
atoms
에 속한 컴포넌트 여러 개를 조합하여 좀 더 복잡한 구조를 만드는 컴포넌트들.molecules
와 atoms
를 섞어서 더 복잡한 구조의 컴포넌트를 만듬.organisms
, atoms
, molecules
를 배치할지 결정해서 사용자가 접근할 수 있는 페이지를 만듬Button 컴포넌트를 만드려면 components/atoms/ 디렉토리에 파일을 생성하면 된다.
새 컴포넌트를 만들 때는 최소한 세 개의 파일을 만들어야한다.
컴포넌트 파일
, 스타일 파일
, 테스트 파일
components/
- atoms/
- Button/
- index.tsx
- button.test.tsx
- button.styled.tsx # 또는 styles.module.scss
컴포넌트를 만들지 않고 다양한 목적으로 사용할 수 있는 코드들은 utilities
디렉토리에 저장한다.
함수 각각 목적에 맞게 유틸리티 함수 파일을 각각 작성
한다.
유틸리티 함쉐 맞는 테스트파일도 생성
한다.
Next.js에서는 정적 파일을 public
디렉토리 아래에 두면 나머지는 프레임워크가 알아서 해준다.
public 디렉토리 아래에 assets 디렉토리를 만들고 아래에 유형별 디렉토리를 만든다.
public/
- assets/
- js/
- css/
- icons/
- images/
해당 정적 파일은 https://localhost:3000/assets/js/ 이러한 경로로 접근할 수 있다.
이미지는 내장 Image 컴포넌트를 사용해서 처리하는 것이 좋다.
Emotion과 같은 CSS-in-JS 프레임워크는 컴포넌트별로 스타일 파일을 만듬
여러 곳에서 사용하는 공통 스타일이나 유틸리티 기능은 styles
디렉토리 이용.
서브파티 라이브러리를 감싸는 스크립트 파일을 관리.
Next.js에서는 수많은 외부 소스에서 데이터를 가져올 수 있다.
데이터베이스에 직접 접근하고 특정 데이터를 가져오기 위해 쿼리문을 던질 수도 있지만 Next.js는 전체 애플리케이션의 프론트엔드 영역만 담당하는 것이 좋다
→ 안전하지 않기 때문
getServerSideProps 함수 내에서 REST API 호출.
HTTP 헤더로 올바른 인증 토큰을 함께 전송해야 하는 경우
export async function getServerSideProps(ctx) {
const { username } = ctx.query;
const userReq = await axios.get('URL', {
headers: {
authorization: process.env.API_TOKEN
}
})
.
.env 파일에 중요 데이터 저장
API_TOKEN=realworldnextjs
만일 해당 페이지로 접근했을 때 404 에러가 나는 경우
→ getServerSideProps 함수에 코드 추가
export async function getServerSideProps(ctx) {
const { username } = ctx.query;
const userReq = await axios.get('URL', {
headers: {
authorization: process.env.API_TOKEN
}
})
if (userReq.status === 404) {
return {
notFound: true
};
}
...
notFound: true
를 return하면 Next.js에서 해당 페이지를 기본적으로 404 처리 페이지로 표시할 수 있다.
별다른 설정 없이 알아서 기본 404페이지를 표시함.
클라이언트보다 서버가 데이터를 요청하는 것이 좀 더 안전
→ API 엔드포인트, 전송하는 매개변수값, HTTP 헤더, 인증 토큰값들이 외부에 노출되지 않음.
브라우저에서 HTTP 요청을 보낼 때는 다음 사항을 지켜야 함.
클라이언트가 데이터를 불러오는 시점
리액트에서 사용하듯이 API 요청을 하면 된다.
브라우저에서 데이터를 불러오면 두 가지 문제가 발생할 수 있다.
pages/api
디렉토리에서 해당 api 파일을 만들어서 작성하면 됨.하지만 여전히 악의적인 사용자가 해당 API 경로로 접근하여 개인 정보를 얻을 수 있음
→ - 컴포넌트를 오직 서버에서만 렌더링하도록 만듬,
- JWT, API Key 등과 같은 인증 기법을 사용
- 백엔드 프레임워크를 사용
리액트 애플리케이션의 상태관리에 있어 특히 어려운 점은 데이터의 흐름이 단방향이라는 것이다.
부모 컴포넌트는 자식 컴포넌트에게 속성의 형태로 상태를 전달할 수 있지만 부모에게 상태를 전달할 수는 없다.
외부 라이브러리 없이 Context API를 사용해서 애플리케이션의 전역 상태를 관리할 수 있다.
count라는 number 변수를 Context API를 사용해 전역 상태 관리 및 업데이트를 해보자
import { createContext, useState } from 'react';
type State = {
state: { count: number };
actions: {
setCount: React.Dispatch<React.SetStateAction<number>>;
};
};
const CountContext = createContext<State>({
state: { count: 0 },
actions: {
setCount: () => {},
},
});
type Props = { children: React.ReactNode };
const CountProvider = ({ children }: Props) => {
const [count, setCount] = useState(0);
const value = {
state: { count },
actions: { setCount },
};
return (
<CountContext.Provider value={value}>{children}</CountContext.Provider>
);
};
const { Consumer: CountConsumer } = CountContext;
export { CountProvider, CountConsumer };
export default CountContext;
createContext
를 사용해서 CountContext를 만들어야 한다. createContext
의 제네릭으로 관리할 state와 actions의 타입이 들어가야 하므로 State
타입을 선언한다. 관리할 변수는 count로 number타입이고 action으로는 count를 업데이트할 setCount함수가 포함된다. CounterProvider
에는 useState
훅을 사용해서 count와 setCount를 선언하고 CountContext의 Provider 속성에 넣을 value값으로 state와 actions에 각각 count와 setCount를 할당한다.import { CountProvider } from '@/components/context/CountContext';
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<CountProvider>
<Component {...pageProps} />
</CountProvider>
);
}
Component
컴포넌트를 CountProvider
로 감싼다. Component 하위의 컴포넌트들은 이제 count와 setCount를 전역변수처럼 사용할 수 있다.import CountContext from '@/components/context/CountContext';
import React, { useContext } from 'react';
export default function Count() {
const { state, actions } = useContext(CountContext);
const onClickButton = () => {
actions.setCount((prev) => prev + 1);
};
return (
<div>
<h1>{state.count}</h1>
<button onClick={onClickButton}>INCREASE</button>
</div>
);
}
useContext
훅을 사용해서 CountContext의 state와 actions를 가져온다.Context API를 사용하면 Provider 하위의 컴포넌트들이 전부 리렌더링이 일어나므로 상태 관리보다는 전역 변수를 사용한다는 개념으로 사용해야 하는 것 같다.
Redux를 사용해서 Context API 대신 전역 상태 관리를 할 수 있다.
Context API와 미들웨어 없는 순수한 Redux는 큰 차이가 없다.
하지만 Redux를 사용하면 수 많은 플러그인, 미들웨어, 디버깅 툴 등을 사용할 수 있음.
redux-thunk
, redux-saga
와 같은 미들웨어를 사용해 비동기 상태를 관리할 수 있고 redux-toolkit
을 사용해 기존 리덕스의 보일러플레이트 코드를 줄일 수 있는 방법도 있다.
zustand를 사용해 전역 상태를 쉽게 관리할 수 있다.
$ yarn add zustand
import { create } from 'zustand'; // create로 zustand를 불러옵니다.
const useStore = create<StateType>((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
export default useStore;
export type StateType = {
bears: number;
increasePopulation: () => void;
removeAllBears: () => void;
};
zustand의 create
함수를 사용해서 store를 초기화하고 세팅할 수 있다. bears
라는 number 타입의 변수를 관리할 것이고, 이를 업데이트하는 함수 increasePopulation
, removeAllBears
도 함께 store로 관리한다. useStore를 export해주고 사용할 컴포넌트에서 사용할 수 있다.import useStore, { StateType } from '@/store';
import styled from '@emotion/styled';
export default function Login() {
const { bears, increasePopulation, removeAllBears } = useStore(
(state) => state
);
return (
<Test>
<h1>{bears} around here ...</h1>
<button onClick={increasePopulation}>one up</button>
<button onClick={removeAllBears}>remove all</button>
</Test>
);
}
const Test = styled.div`
color: red;
`;
useStore를 import해서 전역 상태들을 사용할 수 있다.Context API나 Redux처럼 Provider를 바깥에서 감싸주는 작업을 하지 않고 전역 상태를 관리할 수 있다.
보일러 플레이트 코드도 Redux나 Context API에 비해 매우 적어서 사용하기 용이한 것 같다.