웹 프로그래밍

React 기초(a.k.a.클라이언트 렌더링)

2025-12-08

React 소개

React는 2013년 페이스북이 소개한 SPA(single page application) 솔루션으로, 자바스크립트로 현대적인 웹 애플리케이션을 구축하는 방법을 제공합니다.

SPA의 등장

  • 서버에서 렌더링된 웹사이트
    • 사용자가 URL에 접근하면 웹 서버로 요청 전송
    • 서버가 HTML, CSS, 자바스크립트 파일 반환
    • 페이지 이동 시마다 이 과정 반복
  • SPA 프레임워크: 페이지 전환은 새로운 서버 요청 없이 자바스크립트가 동적으로 새 페이지 렌더링하는 방법을 제공함
    • 서버는 주로 자바스크립트를 최소한의 HTML 파일과 함께 전달함
    • 브라우저가 자바스크립트를 실행하여 전체 애플리케이션을 동적으로 렌더링
  • 현재는 서버와 클라이언트를 모두 다루는 Next.js와 같은 프레임워크가 대중화되어 있음

React의 핵심 개념: 컴포넌트

  • 각 컴포넌트는 HTML, CSS, 자바스크립트를 사용하여 시각적 측면과 기능적 측면을 캡슐화
  • 정의된 컴포넌트들은 계층 구조로 결합되어 전체 애플리케이션을 구축
  • React는 주로 라이브러리로서 컴포넌트에 집중하지만, 유연한 생태계를 통해 프레임워크로서의 기능도 수행

React 프로젝트 설정

Vite를 사용한 React 프로젝트 생성

Vite(빠르다는 뜻의 프랑스어)는 최신 웹 프레임워크를 위한 현대적인 빌드 도구입니다.

  • 개발 서버: React 애플리케이션을 로컬에서 실행
  • 번들러: 프로덕션 배포를 위해 최적화된 파일을 생성

프로젝트 생성

npx create-vite@latest react-stories
  • TypeScript 프로젝트를 선택하세요.
    • 수업에서는 별도의 타입을 다루지 않습니다. 나중에 타입을 추가하세요

프로젝트 구조

  • package.json: 모든 서드파티 의존성 목록과 프로젝트 구성
  • vite.config.js: Vite 구성 파일
  • src/App.jsx: React 컴포넌트가 구현되는 파일
  • src/main.jsx: React 진입점(entry point)
  • src/index.css, src/App.css: 스타일 파일

npm 스크립트

"dev": "vite",           // 개발 서버 실행
"build": "vite build",   // 프로덕션 빌드
"lint": "eslint .",      // 코드 스타일 확인
"preview": "vite preview" // 빌드 테스트

React 컴포넌트

함수형 컴포넌트

모든 React 애플리케이션은 React 컴포넌트를 기반으로 구축됩니다.

// src/App.jsx
function App() {
  return (
    <div>
      <h1>Hello React</h1>
    </div>
  );
}

export default App;

컴포넌트의 특징

  1. 컴포넌트는 자바스크립트 함수입니다
  • 일반 함수와 달리 파스칼 케이스(PascalCase)로 정의
  • 컴포넌트는 대문자로 시작해야 함
  1. 컴포넌트는 HTML과 유사한 코드를 반환합니다
  • JSX(JavaScript XML)라는 새로운 문법 사용
  • 자바스크립트와 HTML을 결합

화살표 함수로 컴포넌트 선언

// 함수 선언식
function App() {
  return (
    <div>
      <h1>Hello React</h1>
    </div>
  );
}

// 화살표 함수 표현식
const App = () => {
  return (
    <div>
      <h1>Hello React</h1>
    </div>
  );
};

간결한 바디 (암시적 return)

JSX만 반환하는 경우 중괄호와 return 문을 생략할 수 있습니다.

// 간결한 바디 (암시적 return)
const App = () => (
  <div>
    <h1>Hello React</h1>
  </div>
);

컴포넌트 선언과 인스턴스화

  • 선언(declaration): 컴포넌트 함수 정의
  • 인스턴스화(instantiation): JSX에서 컴포넌트 사용
function List() { // 컴포넌트 선언
  return (
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  );
}
function App() { // 컴포넌트 인스턴스화
  return (
    <div>
      <List />  {/* 인스턴스 생성 */}
      <List />  {/* 또 다른 인스턴스 */}
    </div>
  );
}

컴포넌트 트리

React로 구성된 페이지는 컴포넌트 트리(component tree)로 구성됩니다.

function App() {
  return (
    <div>
      <h1>My Hacker Stories</h1>
      <Search />  {/* App의 자식 */}
      <List />    {/* App의 자식, Search의 형제 */}
    </div>
  );
}
  • 루트 컴포넌트: 최상위 컴포넌트 (예: App)
  • 부모/자식 컴포넌트: 계층 관계
  • 형제 컴포넌트: 같은 부모를 가진 컴포넌트

React JSX

JSX란?

JSX(JavaScript XML)는 HTML과 자바스크립트를 강력하게 결합하는 문법입니다.

const title = 'React';

function App() {
  return (
    <div>
      <h1>Hello {title}</h1>
    </div>
  );
}

JSX에서 자바스크립트 사용

중괄호 {} 안의 모든 것은 자바스크립트를 보간(interpolation)하는 데 사용할 수 있습니다.

const welcome = {
  greeting: 'Hey',
  title: 'React',
};

function App() {
  return (
    <div>
      <h1>
        {welcome.greeting} {welcome.title}
      </h1>
    </div>
  );
}

JSX와 HTML의 차이점

JSX는 HTML보다 자바스크립트에 더 가깝기 때문에 React는 카멜 케이스(camelCase) 명명 규칙을 사용합니다.

// HTML
<label for="search">Search: </label>
<input id="search" type="text" />

// JSX
<label htmlFor="search">Search: </label>
<input id="search" type="text" />
  • classclassName
  • forhtmlFor
  • onclickonClick

JSX의 내부 동작

JSX는 개발자가 HTML과 자바스크립트를 섞어서 렌더링되어야 할 내용을 표현할 수 있게 해줍니다.

// JSX
const myElement = <h1>Hello {title}</h1>;

// 자바스크립트로 트랜스파일됨
const myElement = React.createElement('h1', null, `Hello ${title}`);

// React에 의해 HTML로 렌더링됨
<h1>Hello React</h1>

React 리스트와 맵

배열의 map() 메서드

React에서 리스트 렌더링을 준비하기 위해 배열의 내장 메서드 map()을 사용합니다.

const numbers = [1, 2, 3, 4];
const exponentialNumbers = numbers.map(function (number) {
  return number * number;
});

console.log(exponentialNumbers); // [1, 4, 9, 16]

리스트 렌더링

React에서는 배열의 내장 map() 메서드를 사용하여 각 아이템에 대한 JSX를 반환합니다.

const list = [
  {
    title: 'React',
    url: 'https://react.dev/',
    author: 'Jordan Walke',
    num_comments: 3,
    points: 4,
    objectID: 0,
  },
  {
    title: 'Redux',
    url: 'https://redux.js.org/',
    author: 'Dan Abramov, Andrew Clark',
    num_comments: 2,
    points: 5,
    objectID: 1,
  },
];

function App() {
  return (
    <div>
      <ul>
        {list.map(function (item) {
          return <li key={item.objectID}>{item.title}</li>;
        })}
      </ul>
    </div>
  );
}

key 속성

리스트의 각 자식은 고유한 key prop을 가져야 합니다.

{list.map(function (item) {
  return <li key={item.objectID}>{item.title}</li>;
})}
  • key는 React가 리스트를 효율적으로 업데이트하는 데 사용
  • 각 아이템의 안정적인 식별자(id 속성)를 사용해야 함
  • 인덱스를 사용하는 것은 피하는 것이 좋음

리스트 아이템 상세 렌더링

<ul>
  {list.map(function (item) {
    return (
      <li key={item.objectID}>
        <span>
          <a href={item.url}>{item.title}</a>
        </span>
        <span>{item.author}</span>
        <span>{item.num_comments}</span>
        <span>{item.points}</span>
      </li>
    );
  })}
</ul>

React 컴포넌트 추출

컴포넌트 분할

컴포넌트는 애플리케이션의 크기에 따라 확장되어야 하므로, 하나의 컴포넌트를 여러 컴포넌트로 분할해야 합니다.

function App() {
  return (
    <div>
      <h1>My React Stories</h1>
      <Search />
      <hr />
      <List />
    </div>
  );
}

function Search() {
  return (
    <div>
      <label htmlFor="search">Search: </label>
      <input id="search" type="text" />
    </div>
  );
}

function List() {
  return (
    <ul>
      {list.map(function (item) {
        return (
          <li key={item.objectID}>
            {/* ... */}
          </li>
        );
      })}
    </ul>
  );
}

React DOM

src/main.jsx 파일에서 App 컴포넌트가 인스턴스화됩니다.

// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);
  • React: React 개발자의 일상적인 업무에 사용
  • React DOM: React 애플리케이션을 네이티브 HTML 세계에 연결

React 이벤트 핸들러

JSX에서의 핸들러 함수

React에서는 JSX에서 선언적인 방식으로 핸들러를 추가합니다.

const Search = () => {
  const handleChange = (event) => {
    console.log(event);
    console.log(event.target.value);
  };

  return (
    <div>
      <label htmlFor="search">Search: </label>
      <input id="search" type="text" onChange={handleChange} />
    </div>
  );
};

합성 이벤트 (Synthetic Event)

React의 합성 이벤트는 본질적으로 브라우저의 네이티브 이벤트를 감싸는 래퍼(wrapper)입니다.

  • React가 싱글 페이지 애플리케이션을 위한 라이브러리로 시작했을 때, 네이티브 브라우저 동작을 방지하기 위해 이벤트에 대한 향상된 기능이 필요
  • 네이티브 HTML 이벤트에 접근해야 한다면 event.nativeEvent를 통해 접근할 수 있음

핸들러 함수 전달 주의사항

함수의 반환 값이 아니라 함수 자체를 핸들러에 전달해야 합니다.

// 이렇게 하지 마세요 (함수 호출 결과를 전달)
<input onChange={handleChange()} />

// 대신 이렇게 하세요 (함수 자체를 전달)
<input onChange={handleChange} />

React Props

Props란?

Props는 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달하는 수단입니다.

function App() { // 부모 컴포넌트
  const stories = [
    { title: 'React', objectID: 0 },
    { title: 'Redux', objectID: 1 },
  ];
  return (
    <div>
      <List list={stories} />  {/* props 전달 */}
    </div>
  );
}

function List(props) { // 자식 컴포넌트
  return (
    <ul>
      {props.list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}

Props의 특징

  • 읽기 전용: Props는 변경할 수 없음(immutable)
  • 단방향 데이터 흐름: 부모 \(\to\) 자식으로만 전달
  • JavaScript 객체: Props는 일반 JavaScript 객체

구조 분해 할당을 통한 Props

Props 객체를 구조 분해하여 더 간결하게 사용할 수 있습니다.

function List({ list }) { // 함수 시그니처에서 구조 분해 (권장)
  return (
    <ul>
      {list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}

컴포넌트 추출과 Props

List 컴포넌트에서 Item 컴포넌트를 추출하고 각 아이템을 전달할 수 있습니다.

const List = (props) => (
  <ul>
    {props.list.map((item) => (
      <Item key={item.objectID} item={item} />
    ))}
  </ul>
);

const Item = (props) => (
  <li>
    <span>
      <a href={props.item.url}>{props.item.title}</a>
    </span>
    <span>{props.item.author}</span>
    <span>{props.item.num_comments}</span>
    <span>{props.item.points}</span>
  </li>
);

React State

State란?

React State는 변경 가능한(mutable) 데이터 구조입니다.

  • Props는 부모에서 자식 컴포넌트로 정보를 전달하는 데 사용
  • State는 시간이 지남에 따라 정보를 수정하는 데 사용

useState 훅

useState 훅을 사용하여 상태 값을 관리할 수 있습니다.

import * as React from 'react';

const Search = () => {
  const [searchTerm, setSearchTerm] = React.useState('');

  const handleChange = (event) => {
    setSearchTerm(event.target.value);
  };

  return (
    <div>
      <label htmlFor="search">Search: </label>
      <input id="search" type="text" onChange={handleChange} />
      <p>
        Searching for <strong>{searchTerm}</strong>.
      </p>
    </div>
  );
};

useState의 구조

const [value, setValue] = React.useState('');
// value: state (현재 상태)
// setValue: state updater function (상태 업데이트 함수)
// '': initial state (초기 상태)
  • useState는 초기 상태를 인자로 받습니다
  • 두 개의 항목이 있는 배열을 반환합니다
    • 첫 번째 항목: 현재 상태
    • 두 번째 항목: 상태를 업데이트하는 함수

렌더링과 리렌더링

  • 초기 렌더링: React 컴포넌트가 브라우저에 표시될 때 발생
  • 리렌더링: state가 변경되면 해당 state를 가진 컴포넌트와 모든 자식 컴포넌트가 다시 렌더링됩니다

콜백 핸들러

콜백 핸들러는 컴포넌트 트리 위쪽으로 소통하기 위한 암시적인 수단입니다.

// A: 부모 컴포넌트에서 핸들러 정의
const App = () => {
  const handleSearch = (event) => {
    console.log(event.target.value);
  };

  return (
    <div>
      {/* B: 자식 컴포넌트로 함수 전달 */}
      <Search onSearch={handleSearch} />
    </div>
  );
};

// C: 자식 컴포넌트에서 함수 호출
const Search = (props) => {
  const handleChange = (event) => {
    props.onSearch(event);  // D: 부모로 다시 호출
  };

  return (
    <input onChange={handleChange} />
  );
};

React 상태 끌어올리기

상태 끌어올리기란?

한 컴포넌트에서 다른 컴포넌트로 state를 이동시키는 과정을 상태 끌어올리기(Lifting State)라고 합니다.

const App = () => {
  const stories = [ /* ... */ ];
  const [searchTerm, setSearchTerm] = React.useState('');

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
  };

  const searchedStories = stories.filter((story) =>
    story.title.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <div>
      <Search onSearch={handleSearch} />
      <List list={searchedStories} />
    </div>
  );
};

상태 위치 결정 규칙

State는 항상 그 State에 관심이 있는 모든 컴포넌트가 State를 직접 관리하거나, State를 관리하는 컴포넌트의 하위 컴포넌트로서 정보를 Props로 받아 사용하는 위치에서 관리해야 합니다.

  • 하위 컴포넌트가 State를 업데이트해야 한다면: 콜백 핸들러를 아래로 전달
  • 하위 컴포넌트가 State를 사용해야 한다면: Props로 전달

제어 컴포넌트 (Controlled Component)

HTML 요소의 value 속성을 활용하여 React의 state를 사용하도록 합니다.

const App = () => {
  const [searchTerm, setSearchTerm] = React.useState('React');

  return (
    <div>
      <Search search={searchTerm} onSearch={handleSearch} />
    </div>
  );
};

const Search = (props) => (
  <div>
    <label htmlFor="search">Search: </label>
    <input
      id="search"
      type="text"
      value={props.search}      {/* React state 사용 */}
      onChange={props.onSearch}
    />
  </div>
);

React Props 고급 기법

객체 구조 분해 할당

// 함수 시그니처에서 구조 분해 (권장)
const Search = ({ search, onSearch }) => (
  <div>
    <label htmlFor="search">Search: </label>
    <input
      id="search"
      type="text"
      value={search}
      onChange={onSearch}
    />
  </div>
);

전개 연산자와 나머지 연산자

// 전개 연산자 사용
const List = ({ list }) => (
  <ul>
    {list.map((item) => (
      <Item key={item.objectID} {...item} />
    ))}
  </ul>
);

// 나머지 연산자 사용
const List = ({ list }) => (
  <ul>
    {list.map(({ objectID, ...item }) => (
      <Item key={objectID} {...item} />
    ))}
  </ul>
);

React 사이드 이펙트

useEffect 훅

React의 useEffect 훅은 컴포넌트의 생명주기에 사이드 이펙트를 포함하는 것을 용이하게 합니다.

const App = () => {
  const [searchTerm, setSearchTerm] = React.useState(
    localStorage.getItem('search') || 'React'
  );

  React.useEffect(() => {
    localStorage.setItem('search', searchTerm);
  }, [searchTerm]);

  // ...
};

useEffect의 구조

React.useEffect(() => {
  // 사이드 이펙트를 실행하는 함수
}, [searchTerm]); // 의존성 배열
  • 첫 번째 인자: 사이드 이펙트를 실행하는 함수
  • 두 번째 인자: 의존성 배열 - 이 변수 중 하나라도 변경되면 사이드 이펙트 함수가 호출됩니다

의존성 배열

  • 의존성 배열 생략: 컴포넌트의 모든 렌더링마다 실행
  • 빈 배열 []: 컴포넌트가 처음 렌더링 될 때만 딱 한 번 호출
  • 의존성 포함: 해당 값이 변경될 때마다 실행

React 커스텀 훅

커스텀 훅이란?

커스텀 훅은 특정 요구사항에 맞춰 자체적인 훅을 만드는 것입니다.

const useStorageState = (key, initialState) => {
  const [value, setValue] = React.useState(
    localStorage.getItem(key) || initialState
  );

  React.useEffect(() => {
    localStorage.setItem(key, value);
  }, [value, key]);

  return [value, setValue];
};

커스텀 훅 사용

const App = () => {
  const [searchTerm, setSearchTerm] = useStorageState(
    'search',
    'React'
  );

  // ...
};
  • 모든 훅 이름 앞에 “use” 접두사를 붙이는 명명 규칙
  • 반환되는 값들이 배열로 반환됩니다

React 프래그먼트

프래그먼트란?

React 프래그먼트는 렌더링 된 출력에 추가하지 않고 형제 엘리먼트들을 하나의 최상위 엘리먼트로 감쌉니다.

const Search = ({ search, onSearch }) => ( // React.Fragment 사용
  <React.Fragment>
    <label htmlFor="search">Search: </label>
    <input
      id="search"
      type="text"
      value={search}
      onChange={onSearch}
    />
  </React.Fragment>
);

const Search = ({ search, onSearch }) => ( // 단축 문법 사용
  <>
    <label htmlFor="search">Search: </label>
    <input
      id="search"
      type="text"
      value={search}
      onChange={onSearch}
    />
  </>
);

children prop

React children prop을 사용하면 React 컴포넌트들을 서로 합성할 수 있습니다.

const App = () => {
  return (
    <div>
      <InputWithLabel
        id="search"
        value={searchTerm}
        onInputChange={handleSearch}
      >
        <strong>Search:</strong>
      </InputWithLabel>
    </div>
  );
};

const InputWithLabel = ({
  id,
  value,
  onInputChange,
  children,
}) => (
  <>
    <label htmlFor={id}>{children}</label>
    <input
      id={id}
      type="text"
      value={value}
      onChange={onInputChange}
    />
  </>
);

React 명령형

선언형 vs 명령형

  • 선언형 프로그래밍: 원하는 결과를 지정하는 데 중점
  • 명령형 프로그래밍: 작업을 수행하는 방법을 자세히 설명하는 단계별 지침 제공

React는 선언형 프로그래밍 접근 방식을 사용하지만, 필요할 때는 명령형 접근 방식을 수행할 수 있습니다.

useRef 훅

useRef 훅을 사용하여 DOM 엘리먼트에 직접 접근할 수 있습니다.

const InputWithLabel = ({
  id,
  value,
  onInputChange,
  isFocused,
  children,
}) => {
  const inputRef = React.useRef();

  React.useEffect(() => {
    if (isFocused && inputRef.current) {
      inputRef.current.focus();
    }
  }, [isFocused]);

  return (
    <>
      <label htmlFor={id}>{children}</label>
      <input
        ref={inputRef}
        id={id}
        type="text"
        value={value}
        onChange={onInputChange}
      />
    </>
  );
};

useRef의 동작

  1. useRef 훅으로 ref 객체 생성
  2. ref는 엘리먼트의 JSX 예약 속성인 ref에 전달
  3. useEffect 훅으로 React의 생명주기에 관여
  4. ref.current 속성을 통해 엘리먼트에 접근

업데이트 이력

버전 변경 이력

버전 날짜 변경 내용
v.20251103 2025-11-03 React 기초 내용 추가

참고문헌