81일차 내용 정리

채공부·2025년 9월 16일

payload : 어떤 action 과 같이 실려가는 data

  • action 객체는 최소한 type 을 가지고 있어야 한다
  • 실제로 전달할 데이터는 payload 에 담는 것이 일반적
dispatch({type:"UPDATE", payload:{name:"kim"}})

reducer 함수가 호출되는 시점

  1. 초기 시점
  • Redux 스토어가 생성되면 reducer가 한 번 자동으로 호출되어 초기 상태를 설정
  1. dispatch(action) 호출 시
  • 어떤 컴포넌트에서 dispatch(action)을 호출하면 reducer가 호출되어 새로운 상태를 계산
// BComponenet action 상태값을 변경
dispatch({type:"UPDATE"})// reducer 함수 호출
const reducer = (state = initState, action) => {
	let newState;
    // action.type 에 따라 state 계산
    return newState;
}
			⭣
AComponent 에서 새로운 상태로 UI 렌더링

state = initState 는 어떻게 동작할까?

function showName(name){
	console.log(name);
}

showName("kim"); 		⭢ console 에 kim 출력
showName(); 			⭢ console 에 undefined 출력

function showName(name = "park"){
	console.log(name);
}

showName("kim") 		⭢ console 에 kim 출력
showName();				⭢ console 에 park 출력

Node.js 콘솔 실습

  • 💡 Tip : Shift + enter = 개행
C:\Users\USER>node

> function showName(name = "park"){
... console.log(name);
... }
undefined

> showName("kim");
kim
undefined

> showName();
park
undefined

상태값 전달 방식 비교

  • StudyChild.jsx 확인

  • state.fortuneToday
    1. "동쪽으로 가면 귀인을 만나요"
    2. "수정된 운세"

  • props 로 부모의 상태값을 전달 받는다

    • 여러 컴포넌트를 거쳐서 props를 전달해야 하므로 불편
      계층 간 상태 전달 문제 발생
<p>오늘의 운세 : <strong>{props.fortune}</strong></p>
  • 전체 코드
function StudyChild(props) {
    const fortuneToday = useSelector(state => {
        return state.fortuneToday;
    });
    return <>
        <p>오늘의 운세 : <strong>{props.fortune}</strong></p>
        <p>오늘의 운세 : <strong>{fortuneToday}</strong></p>
    </>
}

App & Study & StudyChild 의 상태 전달 흐름

  • 최상위 컴포넌트(App) 에서 상태값을 관리

  • 부모의 컴포넌트가 자식 컴포넌트에게 props 로 상태값 전달
    자식 컴포넌트는 전달받은 props 를 활용해 UI 에 출력

  • 부모가 상태를 업데이트할 때 자식 컴포넌트는 자동으로 최신 상태를 반영

// App.jsx
<Study fortune={fortune}/>// Study.jsx
<StudyChild fortune={props.fortune}/>// StudyChild.jsx
<p>오늘의 운세 : <strong>{props.fortune}</strong></p>

로그인 처리

Form.Control : input 요소 (input type="text")

  • LoginModal.jsx
function LoginModal() {

    // store 로부터 loginModal 의 상태값을 읽어온다
    // {title:"Modal의 제목", show:Modal을 띄울지 여부(true or false)} 형식의 object
    const loginModal = useSelector(state => state.loginModal);
    
    // 아이디 비밀번호를 입력했을 때 호출될 함수
    const handleChange = (e) => {

    }

    return <>
        {/* store 에서 관리되는 loginModal 의 show 값을 이용해서 띄울지 말지를 결정 */}
        <Modal show={loginModal.show} size="lg" centered>
            <Modal.Header closeButton>
                <Modal.Title>{loginModal.title}</Modal.Title>
            </Modal.Header>
            <Modal.Body>
                <FloatingLabel controlId='userName' label="User Name" className='mb-3'>
                    <Form.Control onChange={handleChange} name="userName" type="text" />
                </FloatingLabel>
                <FloatingLabel controlId='password' label="Password" className='mb-3'>
                    <Form.Control onChange={handleChange} name="password" type="password" />
                </FloatingLabel>
            </Modal.Body>
            <Modal.Footer>
                <Button>로그인</Button>
            </Modal.Footer>
        </Modal>    
    </>
}
  • LoginModal.jsx 에 useState 함수 추가
    • 입력한 userName 과 password 를 상태값으로 관리
const [state, setState] =  useState();
  • handleChange 함수 완성
    • 아이디 비밀번호를 입력했을 때 호출될 함수
const handleChange = (e) => {
	// 입력한 내용을 상태값에 반영한다
	setState({
		 ...state,
		[e.target.name]:e.target.value
	});
}
  • 버튼에 onClick 이벤트 요소 추가
<Button onClick={handleLogin}>로그인</Button>
  • handleLogin 함수 추가
    • 로그인 버튼을 눌렀을 때 실행할 함수
    • 해당 함수는 즉시 리턴하는 함수가 아니라고 async 로 알려준다
const handleLogin = async () => {
	try{
      	// api 서버에 로그인 요청을 보내고
		const res = await axios.post("/api/v1/login", state)
        // userName 과 password 가 유효한 정보이면 정상 응답이 되고
        
	}catch(err){
      	// userName 과 password 가 틀리면 401 UNAUTHORIZED 에러 응답을 할 예정
		console.log(err);
	}
}
  • RestUserController 클래스 생성
@RequestMapping("/v1")
@RestController
public class RestUserController {

}
  • dto 패키지에 LoginRequest 클래스 생성
@Data
public class LoginRequest {
	private String userName;
	private String password;
}
  • RestUserController 에 ResponseEntity 메소드 생성
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest request){
	// 입력한 userName 과 password 가 유효한 정보인지 여부
	boolean isValid = request.getUserName().equals("kimgura") && request.getPassword().equals("1234");
		
	// 만일 유효하지 않는 정보라면
	if(!isValid) {
		// 401 을 응답
		ResponseEntity<String> entity = 
				ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("아이디 혹은 비밀번호가 틀려요");
		return entity;
	}
		
	// 가상의 토큰
	String token = "api 자유 이용권, 유효기간:2025.09.16, userName:"+request.getUserName();
		
	// 정상 응답을 하면서 token 을 발급
	ResponseEntity<String> entity = ResponseEntity.ok(token);
	return entity;
}

Swagger UI 로 가서 rest-user-controllerPOST 메소드 실행

{
  "userName": "kimgura",
  "password": "1234"
}

{
  "userName": "kimgura2",
  "password": "1234"
}

  • react 서버에 console 로 테스트
console.log(err);

api 자유 이용권, 유효기간:2025.09.16, userName:kimgura

로그인 에러 처리

  • useState 코드 추가
    • 에러 메세지를 상태값으로 관리
const [errorMsg, setErrorMsg] = useState(null);
  • catch 문 안에 setErrorMsg 코드 추가
setErrorMsg(err.response.data);
  • 에러 메세지의 Alert 코드 추가
    조건부 렌더링
{errorMsg && <Alert variant="danger">{errorMsg}</Alert>}

  • handleLogin 함수 안 try 문 안에 코드 추가
    • 토큰을 localStorage 에 저장
    • localStorage
      • 웹 브라우저가 제공하는 자체 기능
      • 지우지 않는 이상 영구 저장
    • Session Storage
      • 웹 브라우저가 닫기 전까지만 저장
localStorage.fakeToken = res.data;

로그인 & 로그아웃

  • handleLogin 함수 안 try 문 안에 payload & action 코드 추가
    • redux store 에 로그인 정보를 저장한다
const payload = {userName:state.userName, role:"ADMIN"};
const action = {type:"USER_INFO", payload};

payload:payload 대신 payload 만 써도 된다

  • useDispatch 함수 추가
    • 액션을 발행할 함수
const dispatch = useDispatch();
  • handleLogin 함수 안 try 문 안에 dispatch 관련 코드 추가
dispatch(action);
// 로그인 폼을 초기화 하고 숨긴다
setErrorMsg(null);
dispatch({
	type:"LOGIN_MODAL",
	payload:{
		title:"",
		show:false
	}
});
  • main.jsx 에 reducer 함수 안에 else if 문 추가
else if(action.type === "USER_INFO"){ 
	newState = {
		...state,
		userInfo:action.payload
	}
}
  • Home.jsx 에 p & 버튼 요소 추가
<p><strong></strong>님 로그인중</p>
<button>로그아웃</button>
  • useSelector 함수 추가
    • 로그인 정보를 읽어오기 (redux store 로 부터)
const userInfo = useSelector(state => state.userInfo);
  • userInfo 를 사용해 조건부로 출력 관리
{ userInfo && <p>
		<strong>{userInfo.userName}</strong>님 로그인중
		<button>로그아웃</button>
	</p>
}
  • handleLogin & handleLogout 함수 생성하고 그 안으로 코드 이동
const handleLogin = () => {
	const action = {
		type:"LOGIN_MODAL",
		payload:{
			title:"로그인 폼입니다",
			show:true
		}
	};
	// action 발행
	dispatch(action)
};

const handleLogout = () => {
	// 토큰 삭제
	delete localStorage.fakeToken;
	// store 에서 관리되는 userInfo 를 null 로 변경한다
	const action = {type:"USER_INFO", payload:null};
	dispatch(action);
};

localStorage
1. 웹 브라우저가 기본 제공해주는 object
2. 문자열만 저장할 수 있다
3. 한 번 저장하면 지우지 않는 이상 영구 저장된다

  • 버튼에 onClick 이벤트 요소 수정 및 추가
<button onClick={handleLogin}>로그인</button>
{ userInfo && <p>
		<strong>{userInfo.userName}</strong>님 로그인중
		<button onClick={handleLogout}>로그아웃</button>
	</p>
}
  • 로그인 상태일 시 로그인 버튼을 없애고 로그아웃 시 로그인 버튼을 활성화

첫번째 방법

{!userInfo && <button onClick={handleLogin}>로그인</button>}
{userInfo && <p>
		<strong>{userInfo.userName}</strong>님 로그인중
		<button onClick={handleLogout}>로그아웃</button>
	</p>
}

in javascript

  • 해당 if 문 의 의미는 만일 값이 존재한다면 이다

  • null , undefined , "" , 0 , false
    값이 존재하지 않는다 라는 취급을 받는다

if(){
	
}
> !true
false

> !null
true
> !undefined
true
> !""
true
> !false
true
> !0
true

두번째 방법

{userInfo ?
	<p>
		<strong>{userInfo.userName}</strong>님 로그인중
		<button onClick={handleLogout}>로그아웃</button>
	</p>
  :
	<button onClick={handleLogin}>로그인</button>
}

로그인 모달창 닫기

  • LoginModal.jsx 에 Modal 요소에 onHide 이벤트 요소 추가
<Modal onHide={handleHide} show={loginModal.show} size="lg" centered>
  • handleHide 함수 추가
    • Modal 창을 닫기 버튼을 누르면 react bootstrap Modal 에는
      hide 이벤트가 발생
    • 해당 이벤트가 발생했을 때 redux store 의 상태값을 변경해서
      모달창이 숨겨지도록 한다
const handleHide = () => {
	dispatch({
		type:"LOGIN_MODAL",
		payload:{title:"", show:false}
	});
};

Redux Reducer 구조 개선

  • main.jsx 에 handler 객체 생성
    • reducer 함수에서 사용할 handler object
const handlers = {};
  • main.jsx 에 reducer2 함수 생성
    • handler obejct 를 사용하는 새로운 reducer 함수
const reducer2 = (state = initState, action) => {
	// 전달된 action 을 수행할 handler 함수를 얻어낸다
	const handler = handlers[action.type];
	// handler 함수가 존재한다면 handler 함수가 리턴하는 state 를 리턴하고 아니면
	// 원래 state 를 리턴한다
	return handler ? handler(state, action) : state;
};
  • handlers 안에 key 값 추가
    • UPDATE_FORTUNE 이라는 KEY 값으로 function type 이 들어 있다
    • 해당 function 안에서는 실행할 코드는 없다
    • 단순히 새로운 상태값 { } 를 리턴하는 코드만 존재
      ➜ 한줄짜리 함수, 즉 람다 함수 모양으로 변경 가능
    • () => 10 : 10 을 리턴하는 함수
      () => "kim" : "kim" 을 리턴하는 함수
      () => {} :화살표 함수
      () => ({}) : object 를 리턴하는 화살표 함수 (람다 함수)
const handlers = {
  	UPDATE_FORTUNE : (state, action) => ({
    	...state,
    	fortuneToday:action.payload
  	}),
  	LOGIN_MODAL : (state, action) => ({
    	...state,
    	loginModal:action.payload
  	}), 
  	USER_INFO : (state, action) => ({
    	...state,
    	userInfo:action.payload
  	})
};

➜ 해당 object 들을 파일을 생성해 따로 빼두어 object 를 해당 함수 안에 import 하여 사용 가능 = 확장성 고려

  • createStore 안에 reducer2 로 변경
const store = createStore(reducer2);

인증 라우팅 및 보호

  • pages 폴더 안에 MemberOnlyPage.jsx 생성
    • 로그인된 회원만 볼 수 있는 page 라고 가정
function MemberOnlyPage() {

    const userInfo = useSelector(state => state.userInfo);

    return<>
        <h3>로그인 된 회원 전용 페이지</h3>
        <p>
            <strong>{userInfo.userName}</strong>님 반갑습니다
        </p>
        <p>내용1</p>
    </>
}
  • index.jsx 에 MemberOnlyPage 라우트 경로 추가
import MemberOnlyPage from "../pages/MemberOnlyPage";

{path:"/member", element:<MemberOnlyPage/>}
  • Home.jsx 에 링크 추가
<h3>회원 전용 링크</h3>
<NavLink to="/member">이동</NavLink>

  • components 폴더 안에 ProtectedRoute.jsx 생성
function ProtectedRoute({children}) {
    //로그인 여부를 알기위해 userInfo 를 얻어낸다.
    const userInfo = useSelector(state=>state.userInfo);
    //현재 경로를 알아내기 위해
    const location=useLocation();
    //action 을 발행하기 위해
    const dispatch=useDispatch();
    //만일 로그인 상태가 아니라면
    if(!userInfo){
        //원래 가려던 목적지 정보와 query 파라미터 정보를 읽어내서 
        const url = location.pathname + location.search;
        //테스트로 출력해보기
        console.log(url);
        const payload={
            show:true,
            title:"해당 페이지는 로그인이 필요 합니다!"
        }
        //로그인창을 띄우는 action 을 발행하면서 payload 를 전달한다.
        dispatch({type:"LOGIN_MODAL", payload});
        // return null 하면  currentRoute 에 빈 페이지가 출력된다.
        return null;
    }
	 // 여기서 children 은 <ProtectedRoute> 컴포넌트의 자식 컴포넌트를 가리킨다
    return children;
}
  • index.jsx 에 라우트 경로 추가에서 <MemberOnlyPage/><ProtectedRoute> 로 감싸주기
    • 해당 component(<MemberOnlyPage/>) 는
      userInfo 정보가 있어야 활성화 된다 (로그인이 필요하다)
      자동으로 LoginModal 이 출력된다
{path:"/member", element: <ProtectedRoute><MemberOnlyPage/></ProtectedRoute>}

  • LoginModal.jsx 에 try 문 안에 코드 추가
    • 로그인 성공 후에 이동할 url
    • null 일 가능성 존재
 const url = loginModal.url;
  • navigate 함수 추가
    • route 이동을 위한 navigate 함수
const navigate = useNavigate();
  • try 문 안에 if 문 추가
    • 원래 가려던 목적지 정보가 있으면 해당 위치로 이동시킨다
 if(url){
	navigate(url);
}
  • Pages 폴더 안에 Game.jsx 생성
function Game() {
    return <>
        <h1>Game 페이지</h1>
        <p>로그인된 회원만 게임 가능</p>
        <NavLink to="/">인덱스로</NavLink>
    </>
}
  • index.jsx 에 Game 라우트 경로 추가
{path:"/game", element:<ProtectedRoute><Game/></ProtectedRoute>}
  • Home.jsx 에 Game 링크 추가
<NavLink to="/game">Game</NavLink>

Spring Backend 보안 필터 적용

spring09.filter 패키지 생성

  • AuthFilter 클래스 생성
    • OncePerRequestFilter 추상 클래스를 상속 받는다
    • @Component 어노테이션을 붙여서 bean 으로 만든다
      ➜ 이렇게 하면 spring boot 서버로 들어오는 모든 요청이 Controller 로
      도달하기 전에 이 필터를 통과하게 된다
@Component
public class AuthFilter extends OncePerRequestFilter{

	// 필터가 수행될 때 호출되는 메소드
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		// 요청의 흐름 걔속 이어가기
		filterChain.doFilter(request, response);
	}
}
  • Home.jsx 에 버튼 추가 및 onClick 이벤트 부여
<h3>Authorization 을 추가한 요청</h3>
<button onClick={()=>{
	axios.get("/api/v1/members", {
		headers:{
			Authorization:"token..."
        }
	})
	.then(res => console.log(res.data))
	.catch(err => console.log(err))
}}>요청 보내기</button>
  • AuthFilter 에 Header 코드 추가
    • 요청의 Header 에 "Authorization" 이라는 키값으로 전달된 문자열이 있는지 읽어와 본다
String authHeader = request.getHeader("Authorization");
System.out.println(authHeader);

token...
profile
학원 공부 내용 정리

0개의 댓글