1. 먼저 큰 덩어리부터 나누기
입문자는 처음부터 버튼, 아이콘, 텍스트를 잘게 나누기보다 페이지를 큰 사각형으로 먼저 보는 것이 좋습니다.
이 화면은 공통적으로 Header, Content, Bottom Actions 세 영역으로 나눌 수 있습니다.
상태나 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.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>
)
}
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 | 주로 쓰는 용도 |
|---|---|---|
| 1 | map() | 배열을 JSX 목록으로 반복 렌더링 |
| 2 | filter() | 삭제, 조건에 맞는 데이터만 남기기 |
| 3 | find() | 조건에 맞는 데이터 한 개 찾기 |
| 4 | includes() | 값 포함 여부 확인, 선택 상태 판단 |
| 5 | some() | 조건을 만족하는 값이 하나라도 있는지 확인 |
| 6 | every() | 모든 값이 조건을 만족하는지 확인 |
| 7 | reduce() | 합계, 누적값, 객체/배열 재구성 |
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)
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로 압축해서 기억하기
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. 요구사항을 함수로 바꾸는 사고 순서
64. 예제: “선택한 파일을 삭제해 주세요”를 코드로 바꾸기
function handleDeleteSelected() {
setFiles(prev =>
prev.filter(file =>
!selectedIds.includes(file.id)
)
)
setSelectedIds([])
}
요구사항 → state → 작업 종류 → JavaScript 메서드 → setter
순서가 자동으로 연결되도록 연습하는 것이 목표입니다.