/todo-next

Primary LanguageTypeScript

Todo App with Next.js + TypeScript

๐Ÿ’ก ํ”„๋กœ์ ํŠธ ์ •๋ณด

  1. ํ”„๋กœ์ ํŠธ ๋ช…: Todo App with Next.js and TypeScript
  2. ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 2022. 8. 27. ~ 2022. 9. 1

๐ŸŒˆ ํ”„๋กœ์ ํŠธ ์‹คํ–‰ ๋ฐฉ๋ฒ•

git clone https://github.com/hjpark625/todo-next.git
cd todo-next
yarn install
yarn start

โญ ๋ฐฐํฌ ๋งํฌ

https://hjpark625.github.io/todo-next/


๐Ÿ“š ํ™œ์šฉ ๊ธฐ์ˆ  ์Šคํƒ

next react typescript styledComponents
redux react-redux

  • ์„ ์ • ์ด์œ 
    • Next.js
      • ์„œ๋ฒ„์‚ฌ์ด๋“œ๋ Œ๋”๋ง(SSR)์„ ํ™œ์šฉํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์‚ฌ์ด๋“œ๋ Œ๋”๋ง(CSR)๋ณด๋‹ค ์šฐ์ˆ˜ํ•œ ์†๋„์˜ ๋ Œ๋”๋ง์„ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
        • CSR์€ ํ…… ๋น„์–ด์žˆ๋Š” HTMLํŒŒ์ผ์„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋ฐ›๊ณ  ๊ทธ ์ดํ›„ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋กœ๋“œํ•ด HTML์„ ๊ทธ๋ ค๋‚ด๋Š” ๋ฐฉ์‹์ธ ๋ฐ˜๋ฉด SSR์€ ์„œ๋ฒ„์ž์ฒด์—์„œ HTML์— ๋ชจ๋“  ์Šคํฌ๋ฆฝํŠธ๋“ค์„ ๊ทธ๋ ค์„œ ํด๋ผ์ด์–ธํŠธ์— ๋‚ด๋ณด๋‚ด๋Š” ๋ฐฉ์‹์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ผ์„ ๋‘ ๋ฒˆ ํ•  ๊ฒƒ์„ ํ•œ ๋ฒˆ์œผ๋กœ ์ค„์—ฌ์ฃผ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.
      • ์œ„์˜ ์„ค๋ช…์— ๊ทผ๊ฑฐํ•˜์—ฌ ๊ฒ€์ƒ‰์—”์ง„์ตœ์ ํ™”(SEO)์—๋„ ์ตœ์ ์˜ ์„ฑ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • React
      • React๋Š” ํ˜„์กดํ•˜๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ๋งŽ์ด ํ™œ์šฉ์ค‘์— ์žˆ๋‹ค.
      • SPA๋ผ๋Š” ๊ฐ•์ ์ด ์กด์žฌํ•ด ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญ ํ•  ๋•Œ ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ๊ฐฑ์‹ ํ•จ์œผ๋กœ์จ ํŠธ๋ž˜ํ”ฝ ๊ฐ์†Œ์™€ ๋ Œ๋”๋ง์—์„œ ํšจ์œจ์ ์ด๋‹ค.
        • ๋น ๋ฅธ ํ™”๋ฉด ์ด๋™์ด ๊ฐ€๋Šฅํ•˜๋ฉฐ ์•ฑ์ฒ˜๋Ÿผ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋™์ž‘์œผ๋กœ ์ตœ์ ์˜ UX๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค.
    • TypeScript
      • ์ •์  ํƒ€์ž… ์ง€์›ํ•˜๋ฏ€๋กœ ์ปดํŒŒ์ผ ๋‹จ๊ณ„์—์„œ ์˜ค๋ฅ˜๋ฅผ ์‚ฌ์ „์— ํฌ์ฐฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ด๋ฅผ ํ†ตํ•ด ๋ฏธ๋ฆฌ ๋””๋ฒ„๊น…์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
        • ๊ฐœ๋ฐœ ๊ฐ„ ๊ฐœ๋ฐœ์ž์˜ ํœด๋จผ ์—๋Ÿฌ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ์ง€ํ•˜์—ฌ ์ถ”ํ›„ ๋ฐฐํฌ ์‹œ์— ๋ฌธ์ œ๋ฅผ ์‚ฌ์ „์ ์œผ๋กœ ์ฐจ๋‹จํ•˜๋Š”๋ฐ ํšจ๊ณผ์ ์ด๋‹ค.
    • Redux / React-Redux
      • ์ „์—ญ์ƒํƒœ ๊ด€๋ฆฌํ•˜๋Š”๋ฐ ์žˆ์–ด ํ‘œ์ค€์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.
      • FLUXํŒจํ„ด์œผ๋กœ ๋ฐ์ดํ„ฐ ํ๋ฆ„์˜ ๋‹จ๋ฐฉํ–ฅ์„ฑ์„ ์ถ”๊ตฌํ•˜์—ฌ ์ถ”ํ›„ ์—๋Ÿฌ๋ฐœ์ƒ ์‹œ ์—๋Ÿฌ์˜ ์›์ธ์„ ํŒŒ์•…ํ•˜๋Š”๋ฐ ์‰ฝ๊ณ  ์œ ์ง€๋ณด์ˆ˜์— ๋งค์šฐ ํƒ„๋ ฅ์ ์ด๋‹ค.
    • Styled-Components
      • CSS-in-JS๋Š” ์งง์€ ๊ธธ์ด์˜ ์œ ๋‹ˆํฌํ•œ ํด๋ž˜์Šค๋ฅผ ์ž๋™์ ์œผ๋กœ ์ƒ์„ฑํ•˜๊ธฐ์— ์ฝ”๋“œ ๊ฒฝ๋Ÿ‰ํ™”์— ํšจ๊ณผ์ ์ด๋‹ค.
      • ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ•์— ์ ํ•ฉํ•˜๊ณ  ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” CSS-in-JS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
      • JS ์ƒ์ˆ˜์™€ ํ•จ์ˆ˜๋ฅผ ์‰ฝ๊ฒŒ ๊ณต์œ ํ•˜์—ฌ props๋ฅผ ํ™œ์šฉํ•œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์— ์šฉ์ดํ•˜๋‹ค.
      • ์ปดํฌ๋„ŒํŠธํ™”ํ•˜์—ฌ ์Šคํƒ€์ผ์„ ์žฌํ™œ์šฉํ•˜๋Š”๋ฐ ์šฉ์ดํ•˜๋‹ค.

๐Ÿ“ ํด๋” ๊ตฌ์กฐ

root
|-- tsconfig.json
|-- README.md
|-- next.config.js
|-- package.json
|-- yarn.lock
|-- .gitignore
|-- .babelrc
|-- .github
|-- compoenents
|   |-- /Todos
|       |-- TodoInsert.tsx
|       |-- TodoList.tsx
|       |-- TodoListItem.tsx
|       |-- TodoTemplate.tsx
|-- pages
|   |-- /api
|   |   |-- hello.ts
|   |-- _app.tsx
|   |-- index.tsx
|   |-- todo.tsx
|-- styles
|   |-- GlobalStyle.tsx
|   |-- palette.ts
|-- store
|-- types

๐Ÿ“ ๊ตฌํ˜„ ๊ธฐ๋Šฅ

  • Todo ๋ฆฌ์ŠคํŠธ ์ž‘์„ฑ(Create)

    • todo.tsx์—์„œ TODO_DATA๋ผ๋Š” ์ดˆ๊ธฐ๊ฐ’ ์—ญํ• ์„ ํ•˜๋Š” ์ƒ์ˆ˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ์ž‘

    • TodoInsert.tsx์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ input์— ์ž…๋ ฅํ•œ ๊ฐ’์„ state์— ์ €์žฅ

      /* TodoInsert.tsx */
      const [todoValue, setTodoValue] = useState('');
      
      const saveInputValue = useCallback(
        (e: React.ChangeEvent<HTMLInputElement>) => {
          setTodoValue(e.target.value); // input์— ์ž‘์„ฑ๋˜๋Š” string์„ state์— ์ €์žฅ
        },
        [],
      );
    • ์œ„์˜ ์ฝ”๋“œ๋ฅผ Redux๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค๋ฉด...

      /* TodoInsert.tsx */
      const saveInputValue = useCallback(
        (e: React.ChangeEvent<HTMLInputElement>) => {
          dispatch(changeField(e.target.value));
        },
        [],
      );
      
      /* todos.ts */
      export const changeField = (value: string) => ({
        type: CHANGE_FIELD,
        value,
      });
      • ๊ธฐ์กด์— setState๋ฅผ ํ™œ์šฉํ•ด value๋ฅผ ์ €์žฅํ–ˆ๋‹ค๋ฉด dispatchํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•ด todos.ts์— ์žˆ๋Š” ์•ก์…˜์ƒ์„ฑํ•จ์ˆ˜์ธ changeField๋ฅผ e.target.value๋ฅผ payload์— ๋‹ด์•„ ์‹คํ–‰์‹œํ‚จ๋‹ค.
    • ์ €์žฅ๋œ value๋ฅผ ์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ todo.tsx์—์„œ props๋กœ ๋‚ด๋ ค์ฃผ๋Š” onInsertํ•จ์ˆ˜์— ํฌํ•จ์‹œ์ผœ submit

      /* todo.tsx */
      const [todos, setTodos] = useState(TODO_DATA);
      
      const nextId = useRef(1);
      
      const onInsert = useCallback(
        (text: string) => {
          const todo = {
            id: nextId.current,
            todo: text,
            checked: false,
          };
          setTodos(todos.concat(todo)); // ์ƒ์ˆ˜๋ฐ์ดํ„ฐ ํ™œ์šฉํ•ด concat์œผ๋กœ ์ƒˆ๋กœ์šด ๋ฆฌ์ŠคํŠธ ์ถ”๊ฐ€
          nextId.current += 1; // Todo๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด ref์˜ ๊ฐ’์„ 1์”ฉ ๋”ํ•จ
        },
        [todos],
      );
      
      /* ... */
      
      const TODO_DATA = [
        {
          id: 0,
          text: '',
          checked: false,
        },
      ];
    • ์œ„์˜ ์ฝ”๋“œ๋ฅผ Redux๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค๋ฉด...

      /* todo.tsx */
      const nextId = useRef(1);
      
      const onInsert = useCallback(
        (text: string) => {
          dispatch(addTodo(text, nextId));
          nextId.current++; // Todo๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด 1์”ฉ ๋”ํ•˜๊ธฐ
        },
        [todos],
      );
      
      /* todos.ts */
      export const addTodo = (
        inputValue: string,
        nextId: React.MutableRefObject<number>,
      ) => ({
        type: ADD_TODO,
        nextId, // ์ƒˆ๋กœ๋“ค์–ด์˜จ id๊ฐ’
        inputValue, // changeField ํ•จ์ˆ˜๋กœ ์ €์žฅ๋œ e.target.value๊ฐ’
      });
  • Todo ๋ฆฌ์ŠคํŠธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ(Read)

    • todo.tsx์—์„œ useSelector๋ฅผ ํ™œ์šฉํ•ด ๋ฐฐ์—ด๋กœ ๋งŒ๋“ค์–ด์ง„ todos๋ฅผ props๋กœ TodoList.tsx๋กœ ์ „๋‹ฌ ํ›„ map์„ ์ด์šฉํ•ด ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋“ค์„ ๋ Œ๋”๋ง
  • Todo ์ˆ˜์ •ํ•˜๊ธฐ(Update)

    • edit๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์ˆ˜์ • input์ฐฝ์ด ๋‚˜ํƒ€๋‚˜๊ณ  ๊ธฐ์กด ๊ฐ’์„ ๋„์šฐ๋„๋ก ๊ตฌํ˜„

      • ์ˆ˜์ •๋ฒ„ํŠผ ํด๋ฆญํ•  ๋• boolean๊ฐ’์„ ๊ฐ€์ง„ isEdit์ด๋ผ๋Š” state๋ฅผ ํ™œ์šฉ
    • Createํ• ๋•Œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ edit input์ฐฝ์˜ ์ž…๋ ฅ๋˜๋Š” string์„ state์— ์ €์žฅ

    • ์ˆ˜์ •๋ฒ„ํŠผ ํด๋ฆญ ์‹œ input์ฐฝ์— focus๋˜๋„๋ก useRef์™€ useLayoutEffect๋ฅผ ํ™œ์šฉํ•ด์„œ ๊ตฌํ˜„

      const editRef = useRef<HTMLInputElement | null>(null);
      useLayoutEffect(() => {
        if (editRef.current !== null) return editRef.current.focus();
      });
    • todo.tsx์—์„œ onEditํ•จ์ˆ˜๋ฅผ ์ œ์ž‘ํ•ด props๋กœ ์ „๋‹ฌ

      const onEdit = useCallback(
        (e: React.FormEvent<HTMLFormElement>, id: number, editTodo: string) => {
          e.preventDefault();
          if (!editTodo) return alert('๋นˆ ์นธ์œผ๋กœ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); // input์ฐฝ์ด ๋นˆ ์นธ์ผ ๋•Œ alert์ฐฝ ํ™œ์šฉ
          setTodos(
            todos.map((todo) =>
              todo.id === id ? { ...todo, todo: editTodo } : todo,
            ),
          ); // ์ƒˆ๋กญ๊ฒŒ ์ˆ˜์ •๋˜๋Š” ํ…์ŠคํŠธ๋ฅผ todo ์Šคํ…Œ์ดํŠธ์— ๋ฐ˜์˜
        },
        [todos],
      );
      • ์ˆ˜์ • ํ›„ ์—”ํ„ฐ๋ฅผ ์ณค์„ ๋•Œ submit์œผ๋กœ ์ƒˆ๋กญ๊ฒŒ ์ˆ˜์ •๋˜๋Š” ํ…์ŠคํŠธ๋ฅผ todos์— ๋ฐ˜์˜ํ•˜๊ณ  ์ˆ˜์ •๋œ ํ…์ŠคํŠธ๋ฅผ ์žฌ๋ Œ๋”๋ง
    • ์œ„์˜ ์ฝ”๋“œ๋ฅผ Redux๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค๋ฉด...

      /* todo.tsx */
      const onEdit = useCallback(
        (e: React.FormEvent<HTMLFormElement>, edit_value: string, id: number) => {
          e.preventDefault();
          dispatch(editTodo(edit_value, id));
        },
        [todos],
      );
      
      /* todos.ts */
      export const editTodo = (edit_value: string, id: number) => ({
        type: EDIT_TODO,
        edit_value,
        id,
      });
  • Todo ๋ฆฌ์ŠคํŠธ ์‚ญ์ œํ•˜๊ธฐ(Delete)

    • filter๋ฉ”์†Œ๋“œ๋ฅผ ํ™œ์šฉํ•ด์„œ ํด๋ฆญํžˆ๋ฉด ํด๋ฆญ ๋œ todo์˜ id์™€ ํ•ด๋‹น todo์˜ id๋ฅผ ๋น„๊ตํ•˜์—ฌ ์ผ์น˜ํ•˜๋Š” ๊ฒƒ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ todo๋“ค์„ ๋ฝ‘์•„๋‚ด๊ณ  ๋‚˜๋จธ์ง€ Todo ๋ฆฌ์ŠคํŠธ๋“ค์„ ์žฌ๋ Œ๋”๋ง

      const onRemove = useCallback(
        (id: number) => {
          dispatch(deleteTodo(id));
        },
        [todos],
      );