TIL 24. 03. 05

클린코드 리액트 - Components

컴포넌트란?

Traditionally when creating web pages, web developers marked up their content and then added interaction by sprinkling on some JavaScript. This worked great when interaction was a nice-to-have on the web. Now it is expected for many sites and all apps. React puts interactivity first while still using the same technology: a React component is a JavaScript function that you can *sprinkle with markup*. Defining a component

기존에 웹 페이지를 만들 때는 웹 개발자가 HTML 마크업을 한 다음 자바스크립트를 입히는 방식으로 상호작용을 추가했다. 이 방식은 웹에서 상호작용이 부가적인 기능이었을 때는 잘 작동했다. 이제는 많은 사이트와 모든 애플리케이션에서 사용자의 수많은 상호작용을 다루고 응답하여 보여준다.

React 컴포넌트는 마크업을 적절히 포함할 수 있는 자바스크립트 함수이다. JSX 문법을 사용하여 HTML과 유사한 마크업을 자바스크립트와 함께 사용할 수 있으며, 사용자 이벤트에 반응하는 이벤트 핸들러를 JSX 안에 직접 적용할 수 있다.

컴포넌트는 선언적인 UI 업데이트, 상태 관리를 통해 복잡한 사용자 상호작용을 직관적이고 효율적으로 다룰 수 있게 해준다.

Self-Closing Tags

일반적인 HTML 태그는 여는 태그(<tag>)와 닫는 태그(</tag>)의 쌍으로 존재하고 태그 사이에는 내용을 포함하고 있다. Self-Closing Tags란 여는 태그와 닫는 태그를 구분하지 않고 /를 단일 태그(<tag />) 안에 포함시킨다.

HTML과 유사한 마크업을 작성할 수 있는 JSX 문법을 사용하기 때문에 기본 HTML 요소인지 리액트 컴포넌트인지 명확한 차이를 가져야 한다.

JSX에서는 컴포넌트의 태그가 첫 글자가 대문자로 시작되는 경우 사용자 정의 컴포넌트로 간주하고, 소문자로 시작하면 내장 HTML 태그로 간주한다. 리액트 컴포넌트 이름의 첫 글자는 대문자로 작성한다.

자식 요소를 갖지 않는 컴포넌트나 HTML 요소는 셀프 클로징(<ComponentName /> 또는 <img />) 형태로 작성할 수 있다.

function Component() {
  return (
    <Container>
      <Header />
      <Main>
        <img />
        (...)
      </Main>
      <Footer />
    </Container>
  )
}

Fragment 지향하기

JSX에는 항상 단일 요소를 반환해야 한다는 규칙이 있다. 이는 컴포넌트를 렌더링할 때 React가 최종적으로 페이지에 렌더링되는 HTML에 해당하는 가상 DOM 트리를 생성한다. 그런데 여러 개의 루트 요소가 있는 경우 React는 이를 처리하는 방법을 알지 못해 오류를 일으킨다.

function Component() {
  return (
    <ComponentA /> // ❌ JSX expressions must have one parent element.
    <ComponentB /> 
  )
}

간단한 해결책 중 하나는 여러 JSX 요소를 <div>와 같은 단일 상위 요소로 감싸는 것이다.


function Component() {
  return (
    <div>
      <ComponentA /> 
      <ComponentB /> 
    </div>
  )
}

하지만 div 태그로 인해 스타일이 깨질 수 있고 불필요한 div 태그가 많아진다.

단일 엘리먼트가 필요한 상황에서 엘리먼트들을 <Fragment>로 묶어 함께 그룹화한다. <Fragment>로 엘리먼트들을 그룹화하더라도 실제 DOM에는 아무런 영향을 주지 않으며, 그룹화하지 않은 것과 동일하다.

빈 JSX 태그 <></><Fragment>의 축약 표현.

단, Fragment에 key를 전달하려는 경우 <>…</>구문을 사용할 수 없다. 'react' 에서 Fragment를 명시적으로 불러오고 <Fragment key={}>...</Fragment>를 렌더링해야한다.

function Component({ items }) {
  return (
    {items.map(({ id, title, content }) => 
      <Fragment key={item.id}>
        <h2>{title}</h2>
        <p>{content}</p>
      </Fragment>
    }
  )
}

좋은 컴포넌트 네이밍

일반적으로 컴포넌트는 PascalCase 표기법을 사용한다. 기본 HTML 요소는 lower case를 사용하는데 그 이유는 리액트 컴포넌트를 만드는 함수 React.createElement()에 전달되는 요소는 기본 HTML 요소이기 때문이다.

JSX 컴포넌트 함수로 변환

리액트 컴포넌트의 return 부분에서 함수를 직접 호출하는 것은 피해야 할 패턴 중 하나이다.

function Component() {
  const TopRender = () => {
    return (
      <header>
         <h1>Header</h1>
      </header>
    );
  };
    
  return (
    <div>
      {TopRender()}
    </div>
  )
}

이렇게 사용하더라도 문제 없이 잘 동작하는 것 같지만 여러 문제를 발생시킬 수 있다.

  • 연산된 값의 결과가 무엇인지 예측하기도 힘들고 컴포넌트를 반환하더라도 props를 전달하기 어렵다.
  • 컴포넌트의 return 문 내에서 함수를 호출하면, 그 함수가 매 렌더링마다 호출된다. 이는 성능 저하의 원인이 될 수 있다.
  • 함수가 state를 변경하거나 다른 사이드 이펙트를 발생시켜 예측 불가능한 렌더링 동작을 야기한다.

컴포넌트 내부의 inner 컴포넌트 선언

function OuterComponent() {
  const InnerComponent = () => {
    return <div>Inner Component</div>
  }  
  
  return (
    <div>
      <InnerComponent />
    </div>
  )
}

컴포넌트를 재사용할 것도 아니고 굳이 파일을 하나 더 만들어서 컴포넌트를 선언해야 하나? 라고 생각된다면 컴포넌트 외부에 선언하면 된다.

const InnerComponent = () => {
  return <div>Inner Component</div>
}  

function OuterComponent() {
  return (
    <div>
      <InnerComponent />
    </div>
  )
}

컴포넌트 내부에 컴포넌트를 선언하게 되면 컴포넌트의 결합도가 증가한다. 컴포넌트가 구조적으로 종속되어 확장성을 고려하여 분리하는 것이 어려워진다.

또한, 상위 컴포넌트가 리렌더링이 되면 하위 컴포넌트 역시 재생성된다. 이는 성능 저하로 이어진다.

컴포넌트 구성하기

컴포넌트 구성은 어떻게 해야할까? 컴포넌트의 일관성을 유지하고 코드를 파악하기 위한 시간을 줄이기 위해 고민할 필요가 있다.

// 1. 상수는 외부에 선언
const DEFAULT_COUNT = 100;

// 2. 타입 선언 (컴포넌트명과 일치하는 Props 타입명)
interface ComponentProps { ... }

// 3. 컴포넌트 선언 방식 (함수 선언문, 함수 표현식)
const Component = ({ prop1, prop2 }: ComponentProps) => {
  // 4. 변수의 선언 위치
  // Flag 변수 -> Ref 변수 -> 서드 파티 라이브러리 -> custom hooks -> 컴포넌트 내부 상태
  const isHold = false;
  
  const ref = useRef(null);
    
  const location = useLocation();
  const queryClient = useQueryClient();
  const state = useSelector((state) => state);
  
  const [value, setValue] = useState();
  
  // 5. Event
  const onClose = () => handleClose();
    
  // 6. Ealry Return JSX
  if (isHold) return null
    
  // 7. Main JSX와 가장 가까운 곳에 위치
  useEffect(() => { ... }, [])
  
  // 8. JSX 반환은 항상 개행 후 작성
  return (
    <div>
      ...
    </div>
  )
}
            
// 9. Style 관련
const Button = styled.button`
  padding: 0.5rem 0;
  ...
`
  
export default Component;

정해진 답이 없기 때문에 자신/팀의 규칙을 만들어 지키면 된다.

카테고리:

업데이트:

댓글남기기