Coding History

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

BlackBirdIT 2024. 9. 24. 16:52

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>
        </>
    );
};

이렇게 된다.

실행하는 모습은

이렇다.

'Coding History' 카테고리의 다른 글

JS 리액트 useEffect  (0) 2024.09.27
JS 리액트 TODO LIST  (2) 2024.09.26
리액트! (JS Filter)  (1) 2024.09.24
chat 웹앱 수업.  (0) 2024.09.11
VO, DTO, Entity의 차이점  (0) 2024.09.09