
먼저 문제 원본은 아래와 같다.
문제에서 실제로 구현해야 하는 조건만 다시 정리하면 이렇다.
LabComp3.jsx는useState()를 사용해서 1씩 증가, 2씩 증가, 10씩 증가하는 카운터 3개를 만든다.- 카운터 3개는 각각 독립적으로 값이 증가해야 하므로
state를 3개 만든다.- 각 카운터는
div로 감싸고, 전체는fragment로 감싼다.LabComp4.jsx는input에 입력한 이름과 글자 수를 화면에 출력한다.LabComp4.jsx는 입력값이 바뀔 때마다state에 저장하고, 화면도 같이 바뀌게 만든다.LabComp5.jsx는 좋아요 여부에 따라 버튼 문구, 좋아요 수, 버튼 색깔을 바꾼다.LabComp5.jsx는liked,likeCount,likeStyle세 개의state를 사용한다.LabComp6.jsx는 할 일을 입력하고 추가 버튼을 누르면 목록에 출력되는Todo List를 만든다.LabComp6.jsx는 새로 입력한 할 일과 기존 할 일 목록을 배열state로 관리한다.main.jsx에서는LabComp3,LabComp4,LabComp5,LabComp6컴포넌트를 모두 불러와 화면에 렌더링한다.
이번 문제들은 모두
React에서state를 어떻게 사용해서 화면을 바꾸는지 확인하는 실습이다.
단순히 태그를 화면에 출력하는 문제가 아니라, 사용자의 클릭이나 입력에 따라 값이 바뀌고 그 값이 다시 화면에 반영되는 흐름을 만드는 문제다.
LabComp3.jsx는 카운터 실습이다.
버튼을 클릭하면 숫자가 증가해야 한다.
여기서 중요한 점은 카운터가 3개라는 것이다.
1씩 증가하는 값, 2씩 증가하는 값, 10씩 증가하는 값은 서로 영향을 주면 안 된다.
그래서 각각 별도의state로 관리해야 한다.
LabComp4.jsx는 입력값 실습이다.
사용자가input에 글자를 입력하면 입력한 이름이 인사 문장에 바로 반영되고, 글자 수도 같이 출력되어야 한다.
즉input의 값이 바뀔 때마다state를 변경하고, 그state값을 화면에 다시 보여주는 흐름이다.
LabComp5.jsx는 조건에 따라 화면이 달라지는 좋아요 버튼 실습이다.
처음에는 좋아요 수가 0이고 버튼 문구는 좋아요이며 색깔은 빨간색이다.
버튼을 누르면 좋아요 상태가 참으로 바뀌고, 좋아요 수는 1 증가하고, 버튼 문구는 좋아요 취소로 바뀌며 색깔도 파란색으로 바뀐다.
다시 누르면 원래 상태로 돌아간다.
LabComp6.jsx는 배열state를 사용하는 목록 실습이다.
할 일을 입력하고 추가 버튼을 누르면 기존 목록은 유지하면서 새 할 일이 목록 아래에 추가되어야 한다.
여기서 중요한 점은 기존 배열을 직접 수정하는 것이 아니라, 기존 배열을 펼친 뒤 새 값을 붙인 새 배열을 만들어setItems()에 전달한다는 것이다.
즉 이 실습의 핵심 흐름은 아래와 같다.
사용자 이벤트 발생 → 이벤트 함수 실행 → setState 함수 호출 → state 값 변경 → 컴포넌트 다시 렌더링 → 화면 변경
이번 실습은
state를 먼저 만들고, 그state를 바꾸는 함수를 연결한 뒤, 바뀐 값을 화면에 출력하는 순서로 진행했다.
1.main.jsx에서 사용할 컴포넌트들을 불러온다.
2.main.jsx에서LabComp3,LabComp4,LabComp5,LabComp6를 렌더링한다.
3.LabComp3.jsx에서useState()를 불러온다.
4. 1씩 증가, 2씩 증가, 10씩 증가 카운터용state를 각각 만든다.
5. 각 버튼에onClick을 연결해서 각state값을 증가시킨다.
6.LabComp4.jsx에서 이름을 저장할namestate를 만든다.
7.input의onChange에handleChange함수를 연결한다.
8.handleChange에서e.target.value를 꺼내name에 저장한다.
9.name값과name.length를 화면에 출력한다.
10.LabComp5.jsx에서liked,likeCount,likeStylestate를 만든다.
11.handleLike함수에서 현재 좋아요 상태에 따라 값을 다르게 변경한다.
12. 버튼 문구는 삼항 연산자로좋아요또는좋아요 취소가 출력되게 한다.
13.LabComp6.jsx에서 할 일 목록을 저장할items배열state를 만든다.
14. 추가 버튼을 클릭하면input값을 가져온다.
15. 입력값 앞뒤 공백을 제거한 뒤 값이 있을 때만 배열에 추가한다.
16.[...items, newItem]으로 새 배열을 만들어setItems()에 전달한다.
17.items.map()으로 배열 안의 할 일을 하나씩li로 출력한다.
이제 각 컴포넌트를 문제 순서대로 정리한다.
이번 실습에서 계속 반복되는 핵심은state값을 직접 바꾸는 것이 아니라, 반드시setState함수를 통해 값을 바꾼다는 점이다.
setState함수가 실행되어야React가 값이 바뀐 것을 알고 화면을 다시 그린다.1. main.jsx에서 컴포넌트 렌더링하기
main.jsx는 화면에 어떤 컴포넌트를 보여줄지 정하는 시작 파일이다.
이번 실습에서는LabComp3,LabComp4,LabComp5,LabComp6컴포넌트를 모두 메인 화면에 추가해야 한다.
그래서 각 컴포넌트를import로 불러온 뒤,StrictMode안에 순서대로 작성했다.// main.jsx import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { LabComp3 } from './LabComp3'; import { LabComp4 } from './LabComp4'; import { LabComp5 } from './LabComp5'; import { LabComp6 } from './LabComp6'; import './index.css'; createRoot(document.getElementById('root')).render( <StrictMode> <LabComp3 /> <LabComp4 /> <LabComp5 /> <LabComp6 /> </StrictMode>, );
main.jsx에 컴포넌트를 추가해야 브라우저 화면에서 해당 컴포넌트들이 보인다.
즉 컴포넌트 파일을 만들기만 하면 되는 것이 아니라, 최종적으로main.jsx에서 렌더링해야 한다.
렌더링 흐름
먼저
createRoot()는index.html에 있는root영역을 찾는다.createRoot(document.getElementById('root'))이 코드는
React화면이 들어갈 기준 위치를 잡는 코드다.
그 다음render()안에 보여줄 컴포넌트를 넣는다..render( <StrictMode> <LabComp3 /> <LabComp4 /> <LabComp5 /> <LabComp6 /> </StrictMode>, );그러면
React는root영역 안에 네 개의 컴포넌트를 순서대로 렌더링한다.
정리하면 흐름은 아래와 같다.
index.html의 root 찾기 → main.jsx 실행 → LabComp3 렌더링 → LabComp4 렌더링 → LabComp5 렌더링 → LabComp6 렌더링
2. LabComp3.jsx: 3개의 카운터 만들기
LabComp3.jsx는 버튼을 누를 때 숫자가 증가하는 카운터를 만드는 컴포넌트다.
화면에는 1씩 증가, 2씩 증가, 10씩 증가 카운터가 각각 있어야 한다.
이 문제에서 중요한 점은 카운터가 서로 독립적으로 움직여야 한다는 것이다.
1씩 증가 버튼을 눌렀다고 해서 2씩 증가 카운터나 10씩 증가 카운터 값이 같이 바뀌면 안 된다.
그래서state를 3개로 나누어 만들었다.// LabComp3.jsx import { useState } from 'react'; export function LabComp3() { const [count, setCount] = useState(0); const [count2, setCount2] = useState(0); const [count10, setCount10] = useState(0); return ( <> <div> <h3>1씩 증가 카운터</h3> <p>현재 값: {count}</p> <button onClick={() => setCount(count + 1)}>1씩증가</button> </div> <div> <h3>2씩 증가 카운터</h3> <p>현재 값: {count2}</p> <button onClick={() => setCount2(count2 + 2)}>2씩증가</button> </div> <div> <h3>10씩 증가 카운터</h3> <p>현재 값: {count10}</p> <button onClick={() => setCount10(count10 + 10)}>10씩증가</button> </div> </> ); }
count는 1씩 증가 카운터의 현재 값이다.
count2는 2씩 증가 카운터의 현재 값이다.
count10은 10씩 증가 카운터의 현재 값이다.
각state는 서로 다른 저장 공간을 가진다.
그래서 한 버튼을 눌러도 연결된state만 바뀐다.
1씩 증가 카운터 흐름
1씩 증가 버튼에는 아래 코드가 연결되어 있다.
<button onClick={() => setCount(count + 1)}>1씩증가</button>버튼을 클릭하면 아래 함수가 실행된다.
() => setCount(count + 1)여기서 가장 중요한 코드는 아래 한 줄이다.
setCount(count + 1)현재
count값이 아래와 같다고 생각하면 된다.0먼저
count + 1이 계산된다.count + 1이 단계의 결과는 아래와 같다.
0 + 1 = 1그 다음 계산된 값
1이setCount()에 전달된다.setCount(1)그러면
React는count값이0에서1로 바뀌었다고 판단하고 화면을 다시 렌더링한다.// 출력결과 // 현재 값: 1즉 1씩 증가 버튼은
count만 변경한다.
그래서 다른 카운터 값은 그대로 유지된다.
2씩 증가 카운터 흐름
2씩 증가 버튼에는 아래 코드가 연결되어 있다.
<button onClick={() => setCount2(count2 + 2)}>2씩증가</button>버튼을 클릭하면 아래 함수가 실행된다.
() => setCount2(count2 + 2)여기서 가장 중요한 코드는 아래 한 줄이다.
setCount2(count2 + 2)현재
count2값이 아래와 같다고 생각하면 된다.0먼저
count2 + 2가 계산된다.count2 + 2이 단계의 결과는 아래와 같다.
0 + 2 = 2그 다음 계산된 값
2가setCount2()에 전달된다.setCount2(2)그러면
React는count2값이 바뀐 것을 알고 화면을 다시 렌더링한다.// 출력결과 // 현재 값: 2즉 2씩 증가 버튼은
count2만 변경한다.
count와count10은 다른state이기 때문에 영향을 받지 않는다.
10씩 증가 카운터 흐름
10씩 증가 버튼에는 아래 코드가 연결되어 있다.
<button onClick={() => setCount10(count10 + 10)}>10씩증가</button>버튼을 클릭하면 아래 함수가 실행된다.
() => setCount10(count10 + 10)여기서 가장 중요한 코드는 아래 한 줄이다.
setCount10(count10 + 10)현재
count10값이 아래와 같다고 생각하면 된다.0먼저
count10 + 10이 계산된다.count10 + 10이 단계의 결과는 아래와 같다.
0 + 10 = 10그 다음 계산된 값
10이setCount10()에 전달된다.setCount10(10)그러면
React는count10값이 바뀐 것을 알고 화면을 다시 렌더링한다.// 출력결과 // 현재 값: 10이 흐름을 보면 세 카운터가 왜 각각 다른
state를 가져야 하는지 알 수 있다.
각 카운터는 자기 버튼을 눌렀을 때 자기 값만 바뀌어야 하기 때문이다.
3. LabComp4.jsx: input 입력값 화면에 출력하기
LabComp4.jsx는 사용자가input에 입력한 값을 화면에 바로 보여주는 컴포넌트다.
입력한 이름은 인사 문장에 들어가고, 입력한 글자 수는 숫자로 출력된다.
이 컴포넌트에서는 이름을 저장하기 위해name이라는state를 만들었다.
처음에는 아무 글자도 입력하지 않았기 때문에 초기값은 빈 문자열이다.// LabComp4.jsx import { useState } from 'react'; export function LabComp4() { const [name, setName] = useState(''); function handleChange(e) { setName(e.target.value); } return ( <> <hr /> <div> <h3>이름 입력</h3> <input type="text" placeholder="이름을 입력하세요" onChange={handleChange} /> <p>안녕하세요, {name}님</p> <p>글자 수: {name.length}</p> </div> </> ); }
input에는onChange={handleChange}가 연결되어 있다.
onChange는 사용자가 글자를 입력하거나 지울 때 실행된다.
그래서 입력값이 바뀔 때마다handleChange함수가 실행되고, 그 안에서 현재 입력값을namestate에 저장한다.
input 입력 흐름
input태그에는 아래 코드가 연결되어 있다.<input type="text" placeholder="이름을 입력하세요" onChange={handleChange} />사용자가 글자를 입력하면 아래 함수가 실행된다.
function handleChange(e) { setName(e.target.value); }여기서 가장 중요한 코드는 아래 한 줄이다.
setName(e.target.value);사용자가
유니코라고 입력했다고 생각하면 된다.유니코이때
e.target은 이벤트가 발생한input태그를 의미한다.e.target그리고
e.target.value는 그input안에 현재 입력된 값을 의미한다.e.target.value이 단계의 결과는 아래와 같다.
'유니코'그 다음 이 값이
setName()에 전달된다.setName('유니코');그러면
React는name값이 빈 문자열에서유니코로 바뀌었다고 판단하고 화면을 다시 렌더링한다.
화면에서는 아래 두 부분이 같이 바뀐다.<p>안녕하세요, {name}님</p><p>글자 수: {name.length}</p>
name값이유니코이면 출력 결과는 아래와 같다.// 출력결과 // 안녕하세요, 유니코님 // 글자 수: 3즉
input에 입력된 값은e.target.value로 꺼내고, 그 값을setName()으로state에 저장한다.
그 결과 화면의 인사 문장과 글자 수가 같이 바뀐다.
4. LabComp5.jsx: 좋아요 버튼 만들기
LabComp5.jsx는 좋아요 버튼을 클릭할 때 좋아요 여부, 좋아요 수, 버튼 색깔, 버튼 문구가 함께 바뀌는 컴포넌트다.
이 컴포넌트에서는state를 3개 사용했다.
liked는 현재 좋아요를 누른 상태인지 저장한다.
likeCount는 좋아요 수를 저장한다.
likeStyle은 버튼 색깔을 저장한다.// LabComp5.jsx import { useState } from 'react'; export function LabComp5() { const [liked, setLiked] = useState(false); const [likeCount, setLikeCount] = useState(0); const [likeStyle, setLikeStyle] = useState({ color: 'red' }); function handleLike() { if (liked) { setLiked(false); setLikeCount(likeCount - 1); setLikeStyle({ color: 'red' }); } else { setLiked(true); setLikeCount(likeCount + 1); setLikeStyle({ color: 'blue' }); } } return ( <> <hr /> <div> <h3>좋아요 버튼을 만들어 보자</h3> <p>좋아요 수: {likeCount}</p> <button onClick={handleLike} style={likeStyle}> {liked ? '좋아요 취소' : '좋아요'} </button> </div> </> ); }처음 화면에서는
liked가false다.
그래서 버튼 문구는좋아요로 출력된다.
좋아요 수는0이고 버튼 색깔은 빨간색이다.
버튼을 클릭하면handleLike함수가 실행된다.
이 함수는 현재liked값이 참인지 거짓인지에 따라 서로 다른 코드를 실행한다.
좋아요 클릭 흐름
좋아요 버튼에는 아래 코드가 연결되어 있다.
<button onClick={handleLike} style={likeStyle}> {liked ? '좋아요 취소' : '좋아요'} </button>버튼을 클릭하면 아래 함수가 실행된다.
function handleLike() { if (liked) { setLiked(false); setLikeCount(likeCount - 1); setLikeStyle({ color: 'red' }); } else { setLiked(true); setLikeCount(likeCount + 1); setLikeStyle({ color: 'blue' }); } }처음 상태는 아래와 같다.
liked = false likeCount = 0 likeStyle = { color: 'red' }처음에는
liked가false이므로if (liked)조건은 거짓이다.
그래서else영역이 실행된다.setLiked(true); setLikeCount(likeCount + 1); setLikeStyle({ color: 'blue' });먼저
setLiked(true)가 실행된다.setLiked(true);그러면 좋아요 상태가
false에서true로 바뀐다.
그 다음setLikeCount(likeCount + 1)이 실행된다.setLikeCount(likeCount + 1);현재
likeCount는0이므로 결과는 아래와 같다.0 + 1 = 1그래서 좋아요 수는
1이 된다.
마지막으로setLikeStyle({ color: 'blue' })가 실행된다.setLikeStyle({ color: 'blue' });그러면 버튼 글자 색깔이 파란색으로 바뀐다.
이 세 개의state변경 후 화면은 다시 렌더링된다.// 출력결과 // 좋아요 수: 1 // 버튼 문구: 좋아요 취소 // 버튼 색깔: blue버튼 문구는 아래 삼항 연산자로 결정된다.
{liked ? '좋아요 취소' : '좋아요'}
liked가true이면좋아요 취소가 출력된다.
liked가false이면좋아요가 출력된다.
좋아요 취소 클릭 흐름
좋아요 취소 상태에서 버튼을 다시 클릭하면 현재 상태는 아래와 같다.
liked = true likeCount = 1 likeStyle = { color: 'blue' }이번에는
liked가true이므로if (liked)조건이 참이다.
그래서 아래 코드가 실행된다.setLiked(false); setLikeCount(likeCount - 1); setLikeStyle({ color: 'red' });먼저
setLiked(false)가 실행된다.setLiked(false);그러면 좋아요 상태가
true에서false로 바뀐다.
그 다음setLikeCount(likeCount - 1)이 실행된다.setLikeCount(likeCount - 1);현재
likeCount는1이므로 결과는 아래와 같다.1 - 1 = 0그래서 좋아요 수는 다시
0이 된다.
마지막으로setLikeStyle({ color: 'red' })가 실행된다.setLikeStyle({ color: 'red' });그러면 버튼 글자 색깔이 빨간색으로 돌아간다.
이 세 개의state변경 후 화면은 다시 렌더링된다.// 출력결과 // 좋아요 수: 0 // 버튼 문구: 좋아요 // 버튼 색깔: red즉 좋아요 버튼은 하나의
state만 바꾸는 것이 아니라, 현재 상태에 따라 여러state를 함께 바꾸는 예제다.
5. LabComp6.jsx: Todo List 만들기
LabComp6.jsx는 할 일을 입력하고 추가 버튼을 누르면 목록에 할 일이 추가되는 컴포넌트다.
이번 코드에서는 할 일 목록뿐만 아니라, 입력창에 입력되는 글자도state로 관리한다.
그래서state는 2개가 필요하다.
tasks는 추가된 할 일 목록 배열을 저장한다.
inputText는 현재input에 입력 중인 글자를 저장한다.// LabComp6.jsx import { useState } from 'react'; function TodoList() { const [tasks, setTasks] = useState([]); const [inputText, setInputText] = useState(''); function handleSubmit(event) { event.preventDefault(); const trimmedText = inputText.trim(); if (!trimmedText) { alert('할 일을 입력하세요'); return; } setTasks(prevTasks => [...prevTasks, trimmedText]); setInputText(''); } return ( <div className="mini-todo-list"> <h3>Todo List</h3> <form className="inputButton" onSubmit={handleSubmit}> <input type="text" placeholder="할 일을 입력하세요" value={inputText} onChange={event => setInputText(event.target.value)} /> <button type="submit">추가</button> </form> {tasks.length === 0 ? ( <p>아직 추가된 할 일이 없습니다.</p> ) : ( <ul> {tasks.map((task, index) => ( <li key={`${task}-${index}`}>{task}</li> ))} </ul> )} </div> ); } export function LabComp6() { return ( <> <hr /> <TodoList /> </> ); }이번 코드에서
TodoList는 실제 할 일 목록 기능을 담당하는 컴포넌트다.
LabComp6는 화면에서 구분선을 넣고TodoList를 렌더링하는 바깥 컴포넌트다.
여기서 중요한 점은input값을 직접document.querySelector()로 찾지 않는다는 것이다.
입력창의 값은inputTextstate에 저장하고, 할 일 목록은tasks배열state에 저장한다.
state 역할 정리
먼저 아래 코드는 할 일 목록을 저장하는
state다.const [tasks, setTasks] = useState([]);처음에는 추가된 할 일이 없으므로 초기값은 빈 배열이다.
[]
tasks에는 사용자가 추가한 할 일들이 배열 형태로 저장된다.
예를 들어 할 일을 2개 추가하면 아래처럼 된다.['리액트 복습하기', '리액트 실습하기']그 다음 아래 코드는 입력창의 현재 글자를 저장하는
state다.const [inputText, setInputText] = useState('');처음에는 입력한 글자가 없으므로 초기값은 빈 문자열이다.
''사용자가
input에리액트 복습하기라고 입력하면inputText값은 아래처럼 바뀐다.'리액트 복습하기'즉
tasks는 추가 완료된 할 일 목록이고,inputText는 현재 입력 중인 글자다.
input 입력 흐름
입력창은 아래 코드로 만들어진다.
<input type="text" placeholder="할 일을 입력하세요" value={inputText} onChange={event => setInputText(event.target.value)} />여기서
value={inputText}는 입력창의 값을inputTextstate와 연결한다는 뜻이다.
즉 화면에 보이는 입력값은inputText값이다.
그리고 아래 코드는 사용자가 글자를 입력할 때마다 실행된다.onChange={event => setInputText(event.target.value)}사용자가
input에 아래처럼 입력했다고 생각하면 된다.리액트 복습하기그러면
event.target.value에는 현재 입력창의 값이 들어 있다.event.target.value이 단계의 결과는 아래와 같다.
'리액트 복습하기'그 값이
setInputText()에 전달된다.setInputText('리액트 복습하기')그러면
inputText값이 빈 문자열에서리액트 복습하기로 바뀐다.inputText = '리액트 복습하기'이렇게
input의 값을state로 관리하는 방식을 제어 컴포넌트라고 볼 수 있다.
입력창이 자기 값을 혼자 가지고 있는 것이 아니라,React의state가 입력값을 관리하는 구조다.
form 제출 흐름
이번 코드는 버튼에
onClick을 직접 연결하지 않았다.
대신form에onSubmit을 연결했다.<form className="inputButton" onSubmit={handleSubmit}>그리고 버튼은
submit버튼이다.<button type="submit">추가</button>사용자가 추가 버튼을 클릭하면
form이 제출되고, 그 결과handleSubmit함수가 실행된다.function handleSubmit(event) { event.preventDefault(); const trimmedText = inputText.trim(); if (!trimmedText) { alert('할 일을 입력하세요'); return; } setTasks(prevTasks => [...prevTasks, trimmedText]); setInputText(''); }여기서 가장 먼저 실행되는 코드는 아래 한 줄이다.
event.preventDefault();
form은 원래 제출되면 페이지를 새로고침하려고 한다.
하지만React에서는 페이지를 새로고침하지 않고 현재 화면에서state만 바꿔야 한다.
그래서event.preventDefault()로 기본 제출 동작을 막는다.
그 다음 아래 코드가 실행된다.const trimmedText = inputText.trim();
inputText에 저장된 현재 입력값에서 앞뒤 공백을 제거한다.
예를 들어 사용자가 앞뒤에 공백을 넣고 아래처럼 입력했다고 생각하면 된다.' 리액트 복습하기 '
trim()을 실행하면 앞뒤 공백이 제거된다.'리액트 복습하기'그래서
trimmedText에는 정리된 할 일 글자가 저장된다.const trimmedText = '리액트 복습하기';그 다음 아래 조건문이 실행된다.
if (!trimmedText) { alert('할 일을 입력하세요'); return; }
trimmedText가 비어 있으면!trimmedText는 참이 된다.
즉 사용자가 아무것도 입력하지 않았거나 공백만 입력했을 때는 알림창을 띄우고 함수를 끝낸다.alert('할 일을 입력하세요'); return;이렇게 하면 빈 할 일이 목록에 추가되는 것을 막을 수 있다.
할 일 추가 흐름 1: 빈 배열에 첫 번째 할 일 추가하기
처음에는 아직 추가한 할 일이 없기 때문에
tasks값은 빈 배열이다.[]이 상태에서 사용자가
input에 아래처럼 입력한다.리액트 복습하기입력하는 순간
onChange가 실행되고,inputText에 입력값이 저장된다.setInputText('리액트 복습하기')현재 상태는 아래와 같다.
tasks = [] inputText = '리액트 복습하기'이제 추가 버튼을 클릭하면
form의onSubmit이 실행되고,handleSubmit함수가 호출된다.<form className="inputButton" onSubmit={handleSubmit}>먼저 페이지 새로고침을 막는다.
event.preventDefault();그 다음
inputText의 앞뒤 공백을 제거해서trimmedText에 저장한다.const trimmedText = inputText.trim();현재
inputText가리액트 복습하기이므로trimmedText는 아래와 같다.const trimmedText = '리액트 복습하기';
trimmedText에 글자가 있으므로 아래 조건문은 실행되지 않고 넘어간다.if (!trimmedText) { alert('할 일을 입력하세요'); return; }그 다음 가장 중요한 코드가 실행된다.
setTasks(prevTasks => [...prevTasks, trimmedText]);여기서
prevTasks는tasks의 이전 값을 의미한다.
처음에는tasks가 빈 배열이므로prevTasks도 빈 배열이다.prevTasks = []이제 아래 부분을 보면 된다.
[...prevTasks, trimmedText]여기서
...prevTasks는spread문법이다.
spread는 배열 안의 값을 새 배열 안으로 하나씩 펼쳐 넣을 때 사용한다.
현재prevTasks는 빈 배열이다.[]그래서
...prevTasks로 펼칠 값이 없다.[...prevTasks]결과는 그대로 빈 배열이다.
[]그 다음 뒤에
trimmedText가 붙는다.[...prevTasks, trimmedText]
trimmedText값이리액트 복습하기이므로 새 배열은 아래처럼 만들어진다.['리액트 복습하기']따라서 실제로는 아래 코드처럼 새 배열이
setTasks()에 전달되는 것과 같다.setTasks(['리액트 복습하기']);그러면
React는tasks값이 빈 배열에서 새 배열로 바뀌었다고 판단하고 화면을 다시 렌더링한다.// 출력결과 // 리액트 복습하기마지막으로 아래 코드가 실행된다.
setInputText('');이 코드는 입력창 값을 다시 빈 문자열로 바꾼다.
입력창의value는inputText와 연결되어 있기 때문에,inputText가 빈 문자열이 되면 입력창도 비워진다.
흐름을 정리하면 아래와 같다.
input에 리액트 복습하기 입력 → inputText에 저장 → 추가 버튼 클릭 → handleSubmit 실행 → 새로고침 방지 → trim()으로 공백 제거 → prevTasks 빈 배열 펼치기 → trimmedText 붙이기 → 새 배열 생성 → setTasks() 실행 → 화면 다시 렌더링 → inputText 초기화
할 일 추가 흐름 2: 이미 값이 있는 배열에 새 할 일 추가하기
이제 이미
tasks배열에 값이 하나 들어 있는 상태라고 생각하면 된다.['리액트 복습하기']이 상태에서 사용자가
input에 아래처럼 다시 입력한다.리액트 실습하기입력하는 순간
onChange가 실행되고inputText에 새 입력값이 저장된다.setInputText('리액트 실습하기')현재 상태는 아래와 같다.
tasks = ['리액트 복습하기'] inputText = '리액트 실습하기'이제 추가 버튼을 클릭하면 다시
handleSubmit함수가 실행된다.function handleSubmit(event) { event.preventDefault(); const trimmedText = inputText.trim(); if (!trimmedText) { alert('할 일을 입력하세요'); return; } setTasks(prevTasks => [...prevTasks, trimmedText]); setInputText(''); }먼저 페이지 새로고침을 막는다.
event.preventDefault();그 다음 현재 입력값을 정리한다.
const trimmedText = inputText.trim();이번에는
trimmedText에 아래 값이 저장된다.const trimmedText = '리액트 실습하기';
trimmedText에 값이 있으므로 빈 값 검사 조건은 통과한다.if (!trimmedText) { alert('할 일을 입력하세요'); return; }그 다음 다시 가장 중요한 코드가 실행된다.
setTasks(prevTasks => [...prevTasks, trimmedText]);이번에는 기존
tasks에 이미 값이 들어 있다.
그래서prevTasks는 아래와 같다.prevTasks = ['리액트 복습하기']먼저
...prevTasks가 기존 배열 안의 값을 새 배열 안으로 펼친다.[...prevTasks]이때
...prevTasks는spread문법이다.
기존 배열을 하나의 값으로 넣는 것이 아니라, 배열 안에 있던 값을 하나씩 펼쳐서 새 배열 안에 넣는다.
따라서 이 단계의 결과는 아래와 같다.['리액트 복습하기']그 다음 뒤에 새로 입력한
trimmedText가 붙는다.[...prevTasks, trimmedText]
trimmedText값이리액트 실습하기이므로 새 배열은 아래처럼 만들어진다.['리액트 복습하기', '리액트 실습하기']따라서 실제로는 아래 코드처럼 새 배열이
setTasks()에 전달되는 것과 같다.setTasks(['리액트 복습하기', '리액트 실습하기']);그러면
React는tasks값이 새 배열로 바뀌었다고 판단하고 화면을 다시 렌더링한다.// 출력결과 // 리액트 복습하기 // 리액트 실습하기마지막으로 입력창 값을 비운다.
setInputText('');이 흐름을 보면 새 할 일을 추가할 때 기존 배열을 없애는 것이 아니라, 기존 배열의 값을 먼저 펼치고 그 뒤에 새 값을 붙인다는 것을 알 수 있다.
즉 핵심은 아래와 같다.setTasks(prevTasks => [...prevTasks, trimmedText]);이 코드는 이전 할 일 목록을 기준으로 새 배열을 만든다.
prevTasks를 펼쳐 기존 할 일을 유지하고, 그 뒤에 새 할 일인trimmedText를 붙인다.
흐름을 정리하면 아래와 같다.
기존 tasks에 리액트 복습하기 존재 → input에 리액트 실습하기 입력 → inputText에 저장 → 추가 버튼 클릭 → handleSubmit 실행 → prevTasks로 이전 배열 받기 → spread로 기존 값 펼치기 → trimmedText를 뒤에 붙이기 → 새 배열 생성 → setTasks() 실행 → 화면 다시 렌더링 → inputText 초기화
spread와 rest 구분하기
...문법은 모양이 같아서 헷갈릴 수 있다.
하지만 쓰이는 위치에 따라spread가 될 수도 있고rest가 될 수도 있다.
이번 코드의...prevTasks는spread다.setTasks(prevTasks => [...prevTasks, trimmedText]);여기서
...prevTasks는 기존 배열 안의 값을 새 배열 안으로 펼친다.
즉 값을 하나로 묶는 것이 아니라, 이미 있는 배열을 풀어서 새 배열 안에 넣는다.
예를 들어prevTasks가 아래와 같다면,['리액트 복습하기', '리액트 실습하기']
...prevTasks는 배열 안의 값을 펼친다.[...prevTasks]결과는 아래와 같다.
['리액트 복습하기', '리액트 실습하기']그 다음 뒤에 새 값이 붙으면 새로운 배열이 만들어진다.
[...prevTasks, '최신 체크하기']결과는 아래와 같다.
['리액트 복습하기', '리액트 실습하기', '최신 체크하기']즉
spread는 펼치는 문법이다.
반대로rest는 여러 값을 하나로 묶는 문법이다.const [first, ...rest] = ['리액트 복습하기', '리액트 실습하기', '최신 체크하기'];위 코드에서
first에는 첫 번째 값이 들어간다.first = '리액트 복습하기'그리고
...rest는 남은 값들을 하나의 배열로 묶는다.rest = ['리액트 실습하기', '최신 체크하기']따라서 이번 실습의
...prevTasks는rest가 아니라spread다.
정리하면 아래와 같다.
spread는 기존 배열 안의 값을 펼친다.rest는 남은 값들을 하나의 배열로 묶는다.[...prevTasks, trimmedText]에서...prevTasks는 기존 할 일 목록을 펼치는spread문법이다.그래서
setTasks(prevTasks => [...prevTasks, trimmedText])는 기존 할 일 배열을 펼친 뒤, 새 할 일을 뒤에 붙여 새 배열을 만드는 코드다.
할 일 목록 출력 흐름
할 일 목록 출력은
tasks.length에 따라 달라진다.{tasks.length === 0 ? ( <p>아직 추가된 할 일이 없습니다.</p> ) : ( <ul> {tasks.map((task, index) => ( <li key={`${task}-${index}`}>{task}</li> ))} </ul> )}처음에는
tasks가 빈 배열이다.[]그래서
tasks.length는0이다.tasks.length === 0이 조건이 참이므로 화면에는 아래 문장이 출력된다.
아직 추가된 할 일이 없습니다.할 일을 하나 이상 추가하면
tasks.length는0이 아니다.
예를 들어tasks값이 아래와 같다고 생각하면 된다.['리액트 복습하기', '리액트 실습하기']이제 조건은 거짓이 된다.
tasks.length === 0그래서 문장 대신
ul목록이 출력된다.<ul> {tasks.map((task, index) => ( <li key={`${task}-${index}`}>{task}</li> ))} </ul>
map()은 배열 안의 값을 하나씩 꺼내 반복한다.
첫 번째 반복에서는task와index값이 아래처럼 들어온다.task = '리액트 복습하기' index = 0그래서 아래
li가 만들어진다.<li key="리액트 복습하기-0">리액트 복습하기</li>두 번째 반복에서는
task와index값이 아래처럼 들어온다.task = '리액트 실습하기' index = 1그래서 아래
li가 만들어진다.<li key="리액트 실습하기-1">리액트 실습하기</li>최종적으로 화면에는 아래처럼 출력된다.
// 출력결과 // 리액트 복습하기 // 리액트 실습하기즉
tasks배열이 비어 있으면 안내 문장을 보여주고, 값이 하나라도 있으면map()으로 목록을 만들어 출력한다.
앞에서 단계별로 나누어 확인한 코드를 실제 실행 가능한 형태로 다시 정리하면 아래와 같다.
이번 실습에서 필요한 파일은main.jsx,LabComp3.jsx,LabComp4.jsx,LabComp5.jsx,LabComp6.jsx다.main.jsx
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { LabComp3 } from './LabComp3'; import { LabComp4 } from './LabComp4'; import { LabComp5 } from './LabComp5'; import { LabComp6 } from './LabComp6'; import './index.css'; createRoot(document.getElementById('root')).render( <StrictMode> <LabComp3 /> <LabComp4 /> <LabComp5 /> <LabComp6 /> </StrictMode>, );
main.jsx는 네 개의 실습 컴포넌트를 화면에 렌더링한다.
이 파일에 컴포넌트를 추가해야 브라우저에서 해당 컴포넌트 결과를 확인할 수 있다.
LabComp3.jsx
import { useState } from 'react'; export function LabComp3() { const [count, setCount] = useState(0); const [count2, setCount2] = useState(0); const [count10, setCount10] = useState(0); return ( <> <div> <h3>1씩 증가 카운터</h3> <p>현재 값: {count}</p> <button onClick={() => setCount(count + 1)}>1씩증가</button> </div> <div> <h3>2씩 증가 카운터</h3> <p>현재 값: {count2}</p> <button onClick={() => setCount2(count2 + 2)}>2씩증가</button> </div> <div> <h3>10씩 증가 카운터</h3> <p>현재 값: {count10}</p> <button onClick={() => setCount10(count10 + 10)}>10씩증가</button> </div> </> ); }
LabComp3.jsx는 카운터 3개를 만든다.
각 카운터가 독립적으로 증가해야 하므로count,count2,count10을 각각 따로 관리한다.
LabComp4.jsx
import { useState } from 'react'; export function LabComp4() { const [name, setName] = useState(''); function handleChange(e) { setName(e.target.value); } return ( <> <hr /> <div> <h3>이름 입력</h3> <input type="text" placeholder="이름을 입력하세요" onChange={handleChange} /> <p>안녕하세요, {name}님</p> <p>글자 수: {name.length}</p> </div> </> ); }
LabComp4.jsx는 입력값을namestate에 저장한다.
name값이 바뀌면 인사 문장과 글자 수가 다시 계산되어 화면에 출력된다.
LabComp5.jsx
import { useState } from 'react'; export function LabComp5() { const [liked, setLiked] = useState(false); const [likeCount, setLikeCount] = useState(0); const [likeStyle, setLikeStyle] = useState({ color: 'red' }); function handleLike() { if (liked) { setLiked(false); setLikeCount(likeCount - 1); setLikeStyle({ color: 'red' }); } else { setLiked(true); setLikeCount(likeCount + 1); setLikeStyle({ color: 'blue' }); } } return ( <> <hr /> <div> <h3>좋아요 버튼을 만들어 보자</h3> <p>좋아요 수: {likeCount}</p> <button onClick={handleLike} style={likeStyle}> {liked ? '좋아요 취소' : '좋아요'} </button> </div> </> ); }
LabComp5.jsx는 좋아요 상태에 따라 좋아요 수, 버튼 문구, 버튼 색깔을 함께 변경한다.
버튼 문구는liked값에 따라 삼항 연산자로 결정된다.
LabComp6.jsx
import { useState } from 'react'; export function LabComp6() { const [items, setItems] = useState([]); function handleAdd() { const input = document.querySelector('.inputButton input'); const newItem = input.value.trim(); if (newItem) { setItems([...items, newItem]); input.value = ''; } } return ( <> <hr /> <div> <h3>Todo List</h3> <div className="inputButton"> <input type="text" placeholder="할 일을 입력하세요" /> <button onClick={handleAdd}>추가</button> </div> <ul> {items.map((item, index) => { return <li key={index}>{item}</li>; })} </ul> </div> </> ); }
LabComp6.jsx는 입력한 할 일을 배열state에 추가하고,map()으로 배열을 반복 출력한다.
setItems([...items, newItem])은 기존 목록을 유지하면서 새 할 일을 뒤에 붙이는 코드다.
LabComp3.jsx는 버튼을 클릭할 때마다 연결된 카운터 값만 증가한다.
1씩 증가 버튼은count만 바꾸고, 2씩 증가 버튼은count2만 바꾸고, 10씩 증가 버튼은count10만 바꾼다.
LabComp4.jsx는 입력한 이름을 기준으로 인사 문장과 글자 수를 출력한다.
LabComp5.jsx는 좋아요 버튼을 클릭할 때마다 좋아요 상태가 반대로 바뀐다.
LabComp6.jsx는 할 일을 입력하고 추가 버튼을 누르면 목록에 할 일이 추가된다.
이번 실행 결과를 통해state값이 바뀌면 컴포넌트가 다시 렌더링되고, 화면도 변경된 값에 맞게 다시 그려진다는 것을 확인할 수 있다.
이번 실습에서는
React에서state를 사용해 화면을 동적으로 바꾸는 흐름을 확인했다.
클릭, 입력, 배열 추가처럼 사용자의 동작이 발생하면 이벤트 함수가 실행되고, 그 함수 안에서setState계열 함수를 호출해야 화면이 변경된다.
useState()는 컴포넌트 안에서 변경되는 값을 관리할 때 사용한다.useState(0)은 숫자 초기값이 0인state를 만든다.useState('')는 문자열 초기값이 빈 문자열인state를 만든다.useState(false)는 참과 거짓을 관리하는state를 만든다.useState([])는 배열을 관리하는state를 만든다.- 카운터가 여러 개이고 각각 독립적으로 움직여야 한다면
state도 각각 따로 만들어야 한다.setCount(count + 1)은 현재count값에 1을 더한 값을 새state로 저장한다.input에서 입력한 값은 이벤트 객체의e.target.value로 꺼낼 수 있다.- 문자열의 글자 수는
length로 구할 수 있다.- 조건에 따라 버튼 문구를 바꿀 때는 삼항 연산자를 사용할 수 있다.
style={likeStyle}처럼 작성하면 객체 형태의state를 스타일에 적용할 수 있다.- 배열
state에 값을 추가할 때는 기존 배열을 직접 수정하지 않고 새 배열을 만들어야 한다.[...items, newItem]은 기존 배열의 값을 펼치고 뒤에 새 값을 추가한 새 배열을 만든다.setItems([...items, newItem])을 실행하면 할 일 목록state가 새 배열로 바뀐다.map()은 배열 안의 값을 하나씩 꺼내 화면 요소로 바꿀 때 사용한다.map()으로 목록을 출력할 때는 각 항목에key를 넣어야 한다.이번 실습의 핵심은
state가 단순 변수와 다르다는 점이다.
일반 변수 값만 바꾸면React가 화면을 다시 그릴 필요를 알 수 없다.
하지만setState함수를 호출하면React가 값 변경을 감지하고 컴포넌트를 다시 렌더링한다.
그래서 클릭이나 입력으로 화면을 바꾸려면 변경되는 값을state로 관리해야 한다.