.jsx는 브라우저에서 지원되지 않는 파일 확장자이다. 이것이 작동할 수 있는 이유는 리액트 프로젝트에서 이 특별한 확장자를 지원하기 때문이다. 따라서 .jsx 확장자는 개발 서버가 실행될 때 빌드 프로세스에게 해당 파일이 JSX 코드를 포함하고 있다는 것을 알려주는 역할을 하는 것이다.
❗️ 이 확장자를 처리하는 것은 그 빌드 프로세스 뿐이라는 것을 이해하자
그렇기 때문에 .jsx 대신 js만 사용하는 리액트 프로젝트도 찾을 수 있다. 그리고 그 .js 파일 안에서도 JSX 코드를 찾을 수 있다. 이것은 단순히 파일의 빌드 프로세스에 따라 JSX 구문을 사용할 때 어떤 확장자가 예상될지 결정된다.
결국, .jsx와 .js의 차이는 단지 개발자가 파일을 구분하기 위한 용도일 뿐이고, 빌드 도구가 그 파일을 어떻게 처리할지에 대한 설정에 따라 JSX 코드를 쓸 수 있다는 것이다.
JSX 코드는 마치 트리(나무) 모양의 코드 구조를 띄며 리액트에게 각 컴포넌트들이 어떻게 연관되어 있고 UI는 어떻게 보여야 하는지 알려준다. 그 다음, 올바른 명령어를 실행하여 실제 DOM을 제어하며 타겟 구조/코드를 반영한다.
++ JSX를 사용하는 건 선택이다. JSX의 사용은 편하게 해주는 것이지 필수가 아니다.
하지만 JSX는 브라우저가 직접 이해할 수 없으므로, React.createElement()로 변환되는 빌드 과정을 거쳐야 한다.
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 방식으로 이미지를 불러온다면 빌드 프로세스에서 더 안전하다는 것을 알게되었다.
<CoreConcept {...concept} /> - 열자마자 닫기 (Self-closing 태그)
컴포넌트가 독립적이며 자체적으로 동작하거나 props 데이터만으로 충분한 경우 사용
<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 <TabButton>Components</TabButton>
function TabButton({children}) {
return <button>{children}</button>;
}
Attribute Prop <TabButton label="Components"></TabButton>
function TabButton({label}) {
return <button>{label}</button>;
}
<TabButton onSelect={handleSelect}>JSX</TabButton>
<TabButton onSelect={ ()=> handleSelect('jsx') }>JSX</TabButton>
<TabButton onSelect={handleSelect('jsx')}>JSX</TabButton>
👀 왜 사용 불가일까?
JSX 코드를 작성하면, React는 컴포넌트를 렌더링하면서 모든 속성을 평가한다.
평가 과정에서 onSelect={handleSelect('jsx')}와 같은 표현이 있으면, handleSelect('jsx')가 즉시 실행된다.
즉, React가 컴포넌트를 렌더링할 때 함수가 호출되어, handleSelect('jsx')의 반환값이 onSelect에 전달된다.
➡️ 인수를 전달하고 싶다면 화살표 함수로 감싸줘야 한다.
React의 이벤트 속성에는 반드시 함수를 전달해야 한다.
왜냐하면, React는 이벤트가 발생했을 때 전달된 함수를 호출하도록 설계되어 있기 때문이다.
(값 x 함수 o)
id나 class 같은 속성은 자동으로 전달되지 않으므로, 아래의 코드와 같이 수동으로 직접 전달해주어야 한다.
각 속성을 적어서 데이터를 전달하고 구조 분해 할당으로 그 데이터를 다시 꺼내어서 사용한다.
속성을 명시적으로 하나하나 전달하는 방식은 직관적이고 명시적이라는 장점이 있지만, 속성이 많아질 경우 코드가 길어지고 유지보수가 어려워질 수 있다.
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를 사용하면 전달된 모든 속성을 구조 분해하지 않고도 한 번에 넘길 수 있어 코드가 간결해진다.
하지만 어떤 데이터를 꺼내와야할지 헷갈리는 경우가 있다.
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>
);
}
title, children)...props로 묶어서 관리 (id, className, style) - 위의 코드 처럼아래의 코드처럼 기존의 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는 React에서 컴포넌트에 전달되는 props의 데이터 타입과 필수 여부를 검증하는 도구이다.
이를 통해 컴포넌트가 예상치 못한 props를 받을 때 발생하는 오류를 사전에 방지할 수 있다.
예측 가능한 컴포넌트 동작
컴포넌트가 올바른 데이터 타입의 props만 받도록 제한하여 오류를 줄일 수 있다.
가독성과 유지보수성 향상
코드만 보아도 컴포넌트가 기대하는 props의 타입과 구조를 명확히 알 수 있다.
디버깅 편의성
잘못된 props가 전달되면 경고 메시지가 콘솔에 출력된다
| 데이터 타입 | 설명 |
|---|---|
| PropTypes.string | 문자열 |
| PropTypes.number | 숫자 |
| PropTypes.bool | 불리언 |
| PropTypes.array | 배열 |
| PropTypes.object | 객체 |
| PropTypes.func | 함수 |
| PropTypes.node | React 노드 (문자열, JSX, 배열, null 등) |
| PropTypes.element | React 요소 |
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;
React에서는 TypeScript를 사용해 props를 검증하는 것이 더 강력하고 선호되는 방법이다.
| 특징 | PropTypes | TypeScript |
|---|---|---|
| 타입 검증 시점 | 런타임(실행 중) | 컴파일 타임 |
| 타입 선언 | 범위 | props 검증에 국한 |
| 오류 발견 시점 | 잘못된 props가 전달되면 콘솔 경고 | 컴파일 단계에서 오류 표시 |
| 추가 도구 | React 내장 도구 | 추가적으로 TypeScript 설정 필요 |
PropTypes는 간단한 프로젝트나 빠르게 검증 로직을 추가할 때 유용하다.TypeScript는 프로젝트 규모가 커지거나 타입 안정성을 더 강하게 요구하는 경우 적합하다.리액트는 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};
);
간단하게 className을 사용하여 스타일링 하면 된다. 동적으로 스타일링 하고 싶다면 삼항 연산자나 단축 평가 사용하기!
<button className={isSelected ? 'active' : undefined }></button>
✔️ 각각을 태그로 작성
이 방식을 사용하면 리스트 (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>
함수형 컴포넌트에서 상태(state)와 라이프사이클 기능을 사용할 수 있도록 도와주는 React의 특별한 함수이다.
(클래스형 컴포넌트 ➡️ 함수형 컴포넌트)
useState, useEffect 등) // ❌ 잘못된 예
if (someCondition) {
const [state, setState] = useState(0); // 에러 발생
}
// ✅ 올바른 예
const [state, setState] = useState(0);
// ❌ 잘못된 예
function regularFunction() { // 일반 함수 (소문자로 시작)
const [state, setState] = useState(0); // 에러 발생
}
// ✅ 올바른 예
function Component() { // 컴포넌트 함수 (대문자로 시작)
const [state, setState] = useState(0);
}
useState, useReduceruseEffect, useLayoutEffectuseMemo, useCallbackuseRefuseContext