Загружаем каталог…
Загружаем каталог…
React_Day.9 어제 못 끝낸 회원·게시판 프로젝트를 마무리하고, 전역 상태 관리 라이브러리인 Redux(Redux Toolkit) 를 처음 배운 날. Day.8에서 뼈대를 잡던 프로젝트를 오늘 끝냈다. 그다음에는 Day.7의 Context 를 이어받아, 앱이 커졌을 때 쓰는 Redux로 넘어갔다. 1. 회원·게시판 프로젝트 마무리 어제는 로그인, 회원가입, 메뉴까지 하고 게시판 관련 페이지가 빈 컴포넌트였다. 오늘 나머지 페이지를 채웠다. 파일 오늘 한 일 LoginPage 로그인 성공 시 setCurrentUser + localStorage 저장 + /boardList 로 이동, 실패하면 alert MemberListPage 관리자( admin )로 로그인했을 때만 회원 아이디 목록을 보여 줌 CreatePostPage 로그인한 사람만 글 작성, localStorage 의 posts 에 저장 EditPostPage useParams 로 글 id를 읽어 폼에 채우고 수정 내용을 저장 BoardListPage 글 목록 출력, 작성자 본인에게만 수정/삭제 노출 Navibar 로그인하면 아이디님 과 로그아웃 버튼 노출 Day.8에서 짚은 로그인 문제( localStorage 만 저장하고 Context state는 안 바꿈)가 이번에 setCurrentUser(loginUser) 와 navigate('/boardList') , 실패 시 alert 로 채워졌다. 주요 코드 글 작성 : 로그인하지 않았으면 안내하고 로그인 페이지로 보낸다. const { currentUser } = useAuth() // navigate는 화면이 렌더링된 이후에 실행되어야 한다 // (이벤트 핸들러 안이나 useEffect 안에서 호출한다) useEffect(() => { if (!currentUser) { alert('로그인이 필요합니다.') navigate('/login') } }, []) const onSubmit1 = (e) => { e.preventDefault() if (!title.trim() || !content.trim()) { alert('제목과 내용을 입력해주세요') return } let posts = JSON.parse(localStorage.getItem('posts')) || [] // 기존 글 목록 (없으면 []) const newPost = { id: Date.now(), title, content, writeId: currentUser.userId, // 로그인한 사람의 아이디 } posts.push(newPost) localStorage.setItem('posts', JSON.stringify(posts)) navigate('/boardList') // 이벤트 핸들러 안이라서 바로 사용 } 필기에서 posts.push(newPost) 가 괜찮은 이유를 직접 적어 두었다. posts 는 React state가 아니라 localStorage 에서 꺼낸 지역 변수 라서 React가 감시하는 값이 아니고, 그래서 push 로 바꿔도 된다. Day.8에서 정리한 내용 그대로다. (필기에 주석 처리된 setTitle('') 는 이 페이지를 떠나면 컴포넌트가 사라지니 초기화할 필요가 없다는 의문이었는데, 맞는 판단이다.) 글 수정 : 주소의 id로 글을 찾아 폼을 채운다. const { id } = useParams() // 문자열로 반환 const [post, setPost] = useState({ title: '', content: '' }) useEffect(() => { const posts = JSON.parse(localStorage.getItem('posts')) const currentPost = posts.find((x) => x.id === parseInt(id)) if (currentPost) { setPost(currentPost) } // 찾으면 입력창에 기존 제목/내용이 채워진다 }, [id]) const onSubmit1 = (e) => { e.preventDefault() const posts = JSON.parse(localStorage.getItem('posts')) || [] // 같은 id의 글만 수정한 값(post)으로 교체. id와 writeId는 그대로 유지 const newposts = posts.map((x) => x.id === parseInt(id) ? { id: x.id, ...post, writeId: x.writeId } : x ) localStorage.setItem('posts', JSON.stringify(newposts)) navigate('/boardList') } { id: x.id, ...post, writeId: x.writeId } 에서 스프레드 뒤에 writeId 를 다시 쓴 이유 는, post 에 들어 있는 값보다 원래 글의 작성자를 우선하려는 것이다. 객체 스프레드는 뒤에 오는 값이 앞의 값을 덮어쓴다. 글 목록 : 본인 글에만 수정/삭제를 보여 준다. {currentUser && x.writeId === currentUser.userId && ( <div className="flex gap-1"> <Link to={`/posts/edit/${x.id}`}>수정</Link> <button onClick={() => handleDelete(x.id)}>삭제</button> </div> )} const handleDelete = (id) => { const newPosts = posts.filter((x) => { return x.id !== id }) // 삭제할 글만 빼고 새 배열 setPosts(newPosts) // 화면 갱신 localStorage.setItem('posts', JSON.stringify(newPosts)) // 저장소 갱신 } 화면 state( setPosts )와 저장소( localStorage )를 둘 다 갱신 하는 것도 Day.8에서 정리한 원칙이다. 회원 목록 : 관리자만 보이게 했다. {currentUser && currentUser.userId === 'admin' && currentUser.password === 'admin' ? (<ul>{users.length > 0 ? users.map((x, index) => <li key={index}>{x.userId}</li>) : <li>회원이 없습니다.</li>}</ul>) : (<div>회원목록은 관리자만 볼 수 있습니다.</div>)} 전체 흐름을 실제로 돌려 본 결과 코드를 그대로 가져다 전체 앱을 가입 → 로그인 → 글쓰기 → 수정 → 삭제 순서로 실행해 봤다. 핵심 기능은 모두 의도대로 동작했다. 가입하면 /login 으로 이동하고, 없는 계정으로 로그인하면 alert 가 뜬다. 로그인하면 /boardList 로 이동하고 메뉴에 user1님 과 로그아웃이 보인다. 로그인 없이 /posts/create 에 들어가면 로그인이 필요합니다. 가 뜨고 /login 으로 간다. 글쓰기 후 목록에 글이 보이고, 빈 제목/내용은 alert 로 막힌다. 수정 페이지는 기존 제목/내용이 채워져 열리고, 저장하면 목록에 반영된다. 다른 회원( user2 )의 목록 화면에는 수정/삭제가 안 보이고, 작성자가 삭제하면 화면과 저장소에서 같이 사라진다. 회원 목록은 admin 만 목록이 보이고 일반 회원과 비로그인은 안내 문구만 보인다. 돌려 보면서 발견한 허술한 부분 연습용 프로젝트라 기능은 충분하지만, "버튼을 숨기는 것"과 "막는 것"은 다르다 는 점이 크게 보였다. 수정 페이지에 권한 검사가 없다. 목록에서 수정 버튼을 숨겨도 주소창에 /posts/edit/글id 를 직접 입력하면 다른 회원도, 로그아웃한 사람도 폼이 열리고 저장된다. 실제로 user2 가 user1 의 글 제목을 바꿨고(작성자는 그대로 user1 ), 비로그인으로도 열렸다. 수정 페이지에서도 currentUser 와 writeId 를 비교해서 다르면 돌려보내야 한다. 이런 검사는 서버가 있다면 서버에서도 해야 한다. 글이 하나도 없을 때 수정 주소로 들어가면 에러가 난다. JSON.parse(localStorage.getItem('posts')) 가 null 이라 posts.find 에서 Cannot read properties of null (reading 'find') 로 깨진다. 다른 곳처럼 || [] 를 붙이면 된다. 같은 아이디로 여러 번 가입된다. user1 을 두 번 가입하자 users 에 2개가 들어갔다( admin 회원 목록에 user1 이 두 번 나왔다). 가입할 때 users.some(...) 으로 중복을 확인해야 한다. 관리자 판별이 admin / admin 문자열 비교다. 아무나 admin / admin 으로 가입하면 관리자 화면을 볼 수 있다. 실제로는 서버가 권한을 가진 계정을 따로 관리해야 한다. 비밀번호를 평문으로 저장하는 것은 Day.7부터 계속 연습용이라는 전제다. 목록 페이지가 localStorage 를 직접 읽어서 currentUser 를 따로 state로 갖고 있다. 필기에도 " useAuth() 를 사용해도 됨"이라고 적혀 있듯이, Context가 이미 있으니 useAuth() 로 쓰면 코드가 줄어든다. 2. Redux: 전역 상태 저장소 상태 관리 라이브러리. 리액트는 컴포넌트 단위로 상태를 관리하는데, 컴포넌트가 많아질수록 상태 전달이 복잡해진다(props drilling). Redux는 컴포넌트들이 이 상태를 쉽게 공유하도록 도와준다. Day.7에서 Context로 풀었던 문제와 같은 문제를 푸는 도구다. 한 곳(Store)에 상태를 모아 두고, 필요한 컴포넌트가 직접 꺼내 쓰고 , 필요한 컴포넌트만 다시 그린다. 필기에 풀어 쓴 설명을 정리하면 이렇다. 컴포넌트를 서로 연결해서 전달하는 대신, 한 공간에서 상태를 직접 관리할 수 있는 공간 (Store)을 만든다. "상태를 바꾸겠다"는 요청의 종류 를 정하고, 그 요청이 들어오면 상태를 어떻게 바꿀지 함수 로 만들어 둔다. Context와의 차이 구분 Context API Redux 개념 리액트 내장 기능. 전역 상태를 제공/구독하는 간단한 방법 외부 라이브러리. 예측 가능한 상태 관리 도구 주요 목적 테마, 다국어, 로그인 정보 같은 단순 전역 상태 공유 복잡한 비즈니스 로직, 상태 흐름 추적, 대규모 앱의 상태 관리 데이터 흐름 Provider → Consumer Action → Reducer → Store → View (단방향) 복잡성 비교적 단순 설정, 구조, 코드량이 많음 (하지만 툴링이 풍부) 핵심 개념 Store : 앱의 모든 상태를 보관하는 단일 저장소 (예: 사용자 정보, 장바구니, 게시글 목록). Action : 상태를 바꾸고 싶을 때 보내는 메시지 . 객체 모양이다. { type: '...' } Reducer : 액션을 받아서 새로운 state를 만들어 돌려주는 함수 . state를 직접 바꾸지 않는다. Dispatch : 액션을 store에 보내는 함수 . 컴포넌트에서 호출한다. useSelector, useDispatch : 컴포넌트에서 store의 값을 읽고( useSelector ), 액션을 보낼( useDispatch ) 때 쓰는 react-redux 훅. Redux Toolkit : 위 과정을 간단하게 만들어 주는 공식 도구. createSlice , configureStore 를 쓴다. 동작 흐름 컴포넌트 → (dispatch) → 액션 → 리듀서(slice) → 새로운 상태 → 스토어 → (useSelector) → 컴포넌트 반영 식당에 비유하면 이렇다. Action은 주문서 , Reducer는 주문서를 보고 요리(상태 변경)하는 사람 , Store는 완성된 음식(상태)이 놓이는 곳 이다. 사용자 클릭(UI)이 dispatch(action) 으로 주문서를 넣으면 Reducer가 상태를 바꾸고, Store에 저장된 새 상태가 UI에 자동으로 반영된다. Redux의 3가지 원칙 단일 스토어 : 애플리케이션 전체 상태를 하나의 store에서 관리한다. 읽기 전용 상태 : 상태는 직접 바꿀 수 없고 action을 통해서만 바꾼다. 순수 함수로 변경 : 상태 변경은 reducer라는 순수 함수 (같은 입력이면 항상 같은 결과, 바깥 값을 건드리지 않음)로만 처리한다. 설치 npm install @reduxjs/toolkit react-redux 3. Redux Toolkit 실습: 카운터 파일은 세 군데로 나뉜다. slice (상태와 변경 방법), store (slice를 모음), main.jsx (앱에 연결). (1) counterSlice.js: 상태와 상태 변경 방법을 정의 slice = 전역 상태의 한 조각 (예: counterSlice , todoSlice , userSlice ). createSlice 는 액션과 리듀서를 한 번에 만드는 함수다. import { createSlice } from "@reduxjs/toolkit"; const counterSlice = createSlice({ name: "counter", initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, reset: (state) => { state.value = 0; }, }, }); export const { increment, decrement, reset } = counterSlice.actions; // 자동으로 액션 생성자(action creator)를 만들어 준다. export default counterSlice.reducer; createSlice 에 넘기는 옵션이다. 옵션 필수 설명 name 필수 슬라이스 이름. 액션 타입의 앞부분 이 된다 ( counter/increment ) initialState 필수 초기 상태값 (객체, 배열, 숫자 모두 가능) reducers 필수 상태 변경 로직. 이 함수들마다 액션과 리듀서가 자동 생성 된다 extraReducers 선택 비동기 액션이나 다른 slice의 액션 을 처리 (2) store.js: slice를 모아 하나의 store로 import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "./counterSlice"; const store = configureStore({ reducer: { counter: counterReducer, // 슬라이스 등록 }, }); export default store; configureStore() 에 등록하면 모든 컴포넌트가 가져다 쓸 수 있게 된다. (3) main.jsx: 앱에 store를 연결 import { Provider } from 'react-redux' createRoot(document.getElementById('root')).render( <Provider store={store}> <App /> </Provider> ) Context의 Provider와 같은 역할이다. 이 안쪽의 모든 컴포넌트가 store에 접근한다. (4) Counter.jsx: 실제 UI에서 쓰기 import { useSelector, useDispatch } from "react-redux"; import { increment, decrement, reset } from "../redux/counterSlice"; const Counter = () => { const count = useSelector((state) => state.counter.value); // store에서 값 꺼내오기 const dispatch = useDispatch(); // 액션 실행 준비 return ( <div> <h2>카운터: {count}</h2> <button onClick={() => dispatch(increment())}>+1</button> <button onClick={() => dispatch(decrement())}>-1</button> <button onClick={() => dispatch(reset())}>Reset</button> </div> ); }; useSelector : store에서 값을 꺼낸다. useDispatch : 액션을 실행시킨다. 직접 실행해 보니 처음 카운터: 0 , +1 을 두 번 누르고 -1 을 한 번 누르면 1 , Reset 을 누르면 0 이 되었다. 컴포넌트의 이벤트 → dispatch → store → slice → 다시 store → 컴포넌트가 useSelector 로 값을 가져와 {count} 를 출력한다. 액션을 보내면 store가 직접 처리하는 것이 아니라 createSlice 가 만들어 준 reducer 가 상태를 계산하고 store가 업데이트를 관리한다. Action 객체의 모양 { type: "counter/increment", payload: 10 // 추가 데이터 } 액션 생성자를 직접 호출해서 확인했다. increment() // { type: 'counter/increment' } incrementByAmount(10) // { type: 'counter/incrementByAmount', payload: 10 } increment() 처럼 데이터가 필요 없으면 type 만 있고, 데이터를 넘기면 payload 에 들어간다. 리듀서에서는 (state, action) 의 action.payload 로 꺼낸다( state.value += action.payload ). type 은 슬라이스name/리듀서이름 으로 자동 생성된다. 실제로 확인하며 바로잡은 부분 name 이 state의 key가 되는 것이 아니다. 필기의 옵션 표에는 "name: 슬라이스 이름 (state 접근 시 key로 사용됨)"이라고 적혀 있다. 확인해 보니 state.counter.value 에서 counter 는 configureStore 의 reducer: { counter: ... } 에 적은 키 였다. 키를 cnt 로 바꿔서 등록하면 state.cnt.value 로 접근해야 했고, 액션 타입은 여전히 counter/increment 였다. name 은 액션 타입의 앞부분 을 만드는 데 쓰인다. 보통은 둘을 같은 이름으로 맞춰서 헷갈리지 않게 한다. Reducer가 "새로운 Store를 생성"하는 것이 아니다. 필기의 순서에는 "Reducer는 새로운 Store를 생성한다"고 적혀 있다. dispatch 전후를 비교해 보니 store 객체는 그대로이고, store.getState() 가 돌려주는 state 객체만 새로 만들어졌다. 이전 state는 { counter: { value: 0 } } 그대로 남았다. 정확히는 "Reducer가 새로운 state 를 돌려주고, Store가 그걸 저장한다"이다. dispatch 는 store의 기능이다. 필기에 "Action을 Store에 바로 전달하는 것이 아니라 Reducer에 전달해야 한다"고 적혀 있는데, 실제로는 dispatch(action) 으로 store에 액션을 보내면 store가 내부에서 reducer를 호출 한다. 개발자가 reducer를 직접 호출하지 않는다는 점이 핵심이다. state.value += 1 이 "직접 바꾸는 것"처럼 보이는데 괜찮을까? 3원칙은 "상태는 직접 바꾸지 않는다"인데 slice 안에서는 state.value += 1 로 쓴다. Redux Toolkit이 Immer 라는 라이브러리를 내장해서, 이 코드를 받아 원본은 건드리지 않고 새 state를 만들어 준다. 직접 확인했다. reducer({value: 5}, increment()) 를 호출하면 결과는 {value: 6} 인 새 객체이고 원래 {value: 5} 는 그대로였다. 이 방식은 createSlice 안의 reducer에서만 통한다. 일반 함수나 React useState 에서는 여전히 새 객체를 만들어야 한다. 필요한 컴포넌트만 리렌더링 된다는 설명도 확인했다. Counter 는 state.counter.value 를, 다른 컴포넌트( Label )는 state.label.text 를 useSelector 로 읽게 했다. label 만 바꾸면 Label 만 1번 렌더링되고 Counter 는 0번이었고, 반대
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
React_Day.9. React_Day.9 어제 못 끝낸 회원·게시판 프로젝트를 마무리하고, 전역 상태 관리 라이브러리인 Redux(Redux Toolkit) 를 처음 배운 날. Day.8에서 뼈대를 잡던 프로젝트를 오늘 끝냈다. 그다음에는 Day.7의 Context 를 이어받아, 앱이 커졌을 때 쓰는 Redux로 넘어갔다. 1. 회원·게시판 프로젝트 마무리 어제는 로그인, 회원가입, 메뉴까지 하고 게시판 관련 페이지가 빈 컴포넌트였다. 오늘 나머지 페이지를 채웠다. 파일 오늘 한 일 LoginPage 로그인 성공 시 setCurrentUser + localStorage 저장 + /boardList 로 이동, 실패하면 alert MemberListPage 관리자( admin )로 로그인했을 때만 회원…
Открыть источник