밍비
프로그램의 편린
밍비
전체 방문자
오늘
어제
  • 분류 전체보기 (64)
    • Spring (2)
    • TIL (23)
    • 프로그래머스 (12)
    • Udemy (16)
    • Typescript (2)
    • MERN (1)
    • AWS (7)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • overflow-wrap
  • 리스트 조회
  • Edge Locations
  • AWS Regions
  • 리액트 reducer
  • 리액트 생애주기
  • state 관리
  • Availability Zones
  • Points of Presence
  • useNavigate
  • useParams
  • 수평 스케일링
  • 리액트
  • 함수형 update
  • useRef
  • Page Moving
  • react
  • 한입크기로잘라먹는리액트
  • 분산저장소
  • 네이버커넥트
  • 서비스아키텍처
  • DOM
  • useState
  • 리액트 프로젝트 만들기
  • 한입 크기로 잘라먹는 리액트
  • State 합치기
  • state 끌어올리기
  • API 호출
  • 컴포넌트트리
  • 데이터 수정

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
밍비

프로그램의 편린

[React] 일기장 프로젝트 1. 기초 작업
Udemy

[React] 일기장 프로젝트 1. 기초 작업

2023. 4. 25. 02:28
728x90

이 블로그는 유데미 '한입 크기로 잘라먹는 리액트' 강의를 듣고 복습하고자 작성되었습니다!

목차

1. 기본 세팅

2. useReducer와 Context 만들기

 

1. 기본 세팅

기본 세팅에서는 크게 4가지를 할 것입니다.

1. 폰트 세팅
2. 레이아웃 세팅
3. 이미지 에셋 세팅
4. 공통 컴포넌트 세팅

 

1. 폰트 세팅

폰트를 사용할 때는, 해당 폰트가 오픈 폰트 라이선스인지 꼭 확인해야 합니다.

오픈 폰트 라이선스(OFL, Open Font License)는 글꼴 및 관련 SW용으로 특별히 설계된 무료 오픈소스 라이선스입니다.

사용, 번들링, 수정 및 재배포를 허용하는 공통 라이선스에 따라 작업 릴리스가 가능합니다.

오픈폰트 라이선스를 지원하는 대표적인 사이트가 Google Web Fonts입니다.

 

Google Web Fonts를 이용해 폰트를 세팅해봅시다.

1. Google Web Fonts에 접속

2. 폰트 선택 후 select 버튼 누름

3. 오른쪽에 뜬 use on web에서 @import를 누른 후, style 태그를 빼고 복사 -> App.css에 복붙

@import url('https://fonts.googleapis.com/css2?family=Gowun+Batang:wght@400;700&display=swap');

4. use on web의 import문 밑에 font-family: '' 부분 복사 -> App.css의 .app{} 안에 복붙

font-family: 'Roboto', sans-serif;

 

2. 레이아웃 세팅

display: flex를 이용해 헤더의 CSS를 설정해줍니다. 이부분의 코드는 생략하겠습니다.

 

3. 이미지 추가

1. public 폴더 안에 assets 폴더 생성 후, 해당 폴더 안으로 이미지 파일 이동

2. 사용할 때는 img 태그로 이미지들 불러옴

<img src={process.env.PUBLIC_URL + '/assets/emotion1.png'} />

process.env.PUBLIC_URL은 public 폴더까지의 경로를 의미합니다.

 

3. 2가 잘 작동되지 않을 경우 리턴문 밖에 아래의 코드를 추가합니다.

const env = process.env;
env.PUBLIC_URL = env.PUBLIC_URL || '';

 

4. 공통 컴포넌트 세팅

여러 곳에 공통적으로 쓰일 버튼과 헤더 컴포넌트를 만들어봅시다.

 

먼저 버튼 컴포넌트입니다.

1. components 폴더 안에 컴포넌트 생성

const MyButton = ({ text, type, onClick }) => {
  return (
    <button className={'MyButton'} onClick={onClick}>
      {text}
    </button>
  );
};
 
export default MyButton;

MyButton 컴포넌트의 기본 세팅입니다.

 

2. CSS 작성

이부분은 생략하겠습니다.

 

3. App.js에 임포트 후 적용

<MyButton
  text={'button'}
  onClick={() => {
    alert('버튼 클릭');
  }}
  type={'positive'}
/>;

이렇게 적용하면 되겠습니다.

 

그럼 이제 버튼의 타입별로 다른 색을 줘봅시다.

저희는 이렇게 초록색, 빨간색, 그리고 흰색 버튼을 만들 것입니다.

 

1. 클래스명을 MyButton과 타입을 합쳐 전달하기

<button
  className={['MyButton', `MyButton_${type}`].join(' ')}
  onClick={onClick}>
  {text}
</button>;

MyButton과 MyButton_[타입]을 배열로 준 후에 문자열로 합쳤습니다.

이렇게 하면 type이 positive일 경우 클래스는 'MyButton MyButton_positive'가 되어

MyButton 클래스와 MyButton_positive 클래스 모두에 포함되게 됩니다.

여기에 초록색은 positive, 빨간색은 negative, 회색은 default로 타입을 전달해보겠습니다.

 

2. defaultProps 만들기

MyButton.defaultProps = {
  type: 'default',
};

MyButton 호출 시 type prop을 받지 않았을때는 회색 버튼이 되도록 defaultProps에서 type을 'default'로 설정합니다.

 

3. CSS 만들기

.MyButton_default {
  background-color: #ececec;
  color: black;
}
 
.MyButton_positive {
  background-color: #64c964;
  color: white;
}
 
.MyButton_negative {
  background-color: #fd565f;
  color: white;
}

이렇게 타입별 버튼 색을 설정합니다.

 

4. App에서 버튼 생성

<MyButton
  text={'button'}
  onClick={() => {
    alert('버튼 클릭');
  }}
  type={'positive'}
/>
<MyButton
  text={'button'}
  onClick={() => {
    alert('버튼 클릭');
  }}
  type={'negative'}
/>
<MyButton
  text={'button'}
  onClick={() => {
    alert('버튼 클릭');
  }}
/>

이렇게 여러 버튼을 만들어보았습니다.

 

5. 예외적인 값을 type props로 받았을 때 default type이 되도록 처리

const btnType = ['positive', 'negative'].includes(type) ? type : 'default';

삼항연산자를 사용해, positive나 negative 중 하나인지 알아보고, 그게 아닐 경우 무조건 'default'로 btnType를 설정시켜 적용합니다.

 

 

이제 또다른 공통 컴포넌트인 헤더를 만들어보겠습니다.

 

1. components 폴더 안에 MyHeader.js 만들기

const MyHeader = ({ headText, leftChild, rightChild }) => {
  return (
    <header>
      <div className='head_btn_left'>{leftChild}</div>
      <div className='head_text'>{headText}</div>
      <div className='head_btn_right'>{rightChild}</div>
    </header>
  );
};
 
export default MyHeader;

2. App.js에서 임포트 후 컴포넌트 추가하기

<MyHeader
  headText={'App'}
  leftChild={
    <MyButton text={'왼쪽 버튼'} onClick={() => alert('왼쪽 클릭')} />
  }
  rightChild={
    <MyButton
      text={'오른쪽 버튼'}
      onClick={() => alert('오른쪽 클릭')}
    />
  }
/>

3. CSS 설정하기

css 코드는 생략하겠습니다.

 

2. useReducer와 Context 만들기

이번에는 일기 state를 편하게 관리하도록 Reducer를 세팅하고,

일기 state와 여러 관리 함수를 다른 컴포넌트에 공급할 Context를 세팅해봅시다.

 

1. useReducer로 일기 state 관리

먼저 useReducer를 만들어보겠습니다.

useReducer는 컴포넌트 속 여러 상태변화함수를 분리시키는 일을 합니다.

일기 프로그램을 예로 들자면 App 컴포넌트 속 onCreate, onEdit, onRemove를 분리하는 것입니다.

원래 데이터를 추가, 수정, 삭제할 때 App의 state를 사용해야 했기 때문에 위의 함수들이 모두 App 컴포넌트 안에 있었으나,

useReducer를 사용하면 이처럼 복잡한 상태관리 로직을 App 컴포넌트 밖으로 분리할 수 있습니다.

 

useReducer를 사용해 state를 관리해봅시다.

1. useReducer 임포트

import { useReducer } from 'react';

2. state처럼 선언

const [data, dispatch] = useReducer(reducer, []);

여기서 data는 state, dispatch는 setState 비슷한 것인데,

dispatch는 setState와 다르게 실제 상태변화는 reducer가 처리한다는 점에서 차이가 있습니다.

useReducer의 인자 속 reducer는 실제 상태변화를 담당하는 함수고, []는 초기값입니다.

 

reducer를 정의하고, 이후 컴포넌트 리턴에서

onClick={()=>dispatch({type:1})}

이런식으로 사용하게 될 것입니다.

(여기서 dispatch가 setState같은 역할이고,

type은 setState에서 여러 경우를 처리할 수 있게 switch문을 사용할 때 줄 인자입니다.

실질적인 상태변화는 reducer가 합니다!)

 

3. App.js 속 App 컴포넌트 밖에서 reducer 선언

const reducer = (state, action) => {
  let newState = [];
  switch (action.type) {
    case 'INIT': {
      return action.data;
    }
    case 'CREATE': {
      const newItem = {
        ...action.data,
      };
      newState = [newItem, ...state];
      break;
    }
    case 'REMOVE': {
      newState = state.filter((it) => it.id !== action.targetId);
      break;
    }
    case 'EDIT': {
      newState = state.map((it) =>
        it.id === action.data.id ? { ...action.data } : it,
      );
      break;
    }
    default:
      return state;
  }
  return newState;
};

case에 따라 다른 state의 상태변화를 처리할 reducer를 정의합니다.

 

4. App 컴포넌트 안에서 onCreate, onRemove, onEdit의 함수 구현하기

const [data, dispatch] = useReducer(reducer, []);
const dataId = useRef(6);

먼저 useReducer와 useRef를 호출합니다.

dummyData에서 id를 1~5까지 정의해놨기 때문에, dataId의 초기값은 6부터 시작하도록 만들었습니다.

//CREATE
const onCreate = (date, contents, emotion) => {
  dispatch({
    type: 'CREATE',
    data: {
      id: dataId.current,
      date: new Date(date).getTime(),
      contents,
      emotion,
    },
  });
  dataId.current += 1;
};
//REMOVE
const onRemove = (targetId) => {
  dispatch({ type: 'REMOVE', targetId });
};
//EDIT
const onEdit = (targetId, date, contents, emotion) => {
  dispatch({
    type: 'EDIT',
    data: {
      id: targetId,
      date: new Date(date).getTime(),
      contents,
      emotion,
    },
  });
};

이 함수에서 하는 일은, reducer에서 할일을 구분할 수 있도록 type과 처리할 데이터를 reducer에 전달하는 것입니다.

 

각 기능별로 코드를 설명해보겠습니다.

- onCreate는 날짜, 내용, 감정을 받아서 새 일기객체를 생성한 뒤 'CREATE' 타입과 함께 action으로 전달합니다.

그러면 3번의 코드 속 reducer에서 이걸 받아서 해당 객체로 newItem을 생성한 후,

state에 추가해 newState를 만듭니다.

- onRemove는 targetId를 받아서 타입과 함께 그대로 action으로 전달합니다.

그러면 reducer에서 이걸 받아서 해당 id의 일기를 지워 newState를 반환합니다.

- onEdit은 targetId, 날짜, 내용, 감정 받아서 날짜를 업데이트한 후,

데이터를 만들어서 타입과함께 action으로 전달합니다.

그러면 reducer에서 받아서 해당 id의 일기 객체를 action의 데이터로 수정합니다.

 

2. Context로 컴포넌트 관리

우리는 DiaryItem 컴포넌트에서 일기 삭제와 수정을 다룰 것인데,

이때 onRemove와 onEdit이 DiaryList를 거쳐서 DiaryItem으로 가야 한다는 것이 문제가 됩니다.

DiaryList는 그저 거쳐가기만 하므로, 코드 수정 시 불편함이 생깁니다.

이처럼 실제로 prop을 사용하지 않으면서 전달을 위해 컴포넌트들을 거쳐가는 것을 Props Drilling이라고 합니다.

 

이를 해결하기 위해 나온 것이 Context입니다.

위 구조도처럼 Provider를 사용하면, 원하는 자식 컴포넌트에 props를 바로 전달해 Props Drilling을 없앨 수 있습니다.

그리고 Context란 위처럼 provider가 공급하는 모든 data에 접근할 수 있는 컴포넌트의 영역을 말합니다.

 

먼저 일기 State를 공급할 Context를 만들어 공급해봅시다.

1. 일기 State를 관리할 Context 선언과 export

export const DiaryStateContext = React.createContext();

Context도 다른 컴포넌트들처럼 export를 거칩니다.

다만 선언을 App.js에서 하므로, export default 대신 그냥 export를 써서 보냅니다.

App.js 파일이므로 export default는 app 컴포넌트가 할 것입니다.

 

2. App.js에서 Provider 컴포넌트만들기

return (
  <DiaryStateContext.Provider value={data}>
    <BrowserRouter>
      <div className='App'>
        <h2>App.js</h2>
        <Routes>
          <Route path='/' element={<Home />} />
          <Route path='/new' element={<New />} />
          <Route path='/edit' element={<Edit />} />
          <Route path='/diary/:id' element={<Diary />} />
        </Routes>
      </div>
    </BrowserRouter>
  </DiaryStateContext.Provider>
);

이렇게 return 속 다른 태그들을 다 Provider로 감쌉니다.

속성의 value={data}는 앞의 useReducer에서 선언한 그 data 상태를 자식 컴포넌트들이 다 공유할 수 있다는 뜻입니다.

 

 

다음으로 state 말고 Dispatch 함수들을 공급할 Context도 만들어봅시다.

1. 일기 Dispatch를 관리할 Context 선언과 export

export const DiaryDispatchContext = React.createContext();

2. 리턴에서 다른 태그들 감싸기

return ( 
<DiaryStateContext.Provider value={data}>
  <DiaryDispatchContext.Provider value={{onCreate, onEdit, onRemove}}>
    <BrowserRouter>
      <div className='App'>
        <h2>App.js</h2>
        <Routes>
          <Route path='/' element={<Home />} />
          <Route path='/new' element={<New />} />
          <Route path='/edit' element={<Edit />} />
          <Route path='/diary/:id' element={<Diary />} />
        </Routes>
      </div>
    </BrowserRouter>
  </DiaryDispatchContext.Provider>
</DiaryStateContext.Provider>
);

DiaryDispatchContext의 Provider로 onCreate, onEdit, onRemove의 Dispatch함수를 공급하게 하였습니다.

 

리턴문을 보면 DiaryStateContext의 Provider가 가장 바깥을 감싸고 있고,

그 안에 DiaryDispatchContext의 Provider가 있는 것을 확인할 수 있습니다.

 

오늘은 여러 기초 프로그램 세팅과 더불어 전체 프로그램에서 공통적으로 사용할 여러 컴포넌트를 만들고,

일기 state를 편하게 관리하도록 Reducer와 Context를 만들어보았습니다.

다음에는 본격적으로 각 페이지들을 만들어보도록 하겠습니다.

728x90

'Udemy' 카테고리의 다른 글

[React] 일기장 프로젝트 - 후기  (1) 2023.04.30
[React] 일기장 프로젝트 2. 페이지 구현 - 홈화면  (0) 2023.04.25
[React] 일기장 프로젝트 0. 페이지 라우팅  (0) 2023.04.24
[React] 리액트 생애주기와 useEffect  (0) 2023.04.17
[React] 데이터 삭제하기, 데이터 수정하기  (0) 2023.04.17
    밍비
    밍비

    티스토리툴바