리액트 (템플릿 리터럴, 컴포넌트 리팩토링, onChange)

BlackBird·2024년 9월 24일

개발자 취업 일지

목록 보기
99/116

js 함수 실행 방법이 여러가진데 그걸 배우는 와중에 템플릿 리터럴도 배웠다. 내 개인프로젝트할 때 사용했어서 뭔지는 대충 안다.

템플릿 리터럴

function a(index){
  console.log(`a실행, index : ${index}`);
}

결과 :

a실행 : index : undefind

${}이거만 보면 짜증이 나네.. 여튼 이렇게 활용할 수 있다.

프론트엔드에서는 HTML을 데이터와 결합해서 DOM을 다시 그려야 하는 일이 빈번하기 때문에, 템플릿을 좀 더 쉽게 편집하고 작성해야 할 필요가 있어서, 이러한 기능이 추가되었다고 한다.

내 개인 프로젝트에서 애를 먹었던 이유는 이게 프로젝트 내에서 한번 해석 하고, js에서 한번 해석 하는 과정에서 두번 해석을 하려고 해서 null을 반환하는 문제가 있었다. 때문에 ${'${}'}이런식으로 이중으로 감싸야하는 이슈가 있었기 때문이다. (정확한 이유는 아닐지도 모른다. 구글링해서 해결 방법을 찾는 와중에 이런 방법이 있어서 써보니까 해결 되길래 원인이 이런게 아닐까 짐작하는것.)

let a = 1;
let b = 2;
let c = "1 + 2는?";
let sum = `${c} ${a+b}`;
console.log(str);   //1 + 2는? 3

여튼 템플릿 리터럴을 간단하게 사용하면 이런 느낌이다.


본론으로 가서 js를 사람들이 싫어하는 이유중 하나. 함수실행 방식이 너무 많다는 것.

console.clear();

function a(index) {
  console.log(`a 실행, index : ${index}`);
}
// function b(){
//   a();
// }
// const b = function(){
//   a();
// }
// const b = () => {
//   a();
// }

const index = 2;
// const b = () => a(index);
const b = () => {
  a(index);
};
const c = a;

b();
c();

이게 결과가 다 a함수를 실행시킨 결과가 나온다. 모두가 함수 실행 방식이다. 눈에 익혀두자.

여튼 리액트에서 리터럴을 왜 쓰는지는 살짝 알 것 같긴하다. 내 프로젝트에서도 쓴 이유가 spotify card를 생성할 때 변수가 html에 들어가니까 엄청 편했던 것 같다. 그런 느낌이니까 쓰지 않을까.

컴포넌트 리팩토링

우선 전의 포스트의 정수 기록 앱을 컴포넌트 단위로 리팩토링하는 것으로 시작했다.

console.clear();

import React, { useState } from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";

const NumberRecorderForm = ({
	number,
	setNumber,
	saveRecord,
	clearNumbers
}) => {
	return (
		<>
			<div>
				<span>숫자 : {number}</span>
				&nbsp;
				<button>취소</button>
			</div>
			<div>
				<button
					onClick={() => {
						setNumber(number + 1);
					}}
				>
					증가
				</button>
				&nbsp;
				<button
					onClick={() => {
						setNumber(number - 1);
					}}
				>
					감소
				</button>
				&nbsp;
				<button onClick={() => saveRecord()}>기록</button>
				&nbsp;
				<button onClick={clearNumbers}>전체기록삭제</button>
			</div>
		</>
	);
};

const NumberRecorderList = ({ recordNums, setRecordNums, removeNumber }) => {
	return (
		<>
			<div>
				{recordNums.length == 0 ? (
					<div>기록 없음</div>
				) : (
					<>
						<h3>기록</h3>
						<ul>
							{recordNums.map((recordNum, index) => (
								<li key={index}>
									<span>
										{index + 1}: {recordNum}
									</span>
									&nbsp;
									<button onClick={() => removeNumber(index)}>삭제</button>
								</li>
							))}
						</ul>
					</>
				)}
			</div>
		</>
	);
};

const App = () => {
	const [number, setNumber] = useState(0);
	const [recordNums, setRecordNums] = useState([10, 20, 30]);

	const saveRecord = () => {
		setNumber(0);
		setRecordNums([...recordNums, number]);
	};

	const clearNumbers = () => {
		setRecordNums([]);
	};

	const removeNumber = (index) => {
		setRecordNums(recordNums.filter((_, _index) => _index != index));
	};

	return (
		<>
			<NumberRecorderForm
				number={number}
				setNumber={setNumber}
				saveRecord={saveRecord}
				clearNumbers={clearNumbers}
			/>
			<NumberRecorderList
				recordNums={recordNums}
				setRecordNums={setRecordNums}
				removeNumber={removeNumber}
			/>
		</>
	);
};

ReactDOM.render(<App />, document.getElementById("root"));

잘 살펴 보면 메서드가 분리되었다는 것을 확인할 수 있고 App 메서드의 return 에서 태그를 잘 살펴보면 매개변수를 넘겨주면서 제 기능을 하게 만들어주고 있는 것도 확인 할 수 있다. 여기서 매개변수를 어떻게 받는가 조금 헷갈릴 수도 있는데,

const NumberRecorderForm = ({
	number,
	setNumber,
	saveRecord,
	clearNumbers
}) => {
  
/////////////////////////////////////////////

	return (
		<>
			<NumberRecorderForm
				number={number}
				setNumber={setNumber}
				saveRecord={saveRecord}
				clearNumbers={clearNumbers}
			/>
			<NumberRecorderList
				recordNums={recordNums}
				setRecordNums={setRecordNums}
				removeNumber={removeNumber}
			/>
		</>
	);

이렇게 떼어내서 보면 조금 더 직관적으로 보인다. 분리하면서 NumberRecorderFormNumberRecorderList에서 원래 사용하던 변수들을 쓸 수 없게 되어서 App메서드 안에 있던걸 매개변수로 다시 넣어주는 작업이라고 생각하면 쉽다.

여기서 li를 또 따로 빼서 한번 더 리팩토링.

const NumberRecordListItem({index, recordNum, removeNumber}) => {
  return (
    <>
    	<li key={index}>
			<span>
				{index + 1}: {recordNum}
			</span>
			&nbsp;
			<button onClick={() => removeNumber(index)}>삭제</button>
		</li>
    </>
	);
};

const NumberRecorderList = ({ recordNums, setRecordNums, removeNumber }) => {
	return (
		<>
			<div>
				{recordNums.length == 0 ? (
					<div>기록 없음</div>
				) : (
					<>
						<h3>기록</h3>
						<ul>
							{recordNums.map((recordNum, index) => (
								<NumberRecordListItem
      								index={index}
									recordNum={recordNum}
									removeNumber={removeNumber}
							))}
						</ul>
					</>
				)}
			</div>
		</>
	);
};

그 부분만 따로 빼서 보면 이런 결과다. 결국 똑같은 일 한거다. 분리했고, 또 그에 필요한 매개변수를 넘겨주는 일이다. 분리한 곳에는 해당 메서드를 태그로 불러오고... 똑같다.

이걸 활용해서 수정버튼으로 index 값을 수정하는 코드를 만들어보았다.

const NumberRecorderList = ({ recordNums, setRecordNums, removeNumber, modifyNumber, number}) => {
	return (
		<>
			<div>
				{recordNums.length == 0 ? (
					<div>기록 없음</div>
				) : (
					<>
						<h3>기록</h3>
						<ul>
							{recordNums.map((recordNum, index) => (
								<li key={index}>
									<span>
										{index + 1}: {recordNum}
									</span>
									&nbsp;
									<button onClick={() => removeNumber(index)}>삭제</button>
									&nbsp;
									<button onClick={() => modifyNumber(index, number)}>수정</button>
								</li>
							))}
						</ul>
					</>
				)}
			</div>
		</>
	);
};
//////////////////
	const modifyNumber = (index, newValue) => {
    setRecordNums(
        recordNums.map((recordNum, _index) =>
            _index === index ? newValue : recordNum
        )
    );
/////////////////
      			<NumberRecorderList
				recordNums={recordNums}
				setRecordNums={setRecordNums}
				removeNumber={removeNumber}
				modifyNumber={modifyNumber}
				number={number}
			/>

길어서 축약했다.

내 방식이 틀렸을 수도 있는데, NumberRecorderForm에서 증가 감소로 입력한 값을 받아서 수정하는 걸 원해서 number 값까지 받아왔고, modifyNumber메서드를 생성해서 수정 버튼으로 처리했다.

여기서 해보면 된다. (솔직히 제 기능하는데 뭐 틀리고 자시고가 있겠냐만은..)

난 이렇게 했는데 강사님은 input을 사용하시려고 하는 것 같았다.

리액트 input 관련 글을 참고해서 구현해보자.

	const [inputValues, setInputValues] = useState([...recordNums]);

	useEffect(() => {
		setInputValues([...recordNums]);
	}, [recordNums]);

	const handleInputChange = (index, value) => {
		const newInputValues = [...inputValues];
		newInputValues[index] = value;
		setInputValues(newInputValues);
	};

	return (
		<>
			<div>
				{recordNums.length == 0 ? (
					<div>기록 없음</div>
				) : (
					<>
						<h3>기록</h3>
						<ul>
							{recordNums.map((recordNum, index) => (
								<li key={index}>
									<span>
										{index + 1}: {recordNum}
									</span>
									&nbsp;
									<button onClick={() => removeNumber(index)}>삭제</button>
									&nbsp;
									<input
										type="number"
										value={inputValues[index]}
										onChange={(e) => handleInputChange(index, e.target.value)}
									/>
									<button
										onClick={() => modifyNumber(index, parseInt(inputValues[index]))}
									>
										수정
									</button>
								</li>
							))}
						</ul>
					</>
				)}
			</div>
		</>
	);
};

이렇게 해봤따

여기서 해봐라!

강사님은 article로 예를 들어서 설명부터 하셨다.

console.clear();

import React, { useState } from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";

const ArticleDetail = ({ id }) => {
	const [editModeStatus, setEditModeStatus] = useState(false);

	if (editModeStatus) {
		return (
			<>
				<form>
					<div>수정모드</div>
					<span>번호</span>
					&nbsp;
					<span>{id}</span>
					<div>
						<span>제목 : </span>
						&nbsp;
						<input type="text" placeholder="제목 입력" />
					</div>
					<div>
						<span>내용 : </span>
						&nbsp;
						<input type="text" placeholder="내용 입력" />
					</div>
					<div>
						<button type="submit">수정완료</button>
						&nbsp;
						<button type="button"  onClick={() => {setEditModeStatus(false)}}>수정취소</button>
					</div>
				</form>
			</>
		);
	}

	const title = "1번 글 제목";
	const body = "1번 글 내용";

	return (
		<>
			<h3>{id}번 게시글</h3>
			<div>제목 : {title}</div>
			<div>내용 : {body}</div>
			<button onClick={() => {setEditModeStatus(true)}}>수정</button>
			<hr />
		</>
	);
};

const App = () => {
	return (
		<>
			<ArticleDetail id={1} />
		</>
	);
};

ReactDOM.render(<App />, document.getElementById("root"));

useState를 수정 버튼으로 true, 수정 취소 버튼으로 false로 바꾼다. 이는 수정모드와 글을 보는 모드로 바꾼다. 아직 수정기능을 도입하지는 않았고, 우선 이게 어떤 방식으로 이렇게 되는건지 살펴보면 된다.

여기서 핵심은
const [editModeStatus, setEditModeStatus] = useState(false);, 여기와
<button type="button" onClick={() => {setEditModeStatus(false)}}>수정취소</button> 여기,
<button onClick={() => {setEditModeStatus(true)}}>수정</button>그리고 여기다.

직접 어떻게 돌아가는지 보면

버튼을 눌러보면 된다. 전환되는 이유는 버튼을 통해 useState의 상태를 변화시킨다고 보면 된다. 그럼 여기서 의문이 왜 1번 게시글의 제목 내용이 사라지는가?에 대한 대답은 if문을 통해서 return 시키기 때문이라고 이해하면 된다.

이제 이걸 아까 우리 코드에 적용시켜보면 된다.

일단 내가 만든 수정은 뒤로하고, 저 상태 자체를 그대로 옮겨서 구현해보면.

const NumberRecorderListItem = ({ index, recordNum, removeNumber }) => {
	const [editModeStatus, setEditModeStatus] = useState(false);

	const readView = (
		<>
			<button onClick={() => setEditModeStatus(true)}>수정</button>
		</>
	);

	const editView = (
		<>
			<input type="number" placeholder="숫자 입력" min="0" value={recordNum}/>
			&nbsp;
			<button onClick={() => setEditModeStatus(false)}>수정 완료</button>
			&nbsp;
			<button onClick={() => setEditModeStatus(false)}>수정 취소</button>
		</>
	);

	return (
		<>
			<li key={index}>
				<span>
					{index + 1}: {recordNum}
				</span>
				&nbsp;
				<button onClick={() => removeNumber(index)}>삭제</button>
				&nbsp;
				{editModeStatus ? editView : readView}
			</li>
		</>
	);
};

이런 코드가 된다. setEditModeStatus로 상태관리를 하고, {editModeStatus ? editView : readView} 삼향연산자로 editView를 보여주느냐 readView를 보여주느냐를 결정한다.

복잡해 보일 수 있는데 결국 아까 게시글이랑 같은 코드다.

아직 수정은 구현하지 않았다. value={recordNum}에 문제가 있는지 값이 바뀌지 않는 문제와 아직 수정을 완료하는 로직도 구현이 안됐다.

우선 value={recordNum}useState를 사용하지 않았기 때문에 고정값이 되어서 수정이 되지 않는 것이다.

const [inputNumberValue, setInputNumberValue] = useState(recordNum);로 위에 먼저 선언해주고 사용하면 된다.

그리고 아까 내가 만든 수정에서도 사용했던 onchangeinput 태그 안에서 사용해주면 된다.

<input 
type="number" 
placeholder="숫자 입력"
value={inputNumberValue} //여기 inputNumberValue를 넣어주면 됨!
onChange={(e) => setInputNumberValue(e.target.value)}
/>

onChange={(e) => setInputNumberValue(e.target.value)}이걸 사용해서 input 태그 값이 바뀌는 것을 허용하면 된다.

그럼 이제 준비준비는 끝났으니 modify 메서드를 만들어주면 된다.

여튼 종합한 코드를 보면

const NumberRecorderListItem = ({
	index,
	recordNums,
	recordNum,
	removeNumber,
	setRecordNums
}) => {
	const [inputNumberValue, setInputNumberValue] = useState(recordNum);
	const [editModeStatus, setEditModeStatus] = useState(false);

	const modifyNumber = () => {
		if (recordNum == inputNumberValue) {
			setEditModeStatus(false);
			return;
		}

		if (!inputNumberValue) {
			setEditModeStatus(false);
			return;
		}

		setRecordNums(
			recordNums.map((_number, _index) =>
				_index == index ? inputNumberValue : _number
			)
		);
		setEditModeStatus(false);
	};

	const readView = (
		<>
			<button onClick={() => setEditModeStatus(true)}>수정</button>
		</>
	);

	const editView = (
		<>
			<input
				type="number"
				placeholder="숫자 입력"
				min="0"
				value={inputNumberValue}
				onChange={(e) => setInputNumberValue(e.target.value)}
			/>
			&nbsp;
			<button onClick={modifyNumber}>수정 완료</button>
			&nbsp;
			<button onClick={() => setEditModeStatus(false)}>수정 취소</button>
		</>
	);

	return (
		<>
			<li key={index}>
				<span>
					{index + 1}: {recordNum}
				</span>
				&nbsp;
				<button onClick={() => removeNumber(index)}>삭제</button>
				&nbsp;
				{editModeStatus ? editView : readView}
			</li>
		</>
	);
};

const NumberRecorderList = ({ recordNums, setRecordNums, removeNumber }) => {
	return (
		<>
			<div>
				{recordNums.length == 0 ? (
					<div>기록 없음</div>
				) : (
					<>
						<h3>기록</h3>
						<ul>
							{recordNums.map((recordNum, index) => (
								<NumberRecorderListItem
									recordNum={recordNum}
									recordNums={recordNums}
									index={index}
									removeNumber={removeNumber}
									setRecordNums={setRecordNums}
								/>
							))}
						</ul>
					</>
				)}
			</div>
		</>
	);
};

이렇게 된다.

실행하는 모습은

이렇다.

profile
한영신의 벨로그입니다.

0개의 댓글