[LG CNS AM CAMP 1기] 프론트엔드 9 | React

letthem·2025년 1월 7일
0

LG CNS AM CAMP 1기

목록 보기
9/16
post-thumbnail

할 일 목록 기능 구현

App 컴포넌트에 할 일 목록 데이터를 관리하는 상태 변수를 추가하고, TodoList 컴포넌트의 props 변수로 전달

import { useState } from 'react';
import './App.css';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, checked: true, text: "자바스크립트 공부하기" },
    { id: 2, checked: false, text: "리액트 공부하기" },
    { id: 3, checked: false, text: "할 일 목록 앱 만들기" },
  ]);

  return (
    <TodoTemplate>
      <TodoInsert />
      <TodoList todos={todos} />
    </TodoTemplate>
  );
}

export default App;

TodoList 컴포넌트에서 props로 전달된 배열 값을 이용해서 할 일(TodoListItem)을 출력

import TodoListItem from './TodoListItem';
import './TodoList.css';

export default function TodoList({ todos }) {
  return (
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem key={todo.id} todo={todo}/>
      ))}
    </div>
  );
}

TodoListItem 컴포넌트에서 props로 전달된 할 일 내용(todo)을 출력

import './TodoListItem.css';
import { MdCheckBox, MdCheckBoxOutlineBlank, MdRemoveCircleOutline } from 'react-icons/md';

const TodoListItem = ({ todo }) => {
  const { id, checked, text } = todo;

  return (
    <div className="TodoListItem">
      <div className={checked ? 'checkBox checked' : 'checkBox'}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove">
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

export default TodoListItem;

할 일 추가 기능 구현

TodoInsert 컴포넌트에 입력 기능을 구현

import { useState } from 'react';
import './TodoInsert.css';
import { MdAdd } from 'react-icons/md';

const TodoInsert = () => {
  const [value, setValue] = useState('');
  const changeValue = e => setValue(e.target.value);

  return (
    <form className="TodoInsert">
      <input type="text" placeholder="할일을 입력하세요." value={value} onChange={changeValue}/>
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};
export default TodoInsert;

App 컴포넌트에 todos 상태변수에 값을 추가하는 함수를 정의하고, 해당 함수를 TodoInsert 컴포넌트의 props 변수로 전달

import { useRef, useState } from 'react';
import './App.css';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, checked: true, text: '자바스크립트 공부하기' },
    { id: 2, checked: false, text: '리액트 공부하기' },
    { id: 3, checked: false, text: '할 일 목록 앱 만들기' },
  ]);

  const nextId = useRef(4);

  // Create ⭐️⭐️⭐️
  const insertTodo = (text) => {
    const newTodos = todos.concat({ id: nextId.current, checked: false, text });
    setTodos(newTodos);
    nextId.current++;
  };

  return (
    <TodoTemplate>
      <TodoInsert insertTodo={insertTodo} />
      <TodoList todos={todos} />
    </TodoTemplate>
  );
}

export default App;

TodoInsert 컴포넌트에서 버튼을 클릭했을 때 props 변수로 전달 받은 insertTodo 함수를 호출

import { useState } from 'react';
import './TodoInsert.css';
import { MdAdd } from 'react-icons/md';

const TodoInsert = ({ insertTodo }) => {
  const [value, setValue] = useState('');
  const changeValue = (e) => setValue(e.target.value);

  const onSubmit = e => {
    e.preventDefault();
    insertTodo(value);
    setValue("");
  }
  
  return (
    <form className="TodoInsert" onSubmit={onSubmit}>
      <input type="text" placeholder="할일을 입력하세요." value={value} onChange={changeValue} />
      <button type="submit" >
        <MdAdd />
      </button>
    </form>
  );
};
export default TodoInsert;

button의 type="submit"은 form이 submit되기 때문에 form의 onSubmit에 함수를 넣어주자

할 일 삭제 기능 구현

CRUD

create read update delete

App 컴포넌트에 todos 상태변수에 값을 삭제하는 함수를 정의하고, 해당 함수를 todoList 컴포넌트에 props 변수로 전달

import { useRef, useState } from 'react';
import './App.css';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, checked: true, text: '자바스크립트 공부하기' },
    { id: 2, checked: false, text: '리액트 공부하기' },
    { id: 3, checked: false, text: '할 일 목록 앱 만들기' },
  ]);

  const nextId = useRef(4);

  const insertTodo = (text) => {
    const newTodos = todos.concat({ id: nextId.current, checked: false, text });
    setTodos(newTodos);
    nextId.current++;
  };

  // Delete ⭐️⭐️⭐️
  const removeTodo = (id) => {
    const newTodos = todos.filter((todo) => todo.id !== id);
    setTodos(newTodos);
  };

  return (
    <TodoTemplate>
      <TodoInsert insertTodo={insertTodo} />
      <TodoList todos={todos} removeTodo={removeTodo} />
    </TodoTemplate>
  );
}

export default App;

TodoList 컴포넌트에서 props로 전달받은 removeTodo 함수를 TodoListItem 컴포넌트의 props로 전달

import TodoListItem from './TodoListItem';
import './TodoList.css';

export default function TodoList({ todos, removeTodo }) {
  return (
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem key={todo.id} todo={todo} removeTodo={removeTodo} />
      ))}
    </div>
  );
}

TodoListItem 컴포넌트에서 삭제 버튼을 클릭하면 props 변수로 전달받은 removeTodo 함수를 호출

import './TodoListItem.css';
import { MdCheckBox, MdCheckBoxOutlineBlank, MdRemoveCircleOutline } from 'react-icons/md';

const TodoListItem = ({ todo, removeTodo }) => {
  const { id, checked, text } = todo;

  return (
    <div className="TodoListItem">
      <div className={checked ? 'checkBox checked' : 'checkBox'}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove" onClick={() => removeTodo(id)}> {/* id 전달 */}
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

export default TodoListItem;

할 일 수정 기능 구현

App 컴포넌트에 todos 상태변수에 값을 수정(checked 값 토글)하는 함수를 정의하고, 해당 함수를 TodoList 컴포넌트의 props로 전달

import { useRef, useState } from 'react';
import './App.css';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, checked: true, text: '자바스크립트 공부하기' },
    { id: 2, checked: false, text: '리액트 공부하기' },
    { id: 3, checked: false, text: '할 일 목록 앱 만들기' },
  ]);

  const nextId = useRef(4);

  const insertTodo = (text) => {
    const newTodos = todos.concat({ id: nextId.current, checked: false, text });
    setTodos(newTodos);
    nextId.current++;
  };

  const removeTodo = (id) => {
    const newTodos = todos.filter((todo) => todo.id !== id);
    setTodos(newTodos);
  };

  // Update ⭐️⭐️⭐️⭐️⭐️
  const toggleTodo = (id) => {
    const newTodos = todos.map((todo) =>
      todo.id === id ? { ...todo, checked: !todo.checked } : todo
    );
    setTodos(newTodos);
  };

  return (
    <TodoTemplate>
      <TodoInsert insertTodo={insertTodo} />
      <TodoList todos={todos} removeTodo={removeTodo} toggleTodo={toggleTodo} />
    </TodoTemplate>
  );
}

export default App;
const toggleTodo = (id) => {
  const newTodos = todos.map((todo) =>
    todo.id === id ? { ...todo, checked: !todo.checked } : todo
  );
  setTodos(newTodos);
};

이 코드 아주 중요하다 !! ⭐️⭐️⭐️⭐️⭐️

TodoList 컴포넌트에서 props로 전달받은 toggleTodo 함수를 TodoListItem 컴포넌트의 props로 전달

import TodoListItem from './TodoListItem';
import './TodoList.css';

export default function TodoList({ todos, removeTodo, toggleTodo }) {
  return (
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem key={todo.id} todo={todo} removeTodo={removeTodo} toggleTodo={toggleTodo} />
      ))}
    </div>
  );
}

TodoListItem 컴포넌트에서 체크 박스를 클릭하면 props로 전달받은 toggleTodo 함수를 호출

import './TodoListItem.css';
import { MdCheckBox, MdCheckBoxOutlineBlank, MdRemoveCircleOutline } from 'react-icons/md';

const TodoListItem = ({ todo, removeTodo, toggleTodo }) => {
  const { id, checked, text } = todo;

  return (
    <div className="TodoListItem">
      <div className={checked ? 'checkBox checked' : 'checkBox'} onClick={() => toggleTodo(id)}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove" onClick={() => removeTodo(id)}>
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

export default TodoListItem;

배포 가능한 형태로 빌드

빌드

npm run build

배포 가능한 형태로 빌드한 결과물 ⬇️

개발 서버를 설치 및 실행

npm i http-server -g ← 웹 서버를 설치

cd build ← 빌드 디렉토리로 이동

index.html <= 기본 페이지. 해당 페이지에서 빌드된 JS 파일을 로딩

npx http-server ← 웹 서버를 실행. 현재 디렉토리를 Web Document Root 디렉토리로 설정한 상태로 웹 서버를 실행.

  • Web Document Root 디렉토리 : index.html 이 있는 디렉토리

브라우저를 이용해서 애플리케이션 실행을 확인


Context API와 useContext 훅을 이용해서 insertTodo, removeTodo, toggleTodo 함수와 todos 변수를 props 변수로 전달하지 않고 사용할 수 있도록 수정

1. 컨텍스트 생성 => TodoContext.js

import { createContext } from "react";

const TodoContext = createContext();

export default TodoContext;

2. 프로바이더 생성 => TodoProvider.js

import { useRef, useState } from 'react';
import TodoContext from './TodoContext';

export default function TodoProvider({ children }) {
  const [todos, setTodos] = useState([
    { id: 1, checked: true, text: '자바스크립트 공부하기' },
    { id: 2, checked: false, text: '리액트 공부하기' },
    { id: 3, checked: false, text: '할 일 목록 앱 만들기' },
  ]);

  const nextId = useRef(4);

  // create
  const insertTodo = (text) => {
    const newTodos = todos.concat({ id: nextId.current, checked: false, text });
    setTodos(newTodos);
    nextId.current++;
  };

  // delete
  const removeTodo = (id) => {
    const newTodos = todos.filter((todo) => todo.id !== id);
    setTodos(newTodos);
  };

  // update
  const toggleTodo = (id) => {
    const newTodos = todos.map((todo) =>
      todo.id === id ? { ...todo, checked: !todo.checked } : todo
    );
    setTodos(newTodos);
  };

  return (
    <TodoContext.Provider value={{ todos, insertTodo, removeTodo, toggleTodo }}>
      {children}
    </TodoContext.Provider>
  );
}

3. 프로바이더 설정 => App.js => TodoProvider 컴포넌트로 자식 컴포넌트를 감싸고, 컨텍스트를 통해서 제공받을 수 있는 상태변수, 상태변수 변경 함수, props 변수를 삭제

import './App.css';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';
import TodoProvider from './components/TodoProvider';

function App() {
  return (
    <TodoProvider>
      <TodoTemplate>
        <TodoInsert />
        <TodoList />
      </TodoTemplate>
    </TodoProvider>
  );
}

export default App;

4. 컨텍스트 변수를 사용 => 하위 컴포넌트

TodoInsert.js

import { useContext, useState } from 'react';
import './TodoInsert.css';
import { MdAdd } from 'react-icons/md';
import TodoContext from './TodoContext';

const TodoInsert = () => {
  const { insertTodo } = useContext(TodoContext);
  const [value, setValue] = useState('');

  const changeValue = (e) => setValue(e.target.value);

  const onSubmit = (e) => {
    e.preventDefault();
    insertTodo(value);
    setValue('');
  };

  return (
    <form className="TodoInsert" onSubmit={onSubmit}>
      <input type="text" placeholder="할일을 입력하세요." value={value} onChange={changeValue} />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};
export default TodoInsert;

TodoList.js

import TodoListItem from './TodoListItem';
import './TodoList.css';
import { useContext } from 'react';
import TodoContext from './TodoContext';

export default function TodoList() {
  const { todos } = useContext(TodoContext);

  return (
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem key={todo.id} todo={todo} />
      ))}
    </div>
  );
}

TodoListItem.js

🚨 todo 는 context 변수가 아니고 부모에게 받는 props 다 !

import { useContext } from 'react';
import './TodoListItem.css';
import { MdCheckBox, MdCheckBoxOutlineBlank, MdRemoveCircleOutline } from 'react-icons/md';
import TodoContext from './TodoContext';

const TodoListItem = ({ todo }) => {
  const { removeTodo, toggleTodo } = useContext(TodoContext);
  const { id, checked, text } = todo;

  return (
    <div className="TodoListItem">
      <div className={checked ? 'checkBox checked' : 'checkBox'} onClick={() => toggleTodo(id)}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove" onClick={() => removeTodo(id)}>
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

export default TodoListItem;

리액트 라우터

정적 서비스 : 파일 고정. 파일을 요청해야만 내려갔다.

⬇️

동적 서비스 : CGI 에만 요청해서 CGI가 해당 파일만 제공

  • CGI(Common Gateway Interface) 가 생긴 이후 동적 서비스가 가능해졌다. (Web의 시작)

여러 명의 요청을 처리할 수 있도록 멀티 쓰레드 환경으로 : 자바 servlet/JSP

서버가 내뱉는 최종적인 결과물은 html 문서이다.

MPA : Multiple Page Application

SPA : 최초 index.html 가 한 번에 내려오고 라우팅을 통해 필요에 따라 보여지게 된다.

라우터 종류

BrowserRouter

  • React Router v6 이전부터 사용되어 온 방식으로, 선언적이고 간단한 설정을 제공
  • HTML5의 History API를 기반으로 URL을 관리
  • 애플리케이션의 루트 컴포넌트로 설정하며, 내부에서 <Routes>, <Route>를 통해 라우팅 구조를 정의
  • 라우터 데이터를 명시적으로 로드하거나 관리하는 기능이 제한되어 있음
import { BrowerRouter, Routes, Route } from "react-router-dom";
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </BrowserRouter>
  );
}

라우터 적용

RouterProvider

  • React Router v6.4 이상에서 도입된 방식으로, 라우팅을 더 유연하고 동적으로 설정할 수 있도록 설계
  • createBrowserRouter와 함께 사용되며, 데이터 로딩, 에러 핸들링, 동적 라우팅 설정 등이 포함된 라우트 객체 기반의 라우팅을 제공
  • 라우팅 설정이 객체 기반으로 변경되며, 데이터 로드와 에러 처리 기능이 포함
  • 큰 규모의 프로젝트에 유용하다
const router = createBrowserRouter([
  {
  	path: "/",
    element: <Home />,
    loader: async () => {
      const data = await fetchDataFromHome();
      return data;
    },
    errorElement: <ErrorPage />
  },
  {
  	path: "/about",
    element: <About />,
  },
  {
  	path: "/contact",
    element: <Contact />,
  },
]);

function App() {
  return <RouterProvider router={router} />;

라우팅할 페이지 컴포넌트 생성

Home.js

export default function Home() {
  return (
    <div>
      <h1>Home</h1>
      <h2>가장 먼저 보이는 페이지</h2>
    </div>
  );
}

About.js

export default function About() {
  return (
    <div>
      <h1>About</h1>
      <h2>리액트 라우트 연습</h2>
    </div>
  );
}

BrowserRouter 사용

App.js 파일에 react-router-dom에 내장되어 있는 BrowserRouter 컴포넌트를 추가

import { BrowserRouter } from 'react-router-dom'
import Home from './Home';
function App() {
  return (
    <BrowserRouter></BrowserRouter>
  );
}

export default App;

Route 컴포넌트로 특정 경로에 원하는 컴포넌트 보여주기

Route 컴포넌트 : 주소 패턴에 따라 다른 컴포넌트를 제공

ex) http://localhost:3000/ ⇒ Home 컴포넌트가 제공
http://localhost:3000/about ⇒ About 컴포넌트가 제공

<Routes>
  <Route path="주소 규칙" element={보여 줄 컴포넌트 JSX} />
</Routes>

App.js 파일에 Home, About 컴포넌트로의 라우팅을 추가

import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Home from './Home';
import About from './About';
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home/>}/>
        <Route path="/about" element={<About />}/>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

브라우저 주소창에 아래 주소를 입력해서 컴포넌트 실행을 확인

http://localhost:3000/ ⇒ Home 컴포넌트가 제공

http://localhost:3000/about ⇒ About 컴포넌트가 제공

Link 컴포넌트 => 클릭하면 다른 주소로 이동시켜주는 컴포넌트

  • HTML5 History API를 사용해서 브라우저의 주소만 바꿀 뿐 페이지를 새로 불러오지는 않음
<Link to="경로">링크 이름</Link>
import { BrowserRouter, Link, Route, Routes } from 'react-router-dom'
import Home from './Home';
import About from './About';
function App() {
  return (
    <BrowserRouter>
      <ul>
        <li><Link to="/"></Link></li>
        <li><Link to="/about">소개</Link></li>
      </ul>
      <Routes>
        <Route path="/" element={<Home/>}/>
        <Route path="/about" element={<About />}/>
      </Routes>
    </BrowserRouter>
  );
}

export default App;


  • Link 태그를 사용하면 화면은 바뀌지만 네트워크 탭 요청은 그대로다. (상태변수 초기화 X)
  • a 태그를 사용하면 네트워크 탭 요청이 일어나서 상태변수가 초기화된다. => a 태그를 사용하면 안 된다.(submit 일어나지 않도록 해야한다.)

같은 컴포넌트에 다른 경로 설정

About 컴포넌트로 라우팅하는 /info 경로를 추가

import { BrowserRouter, Link, Route, Routes } from 'react-router-dom'
import Home from './Home';
import About from './About';
function App() {
  return (
    <BrowserRouter>
      <ul>
        <li><Link to="/"></Link></li>
        <li><Link to="/about">소개</Link></li>
        <li><Link to="/info">정보</Link></li>
      </ul>
      <Routes>
        <Route path="/" element={<Home/>}/>
        <Route path="/about" element={<About />}/>
        <Route path="/info" element={<About />}/>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

가변 데이터를 컴포넌트로 전달하는 방법 => 파라미터(parameter) 또는 쿼리 문자열(query string)

  • 파라미터 => /profile/honggildong
  • 쿼리문자열 => /profile?name=honggildong&age=23

파라미터를 이용한 가변 데이터 처리

  • 파라미터 이름은 라우트를 설정할 때 Route 컴포넌트의 path props에 :파라미터이름 형식으로 설정
  • 파라미터 조회(참조)는 useParams 훅 함수를 이용해 객체 형태로 조회

Profile.js => 파라미터로 전달된 사용자 식별자와 일치하는 사람의 정보를 출력

import { useParams } from "react-router-dom";

const users = {
  mrgo: {
    name: '고길동',
    desc: "둘리를 싫어하는 자"
  },
  mrhong: {
    name: '홍길동',
    desc: "호부호형을 원하는 자"
  } 
};

// http://localhost:3000/profile/mrgo
//                               ~~~~ 
//                      userid 변수 이름으로 전달
// 주소에 포함된 사용자 식별자(여기에서는 userid)에 사용자 정보를 출력
export default function Profile() {
  // 주소에 포함된 파라미터를 추출
  const params = useParams();

  // 파라미터에서 userid의 값을 추출해서
  // 해당 값을 이용해서 users 객체에서 일치하는 사용자 정보를 추출
  const profile = users[params.userid];


  return (
    <>
      {
        profile ? (
          <>
            <h1>{profile.name}</h1>
            <h2>{profile.desc}</h2>
          </>
        ) : (
          <h1>일치하는 사용자가 없습니다.</h1>
        )
      }
    </>
  )
}

🔑

const params = useParams();
const profile = user[params.userid];

App.js 파일에 Profile 컴포넌트를 호출하는 링크와 Route를 추가

import { BrowserRouter, Link, Route, Routes } from 'react-router-dom'
import Home from './Home';
import About from './About';
import Profile from './Profile';
function App() {
  return (
    <BrowserRouter>
      <ul>
        <li><Link to="/"></Link></li>
        <li><Link to="/about">소개</Link></li>
        <li><Link to="/info">정보</Link></li>
        <li><Link to="/profile/mrgo">고길동 프로필</Link></li>
        <li><Link to="/profile/mrhong">홍길동 프로필</Link></li>
        <li><Link to="/profile/none">없는 프로필</Link></li>
      </ul>
      
      <Routes>
        <Route path="/" element={<Home/>} />
        <Route path="/about" element={<About />} />
        <Route path="/info" element={<About />} />
        <Route path="/profile/:userid" element={<Profile />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

🔑

<Route path="/profile/:userid" element={<Profile />} />




쿼리 스트링을 이용한 가변 데이터 처리

쿼리 스트링

....?query_name=query_value&query_name2=query_value2
ex) ?name=hong&age=23&phone=01029822002

  • 쿼리 스트링은 Route 컴포넌트에 추가 설정 없이 사용이 가능
  • 쿼리 스트링은 useLocation 훅 함수가 반환하는 location 객체의 search 항목을 활용하여 추출

useLocation 훅 함수 => location 객체를 반환

  • pathname: 현재 주소의 경로 (쿼리 스트링 제외)
  • search: ? 문자를 포함한 쿼리 스트링 값
    • ex) ?name=hong&age=23&phone=01029822002
  • hash: 주소의 # 문자열 뒤의 값
  • state: 페이지로 이동할 때 임의로 넣을 수 있는 상태 값
  • key: location 객체의 고유 값

useSearchParams 훅 함수

  • 리액트 라우터 v6 부터는 쿼리 스트링을 쉽게 조작할 수 있도록 useSearchParams 훅 함수를 제공
const [searchParams, setSearchParams] = useSearchParams();
       ~~~~~~~~~~~~
      - 첫 번째 값은 쿼리 파라미터를 조회하거나 수정하는 메서드들이 담긴 객체를 반환
      	- get 메서드를 통해 특정 쿼리 파라미터를 조회
        - set 메서드를 통해 특정 쿼리 파라미터를 업데이트
        - 만약 조회 시 쿼리 파라미터가 존재하지 않는다면 null을 반환
      - 두 번째 값은 쿼리 파라미터를 객체 형태로 업데이터할 수 있는 함수를 반환

qs 라이브러리 => 쿼리 스트링 파싱을 도와주는 라이브러리

npm install qs

사용하는 방법 ⬇️

const location = useLocation();
const queries = qs.parse(location.search, { ignoreQueryPrefix: true });
      ~~~~~~~                               ~~~~~~~~~~~~~~~~~~~~~~~~
      |                                          ?를 파싱에서 제외
      <-- 쿼리 문자열을 이름, 값 형식으로 반환
          ex) { name: "hong", age: 23 }
                                       

About 컴포넌트를 수정 => location.search 값 중 detail 값이 true인 경우 추가 정보가 출력되도록 수정

import { useLocation } from "react-router-dom";
import qs from 'qs';

export default function About() {
  // 쿼리 스트링을 추출 ==> ?aaaa=aaaa&bbbb=bbbb&cccc=cccc
  const location = useLocation();
  const queries = qs.parse(location.search, { ignoreQueryPrefix: true });
  console.log(queries);

  // http://localhost:3000/about?detail=true&name=hong&age=23 형태로 요청하면
  // { detail: 'true', name: 'hong', age: '23' }
  //           ~~~~~         ~~~~~        ~~~~ <== 쿼리 스트링으로 전달되는 모든 값은 문자열 타입을 가짐
  return (
    <div>
      <h1>About</h1>
      <h2>리액트 라우트 연습</h2>
      {
        queries.detail === "true" && <h2>상세 내역입니다.</h2>
      }
    </div>
  );
}

브라우저 창에서 아래 주소를 입력했을 때 상세 내용 출력 여부를 확인

http://localhost:3000/about
http://localhost:3000/about?
http://localhost:3000/about?none=true
http://localhost:3000/about?none=
http://localhost:3000/about?deatil=true ⇐ 상세 내용이 출력
http://localhost:3000/about?detail=false
http://localhost:3000/about?detail=

useSearchParams 훅을 사용하도록 About 컴포넌트를 수정

import { useSearchParams } from "react-router-dom";

export default function About() {
  /*
  const location = useLocation();
  const queries = qs.parse(location.search, { ignoreQueryPrefix: true });
  */

  const [searchParams, setSearchParams] = useSearchParams();
  const detail = searchParams.get("detail");
  return (
    <div>
      <h1>About</h1>
      <h2>리액트 라우트 연습</h2>
      {
        detail === "true" && <h2>상세 내역입니다.</h2>
      }
    </div>
  );
}

searchParams를 사용하면 별도로 파싱할 필요 없이 get으로 가져오면 되니 간편하다 !

서브 라우트 = 중첩 라우트

복잡한 애플리케이션의 URL과 화면 계층을 효과적으로 관리
Outlet과 children 속성을 활용해 계층 구조를 반영한 라우팅을 설정
특정 라우트의 하위 경로로 구성된 라우트
부모 라우트의 레이아웃을 공유하면서 자식 컴포넌트를 렌더링

profiles.js => Profile 컴포넌트로의 링크(Link)와 라우팅 결과를 출력할 Outlet을 포함하는 컴포넌트

🔑 Outlet

import { Link, Outlet } from "react-router-dom";

export default function Profiles() {
  return (
    <>
      <h1>사용자 목록</h1>
      <ul>
        <li><Link to="/profiles/mrgo">고길동 프로필</Link></li>
        <li><Link to="/profiles/mrhong">홍길동 프로필</Link></li>
        <li><Link to="/profiles/none">없는 프로필</Link></li>
      </ul>
      <hr/>
      {/* Route의 children으로 들어오는 JSX 엘리먼트를 보여주는 역할 */}
      <Outlet />
    </>
  )
}

App.js 파일에 Profile 컴포넌트와 관련된 Link와 Route를 제거하고, Profiles 컴포넌트와 관련된 Link와 Route를 추가

import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Profiles from "./Profiles";

function App() {
  return (
    <BrowserRouter>
      <ul>
        <li><Link to="/"></Link></li>
        <li><Link to="/about">소개</Link></li>
        <li><Link to="/info">정보</Link></li>
        {/*
        <li><Link to="/profile/mrgo">고길동 프로파일</Link></li>
        <li><Link to="/profile/mrhong">홍길동 프로파일</Link></li>
        <li><Link to="/profile/none">없는 프로파일</Link></li>
        */}
        <li><Link to="/profiles">프로파일</Link></li>
      </ul>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/info" element={<About />} />
        {/*
        <Route path="/profile/:userid" element={<Profile />} />
        */}
        <Route path="/profiles" element={<Profiles />}>
          <Route path=":userid" element={<Profile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

공통 레이아웃 컴포넌트

중첩된 라우트와 Outlet 컴포넌트를 이용해 각 페이지(컴포넌트)에서 공통적으로 보여줘야 하는 레이아웃을 처리할 때 유용

Layout.js ⇒ 메뉴를 <header> 태그를 포함하고, 각 메뉴를 클릭했을 때 나타낼 컴포넌트는 <main> 태그에 출력

import { Link, Outlet } from "react-router-dom";
import "./Layout.css";

export default function Layout() {
  return (
    <div>
      <header>
        <ul>
          <li><Link to="/"></Link></li>
          <li><Link to="/about">소개</Link></li>
          <li><Link to="/info">정보</Link></li>
          <li><Link to="/profiles">프로파일</Link></li>
        </ul>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  )
}

App.js 수정 => 메뉴를 제거하고, Layout 컴포넌트로 각 페이지 컴포넌트를 둘러쌈

둘러싸야만 서브라우팅이 되고 컴포넌트들이 자식이 되어 Outlet으로 간다.

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Profiles from "./Profiles";
import Profile from "./Profile";
import Layout from "./Layout";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<Layout />}>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/info" element={<About />} />
          <Route path="/profiles" element={<Profiles />}>
            <Route path=":userid" element={<Profile/>} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Layout으로 둘러싸주기 !! (중첩 라우트 사용)

리액트 라우트의 부가 기능

useNavigate 훅

Link 컴포넌트를 사용하지 않고 페이지를 이동할 때 사용하는 훅 함수

Layout.js ⇒ 이전 페이지로 이동 버튼과 정보 페이지로 이동 버튼을 추가

import { Link, Outlet, useNavigate } from "react-router-dom";
import "./Layout.css";

export default function Layout() {
  const navigate = useNavigate();
  return (
    <div>
      <header>
        <ul>
          <li><Link to="/"></Link></li>
          <li><Link to="/about">소개</Link></li>
          <li><Link to="/info">정보</Link></li>
          <li><Link to="/profiles">프로파일</Link></li>
        </ul>
        <button onClick={() => navigate(-1)}>이전 페이지로 이동</button>
        <button onClick={() => navigate("/info")}>정보 페이지로 이동</button>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  )
}

🔑 이전 페이지로 이동 : onClick={() => navigate(-1)}

링크에서 사용하는 경로가 현재 라우트의 경로와 일치하는 경우 특정 스타일 또는 CSS 클래스를 적용하는 컴포넌트
이 컴포넌트를 사용하면 style 또는 className을 설정할 때 { isActive: boolean } 을 매개변수로 전달받는 함수를 정의할 수 있다.

Layout.js => 소개 메뉴를 클릭하면 글자색을 붉은색으로 설정

import { Link, NavLink, Outlet, useNavigate } from "react-router-dom";
import "./Layout.css";

export default function Layout() {
  const navigate = useNavigate();
  return (
    <div>
      <header>
        <ul>
          <li><Link to="/"></Link></li>
          <li><NavLink to="/about" style={
            ({ isActive }) => isActive ? { color: "red" } : undefined
          }>소개</NavLink></li>
          <li><Link to="/info">정보</Link></li>
          <li><Link to="/profiles">프로파일</Link></li>
        </ul>
        <button onClick={() => navigate(-1)}>이전 페이지로 이동</button>
        <button onClick={() => navigate("/info")}>정보 페이지로 이동</button>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  )
}

Not Found 페이지

Route 컴포넌트의 path props의 값으로 *를 사용하면 아무 텍스트나 매칭한다는 뜻이 되며, 라우트 엘리먼트의 상단에 위치하는 라우트의 규칙을 모두 확인하고, 일치하는 라우트가 없다면 이 라우트가 화면에 나타나게 됨

NotFound.js

const NotFound = () => {
  return (
    <div style={{
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      fontSize: 64,
      position: "absolute",
      width: "100%",
      height: "100%",
    }}>404 Not Found</div>
  );
}
export default NotFound;

App.js ⇒ NotFound 컴포넌트를 라우트로 등록

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Profiles from "./Profiles";
import Profile from "./Profile";
import Layout from "./Layout";
import NotFound from "./NotFound";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<Layout />}>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/info" element={<About />} />
          <Route path="/profiles" element={<Profiles />}>
            <Route path=":userid" element={<Profile/>} />
          </Route>
        </Route>
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

컴포넌트를 화면에 보여주는 순간 다른 컴포넌트(페이지)로 이동할 때 사용
페이지를 리다이렉트할 때 사용

Login.js

const Login = () => {
  return (
    <h1>로그인 페이지</h1>
  )
}
export default Login;

MyPage.js

import { Navigate } from "react-router-dom";

const MyPage = () => {
  const isLoggedIn = false;

  if (!isLoggedIn) {
    return <Navigate to="/login" replace={true} />
  }

  return (
    <h1>마이 페이지</h1>
  )
}

export default MyPage;

App.js => Login, MyPage 라우트를 추가

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Profiles from "./Profiles";
import Profile from "./Profile";
import Layout from "./Layout";
import NotFound from "./NotFound";
import Login from "./Login";
import MyPage from "./MyPage";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<Layout />}>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/info" element={<About />} />
          <Route path="/profiles" element={<Profiles />}>
            <Route path=":userid" element={<Profile/>} />
          </Route>
          <Route path="/login" element={<Login />} />
          <Route path="/mypage" element={<MyPage />} />
        </Route>
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Layout.js => 마이페이지 메뉴 추가

import { Link, NavLink, Outlet, useNavigate } from "react-router-dom";
import "./Layout.css";

export default function Layout() {
  const navigate = useNavigate();
  return (
    <div>
      <header>
        <ul>
          <li><Link to="/"></Link></li>
          <li><NavLink to="/about" style={
            ({ isActive }) => isActive ? { color: "red" } : undefined
          }>소개</NavLink></li>
          <li><Link to="/info">정보</Link></li>
          <li><Link to="/profiles">프로파일</Link></li>
          <li><Link to="/mypage">마이페이지</Link></li>
        </ul>
        <button onClick={() => navigate(-1)}>이전 페이지로 이동</button>
        <button onClick={() => navigate("/info")}>정보 페이지로 이동</button>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  )
}

테스트 => 마이페이지 메뉴를 클릭하면 로그인 페이지로 이동하는 것을 확인

const isLoggedIn = false; 이기 때문에 로그인 페이지로 이동한다.

<Navigate to="/login" replace={true} />

프로파일 → 마이페이지로 이동하면 히스토리에 다음과 같이 기록 => /profile → /login
이전 페이지로 이동을 클릭하면 => /profile

<Navigate to="/login" replace={false} />

프로파일 → 마이페이지로 이동하면 히스토리에 다음과 같이 기록 => /profile → /mypage → /login
이전 페이지로 이동을 클릭하면 => /mypage → /login

const isLoggedIn = true; 로 바꾸면 마이페이지가 나온다.

replace={true} 로 해놓자 !!

0개의 댓글