React 응용하기: 메모장 만들기

조민성·2024년 10월 1일

React

목록 보기
7/9

1. 메모장 프로젝트의 소개 및 설명

1.1. 설계 및 기본 컴포넌트 구현

  • 왼쪽 사이드바에서는 현재 등록된 메모의 목록이, 오른쪽 메모 편집에서는 제목과 내용을 수정할 수 있는 입력 폼.
  • 사이드바 구조: SideBarHeader, MemoList(MemoItem), SideBarFooter
  • 메모 편집 구조: MemoContainer(MemoTitle + MemoContent)
  • 메모 리스트 = [메모1, 메모2, 메모3, 메모4]

1.2. 메모 수정 및 선택 기능 구현

  • 사이드바에서 선택한 메모를 오른쪽 편집 영역에서 확인하고 수정.
  • 수정된 내용은 다른 메모로 이동하더라도 보존.

1.3. 메모 추가 기능 구현

  • 사이드바 최하단에는 새로운 메모를 추가할 수 있는 버튼 존재
  • 새로운 메모 추가 시 초기 제목은 ‘untitled’로 설정되며, 내용은 비어있음
  • 새로운 메모 추가 시 해당 메모가 자동으로 선택됨

1.4. 메모 삭제 기능 구현

  • 사이드바의 각 메모 오른쪽에는 메모를 삭제하는 x 버튼 존재
  • x버튼 클릭 시 해당 메모는 삭제, 해당 메모를 선택한 상태라면 가장 첫 번째 메모로 선택이 변경됨
  • 모든 메모 삭제 시, 오른쪽 편집 영역에는 “메모가 없습니다. 새로운 메모를 생성해주세요” 라는 문구가 출력됨

1.5. 메모 저장 기능 구현

  • 브라우저를 닫아도 편집한 메모가 유지됨 (localStroage 사용)

2. 기본 컴포넌트 구현하기

2.1. App.js

function App (){
    const [memos, setMemos] = useState([
        {
            title: 'Memo 1',
            content: 'This is memo1',
            createdAt: 1719983657532, // 생성한 시간 값.
            updatedAt: 1719983657532, // 업데이트된 시간 값.
        },
        {
            title: 'Memo 2',
            content: 'This is memo2',
            createdAt: 1719983697309, // 생성한 시간 값.
            updatedAt: 1719983697309, // 업데이트된 시간 값.
        }
    ]);

    return (
            <div className="App">
            <SideBar memos={memos}/>
            <MemoContainer/>
        </div>

    );
}

export default App;

2.2. MemoContainer.js

function MemoContainer(){
    return(
        <div className="MemoContainer">
            <div>
                <input type="text" className="MemoContainer_title"/>
            </div>
            <div>
                <textarea className="MemoContainer_content"/>
            </div>
        </div>
    )
}

export default MemoContainer;

2.3. Sidebar.js

import MemoList from "./MemoList";
import SideBarHeader from "./SideBarHeader";
import SideBarFooter from "./SideBarFooter";

function SideBar({memos}){
    return(
        <div className="SideBar">
            <SideBarHeader/>
            <MemoList memos={memos}/>
            <SideBarFooter/>
        </div>
    )
}
export default SideBar;

2.4. MemoList.js

function MemoList({memos}){
    return(
        <div>
            {
                memos.map((memo, index)=>(
                    <div key={index}>{memo.title}</div>
                ))
            }
        </div>
    );
}
export default MemoList;

2.5. SideBarHeader.js

function SideBarHeader({memos}){
    return(
        <div className="SideBar">
            SideBarHeader
        </div>
    )
}
export default SideBarHeader;

2.6. SideBarFooter.js

function SideBarFooter({memos}){
    return(
        <div className="SideBar">
            SideBarFooter
        </div>
    )
}
export default SideBarFooter;

3. 메모 수정 기능 및 메모 선택 기능 구현

3.1. MemoContainer.js에 메모 제목과 내용을 수정해주기

function MemoContainer({memo, setMemo}){
    return(
        <div className="MemoContainer">
            <div>
                <input
                    type="text"
                    className="MemoContainer_title"
                    value={memo.title}
                    onChange={(e)=>{
                        setMemo({
                            ...memo,
                            title: e.target.value,   // 메모 타이틀의 값이 변화하면 value를 업데이트.
                            updatedAt: new Date().getTime(), // setMemo 업데이트 시 시간도 업데이트.
                        });
                    }}
                />
            </div>
            <div>
                <textarea
                    className="MemoContainer_content"
                    value={memo.content}
                    onChange={(e)=>{
                        setMemo({
                            ...memo,
                            content: e.target.value,   // 메모 내용의 값이 변화하면 value를 업데이트.
                            updatedAt: new Date().getTime(), // setMemo 업데이트 시 시간도 업데이트.
                        });
                    }}
                />
            </div>
        </div>
    )
}

export default MemoContainer;

3.2. App.js에서 setMemo 변수를 이용해 내용을 ReRendering해주기

    const setMemo = (newMemo) => {
        // memos[selectedMemoIndex] = newMemo;
        //불변성의 유지를 위해, 아예 newMemos라는 새로운 배열을 만들고, 여기에 기존 배열을 풀어서 렌더링을 위해 다시 집어넣어준다.
        const newMemos = [...memos];
        newMemos[selectedMemoIndex] = newMemo;
        setMemos(newMemos);

        //setMemos([...memos]);   //[...memos]를 이용해 memos의 배열을 풀었다가 다시 감싸는 이유: 기존 reference와 새롭게 변화하는
        //값에 대해 setMemos가 변화했다고 인식해서 새롭게 렌더링을 하기 위해.
        //만약 [...memos] 대신 memos만 적으면, 내부에서 값은 바뀌지만, 바뀐 값이 리렌더링되지는 않는다.
    }

3.3. 메모를 선택하기 위해 새롭게 메모 내용을 불러와주는 컴포넌트인 MemoItem.js를 만들어주고, MemoList.js에 onClick을 넣어주기

function MemoItem({children, onClick, isSelected}){ //부모 함수에서 onClick 때 실행되는 것을 받아옴.
    return(

            <div className={"MemoItem" + (isSelected ? ' selected' : '')}
                onClick={onClick}
            >
                {children}
            </div>
    );
}
export default MemoItem;
import MemoItem from "./MemoItem";

function MemoList({memos, setSelectedMemoIndex, selectedMemoIndex}){
    return(
        <div>
            {
                memos.map((memo, index)=>(
                    <MemoItem
                        key = {index}
                        onClick={()=>{  //MemoItem을 클릭할 때의 onClick 함수가 여기서 구현된다.
                            //사이드바의 메모 리스트를 클릭하면 메모가 수정된다.
                            setSelectedMemoIndex(index);
                        }}
                        isSelected={index === selectedMemoIndex}
                        index = {index}
                        setSelectedMemoIndex={setSelectedMemoIndex}

                    >{memo.title}</MemoItem>
                ))
            }
        </div>
    );
}
export default MemoList;

3.4. App.js에 selectedMemoIndex와 setSelectedMemoIndex를 선언해주기

function App (){
    return (
            <div className="App">
            <SideBar
                memos={memos}
                selectedMemoIndex={selectedMemoIndex}
                setSelectedMemoIndex={setSelectedMemoIndex}/>
            <MemoContainer memo={memos[selectedMemoIndex]}
            setMemo={setMemo}/>
        </div>

    );
}

export default App;

3.5. 시각적인 변화를 보여주기 위해 App.css 구현하기.

.App {
  text-align: center;
  display: flex;
  height: 100vh;  /* vh = 윈도우 창의 높이 단위.*/
}
.SideBar{
  background-color: aquamarine;
  width: 200px;
}
.MemoContainer{
  background-color: beige;
  flex: 1; /* flex: 해당 페이지에서 남은 만큼 넓이를 부여함.*/
}
.MemoContainer_title{
  width: 100%;
  box-sizing: border-box; /* border-box 사용 이유: input 컨테이너가 여백을 꽉 채워서 출력되기 때문에, 기존 페이지에 맞출 필요가 있다.*/
  font-size: 2rem;
}
.MemoContainer_content{
  width: 100%;
  font-size: 1.1rem;
}
.MemoItem{
  width: 100%;
  white-space: nowrap;  /*사이드바의 제목이 다음 줄로 넘어가지 않게 만들어줌.*/
  text-overflow: ellipsis;  /*사이드바를 넘어간 제목은 ...으로 처리해줌*/
  overflow: hidden;
}
.MemoItem.selected{
  background-color: rgba(100, 100, 100, 0.5); /*선택된 메모의 색을 변경해줌*/
  font-weight: 500;
}

3.6. npm start시 화면

변화창.png

4. 메모 추가 기능 구현

4.1. SideBarFooter.js의 css 변경 후 버튼화하기

function SideBarFooter({onClick}){
    //새로운 메모를 추가하는 버튼이 구현됨.
    return(
        <div className="SideBar">
            <button
                className="SideBarFooter_add-Button"
                onClick={onClick}
                //onClick 함수를 통해 버튼 클릭 시 변화를 줌
            >
                +
            </button>
        </div>
    )
}
export default SideBarFooter;

4.2. App.js에 addMemo 함수를 생성하여 버튼 클릭 시 변화를 정의하기

    const addMemo = () => {
        const now = new Date().getTime();
        setMemos([...memos, {
            title: 'Untitled',
            content: '',
            createdAt: new Date().getTime(),
            updatedAt: new Date().getTime()},   //새 메모 생성 시 제목은 'Untitled',
        ]);
        setSelectedMemoIndex(memos.length);
        //새 메모 생성 시, 자동으로 가장 마지막으로 생성된 메모가 띄워짐.
    };
    
    return (
            <div className="App">
            <SideBar
                memos={memos}
                addMemo={addMemo} //메모 추가 기능 탑재.
                selectedMemoIndex={selectedMemoIndex}
                setSelectedMemoIndex={setSelectedMemoIndex}/>
            <MemoContainer memo={memos[selectedMemoIndex]}
            setMemo={setMemo}/>
        </div>

    );
}

export default App;

4.3. SideBar.js에 addMemo 함수를 추가하여 적용하기.

import MemoList from "./MemoList";
import SideBarHeader from "./SideBarHeader";
import SideBarFooter from "./SideBarFooter";

function SideBar({memos, addMemo, setSelectedMemoIndex, selectedMemoIndex}){
    return(
        <div className="SideBar">
            <SideBarHeader/>
            <MemoList
                memos={memos}
                selectedMemoIndex={selectedMemoIndex}
                setSelectedMemoIndex={setSelectedMemoIndex}/>
            <SideBarFooter onClick={addMemo}/>
            {/*//addMemo: 메모를 추가해주는 함수.*/}
        </div>
    )
}
export default SideBar;

5. 메모 삭제 기능 구현

5.1. SideBar에 삭제 버튼을 구현하기 위해, MemoItem.js에 x버튼 생성해주기


function MemoItem({children, onClickItem, onClickDelete, isSelected}){ //부모 함수에서 onClick 때 실행되는 것을 받아옴.
    return(

            <div className={"MemoItem" + (isSelected ? ' selected' : '')}
                onClick={onClickItem}
            >
                {children}
                <button className="MemoItem_delete_button" //삭제 버튼 구현.
                // onClickDelete라는 삭제 기능을 구현할 함수를 구현.
                onClick={onClickDelete}>X</button>
            </div>
    );
}
export default MemoItem;

5.2. MemoList.js에 onClickDelete라는 함수를 정의해주기

import MemoItem from "./MemoItem";

function MemoList({memos, setSelectedMemoIndex, selectedMemoIndex, deleteMemo}){
    return(
        <div>
            {
                memos.map((memo, index)=>(
                    <MemoItem
                        key = {index}
                        onClickItem={()=>{  //MemoItem을 클릭할 때의 onClick 함수가 여기서 구현된다.
                            //사이드바의 메모 리스트를 클릭하면 메모가 수정된다.
                            setSelectedMemoIndex(index);
                        }}
                        onClickDelete={(e)=>{ //MemoItem 옆 x버튼을 누를 시, deleteMemo라는 이벤트가 발생.
                        //버튼 누를 시 해당 index의 메모가 삭제됨.
                            deleteMemo(index);
                            e.preventDefault();
                            e.stopPropagation();
                        }}
                        isSelected={index === selectedMemoIndex}

                    >{memo.title}</MemoItem>
                ))
            }
        </div>
    );
}
export default MemoList;

5.3. App.js에 deleteMemo 함수의 정의 적용하기

function App (){

    const deleteMemo = (index) => { //deleteMemo의 의미: newMemos라는 새로운 memos를 생성한 뒤,
    //해당 index의 memo가 선택되면, 그 memo를 0으로 지워버리는 역할.
        const newMemos = [...memos];
        newMemos.splice(index, 1);
        setMemos(newMemos);
        if(index === selectedMemoIndex){
            setSelectedMemoIndex(0);
        }
    }

    return (
            <div className="App">
            <SideBar
                memos={memos}
                addMemo={addMemo}
                selectedMemoIndex={selectedMemoIndex}
                setSelectedMemoIndex={setSelectedMemoIndex}
                deleteMemo={deleteMemo}/> //deleteMemo 함수 적용
            <MemoContainer memo={memos[selectedMemoIndex]}
                   setMemo={setMemo}/>
        </div>

    );
}

export default App;

5.4. SideBar.js에 deleteMemo 적용해주기

import MemoList from "./MemoList";
import SideBarHeader from "./SideBarHeader";
import SideBarFooter from "./SideBarFooter";

function SideBar({memos, addMemo, setSelectedMemoIndex, selectedMemoIndex, deleteMemo}){
    return(
        <div className="SideBar">
            <SideBarHeader/>
            <MemoList
                memos={memos}
                selectedMemoIndex={selectedMemoIndex}
                setSelectedMemoIndex={setSelectedMemoIndex}
                deleteMemo={deleteMemo}
            />
            <SideBarFooter onClick={addMemo}/>
            {/*//addMemo: 메모를 추가해주는 함수.*/}
        </div>
    )
}
export default SideBar;

6. LocalStorage를 이용한 데이터 저장

  • Cookie: 네트워크 요청 시, 서버로 함께 저장되는 문자열 데이터.
  • Local Storage: key-value 쌍으로 데이터를 저장/조회할 수 있는 데이터 저장소. 서버로 전달되지 않고, 지우지 않는 이상 브라우저나 탭을 닫았다 열어도 유지됨
  • Session Storage: Local Storage와 같으나, 같은 탭 안에서만 데이터가 유지되고, 탭이나 브라우저를 닫으면 데이터가 지워짐.

profile
사람도 사랑도 계획적으로

0개의 댓글