[FedEx 스터디] Next.js 코드, 디렉토리 구성, 상태 관리

상민·2023년 3월 18일
0

[FedEx] 스터디

목록 보기
2/10
post-thumbnail

코드 구성과 데이터 불러오기

디렉토리 구조 구성

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
    가장 기본적인 컴포넌트
    button, input, p같은 표준 HTML요소를 감싸는 용도로 사용
  • molecules
    atoms에 속한 컴포넌트 여러 개를 조합하여 좀 더 복잡한 구조를 만드는 컴포넌트들.
  • organisms
    moleculesatoms를 섞어서 더 복잡한 구조의 컴포넌트를 만듬.
    ex) 회원 가입 양식, 푸터, 캐러셀 등..
  • templates
    일종의 페이지 스켈레톤.
    어디에 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 디렉토리 이용.

lib 파일 구성

서브파티 라이브러리를 감싸는 스크립트 파일을 관리.

데이터 불러오기 (Data Fetching)

Next.js에서는 수많은 외부 소스에서 데이터를 가져올 수 있다.

데이터베이스에 직접 접근하고 특정 데이터를 가져오기 위해 쿼리문을 던질 수도 있지만 Next.js는 전체 애플리케이션의 프론트엔드 영역만 담당하는 것이 좋다

→ 안전하지 않기 때문

서버에서 REST API 사용하기

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페이지를 표시함.

클라이언트에서 REST API 사용하기

클라이언트보다 서버가 데이터를 요청하는 것이 좀 더 안전

→ API 엔드포인트, 전송하는 매개변수값, HTTP 헤더, 인증 토큰값들이 외부에 노출되지 않음.

브라우저에서 HTTP 요청을 보낼 때는 다음 사항을 지켜야 함.

  1. 믿을 수 있는 곳에만 HTTP 요청을 보내야 한다
  2. SSL 인증서를 통해 안전하게 접근할 수 있는 곳의 HTTP API만 사용해야 한다
  3. 브라우저에서 원격 데이터베이스에 직접 연결하면 안된다.

클라이언트가 데이터를 불러오는 시점

  1. 컴포넌트가 마운트 된 후
  2. 특정 이벤트가 발생한 후

리액트에서 사용하듯이 API 요청을 하면 된다.

브라우저에서 데이터를 불러오면 두 가지 문제가 발생할 수 있다.

  1. CORS
    CORS는 브라우저에서 제공하는 보안 기능.
    서버가 데이터를 불러오는 경우에는 여러 도메인에서 원격 서버의 API를 호출해도 문제가 생기지 않음.
  2. 클라이언트에 인증 토큰이 노출될 수 있음.
    → Next API Route를 사용해서 해결 가능.
    서버에서 API요청을 한 후 결과만 클라이언트로 전송하는 방식.
    pages/api 디렉토리에서 해당 api 파일을 만들어서 작성하면 됨.

하지만 여전히 악의적인 사용자가 해당 API 경로로 접근하여 개인 정보를 얻을 수 있음

→ - 컴포넌트를 오직 서버에서만 렌더링하도록 만듬,
- JWT, API Key 등과 같은 인증 기법을 사용
- 백엔드 프레임워크를 사용

지역 및 전역 상태 관리

리액트 애플리케이션의 상태관리에 있어 특히 어려운 점은 데이터의 흐름이 단방향이라는 것이다.

부모 컴포넌트는 자식 컴포넌트에게 속성의 형태로 상태를 전달할 수 있지만 부모에게 상태를 전달할 수는 없다.

전역 상태 관리

Context API

외부 라이브러리 없이 Context API를 사용해서 애플리케이션의 전역 상태를 관리할 수 있다.

count라는 number 변수를 Context API를 사용해 전역 상태 관리 및 업데이트를 해보자

  • context/countContext.tsx
    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를 할당한다.
  • pages/_app.tsx
    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를 전역변수처럼 사용할 수 있다.
  • pages/count/index.tsx
    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

Redux를 사용해서 Context API 대신 전역 상태 관리를 할 수 있다.

Context API와 미들웨어 없는 순수한 Redux는 큰 차이가 없다.

하지만 Redux를 사용하면 수 많은 플러그인, 미들웨어, 디버깅 툴 등을 사용할 수 있음.

redux-thunk, redux-saga와 같은 미들웨어를 사용해 비동기 상태를 관리할 수 있고 redux-toolkit 을 사용해 기존 리덕스의 보일러플레이트 코드를 줄일 수 있는 방법도 있다.

Zustand

zustand를 사용해 전역 상태를 쉽게 관리할 수 있다.

$ yarn add zustand
  • store.ts
    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해주고 사용할 컴포넌트에서 사용할 수 있다.
  • pages/login/index.ts
    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에 비해 매우 적어서 사용하기 용이한 것 같다.

profile
FE Developer

0개의 댓글