본문 바로가기

나야, 리액트 스터디

[week 1] 리액트 공식문서 - UI표현하기(첫번째 컴포넌트, 컴포넌트 import 및 export하기, JSX로 마크업 작성하기, 중괄호에 있는 JSX안에서 자바스크립트 사용하기, 컴포넌트에 props 전달하기)

안녕하세요! 웹파트 OB 김건휘입니다. 이번 시간에는 리액트 공식문서 - UI표현하기(첫번째 컴포넌트, 컴포넌트 import 및 export하기, JSX로 마크업 작성하기, 중괄호에 있는 JSX안에서 자바스크립트 사용하기, 컴포넌트에 props 전달하기)를 읽고 정리한 내용에 대한 아티클을 작성해보겠습니다.

🌐컴포넌트란?

컴포넌트는 리액트 앱을 구성하는 기본 단위로, 재사용 가능한 UI 조각을 말한다. 이 컴포넌트들은 각각 자체적인 렌더링 로직과 상태 관리를 가지며, 복잡한 UI를 구성하기 위해 서로 조합하고 재사용할 수 있습니다.

 

다음 사진과 같이 컴포넌트들를 조합하여 화면을 구성할 수 있다. 컴포넌트를 한 번 정의한 다음 원하는 곳에서 원하는 만큼 여러 번 사용할 수 있다는 점이 바로 React의 킥이라고 할 수 있다.

 

🔥주의사항 - React 컴포넌트는 대문자로 시작해야 한다
<Component />처럼 JSX에서 컴포넌트는 대문자로 시작해야 React가 이를 사용자 정의 컴포넌트로 인식한다.

 

//Profile.jsx

export default function Profile() {
  return (
    <img
      src="https://i.imgur.com/MK3eW3As.jpg"
      alt="Katherine Johnson"
    />
  );
}



//Gallery.jsx
import Profile from './Profile.js';

export default function Gallery() {
  return (
    <section>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}

 

import와 export를 사용하여, 다음과 같이 별도의 파일에서 정의해준 Profile 컴포넌트를 import해와서 원하는 곳에서 여러번 재사용할 수 있다. 또한, 리액트는 JSX로 마크업을 작성한다.

 

🌐JSX(JavaScript XML)란?

JSX는JavaScript와 HTML을 결합한 문법으로, React에서 UI를 정의할 때 사용하는 문법이다. JSX는 자바스크립트 코드 안에서 HTML과 비슷한 구조를 표현할 수 있게 해주며, React 컴포넌트의 구조를 직관적으로 작성할 수 있게 도와준다.

 

📌JSX의 특징 및 동작 원리

 

1. HTML과 유사하지만 JavaScript 확장 문법

JSX는 HTML과 비슷하게 보이지만, 내부적으로 JavaScript로 변환된다. 브라우저는 JSX를 이해하지 못하므로 Babel 같은 도구가 JSX 코드를 React.createElement() 함수 호출로 변환한다.

 

const element = <h1>Hello, World!</h1>;

 

위 코드는 Babel(React에서 사용하는 JSX 문법을 브라우저가 이해할 수 있는 JavaScript 코드로 변환해주는 도구)을 통해 다음과 같이 변환된다.

const element = React.createElement('h1', null, 'Hello, World!');

 

2. JavaScript 표현식 사용 가능 ({} 중괄호)

JSX 내부에서는 중괄호 {}를 사용하여 JavaScript 표현식을 사용할 수 있다.

const name = 'Alice';
const element = <h1>Hello, {name}!</h1>;

- 조건문, 반복문, 함수 호출 등의 자바스크립트 코드를 JSX 안에 넣어 사용할 수 있다.

 

📌JSX의 장점

 

1. UI의 명확한 구조 표현
HTML과 유사한 문법을 사용하므로, 컴포넌트의 UI 구조를 쉽게 이해할 수 있다. => 적절한 시맨틱 태그의 중요성이 JSX에도 적용된다.

 

2. JavaScript와의 강력한 결합
JavaScript 표현식을 사용해 동적 데이터를 다루기 쉬워진다.

 

🌐컴포넌트에 props 전달하기

React 컴포넌트는 props를 이용해 서로 통신할 수 있다. 부모 컴포넌트는 props를 줌으로써 몇몇의 정보를 자식 컴포넌트에게 전달할 수 있다. 객체, 배열, 함수를 포함한 모든 JavaScript 값을 전달할 수 있다. 리액트에서 props는 정말 중요한 역할을 한다. props를 사용하지 않는다면 리액트를 사용할 이유가 없을 정도로 강력한 무기이다. props는 리액트에서 매우 중요한 개념이므로 제대로 이해하고 넘어가자!

 

📌props를 활용하는 다양한 상황

1. 부모-자식 간 데이터 전달

- 상위 컴포넌트가 하위 컴포넌트에 데이터를 전달하는 방식이 props

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

function App() {
  return <Greeting name="Alice" />;
}

부모 컴포넌트(App)에서 자식 컴포넌트(Greeting)으로 name을 props로 전달하고 있다.

 

2. 컴포넌트의 재사용성 증가

- 동일한 컴포넌트를 다양한 props로 호출하여 여러 상황에 맞게 재사용할 수 있다.

function Button({ label, color }) {
  return <button style={{ backgroundColor: color }}>{label}</button>;
}

function App() {
  return (
    <>
      <Button label="Primary" color="blue" />
      <Button label="Danger" color="red" />
    </>
  );
}

button의 text와 backgroundColor를 props로 전달해주어 Button 컴포넌트의 재사용성을 증가 시켜줄 수 있다.

 

3. UI와 로직의 분리

function UserProfile({ user }) {
  return <h1>{user.name}</h1>;
}

function App() {
  const user = { name: "Alice", age: 25 };
  return <UserProfile user={user} />;
}

부모 컴포넌트가 데이터를 제어하고, 자식 컴포넌트는 데이터를 표현하는 역할만 담당하게 할 수 있다.

 

4. 콜백 함수로 상위 컴포넌트와 소통 가능

function Button({ onClick }) {
  return <button onClick={onClick}>Click Me</button>;
}

function App() {
  const handleClick = () => alert('Button clicked!');
  return <Button onClick={handleClick} />;
}

props를 통해 자식 컴포넌트가 콜백 함수를 호출하여 상위 컴포넌트와 소통할 수 있다. 이를 통해 양방향 데이터 흐름이 가능하다.

 

5. 조건부 렌더링과 동적 UI 구현 가능

function Message({ isLoggedIn }) {
  return <p>{isLoggedIn ? 'Welcome back!' : 'Please log in.'}</p>;
}

function App() {
  return <Message isLoggedIn={true} />;
}

props를 통해 동적으로 데이터를 변경하면서 UI를 제어할 수 있다. 이를 통해 조건부 렌더링이나 다양한 UI 상태를 쉽게 구현할 수 있다.

 

6. 컴포넌트 간 결합도 감소

- props를 통해 데이터를 명시적으로 전달함으로써 컴포넌트들이 서로 느슨하게 결합된다. => 응집도와 결합도가 뭔데?

 

❗️응집도 (Cohesion)

응집도는 하나의 컴포넌트 내부에 존재하는 요소들이 서로 얼마나 밀접하게 관련되어 있는지를 나타낸다. 응집도가 높은 컴포넌트는 잘 정의된 목적을 가지고 있으며, 해당 컴포넌트 안의 모든 기능이 이 목적과 직접적으로 관련되어 있다. 응집도가 높으면 컴포넌트의 재사용성, 유지보수성, 이해하기 쉬움이 향상된다.

 

❗️결합도 (Coupling)

결합도는 서로 다른 컴포넌트들 간의 의존성의 정도를 나타낸다. 결합도가 낮은 시스템은 각 컴포넌트가 독립적으로 작동하며, 다른 컴포넌트와의 의존성이 적다. 결합도가 낮을수록 컴포넌트를 수정하거나 대체하기가 더 쉬워지고, 시스템의 유연성이 증가한다.

 

- 컴포넌트 내부에서 직접 상태를 가지지 않고, 외부에서 필요한 데이터를 주입받으므로 테스트와 유지보수가 쉬워진다.

 

7. 상태 관리와의 통합

function Counter({ count }) {
  return <h1>Count: {count}</h1>;
}

function App() {
  const [count, setCount] = React.useState(0);
  return (
    <>
      <Counter count={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </>
  );
}

props는 상태(state)와 함께 사용되어 더 복잡한 UI와 데이터 흐름을 쉽게 관리할 수 있게 해준다.

상태와 props를 함께 사용해 상위 컴포넌트에서 데이터 관리를 하고, 자식 컴포넌트는 데이터를 표시하기만 한다.

 

8. 컴포넌트 간 확장성 제공 (children)

// Card.js
import React from 'react';

function Card({ children }) {
  return (
    <div style={{ border: '1px solid #ddd', padding: '16px', borderRadius: '8px' }}>
      {children}
    </div>
  );
}

export default Card;
// App.js
import React from 'react';
import Card from './Card';

function App() {
  return (
    <div style={{ padding: '20px' }}>
      <Card>
        <h2>Welcome</h2>
        <p>This is a reusable card component.</p>
      </Card>

      <Card>
        <img src="https://via.placeholder.com/150" alt="Sample" />
        <p>Image inside a card.</p>
      </Card>

      <Card>
        <button onClick={() => alert('Button clicked!')}>Click Me</button>
      </Card>
    </div>
  );
}

export default App;

Card 컴포넌트는 children을 사용하여 동적으로 콘텐츠를 삽입할 수 있다.

어떤 종류의 콘텐츠도 전달 가능하며, 이를 통해 컴포넌트의 재사용성이 높아진다. => children을 사용하면 하나의 컴포넌트로 여러 종류의 UI를 처리할 수 있다.

 

🔥children을 잘 활용하면 변경에 유연한, 확장 가능성있는 컴포넌트를 설계할 수 있다!! 

 

  • Props를 전달하려면 HTML 어트리뷰트를 사용할 때와 마찬가지로 JSX에 props를 추가합니다.
  • Props를 읽으려면 function Avatar({ person, size }) 구조 분해 할당 문법을 사용합니다.
  • size = 100 과 같은 기본값을 지정할 수 있으며, 이는 누락되거나 undefined 인 props에 사용됩니다.
  • 모든 props를 <Avatar {...props} />로 전달할 수 있습니다. JSX spread 문법을 사용할 수 있지만 과도하게 사용하지 마세요!
  • <Card><Avatar /></Card>와 같이 중첩된 JSX는 Card컴포넌트의 자식 컴포넌트로 나타납니다.
  • Props는 읽기 전용 스냅샷으로, 렌더링 할 때마다 새로운 버전의 props를 받습니다.
  • Props는 변경할 수 없습니다. 상호작용이 필요한 경우 state를 설정해야 합니다.

해당 내용은 공식문서 컴포넌트에 props 전달하기 카테고리의 마지막에 나와있는 요약본이다.

해당 내용을 읽고 바로바로 이해가 되고 납득이 되었다면 props에 대해서 어느정도 통달?했다고 할 수 있다.

 

"Props는 변경할 수 없습니다. 상호작용이 필요한 경우 state를 설정해야 합니다."

function Counter({ initialCount }) {
  // 잘못된 예: props를 직접 수정 시도
  initialCount += 1; // ❌ props는 변경할 수 없습니다.

  return <h1>Count: {initialCount}</h1>;
}

function App() {
  return <Counter initialCount={0} />;
}

 

리액트는 부모에서 전달된 props를 불변(immutable)으로 간주한다. 따라서 위와 같이 자식 컴포넌트는 props를 직접 변경할 수 없다.

=> 상호작용이 필요한 경우 state(상태)를 사용해야 한다!

 

import { useState } from 'react';

function Counter({ initialCount }) {
  // state를 사용해 동적으로 값 관리
  const [count, setCount] = useState(initialCount);

  const increment = () => setCount(count + 1); // count 값 증가

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

function App() {
  return <Counter initialCount={0} />;
}

export default App;

 

사용자 상호작용(버튼 클릭)에 따라 값을 변경하려면 state를 사용해 변경 가능한 상태를 관리해야 한다.

useState 훅을 사용하면 초기값으로 initialCount를 설정하고, 상태가 변경될 때마다 컴포넌트가 리렌더링된다.

 

🧐궁금 Point

더보기

리액트 공식문서를 읽으면서 들었던 궁금증 중 하나는 JSX에서 함수를 직접 전달하는 것과 래핑(wrapping)한 함수를 전달하는 것의 차이점은 무엇일까?이다.

- 예를들어서 JSX 안에서 <button onClick={() => handleClick()} />과 <button onClick={handleClick} /> 있으면, 성능과 가독성에 어떤 영향 들이 있는지 차이점과 장단점에 대해서 궁금했다.

 

React에서 props는 불변(immutable)이어야 한다고 하지만, 만약 컴포넌트 내에서 props를 직접 수정하면 어떤 문제들이 발생할까? 

- 예측 가능성을 높이기 위해 props를 불변으로 간주하는건 납득이 가지만, 오히려 리소스가 적은 props 변경에 있어서는 state를 사용하여 얻는 성능문제(리렌더링 발생)보다 이점이 크지 않을까?라는 궁금증이 들었다.