HTML을 React JSX로 옮길 때
무엇부터 해야 할까?

FAQ 화면을 예제로 정리한 React 입문자용 작업순서 가이드

1. 제일 먼저 기억할 전체 흐름

처음부터 useState, filter, 이벤트 함수를 한 번에 만들려고 하지 않는다.

HTML 구조 확인 ↓ 반복되는 데이터 분리 ↓ 화면에서 바뀌는 값 찾기 ↓ state 만들기 ↓ map으로 반복 렌더링 ↓ 이벤트 연결 ↓ filter 등 계산값 만들기 ↓ 조건부 렌더링 ↓ useRef / useMemo 같은 부가기능
핵심: categories와 faqs를 만든 다음 바로 filter부터 작성하는 것이 아니라, 먼저 “이 화면에서 사용자 조작으로 무엇이 바뀌는가?”를 찾아야 한다.

2. HTML을 받으면 React부터 생각하지 않는다

먼저 화면을 그냥 HTML 화면으로 분석한다.

처음에는 React 문법보다 화면 덩어리를 나누는 것이 먼저다.

3. 그다음 반복되는 것을 찾는다

카테고리

const categories = [
  '전체',
  '회원',
  '우리아파트',
  '공동현관문'
]

FAQ

const faqs = [
  {
    id: 1,
    subject: '회원',
    question: '회원 비밀번호를 잊어버렸어요',
    answer: '...'
  },
  {
    id: 2,
    subject: '회원',
    question: '회원 인증 번호가 오지 않습니다',
    answer: '...'
  }
]
같은 모양의 HTML이 반복되면 배열로 만들 수 있는지 먼저 생각한다.

4. 배열 다음에는 state 후보를 찾는다

질문은 하나다.

“사용자가 조작하면 이 화면에서 무엇이 바뀌는가?”

현재 화면에서 바뀌는 값

const [activeCategory, setActiveCategory] = useState('전체')
const [searchInput, setSearchInput] = useState('')
const [submittedSearch, setSubmittedSearch] = useState('')

searchInput과 submittedSearch를 왜 따로 둘까?

searchInput
→ 지금 input에 입력 중인 값

submittedSearch
→ 검색 버튼을 눌러 확정된 값

예를 들어 사용자가 아직 검색 버튼을 누르지 않았다면:

searchInput === '비밀번호'
submittedSearch === ''

검색 버튼을 누르면:

setSubmittedSearch(searchInput)

5. HTML은 우선 JSX로 그대로 옮겨도 된다

처음부터 map, filter까지 넣지 않아도 된다.

function Feature() {
  return (
    <main>
      <section className="search-area">
        <input type="text" />
        <button type="button">x</button>
        <button type="button">검색</button>
      </section>
    </main>
  )
}

이 단계의 목표는 단 하나다.

“기존 HTML 화면이 React에서도 일단 똑같이 보이게 만들기”

6. 이제 반복 HTML을 map으로 바꾼다

categories

{categories.map((category) => (
  <button key={category}>
    {category}
  </button>
))}

해석은 간단하다.

categories를 하나씩 꺼낸다 ↓ 꺼낸 값을 category라고 부른다 ↓ 그 category로 button을 만든다

faqs

{faqs.map((faq) => (
  <details key={faq.id}>
    <summary>{faq.question}</summary>
    <div>{faq.answer}</div>
  </details>
))}
처음에는 visibleFaqs까지 만들지 말고 그냥 faqs.map부터 완성해도 된다.

7. 화면이 보이면 그다음 이벤트를 연결한다

input 입력

const [searchInput, setSearchInput] = useState('')

<input
  value={searchInput}
  onChange={(event) => {
    setSearchInput(event.target.value)
  }}
/>
사용자가 입력 ↓ onChange 발생 ↓ event.target.value ↓ setSearchInput(...) ↓ state 변경 ↓ React 재렌더링

카테고리 클릭

<button
  onClick={() => setActiveCategory(category)}
>
  {category}
</button>

8. React에서는 DOM보다 state를 바꾼다

jQuery식 사고

버튼 클릭 ↓ DOM 찾기 ↓ active 제거 ↓ active 추가

React식 사고

버튼 클릭 ↓ state 변경 ↓ React가 다시 렌더링
className={
  activeCategory === category
    ? 'chip active'
    : 'chip'
}

또는:

className={`chip${activeCategory === category ? ' active' : ''}`}

9. 카테고리 선택까지 되면 filter를 붙인다

const visibleFaqs = faqs.filter((faq) => {
  return (
    activeCategory === '전체' ||
    faq.subject === activeCategory
  )
})

예를 들어 activeCategory가 회원이면 회원 FAQ만 남는다.

중요: visibleFaqs는 state가 아니라 계산값이다.
사용자가 직접 바꾸는 원본 값
→ useState

원본 값으로 계산 가능한 값
→ 일반 변수

예:

activeCategory      → state
searchInput         → state
submittedSearch     → state

visibleFaqs         → 계산값
visibleFaqs.length  → 계산값
keyword             → 계산값

10. 그다음 검색 조건을 합친다

const keyword = submittedSearch.trim()

const visibleFaqs = faqs.filter((faq) => {
  const matchesCategory =
    activeCategory === '전체' ||
    faq.subject === activeCategory

  const matchesSearch =
    keyword === '' ||
    faq.subject.includes(keyword) ||
    faq.question.includes(keyword) ||
    faq.answer.includes(keyword)

  return matchesCategory && matchesSearch
})

처음부터 이 완성형으로 가지 말고 단계적으로 키우는 게 좋다.

1단계: faqs 그대로 ↓ 2단계: 카테고리 filter ↓ 3단계: 검색 filter ↓ 4단계: 카테고리 + 검색 ↓ 5단계: subject/question/answer 전체 검색

11. 이벤트 함수는 길어질 때 밖으로 뺀다

const handleCategoryClick = (category) => {
  setSearchInput('')
  setSubmittedSearch('')
  setActiveCategory(category)
}

처음에는 JSX 안에 직접 써도 된다.

onClick={() => {
  setSearchInput('')
  setSubmittedSearch('')
  setActiveCategory(category)
}}

길어지면 handleCategoryClick 같은 함수로 분리한다.

12. 조건부 렌더링은 한국말로 먼저 정한다

검색 전

{!submittedSearch && (
  <>
    <h1>자주 묻는 질문</h1>
    <div>카테고리</div>
  </>
)}

검색 후

{submittedSearch && (
  <div>
    "{submittedSearch}" 검색 결과
  </div>
)}

FAQ가 있으면 / 없으면

{visibleFaqs.length > 0 ? (
  <div>FAQ 목록</div>
) : (
  <div>검색결과가 없습니다.</div>
)}
먼저 “있으면 무엇을 보여주고, 없으면 무엇을 보여줄지”를 한국말로 정한 뒤 코드로 옮긴다.

13. Enter, clear, useRef는 마지막에 붙여도 된다

Enter 검색

const handleSearchKeyDown = (event) => {
  if (event.key === 'Enter') {
    handleSearch()
  }
}

useRef

const searchInputRef = useRef(null)

<input ref={searchInputRef} />

searchInputRef.current?.focus()

핵심 검색/필터 기능 없이도 화면은 만들 수 있으므로, DOM 직접 접근이 필요한 기능은 뒤로 미뤄도 된다.

useMemo

현재 규모에서는 먼저 일반 변수 방식으로 이해하는 편이 더 좋다.

const visibleFaqs = faqs.filter(...)

성능 최적화가 필요할 때 useMemo를 추가한다.

14. 실무에서 따라가기 좋은 작업순서

순서 생각할 것 실제 코드
1기존 HTML 확인구조 분석
2JSX로 이동class → className
3반복 요소 찾기categories, faqs
4반복 데이터 배열화const 배열
5반복 렌더링map()
6변하는 값 찾기state 후보
7state 선언useState()
8state와 UI 연결value, className
9이벤트 연결onClick, onChange
10계산값 만들기filter()
11상황별 화면&&, ? :
12이벤트 코드 정리handleXXX
13편의기능Enter, clear
14DOM 직접 접근useRef
15성능 최적화useMemo
16코드가 커짐컴포넌트 분리

15. HTML → React 변환 체크리스트

①화면을 고정 UI / 반복 UI로 나눴는가?
②반복되는 데이터는 배열이나 객체로 분리했는가?
③사용자 조작으로 변하는 값을 찾았는가?
④진짜 state여야 하는 값만 useState로 만들었는가?
⑤계산 가능한 값은 일반 변수로 만들었는가?
⑥map으로 반복 렌더링했는가?
⑦onClick / onChange로 state만 변경하고 있는가?
⑧DOM class를 직접 붙였다 떼는 방식으로 처리하고 있지는 않은가?
⑨조건부 렌더링은 먼저 한국말로 조건을 정했는가?
⑩useRef, useMemo는 꼭 필요한 시점에 추가했는가?

16. 이 화면을 아주 단순하게 보면

const categories = [...]
const faqs = [...]

function Feature() {
  // 1. 변하는 값
  const [activeCategory, setActiveCategory] = useState('전체')
  const [searchInput, setSearchInput] = useState('')
  const [submittedSearch, setSubmittedSearch] = useState('')

  // 2. 화면에 보여줄 데이터 계산
  const visibleFaqs = faqs.filter(...)

  // 3. 사용자 행동
  const handleCategoryClick = () => {}
  const handleSearch = () => {}
  const handleClear = () => {}

  // 4. 화면
  return (
    <>
      검색창
      categories.map(...)
      visibleFaqs.map(...)
    </>
  )
}
① 데이터 ② state ③ 계산 / 이벤트 ④ JSX

React 파일이 복잡해 보여도 우선 이 네 덩어리로 나눠서 보면 훨씬 쉽다.

17. GPT가 만들어준 코드 공부법

완성 코드를 외우는 것보다 아래 순서로 직접 다시 복원하는 편이 훨씬 좋다.

1
JSX 화면만 다시 만들기

검색창 + categories.map + faqs.map까지만 직접 작성한다.

2
카테고리 상태 하나만 추가

activeCategory와 active class 처리만 완성한다.

3
검색 input 상태 추가

searchInput과 onChange까지만 연결한다.

4
카테고리 filter 추가

visibleFaqs를 만든다.

5
검색 / 조건부 렌더링 추가

submittedSearch, 검색 결과, empty 화면을 붙인다.

6
마지막에 편의기능

Enter, clear, focus, useRef 등을 추가한다.

18. 최종 사고 순서

HTML을 JSX로 옮긴다 ↓ 반복되는 게 있나? → 배열 + map ↓ 사용자가 건드려서 바뀌는 게 있나? → useState ↓ 사용자가 어떤 행동을 하나? → onClick / onChange ↓ state에 따라 계산되는 게 있나? → filter / find / map ↓ state에 따라 보였다 사라지는 게 있나? → && / 삼항연산자 ↓ DOM 직접 접근이 필요한가? → useRef ↓ 성능 문제가 실제로 있는가? → useMemo

19. Feature.jsx 원본 전체

첨부 원본
import { useState, useMemo, useRef } from 'react'

const categories = ['전체', '회원', '우리아파트', '공동현관문']

const faqs = [
    {
        id: 1,
        subject: '회원',
        question: '회원 비밀번호를 잊어버렸어요',
        answer:
            '로그인 화면에서 "비밀번호 찾기"를 클릭하면 가입한 이메일이나 휴대폰 번호로 임시 비밀번호를 발급받을 수 있습니다.',
    },
    {
        id: 2,
        subject: '회원',
        question: '회원 인증 번호가 오지 않습니다',
        answer: '스팸 문자함 또는 스팸 메일함을 확인해 주세요. 070 번호가 차단되어 있다면 해제한 뒤 다시 시도해 주세요.',
    },
    {
        id: 3,
        subject: '회원',
        question: '회원 탈퇴하고 싶습니다',
        answer: '[마이페이지] > [회원 정보 수정] 맨 아래에 있는 "회원 탈퇴" 버튼을 통해 진행할 수 있습니다.',
    },
    {
        id: 4,
        subject: '우리아파트',
        question: '우리 아파트 메뉴에서는 무엇을 확인할 수 있나요?',
        answer: '아파트 공지, 일정, 게시글, 관리사무소 정보 등 우리 단지 관련 정보를 확인할 수 있습니다.',
    },
    {
        id: 5,
        subject: '공동현관문',
        question: '공동현관문 출입은 어떻게 이용하나요?',
        answer: '단지에서 제공하는 인증 및 출입 설정에 따라 공동현관문 출입 기능을 사용할 수 있습니다.',
    },
    {
        id: 6,
        subject: '공동현관문',
        question: '공동현관문이 고장 나서 열리지 않아요',
        answer: '자동문 안쪽이나 측면에 있는 "정지(수동) 버튼"을 누르면 모터 작동이 멈춰 수동으로 문을 열 수 있습니다.',
    },
]

function Feature() {
    const [activeCategory, setActiveCategory] = useState('전체')
    const [searchInput, setSearchInput] = useState('')
    const [submittedSearch, setSubmittedSearch] = useState('')
    const searchInputRef = useRef(null)

    // 20260909: useMemo 사용 예시는 비교용으로 주석 처리했습니다.
    // const visibleFaqs = useMemo(() => {
    //     const keyword = submittedSearch.trim()

    //     return faqs.filter((faq) => {
    //         const matchesCategory = activeCategory === '전체' || faq.subject === activeCategory
    //         const matchesSearch =
    //             keyword === '' ||
    //             faq.subject.includes(keyword) ||
    //             faq.question.includes(keyword) ||
    //             faq.answer.includes(keyword)

    //         return matchesCategory && matchesSearch
    //     })
    // }, [activeCategory, submittedSearch])

    // 20260909: useMemo 없이 렌더링 때마다 일반 변수로 FAQ 목록을 계산합니다.
    const keyword = submittedSearch.trim()
    const visibleFaqs = faqs.filter((faq) => {
        const matchesCategory = activeCategory === '전체' || faq.subject === activeCategory
        const matchesSearch =
            keyword === '' ||
            faq.subject.includes(keyword) ||
            faq.question.includes(keyword) ||
            faq.answer.includes(keyword)

        return matchesCategory && matchesSearch
    })

    const handleCategoryClick = (category) => {
        setSearchInput('')
        setSubmittedSearch('')
        setActiveCategory(category)
    }

    const handleSearch = () => {
        setActiveCategory('전체')
        setSubmittedSearch(searchInput)
        searchInputRef.current?.focus()
    }

    const handleSearchKeyDown = (event) => {
        if (event.key === 'Enter') {
            handleSearch()
        }
    }

    const handleClear = () => {
        setSearchInput('')
        setSubmittedSearch('')
        searchInputRef.current?.focus()
    }

    return (
        <main>
            <section className="search-area">
                <div className="search-box">
                    <input
                        id="searchInput"
                        type="text"
                        placeholder="질문을 입력해주세요"
                        ref={searchInputRef}
                        autoComplete="off"
                        onChange={(event) => setSearchInput(event.target.value)}
                        onKeyDown={handleSearchKeyDown}
                        value={searchInput}
                    />
                    <button
                        id="clearBtn"
                        className={`clear-btn${searchInput ? ' show' : ''}`}
                        type="button"
                        aria-label="검색어 지우기"
                        onClick={handleClear}
                    >
                        x
                    </button>
                    <button id="searchBtn" className="search-btn" type="button" aria-label="검색" onClick={handleSearch}>
                        <span></span>
                    </button>
                </div>
            </section>

            {/* 질문을 입력박스로 검색할때는 숨겨야하는데... */}
            <section id="faqHome" className="faq-home">
                {!submittedSearch && (
                    <>
                        <h1>자주 묻는 질문</h1>

                        <div className="category-list" role="tablist" aria-label="FAQ 카테고리">
                            {categories.map((category) => (
                                <button
                                    className={`chip${activeCategory === category ? ' active' : ''}`}
                                    key={category}
                                    onClick={() => handleCategoryClick(category)}
                                    type="button"
                                >
                                    {category}
                                </button>
                            ))}
                        </div>
                    </>
                )}

                {submittedSearch && (
                    <div id="resultSummary" className="result-summary">
                        <span>i</span> "<b>{submittedSearch}</b>"에 대한 결과가 {visibleFaqs.length}개 있습니다.
                    </div>
                )}

                {visibleFaqs.length > 0 ? (
                    <div id="faqList" className="faq-list">
                        {visibleFaqs.map((faq, index) => (
                            <details className="faq-item" key={faq.id} name="faq" open={index === 0}>
                                <summary className="faq-question">
                                    <span>{faq.question}</span>
                                    <i></i>
                                </summary>
                                <div className="faq-answer">
                                    <div>{faq.answer}</div>
                                </div>
                            </details>
                        ))}
                    </div>
                ) : (
                    <div id="emptyState" className="empty-state">
                        <div className="face">:)</div>
                        <strong>검색결과가 없습니다.</strong>
                        <p>
                            원하는 결과가 없다면
                            <br />
                            1:1 문의를 이용해주세요.
                        </p>
                        <button type="button">1:1 문의하기</button>
                    </div>
                )}
            </section>
        </main>
    )
}

export default Feature