레벨2 점심 뭐 먹지 리뷰 정리

정우시·2023년 4월 17일
1

우아한테크코스

목록 보기
3/13

웹 프론트엔드 레벨2 - 2023

미션 - 점심 뭐 먹지

프로그래밍 요구사항

  • 리액트 클래스 컴포넌트로 먼저 작성한 후 함수형 컴포넌트로 마이그레이션을 한다.
  • 함수형 컴포넌트로 마이그레이션을 한 후 Custom Hooks을 이용하여 재사용 가능한 기능을 분리한다.
  • 타입스크립트를 이용한다.

리뷰어: 체프


  1. object 타입 - Main.tsx
export default class Main extends Component<object, MainState> {
  state: MainState = {
    category: '전체',
    sorting: '이름순',
    restaurantId: undefined,
    isModalOpen: false,
  };

체프의 코멘트

object 타입을 사용했을 때 문제점은 무엇이 있을까요?

이 코멘트를 읽고 object라는 타입에 대해서 다시 한번 찾아보게 되었습니다.

먼저 클래스형 컴포넌트에서 state의 초깃값을 설정할 때 object 타입을 사용하는 것이 일반적이었습니다. 하지만 object라는 것은 모든 것을 나타내기 때문에 어떠한 프로퍼티나 메소드가 있는지에 대한 정보를 포함하지 않습니다.

따라서 object 타입을 사용할 경우 원하는 프로퍼티에 접근할 수 없는 경우가 있을 수 있어 에러가 발생할 가능성이 있습니다. 이러한 점을 고려하여 앞으로는 인터페이스나 타입을 이용하는 것이 더 좋습니다.

근데 해당 코드에는 object와 MainState라고 설정해준 인터페이스가 같이 설정되어 있습니다. 다시 확인을 해보니 MainState라고 적어주기만 해도 타입 관련된 에러가 나오지 않았습니다.

아마 처음에 object를 붙이고 MainState라는 인터페이스를 만들어서 타입을 설정해주었는데, 안지우고 놔둔 거 같습니다. 😅

이번 2단계 미션의 경우 클래스형 컴포넌트를 함수형 컴포넌트로 마이그레이션 하였기 때문에 object라는 타입이 불필요하여 삭제할 수 있었습니다.


  1. 리액트의 ChangeEvent - RestaurantFilter.tsx
import { Component, ChangeEvent } from 'react';
import { Option } from '../../types/types';
import Filter from '../Filter/Filter';
import styles from './RestaurantFilter.module.css';
import { RestaurantFilterProps } from '../../types/types';

export default class RestaurantFilter extends Component<RestaurantFilterProps> {
  handleCategoryChange = (event: ChangeEvent<HTMLSelectElement>) => {

체프의 코멘트
⎻ 취향 차이가 있을 수 있어서 개인적으로 드리는 말씀인데요.
리액트의 ChangeEvent와 자바스크립트의 ChangeEvent가 혼동될 가능성이 있어서 저는 React.ChangeEvent와 같이 작성하는 것을 더 선호하는 편이예요. 참고 부탁드릴게요!


  1. 타입스크립트에서의 물음표 - types.ts
import { ChangeEvent } from 'react';

export interface Option {
  value: string;
  label: string;
}

export interface MainState {
  category: string;
  sorting: string;
  restaurantId: number | undefined;

체프의 코멘트

restaurantId?: number와의 차이점은 무엇일까요?

restaurantId?: number는 TypeScript 3.0부터 도입된 "Optional Properties" 기능으로 restaurantId가 선택적으로 존재할 수 있음을 나타냅니다.

그리고 값이 할당되지 않을 때 자동으로 undefined가 할당됩니다. 따라서 restaurantId: number | undefined와 동일한 의미를 가집니다.

해당 코드를 작성했을 당시에는 '?는 옵셔널 체이닝이 아닌가?'라고 생각하여 (물음표(?) === 나쁜 거 ㅎㅎ) 사용하지 않았습니다. 😅

허나 Optional 관련된 정보를 찾아보니 물음표가 무조건 안좋은 것은 아니었습니다.

타입스크립트에서의 물음표

우선 물음표(?)는 Optional Properties(선택적 프로퍼티, ?:)와 Optional Chaning(옵셔널 체이닝, ?)에 사용될 수 있습니다.

Optional Properties

Optional Properties의 뜻은 매개변수를 함수 호출 시 생략할 수 있다는 것을 의미합니다. 따라서 이는 코드를 작성하는 개발자가 함수 호출 시 이 매개변수의 값을 제공하지 않아도 되며, 이 매개변수의 값이 undefined일 수 있다는 것을 나타내기도 합니다.

따라서, 매개변수를 선택적으로 받아야 하는 경우 restaurantId?: number을 사용하는 것이 더 명확하고 간결한 코드를 작성할 수 있습니다.

Optional Chaning (옵셔널 체이닝)

옵셔널 체이닝은 JavaScript에서 객체 속성에 접근할 때 발생할 수 있는 TypeError 예외를 방지하기 위한 기능입니다. 따라서, TypeScript에서 ?를 사용하는 것은 옵셔널 체이닝을 사용하는 것입니다.

옵셔널 체이닝이 항상 안좋은 것인가?

하지만, 옵셔널 체이닝이 안좋은 것은 아닙니다. 옵셔널 체이닝은 코드 안정성을 높일 수 있는 기능입니다. 예를 들어, 객체에서 undefined인 속성에 접근하려고 하면 발생하는 TypeError 예외를 방지할 수 있습니다. 이는 개발자가 코드를 작성할 때 속성의 존재 여부를 확인하는 과정을 줄이고, 더 간결하고 가독성 높은 코드를 작성할 수 있도록 도와줍니다.

안좋은 것은 아니지만 남용 금지

물론, 항상 옵셔널 체이닝을 사용하는 것이 최선인 것은 아닙니다. 일부 상황에서는 필수적인 속성에 접근할 때 undefined를 반환하거나 예외를 발생시키는 것이 코드 안정성을 높일 수 있습니다. 따라서, 상황에 맞게 적절하게 옵셔널 체이닝을 사용하는 것이 중요합니다.

하지만 함수형으로 변경하면서 해당 interface 삭제 ^_^;

다만 함수형 컴포넌트로 리펙토링을 하면서 해당 코드가 불필요하게 되어 삭제를 하게 되었습니다.


  1. React.FC - Filter.tsx
import { FilterProps } from '../../types/types';
import styles from './Filter.module.css';

const Filter = ({ name, options, onChange }: FilterProps) => {
  const optionElements = options.map(option => {
    return (
      <option key={option.value} value={option.value}>
        {option.label}
      </option>
    );
  });

  return (
    <select className={styles.filter} name={name} onChange={onChange}>
      {optionElements}
    </select>
  );
};

export default Filter;

체프의 코멘트

(뜬금없는 질문)
React.FC에 대해서 알고 계신가요? 컴포넌트를 React.FC로 타이핑할 때와 그렇지 않을 때는 어떤 차이가 있을까요? (React 18 버전 기준으로)

React.FC는 TypeScript에서 React 함수형 컴포넌트를 정의할 때 사용되는 제네릭 타입입니다. FC는 Functional Component의 약자입니다. React.FC를 사용하면 const Filter: React.FC<FilterProps> = ({ name, options, onChange }) => {... 처럼 수정을 할 수 있습니다.

또한 React 18 버전에서는 React.FC 타입의 암묵적인 children 선언이 제거되었지만, propTypes와 defaultProps 정의는 해주기 때문에 React.FC를 사용하면 이를 정의할 필요가 없습니다.

따라서 React.FC를 사용하면 컴포넌트의 props 타입이 명시적으로 선언되기 때문에 가독성이 좋아지고 코드를 더욱 명확하게 작성할 수 있습니다.

React.FC는 함수형 컴포넌트가 ReactElement 또는 null을 반환한다는 것을 명시적으로 나타내는 데 사용됩니다. 이것은 함수형 컴포넌트가 React 클래스 컴포넌트와 달리 반환 타입을 명시적으로 지정하지 않아도 JSX 요소를 반환하는 것으로 간주되도록 합니다.

그렇기에 이것이 React.FC의 단점이 될 수 있습니다. 왜냐하면 React.FC가 명시적인 반환 타입을 가지지 않기 때문입니다. 이로 인해 일부 상황에서 예기치 않은 동작을 할 수 있습니다.


  1. 리액트 커스텀 훅

커스텀 훅이란?

커스텀 훅은 리액트에서 함수 컴포넌트의 로직을 추상화하고 재사용 가능한 코드로 만드는 기능입니다.

커스텀 훅을 만들기 위한 기준

  • 로직의 관심사 분리

    • 커스텀 훅은 리액트에서 사용하는 컴포넌트의 로직을 추상화하여 만드는 것이 목적입니다. 따라서 커스텀 훅은 리액트의 라이프사이클 메서드, 상태 관리, 이벤트 헨들링 등과 같은 리액트 관련 로직을 추상화하여 만듭니다.
  • 코드의 추상화 수준

    • 커스텀 훅은 컴포넌트 내부 로직을 추상화하여 만드는 경우가 많기 때문에, 여러 컴포넌트에서 재새용이 가능합니다.
  • 의존성

    • 커스텀 훅은 리액트의 라이프사이클 메서드, 상태 관리, 이벤트 핸들링과 같은 리액트 로직에 의존합니다.

커스텀 훅 예시 코드

import React, { useState } from "react";

const useLocal = () => {
  const [local, setLocal] = useState(localStorage.getItem("local") || "");

  const handleChange = (e) => {
    const newLocal = e.target.value;
    setLocal(newLocal);
    localStorage.setItem("local", newLocal);
  };

  return [local, handleChange];
};

const App = () => {
  const [local, isChange] = useLocal();

  return (
    <div>
      <h1>안녕하세요, {local}!</h1>
      <input type="text" value={local} onChange={isChange} />
    </div>
  );
};

export default App;
  • useLocal 반환 값으로 state의 변수와 커스텀 훅 안에 선언해준 함수를 같이 반환을 해줍니다.

  • App 컴포넌트에서 구조 분해 할당을 통해 useLocal 커스텀 훅을 호출하여 반환값을 추출한다.

  • onChange 이벤트에 커스텀 훅에서 받아온 함수를 구조 분해 할당으로 넣어준다.

  • isChange 함수가 input 요소의 onChange 이벤트 핸들러로 설정되면, input 요소의 값이 변경될 때마다 handleChange 함수가 호출된다.

점심 뭐 먹지 커스텀 훅

useEscapeKey.tsx

import { useCallback, useEffect } from 'react';

const useEscapeKey = (handleClose: () => void) => {
  const handleKeyDown = useCallback(
    (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        handleClose();
      }
    },
    [handleClose]
  );

  useEffect(() => {
    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [handleKeyDown]);
};

export default useEscapeKey;

Modal.tsx

import styles from '../Modal/Modal.module.css';
import { getSelectedRestaurant } from '../../data/parseFn';
import { ModalProps } from '../../types/types';
import { CATEGORY_TO_FILENAME } from '../../image/image';
import useEscapeKey from '../../hooks/useEscapeKey';

const Modal = ({ restaurantId, handleClose }: ModalProps) => {
  useEscapeKey(handleClose);
  const selectedRestaurant = getSelectedRestaurant(restaurantId);
  const imageFile = CATEGORY_TO_FILENAME[selectedRestaurant.category];

  return (
    <div className={styles.backdrop}>
      <div className={styles.container}>
        <div className={styles.img}>
          <img src={imageFile} alt={selectedRestaurant.category} className={styles.icon} />
        </div>
        <div>
          <h3 className={styles.subtitle}>{selectedRestaurant.name}</h3>
          <div className={styles.distance}>{`캠퍼스부터 ${selectedRestaurant.distance}분 내`}</div>
          <div className={styles.description}>{selectedRestaurant.description}</div>
          <div className={styles.link}>
            <a href={selectedRestaurant.link}>{selectedRestaurant.link}</a>
          </div>
        </div>
        <button className={styles.button} onClick={handleClose}>
          닫기
        </button>
      </div>
    </div>
  );
};

export default Modal;
  • useEscapeKey를 호출하여 handleClose함수를 전달
profile
프론트엔드 공부하고 있는 정우시입니다.

0개의 댓글