Загружаем каталог…
Загружаем каталог…
React_Day.5 게시판에 날짜와 수정 기능을 붙여 완성하고, 렌더링과 무관하게 값을 들고 있는 useRef , 폼 하나로 추가·수정을 처리하는 학생 관리, 불필요한 리렌더링을 막는 memo 까지 정리한 날. Day.4에서 배열 state의 추가·삭제·수정 패턴을 익혔다. 오늘은 그걸 게시판에 마저 적용하고, 새로운 도구 두 가지( useRef , memo )를 배웠다. 1. 게시판 완성: 작성 시각과 글 수정 글을 쓴 시각을 같이 저장하고, 상세 화면에서 수정 모드 로 바꿔 고칠 수 있게 했다. 날짜 문자열 만들기 const now = new Date() const todayNow = `${now.getFullYear()}-` + `${(now.getMonth() + 1).toString().padStart(2, '0')}-` + `${now.getDate().toString().padStart(2, '0')} ` + `${now.getHours().toString().padStart(2, '0')}:` + `${now.getMinutes().toString().padStart(2, '0')}` // 예) 2026-09-30 17:32 new Date() 로 현재 시각 객체를 만든다. getMonth() 는 0부터 시작 (1월이 0)해서 항상 +1 을 해 준다. padStart(2, '0') : 문자열이 2자리가 되도록 앞에 0 을 채운다. 9 월이 09 로 나온다. 글 작성할 때와 수정할 때 같은 코드를 두 번 쓰게 되는데, 함수로 한 번만 만들어 두면 깔끔하다. const getNow = () => { const now = new Date() const p = (n) => String(n).padStart(2, '0') return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())} ${p(now.getHours())}:${p(now.getMinutes())}` } 수정 모드 Day.4에서 한 "수정 중인가?"를 state로 기억하는 방식이다. 이번에는 글 하나를 상세로 보는 화면이라 modiMode 라는 true/false 로 처리한다. const [modiMode, setModiMode] = useState(false) const updatePost = () => { // 저장 버튼: posts에서 수정한 글(id가 같은 것)만 새 제목/내용/시각으로 교체한다 const updated = posts.map((x) => x.id === selPost1.id ? { ...x, title: selPost1.title, content: selPost1.content, date: getNow() } : x ) setPosts(updated) // 바뀐 배열을 state에 저장하고 setModiMode(false) // 수정 모드를 끈다 } <div className="post-detail"> {selPost1 && ( <div> {modiMode ? ( <> {/* value를 state와 연결 → 입력창의 내용을 React가 관리한다 */} {/* 입력 → onChange가 새 객체로 selPost1을 바꿈 → 리렌더링 → value에 반영 */} <input type="text" value={selPost1.title} onChange={(e) => setSelPost1({ ...selPost1, title: e.target.value })} /> <textarea value={selPost1.content} onChange={(e) => setSelPost1({ ...selPost1, content: e.target.value })} /> <button className="ModiBtn" onClick={updatePost}>저장</button> </> ) : ( <> <h2>{selPost1.title}</h2> <h2>{selPost1.content}</h2> <p>{selPost1.date}</p> <button className="ModiBtn" onClick={() => setModiMode(true)}>수정</button> </> )} </div> )} </div> 수정 중에는 selPost1 (선택한 글의 복사본)만 바뀌고, 목록( posts )은 저장을 눌러야 바뀐다. 실행해서 확인해 보니 수정 중에는 목록의 제목이 그대로 T1, T2 였고, 저장하자 T1-edit, T2 로 바뀌었다. 저장 전에는 임시 초안을 고치는 셈이다. 돌려 보면서 발견한 문제 두 가지 (1) 저장 직후 상세 화면의 날짜가 옛날 값이다. 저장하면 posts 에는 새 날짜가 들어가지만, 상세 화면이 보여 주는 selPost1 에는 새 날짜가 반영되지 않는다. 목록의 날짜는 갱신됐는데( old1 → 현재 시각) 상세의 날짜는 old1 그대로였다. 다시 그 글을 클릭해야 맞춰진다. 저장할 때 같이 갱신해 주면 된다. setSelPost1({ ...selPost1, date }) // updatePost 안에서 상세용 객체도 같이 갱신 Day.3, Day.4에서 정리한 것처럼 id만 저장하고 posts.find(...) 로 꺼내 쓰는 방식 이면 이 문제 자체가 생기지 않는다. 다만 이 방식은 수정 중인 초안을 별도 state로 따로 들고 있어야 한다. (2) 수정 모드에서 다른 글을 클릭하면 수정 모드가 유지된다. T1 을 수정 중에 T2 를 클릭하면, T2 의 상세가 아니라 T2 의 내용이 담긴 입력창 이 뜬다(입력창 값이 T2 였다). modiMode 가 그대로 true 이기 때문이다. 글을 선택하는 함수에서 수정 모드를 꺼 주면 된다. const selPost = (post) => { setSelPost1(post) setModiMode(false) } 참고로 필기 코드의 updatePost 함수 안에서 또 const updatePost = posts.map(...) 로 같은 이름의 변수를 만들고 있었다. 문법상 오류는 아니지만 바깥 함수 이름을 안쪽이 가려서 읽기 어려워서, 위 코드에서는 updated 로 바꿨다. Day.4에서 짚은 내용은 이 코드에도 그대로 남아 있다. 삭제 버튼이 클릭 가능한 div 안쪽에 있어서 e.stopPropagation() 이 필요하고, id: posts.length + 1 은 삭제 후 id가 겹칠 수 있다. CSS 스타일은 Board.css 로 따로 빼서 import "./Board.css" 로 불러온다. CSS는 직접 다 외워서 쓰기보다 AI를 적극 활용하라 는 조언을 들었고, 이 파일도 그렇게 만들었다. 핵심은 이 정도다. * { margin: 0; padding: 0; box-sizing: border-box; } /* 기본 여백 제거 */ .board-app { padding: 20px; max-width: 600px; margin: 0 auto; /* 가운데 정렬 */ border: 2px solid pink; border-radius: 10px; } .board-item { border: 1px solid gray; margin: 10px 0; padding: 10px; cursor: pointer; border-radius: 5px; transition: 0.2s; } .board-item:hover { background-color: #ffe6f0; } /* 마우스를 올리면 색 변경 */ .post-detail textarea { height: 200px; } box-sizing: border-box 는 padding 과 border 를 width 에 포함해서 계산하게 해서, 입력창을 width: 100% 로 줘도 삐져나오지 않게 한다. 2. useRef: 렌더링과 상관없이 값을 들고 있기 useRef(초기값) 은 { current: 초기값 } 모양의 객체 를 만들어 준다. 이 객체는 다시 렌더링되어도 같은 객체가 유지 되고, current 를 바꿔도 리렌더링이 일어나지 않는다. useState 와 비교하면 이렇다. useState useRef 값을 바꾸면 리렌더링 (화면 갱신) 리렌더링 없음 렌더링 사이에 값 유지 O O 바꾸는 방법 setXxx(새값) ref.current = 새값 값이 바뀌어도 화면은 그대로 const inRef = useRef(1) <button onClick={() => { inRef.current++ console.log(inRef.current) }}>버튼 1 증가</button> 버튼을 두 번 누르면 콘솔에 2 , 3 이 찍힌다. 값은 올라가고 있지만 컴포넌트 함수는 처음 한 번 말고는 다시 실행되지 않았다(렌더링 횟수 1). 화면에 {inRef.current} 를 그려 놓았어도 다음에 렌더링될 때까지는 안 바뀐다. 이전 값을 기억하는 패턴 const cntRef = useRef() // 초기값 없음 → undefined const [init, setInit] = useState(0) useEffect(() => { cntRef.current = init }, [init]) return ( <div> <p>{init}</p> <p>{cntRef.current}</p> <button onClick={() => setInit(init + 1)}>state 1증가</button> </div> ) 버튼을 누를 때마다 화면의 두 숫자가 이렇게 바뀐다. (처음) 0, (비어 있음) 1회 클릭 1, 0 2회 클릭 2, 1 3회 클릭 3, 2 state 가 바뀌어 컴포넌트가 다시 그려질 때 {cntRef.current} 는 아직 이전 값 을 들고 있다. useEffect 는 렌더링이 끝난 뒤에 실행되기 때문이다. 순서는 이렇다. 버튼 클릭 → init 이 1로 바뀜 컴포넌트가 다시 실행되면서 화면을 그린다. 이때 cntRef.current 는 아직 0 이다. 화면이 그려진 뒤에 useEffect 가 실행되어 cntRef.current 를 1 로 바꾼다. (리렌더링은 안 일어나서 화면에는 반영되지 않는다.) 그래서 현재 값과 바로 직전 값 을 나란히 보여 주는 패턴으로 쓸 수 있다. 필기에서는 초기값 없이 useRef() 로 만들었는데, 이 값은 undefined 다. 필기에는 "undefined는 (null, false, 0, "")와 같다"고 적었는데, 모두 falsy (조건문에서 거짓으로 취급)라는 점은 같지만 같은 값은 아니다. useRef() 는 undefined , useRef(null) 은 null 이 들어간다. 참고로 React 공식 문서는 렌더링 중에 ref.current 를 읽어서 화면에 그리는 것 을 권하지 않는다. 위 코드는 "ref는 리렌더링을 일으키지 않는다"는 동작을 눈으로 보려는 실험용이다. state와 ref를 같이 쓸 때 const ref = useRef() const [num, setNum] = useState(0) useEffect(() => { ref.current = num }, [num]) const increase = () => { ref.current++ } // ref만 올린다 버튼은 두 개다. 1증가 는 state 를, ref증가 는 ref 만 올린다. 실행해 보면 이렇다. 1증가 클릭 → 화면: 1, 0 ref증가 클릭 → 화면: 1, 0 (ref는 올랐지만 화면 그대로) ref증가 클릭 → 화면: 1, 0 1증가 클릭 → 화면: 2, 3 ← 리렌더링되는 순간에야 ref에 쌓인 값이 보인다 ref는 바뀌어도 화면을 갱신하지 못하고, 어떤 이유로든 다른 곳에서 리렌더링이 일어나야 그제야 쌓인 값이 화면에 보인다. 타이머 id를 ref에 저장하고 cleanup 하기 useRef 가 가장 많이 쓰이는 곳 중 하나가 타이머 id 보관 이다. const [num, setNum] = useState(0) const ref = useRef(null) useEffect(() => { ref.current = setInterval(() => { setNum((n) => n + 1) // 1초마다 숫자를 올린다 → 1초마다 리렌더링 }, 1000) // cleanup 함수: useEffect 안에서 return하는 함수 return () => clearInterval(ref.current) }, []) return <div>{num}</div> setInterval 은 타이머의 id를 돌려준다. 나중에 clearInterval(id) 로 멈추려면 그 id를 기억해야 하고, 값이 바뀌어도 화면은 상관없으니 ref 에 저장한다. 의존성 배열이 [] 라서 처음 한 번만 타이머를 만든다. setNum((n) => n + 1) 처럼 함수형 업데이트 를 쓴 이유는 Day.2에서 한 것과 같다. 이 effect는 처음 한 번만 만들어져서 안쪽의 num 이 처음 값(0)에 고정 된다. setNum(num + 1) 로 쓰면 계속 0 + 1 만 설정하게 된다. cleanup 함수 는 컴포넌트가 화면에서 사라질 때(언마운트) 실행되어 타이머, 이벤트 리스너처럼 메모리를 차지하는 것들을 정리한다. 필기에는 "언마운트될 때 실행"이라고만 적었는데, 보충하면 의존성 배열의 값이 바뀌어 effect가 다시 실행되기 직전에도 이전 effect의 cleanup이 먼저 실행된다. cleanup이 정말 필요한지 확인해 봤다. 개발 모드의 StrictMode 는 effect를 한 번 실행 → cleanup → 다시 실행 해서 정리가 제대로 짜여 있는지 검사한다. 200ms 간격 타이머로 1.05초 뒤 숫자를 비교했다. 1.05초 뒤 숫자 cleanup 있음 5 (정상) cleanup 없음 10 (타이머가 2개 돌아서 2배 빠름) cleanup을 빼면 첫 번째 타이머가 멈추지 않고 남아 있어서 숫자가 두 배로 올라갔다. 타이머를 만들었으면 정리하는 코드도 같이 쓴다. 3. 학생 정보 관리: 폼 하나로 추가와 수정 Day.4에서는 입력창 state 를 각각 만들었다. 이번에는 입력창이 여러 개일 때 state 하나(객체)로 묶어서 관리하고, 폼과 목록을 컴포넌트로 나눴다. 구조는 이렇다. App (students, selected state + 추가/수정/삭제 함수) ├─ StudentForm (입력 폼, 입력값 state) └─ StudentList (목록, 수정/삭제 버튼) App: 상태와 동작을 한곳에 const App = () => { const [students, setStudents] = useState([]) const [selected, setSelected] = useState(null) // 수정하려고 고른 학생 const onAddUpdate = (student) => { if (student.id) { // 수정: id가 같으면 폼에서 받은 새 객체로 교체, 아니면 기존 객체 유지 setStudents((stu) => stu.map((x) => (x.id === student.id ? student : x))) } else { // 추가: 이전 배열을 복사하고, 폼에서 받은 값(name, age, major)에 고유 id를 붙여 넣는다 setStudents((stu) => [...stu, { ...student, id: Date.now() }]) } setSelected(null) } const onEdit = (stu) => { setSelected(stu) } const onDelete = (id) => { setStudents((stu) => stu.filter((x) => x.id !== id)) } return ( <div> <h2>학생 정보 관리</h2> <StudentForm onSubmit={onAddUpdate} selected={selected} /> <StudentList students={students} onEdit={onEdit} onDelete={onDelete} /> </div> ) } 추가인지 수정인지는 student.id 가 있는지로 구분한다. 새로 입력한 학생에는 id가 없고, 목록에서 골라 온 학생에는 id가 있다. setStudents((stu) => ...) : set 함수에 함수를 넘기면 "현재 최신 state"를 인자로 받는다. Day.2의 함수형 업데이트와 같다. StudentForm: 입력 하나하나가 아니라 객체로 const StudentForm = ({ onSubmit, selected }) => { const [student, setStudent] = useState({ name: '', age: '', major: '' }) const handleChange = (e) => { const { name, value } = e.target // e.target의 name 속성값과 value를 꺼내서 쓴다 setStudent((stu) => ({ ...stu, [name]: value, // 계산된 속성명: name 변수의 '값'이 키가 된다 })) } const handleSubmit = (e) => { e.preventDefault() // form의 기본 동작(페이지 새로고침)을 막는다 if (!student.name) return // 이름이 비어 있으면 무시 onSubmit(student) // 부모가 내려준 함수에 입력한 객체를 넘긴다 setStudent({ name: '', age: '', major: '' }) // 입력 칸 초기화 } // selected가 바뀔 때마다: 선택한 학생이 있으면 폼에 채우고, 없으면 빈 폼으로 useEffect(() => { setStudent(selected || { name: '', age: '', major: '' }) }, [selected]) return ( <form onSubmit={handleSubmit}> <input placeholder="이름" value={student.name} name="name" onChange={handleChange} /> <input placeholder="나이" value={student.age} name="age" onChange={handleChange} /> <input placeholder="전공" value={student.major} name="major" onChange={handleChange} /> <button type="submit">{student.id ? '수정' : '추가'}</button> </form> ) } 여기서 새로 배운 것들이다. name 속성 + [name]: value : 필기에서 헷갈렸던 부분이다. const { name, value } = e.target 의 name 은 객체의 키 이름이 아니라 <input name="age"> 처럼 태그에 적어 둔 name 속성값 이다. 입력창에 name="age" 를 붙여 두면 handleChange 하나로 세 칸을 전부 처리한다. [name]: value 는 대괄호 안의 변수 값을 키로 쓰는 문법(계산된 속성명)이라서, name 이 'age' 이면 { age: value } 가 된다. <form onSubmit> + e.preventDefault() : 폼을 제출하면 브라우저가 기본적으로 페이지를 새로고침한다. 이걸 막아야 React 화면이 유지된다. 버튼에 type="submit" 을 두면 Enter 키로도 제출 된다. useEffect(..., [selected]) : 부모에서 selected 가 바뀌면 폼의 내용을 그 학생 값으로 맞춘다. 선택이 null 이 되면 빈 폼으로 돌아간다. 버튼 글자는 student.id 가 있느냐(수정 모드인지)로 바뀐다. StudentList const StudentList = ({ students, onEdit, onDelete }) => ( <ul> {students.map((stu) => ( <li key={stu.id}> {stu.name}|{stu.age}|{stu.major} <button onClick={() => onEdit(stu)}>수정</button> <button onClick={() => onDelete(stu.id)}>삭제</button> </li> ))} </ul> ) 실행해 보니 흐름은 이랬다. 이름을 비운 채 제출 → 목록 0개 (무시됨) kim / 20 입력 후 제출 → 목록: kim|20, 폼 비워짐 수정 클릭 → 폼에 kim, 20이 채워지고 버튼이 '수정'으로 바뀜 나이를 21로 바꿔 제출 → 목록: kim|21, 버
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
React_Day.5. React_Day.5 게시판에 날짜와 수정 기능을 붙여 완성하고, 렌더링과 무관하게 값을 들고 있는 useRef , 폼 하나로 추가·수정을 처리하는 학생 관리, 불필요한 리렌더링을 막는 memo 까지 정리한 날. Day.4에서 배열 state의 추가·삭제·수정 패턴을 익혔다. 오늘은 그걸 게시판에 마저 적용하고, 새로운 도구 두 가지( useRef , memo )를 배웠다. 1. 게시판 완성: 작성 시각과 글 수정 글을 쓴 시각을 같이 저장하고, 상세 화면에서 수정 모드 로 바꿔 고칠 수 있게 했다. 날짜 문자열 만들기 const now = new Date() const todayNow = `${now.getFullYear()}-` + `${(now.getMonth() +…
Открыть источник