React에는 여러가지 상태관리하는 방법들과 라이브러리들이 있다. 대표적으로 useState를 가장 많이 사용할 것이다. 또 라이브러리에는 Redux가 존재한다. 이번에 알아볼 것은 React가 이제 업데이트를 하면서 제공하는 useReducer이다.

 

useState 같은 경우는 기본적인 React의 상태관리이다. 하지만 코드가 지저분해지는 경우가 종종 있을 뿐만 아니라 이 코드를 재사용하기도 어렵다. 그래서 나온 것이 useReducer이다. useReducer 같은 경우에는 코드의 재사용이 가능하다.

어떻게 가능한지 살펴보자.

 

 

일단 이렇게 reducer라는 폴더를 따로 만든 다음에 useReducer와 연결할 js 파일을 하나 만들어 준다.

그리고 그 JS파일에 이제 상태를 변경할 것들을 코드로 작성해 주면 되는데

export default function foodReducer(food, action) {
    switch (action.type) {
        case 'updated': {
            const {prev, current} = action;
            return {
                ...food,
                chefs: food.chefs.map(chef => {
                    if (chef.name === prev) {
                        return {...chef, name: current};
                    }
                    return chef;
                }),
            };
        }

        case 'added': {
            const {name, title} = action;
            return {
                ...food,
                chefs: [...food.chefs, {name, title}],
            };
        }

        case 'deleted': {
            return {
                ...food,
                chefs: food.chefs.filter(chef => chef.name != action.name),
            };
        }
        default: {
            throw Error(`알 수 없는 액션 타입이다.`);
        }
    }
}

 

이 foodReducer라는 함수는 파라미터를 두 개를 받는다. 하나는 food고 다른 하나는 action이다. food는 이제 우리가 연결할 컴포넌트에 있는 상태관리하는 변수가 되겠고 action은 useReducer에서 dispatch라는 함수를 지원하는데 거기에 입력하는 객체 혹은 값이라고 할 수 있다.

 

그래서 이 switch문을 통해서 내가 업데이트를 하고 싶으면 action에서 받아오는 type에 따라 상태관리에 관한 코드들을 입력해주면 된다.

 

import React, {useReducer} from 'react';
import foodReducer from './reducer/food_reducer';

export default function AppFoodReducer() {
    const [food, dispatch] = useReducer(foodReducer, initialFood);
    const handleUpdate = () => {
        const prev = prompt(`어떤 쉐프의 이름을 바꾸고 싶은가요?`);
        const current = prompt(`쉐프의 이름을 무엇으로 바꾸고 싶은가요?`);
        dispatch({type: 'updated', prev, current});
    };

    const handleAdd = () => {
        const name = prompt(`쉐프의 이름을 입력해주세요`);
        const title = prompt(`쉐프의 타이틀을 입력해주세요`);

        dispatch({type: 'added', name, title});
    };

    const handleDelete = () => {
        const name = prompt(`누구의 이름을 삭제하고 싶은가요?`);
        dispatch({type: 'deleted', name});
    };
    return (
        <div>
            <h1>
                Best Pasta의 {food.name}는 별점 {food.rating}
            </h1>
            <p>{food.name}의 가게의 주방장은 : </p>
            <ul>
                {food.chefs.map((chef, index) => (
                    <li key={index}>
                        {' '}
                        {chef.name} ({chef.title})
                    </li>
                ))}
            </ul>
            <button onClick={handleUpdate}>주방장 이름 바꾸기</button>
            <button onClick={handleAdd}>주방장 추가하기</button>
            <button onClick={handleDelete}>주방장 삭제</button>
        </div>
    );
}

const initialFood = {
    name: '스파게티',
    rating: '★★★★★',
    chefs: [
        {name: 'jay', title: 'leader chef'},
        {name: 'bread', title: '견습생 쉐프'},
    ],
};

 

food_reducer에 연결한 AppFoodReducer를 살펴보면 useReducer에 우리가 만든 food_reducer 함수를 전달하고 최초의 상태관리 값도 같이 전달하면 된다. 이후에 food라는 변수와 dispatch 함수를 지원하는데 dispatch 함수를 통해서 원하는 type과 값들을 객체로 전달해주면 dispatch가 food_reducer에 action이라는 파라미터로 넘겨준다. 

 

 

내가 이걸 공부하면서 느낀건 바로 useReducer로 적용해서 상태관리를 하는 것은 매우 힘들다고 생각이 든다. 일단은 먼저 useState를 통해서 상태관리를 하는데 너무 코드가 지저분해지거나 좀 반복적인 코드가 보인다? 혹은 이걸 다른 컴포넌트에도 재사용할 수 있겠는데 라고 느낀다면 그때 useReducer를 사용하는 것이 좋다고 생각한다. 

'React' 카테고리의 다른 글

Redux에 대해서  (0) 2024.06.15
ContextAPI 이용해서 다크모드 사용하기(React)  (2) 2024.01.07
Component 재사용하기 (React)  (0) 2023.12.31
React 상태관리 - UseImmer  (0) 2023.12.27
마우스 따라가는 원형 만들기(React)  (0) 2023.12.24

리액트를 공부하다가 useState를 객체 단위로 관리해서 사용할 때 아주 중요하고 유용하다는 걸 깨달았다. 

그래서 이 스프레드 연산자에 대한 이해가 좀 필요할 거 같다.

 

수많은 방법들의 응용이 있겠지만 나는 간단하게 이게 리액트에서 왜 중요한지 좀 보고자 한다.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body></body>
    <script>
        const test = {
            name: 'jay',
            age: 27,
            job: {title: 'developer', level: 'junior'},
        };

        let practice = {...test, age: 33};
        console.log(test);
        console.log(practice);
    </script>
</html>

 

스프레드 연산자는 배열도 가능하고 객체도 가능하다. 나는 객체로 연습을 해봤다.

스프레드 연산자는 { ...test }를 하면 변수에 이 test의 값이 펼쳐져서 복사가 된다. 

뒤에 쉼표(,)를 붙이고 값이 넣으면 해당 값이 덮어 씌여져서 복사가 된다. 

 

이게 그렇다면 왜 리액트에서 중요한가?

 

import React from 'react';
import {useState} from 'react';

export default function AppPractice() {
    const [practice, setPractice] = useState({
        name: 'jay',
        age: 27,
        job: {title: 'developer', level: 'junior'},
    });
    return (
        <div>
            <h1>이름 : {practice.name}</h1>
            <h1>나이 : {practice.age}</h1>
            <h1>직업 : {practice.job.title}</h1>
            <h1>레벨 : {practice.job.level}</h1>

            <button
                onClick={() => {
                    const name = prompt('이름을 입력해주세요');
                    setPractice(prev => ({
                        ...prev,
                        name: name,
                    }));
                }}
            >
                이름변경
            </button>
        </div>
    );
}

 

리액트에서 객체 단위로 상태를 관리할 때 이 스프레드 연산자가 없으면 진행하기 힘들 정도로 중요하다고 생각한다.

이전 객체의 값을 나열해서 값을 덮어 씌워서 상태를 업데이트 해야기 때문에 스프레드 연산자는 매우 중요하며 객체 안의 객체의 상태도 업데이트 할 때 매우 유용하며 필수적이다.

 

import React from 'react';
import {useState} from 'react';

export default function AppPractice() {
    const [practice, setPractice] = useState({
        name: 'jay',
        age: 27,
        job: {title: 'developer', level: 'junior'},
    });
    return (
        <div>
            <h1>이름 : {practice.name}</h1>
            <h1>나이 : {practice.age}</h1>
            <h1>직업 : {practice.job.title}</h1>
            <h1>레벨 : {practice.job.level}</h1>

            <button
                onClick={() => {
                    const name = prompt('이름을 입력해주세요');
                    setPractice(prev => ({
                        ...prev,
                        name: name,
                    }));
                }}
            >
                이름변경
            </button>
            <button
                onClick={() => {
                    const level = prompt('레벨을 입력해주세요');
                    setPractice(prev => ({
                        ...prev,
                        job: {...practice.job, level: level},
                    }));
                }}
            >
                레벨변경
            </button>
        </div>
    );
}

마우스를 따라가는 원형을 한 번 만들어보자. 

 

일단 Component를 만들어야겠지.

 

import React, {useState} from 'react';
import './AppXY.css';

export default function AppPractice() {
    return (
        <div class="background">
            <divclassName="test"></div>
        </div>
    );
}

 

간단한 component를 한 번 만들어봤고 

* {
    background-color: beige;
    width: 100%;
    height: 100%;
}

.background {
    width: 100%;
    height: 100%;
}

.test {
    width: 2rem;
    height: 2rem;
    background-color: red;
    border-radius: 50%;
}

 

CSS도 간단하게 적용해 보았다.

 

그러면 이제 따라게 만들어야 하는데 어떻게 할 수 있을까???

일단은 먼저 마우스가 이동하는 좌표값을 알아야 한다. 즉 마우스가 이동할 때의 event를 가져와서 거기에 있는 event.clientX값과 event.clientY값을 가져와서 이제 .test 클래스의 도형에 transform해서 x좌표와 y좌표를 변화시켜주면 되지 않을까라고 생각해보고 접근해보자.

 

React에 Javascript 처럼 event를 등록하고 싶다면 등록하고 싶은 tag에 on~~라는 이벤트들을 등록하면 된다. 

Click에 관한 event를 등록하고 싶다. onClick

어떤 변화에 대한 event를 등록하고 싶다. onChange 등등

 

이번에 마우스를 포인터에 대한 이동 event를 등록할꺼기 때문에 우리는 onPointerMove라는 이벤트를 등록해서 사용하자.

 

import React, {useState} from 'react';
import './AppXY.css';

export default function AppPractice() {
    return (
        <div 
        	class="background"
        	onPointerMove={event => {
            	console.log(event.clientX, event.clientY);
        >
            <divclassName="test"></div>
        </div>
    );
}

 

 

일단 console로 한 번 찍어보면 이런 식으로 이제 좌표값이 찍힌다. 

이 좌표값을 이제 useState를 활용하여 값을 담고 값이 변화할 때마다 set함수를 이용해서 변경된 값을 업데이트를 해줘서

이제 .test 클래스 tag에 style을 주면 될 것 같다.

 

import React, {useState} from 'react';
import './AppXY.css';

export default function AppPractice() {
	const [x, setX] = useState(0);
    const [y, setY] = useState(0);
    
    return (
        <div 
        	class="background"
        	onPointerMove={event => {
            	console.log(event.clientX, event.clientY);
                setX(event.clientX);
                setY(event.clientY);
            }}
        >
            <div 
            className="test"
            style={{
            	transform: `translate(${position.x}px, ${position.y}px)`
            }}
            ></div>
        </div>
    );
}

 

 

 

잘 된 것을 볼 수 있다.

'React' 카테고리의 다른 글

Redux에 대해서  (0) 2024.06.15
ContextAPI 이용해서 다크모드 사용하기(React)  (2) 2024.01.07
Component 재사용하기 (React)  (0) 2023.12.31
React 상태관리 - UseImmer  (0) 2023.12.27
React 상태관리 - useReducer  (0) 2023.12.26

배열 안의 데이터를 정렬할 때 우리는 sort함수를 많이 쓴다.

예를 들어 [3, 2, 4, 6, 7]의 배열을 오름차순 하고 싶어서 .sort( )를 사용한다. 

function solution(arr1) {
	let answer;

    answer = arr1.sort();

    return answer;
}

let arr1 = [3, 2, 4, 6, 7];

console.log(solution(arr1));

 

결과값을 보면 잘나온다. 그런데 분명 주의해야 할 것이 있다. 

이 배열에 두자리 수나 세자리 수가 나온다고 해보자

 

function solution(arr1) {
	let answer;

    answer = arr1.sort();

    return answer;
}

let arr1 = [3, 200, 40, 6, 7];

console.log(solution(arr1));

결과값이 이렇게 나와버린다...

이게 왜 이런것일까????

 

알아본 결과 그냥 Array.sort( )를 해버리면 안의 데이터가 문자로 바뀌면서 사전식 정렬로 바뀐다는 것이다. 

function solution(arr1) {
	let answer;

    answer = arr1.sort();

    return answer;
}

let arr1 = [3, 200, '님', 40, 6, 7, '김', '밈', 100];

console.log(solution(arr1));

결과는 이렇다. 그렇다면 숫자를 어떻게 잘 오름차순 혹은 내림차순으로 해야할까?

 

sort함수를 잘 보면 파라미터를 받을 수 있고 이 파라미터들을 서로 뺐을 때 음수면 오름차순 양수면 내림차순으로 할 수 있다.

<script>
	function solution(arr1) {
	let answer;

	answer = arr1.sort((a, b) => a - b); // 오름차순

	return answer;
	}

	let arr1 = [3, 200, 40, 6, 7, 100];

	console.log(solution(arr1));
</script>

오름차순 결과값

<script>
	function solution(arr1) {
	let answer;

	answer = arr1.sort((a, b) => b - a); // 내림차순

	return answer;
	}

	let arr1 = [3, 200, 40, 6, 7, 100];

	console.log(solution(arr1));
</script>

내림차순 결과값

정리하자면 그냥 Array.sort( )를 하면 정렬은 될 지 모르지만 이게 사전값 순으로 정렬된다는 점. 그리고 안의 데이터가 문자로 바뀐다는 점.

그래서 숫자를 오름차순 내림차순 하고 싶다면 sort( ) 함수에 파라미터 a, b를 가져와서 음수 혹은 양수의 리턴값을 줘야 한다는 점을 기억하자!

결과부터 보자면 딱 이런 느낌을 내고 싶었다. 하지만 이렇게 까지 만들기에 좀 오래걸렸다. 이제 그 이유들을 한 번 살펴보자.

내가 지금까지 공부한 validation 체크는 크게 두가지 방법이 있다.

 

1. Form에서 바로 데이터를 보낸 다음에 BindResult를 이용해 값이 컨트롤러에 도착하기 전에 filter? 에서 먼저 체크를 한 다음에 Validation Check한 값을 다시 Redirect해서 페이지에 메시지를 띄우는 방법

2. Form에 적은 데이터를 $.ajax를 이용해서 값을 보낸 다음에 Controller에서 BindingResult의 filedError 값을 Map에 저장한 후에 badRequest.body를 이용해서 클라이언트에 보내는 방법.

 

첫 번째 방법은 이미 한 번 해봤는데 나중에 다시 그 부분을 수정할 일이 있을 때 그때 블로그에 작성하도록 하고 이번에는 두 번째 방법을 이용해서 체크해보고자 한다.

 

signup_button.addEventListener('click', () => {

	let data = {
		username : username.value,
		userId : userId.value,
		password : password.value,
		address : address.value
	}
	
	$.ajax({
		async: false,
		type: "POST",
		url: "/auth/signup",
		data: JSON.stringify(data),
		contentType: "application/json; charset=utf-8",
		dataType: "json",
	})
	.done((response) => {
		console.log(response);
		location.href = "/"
	})
	.fail((error) => {
		let errorMessages = error.responseJSON;
		console.log(errorMessages);
		showErrorMessage(errorMessages);

	})
});

이렇게 이제 서버에 요청을 보냈다. 서버는 Controller 단에서 이걸 받고 이제 유효한지를 체크하겠지.

객체는

package com.project.blueshirt.model;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Member {
	@NotBlank
	private String username;
	
	@NotBlank
	private String userId;
	
	@NotBlank
	@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[-~!@#$%^&*_+=])[a-zA-Z\\d-~!@#$%^&*_+=]{8,16}$", 
			message = "비밀번호 최소 8글자 이상, 특수문자 넣어서 써주세요")
	private String password;
	private String address;
	private String role;
}

이렇게 간단하게 만들었고 @NotBlank를 하면 기본 출력값이 있고 message = ""를 적으면 원하는 메시지를 보낼 수 있다.

일단 나는 username, userId, password만 validation check를 하게 했다.

 

@PostMapping("/auth/signup")
	public ResponseEntity<?> signup(@Valid @RequestBody Member member , BindingResult bindingResult) {
		
		boolean status = false;
		
		if(bindingResult.hasErrors()) {
			// validation 체크
			
			Map<String, String> errorMessage = new HashMap<String, String>();
			
			bindingResult.getFieldErrors().forEach(error -> {
				errorMessage.put(error.getField(), error.getDefaultMessage());
			});
			
			
			return ResponseEntity.badRequest().body(errorMessage);
			
		}
		
		try {
			status = memberService.signUpMember(member);
		} catch (Exception e) {
			e.printStackTrace();
			return ResponseEntity.internalServerError().body(status);
		}
		
		return ResponseEntity.ok().body(status);

Controller단에서 이제 error가 있으면 그걸 Map에 저장한 후에 다시 클라이언트로 보낸다.

 

해당 값이 이렇게 오는 걸 볼 수 있다.

그런데 문제는 이렇게 온 값을 어떻게 화면에 보여줄 것인가가 문제였다.

처음 시도 했을 때는 어? 이거 이렇게 하면 되겠는데 해서 해보았다.

 

와 그런데 결과가 이렇게 나오드라...

회원가입을 버튼을 누른 수 만큼 계속해서 생성되는데 이걸 도저히 해결할 방법이 보이지 않았다. 

이걸 구현한 방법은

function showErrorMessage(errorMessages) {
	const usernameData = document.querySelector('.usernameData')
	const userIdData = document.querySelector('.userIdData')
	const passwordData = document.querySelector('.passwordData')
	
	let errorMessage = document.createElement("div");
	let spanTag = document.createElement('span');
	
	errorMessage.append(spanTag);
	
	if(errorMessages.username) {
		spanTag.append(errorMessages.username);
		usernameData.appendChild(errorMessage);
	
	} else {
		
	}
	if(errorMessages.password) {
		password.after(errorMessages.password);
	} else {
		password.after("")
	}
	if(errorMessages.userId) {
		userId.after(errorMessages.userId);
	} else {
		userId.after("")
	}
}

일단 생각은 이렇게 했다. div Element를 생성한 후에 ErrorMessage를 그 안에 집어 넣고 username 에러이면 usernameData 태그 뒤에 insert하면 되지 않겠나? 라고 생각해서 구현했는데 저렇게 구현되서

어떻게 해야하지??를 진짜 하루종일 생각했다.

 

근데 해결해서 너무 기쁘다.

function showErrorMessage(errorMessages) {
	
	// 이런 방법을 써보자 display를 none으로 해놓고 에러가 있으면
	// 보여주는 방식을 사용하면 되는 거 아닌가?
	
	
	let errorMessageUsername = document.querySelector('.errorMessage.username');;
	let errorMessageUserId = document.querySelector('.errorMessage.userId');;
	let errorMessagePassword = document.querySelector('.errorMessage.password');;
	
	if(errorMessages.username) {		
		errorMessageUsername.innerText = errorMessages.username;
		errorMessageUsername.style.visibility = 'visible';
	} else {
		errorMessageUsername.style.visibility = 'hidden';
	}
	
	if(errorMessages.userId) {
		errorMessageUserId.innerText = errorMessages.userId;
		errorMessageUserId.style.visibility = 'visible';
	} else {
		errorMessageUserId.style.visibility = 'hidden';
	}
	
	if(errorMessages.password) {
		errorMessagePassword.innerText = errorMessages.password;
		errorMessagePassword.style.visibility = 'visible';
	} else {
		errorMessagePassword.style.visibility = 'hidden';
	}
    }

css visibility를 이용해서 값을 보여주고 안보여주고로 설정하는 방법을 택했다. 미리 div 태그를 하나 만들어 놓고 이거를 보이지 않게 설정했다가 error값이 있으면 이 에러값을 div에 넣고 보여주는 방식으로 하니 계속 중복해서 값이 추가되는 상황이 방지 됐다. 그리고 꾸미기도 편하고.

BlueShirtProject Header

위의 빨간 부분을 로그인 시 바꾸고 싶다. 이름이 나오고, 로그아웃 이런 형태로 변경하고 싶다.

 

전에 같은 경우는 어떻게 했냐면 Controller에서 로그인 인증과 함께 회원정보를 자바스크립트에 보내서 자바스크립트를 활용해서 바꾸도록 만들었다. 뭐 그렇게 해도 되지만 조금 개인적으로 생각했을 때 그건 옳은 방법이 아니었다.

분명 뭔가 좀 더 세련된 방법이 있을것이다라고 생각했다. 그때 발견한 것이 쿠키와 세션을 이용하는 방법이었다.

 

그러나 나는 지금 SpringSecurity를 이용하고 있다. 뭔가 이 SpringSecurity를 이용해서 좀 더 간편한 방법이 분명히 있을 것이다라고 생각하고 구글 검색을 통해서 한 번 알아보았다.

 

일단 검색을 해보니까 SpringSecurity로 로그인시 자동으로 세션과 쿠기가 만들어져서 관리 된다는 것이다. 이걸 이용하고 또 로그아웃이 이걸 제거해주면 아주 완벽하게 로그인, 로그아웃이 될 것이다. 그리고 저 윗부분에 대한 변경도 가능할 것이다. 

 

@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		
		http
			.csrf().disable()
			.authorizeRequests()
				.antMatchers("/", "/signin/**", "/signup", 
							"/auth/**", "/chungCompany", "/estimates/**",
							"/items/**" ,"/review/**" ,"/admin/**", "/api/**", "/image/**", "/order/**" )
				.permitAll()
				.anyRequest()
				.authenticated()
			.and()
				.formLogin()
//				.usernameParameter("userId")
				.loginPage("/signin")
				.loginProcessingUrl("/auth/loginProc")
				.failureHandler(customFailureHandler)
				.defaultSuccessUrl("/")
			.and()
				.logout()
				.logoutSuccessUrl("/")
				.invalidateHttpSession(true)
				.deleteCookies("JSESSIONID");
		
		return http.build();
	}

SpringConfig에서 logout( ) 부분을 추가했다. invalidateHttpSession(true)로 설정해 놓으면 로그아웃시 session을 종료하고 deleteCookies를 통해서 쿠키도 제거할 수 있다고 한다.

 

그 다음에 이 사람이 로그인 했는지 안했는지를 확인한 다음에 로그인 했으면 상단 메뉴를 바꾸고 아니면 안바꾸는 식으로 가야하는데 이 로그인 했는지 안했는지에 대한 체크가 쉽지 않았다.

 

예전에는 login이라는 기능을 Controller에서 만들어서 구현해서 썼는데 내가 SpringSecurity를 이용하고 있는데 분명히 연동되는 뭐 그런 기능이 있을 것이다라고 생각이 들었다. 그래서 검색을 해보니까. thymeleaf에 아주 기가막힌 기능이 있더라.

 

sec:authorize="!isAuthenticated()"

 

이게 타임리프와 스프링과 강력하게 연동되어있기 때문에 가능한 기능이라고 봤다. 이걸 이용해서 header 부분을

<ul sec:authorize="!isAuthenticated()" class="login__menu">
	<li class="login" onclick="location.href='/signin'">로그인</li>
    <li class="signup" onclick="location.href='/signup'">회원가입</li>
</ul>
                 
<ul sec:authorize="isAuthenticated()"  class="login__menu">
    <li class="user_name" th:text="|${#authentication.principal.username}님 환영합니다.|"></li>
    <li class="user_logout" onclick="location.href='/logout'">로그아웃</li>
</ul>

이런 식으로 구현했다.

 

이름 뒤에 꼭 "~님 환영합니다."도 넣고 싶어서 검색해서 찾아보니 authentication.principal.username을 이용하면 이름을 가져올 수 있다고 하여

th:text="|${#authentication.principal.username}님 환영합니다.|" 이렇게 구현했다.

 

home 부분header
견적문의 페이지 header

home 뿐만 아니라 다른 페이지에서도 잘 바껴있는 것을 볼 수있다.

처음에 아이디 패스워드 체크를 구현하려고 했는데 이상하게 잘 되지 않았다. 

'아니 왜 이게 계속 로그인이 되는 거지? 나는 로그인 하는 기능을 구현한 적이 없는데...?' 보니까 내가 Spring Security를 이용해서 로그인을 진행했다. 그러니 일반적으로 아이디 패스워드 체크를 구현하려고 하니 계속 진행이 되지 않았다. 

그럼 어떻게 해야할까? 고민을 하다가 이건 내가 절대 생각만으로는 안되는 부분이구나라고 느껴서 바로 구글에 검색해보니 failureHandler를 등록해서 진행하는 방법이 있더라.

 

@Configuration
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.csrf().disable()
			.authorizeRequests()
				.antMatchers("/", "/signin/**", "/signup", 
							"/auth/**", "/chungCompany", "/estimates/**",
							"/items/**" ,"/review/**" ,"/admin/**", "/api/**", "/image/**", "/order/**" )
				.permitAll()
				.anyRequest()
				.authenticated()
			.and()
				.formLogin()
//				.usernameParameter("userId")
				.loginPage("/signin")
				.loginProcessingUrl("/auth/loginProc")
				.failureHandler(customFailureHandler)
				.defaultSuccessUrl("/");
		
		return http.build();
	}
	
}

failureHandler부분에 customFailureHandler를 등록해주면 되더라.

 

package com.project.blueshirt.config.handler;

import java.io.IOException;
import java.net.URLEncoder;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler{

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		
		String errorMessage = "";
		if(exception instanceof BadCredentialsException) {
			errorMessage = "아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해 주세요";
		} else if(exception instanceof UsernameNotFoundException) {
			errorMessage = "계정이 존재하지 않습니다. 회원가입 진행 후 로그인 해주세요";
		}
		
		errorMessage = URLEncoder.encode(errorMessage, "UTF-8");
		
		setDefaultFailureUrl("/signin?error=true&exception="+ errorMessage);
		
		super.onAuthenticationFailure(request, response, exception);
	}
	
}

PageController

@Slf4j
@Controller
@RequiredArgsConstructor
public class MemberPageController {
	@GetMapping("/signin") 
	public String SignInPage(@RequestParam(value = "error", required = false) String error,
							@RequestParam(value = "exception", required = false) String exception,
							Model model) {
		
		model.addAttribute("error", error);
		model.addAttribute("exception", exception);
		
		return "page/member/signin";
	}
 }

결과값은

아이디를 입력하지 않거나, 패스워드를 입력하지 않았을때, 혹은 아이디나 패스워드를 틀리게 입력했을 때 해당 문구가 나오도록 구현했다. 생각해보니까  조금 더 유저를 생각해서 만든다면 아이디 패스워드를 입력하지 않았을 때는 아이디, 패스워드를 입력해주세요라고 나오는 게 맞다고 본다. 이 부분은 또 좀 더 프로젝트를 다듬으면서 고쳐나가도록 하자. 

 

※ 문득 드는 생각.

Controller에 보면 @RequestParam을 쓴다. 왜 @RequestParam을 쓸까?라고 문득 블로그를 작성하면서 생각을 해본다. 보면  

이렇게 URL에 Error메시지가 등록되는 걸 볼 수 있다. 그렇다면 이 SpringSecurity에 등록한 customFailureHandler는 Controller에 도착하기 전에 Servelt? 단계라고 하나 filter? 이 부분에서 먼저 체크가 들어간다는 소리가 된다. 

 

이런식으로 말이다. 정확한 건 좀 더 공부가 필요해 보이나 내가 생각해봤을 때는 이렇게 작동하는 것이 맞다고 본다. 근데 조금 번거로워 보이기도 하다. 굳이 filter에서 다시 클라이언트로 보내서 다시 controller로 요청을 보내는 것이 맞는가? 그냥 Controller에서 입력값 에러를 감지해서 처리하는 게 더 효율적이지 않나라고 생각해본다.

 

혹시 고수님들이 이걸 보시면 따끔한 지적 부탁드립니다~

내 혼자 진행하는 사이드 프로젝트가 하나 있다. BlueShirt Project라고. 친구가 운영하는 청소회사 사이트를 한 번 만들어 보는 것이다. 목표는 생각 해보는 기능 하나씩 다 넣어보면서 나중에는 진짜 도메인을 사서 한 번 배포하는 것이다. 

아무튼 그래서 이걸로 이 기능 저 기능 생각해보면서 하나씩 만들어 가보고 그에 대한 생각들과 시행착오들을 기록해 나갈 것이다.

 

여기서 사용하는 스택들을 정리해보자.

- SpringBoot

- Thymeleaf

- MariaDB -> MySQL

 

이렇게 간단하게 정리할 수 있겠다.

그렇다면 간단한 아이디 찾기 기능 시작해보자~

 

UI가 너무 단순하지만. 나중에 꼭 꾸밀 것이다.

다른 웹사이트 페이지보면 아이디 찾기에 이름, 휴대전화 혹은 이메일을 입력해서 찾곤 하지만 그건 나중에 점차 발전시키기로 하고 이번에는 그냥 이름만으로 어떻게 아이디를 찾을 수 있을지 보자.

 

일단 생각을 먼저 해보자. 이미 회원가입은 되어있고 이름을 입력하고 아이디 찾기 버튼을 누르면 이 이름 데이터가 서버로 전송될 것이다. 그러면 이름 데이터를 가지고 DB를 조회해서 있으면은 아이디를 가져오고 없으면 아이디가 없다고 표시해주면 끝일 것이다. 

 

그림으로 그려보자.

 

이런 흐름이고 이걸 이제 코드로 나타내면 된다. 

'use strict';
const findIdButton = document.querySelector('.findId__button');
const findIdContainer = document.querySelector('.findId__container');
let username = document.querySelector('#username');

findIdButton.addEventListener('click', (event) => {
	event.preventDefault();
	let userNameData = username.value;
	console.log(userNameData);
	
	$.ajax({
		type: "POST",
		url: "/auth/findId",
		data: userNameData,
		contentType: "text/plain; charset=utf-8",
		dataType: "text"
	})
	.done((response) => {
		console.log(response);
		showingFindId(response);
	})
	.fail((error) => {
		console.log(error);
	})
})

function showingFindId(data) {
	if(data === null || data.length == 0) {	
		console.log(typeof(data));	
		findIdContainer.innerHTML = `
			<span> 입력한 이름에 대한 아이디가 없습니다 </span>
		`
	} else {
		findIdContainer.innerHTML = `
		<span> 아이디는 ${data} 입니다. </span>
	`	
	}
}

일단 자바스크립트로 서버에 데이터를 보내는 것을 구현했다. $.ajax를 사용했다.

 

Controller

@Slf4j
@RestController
@RequiredArgsConstructor
public class MemberController {
	private final MemberService memberService;
    /*
	 * 로그인 페이지에서 아이디 찾기 기능
	 */
	@PostMapping("/auth/findId")
	public ResponseEntity<?> findId(@RequestBody(required = false) String userNameData) {
		
		log.info("username = {}", userNameData);
		String findId;
		
		try {
			findId = memberService.findId(userNameData);
			log.info("findId = {}", findId);
		} catch (Exception e) {
			e.printStackTrace();
			return ResponseEntity.internalServerError().body(null);
		}
		
		return ResponseEntity.ok().body(findId);
	}

}

 

Service

@Slf4j
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{
	@Override
	public String findId(String username) throws Exception {
		
		String userId = memberRepository.findId(username);
		
		if(userId == null) {
			return null;
		} else {
			return userId;
		}
	}
}

MemberService는 인터페이스로 간단하게 만든 것이고 이걸 MemberServiceImpl을 통해서 구현했다.

 

Repository

@Repository
@Mapper
public interface MemberRepository {
	public String findId(String username) throws Exception;
}

 

MyBatis

<select id="findId" parameterType="String" resultType="String">
	select
		userId
	from
		member
	where
		username = #{username}
	
</select>

 

결과값

아이디가 존재할 시
아이디가 존재하지 않을시.

 

여기서 주의할 것이 하나 있다.

이제 자바스크립트를 이용해서 $.ajax로 데이터를 보낼 때 저기 입력란에 아무것도 입력하지 않고 아이디 찾기 버튼을 누르면 controller에서 받는 데이터가 없기 때문에 에러가 생긴다. 이걸 어떻게 해결할 지 몰라서 진짜 한 2일동안 생각하면서 식겁했는데 해결은 간단했다.

 

public ResponseEntity<?> findId(@RequestBody(required = false) String userNameData)

 

Controller에서 @RequestBody를 이용해서 JSON 데이터를 받을때 required = false를 해주면 알아서 빈 데이터 처리해 주는 것이다.

많이 보고 사용하지만 항상 헷갈리는 Break문과 Continue문. 쉽지만 제대로 정리안해서 항상 헷갈리는 것이 문제다. 이번에 제대로 한 번 정리해보고 또 내가 예전에 어느 회사에 면접을 보러 갔을 때 Java 문제를 푼 적이 있는데 그 때 생소했던 것들과 연관 된 내용들이 있어 한 번 정리해보고자 한다.

 

일단 Break 문은 말 그대로 만났을 때 곧바로 그 문장을 멈추는 것이다. 

class Break {
	public static void main(String[] args) { 
		
		// break - 자신이 포함된 하나의 반복문을 벗어난다.
		
		int sum = 0;
		int i   = 0;

		while(true) {
			if(sum > 100)
				break;
			++i;
			sum += i;
		} // end of while

		System.out.println("i=" + i);
		System.out.println("sum=" + sum);
	}   
}

자 위의 코드를 보면 while 반복문이 사용 되는데 조건에 True가 있는 걸 볼 수 있다. 이는 무한 반복문이 작동된다.

안에 If문이 있고 sum이 100을 넘으면 Break가 작동되는 코드다. 즉 break를 만나면 무한 반복문이 끝나면서 밑의 

println 구문이 동작된다. 결과를 한 번 보자.

위의 break문의 결과

다음은 Continue문을 알아보자.

Break문은 딱 멈추고 반복문이라던지 조건문을 빠져오는 것이라면 Continue문은 반복문에서 많이 사용되며

자신이 포함된 반복문의 끝으로 이동한다. 그러곤 다시 반복으로 넘어간다. 이는 전체 반복 중에서 특정 조건시 반복을 건너뛸 때 유용하다.

 

class Continue {
	public static void main(String[] args) {
		
		// continue문
		// 자신이 포함된 반복문의 끝으로 이동 - 다시 반복으로 넘어감
		// 전체 반복 중에서 특정 조건시 반복을 건너뛸 때 유용.
		
		for(int i=0;i <= 10;i++) {
			if (i%3==0)
				continue;
			System.out.println(i);
		}
	}
}

위의 코드를 보면 반복문 안에 If 조건문이 있는데 지금 i % 3 == 0 이면 continue를 만나게 되어있다. 즉 3의 배수인 수를 만나면 continue를 만나서 반복문의 끝으로 이동한다. 그렇게 되면 i가 3의 배수인 수일 때는 println 문을 실행하지 않고 넘어간다는 뜻이다.

결과를 한 번 보자

continue문 실행 결과

예상 했던 대로 3의 배수는 출력되지 않았다.

이제 이 두 개를 응용한 코드를 한 번 살펴보자.

import java.util.*;

class BreakContinue{
	public static void main(String[] args) { 
		int menu = 0, num  = 0;
		Scanner scanner = new Scanner(System.in);

		outer:   // while문에 outer라는 이름을 붙인다.
		while(true) {
			System.out.println("(1) square");
			System.out.println("(2) square root");
			System.out.println("(3) log");
			System.out.print("원하는 메뉴(1~3)를 선택하세요.(종료:0)>");

			String tmp = scanner.nextLine(); // 화면에서 입력받은 내용을 tmp에 저장
			menu = Integer.parseInt(tmp);    // 입력받은 문자열(tmp)을 숫자로 변환

			if(menu==0) {  
				System.out.println("프로그램을 종료합니다..");
				break;
			} else if (!(1<= menu && menu <= 3)) {
				System.out.println("메뉴를 잘못 선택하셨습니다.(종료는0)");
				continue;		
			}

			for(;;) {  //무한 반복문
		      System.out.print("계산할 값을 입력하세요.(계산 종료:0, 전체 종료: 99)>");
				tmp = scanner.nextLine();    // 화면에서 입력받은 내용을 tmp에 저장
				num = Integer.parseInt(tmp); // 입력받은 문자열(tmp)을 숫자로 변환

				if(num==0)  
					break;        // 계산 종료. for문을 벗어난다.

				if(num==99) 
					break outer;  // 전체 종료. for문과 while문을 모두 벗어난다.

				switch(menu) {
					case 1: 
						System.out.println("result="+ num*num);		
						break;
					case 2: 
						System.out.println("result="+ Math.sqrt(num)); 
						break;
					case 3: 
						System.out.println("result="+ Math.log(num));  
						break;
				} 
			} // for(;;)
		} // while의 끝
	} // main의 끝
}

자 여기서 outer: while(true)하는 구문이 있다. 이는 나중에 break문을 사용할 때 전체 반복문을 빠져나올 때 사용된다. 

이 코드를 보면 생각나는 것이 바로 앞에서 말했던 어떤 회사의 자바 문제를 풀 때 봤던 문제가 생각난다. 

나는 이 코드를 정말 자바 공부를 하면서 그 때 처음봤다. 그때는 이걸 잘못된 코드라고 생각하고 답을 그냥 RuntimeException이 난다라고 결론 내렸었는데 지금에 와서 이게 전체 반복문을 빠져나올 때 사용하는 구문이라는 걸 이제야 알았다.

 

또 하나. 그 회사 시험에서 봤었는데 잘못됐다라고 생각한 것이 하나 더 있다.

바로 for(; ;) 구문이다. 내가 처음 이걸 봤을 때 아니 for 문에 조건 식이 없어?? 이건 완전 잘못된 거 아니야??

이것도 바로 RuntimeException이다 라고 생각했는데 오늘 제대로 배웠다.

이건 while(true)와 마찬가지로 무한반복문을 사용할 때 사용하는 것이다. 즉 for문으로 무한반복을 사용하고 싶으면

for(; ;)이렇ㅎ게 하거나 for(;true;)이렇게 하면 된다. 

 

지금 생각하면 그 회사문제는 제대로 된 것이었는데 나는 그때 아니 뭐 이런 문제가 다 있어?? 완전 옛날 자바 아니야? 하고 회사를 이상하게 생각했었다. 정말 그 회사에게 미안하고 나도 반성한다....

물론 그 회사엔 입사하지 못했지만 그 땐 이상한 회사라고만 생각했다...

 

정말 반성하고 제대로 공부하고 기록하고 적용하자!!!

'Java' 카테고리의 다른 글

while문  (0) 2023.08.17
Switch문  (0) 2023.08.17
형변환(Java)  (0) 2023.08.06
Printf(Java)  (0) 2023.08.02
Static에 대하여 - JAVA  (0) 2023.02.15

Switch문 때와 비슷한 경우의 while문이다. Switch문 때도 이런 말을 했었다.

'IF~ELSE 구문은 정말 많이 써도 Switch문은 잘 안써서 낯설다고' While문도 마찬가지다

반복문을 사용할 때 거의 For문만 사용했지 While은 좀 낯설다. 여기에 더해 do while은 정말 더 낯설다. 

예전에 백준 문제 풀 때 while문을 정말 많이 활용했었는데 그래도 한 번 더 정리 해보도록 하자.

 

import java.util.*;

class While {
	public static void main(String[] args) { 
		int num = 0, sum = 0;
		System.out.print("숫자를 입력하세요.(예:12345)>");

		Scanner scanner = new Scanner(System.in);
		String tmp = scanner.nextLine();  // 화면을 통해 입력받은 내용을 tmp에 저장
		num = Integer.parseInt(tmp);      // 입력받은 문자열(tmp)

		while(num!=0) {    
			sum += num%10; 	// sum = sum + num%10;
			System.out.printf("sum=%3d num=%d%n", sum, num);
			num /= 10;   
		}

		System.out.println("각 자리수의 합:"+sum);
	}
}

while문도 그렇게 어렵지 않다. while옆에 조건식을 넣으면 된다. True면 계속 아래 코드가 실행되는 것이고 False가 되면 반복문이 멈추게 되는 것이다. 

 

다음은 do while문을 알아보자.

do while문은 정말 거의 사용을 안해봐서 좀 낯설다 사실

import java.util.*;

class DoWhile {
	public static void main(String[] args) { 
		
		// do-while문 블럭{}을 최소한 한 번 이상 반복 - 사용자 입력 받을 때 유용
		
		int input  = 0, answer = 0;

		answer = (int)(Math.random() * 100) + 1; // 1~100사이의 임의 수를 저장
		Scanner scanner = new Scanner(System.in);

		do {
			System.out.print("1과 100사이의 정수를 입력하세요.>");
			input = scanner.nextInt();

			if(input > answer) {
				System.out.println("더 작은 수로 다시 시도해보세요.");	
			} else if(input < answer) {
				System.out.println("더 큰수로 다시 시도해보세요.");			
			}
		} while(input!=answer);

		System.out.println("정답입니다.");
	}
}

Do~While의 큰 특징은 블럭{ }을 최소한 한 번 이상 반복한다는 것이다. 이는 사용자의 입력을 받을 때 유용하다.

이 Do 블록이 일단 while의 조건에 맞든 안맞든 무조건 한 번 실행 된다. 그리고 뒤에 while문의 조건에 따라서 또 반복 될 지 안될지를 결정하게 된다. 

 

사실 그렇게 어렵진 않지만 거의 사용을 안해봤기에 생소해서 한 번 이렇게 기록해봤다.

이렇게 기록해놓으면 나중에 또 와서 아 이런 게 있었구나 쉽게 찾아 볼 수 있어서 좋다.

'Java' 카테고리의 다른 글

Break, Continue  (0) 2023.08.17
Switch문  (0) 2023.08.17
형변환(Java)  (0) 2023.08.06
Printf(Java)  (0) 2023.08.02
Static에 대하여 - JAVA  (0) 2023.02.15

+ Recent posts