완벽가이드 - 리액트 핵심

primav·2024년 11월 18일

React

목록 보기
20/35
post-thumbnail

📌 JSX

.jsx는 브라우저에서 지원되지 않는 파일 확장자이다. 이것이 작동할 수 있는 이유는 리액트 프로젝트에서 이 특별한 확장자를 지원하기 때문이다. 따라서 .jsx 확장자는 개발 서버가 실행될 때 빌드 프로세스에게 해당 파일이 JSX 코드를 포함하고 있다는 것을 알려주는 역할을 하는 것이다.

❗️ 이 확장자를 처리하는 것은 그 빌드 프로세스 뿐이라는 것을 이해하자

그렇기 때문에 .jsx 대신 js만 사용하는 리액트 프로젝트도 찾을 수 있다. 그리고 그 .js 파일 안에서도 JSX 코드를 찾을 수 있다. 이것은 단순히 파일의 빌드 프로세스에 따라 JSX 구문을 사용할 때 어떤 확장자가 예상될지 결정된다.

결국, .jsx.js의 차이는 단지 개발자가 파일을 구분하기 위한 용도일 뿐이고, 빌드 도구가 그 파일을 어떻게 처리할지에 대한 설정에 따라 JSX 코드를 쓸 수 있다는 것이다.

JSX의 역할

JSX 코드는 마치 트리(나무) 모양의 코드 구조를 띄며 리액트에게 각 컴포넌트들이 어떻게 연관되어 있고 UI는 어떻게 보여야 하는지 알려준다. 그 다음, 올바른 명령어를 실행하여 실제 DOM을 제어하며 타겟 구조/코드를 반영한다.

++ JSX를 사용하는 건 선택이다. JSX의 사용은 편하게 해주는 것이지 필수가 아니다.
하지만 JSX는 브라우저가 직접 이해할 수 없으므로, React.createElement()로 변환되는 빌드 과정을 거쳐야 한다.


📌 속성

이미지 불러오기 - import

React에서 이미지를 import 하면, 빌드 도구 (Vite, Webpack 등)가 해당 파일을 정적으로 분석하여 번들링 과정에서 올바른 경로로 변환한다.

따라서, 빌드 후 이미지 파일이 다른 폴더로 이동하거나 이름이 해시 처리될 경우에도 import한 경로는 자동으로 업데이트된다.

이미지 import 장점 (모듈 처럼)

  • 파일 경로 관리 용이 : 프로젝트 구조 변경되어도 일일이 경로를 수정할 필요 없음
  • 빌드 최적화 : 빌드 과정에서 이미지 크기 최적화, 압축, 캐싱 자동으로 적용
  • 일관성있는 접근 방식 : 다른 자산 (CSS, JS)과의 접근 방식과 일관됨

✔️ Before : 이미지 경로로 불러오기

<img src="./src/image.png">

✔️ After : 이미지를 import 로 불러오기

import image from './src/image.png';

<img src="./src/image.png">

👀 알게된 점

예전에 바닐라 JS로 SPA 개념을 이용하여 프로젝트를 만들어본 적이 있는데, 이때 배포를 하려고 할 때 경로 문제로 이미지가 안불러와져 각각의 파일로 들어가서 전부 절대 경로로 수정한 상황이 있었다.

SPA 특성상 다른 파일의 코드를 API로 불러오는 경우 경로의 문제가 많았는데, 이때 리액트에서 import 방식으로 이미지를 불러온다면 빌드 프로세스에서 더 안전하다는 것을 알게되었다.


컴포넌트 불러오는 방법

  1. <CoreConcept {...concept} /> - 열자마자 닫기 (Self-closing 태그)
    컴포넌트가 독립적이며 자체적으로 동작하거나 props 데이터만으로 충분한 경우 사용

  2. <TabButton>Components</TabButton> - 사이에 넣고싶은 것 있을 때 (children)
    컴포넌트가 외부에서 제공되는 유동적인 내용을 포함해야 하거나, 자식 컴포넌트를 렌더링하는 경우 사용

특징Self-closing 태그(<CoreConcept {...concept} />)Opening/Closing 태그 (Components)
데이터 전달 방식props로만 전달 ({...props} 포함 가능)태그 사이 내용은 children으로 전달
사용 목적고정된 데이터나 구조를 렌더링컴포넌트 내용이 유동적이거나, 텍스트/JSX를 직접 지정할 때 사용
유연성데이터 형태가 고정적전달하는 데이터에 다양한 형태(JSX, HTML 구조 등) 가능
태그 형태열자마자 닫기(Self-closing)열고 닫는 태그 (Opening/Closing Tag)
사용 시기간단히 컴포넌트를 동작시키고자 할 때 적합컴포넌트 내부에 동적으로 렌더링할 내용을 포함할 때 적합

2번째 방법에서 태그 사이의 텍스트를 꺼낼 때 children을 사용한다.

export default function TabButton({children}) {
  return (
    <li>
    	<button>{children}</button>
    </li>
);}

children Prop vs Attribute Prop

  • children Prop
    버튼 내용이 동적으로 변하거나 복잡한 JSX 구조를 포함해야 할 때 사용
    • ex) 아이콘과 텍스트를 함께 렌더링하는 경우
	<TabButton>Components</TabButton>

	function TabButton({children}) {
     	return <button>{children}</button>; 
    }
  • Attribute Prop
    버튼 내용이 단순한 문자열처럼 고정적이고 간단할 때 사용
    • ex) 버튼 라벨만 전달하는 경우
	<TabButton label="Components"></TabButton>

	function TabButton({label}) {
     	return <button>{label}</button>; 
    }

이벤트 함수에 매개변수 전달하기

<TabButton onSelect={handleSelect}>JSX</TabButton>
  • 그냥 함수 호출
  • 기본적으로 이벤트 객체(event)가 자동 전달됨
<TabButton onSelect={ ()=> handleSelect('jsx') }>JSX</TabButton>
  • 화살표 함수로 호출 (매개변수 전달)
  • 사용자 정의 인수('jsx')를 명시적으로 전달
<TabButton onSelect={handleSelect('jsx')}>JSX</TabButton>
  • 절대 사용 불가

👀 왜 사용 불가일까?

JSX 코드를 작성하면, React는 컴포넌트를 렌더링하면서 모든 속성을 평가한다.
평가 과정에서 onSelect={handleSelect('jsx')}와 같은 표현이 있으면, handleSelect('jsx')가 즉시 실행된다.
즉, React가 컴포넌트를 렌더링할 때 함수가 호출되어, handleSelect('jsx')반환값onSelect에 전달된다.

➡️ 인수를 전달하고 싶다면 화살표 함수로 감싸줘야 한다.

React의 이벤트 속성에는 반드시 함수를 전달해야 한다.
왜냐하면, React는 이벤트가 발생했을 때 전달된 함수를 호출하도록 설계되어 있기 때문이다.
(값 x 함수 o)


Props 속성 하나씩 전달 (id, class)

idclass 같은 속성은 자동으로 전달되지 않으므로, 아래의 코드와 같이 수동으로 직접 전달해주어야 한다.
각 속성을 적어서 데이터를 전달하고 구조 분해 할당으로 그 데이터를 다시 꺼내어서 사용한다.

속성을 명시적으로 하나하나 전달하는 방식은 직관적이고 명시적이라는 장점이 있지만, 속성이 많아질 경우 코드가 길어지고 유지보수가 어려워질 수 있다.

export default function Examples() { // 데이터 전달

  function handleSelect(selectedButton) {
  return (
    <Section title="Examples" ⭐️ d="examples" className="" ⭐️️>
      {/* id는 props로 넘길 수 없음!! */}
      <h2>Examples</h2>
      <menu>
        ...
      </menu>
      {tabContent}
    </Section>
  );
}

export default function Section(⭐️ { title, id, className, children } ️⭐️) { // 데이터 받기
  return (
    <section id={id} className={className}>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

하지만 이렇게 하나하나 수동으로 속성을 설정하는 방식은 비효율적이다.
그러면 어떻게 사용하는 것이 효율적일까?

...props

...props를 사용하면 전달된 모든 속성을 구조 분해하지 않고도 한 번에 넘길 수 있어 코드가 간결해진다.
하지만 어떤 데이터를 꺼내와야할지 헷갈리는 경우가 있다.

export default function Examples() { // 데이터 전달

  function handleSelect(selectedButton) {
  return (
    <Section title="Examples" ⭐️ d="examples" className="" ⭐️️>
      {/* id는 props로 넘길 수 없음!! */}
      <h2>Examples</h2>
      <menu>
        ...
      </menu>
      {tabContent}
    </Section>
  );
}

export default function Section(⭐️ { title, children, ...props } ️⭐️) { // 데이터 받기
  return (
    <section {...props}>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

➡️ 혼합해서 사용하기 (...props + 속성 따로)

  • 주요 속성은 명시적으로 관리 (title, children)
  • 기타 속성은 ...props로 묶어서 관리 (id, className, style) - 위의 코드 처럼

JSX 엘리먼트의 집합을 props로 전달

아래의 코드처럼 기존의 Examples 컴포넌트에서 Tabs 컴포넌트를 분리한다.
그 이유는 책임 분리를 통해 코드가 더 읽기 쉬워지고 유지보수하기 좋아지기 때문이다.

  • Examples.jsx - 데이터와 상태를 관리하는 부분
  • Tabs.jsx - 탭 UI를 구성하는 부분

만약, 버튼을 Tabs 컴포넌트에서 직접 정의했다면, 고정된 형태로 관리할 수 밖에 없다.
리액트의 단방향 특성상 자식 컴포넌트에서 상태 변경을 할 수 없기 때문!!

근데 보낼 데이터가 많으니 JSX로 한꺼번에 객체로 모아서 보내는 것이당

✔️ 데이터 전달 - Tabs.jsx
<Tabs
  buttons={ // 버튼의 상태 & 동작 관리는 전부 이곳에서 이루어짐
    <>
      <TabButton
        isSelected={selectedTopic === "components"}
        onSelect={() => handleSelect("components")}
      >
        Components
      </TabButton>
      <TabButton
        isSelected={selectedTopic === "jsx"}
        onSelect={() => handleSelect("jsx")}
      >
        JSX
      </TabButton>
      {/* ... */}
    </>
  }
>
  {tabContent}
</Tabs>

✔️ 컴포넌트 (데이터 받음) - Examples.jsx

function Tabs({ children, buttons }) { // 임의로 props 이름 지정해서 받아오기 (jsx 객체가 넘어옴)
  return (
    <>
      <menu>{buttons}</menu> {/* 버튼 렌더링 */}
      {children}           {/* 탭 내용 렌더링 */}
    </>
  );
}

👀 그렇다면 왜 JSX 객체를 한번에 넘길까?

  • 부모 컴포넌트 (Examples) - 상태와 로직 관리
    그 상태를 자식에게 props를 통해 전달하여 UI를 업데이트 한다.

  • 자식 컴포넌트 (Tabs)
    부모에게 받은 상태를 바탕으로 UI를 렌더링한다.

간단하게 말하면, 부모와 자식 컴포넌트 간의 동작을 분리하여 재사용성, 유연성을 높이기 위해 컴포넌트를 분리하고 JSX를 객체로서 넘긴다. (필요한 요소를 한번에 넘기기 위해)

💡 결론

Tabs 컴포넌트는 UI의 구조와 렌더링을 담당한다.
상태나 버튼 클릭 후 발생하는 동작은 부모인 Examples 컴포넌트에서 관리하고, 그 상태를 props로 전달하는 방식이다.

따라서 Tabs 컴포넌트는 상태 변경을 알지 못하며, 단순히 부모에서 넘겨준 대로 버튼들을 화면에 렌더링(그림)하는 역할만 한다고 할 수 있다.

컨테이너 요소를 만들기

아래 코드와 같이 버튼 그룹을 감싸는 컨테이너 요소를 만드는 방법이 있다.

이렇게 만드는 이유는 만약 Tabs 컴포넌트를 사용하고 싶은데 버튼을 menu로 감싸고 싶지 않고, div, ul, ol 등으로 변화해서 사용하고 싶은 경우에 자신이 원하는 컨테이너 태그를 동적으로 선택할 수 있도록 하는 방식이다.

만약 div, menu 로 고정하고 싶다면 굳이 만들 필요 없음

✔️ Examples.jsx

<Tabs
    ButtonsContainer="menu"
    buttons={...}>
</Tabs>

✔️ Tabs.jsx
                 
export default function Tabs({ children, buttons, ButtonsContainer }) {
  return (
    <>
      <ButtonsContainer>{buttons}</ButtonsContainer>
      {children}
    </>
  );
}

👀 헷갈렸던 점

그러면 이때 <ButtonsContainer>란 무엇일까? 컴포넌트일까 HTML 태그일까?

정답은 자기 정의한 React 컴포넌트로 이해를 해야 한다.
그렇다면 HTML 태그는 div, menu와 같이 따로 이름이 있는데 우리가 마음대로 지정해도 될까?

React에서는 ButtonsContainer를 임의로 이름을 지정한 컴포넌트로 사용하고, 이를 JSX 요소로 취급한다.
말이 어렵지만 ... 그냥 임의로 만든 태그이며 기본 HTML 태그 처럼 사용한다고 이해하자 ..

Container="section" 이라고 지정하면
<Container>{buttons}</Container> = <section>{buttons}</section> 으로 표현된다.

📌 PropTypes

PropTypes는 React에서 컴포넌트에 전달되는 props의 데이터 타입과 필수 여부를 검증하는 도구이다.
이를 통해 컴포넌트가 예상치 못한 props를 받을 때 발생하는 오류를 사전에 방지할 수 있다.

왜 사용할까?

  • 예측 가능한 컴포넌트 동작
    컴포넌트가 올바른 데이터 타입의 props만 받도록 제한하여 오류를 줄일 수 있다.

  • 가독성과 유지보수성 향상
    코드만 보아도 컴포넌트가 기대하는 props의 타입과 구조를 명확히 알 수 있다.

  • 디버깅 편의성
    잘못된 props가 전달되면 경고 메시지가 콘솔에 출력된다

PropTypes 정의 방법

데이터 타입설명
PropTypes.string문자열
PropTypes.number숫자
PropTypes.bool불리언
PropTypes.array배열
PropTypes.object객체
PropTypes.func함수
PropTypes.nodeReact 노드 (문자열, JSX, 배열, null 등)
PropTypes.elementReact 요소
import PropTypes from 'prop-types';

function Section({ title, id, className, children }) {
  return (
    <section id={id} className={className}>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

// PropTypes를 사용하여 props의 타입과 필수 여부를 정의
Section.propTypes = {
  title: PropTypes.string.isRequired, // title은 문자열이어야 하며 필수
  id: PropTypes.string,              // id는 문자열일 수 있음(필수 아님)
  className: PropTypes.string,       // className도 문자열일 수 있음(필수 아님)
  children: PropTypes.node,          // children은 렌더링 가능한 React 노드
};

export default Section;

TypeScript vs PropTypes

React에서는 TypeScript를 사용해 props를 검증하는 것이 더 강력하고 선호되는 방법이다.

특징PropTypesTypeScript
타입 검증 시점런타임(실행 중)컴파일 타임
타입 선언범위props 검증에 국한
오류 발견 시점잘못된 props가 전달되면 콘솔 경고컴파일 단계에서 오류 표시
추가 도구React 내장 도구추가적으로 TypeScript 설정 필요
  • PropTypes는 간단한 프로젝트나 빠르게 검증 로직을 추가할 때 유용하다.
  • TypeScript는 프로젝트 규모가 커지거나 타입 안정성을 더 강하게 요구하는 경우 적합하다.

📌 상태

상태 (State) 가 필요한 이유

리액트는 JSX 코드를 보고 현재 렌더링된 UI와 비교하기 때문에 UI를 업데이트 하려면 이 코드가 리액트에 의해 재평가 되어야 한다. 재평가시 변화를 탐지한다면 그에 맞춰 UI를 업데이트 한다.

리액트는 컴포넌트 함수를 발견했을 때 한번만 실행(렌더링)한다.
따라서 아래의 코드에서 TabButton 컴포넌트는 3번 발견했을 때 3번 실행이 되는 것이고 App 컴포넌트는 index.html에 의해 한번만 실행되므로, App의 UI는 변하지 않는다.

export default function App() {
  return (
    <>
      <TabButton>Components</TabButton>
      <TabButton>Props</TabButton>
      <TabButton>State</TabButton>
    </>
  );
}

💡 상태(State)와 컴포넌트 렌더링 동작 원리

React는 컴포넌트의 상태(State)나 속성(Props)이 변경될 때 해당 컴포넌트와 그 자식 컴포넌트를 다시 렌더링한다. 하지만 상태나 속성이 변하지 않는 한, 컴포넌트 함수는 한 번만 실행된다.

React 렌더링의 특징

  • 컴포넌트 함수 실행
    : React는 컴포넌트 함수(App, TabButton 등)를 호출하여 JSX를 생성

  • DOM 비교
    : 생성된 JSX는 React가 기존 UI와 비교하여 변경된 부분만 업데이트


조건적 콘텐츠 렌더링

  • 삼항 연산자
	{!selectedTopic ? <p>Please Select a topic. </p> : <div>...</div>}
  • 단축 평가
	{!selectedTopic && <p>Please Select a topic. </p>}
  • 변수 사용
let tabContent = <p>Please Select a topic. </p>;

if (selectedTopic) {
 tabContent = <div>...</div> 
}

return (
  {tabConent};
);

CSS 동적 스타일링

간단하게 className을 사용하여 스타일링 하면 된다. 동적으로 스타일링 하고 싶다면 삼항 연산자나 단축 평가 사용하기!

<button className={isSelected ? 'active' : undefined }></button>

리스트 (객체) 데이터 동적 출력 (map)

✔️ 각각을 태그로 작성
이 방식을 사용하면 리스트 (CORE_CONCEPT 객체)가 삭제되거나 추가되면 그에 따라 또 코드를 바꿔야 한다.

<ul>
  <CoreConcept {...CORE_CONCEPT[0]} />
  <CoreConcept {...CORE_CONCEPT[1]} />
  <CoreConcept {...CORE_CONCEPT[2]} />
</ul>

✔️ map 함수를 이용하여 동적으로 관리
이렇게 하면 리스트가 변경되어도 코드를 수정해줄 필요가 없다. 또한 간단하다.

❗️ 꼭 키를 지정해줘야 한다. (객체의 유니크한 값으로) --> key 지정 안하면 에러남

<ul>
  {CORE_CONCEPTS.map((conceptItem) => (
    	<CoreConcept key={concept.title}{...CORE_CONCEPT} />
   ))}
</ul>

➡️ ex)

<ul>
  {[1, 2, 3].map(number => <li key={number}>{number}</li>}
</ul>

📌 Hook

함수형 컴포넌트에서 상태(state)와 라이프사이클 기능을 사용할 수 있도록 도와주는 React의 특별한 함수이다.
(클래스형 컴포넌트 ➡️ 함수형 컴포넌트)

Hook의 특징

  • 훅은 함수이다. (useState, useEffect 등)
  • 함수형 컴포넌트에서만 사용할 수 있다
  • React의 상태와 라이프사이클 기능을 함수형 컴포넌트에서도 사용할 수 있게 한다.
  • 항상 use라는 접두어로 시작한다.

Hook 사용 규칙

  • 훅은 최상위 레벨에서만 호출
	// ❌ 잘못된 예
	if (someCondition) {
 	 const [state, setState] = useState(0); // 에러 발생
	}

	// ✅ 올바른 예
	const [state, setState] = useState(0);
  • 훅은 React 컴포넌트 함수에서만 호출
	// ❌ 잘못된 예
	function regularFunction() { // 일반 함수 (소문자로 시작)
 	 const [state, setState] = useState(0); // 에러 발생
	}

	// ✅ 올바른 예
	function Component() { // 컴포넌트 함수 (대문자로 시작)
  	const [state, setState] = useState(0);
	}

Hook의 종류

  • 상태 관리 훅: useState, useReducer
  • 라이프사이클 훅: useEffect, useLayoutEffect
  • 성능 최적화 훅: useMemo, useCallback
  • 참조 훅: useRef
  • 컨텍스트 훅: useContext

0개의 댓글