이 날은 useContext()라는 React Hook과 router에 대해 배웠다. 얼른 학습내용을 정리하며 소화시켜보자!

React Hook - useContext()
많은 컴포넌트에 props를 한번에 전달해야하는 경우에 사용한다.
useContext를 사용하면 컴포넌트의 재사용이 어려워질 수 있어서 꼭 필요할 때만 사용해야 한다.
수업시간에 실험했던 예시 코드를 살펴보자.
// ThemeContext.js
import { createContext } from 'react';
export const ThemeContext = createContext();
// Box.jsx
import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
export default function Box() {
const { isDark, setIsDark } = useContext(ThemeContext);
return (
<>
<h2>Theme Context 사용하기</h2>
<div style={isDark ? { backgroundColor: '#000', color: '#fff' } : {}}>
Theme: {isDark ? 'night' : 'day'}
</div>
<button onClick={() => setIsDark(!isDark)}>테마 변경하기</button>
</>
);
}
// app.js
import { useState } from 'react';
import { ThemeContext } from './contexts/ThemeContext';
import Box from './components/Box';
function App() {
const [isDark, setIsDark] = useState(false);
return (
<div className="App">
<h1>context API 사용하기</h1>
<div>전역적으로 접근 가능한 값이 있을 때 접근을 쉽게 하려고 사용함</div>
<ThemeContext.Provider value={{ isDark, setIsDark }}>
<Box />
</ThemeContext.Provider>
</div>
);
}
export default App;
useContext 기능으로 다크모드 전환 버튼 만들기
- 일단 ThemeContext.js에서, useContext 기능을 사용하기 위해 react로부터 import했고, ThemeContext라는 변수에 createContext() 메소드를 담았다.
- ThemeContext라는 요소가 어떤 기능을 할지 Box.jsx 컴포넌트에 작성하는데,
우선 useContext 기능을 사용하기 위해 import했고, 앞서 만든 ThemeContext 파일도 import한다. - 함수형 컴포넌트를 선언하고, useContext 기능을 사용하여 전역적으로 접근 가능한 context 중에 인자로 전달된 ThemeContext에 접근하겠다고 선언했다.
- return문 내부에는 브라우저에서 보여줄 내용을 작성했다.
- 그리고 이렇게 만들어진 파일들을 사용하기 위해 app.js에서 import하고, 현재 상태를 확인하기 위해 isDark, setIsDark를 state로 관리한다.
- 미리 만들어둔 ThemeContext를 컴포넌트처럼 불러오는데, provider는 context를 하위 컴포넌트에게 전달하는 역할을 하고, value 속성에 들어오는 값이 모든 하위 컴포넌트에서 접근할 수 있는 데이터의 내용이다.
→ 위 코드에서는 ThemeContext.Provider 아래에 있는 Box 컴포넌트만 ThemeContext가 관리하는 isDark, setIsDark에 접근할 수 있다.
(value를 객체형으로 넣을 때 key와 value가 같으면 생략 가능하다.)
useContext 기능으로 이름과 나이를 변경할 수 있는 입력창 만들기
// AgeContext.js
import { createContext } from 'react';
const defaultAge = {
age: 0,
setAge: () => {},
};
export const AgeContext = createContext(defaultAge);
// UserContext.js
import { createContext } from 'react';
const defaultUser = {
name: 'layla',
setName: () => {},
};
export const UserContext = createContext(defaultUser);
- 일단 context.js 파일을 만들어서, createContext를 react로부터 import 해서 context를 만든다.
- 여기에 들어갈 기본값을 객체 형태로 설정하는데, 기본값에 적혀있는 setter 함수는 이름/나이를 변경하는 state의 setter 함수가 될 것이다.
- 만들어진 context를 사용하기 위해 export 하며, createContext의 인자로 앞서 설정한 기본값을 준다.
// AgeProvider.jsx
import { useState } from 'react';
import { AgeContext } from '../../contexts/AgeContext';
export default function AgeProvider(props) {
const [age, setAge] = useState(0);
return (
<AgeContext.Provider value={{ age, setAge }}>
{props.children}
</AgeContext.Provider>
);
}
// UserProvider.jsx
import { useState } from 'react';
import { UserContext } from '../../contexts/UserContext';
export default function UserProvider(props) {
const [name, setName] = useState('layla');
return (
<UserContext.Provider value={{ name, setName }}>
{props.children}
</UserContext.Provider>
);
}
- provider.jsx 파일에서는 만들어둔 context 파일을 사용하기 위해 import한다.
- 현재 상태를 관리하기 위해 useState를 import하고, 구조분해하여 useState() 메서드에 담고 초기값을 설정했다.
→ state의 경우 provider 컴포넌트를 따로 만들어서 해당하는 state를 각자의 provider에서 관리한다. (이렇게 하는게 유지/보수가 더 쉬울 것 같다는 개인적인 생각.) - 함수형 컴포넌트를 만들어서 인자로 props를 넘기고, return 내부에 context.provider로 어떤 데이터를 보낼지 작성한다. 이렇게 보내진 데이터는 context.provider 하위에 있는 컴포넌트들만 접근할 수 있다.
// Profile.jsx
import { useContext, useRef } from 'react';
import { AgeContext } from '../contexts/AgeContext';
import { UserContext } from '../contexts/UserContext';
export default function Profile() {
const { age, setAge } = useContext(AgeContext);
const { name, setName } = useContext(UserContext);
const ageRef = useRef();
const nameRef = useRef();
const ChangeInfo = () => {
if (
nameRef.current.value.length < 1 ||
nameRef.current.value.trim() === ''
) {
alert('이름을 다시 입력해주세요!');
nameRef.current.focus();
return;
} else if (
ageRef.current.value.length === 0 ||
ageRef.current.value.trim() === '' ||
Number(ageRef.current.value) < 0
) {
alert('나이를 다시 입력해주세요!');
ageRef.current.focus();
return;
}
setAge(Number(ageRef.current.value));
setName(nameRef.current.value);
ageRef.current.value = '';
nameRef.current.value = '';
};
return (
<>
<h3>user Profile</h3>
<div>이름: {name}</div>
<div>나이: {age}</div>
<input type="text" placeholder="이름 입력하세요." ref={nameRef} />
<button onClick={ChangeInfo}>이름 변경하기</button>
<br />
<input type="number" placeholder="나이를 입력해주세요." ref={ageRef} />
<button onClick={ChangeInfo}>프로필 변경~</button>
</>
);
}
- 앞서 만든 모든 파일과 브라우저에서 보여질 내용을 Profile.jsx에 작성한다.
- 기능을 사용하려고 useContext, useRef를 import했고, 만들어둔 context 파일도 사용을 위해 import했다.
- 함수형 컴포넌트 선언문에 useContext 기능으로 넘겨받을 데이터를 구조분해하여 담았고, 해당 메서드의 인자가 이 데이터들을 받게 된다.
- 입력창에 들어온 값으로 브라우저에 보여지는 값이 바뀌어야 하니까 useRef 기능으로 DOM 요소에 접근할 수 있도록 설정하고, input 태그에 ref 속성으로 연결했다.
- 버튼을 클릭했을 때 입력창에 들어온 값을 확인하여, 값이 정상적이지 않을 경우에 alert를 띄우고 입력창에 포커스될 수 있도록 if문으로 코드를 작성했다.
- 입력창에 들어오는 값이 앞으로 변화될 값이기 때문에, setter함수의 인자로 넣어주었다. 그리고 useRef.current.value에 빈 문자열을 할당하여 입력이 끝나면 input을 reset 시킬 수 있도록 했다.
- 브라우저에 보여질 내용을 return문 내부에 작성했다.
// App.js
import AgeProvider from './components/provider/AgeProvider';
import UserProvider from './components/provider/UserProvider';
import Profile from './components/Profile';
function App() {
return (
<div className="App">
<AgeProvider>
<UserProvider>
<Profile />
</UserProvider>
</AgeProvider>
</div>
);
}
export default App;
context와 컴포넌트 제작이 끝나고, app.js에서 컴포넌트들 import해서 return문 내부에 불러와서 사용한다.
React - Router
라우팅(routing)은 특정 페이지에서 다른 페이지로 넘어갈 수 있도록 경로를 설정하는 것을 의미한다.
HTML에서는 다수의 HTML 파일이 있고 a href를 통해 해당 파일로 이동했지만, React에서는 App.js라는 큰 틀 아래에서 컴포넌트만 바뀌어서 렌더링된다.
💡 여기서 잠깐!
React의 router는 단일 웹페이지로 돌아가는 애플리케이션, SPA (Single Page Application)이다.
SPA는 브라우저에서 JS를 이용해 웹 페이지의 HTML 요소를 동적으로 생성/조작하고,
페이지가 하나이기 때문에 검색 엔진 최적화(SEO)에는 적합하지 않다.
여러 페이지를 만들고 요청에 맞는 페이지를 보여주는 방식은 MPA (Multi Page Application)라고 한다.
React Router는 개발자가 주소별로 다른 컴포넌트를 보여주기 위해 사용하는 라이브러리이고, 사용하려면 아래 명령어로 설치해야 한다.
$ npm i react-router-dom
Browser Router
Browser Router는 App 컴포넌트에서 발생하는 주소값의 변경을 감지하여, 새로고침을 하지 않아도 새로운 컴포넌트를 렌더링해주는 역할을 한다. React Router 기능을 제대로 사용하려면, 반드시 아래 사진처럼 index.js에서 App 컴포넌트를 BrowserRouter로 감싸주어야 한다.
→ 크기 순서: index.js > app.js > 각종 컴포넌트
여기까지 설정이 완료됐다면 React Router 기능을 사용할 준비는 끝났다. 이제 코드를 작성해보면 되는데, 그 전에 반드시 알아야 하는 개념들이 있다.
- Routes, Route - 경로가 일치하는 컴포넌트를 렌더링해주도록 하는 것.
"여기로 이동하면 이것을 보여주세요" 라는 의미.
route의 속성으로 들어오는 path에 경로를, element에 보여줄 컴포넌트를 작성한다.
다수의 route를 routes로 묶어서 사용한다. - Link - 반드시 대문자로 작성해야하는 Link 컴포넌트는 경로를 변경해주는 기능을 갖고 있다.
HTML의 a 태그는 새로고침해서 렌더링하지만, Link는 새로고침을 하지 않으면서 페이지가 전환된다. - useNavigate() - Link 컴포넌트와 비슷한 기능을 가진 React Hook
뒤로 가기, 앞으로 가기, 홈으로 등을 설정할 수 있다. - Page Not Found - 잘못된 URL에 대한 예외 처리를 의미한다.
문제가 생겼을 때, 직접 만든 페이지를 보여주는 것이 전체적으로 통일감이 있어서 좋다.
앞서 설명한 개념들을 응용해보면 아래 코드와 같다.
// Header.jsx
import { Link } from 'react-router-dom';
export default function Header() {
return (
<>
<header>
<nav>
<Link to="/">홈으로</Link>
<br />
<Link to="/board">게시판</Link>
<br />
<Link to="/profile">마이 페이지</Link>
</nav>
</header>
</>
);
}
- 일단 Link 기능을 사용하기 위해 react-router-dom으로부터 import를 한다.
- 함수형 컴포넌트 선언문 return 내부에 코드를 작성하는데, to 속성의 값으로 이동할 경로를 작성한다.
(사용법이 HTML의 a href랑 아주 비슷함)
// BoardDetail.jsx
import { useNavigate } from 'react-router-dom';
export default function BoardDetail() {
const navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-1)}>뒤로 가기</button>
<button onClick={() => navigate('/board')}>게시판으로</button>
<button onClick={() => navigate('/')}>홈으로</button>
<h1>여기는 Board Detail 페이지 컴포넌트</h1>
</>
);
}
- useNavigate 기능을 사용하기 위해 react-router-dom으로부터 import를 한다.
- 함수형 컴포넌트 선언문 내부에 useNavigate() 메서드를 navigate라는 변수에 저장한다.
- return 내부에 브라우저에서 보여줄 부분을 작성하는데, 버튼에 onClick으로 navigate 함수를 연결했다.
→ 슬래시는 루트 경로를 의미하여 홈으로 이동하고, -1은 이전에 본 페이지로 돌아간다.
// Board.jsx
import { useNavigate } from 'react-router-dom';
export default function Board() {
const navigation = useNavigate();
return (
<>
<h1>board 컴포넌트</h1>
<div onClick={() => {navigation('1')}}>
1번 게시글 보러 가기
</div>
</>
);
}
- 마찬가지로 useNavigate 기능을 사용하기 위해 react-router-dom으로부터 import를 한다.
- 함수형 컴포넌트 선언문 내부에 useNavigate() 메서드를 navigation이라는 변수에 저장한다.
- return 내부 div에 onClick으로 navigation 함수를 연결했는데,
이 함수에 인자로 슬래시 없이 값을 보내면 주소 뒤에 슬래시가 붙고, 슬래시를 작성하면 루트 기준으로 이동한다.
주소 뒤에 슬래시 붙는 경우: navigation('1'); → localhost/board/1
루트 기준으로 이동하는 경우: navigation('/1'); → localhost/1
// app.js
import { Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Home from './pages/Home';
import Profile from './pages/Profile';
import Board from './pages/Board';
import BoardDetail from './pages/BoardDetail';
import NotFound from './pages/NotFound';
function App() {
return (
<div className="App">
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/board" element={<Board />} />
<Route path="/board/1" element={<BoardDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}
export default App;
- 이렇게 만든 컴포넌트와 페이지들을 app.js에서 전부 import 해서 route로 경로를 설정하면, 해당 컴포넌트를 클릭했을 때 그에 맞는 경로로 주소창이 바뀌며 선택한 컴포넌트가 보여지게 된다.
→ 경로에 슬래시 하나만 입력하면 루트 경로를 의미한다.
→ 경로에 별(*)을 입력하면, 앞서 설정한 경로들 이외의 URL일 경우에 NotFound 컴포넌트를 보여주게 된다.
여기까지가 이 날 배운 내용이었다!
useContext()를 수업 시간에 들을 때는 분명 이해했는데, 지금 너무 피곤해서 그런지 다시 코드를 읽어보면서 약간 아리송한 부분이 있어서 추가적으로 연구를 조금 더 해야할 것 같다.

'💻 Frontend > React' 카테고리의 다른 글
[코딩온] 프론트엔드 입문 Day 45~46 (Sass with React) (0) | 2024.04.22 |
---|---|
[코딩온] 프론트엔드 입문 Day 45 (React 파라미터&쿼리, styling) (0) | 2024.04.19 |
[코딩온] 프론트엔드 입문 Day 43 (React Lifecycle, Hook 1) (0) | 2024.04.15 |
[코딩온] 프론트엔드 입문 Day 42 (React event, ref) (0) | 2024.04.13 |
React - 다수의 객체로 이루어진 배열 데이터 다루기 (0) | 2024.04.09 |