파일첨부 화면 React 컴포넌트 설계 순서

세 개의 PNG 화면을 기준으로, 화면을 사각 박스로 나누고 어떤 순서로 React 컴포넌트로 쪼개면 좋은지 설명합니다.

1. 먼저 큰 덩어리부터 나누기

입문자는 처음부터 버튼, 아이콘, 텍스트를 잘게 나누기보다 페이지를 큰 사각형으로 먼저 보는 것이 좋습니다. 이 화면은 공통적으로 Header, Content, Bottom Actions 세 영역으로 나눌 수 있습니다.

Header Content/List Bottom Actions Upload/Title Notice/Selected
파일첨부1.png empty
파일이 없는 파일첨부 화면
파일첨부2.png list
파일 목록이 있는 파일첨부 화면
파일첨부3.png selecting
파일이 선택된 파일첨부 화면
핵심: 화면이 달라져도 계속 남아 있는 영역은 공통 컴포넌트 후보입니다. 화면마다 내용이 바뀌는 영역은 상태나 props로 제어할 후보입니다.

2. React 작업순서

React 화면 작업은 바로 코드를 치기보다, 디자인을 보고 영역과 상태를 먼저 정리한 뒤 구현하는 순서가 좋습니다. 아래 순서대로 가면 컴포넌트를 어디서 쪼개야 하는지 덜 헷갈립니다.

1디자인 확인

먼저 PNG 화면 3장을 보고 공통으로 유지되는 부분과 화면마다 달라지는 부분을 구분합니다. 이 예시는 파일첨부 풀팝업의 empty, list, selecting 상태입니다.

2큰 영역 분리

화면을 가장 큰 사각형부터 나눕니다. 이 화면은 FullPopupHeader, FullPopupBody, FullPopupFooter 세 영역으로 먼저 나눌 수 있습니다.

3반복되는 영역 찾기

같은 모양이 여러 번 나오는 부분을 찾습니다. 첨부파일 카드가 반복되므로 파일 한 개는 FileItem, 목록 전체는 FileList 후보가 됩니다.

4상태에 따라 바뀌는 영역 찾기

파일이 없을 때는 EmptyAttachView, 파일이 있을 때는 AttachedFileView가 보입니다. 선택된 파일이 있으면 하단 버튼도 취소 / 선택삭제로 바뀝니다.

5컴포넌트 Tree 작성

바로 구현하지 말고 먼저 트리를 씁니다. 이 화면은 FullPopup 아래에 FileAttachContent와 FileAttachFooter가 들어가는 구조입니다.

6각 컴포넌트 책임 결정

FullPopup은 팝업 껍데기, FileAttachContent는 파일첨부 본문, FileItem은 파일 한 개처럼 컴포넌트마다 할 일을 하나씩 정합니다.

7props 결정

하위 컴포넌트가 화면을 그리는 데 필요한 값과 클릭 이벤트를 props로 정합니다. 예를 들면 files, selectedIds, onToggleFile, onAttach입니다.

8state 위치 결정

여러 컴포넌트가 같이 써야 하는 상태는 위쪽에 둡니다. 이 화면에서는 files와 selectedIds를 FileAttachPopup 또는 그 부모에서 관리하는 편이 좋습니다.

9이벤트 흐름 결정

데이터는 위에서 아래로 내려가고, 이벤트는 아래에서 위로 올라갑니다. FileItem 클릭은 onToggleFile(file.id)로 부모에게 알려줍니다.

10구현

정한 트리와 props를 기준으로 JSX를 작성합니다. 먼저 큰 컴포넌트를 만들고, 그 안에 map, 조건부 렌더링, 버튼 이벤트를 채워 넣습니다.

11중복 발견 후 공통화

처음부터 모든 것을 공통 컴포넌트로 만들 필요는 없습니다. 구현 후 비슷한 버튼, 리스트, 팝업 구조가 반복되면 그때 FullPopup, FileItem처럼 공통화합니다.

3. 추천 컴포넌트 구조

이 화면은 아래처럼 나누면 역할이 명확합니다. 처음에는 이 정도만 나누고, 더 작게 쪼개는 것은 나중에 해도 됩니다.

컴포넌트 트리

FullPopup ├─ FullPopupHeader │ ├─ title: "파일첨부" │ └─ close button ├─ FullPopupBody │ └─ FileAttachContent │ ├─ EmptyAttachView │ │ ├─ UploadDropzone │ │ └─ NoticeBox │ └─ AttachedFileView │ ├─ FileSectionHeader │ │ ├─ title: "첨부파일" │ │ └─ 전체선택 button │ └─ FileList │ └─ FileItem 반복 └─ FullPopupFooter └─ FileAttachFooter ├─ 파일첨부 / 확인 └─ 취소 / 선택삭제

추천 파일 구조

src/ ├─ components/ │ ├─ FullPopup/ │ │ ├─ FullPopup.jsx │ │ ├─ FullPopup.css │ │ ├─ FullPopupHeader.jsx │ │ ├─ FullPopupBody.jsx │ │ └─ FullPopupFooter.jsx │ └─ FileAttach/ │ ├─ FileAttachContent.jsx │ ├─ FileAttach.css │ ├─ EmptyAttachView.jsx │ ├─ AttachedFileView.jsx │ ├─ FileList.jsx │ ├─ FileItem.jsx │ └─ FileAttachFooter.jsx └─ utils/ ├─ cn.js ├─ focusTrap.js └─ pageScrollLock.js
처음 만들 때는 이렇게 시작하세요: 입문 단계에서는 팝업 공통 스타일은 FullPopup.css, 파일첨부 전용 스타일은 FileAttach.css에 나누어 두는 편이 이해하기 쉽습니다. FullPopup은 다른 화면에서도 재사용될 수 있고, FileAttach는 파일첨부 기능에만 집중할 수 있습니다.

컴포넌트별 책임

컴포넌트 하는 일 받으면 좋은 props
FullPopup 전체 화면 팝업의 공통 껍데기를 담당합니다. open, title, footer, onClose, children
FullPopupHeader 상단 제목과 닫기 버튼을 보여줍니다. title, onClose
FileAttachContent 파일첨부 화면의 상태와 내용을 담당합니다. files, selectedIds, onAttach, onToggleFile, onSelectAll
EmptyAttachView 파일이 없을 때 업로드 영역과 주의사항을 보여줍니다. onAttach
AttachedFileView 첨부파일 제목, 전체선택 버튼, 파일 목록을 보여줍니다. files, selectedIds, onToggle, onSelectAll
FileItem 파일 한 개의 선택 버튼, 이름, 썸네일을 보여줍니다. file, selected, onToggle
FullPopupFooter 팝업 하단 영역의 공통 자리만 잡아줍니다. children
FileAttachFooter 파일첨부 화면의 선택 상태에 맞는 하단 버튼을 보여줍니다. hasSelection, onAttach, onConfirm, onCancelSelection, onDeleteSelected

4. React 코드 뼈대 예시

실제 프로젝트에서는 스타일 클래스와 파일 업로드 로직을 붙이면 됩니다. 아래 코드는 컴포넌트 분리 구조를 이해하기 위한 뼈대입니다.

function FileAttachPopup({
  open,
  files,
  selectedIds,
  onClose,
  onAttach,
  onConfirm,
  onToggleFile,
  onSelectAll,
  onCancelSelection,
  onDeleteSelected,
}) {
  const hasSelection = selectedIds.length > 0

  return (
    <FullPopup
      open={open}
      title="파일첨부"
      onClose={onClose}
      footer={
        <FileAttachFooter
          hasSelection={hasSelection}
          onAttach={onAttach}
          onConfirm={onConfirm}
          onCancelSelection={onCancelSelection}
          onDeleteSelected={onDeleteSelected}
        />
      }
    >
      <FileAttachContent
        files={files}
        selectedIds={selectedIds}
        onAttach={onAttach}
        onToggleFile={onToggleFile}
        onSelectAll={onSelectAll}
      />
    </FullPopup>
  )
}

FullPopup 공통 껍데기

function FullPopup({ open, title, onClose, footer, children }) {
  if (!open) return null

  return (
    <section className="full-popup" role="dialog" aria-modal="true">
      <FullPopupHeader title={title} onClose={onClose} />

      <main className="full-popup-body">
        {children}
      </main>

      <FullPopupFooter>
        {footer}
      </FullPopupFooter>
    </section>
  )
}

파일첨부 본문

function FileAttachContent({
  files,
  selectedIds,
  onAttach,
  onToggleFile,
  onSelectAll,
}) {
  const hasFiles = files.length > 0

  if (!hasFiles) {
    return <EmptyAttachView onAttach={onAttach} />
  }

  return (
    <AttachedFileView
      files={files}
      selectedIds={selectedIds}
      onToggleFile={onToggleFile}
      onSelectAll={onSelectAll}
    />
  )
}

파일첨부 하단 버튼

function FileAttachFooter({
  hasSelection,
  onAttach,
  onConfirm,
  onCancelSelection,
  onDeleteSelected,
}) {
  if (hasSelection) {
    return (
      <>
        <button type="button" onClick={onCancelSelection}>취소</button>
        <button type="button" onClick={onDeleteSelected}>선택삭제</button>
      </>
    )
  }

  return (
    <>
      <button type="button" onClick={onAttach}>파일첨부</button>
      <button type="button" onClick={onConfirm}>확인</button>
    </>
  )
}

이 props들을 추가한 이유

props는 크게 두 종류로 보면 이해하기 쉽습니다. files, selectedIds처럼 화면을 그리는 데 필요한 데이터 props가 있고, onClose, onAttach처럼 사용자의 행동을 부모에게 알려주는 이벤트 props가 있습니다.

open

FullPopup을 화면에 보여줄지 결정하는 값입니다. false이면 팝업 전체를 렌더링하지 않습니다.

title

FullPopupHeader에 표시할 제목입니다. 파일첨부 화면에서는 "파일첨부"를 넘기지만, 다른 팝업에서는 다른 제목을 재사용할 수 있습니다.

children

FullPopupBody 안에 들어갈 실제 화면 내용입니다. 여기서는 FileAttachContent가 children으로 들어갑니다.

footer

FullPopupFooter 안에 들어갈 하단 버튼 영역입니다. 파일첨부 화면에서는 선택 상태에 따라 버튼이 바뀌는 FileAttachFooter를 넘깁니다.

files

첨부된 파일 목록입니다. 이 값으로 files.length > 0을 판단해서 빈 화면을 보여줄지, 파일 목록 화면을 보여줄지 결정합니다.

selectedIds

현재 선택된 파일 id 목록입니다. 선택된 파일이 있으면 하단 버튼을 취소 / 선택삭제로 바꾸고, 각 FileItem의 선택 상태도 이 값으로 판단합니다.

onClose

상단 X 버튼을 눌렀을 때 실행할 함수입니다. 실제로 모달을 닫는 상태는 보통 부모 컴포넌트가 가지고 있으므로 함수로 전달받습니다.

onAttach

파일첨부 버튼이나 업로드 영역을 눌렀을 때 실행할 함수입니다. 파일 선택창을 열거나 업로드 로직을 시작하는 역할입니다.

onConfirm

파일 첨부를 완료하고 확인 버튼을 눌렀을 때 실행할 함수입니다. 첨부된 파일을 저장하거나 다음 단계로 넘길 때 사용합니다.

onToggleFile

파일 한 개를 선택하거나 선택 해제할 때 실행할 함수입니다. FileItem은 클릭 사실만 알려주고, 실제 selectedIds 변경은 부모가 처리합니다.

onSelectAll

전체선택 버튼을 눌렀을 때 실행할 함수입니다. 모든 파일 id를 선택 목록에 넣는 로직은 파일 목록 전체를 알고 있는 부모 쪽에 두는 것이 자연스럽습니다.

onCancelSelection

선택 모드를 취소할 때 실행할 함수입니다. 보통 selectedIds를 빈 배열로 만들어 선택 상태를 초기화합니다.

onDeleteSelected

선택된 파일들을 삭제할 때 실행할 함수입니다. 삭제 후에는 files와 selectedIds를 함께 갱신해야 하므로 부모가 처리하는 것이 좋습니다.

하위 컴포넌트에는 필요한 props만 넘기기

하위 컴포넌트 넘기는 props 이유
FullPopupHeader title, onClose 상단 영역은 제목을 보여주고 닫기 버튼만 처리하면 됩니다.
FullPopupBody children 공통 팝업은 본문 내용이 무엇인지 몰라도 되므로 children으로 받아 끼워 넣습니다.
FileAttachContent files, selectedIds, onAttach, onToggleFile, onSelectAll 파일이 없는 화면과 파일 목록 화면 중 무엇을 보여줄지 결정하는 파일첨부 전용 본문입니다.
EmptyAttachView onAttach 파일이 없을 때 필요한 행동은 파일 첨부뿐입니다.
AttachedFileView files, selectedIds, onToggleFile, onSelectAll 파일 목록을 그리고, 각 파일의 선택 여부를 판단하고, 개별 선택과 전체선택을 처리해야 합니다.
FileItem file, selected, onToggle 파일 한 개만 담당하므로 전체 배열이 아니라 자기 파일과 선택 여부만 받습니다.
FileAttachFooter hasSelection, 버튼별 이벤트 함수 선택 상태에 따라 버튼 구성이 달라지고, 각 버튼 클릭을 부모에게 알려야 합니다.
핵심 규칙: 데이터는 위에서 아래로 내려주고, 이벤트는 아래에서 위로 알려줍니다.

파일 목록 부분

function AttachedFileView({ files, selectedIds, onToggleFile, onSelectAll }) {
  return (
    <section className="attached-file-view">
      <div className="file-section-header">
        <h2>첨부파일</h2>
        <button type="button" onClick={onSelectAll}>전체선택</button>
      </div>

      <ul className="file-list">
        {files.map((file) => (
          <FileItem
            key={file.id}
            file={file}
            selected={selectedIds.includes(file.id)}
            onToggle={() => onToggleFile(file.id)}
          />
        ))}
      </ul>
    </section>
  )
}

파일 카드 한 개

function FileItem({ file, selected, onToggle }) {
  return (
    <li className={selected ? "file-item selected" : "file-item"}>
      <button type="button" className="check-button" onClick={onToggle}>
        {selected ? "선택됨" : "선택"}
      </button>

      <span className="file-name">{file.name}</span>

      <img className="file-thumb" src={file.thumbnailUrl} alt="" />
    </li>
  )
}
연습 방법: 먼저 박스 3개로 Header / Body / Footer만 구현하고, 그 다음 Body 안을 EmptyAttachView와 AttachedFileView로 나누세요. 마지막에 FileItem을 map으로 반복하면 화면 구조가 자연스럽게 잡힙니다.

5. 전체 파일 윤곽 코드

아래 코드는 지금까지 정리한 구조를 실제 파일로 만든다고 가정했을 때의 대략적인 윤곽입니다. 그대로 완성본이라기보다, 어떤 파일에 어떤 책임과 props가 들어가는지 확인하기 위한 출발점입니다.

폴더 기준: FullPopup은 여러 화면에서 재사용할 공통 팝업이고, FileAttach는 파일첨부 화면 전용 컴포넌트입니다.

components/FullPopup/FullPopup.jsx

import "./FullPopup.css"
import { FullPopupHeader } from "./FullPopupHeader"
import { FullPopupBody } from "./FullPopupBody"
import { FullPopupFooter } from "./FullPopupFooter"

export function FullPopup({ open, title, onClose, footer, children }) {
  if (!open) return null

  return (
    <section className="full-popup" role="dialog" aria-modal="true">
      <FullPopupHeader title={title} onClose={onClose} />
      <FullPopupBody>{children}</FullPopupBody>
      <FullPopupFooter>{footer}</FullPopupFooter>
    </section>
  )
}

components/FullPopup/FullPopupHeader.jsx

export function FullPopupHeader({ title, onClose }) {
  return (
    <header className="full-popup-header">
      <h1>{title}</h1>
      <button type="button" className="full-popup-close" onClick={onClose} aria-label="닫기">
        ×
      </button>
    </header>
  )
}

components/FullPopup/FullPopupBody.jsx

export function FullPopupBody({ children }) {
  return <main className="full-popup-body">{children}</main>
}

components/FullPopup/FullPopupFooter.jsx

export function FullPopupFooter({ children }) {
  return <footer className="full-popup-footer">{children}</footer>
}

components/FileAttach/FileAttachPopup.jsx

import { FullPopup } from "../FullPopup/FullPopup"
import { FileAttachContent } from "./FileAttachContent"
import { FileAttachFooter } from "./FileAttachFooter"
import "./FileAttach.css"

export function FileAttachPopup({
  open,
  files,
  selectedIds,
  onClose,
  onAttach,
  onConfirm,
  onToggleFile,
  onSelectAll,
  onCancelSelection,
  onDeleteSelected,
}) {
  const hasSelection = selectedIds.length > 0

  return (
    <FullPopup
      open={open}
      title="파일첨부"
      onClose={onClose}
      footer={
        <FileAttachFooter
          hasSelection={hasSelection}
          onAttach={onAttach}
          onConfirm={onConfirm}
          onCancelSelection={onCancelSelection}
          onDeleteSelected={onDeleteSelected}
        />
      }
    >
      <FileAttachContent
        files={files}
        selectedIds={selectedIds}
        onAttach={onAttach}
        onToggleFile={onToggleFile}
        onSelectAll={onSelectAll}
      />
    </FullPopup>
  )
}

components/FileAttach/FileAttachContent.jsx

import { EmptyAttachView } from "./EmptyAttachView"
import { AttachedFileView } from "./AttachedFileView"

export function FileAttachContent({
  files,
  selectedIds,
  onAttach,
  onToggleFile,
  onSelectAll,
}) {
  if (files.length === 0) {
    return <EmptyAttachView onAttach={onAttach} />
  }

  return (
    <AttachedFileView
      files={files}
      selectedIds={selectedIds}
      onToggleFile={onToggleFile}
      onSelectAll={onSelectAll}
    />
  )
}

components/FileAttach/EmptyAttachView.jsx

export function EmptyAttachView({ onAttach }) {
  return (
    <section className="empty-attach-view">
      <button type="button" className="upload-dropzone" onClick={onAttach}>
        <span className="upload-icon" aria-hidden="true">⇧</span>
        <strong>파일을 첨부해 주세요.</strong>
        <span>파일 용량은 5MB 이하로 제한합니다.</span>
      </button>

      <div className="attach-notice">
        <h2>주의사항</h2>
        <ul>
          <li>첨부파일은 이미지(jpg, png, jpeg) 및 pdf만 업로드 가능합니다.</li>
          <li>첨부파일 용량은 5MB 이하로 제한됩니다.</li>
          <li>상세내역 및 비용 정보가 보이는 이미지를 첨부해 주세요.</li>
        </ul>
      </div>
    </section>
  )
}

components/FileAttach/AttachedFileView.jsx

import { FileList } from "./FileList"

export function AttachedFileView({ files, selectedIds, onToggleFile, onSelectAll }) {
  return (
    <section className="attached-file-view">
      <div className="file-section-header">
        <h2>첨부파일</h2>
        <button type="button" onClick={onSelectAll}>전체선택</button>
      </div>

      <FileList
        files={files}
        selectedIds={selectedIds}
        onToggleFile={onToggleFile}
      />
    </section>
  )
}

components/FileAttach/FileList.jsx

import { FileItem } from "./FileItem"

export function FileList({ files, selectedIds, onToggleFile }) {
  return (
    <ul className="file-list">
      {files.map((file) => (
        <FileItem
          key={file.id}
          file={file}
          selected={selectedIds.includes(file.id)}
          onToggle={() => onToggleFile(file.id)}
        />
      ))}
    </ul>
  )
}

components/FileAttach/FileItem.jsx

import { cn } from "../../utils/cn"

export function FileItem({ file, selected, onToggle }) {
  const isPdf = file.type === "pdf"

  return (
    <li className={cn("file-item", { selected })}>
      <button
        type="button"
        className="file-check"
        onClick={onToggle}
        aria-label={selected ? "파일 선택 해제" : "파일 선택"}
      >
        {selected ? "✓" : ""}
      </button>

      <span className="file-name">{file.name}</span>

      {isPdf ? (
        <span className="file-pdf" aria-label="PDF 파일">PDF</span>
      ) : (
        <img className="file-thumb" src={file.thumbnailUrl} alt="" />
      )}
    </li>
  )
}

components/FileAttach/FileAttachFooter.jsx

export function FileAttachFooter({
  hasSelection,
  onAttach,
  onConfirm,
  onCancelSelection,
  onDeleteSelected,
}) {
  if (hasSelection) {
    return (
      <>
        <button type="button" className="btn secondary" onClick={onCancelSelection}>
          취소
        </button>
        <button type="button" className="btn danger" onClick={onDeleteSelected}>
          선택삭제
        </button>
      </>
    )
  }

  return (
    <>
      <button type="button" className="btn secondary" onClick={onAttach}>
        파일첨부
      </button>
      <button type="button" className="btn primary" onClick={onConfirm}>
        확인
      </button>
    </>
  )
}

사용 예시: 부모 화면

import { useState } from "react"
import { FileAttachPopup } from "./components/FileAttach/FileAttachPopup"

const initialFiles = [
  {
    id: "1",
    name: "진단명세서_000000.jpg",
    type: "image",
    thumbnailUrl: "/images/sample-thumb.jpg",
  },
  {
    id: "2",
    name: "입원확인서.png",
    type: "image",
    thumbnailUrl: "/images/sample-thumb.jpg",
  },
]

export function ExamplePage() {
  const [open, setOpen] = useState(false)
  const [files, setFiles] = useState(initialFiles)
  const [selectedIds, setSelectedIds] = useState([])

  function handleToggleFile(fileId) {
    setSelectedIds((prev) =>
      prev.includes(fileId)
        ? prev.filter((id) => id !== fileId)
        : [...prev, fileId]
    )
  }

  function handleSelectAll() {
    setSelectedIds(files.map((file) => file.id))
  }

  function handleDeleteSelected() {
    setFiles((prev) => prev.filter((file) => !selectedIds.includes(file.id)))
    setSelectedIds([])
  }

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        파일첨부 열기
      </button>

      <FileAttachPopup
        open={open}
        files={files}
        selectedIds={selectedIds}
        onClose={() => setOpen(false)}
        onAttach={() => console.log("파일첨부")}
        onConfirm={() => setOpen(false)}
        onToggleFile={handleToggleFile}
        onSelectAll={handleSelectAll}
        onCancelSelection={() => setSelectedIds([])}
        onDeleteSelected={handleDeleteSelected}
      />
    </>
  )
}

6. React에서 자주 사용하는 JavaScript 메서드 TOP 10

React에서 자주 보이는 map, filter, find 같은 기능은 React 전용 API가 아니라 JavaScript의 배열/문자열 메서드입니다. React에서는 state와 props 데이터를 가공해서 렌더링하거나 새로운 state를 만들 때 특히 자주 사용합니다.

순위메서드/API주로 쓰는 용도
1map()배열을 JSX 목록으로 반복 렌더링
2filter()삭제, 조건에 맞는 데이터만 남기기
3find()조건에 맞는 데이터 한 개 찾기
4includes()값 포함 여부 확인, 선택 상태 판단
5some()조건을 만족하는 값이 하나라도 있는지 확인
6every()모든 값이 조건을 만족하는지 확인
7reduce()합계, 누적값, 객체/배열 재구성

1. map() — 배열을 반복해서 새로운 배열 만들기

React에서 가장 자주 보이는 메서드입니다. 파일 목록, 메뉴, 탭, 카드처럼 반복되는 UI를 만들 때 사용합니다.

const files = [
  { id: "1", name: "A.jpg" },
  { id: "2", name: "B.jpg" },
]

const names = files.map((file) => file.name)

console.log(names)
// ["A.jpg", "B.jpg"]

React에서는 다음처럼 JSX를 반복합니다.

{files.map((file) => (
  <FileItem
    key={file.id}
    file={file}
  />
))}

2. filter() — 조건에 맞는 값만 남기기

filter()는 원본 배열을 변경하지 않고, 조건이 true인 값만 모아서 새 배열을 만듭니다.

const numbers = [1, 2, 3, 4, 5]

const result = numbers.filter((number) => number !== 1)

console.log(result)
// [2, 3, 4, 5]

React에서는 선택한 파일을 삭제할 때 이런 식으로 연결됩니다.

setFiles((prev) =>
  prev.filter((file) =>
    !selectedIds.includes(file.id)
  )
)

3. find() — 조건에 맞는 값 한 개 찾기

filter()가 여러 개를 찾는다면 find()는 가장 먼저 찾은 한 개를 반환합니다.

const users = [
  { id: 1, name: "Kim" },
  { id: 2, name: "Lee" },
]

const user = users.find((item) => item.id === 2)

console.log(user)
// { id: 2, name: "Lee" }

선택된 값의 label을 찾을 때도 자주 씁니다.

const selectedItem = items.find(
  (item) => item.value === value
)

4. includes() — 값이 포함되어 있는지 확인

includes()는 결과로 true 또는 false를 반환합니다.

const selectedIds = ["1", "3"]

selectedIds.includes("1")
// true

selectedIds.includes("2")
// false

파일이 선택되었는지 판단할 때 자연스럽게 사용할 수 있습니다.

const selected = selectedIds.includes(file.id)
주의: 1과 "1"은 다른 값입니다. [1, 2].includes("1")은 false입니다.

5. some() — 하나라도 조건을 만족하는가?

배열 안에 조건을 만족하는 값이 하나라도 있으면 true입니다.

const files = [
  { name: "A.jpg", type: "image" },
  { name: "B.pdf", type: "pdf" },
]

const hasPdf = files.some((file) => file.type === "pdf")

console.log(hasPdf)
// true

React에서는 "선택된 항목이 하나라도 있는가?" 같은 조건을 만들 때 유용합니다.

6. every() — 모든 값이 조건을 만족하는가?

some()의 반대 느낌입니다. 모든 요소가 조건을 만족해야 true가 됩니다.

const files = [
  { size: 3 },
  { size: 4 },
]

const allUnder5MB = files.every((file) => file.size <= 5)

console.log(allUnder5MB)
// true

전체선택 여부, 전체 유효성 검사 등을 판단할 때 자주 사용합니다.

7. reduce() — 여러 값을 하나로 누적하기

합계, 총 개수, 그룹화처럼 배열 전체를 순회해서 하나의 결과로 만들 때 사용합니다.

const prices = [1000, 2000, 3000]

const total = prices.reduce(
  (sum, price) => sum + price,
  0
)

console.log(total)
// 6000

장바구니 총액, 선택 파일 총 용량 계산 등에 사용할 수 있습니다.

가장 먼저 익힐 순서: map → filter → find → includes → some/every → reduce

React 코드에서 실제로 어떻게 연결되는가?

// 반복 렌더링
files.map((file) => <FileItem key={file.id} file={file} />)

// 특정 파일 삭제
files.filter((file) => file.id !== deleteId)

// 선택된 데이터 하나 찾기
items.find((item) => item.value === value)

// 선택 여부 확인
selectedIds.includes(file.id)

// 하나라도 PDF가 있는지
files.some((file) => file.type === "pdf")

// 모든 파일이 용량 조건을 만족하는지
files.every((file) => file.size <= 5)

// 전체 파일 용량
files.reduce((total, file) => total + file.size, 0)
핵심: React 자체가 새로운 배열 문법을 만드는 것이 아닙니다. JavaScript 메서드로 데이터를 가공한 뒤, 그 결과를 JSX 렌더링이나 setState에 연결하는 방식입니다.

7. React 상태 변경 패턴 — 실전 사전

React의 useState 자체보다 중요한 것은 요구사항을 보고 어떤 state를 어떤 방식으로 바꿀지 판단하는 능력입니다. 아래 패턴들은 실무 UI에서 반복해서 등장하는 대표적인 상태 변경 방식입니다.

기본 사고 순서: 어떤 state? → 추가 / 삭제 / 수정 / 선택 / 초기화? → 이전 state가 필요한가? → 배열이면 map/filter/spread, 객체면 {...prev}

핵심 패턴 한눈에 보기

하고 싶은 일대표 패턴주요 도구
값 교체setState(value)직접 값
이전 값 기준 변경setState(prev => ...)함수형 업데이트
boolean 반전prev => !prev!
배열 추가[...prev, newItem]spread
배열 삭제filter()조건이 true인 것만 유지
배열 특정 항목 수정map()id 비교 + 객체 spread
하나 찾기find()조회
포함 여부includes()선택 여부
하나라도 만족some()조건 확인
전부 만족every()전체선택/검증

CRUD로 압축해서 기억하기

Create 추가 → [...prev, newItem] Read 조회 → find() / filter() / includes() / some() / every() Update 수정 → map() + { ...item, 변경속성 } Delete 삭제 → filter()

1. 단순 값 변경

이전 값이 필요하지 않으면 setter에 새 값을 바로 넣습니다.

const [name, setName] = useState("")

setName("홍길동")
setName("")

2. 숫자 증가 / 감소

현재 값을 기준으로 다음 값을 만들 때는 prev 형태를 사용합니다.

const [count, setCount] = useState(0)

setCount(prev => prev + 1)
setCount(prev => prev - 1)
setCount(prev => prev + 10)

3. boolean 열기 / 닫기 / Toggle

Modal, Dropdown, Accordion 같은 UI에서 매우 자주 사용합니다.

const [open, setOpen] = useState(false)

setOpen(true)          // 열기
setOpen(false)         // 닫기
setOpen(prev => !prev) // Toggle

4. 배열에 하나 추가

setFiles(prev => [
  ...prev,
  newFile
])

5. 배열 맨 앞에 추가

setFiles(prev => [
  newFile,
  ...prev
])

6. 배열에 여러 개 추가

setFiles(prev => [
  ...prev,
  ...newFiles
])

7. 특정 데이터 하나 삭제

삭제 = filter()로 기억하면 됩니다.

setFiles(prev =>
  prev.filter(file =>
    file.id !== targetId
  )
)

8. 여러 데이터 삭제

const deleteIds = ["1", "3"]

setFiles(prev =>
  prev.filter(file =>
    !deleteIds.includes(file.id)
  )
)

9. 조건에 맞는 데이터 삭제

setItems(prev =>
  prev.filter(item =>
    !item.completed
  )
)

10. 배열 전체 초기화

setFiles([])

11. 특정 객체 수정

배열 안 특정 항목 수정 = map() 패턴입니다.

setUsers(prev =>
  prev.map(user =>
    user.id === targetId
      ? {
          ...user,
          name: "홍길동"
        }
      : user
  )
)

12. 객체의 특정 속성 Toggle

setUsers(prev =>
  prev.map(user =>
    user.id === targetId
      ? {
          ...user,
          active: !user.active
        }
      : user
  )
)

13. 특정 항목 체크 / 체크 해제

setItems(prev =>
  prev.map(item =>
    item.id === targetId
      ? { ...item, checked: true }
      : item
  )
)

setItems(prev =>
  prev.map(item =>
    item.id === targetId
      ? { ...item, checked: false }
      : item
  )
)

14. 모든 항목 체크 / 체크 해제

setItems(prev =>
  prev.map(item => ({
    ...item,
    checked: true
  }))
)

setItems(prev =>
  prev.map(item => ({
    ...item,
    checked: false
  }))
)

15. 선택 ID 하나 추가

setSelectedIds(prev => [
  ...prev,
  id
])

16. 선택 ID 하나 제거

setSelectedIds(prev =>
  prev.filter(itemId =>
    itemId !== id
  )
)

17. 선택 ID Toggle

Checkbox, 파일선택, 즐겨찾기 등에서 가장 많이 재사용되는 패턴 중 하나입니다.

setSelectedIds(prev =>
  prev.includes(id)
    ? prev.filter(itemId =>
        itemId !== id
      )
    : [...prev, id]
)

18. 전체 선택 / 전체 선택 해제

setSelectedIds(
  files.map(file => file.id)
)

setSelectedIds([])

19. 현재 선택 여부 확인

const selected =
  selectedIds.includes(file.id)

20. 전체 선택됐는지 확인

const allSelected =
  files.length > 0 &&
  files.every(file =>
    selectedIds.includes(file.id)
  )

21. 하나라도 선택됐는지 확인

const hasSelected =
  selectedIds.length > 0

// 객체에 selected 속성이 있다면
const hasSelectedItem =
  files.some(file => file.selected)

22. 객체 state의 속성 하나 수정

const [form, setForm] = useState({
  name: "",
  age: "",
  email: ""
})

setForm(prev => ({
  ...prev,
  name: "홍길동"
}))

23. input 값으로 객체 state 수정

function handleNameChange(event) {
  setForm(prev => ({
    ...prev,
    name: event.target.value
  }))
}

24. 여러 input을 하나의 handleChange로 처리

[name]을 이용하는 실무 Form의 대표적인 패턴입니다.

function handleChange(event) {
  const { name, value } = event.target

  setForm(prev => ({
    ...prev,
    [name]: value
  }))
}

25. 객체 여러 속성 한번에 변경

setForm(prev => ({
  ...prev,
  name: "홍길동",
  age: 30
}))

26. Form 초기화

const initialForm = {
  name: "",
  age: "",
  email: ""
}

const [form, setForm] = useState(initialForm)

setForm(initialForm)

27. 중첩 객체 내부 값 수정

중첩된 단계마다 기존 객체를 spread 해야 합니다.

setUser(prev => ({
  ...prev,
  address: {
    ...prev.address,
    city: "부산"
  }
}))

28. 배열 속 특정 객체의 중첩 객체 수정

setUsers(prev =>
  prev.map(user =>
    user.id === targetId
      ? {
          ...user,
          address: {
            ...user.address,
            city: "부산"
          }
        }
      : user
  )
)

29. 숫자 오름차순 / 내림차순 정렬

sort()는 원본을 변경하므로 먼저 배열을 복사하는 습관이 중요합니다.

setItems(prev =>
  [...prev].sort(
    (a, b) => a.price - b.price
  )
)

setItems(prev =>
  [...prev].sort(
    (a, b) => b.price - a.price
  )
)

30. 문자열 정렬

setUsers(prev =>
  [...prev].sort((a, b) =>
    a.name.localeCompare(b.name)
  )
)

31. 배열 순서 뒤집기

setItems(prev =>
  [...prev].reverse()
)

32. 특정 index 데이터 교체

setItems(prev =>
  prev.map((item, index) =>
    index === targetIndex
      ? newItem
      : item
  )
)

33. 특정 조건일 때만 state 변경

setCount(prev => {
  if (prev >= 10) {
    return prev
  }

  return prev + 1
})

34. 최대값 / 최소값 제한

setCount(prev =>
  Math.min(prev + 1, 10)
)

setCount(prev =>
  Math.max(prev - 1, 0)
)

35. 파일 추가

function handleAddFiles(newFiles) {
  setFiles(prev => [
    ...prev,
    ...newFiles
  ])
}

36. 파일 하나 삭제

function handleDeleteFile(fileId) {
  setFiles(prev =>
    prev.filter(file =>
      file.id !== fileId
    )
  )
}

37. 선택된 파일 여러 개 삭제

현재 파일첨부 예제와 직접 연결되는 패턴입니다.

function handleDeleteSelected() {
  setFiles(prev =>
    prev.filter(file =>
      !selectedIds.includes(file.id)
    )
  )

  setSelectedIds([])
}

38. 파일 삭제와 선택 상태를 동시에 정리

하나의 이벤트에서 서로 다른 state setter를 여러 개 실행해도 됩니다.

function handleDeleteFile(fileId) {
  setFiles(prev =>
    prev.filter(file =>
      file.id !== fileId
    )
  )

  setSelectedIds(prev =>
    prev.filter(id =>
      id !== fileId
    )
  )
}

39. Tab / Radio 하나만 선택

const [selectedTab, setSelectedTab] =
  useState("home")

setSelectedTab("product")

const [gender, setGender] =
  useState("male")

setGender("female")

40. Accordion 하나만 열기

const [openId, setOpenId] =
  useState(null)

setOpenId(id)

41. 같은 Accordion 다시 클릭하면 닫기

setOpenId(prev =>
  prev === id
    ? null
    : id
)

42. 여러 Accordion 열고 닫기

const [openIds, setOpenIds] =
  useState([])

setOpenIds(prev =>
  prev.includes(id)
    ? prev.filter(openId =>
        openId !== id
      )
    : [...prev, id]
)

43. 좋아요 / 즐겨찾기 Toggle

setLiked(prev => !prev)

setFavoriteIds(prev =>
  prev.includes(productId)
    ? prev.filter(id =>
        id !== productId
      )
    : [...prev, productId]
)

44. 장바구니 수량 증가 / 감소

setCart(prev =>
  prev.map(item =>
    item.id === productId
      ? {
          ...item,
          quantity: item.quantity + 1
        }
      : item
  )
)

setCart(prev =>
  prev.map(item =>
    item.id === productId
      ? {
          ...item,
          quantity:
            Math.max(1, item.quantity - 1)
        }
      : item
  )
)

45. 장바구니 수량 직접 변경

function handleQuantity(id, quantity) {
  setCart(prev =>
    prev.map(item =>
      item.id === id
        ? {
            ...item,
            quantity
          }
        : item
    )
  )
}

46. 로딩 상태

setLoading(true)

try {
  // API 호출
} finally {
  setLoading(false)
}

47. 에러 저장 / 초기화

setError(error.message)
setError(null)

48. Step / Wizard 다음·이전 단계

setStep(prev => prev + 1)
setStep(prev => prev - 1)

setStep(prev =>
  Math.min(prev + 1, maxStep)
)

49. Modal 열기 / 닫기

setOpen(true)
setOpen(false)

50. 특정 데이터를 선택하고 Modal 열기

function handleEdit(user) {
  setSelectedUser(user)
  setOpen(true)
}

51. 특정 필드 에러 추가 / 제거

setErrors(prev => ({
  ...prev,
  name: "이름을 입력해주세요."
}))

setErrors(prev => ({
  ...prev,
  name: ""
}))

52. 배열 중복 추가 방지

setSelectedIds(prev =>
  prev.includes(id)
    ? prev
    : [...prev, id]
)

53. 객체 배열에서 id 중복 방지

setFiles(prev =>
  prev.some(file =>
    file.id === newFile.id
  )
    ? prev
    : [...prev, newFile]
)

54. 서버 데이터로 배열 전체 교체

기존 state가 필요하지 않으므로 prev를 쓸 필요가 없습니다.

setFiles(response.data)

55. 검색 결과는 꼭 state로 만들 필요가 없다

원본 state는 유지하고 렌더링할 값만 계산하는 패턴입니다.

const [files, setFiles] = useState([])
const [keyword, setKeyword] = useState("")

const filteredFiles =
  files.filter(file =>
    file.name.includes(keyword)
  )

56. 선택된 데이터 하나 찾기

const selectedFile =
  files.find(file =>
    file.id === selectedId
  )

57. 조건을 만족하는 값만 별도 계산

const imageFiles =
  files.filter(file =>
    file.type === "image"
  )

58. 하나라도 조건 만족 여부

const hasPdf =
  files.some(file =>
    file.type === "pdf"
  )

59. 모든 항목 유효 여부

const allValid =
  files.every(file =>
    file.size <= 5
  )

60. 합계 계산

const totalPrice =
  cart.reduce(
    (sum, item) =>
      sum + item.price * item.quantity,
    0
  )

61. 여러 state를 하나의 이벤트에서 변경

function handleReset() {
  setFiles([])
  setSelectedIds([])
  setOpen(false)
  setError(null)
}

62. 이전 state가 필요한 경우와 필요 없는 경우

상황형태예
현재 값을 이용해 다음 값 계산 setState(prev => ...) 증가, Toggle, 배열 추가/삭제/수정
새 값이 이미 정해져 있음 setState(value) setOpen(false), setSelectedIds([])

63. 요구사항을 함수로 바꾸는 사고 순서

① 어떤 state를 변경해야 하지? ② 작업 종류는? ├─ 추가 ├─ 삭제 ├─ 수정 ├─ 선택 └─ 초기화 ③ 현재 state가 필요한가? ├─ YES → setState(prev => ...) └─ NO → setState(새 값) ④ 배열인가? ├─ 추가 → spread [...] ├─ 삭제 → filter() ├─ 수정 → map() ├─ 찾기 → find() └─ 존재 → includes() / some() / every() ⑤ 객체인가? └─ 기존 속성 유지 → { ...prev } ⑥ 여러 state가 동시에 바뀌나? └─ setter를 여러 개 실행

64. 예제: “선택한 파일을 삭제해 주세요”를 코드로 바꾸기

어떤 state? → files, selectedIds 무슨 작업? → 삭제 + 선택 초기화 files는 배열 → 삭제 = filter() 삭제 기준? → selectedIds에 포함된 file.id 포함 여부? → includes() 삭제 후 selectedIds? → []
function handleDeleteSelected() {
  setFiles(prev =>
    prev.filter(file =>
      !selectedIds.includes(file.id)
    )
  )

  setSelectedIds([])
}
최종 핵심: 코드를 통째로 외우는 것이 아니라 요구사항 → state → 작업 종류 → JavaScript 메서드 → setter 순서가 자동으로 연결되도록 연습하는 것이 목표입니다.