강의를 듣다가 모달을 구현하는 두 가지 방식을 배웠다

import React, { useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';

interface ModalProps {
  open: boolean;
  children: React.ReactNode;
  onClose: () => void;
}

function Modal({ open, children, onClose }: ModalProps) {
  const dialog = useRef<HTMLDialogElement | null>(null);

  useEffect(() => {
    if (open) {
      dialog.current?.showModal();
    } else {
      dialog.current?.close();
    }
  }, [open]);

  return createPortal(
    <dialog className="modal" ref={dialog} onClose={onClose}>
      {open ? children : null}
    </dialog>,
    document.getElementById('modal') as Element,
  );
}

export default Modal;

State를 외부에 두고 직관적이고 간단하게 관리하는 방식

import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { createPortal } from 'react-dom';

interface ModalProps {
  children: React.ReactNode;
  buttonCaption: string;
}

const Modal = forwardRef<{ open: () => void; close: () => void }, ModalProps>(
  (
    props: ModalProps,
    ref: React.Ref<{ open: () => void; close: () => void }>
  ) => {
    const dialog = useRef<HTMLDialogElement>(null);
    const modalRoot = document.getElementById('modal-root');
    const { children, buttonCaption } = props;

    useImperativeHandle(ref, () => ({
      open() {
        dialog.current?.showModal();
      },
      close() {
        dialog.current?.close();
      },
    }));
    return createPortal(
      <dialog ref={dialog}>
        {children}
        <form method="dialog">
          <button>{buttonCaption}</button>
        </form>
      </dialog>,
      modalRoot as Element
    );
  }
);

Modal.displayName = 'Modal';

export default Modal;

State를 내부에 두고 useImperativeHandle을 이용하여

상태 제어 함수를 외부에 노출을 해 제어하는 방식이다.

 

두 방법의 장단점은 다음과 같다

 

첫 번째 방법(외부에서 상태 관리)

장점

1. 단순하고 직관적인 상태 관리

단점

1. 컴포넌트 간 의존성 증가

- 부모 컴포넌트의 상태에 의존하게 된다, 이럴 경우 특정 부모 컴포넌트에 종속적이게 되어 재사용성이 낮아질 수 있다.  모달의 상태를 가지고 있는 컴포넌트가 한 화면에 한개만 렌더링 된다면 상관 없겠지만 2개 이상이 되는 순간부터 상태관리가 매우 어려워지게 된다.

2. 비즈니스 로직의 결합

3. 복잡한 로직 처리

 

두 번째 방법(내부에서 상태 관리, 외부에 메서드 제공)

장점

1. 모달 열기/닫기 메서드 제공

단점

1. 조금 더 복잡한 구현

2. 추가적인 코드 필요

- 모달을 사용하기 위해서는 부모 컴포넌트에서 ref를 설정해야 하고, 이를 통해 모달 제어해야 하므로 추가적인 코드 작성이 필요하다

 

내가 선택할 방법

두 번째 방법은 내가 모달창을 여러개 띄우는 웹은 만드는 경우가 아니라면 굳이 사용하지 않을 것 같고

첫 번째 방법을 Context API를 사용하여 전역에 모달의 상태를 만들어 

1. 상태 충돌 가능성 감서

2. 상태 관리의 복잡성 감소

3. 재사용성 향상

4. 비즤스 로직의 결합 감소

등을 꾀한다

하지만 여전히 

1. 상태 관리의 성능 문제

- Context API는 상태가 변경될 때 해당 Contex모두를 리렌더링 하기 때문에 성능에 영향을 끼칠 수 있다

2. 디버깅의 어려움

- 어느 컴포넌트에서 이벤트가 발생했는지 추적하기 어렵고, 여러 Provider를 사용할 경우 더 파악하기 힘들다

등의 문제가 생기는데

추후 Recoil을 도입하여 

해당 문제들을 개선할 것이다

 

 

 

 

기존 코드: 

const handleAddTask = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const taskValue = e.currentTarget.task.value.trim();
    if (!taskValue) {
      alert('태스크를 입력해주세요.');
      return;
    }

    if (curProjectId === undefined) {
      alert('선택된 프로젝트가 없습니다.');
      return;
    }

    setProjects((prevProjects) => {
      const projectIndex = prevProjects.findIndex(
        (project) => project.id === curProjectId
      );

      if (projectIndex === -1) {
        alert('선택된 프로젝트를 찾을 수 없습니다.');
        return prevProjects;
      }

      const newTaskId =
        prevProjects[projectIndex].tasks.reduce(
          (maxId, task) => Math.max(maxId, task.id),
          0
        ) + 1;

      const newTask = { id: newTaskId, task: taskValue };

      const updatedProjects = [...prevProjects];
      updatedProjects[projectIndex].tasks = [
        ...updatedProjects[projectIndex].tasks,
        newTask,
      ];

      return updatedProjects; // setProjects를 다시 호출하지 않고 업데이트된 배열을 반환
    });
  };

 

매개변수를 이벤트로 받았는데 

gtp 01 mini 모델에서 코드의 개선을 요구했다

개선 사항은 

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  if (task.trim()) {
    onAddTask(task);
    setTask('');
  } else {
    alert('태스크를 입력해주세요.');
  }
};

위와 같이 핸들러를 만들어 하위 컴포넌트에서 값 자체를 보내는 것인데

제시해준 이유와 장점을 보니 바꾸는 게 맞는 것 같다

결론

 
 

제시한 코드는 Controlled Components 패턴을 올바르게 따르고 있습니다. React.FormEvent<HTMLFormElement>가 넘어오는 값이 DOM 요소임에도 불구하고, 입력 필드의 값을 React 상태로 관리하고 있기 때문에 Controlled Components로 간주됩니다. Controlled Components를 사용함으로써 다음과 같은 이점을 누릴 수 있습니다:

 
 

 

  • 타입 안전성 강화: TypeScript를 통해 명확한 타입 검증을 수행할 수 있습니다.

 

     form이벤트로 받아오게 되면 TypeScript를 통한 명확한 검증이 안되는데 맞다.
 

 

  • 유지보수성과 확장성 향상: 상태 관리가 중앙집중화되어 있어, 폼 구조 변경 시 유연하게 대응할 수 있습니다.

 

   이 부분도 처음에 무슨 소리인줄 몰랐는데 form 객체의 name을 바꿀 경우 어떤 대응도 하지 못한채 런타임에서 에러를 맞이 할 수 있다. 타입스크립트에서 에러관리가 안되는 것은 곡 피해야 한다
 

 

  • 일관된 데이터 흐름: 모든 폼 데이터가 React 상태를 통해 흐르므로 데이터의 일관성이 유지됩니다.

 

 
 

 

  • 실시간 피드백 및 검증: 사용자가 입력할 때마다 상태가 업데이트되어 실시간으로 입력값을 검증하고 피드백을 제공할 수 있습니다.

 

gtp o1부터 생각을 하기 시작했다는데 상상 이상으로 성능이 좋다

 

 

기본 프로젝트를 typsscript 프로젝트로 바꾸기 위해 GPT에게 물어보았습니다

 

 

이미 프로젝트가 설정되어 있다면, TypeScript, ESLint, Prettier를 추가하는 방법을 설명할게요.

1. TypeScript 설정

먼저, TypeScript와 관련된 패키지를 설치해야 합니다.

npm install --save-dev typescript @types/react @types/react-dom

다음으로, TypeScript 설정 파일을 생성합니다.

npx tsc --init

이 명령어는 기본 tsconfig.json 파일을 생성합니다. 생성된 파일을 열어 프로젝트에 맞게 수정할 수 있습니다. 주요 설정은 다음과 같습니다:

{
  "compilerOptions": {
    "jsx": "react-jsx",
  },
  "include": ["src/**/*"]
}

// vite + typescript를 쓰고 .env를 쓸 것이면 아래와 같이 업데이트를 해주면 좋다
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler"
  },
  "include": ["src/**/*"]
}

2. ESLint 설정

ESLint 설정 파일 .eslintrc.js를 생성하고 필요한 패키지를 설치합니다

npx eslint --init

설정 파일에서 TypeScript와 React 관련 규칙을 추가해야 합니다. .eslintrc.js 파일을 다음과 같이 설정할 수 있습니다:
이 설정을 할때 지금 타입스크립트를 사용중이냐라는 질문을 하는데 바로 타입스크립트로 바꿀거면 yes를 누르도록 하자

module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended'
  ],
  parserOptions: {
    ecmaVersion: 'ESNext',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  rules: {
    // 여기에 프로젝트에 맞는 규칙을 추가하세요.
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
};

3. Prettier 설정

Prettier를 설치하고 설정합니다.

npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier

프로젝트 루트에 .prettierrc 파일을 생성합니다.

{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "endOfLine": "auto"
}

ESLint와의 통합을 위해 .eslintrc.js에 다음과 같은 설정을 추가합니다:

module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended' // Prettier와 통합
  ],
};

4. 스크립트 업데이트

package.json 파일에 ESLint와 Prettier를 실행할 수 있는 스크립트를 추가합니다.

{
  "scripts": {
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "format": "prettier --write ."
  }
}

이제 npm run lint로 코드 린팅을, npm run format으로 코드를 포맷팅할 수 있습니다.

5. src/custom.d.ts 선언

declare module '*.jpg';
declare module '*.png';
declare module '*.jpeg';
declare module '*.gif';


declare module '*.module.css' {
  const classes: { [key: string]: string };
  export default classes;
}

 

이미지 파일의 에러를 막아준다

6. TypeScript로 파일 변환

src 폴더의 .js 또는 .jsx 파일들을 .ts 또는 .tsx로 변환합니다. 이후, TypeScript의 타입 체크 기능을 활용해 코드를 개선해 나가면 됩니다.

 

7. src/types/globla.d.ts 생성

이곳에 global type들을 담아주면 된다

8. src/vite-env.d.ts

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  // 다른 환경 변수들에 대한 타입 정의...
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

이렇게 설정해 주어야 typescript + vite를 사용할 때 에러가 나오지 않는다

+ Recent posts