79일차 내용 정리

채공부·2025년 9월 12일

React 개발환경 & node_modules

  • GitHub 등에서 React 프로젝트를 clone 하면 node_modules 폴더는 존재하지 않는다
  • node_modules 는 용량이 크고 로컬에서만 사용하는 파일이기 때문에 Git 저장소에 포함시키지 않는다

.gitignore

  • vite 로 프로젝트를 만들면 기본적으로 .gitignore 가 생성
  • node_modules가 포함되어 있어 Git에 포함되지 않는다

package.json

⚠️ node_modules가 없으면 실행 불가

  • 최초 react project 를 clone 하면 node_modules 가 없는 상태라서 실행히 불가하다

  • 의존성 설치

    • package.json 파일에 명시된 의존 dependency 가 모두 일괄 다운로드 및 설치가 된다
    • node_modules 폴더가 자동 생성되고 그 안에 설치된다
npm install
  • 설치 확인
    • node_modules 폴더가 만들어진 것을 확인 가능
dir

classnames

특정 클래스를 추가할지 말지를 classnames 없이 구현하면?

  • 이 값(true)이 state 값이라고 생각을 한다면
    • state => truetext-dnager 가 이 위치에 출력
    • state => false'' 아무 것도 출력되지 않는다
<p className={true ? 'text-danger' : ''}>xxx</p>
  • 결과
className={'text-danger')
or
className={''} 

같은 동작을 classnames 를 이용하면?

  • 이 값(true)이 state 값이라고 생각을 한다면
    • state => true 면 왼쪽 object 의 key 값과 동일한 class 추가
    • state => false 면 아무것도 추가되지 않는다
import cn from 'classnames';

<p className = {cn({'text-danger':true})}>xxx</p>

체크박스 제어

  • checked 속성으로 체크 상태를 제어 가능
  • 체크 상태를 기반으로 특정 클래스를 적용하거나 해제 가능
const handleChange2 = (e) => {
	const value = e.target.value; // checkbox 의 value 값 "bg-info" or "text-danger"
	const isChecked = e.target.checked; // 체크 여부 true or false
	// 상태값 변경하기
	setMessageStyle({
		...messageStyle,
		[value]:isChecked
	});
};
  • [value]:isChecked 의 결과 예시 4가지
{"bg-info":true}
{"bg-info":false}
{"text-dnager":true}
{"text-danger":false}

객체에서 하이픈(-) 포함 키

  • my-name처럼 -이 포함된 key 는 jsx 에서 식별자로 인식될 수 없다
  • 반드시 " "로 감싸고 대괄호 표기법을 사용

object 참조 방법

> {num:1, my-name:"kim"}
Uncaught SyntaxError: Unexpected token '-'
➜ my name 에서 - 가 연산자로 인식 되어서 에러 발생

> const m1 = {num:1, "my-name":"kim"}
undefined
➜ object 의 키값을 " "  로 감싸면 해결

> m1
{ num: 1, 'my-name': 'kim' }

> m1.my-name
Uncaught ReferenceError: name is not defined
➜ my name 에서 - 가 연산자로 인식 되어서 에러 발생

> m1["my-name"]
'kim'
➜ .key 값 대신에 ["key값"] 형식으로 object 에 저장된 값을 참조한다

실습 (Client 의 React 작업)

RestClientController 생성 : 고객 목록 , 수정 , 추가할 api 생성
React 에서는 라우팅 정보 추가 등 작업
localhost:9000/clientslocalhost:5173/#/clients 의 페이지 기능과 UI를 동일하게 구현
jsx 페이지는 총 4개 : 목록, 신규 등록, 자세히 보기, 수정

  • App.jsx 에 clients 링크 추가
<Nav.Item>
	<Nav.Link as={NavLink} to="/clients">고객 목록</Nav.Link>
</Nav.Item>
  • index.jsx 에 Clients 라우트 경로 추가
{path:"/clients", element:<Client/>}
  • RestClientController 클래스 생성
@RequestMapping("/v1")
@RequiredArgsConstructor
@RestController
public class RestClientController {
	private final ClientService clientService;
	
	@GetMapping("/clients")
	public List<ClientDto> list() {
	    return clientService.getClients();
	}
	
	@PostMapping("/clients")
	public ClientDto create(@Valid @RequestBody ClientDto dto) {
		// 새 고객 정보를 저장하고
		Long num = clientService.addClient(dto);
		// 방금 추가한 회원의 번호를 dto 에 담아서
		dto.setNum(num);
		// 리턴
		return dto;
	}
	
	@GetMapping("/clients/{num}")
	public ClientDto getClient(@PathVariable Long num) {
		return clientService.getClient(num);
	}
	
	@PutMapping("/clients/{num}")
	public ClientDto update(@PathVariable long num, @Valid @RequestBody ClientDto dto) {
		dto.setNum(num);
		clientService.update(dto);
		return dto;
	}
}

에러 페이지

  • NotFound.jsx 생성
function NotFound() {

    const navigate = useNavigate();

    return<>
        <h1>404</h1>
        <p>페이지를 찾을 수 없습니다</p>
        <p>주소가 잘못 되었거나, 페이지가 이동/삭제 되었을 수 있어요</p>
        <Button as={NavLink} to="/" variant="primary">홈으로</Button>
        <Button onClick={()=>navigate(-1)} variant="warning" className="ms-1">이전 페이지</Button>
    </>
}
  • index.jsx 에서 NotFound 라우트 경로 추가
    • * 위의 라우트 경로를 제외한 모든 경로들은 해당 컴포넌트에서 처리
{path:"*", element:<NotFound/>}

고객 목록

  • App.jsx 에 Client 라우트 경로 추가
{path:"/clients", element:<Client/>}
  • Client.jsx 생성
function Client() {
    // 고객 목록을 상태값으로 관리
    const [list, setList] = useState([]);

    // 컴포넌트가 활성화되는 시점에
    useEffect(()=>{
        // api 서버로부터 고객 목을 얻어와서
        axios.get("/api/v1/clients")
        .then(res=>{
            // 상태값을 넣어준다
            setList(res.data);
        })
    }, []);

    return<>
        <div className="d-flex justify-content-between mt-3 mb-3">
            <h3>고객 목록</h3>
            <NavLink to="/clients/new">신규 등록</NavLink>      
        </div>
        <table className="table table-hover">
            <thead>
                <tr>
                    <th>번호</th>
                    <th>이름</th>
                    <th>생일</th>
                    <th>등록일</th>
                    <th>자세히</th>
                </tr>
            </thead>
            <tbody>
                {list.map(item =>
                    <tr key={item.num}>
                        <td>{item.num}</td>
                        <td>{item.userName}</td>
                        <td>{item.birthday}</td>
                        <td>{item.createdAt}</td>
                        <td>
                            <NavLink className="btn btn-primary" to={`/clients/${item.num}`}>보기</NavLink>
                        </td>
                    </tr>
                )}
            </tbody>
        </table>
    </>
}
  • ClientDto 에 @JsonFormat 어노테이션 추가
    • RestController 에서 dto 를 리턴하면 해당 dto 의 필드에 있는 내용이 json 문자열 로 변경되는데 LocalDataTime 에 있는 날짜 정보를
      @JsonFormat 어노테이션을 이용해서 원하는 형식으로 변경되도록 한다
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd일 HH:mm")
private LocalDateTime createdAt;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd일 HH:mm")
private LocalDateTime updatedAt;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd일")
private LocalDate birthday;

고객 상세 페이지

  • App.jsx 에 ClientDetail 라우트 경로 추가
{path:"/clients/:num", element:<ClientDetail/>},
  • ClientDetail.jsx 생성
function ClientDetail() {

    // location 객체에 state 라는 키값으로 담긴 내용을 읽어오기 위한 hook
    const {state} = useLocation();

    // "/clients/:num" 에서 num 을 읽어내기 위한 hook
    const {num} = useParams();
    // 회원 한 명의 정보를 state 값으로 관리
    const [dto, setDto] = useState({});

    useEffect(()=>{
        // 이름이 없는 화살표 함수를 만들자 마자 call() 하는 구조
        (async ()=>{
            try{
                // 서버로부터 data 가 올 때까지 기다린다
                // 정상 응답이 되면
                const res = await axios.get(`/api/v1/clients/${num}`);
                // 회원 한 명의 정보를 상태값으로 넣어준다
                setDto(res.data);
            }
            catch(err){
                console.log(err);           
            }
        })();
        
    },[]);

    return<>
        {state && <p className="alert alert-success">{state.message}</p>}
        <h3>고객 상세 정보</h3>
        <table className="table table-bordered">
            <tbody>
                <tr>
                    <th>번호</th>
                    <td>{dto.num}</td>
                </tr>
                <tr>
                    <th>이름</th>
                    <td>{dto.userName}</td>
                </tr>
                <tr>
                    <th>생일</th>
                    <td>{dto.birthday}</td>
                </tr>
                <tr>
                    <th>수정일</th>
                    <td>{dto.updatedAt}</td>
                </tr>
                <tr>
                    <th>등록일</th>
                    <td>{dto.createdAt}</td>
                </tr>
            </tbody>
        </table>
        <div className="mt-3">
            <Button variant="primary" as={NavLink} to="/clients">목록으로</Button>
            <Button className="ms-1" variant="warning" as={NavLink} to={`/clients/${dto.num}/edit`}>수정</Button>
        </div>
    </>
}

고객 추가

  • App.jsx 에 ClientForm 라우트 경로 추가
{path:"/clients/new", element:<ClientForm/>}
  • ClientDetail.jsx 에 useLocation 추가
    • location 객체에 state 라는 키값으로 담긴 내용을 읽어오기 위한 hook
const {state} = useLocation();
  • alertstate 상태값 추가
{state && <p className="alert alert-success">{state.message}</p>}
  • ClientForm.jsx 생성
function ClientForm() {

    // route 이동을 위한 함수
    const navigate = useNavigate();
    // 에러를 상태값으로 관리
    const [errors, setErrors] = useState({});

    // 폼에 submit 이벤트가 일어났을 때 실행할 함수
    const handleSubmit = (e) => {
        // 폼 전송 막기
        e.preventDefault();
        // 이벤트가 일어난 바로 그 요소 (form) 의 참조값
        const form = e.target;
        const formData = new FormData(form);
        // 폼에 입력한 내용을 이용해서 object 얻어내기
        const obj = Object.fromEntries(formData);

        // axios.post(form.action, obj)
        axios[form.method](form.action, obj)
        .then(res => {
            // client 정보 추가 성공 시
            // res.data 는 방금 추가한 고객 정보
            navigate(`/clients/${res.data.num}`, {
                state:{
                    message:"고객 정보를 추가했습니다"
                }
            });
        })
        .catch(err => {
            // client 정보 추가 실패 시 (@Valid 위반)
            console.log(err);
            // @Valid 에러 객체를 state 에 넣어준다
            setErrors(err.response.data.errors);
        })
    };

    return<>
        <h3>새 Client 등록 양식</h3>
        <form action="/api/v1/clients" method="post" onSubmit={handleSubmit}>
            <div className="mb-3">
                <label className="form-label" htmlFor="userName">userName <span className="text-danger">*</span></label>
                <input className="form-control" type="text" name="userName" id="userName" placeholder="이름 입력"/>
                {errors.userName && <small className="form-text text-danger">{errors.userName}</small>}
            </div>
            <div className="mb-3">
                <label className="form-label" htmlFor="birthday">생일 (선택)</label>
                <input className="form-control" type="date" name="birthday" id="birthday"/>
                <small className="form-text text-muted">나중에 입력 가능</small>
                {errors.birthday && <small className="form-text text-danger">{errors.birthday}</small>}
            </div>
            <button className="btn btn-success" type="submit">저장</button>
        </form>
    </>
}

고객 정보 수정

  • App.jsx 에 ClientUodateForm 라우트 경로 추가
{path:"/clients/:num/edit", element:<ClientUpdateForm/>}
  • ClientUpdateForm.jsx 생성
function ClientUpdateForm() {
    // "clients/:num:edit" 에서 num 에 전달된 값 읽어내기
    const {num} = useParams();
    // 수정할 고객의 정보를 상태값으로 관리
    const [dto, setDto] = useState({
        userName:"",
        birthday:""
    });
    // 원본 dto 값을 상태값으로 관리
    const [originalDto, setOriginalDto] = useState({});

    // 필드 에러 정보를 상태값으로 관리
    const [errors, setErrors] = useState({});

    useEffect(()=>{
        axios.get(`/api/v1/clients/${num}`)
        .then(res=>{
            setDto(res.data)
            setOriginalDto(res.data);
        });
    },[]);

    const handleChange = (e) => {
        setDto({
            ...dto,
            [e.target.name]:e.target.value
        });
    };

    const navigate = useNavigate();

    // form 에 submit 이벤트가 발생했을 때 실행할 함수
    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            await axios.put(e.target.action, dto);
            alert("수정 완료");
            navigate(`/clients/${num}`, {
                state:{
                    message:"수정된 고객 정보 입니다"
                }
            })
        } catch (err) {
            console.log(err);
            setErrors(err.response.data.errors);
        }
    };

    return<>
        <h3>고객 정보 수정 양식</h3>
        <form action={`/api/v1/clients/${num}`} onSubmit={handleSubmit}>
            <div className="mb-3">
                <label className="form-label" htmlFor="userName">userName</label>
                <input className="form-control" onChange={handleChange} type="text" name="userName" id="userName" 
                        value={dto.userName} placeholder="이름 입력"/>
                {errors.userName && <small className="form-text text-danger">{errors.userName}</small>}
            </div>
            <div className="mb-3">
                <label className="form-label" htmlFor="birthday">생일 (선택)</label>
                <input className="form-control" onChange={handleChange} type="date" name="birthday" id="birthday"
                        value={dto.birthday}/>
                <small className="form-text text-muted">나중에 입력 가능</small>
                {errors.birthday && <small className="form-text text-danger">{errors.birthday}</small>}
            </div>
            <button className="btn btn-success" type="submit">수정</button>
            <button onClick={()=>setDto(originalDto)} className="ms-1 btn btn-secondary" type="reset">취소</button>
        </form>
    </>
}
profile
학원 공부 내용 정리

0개의 댓글