ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React Native로 TodoList 만들기
    React/React Native 2022. 12. 30. 16:48

    React Native로 TodoList 만들기

    뼈대+CSS(styled component 사용)

    - styled component는 install 설치해줘야 한다.

    https://emotion.sh/docs/@emotion/native 참고

     

    yarn add @emotion/react @emotion/native
    npm install @emotion/react @emotion/native

     

    둘중 하나 원하는걸로 설치

     

    import styled from '@emotion/native';

    import 해오기

     

    뼈대 코드

    import {StatusBar} from 'expo-status-bar';
    import styled from '@emotion/native';
    import {TouchableOpacity} from 'react-native';
    import {AntDesign, Feather} from '@expo/vector-icons';
    
    
    const StyledTouchNav = styled.TouchableOpacity`
      width: 100px;
      background-color: gray;
    `;
    
    const StyleText = styled.Text`
      text-align: center;
      line-height: 50%;
      font-weight: 800;
    `;
    const NavContainer = styled.View`
      flex-direction: row;
      justify-content: space-between;
    `;
    const InputContainer = styled.View`
      justify-content: center;
      border-color: black;
      border-bottom-width: 3px;
      border-top-width: 3px;
      margin: 15px 0;
      padding: 15px 0;
    `;
    
    const SafeView = styled.SafeAreaView`
      width: 90%;
      margin: 0 auto;
    `;
    
    const StyledInput = styled.TextInput`
      width: 100%;
      height: 45px;
      border-width: 2px;
      font-size: 18px;
      padding: 0 15px;
    `;
    
    const ItemContainer = styled.View``;
    const Item = styled.View`
      flex-direction: row;
      background-color: #ddd;
      width: 100%;
      height: 50px;
      align-items: center;
      justify-content: space-between;
      padding: 0 10px;
      margin: 0 0 10px 0;
    `;
    
    const Icons = styled.View`
      flex-direction: row;
      width: 90px;
      justify-content: space-between;
    `;
    
    export default function App() {
      
      return (
        // 스크롤뷰를 쓸 때는 style로 스타일링을 하는게 아니고 contentContainerStyle 아래처럼 써야함
        // 노치 아래로 뷰를 나오게 하려면 SafeAreaView 사용
        <SafeView>
          <StatusBar />
          <NavContainer flexWay={'row'}>
            <StyledTouchNav>
              <StyleText>Javascript</StyleText>
            </StyledTouchNav>
            <StyledTouchNav>
              <StyleText>React</StyleText>
            </StyledTouchNav>
            <StyledTouchNav>
              <StyleText>Coding Text</StyleText>
            </StyledTouchNav>
          </NavContainer>
    
          <InputContainer>
            <StyledInput/>
          </InputContainer>
    
          <ItemContainer>
                  <Item>
              <StyleText>
                Todo 들어갈 곳
                    </StyleText>
                    <Icons>
                      <TouchableOpacity>
                        <AntDesign name='checksquare' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity>
                        <Feather name='edit' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity>
                        <AntDesign name='delete' size={24} color='black' />
                      </TouchableOpacity>
                    </Icons>
                  </Item>
          </ItemContainer>
        </SafeView>
      );
    }

     

    결과물

     

    아이콘 사용하는 방법

    https://icons.expo.fyi/

    1번은 import 하라는거고,

    2번은 아이콘을 사용할곳에 복붙하면 된다.

     

    시작하기 전 알고가면 좋을 태그

    https://jeffgukang.github.io/react-native-tutorial/  한번 읽어보면 좋을 것 같다.

     

     

    <TextInput>

    - 일반 HTML의 <input>태그와 React Native의 <TextInput> 태그는 동일하다. 

    - onsubmit 속성 대신 onSubmitEditing 속성을 사용한다. (버튼을 누르면 동작하는게 아니라 폰에서 엔터를 누르면 동작함)

     

    <View>

    - <div> 는 <View> 태그를 사용한다.

    - 기본적으로 flex style이 내장되어 있고, 기본 정렬은 column(세로) 이다.

     

    <Scroll View>

    - 스크롤 되는 <div>는 style을 따로 하는게 아닌, <ScrollView> 태그를 사용한다.

     

    <SafeAreaView>

    - 휴대폰 노치가 화면을 가리는 경우를 방지하기 위해 <SafeAreaView> 태그를 사용하여 Container처럼 사용한다. (IOS만 해당)

    - 노치 화면을 계산해 padding을 자동으로 주기 때문에 style에 padding 속성은 먹히지 않는다.

    - 그래서 body처럼 사용하는게 일반적이다. 항상 외각에 위치해있는 컴포넌트이다.

    - Android 개발만 할거라면 해당 태그는 사용하지 않아도 된다.

     

    <StatusBar>

    - 휴대폰 상단 시간, 배터리 충전여부 등 을 나타낼 수 있다. SafeAreaView 밑에 <StatusBar style='auto' /> 입력하면 된다.

     

    <TouchableOpacity>

    - React Native의 <Button> 태그는 style 지정도 안되는 개 쓰레기이니 <TouchableOpacity> 태그를 버튼태그 대신 사용하자.

     

     

     

    Create, Read

    export default function App() {
      const [text, setText] = useState('');
      const [category, setCategory] = useState(''); // js, react, ct로 카테고리 나눌예정
      const [todos, setTodos] = useState([]); // newTodo를 감싸줄 배열
    
      // 엔터 클릭 시 저장될 Data 양식
      const newTodo = {
        id: Date.now(),
        text,
        isDone: false, // 완료 표시를 위해 추가함
        isEdit: false, // 수정 기능을 위해 추가함
        category,
      };
    
      // 글 추가
      const addTodo = () => {
        setTodos([...todos, newTodo]);
        setText('');
      };
    
      return (
        <SafeView>
          <StatusBar />
          <NavContainer flexWay={'row'}>
            <StyledTouchNav>
              <StyleText>Javascript</StyleText>
            </StyledTouchNav>
            <StyledTouchNav>
              <StyleText>React</StyleText>
            </StyledTouchNav>
            <StyledTouchNav>
              <StyleText>Coding Text</StyleText>
            </StyledTouchNav>
          </NavContainer>
    
          <InputContainer>
            <StyledInput
              onSubmitEditing={addTodo}
              value={text}
              onChangeText={setText}
            />
          </InputContainer>
    
          <ItemContainer>
            {todos.map((todo) => {
              return (
                <Item key={todo.id}>
                  <StyleText>{todo.text}</StyleText>
                  <Icons>
                    <TouchableOpacity>
                      <AntDesign name='checksquare' size={24} color='black' />
                    </TouchableOpacity>
    
                    <TouchableOpacity>
                      <Feather name='edit' size={24} color='black' />
                    </TouchableOpacity>
    
                    <TouchableOpacity>
                      <AntDesign name='delete' size={24} color='black' />
                    </TouchableOpacity>
                  </Icons>
                </Item>
              );
            })}
          </ItemContainer>
        </SafeView>
      );
    }

    등록이 잘 됩니다.

     

    console.log('todos', todos) 결과

     

    https://jsonformatter.curiousconcept.com/#

    콘솔에 찍힌 데이터를 복붙해서 Process 버튼을 클릭하면

    이렇게 데이터가 잘 정리되서 나온다.

     

     

     

     

    Category 분류하기

    	<NavContainer flexWay={'row'}>
            <StyledTouchNav onPress={() => setCategory('js')}>
              <StyleText>Javascript</StyleText>
            </StyledTouchNav>
            <StyledTouchNav onPress={() => setCategory('react')}>
              <StyleText>React</StyleText>
            </StyledTouchNav>
            <StyledTouchNav onPress={() => setCategory('ct')}>
              <StyleText>Coding Text</StyleText>
            </StyledTouchNav>
          </NavContainer>

     

    각 Category 선택 시 배경색상 변경해주기

    	<NavContainer flexWay={'row'}>
            <StyledTouchNav
              onPress={() => setCategory('js')}
              style={{backgroundColor: category === 'js' ? '#0FBCF9' : 'grey'}}
            >
              <StyleText>Javascript</StyleText>
            </StyledTouchNav>
            <StyledTouchNav
              onPress={() => setCategory('react')}
              style={{backgroundColor: category === 'react' ? '#0FBCF9' : 'grey'}}
            >
              <StyleText>React</StyleText>
            </StyledTouchNav>
            <StyledTouchNav
              onPress={() => setCategory('ct')}
              style={{backgroundColor: category === 'ct' ? '#0FBCF9' : 'grey'}}
            >
              <StyleText>Coding Text</StyleText>
            </StyledTouchNav>
          </NavContainer>

    - 카테고리가 ㅇㅇ면 #0FBCF9 색상으로 백그라운드컬러 변경 : 아니면 grey

     

     

    각 category에 해당하는 글만 보이게 하기

    	<ItemContainer>
            {todos.map((todo) => {
              if (category === todo.category) {
                //해당 todo의 category에 해당하는 category에 글이 업로드 되도록 if문 추가
                return (
                  <Item key={todo.id}>
                    <StyleText>{todo.text}</StyleText>
                    <Icons>
                      <TouchableOpacity>
                        <AntDesign name='checksquare' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity>
                        <Feather name='edit' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity>
                        <AntDesign name='delete' size={24} color='black' />
                      </TouchableOpacity>
                    </Icons>
                  </Item>
                )
              }
            })}
          </ItemContainer>

     

     

     

    Done 값으로 완료한 todo 구분하기

    const setDone = (id) => {
      const newTodos = [...todos]; // 얕은복사로 todos를 복사해온다.
      const idx = newTodos.findIndex((todo) => todo.id === id);
      // findIndex로 todo.id와 id가 동일한 것을 idx 변수에 담는다.
      newTodos[idx].isDone = !newTodos[idx].isDone;
      // 선택한 객체의 isDone 값을 현재 isDone의 반대값(flase->true / true->false)로 바꿔준다.
      setTodos(newTodos);
    };
          <ItemContainer>
            {todos.map((todo) => {
              if (category === todo.category) {
                return (
                  <Item key={todo.id}>
                    <StyleText
                      style={{
                        textDecorationLine: todo.isDone ? 'line-through' : 'none',
                        // isDone값이 true면 line-through를 주고, 아니면 아무효과 안줌
                      }}
                    >
                      {todo.text}
                    </StyleText>
                    <Icons>
                      <TouchableOpacity onPress={() => setDone(todo.id)}>
                        // 위 setDone 함수에서 id를 매개변수로 받고있기때문에 todo.id를 매개변수로 넘겨주는 코드
                        <AntDesign name='checksquare' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity>
                        <Feather name='edit' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity>
                        <AntDesign name='delete' size={24} color='black' />
                      </TouchableOpacity>
                    </Icons>
                  </Item>
                );
              }
            })}
          </ItemContainer>

    여기서 알아야 될 것

    - findIndex : 판별 함수를 만족하는 첫 식별자를 반환

     

     

     

    Delete

    const deleteTodo = (id) => {
        Alert.alert('Todo 삭제', '정말 삭제하시겠습니까?', [
          {
            text: '취소',
            style: 'cancel',
          },
          {
            text: '삭제',
            style: 'destructive',
            onPress: () => {
              const newTodos = todos.filter((todo) => todo.id !== id);
              // filter 메소드는 immutable이라 todos에 영향을 미치지 못해서 얕은복사를 하지 않았다.
              setTodos(newTodos);
            },
          },
        ]);
      };

     

    <TouchableOpacity onPress={() => deleteTodo(todo.id)}>
    	{/* 삭제버튼에 delete 기능 추가 */}
    	<AntDesign name='delete' size={24} color='black' />
    </TouchableOpacity>

    Alert 속성 참고 - https://reactnative.dev/docs/alert

     

     

     

    Update

      // 글 수정 (isEdit이 true일때 ㅇㅇㅇ해라 라고 할거기때문에 isDone이랑 로직이 동일함)
      const isEdit = (id) => {
        const newTodos = [...todos];
        const idx = newTodos.findIndex((todo) => todo.id === id);
        newTodos[idx].isEdit = !newTodos[idx].isEdit;
        setTodos(newTodos);
      };
    
      // 수정 text 입력 후 엔터 눌렀을 때 실행
      const editTodo = (id) => {
        const newTodos = [...todos];
        const idx = newTodos.findIndex((todo) => todo.id === id);
        newTodos[idx].text = editText;
        newTodos[idx].isEdit = false; // isEdit값을 닫아준다
        setTodos(newTodos);
        setEditText('');
      };
    <Item key={todo.id}>
      {todo.isEdit ? ( // todo.isEdit이 true이면 TextInput, false면 styleText를 출력해라
        <TextInput
          onSubmitEditing={() => editTodo(todo.id)}
          value={editText}
          onChangeText={setEditText}
          style={{backgroundColor: 'white', flex: 1}}
        />
      ) : (
        <StyleText
          style={{
            textDecorationLine: todo.isDone ? 'line-through' : 'none',
          }}
        >
          {todo.text}
        </StyleText>
      )}

     

     

    CRUD 끝!

     

     

     

    전체코드

    import {StatusBar} from 'expo-status-bar';
    import styled from '@emotion/native';
    import {Alert, TextInput, TouchableOpacity} from 'react-native';
    import {AntDesign, Feather} from '@expo/vector-icons';
    import {useState} from 'react';
    
    const StyledTouchNav = styled.TouchableOpacity`
      width: 100px;
      background-color: gray;
    `;
    
    const StyleText = styled.Text`
      text-align: center;
      line-height: 50%;
      font-weight: 800;
    `;
    const NavContainer = styled.View`
      flex-direction: row;
      justify-content: space-between;
    `;
    const InputContainer = styled.View`
      justify-content: center;
      border-color: black;
      border-bottom-width: 3px;
      border-top-width: 3px;
      margin: 15px 0;
      padding: 15px 0;
    `;
    
    const SafeView = styled.SafeAreaView`
      width: 90%;
      margin: 0 auto;
    `;
    
    const StyledInput = styled.TextInput`
      width: 100%;
      height: 45px;
      border-width: 2px;
      font-size: 18px;
      padding: 0 15px;
    `;
    
    const ItemContainer = styled.View``;
    const Item = styled.View`
      flex-direction: row;
      background-color: #ddd;
      width: 100%;
      height: 50px;
      align-items: center;
      justify-content: space-between;
      padding: 0 10px;
      margin: 0 0 10px 0;
    `;
    
    const Icons = styled.View`
      flex-direction: row;
      width: 90px;
      justify-content: space-between;
    `;
    
    export default function App() {
      const [text, setText] = useState('');
      const [category, setCategory] = useState('');
      const [todos, setTodos] = useState([]);
      const [editText, setEditText] = useState('');
      console.log('editText:', editText);
    
      const newTodo = {
        id: Date.now(),
        text,
        isDone: false,
        isEdit: false,
        category,
      };
    
      // 글 추가
      const addTodo = () => {
        setTodos([...todos, newTodo]);
        setText('');
      };
    
      // 완료 표시
      const setDone = (id) => {
        const newTodos = [...todos];
        const idx = newTodos.findIndex((todo) => todo.id === id);
        newTodos[idx].isDone = !newTodos[idx].isDone;
        setTodos(newTodos);
      };
    
      // 글 삭제
      const deleteTodo = (id) => {
        Alert.alert('Todo 삭제', '정말 삭제하시겠습니까?', [
          {
            text: '취소',
            style: 'cancel',
          },
          {
            text: '삭제',
            style: 'destructive',
            onPress: () => {
              const newTodos = todos.filter((todo) => todo.id !== id);
              // filter 메소드는 immutable이라 todos에 영향을 미치지 못해서 얕은복사를 하지 않았다.
              setTodos(newTodos);
            },
          },
        ]);
      };
    
      // 글 수정 (isEdit이 true일때 ㅇㅇㅇ해라 라고 할거기때문에 isDone이랑 로직이 동일함)
      const isEdit = (id) => {
        const newTodos = [...todos];
        const idx = newTodos.findIndex((todo) => todo.id === id);
        newTodos[idx].isEdit = !newTodos[idx].isEdit;
        setTodos(newTodos);
      };
    
      // 수정 text 입력 후 엔터 눌렀을 때 실행
      const editTodo = (id) => {
        const newTodos = [...todos];
        const idx = newTodos.findIndex((todo) => todo.id === id);
        newTodos[idx].text = editText;
        newTodos[idx].isEdit = false; // isEdit값을 닫아준다
        setTodos(newTodos);
        setEditText('');
      };
    
      return (
        <SafeView>
          <StatusBar />
          <NavContainer flexWay={'row'}>
            <StyledTouchNav
              onPress={() => setCategory('js')}
              style={{backgroundColor: category === 'js' ? '#0FBCF9' : 'grey'}}
            >
              <StyleText>Javascript</StyleText>
            </StyledTouchNav>
            <StyledTouchNav
              onPress={() => setCategory('react')}
              style={{backgroundColor: category === 'react' ? '#0FBCF9' : 'grey'}}
            >
              <StyleText>React</StyleText>
            </StyledTouchNav>
            <StyledTouchNav
              onPress={() => setCategory('ct')}
              style={{backgroundColor: category === 'ct' ? '#0FBCF9' : 'grey'}}
            >
              <StyleText>Coding Text</StyleText>
            </StyledTouchNav>
          </NavContainer>
    
          <InputContainer>
            <StyledInput
              onSubmitEditing={addTodo}
              value={text}
              onChangeText={setText}
            />
          </InputContainer>
    
          <ItemContainer>
            {todos.map((todo) => {
              if (category === todo.category) {
                return (
                  <Item key={todo.id}>
                    {todo.isEdit ? ( // todo.isEdit이 true이면 TextInput, false면 styleText를 출력해라
                      <TextInput
                        onSubmitEditing={() => editTodo(todo.id)}
                        value={editText}
                        onChangeText={setEditText}
                        style={{backgroundColor: 'white', flex: 1}}
                      />
                    ) : (
                      <StyleText
                        style={{
                          textDecorationLine: todo.isDone ? 'line-through' : 'none',
                        }}
                      >
                        {todo.text}
                      </StyleText>
                    )}
                    <Icons>
                      <TouchableOpacity onPress={() => setDone(todo.id)}>
                        <AntDesign name='checksquare' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity onPress={() => isEdit(todo.id)}>
                        <Feather name='edit' size={24} color='black' />
                      </TouchableOpacity>
    
                      <TouchableOpacity onPress={() => deleteTodo(todo.id)}>
                        {/* 삭제버튼에 delete 기능 추가 */}
                        <AntDesign name='delete' size={24} color='black' />
                      </TouchableOpacity>
                    </Icons>
                  </Item>
                );
              }
            })}
          </ItemContainer>
        </SafeView>
      );
    }

     

     

Designed by Tistory.