반응형
반응형

When working with UI in Flutter, there are often cases where you need to measure the size of widgets that have variable sizes depending on their child widgets. Let’s take the example of an OTT streaming app.


In the movie detail page, the plot section shows both text and images. If this area exceeds a certain height, a ‘More’ button appears, partially hiding the content. Pressing ‘More’ reveals the rest of the content.

This plot section does not have a fixed size. Rather, its height varies dynamically based on the amount of text data, device width, and image height. Therefore, to implement such a UI structure, it’s necessary to measure the rendered height of the widget and conditionally set up UI elements like the ‘more’ button and the hidden content based on whether it exceeds a certain height.

So, how can we measure the dynamic size of widgets? In this post, we’ll explore step-by-step how to measure the variable size of widgets through a simple example. Additionally, we’ll delve into the following concepts, addressing Flutter’s rendering process.

WidgetTree, ElementTree, RenderTree
BuildContext
RenderObject
addPostFrameCallback
NotificationListener
Implementation Goals
Let’s briefly examine the example we’ll cover in the post.


The above screenshot depicts a simple page displaying information about movie cast members. It consists of a Text widget for the title section and a ListView widget with ExpansionTile displaying information about the cast members, all wrapped inside a Column. The Text widget for the title section shows the current height of the Column.

class CastInfoPage extends StatelessWidget {
  const CastInfoPage({super.key});@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF000000),
      appBar: AppBar(
        leading: const Icon(
          Icons.arrow_back_ios,
          color: Colors.white,
        ),
        titleSpacing: 0,
        backgroundColor: Colors.black,
        centerTitle: false,
        title: Text(
          'Dune: Part Two',
          style: AppTextStyle.headline1,
        ),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 16) +
              const EdgeInsets.only(top: 20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Height : ${0}',
                style: PretendardTextStyle.bold(
                  size: 24,
                  height: 37,
                  letterSpacing: -0.2,
                ),
              ),
              const SizedBox(height: 10),
              ListView.separated(
                physics: const NeverScrollableScrollPhysics(),
                padding: EdgeInsets.zero,
                shrinkWrap: true,
                itemCount: CastModel.castList.length,
                separatorBuilder: (_, __) => const SizedBox(height: 8),
                itemBuilder: (context, index) {
                  final item = CastModel.castList[index];
                  return ExpansionTile(
                    tilePadding: EdgeInsets.zero,
                    title: Row(
                      children: [
                        ClipRRect(
                          borderRadius: BorderRadius.circular(56 / 2),
                          child: CachedNetworkImage(
                            height: 56,
                            width: 56,
                            imageUrl: item.imgUrl,
                            fit: BoxFit.cover,
                          ),
                        ),
                        const SizedBox(width: 10),
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              item.name,
                              style: AppTextStyle.title1,
                            ),
                            Text(
                              item.role,
                              style: AppTextStyle.body3.copyWith(
                                color: AppColor.gray02,
                              ),
                            )
                          ],
                        ),
                      ],
                    ),
                    children: <Widget>[
                      Text(
                        item.description,
                        style: AppTextStyle.body3,
                      ),
                    ],
                  );
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}
When you click on the ExpansionTile, the widget expands to show detailed information about the cast members. Therefore, the size of the widget changes, requiring adjustment of the height value accordingly. We can summarize the following requirements.

- Must be able to obtain the precise size of the currently rendered widget.
- Should be able to access the value in the measuring widget to handle UI conditionally.
- Must detect changes in widget size dynamically and obtain the changed size (not expandable size).
- Should be easy and convenient to use.
You can check out the implemented example through the following site.

measure_size_implementation
A new Flutter project.
measure-size-builder-example.netlify.app

How are Widgets Drawn?
First, let’s take a look at how widgets are drawn on the screen in Flutter.


You’ve probably heard about the concept that Flutter creates widgets based on a 3 Tree Architecture consisting of Widget, Element, and Render. You may not be very familiar with encountering objects of elements or render trees during Flutter development, as it's not very common. However, understanding the basic concepts of Flutter's widget tree structure can be very helpful when direct manipulation or access to element or render objects is required, as in this example. So, let's try to explain it in a way that's easier to understand.

Widget Tree — Blueprint of a Car
class Lamborghini extends StatelessWidget {
  const Lamborigini({super.key});  
  @override
  Widget build(BuildContext context) {
    return Car(
        paint: RedPaint(),
        engine: 4LV8Engine(),
        wheel: RimsAltaneroShinyBlack(),
        carbon : UpperExteriorCarbon(),
        ...
    );
  }
}
To help understand the three tree structures of widgets, let’s use the analogy of building a Lamborghini. To build a car, various components such as color, engine, etc., need to be determined. In the above code, we’re passing necessary options to the Car class within the build method.


This process is similar to creating a blueprint for the car, defining how the car’s components are structured and shaped. StatelessWidget or StatefulWidget always override the build method and return a widget inside. These codes are returned as a widget tree and internally create the necessary 'element' through createElement().

Key Point!
The code inside the build method is returned as a 'widget tree' and creates an 'element tree'.

Element Tree — Car Parts and Engineers
The element tree generated from the widget tree is comprised of elements that are part of widgets and are responsible for managing widget lifecycle and state changes. While the widget tree contains structural information about the code written by developers, the element tree consists of pieces of widgets created based on the widget tree.


If the widget tree is likened to a blueprint of a car, the element in the element tree can be compared to both the car parts and the engineer managing those parts. Just as a car engineer arranges and manages necessary parts according to the blueprint, the element tree creates elements that are parts of the UI needed for the final rendering of widgets and communicates any changes to the render tree as necessary. Now, let's take a closer look at the characteristics of elements.

Widgets are Immutable, Elements are Mutable
All Flutter widgets are immutable, meaning their content cannot be modified during runtime. This is similar to a car not being able to suddenly transform into a motorcycle. However, elements are mutable, allowing widgets to be changed as needed. In other words, elements can be removed and replaced with new elements.

Role of BuildContext
The BuildContext, which we always pass as an argument when executing the build method in StatelessWidget or StatefulWidget, is used when direct manipulation or access to the Element object is required. It also indicates where the Element created from the widget tree is positioned in the tree. It's like an engineer (BuildContext) examining the blueprint (Widget Tree) to determine where the necessary parts (Element) are and arranging them accordingly.

showDialog<void>(  
  context: context,  
  builder: (BuildContext context) {  
    return AlertDialog(...);  
  },  
);
Similarly, when displaying a popup using methods like showDialog, we always need to pass the BuildContext because we need to know which widget (screen) in the composed tree the dialog should appear on.

Key Point!
- The element tree manages the widget’s lifecycle and communicates changes to the render tree as needed.
- BuildContext is used to determine the position of widgets currently displayed on the screen and plays an important role in manipulating or accessing elements.
-BuildContext is also considered as an Element.

Rendering Tree — Car Manufacturing, Manufactured Car

Once the necessary elements are created, the widget finally creates a Render Tree. It's used to handle the actual rendering via the widget's createRenderObject method, which creates a RenderObject, an object that manages the widget's size and layout information. During this process, the RenderObjectElement, created from the Element Tree, becomes directly involved.

The rendering tree can be likened to the manufacturing of a car using car parts. It completes the car using the components manufactured from the Element Tree.

In the rendering tree, two main methods, layout and paint, are used to actually render the widgets we see. During the layout phase, parent nodes pass constraints to child nodes, and at the lowest level, the final size information is passed back up to determine where and how widgets should be drawn. Then, the paint operation is performed, passing the work to the GPU thread to finally complete the widgets.

Key Point!


What If it Wasn’t Composed of Three Widget Trees?

Now that you have some understanding of the architecture of the widget tree, you can clearly understand the reason why widgets are composed of three widget trees. If you were to replace a wheel on a car, you wouldn’t need to rebuild the entire car from scratch. You’d simply replace the existing wheel with a new one. Flutter operates on a similar principle. When parts of a rendered widget need to change based on state, the corresponding element detects this and communicates the changes to the render tree, allowing only the necessary parts to be re-rendered.

However, if Flutter’s widget tree were composed of only one tree, even widgets that didn’t need to change based on state would be redrawn, leading to inefficiencies. It would be like building a new car every time you change a wheel.

In summary, the fundamental reason Flutter’s widgets are composed of three tree structures is to efficiently re-render only the necessary parts of the screen when changes are needed based on state.

1. Separating Widget Trees and Accessing Render Objects via BuildContext
Now, let’s go through step by step how to derive the size of widgets with variable sizes depending on their child widgets.

As mentioned earlier, the rendered size of a widget exists in the render tree within a RenderObject, and to access this object, we need a BuildContext. Therefore, the formula is established that with just a BuildContext, we can access the rendered size of a widget.

Size size = context.size!;
With this code, we can check the rendered size of a widget accessible via the BuildContext.

However, there is one problem in the current example code’s widget tree.


We want to obtain the rendered size of the Column widget through its associated RenderObject. However, the BuildContext that can access the RenderObject is located higher up at the CaseInfoScreen level. This means that if we proceed like this, we’ll end up measuring not only the size of the Column but also the size of the AppBar, which is a child widget of the Scaffold.


To solve this issue, there are various methods, but the simplest approach is to separate the widget whose size you want to measure, like a StatelessWidget or StatefulWidget. By doing this, a new sub-widget tree is created where the BuildContext is directly accessible through the build(BuildContext context) method of the separated widget. This is commonly referred to as "separating the BuildContext."

class ContentView extends StatefulWidget {
  const ContentView({Key? key});

  @override
  State<ContentView> createState() => _ContentViewState();
}

class _ContentViewState extends State<ContentView> {
  double renderedHeight; // <-- Rendered height of the [ContentView] widget

  @override
  void initState() {
    super.initState();
    /// Accessing the rendered size of the widget via [BuildContext]
    /// and assigning it to the renderedHeight variable
    renderedHeight = context.size?.height ?? 0; 
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'Height : ${renderedHeight}', // <- Displaying the rendered height in a Text widget
        ),
        ...
      ],
    );
  }
}
Now, as shown in the above code, by separating the widget we want to measure into a separate StatefulWidget, we can obtain the rendered height of the desired widget by accessing the size value through the BuildContext within the new build method.

NOTE
If you don’t separate it into a separate widget, using GlobalKey to find the RenderObject can also be a good approach.

2. Obtaining Size When Widget Rendering is Complete
However, running the above code will result in the following runtime error:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building Builder(dirty):
Cannot get size during build.
Why does this error occur?


As explained earlier, in the render tree, the layout method is responsible for passing constraints from parent nodes to child nodes and determining the final size information from the bottom node to the top node to decide where and how the widget should be drawn. The problem arises because an attempt is made to access the size value before the layout method is executed.

To address this issue, Flutter provides the addPostFrameCallback method. This method is used to register a callback that is invoked after the widget has been painted on the screen. In other words, it's a callback method that is executed after the rendering tree operations are completed.

WidgetsBinding.instance!.addPostFrameCallback((_) {
   setState(() {
         renderedHeight = context.size!.height; 
    }); 
});
Inside the addPostFrameCallback callback method, as shown in the code above, you can assign the rendering height of the widget accessed through the BuildContext to the renderedHeight variable. This way, by accessing the rendering size value through the context only after the widget has been rendered, you can assign a value to the renderedHeight variable without encountering any errors.

3. Detecting and Obtaining Size When Widget Size Dynamically Changes
While most of the requirements have been met, there’s one remaining functionality: detecting when the widget’s size changes due to user interaction and displaying the updated size on the screen.


When the ExpansionListTile widget on the screen is clicked, the widget expands, changing its size. However, the current code does not detect the size change and update the value.

To address this, we can use a widget called NotificationListener, which is useful for detecting and handling notifications (such as size changes, scrolling, gestures, etc.) that occur within the widget tree.

NotificationListener(  
  onNotification: (_) { 
    if(renderedHeight != context.size!.height) {
  setState(() {  
renderedHeight = context.size!.height;  
log('height : $renderedHeight');  
   });  
    }
    return true;  
  },  
  child: Column(...)
  )
Wrap the existing Column widget with a NotificationListener widget. Inside the onNotification callback, add logic to detect the size change of the widget and update its size accordingly. In the provided code, the setState method is used to update the changed size.

NOTE
Since NotificationListener can receive events like scrolling or touch gestures, which could lead to unnecessary updates if the size remains the same, the update logic is placed within the condition if(renderedHeight != context.size!.height).

By adding this code, the widget’s height changes are detected, and the size is updated accordingly. However, a new issue arises: continuous execution of the onNotification callback whenever the widget's size changes.

[log] height : 367.0
[log] height : 367.0
[log] height : 369.3854225873947
[log] height : 375.8518112897873
[log] height : 385.81551444530487
[log] height : 435.25
[log] height : 413.13516367971897
[log] height : 430.28125
[log] height : 448.9247215986252
[log] height : 469.3435592651367
[log] height : 491.38857555389404
[log] height : 551.9744523763657
[log] height : 540.0271100997925
[log] height : 567.0
The onNotification callback is repeatedly executed whenever the widget's size changes, leading to multiple unnecessary calls to the setState method. This could potentially impact performance and needs to be addressed.

To solve this issue, we can implement a debouncer logic. A debouncer delays consecutive calls for a certain period and executes the action only after the last call.

/// Deboucner Module
class Debouncer {  
  final Duration delay;  
  Timer? _timer;  
  
  Debouncer(this.delay);  
  
  void run(VoidCallback action) {  
    _timer?.cancel();  
    _timer = Timer(delay, action);  
  }  
}
/// ContentView Widget
double? renderedHeight;  
final Debouncer debouncer = Debouncer(const Duration(milliseconds: 50));

Widget build(BuildContext context) {  
 return NotificationListener(  
 onNotification: (_) {  
     debouncer.run(() {  // <-- Apply Debouncer Callback 
      if(renderedHeight != context.size!.height) {
     setState(() {  
    renderedHeight = context.size!.height;  
    log('height : $renderedHeight');  
      });  
     }
       });  
       return true;  
     },  
     child: Column(...)
      ... 
   }
In the provided code, a Debouncer class is declared, and within the onNotification callback, the debouncer is used to delay consecutive calls to the setState method. This ensures that the setState method is only called after the widget's size has stopped changing, optimizing performance.

4. Modularizing for Ease of Use
Since the functionality of obtaining the rendering size of a variable widget can be applied to different screens or multiple projects, modularizing it for easy use is a good idea.


Therefore, I created a custom widget called MeasureSizeBuilder. This widget encompasses all the logic discussed earlier, and it is designed to allow access to the rendering size of the specified widget through the builder property, which returns the widget whose size needs to be measured. The size can be accessed through the size property within the builder.

Now, let’s take a look at the completed example code.

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:measure_size_builder/measure_size_builder.dart';
import 'package:measure_size_implementation/src/cast_model.dart';
import 'package:measure_size_implementation/src/style/app_color.dart';
import 'package:measure_size_implementation/src/style/app_text_style.dart';

class CastInfoPage extends StatelessWidget {
  const CastInfoPage({Key? key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF000000),
      appBar: AppBar(
        leading: const Icon(
          Icons.arrow_back_ios,
          color: Colors.white,
        ),
        titleSpacing: 0,
        backgroundColor: Colors.black,
        centerTitle: false,
        title: Text(
          'Dune: Part Two',
          style: AppTextStyle.headline1,
        ),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 16) +
              const EdgeInsets.only(top: 20),
          child: MeasureSizeBuilder(
            builder: (context, size) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Height : ${size.height}',
                    style: PretendardTextStyle.bold(
                      size: 24,
                      height: 37,
                      letterSpacing: -0.2,
                    ),
                  ),
                  const SizedBox(height: 10),
                  ListView.separated(
                    physics: const NeverScrollableScrollPhysics(),
                    padding: EdgeInsets.zero,
                    shrinkWrap: true,
                    itemCount: CastModel.castList.length,
                    separatorBuilder: (_, __) => const SizedBox(height: 8),
                    itemBuilder: (context, index) {
                      final item = CastModel.castList[index];
                      return ExpansionTile(
                        tilePadding: EdgeInsets.zero,
                        title: Row(
                          children: [
                            ClipRRect(
                              borderRadius: BorderRadius.circular(56 / 2),
                              child: CachedNetworkImage(
                                height: 56,
                                width: 56,
                                imageUrl: item.imgUrl,
                                fit: BoxFit.cover,
                              ),
                            ),
                            const SizedBox(width: 10),
                            Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  item.name,
                                  style: AppTextStyle.title1,
                                ),
                                Text(
                                  item.role,
                                  style: AppTextStyle.body3.copyWith(
                                    color: AppColor.gray02,
                                  ),
                                )
                              ],
                            ),
                          ],
                        ),
                        children: <Widget>[
                          Text(
                            item.description,
                            style: AppTextStyle.body3,
                          ),
                        ],
                      );
                    },
                  )
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

Finally, we have fulfilled all the requirements we set out to achieve 🎉

I have published the measure_size_builder package used in the example. For those interested in this feature or modularized code, please refer to the following link.

measure_size_builder | Flutter package
Simplest way to get dynamic size of widget
pub.dev

 


https://medium.com/@ximya/get-dynamic-widget-size-in-flutter-f3e12c52ce1f

반응형
반응형

https://game.naver.com/lounge/chzzk/board/detail/4033868

 

⚡치지직 정식오픈 - 본겜시작 : 치지직

⚡치지직 정식오픈 - 본겜시작 : 치지직

game.naver.com

 

네이버가 게임 특화 스트리밍 플랫폼 ‘치지직’을 9일 정식으로 출시했다. 네이버는 정식 서비스를 통해 서비스 안정성을 더욱 높이고 편의 기능을 강화할 방침이다.

 

‘치지직’은 지난 12월 베타서비스로 공개된 서비스다. 신규 서비스임에도 불구하고 치지직은 경쟁 스트리밍 서비스인 ‘트위치’의 한국 시장 철수 시점에 출시되면서 국내 점유율을 높이고 있다. 실제로 모바일 앱 분석 기관인 모바일인덱스 조사에 따르면, 치지직의 12월 사용자의 77% 이상이 트위치 사용자에서 유입됐다. 2024년 3월 기준 치지직 사용자 수는 227만 명으로 국내 스트리밍 플랫폼 점유율 1위 기업인 아프리카TV(248만명)와의 사용자 수 격차가 21만 명으로 좁혀졌다. 참고로 네이버는 치지직의 공식 사용자 수를 공개하지 않았다.

네이버는 치지직을 정식 출시함에 따라, 안정적인 서비스를 제공하며 스트리밍의 편의 요소가 추가할 예정이다. 대표적으로 ‘미션 후원’ 기능과 ‘치지직 클립’이 이번에 지원된다. 미션 후원은 시청자가 후원금을 주기 위해 특정 조건을 걸 수 있는 기능이다. 가령 시청자가 ‘A 게임을 모두 실행할 경우’라는 조건을 걸고, 스트리머가 실제 게임을 전부 시행했을 때 후원이 최종 완료되는 식이다. ‘치지직 클립’을 통해 스트리밍 영상을 간편히 편집하는 도구다. 단순히 영상 편집하는 것 외에도 시청자가 후원을 진행할 때 보내는 영상 메시지를 치지직 클립으로 간편하게 제작해 전달할 수 있다.

치지직 정식 서비스에선 네이버 타서비스의 연계도 강화된다. 네이버는 치지직에서 생성한 스트리머의 숏폼 영상을 네이버앱의 콘텐츠 추천 영역에서 노출할 예정이다. 또한 스트리머 팬카페에 치지직 라이브 진행 여부 및 VOD 영상 노출을 확인할 수 있도록 카페 연동을 더욱 고도화하고, 네이버의 AI 보이스 기술을 적용한 스트리머 보이스 후원 기능도 3분기 내에 발표할 계획이다.

네이버 치지직 김정미 리더는 “치지직은 베타 서비스 기간에 꼼꼼히 서비스의 사용성 및 안정성을 점검하며 완성도를 높여갔고, 스트리밍 시장의 대표 서비스로 빠르게 안착했다”라며 “앞으로도 치지직은 다양한 타 서비스와의 연계, 다채로운 기능 오픈 등을 통해 서비스 경쟁력을 강화하겠다”라고 설명했다.

 

 

반응형
반응형

Python에서 현재 설치된 라이브러리(패키지) 목록을 확인하는 방법은 여러 가지가 있습니다. 
주로 pip 패키지 관리자를 사용하여 설치된 패키지 목록을 조회합니다. 
아래는 다양한 방법으로 현재 설치된 라이브러리 목록을 확인하는 방법을 설명합니다.

1. pip list 명령어 사용

가장 일반적인 방법은 터미널 또는 명령 프롬프트에서 pip list 명령을 사용하여 설치된 모든 패키지 목록을 조회하는 것입니다.


pip list




위 명령을 실행하면 설치된 모든 패키지의 목록이 출력됩니다.


2. pip freeze 명령어 사용

pip freeze 명령은 pip list와 유사하게 현재 환경에 설치된 패키지와 그 버전을 출력합니다. 이 명령은 보통 requirements.txt 파일을 생성하는 데 사용됩니다.


pip freeze




3. Python 스크립트를 통한 확인

Python 스크립트 내에서 pkg_resources 모듈을 사용하여 현재 설치된 패키지를 조회할 수 있습니다.

import pkg_resources

# 현재 설치된 패키지 목록 조회
installed_packages = pkg_resources.working_set
installed_packages_list = sorted(["%s==%s" % (i.key, i.version) for i in installed_packages])

for package in installed_packages_list:
    print(package)




4. conda list (conda 환경에서)

만약 Anaconda 또는 Miniconda와 같은 conda 패키지 관리자를 사용하고 있다면, conda list 명령을 사용하여 현재 환경에 설치된 패키지 목록을 확인할 수 있습니다.

conda list



위 방법 중 하나를 선택하여 현재 Python 환경에 설치된 모든 패키지 목록을 확인할 수 있습니다. 

주로 pip list 명령이 가장 일반적이고 널리 사용되는 방법입니다.

반응형
반응형

책임감 있는 AI, 일명 RAI(Responsible AI)에 대한 관심이 더욱 높아지고 있다. RAI는 신뢰와 도입부터 LLM 환각 관리, 유해한 생성형 AI 콘텐츠 제거에 이르기까지 모든 것을 추진하는 핵심 요소다. 효과적인 RAI로 기업은 더 빠르게 혁신하고, 비즈니스를 더 개선하고, 미래의 AI 규정을 준수하고, 평판이 손상되거나 규제를 어기는 일 없이 계속 경쟁에 참여할 수 있다.

안타깝게도 RAI가 실제로 무엇인지, 무엇을 제공하는지, 어떻게 달성할 수 있는지에 대한 혼란이 있다. 치명적인 결과를 초래할 수 있는 문제다. 그러나 RAI 이니셔티브가 제대로 수행되지 않으면 혁신이 방해받고 지연과 비용이 추가된다. RAI의 정의와 목적에 대한 좋은 해석도 있지만 잘못된 통념과 오해도 만연해 있다. 기업은 RAI는 비용이 많이 들고 비효율적이며 시간 낭비라는 잘못된 통념을 깨뜨리고 AI에 기반해 가치를 창출하는 원동력으로 전환해야 한다.

 

RAI에 대한 오해 중 가장 문제적인 것은 무엇일까? 그리고 지속 가능한 이니셔티브를 추진하기 위해 RAI를 가장 잘 정의하려면 어떻게 해야 할까?


잘못된 통념 1 : RAI는 원칙론일 뿐

어느 거대 IT 기업에서나 설명 가능성, 공정성, 개인정보 보호, 포용성, 투명성과 같은 RAI 원칙을 찾아볼 수 있 수 있다. 이렇게 널리 퍼진 여러 원칙이 있으니 RAI의 중심에 원칙이 있다고 해도 문제가 없을 것이다. 이런 기본적인 기업 원칙은 책임감 있는 개인에게 기대하는 것과 정확히 같은 종류이므로 책임감 있는 AI 보장에 핵심이 된다고 하면 맞는 말일까?

아니, 틀렸다. 모든 기업에는 이미 원칙이 있다. 일반적으로 기업 원칙은 RAI와 정확히 동일할 것이다. 공정, 투명성, 포용에 반대한다고 밝힐 기업이 있을까? 만약 그런 기업이 있다고 해도, AI에는 투명성을 적용하고 기업의 나머지에는 투명성을 적용하지 않을 수가 있을까?

또한 원칙은 AI에 대한 신뢰를 불러일으키는 것보다 사람과 기업에 대한 신뢰를 끌어내는 데에 훨씬 효과적이다. 항공사의 비행기가 목적지까지 안전하게 데려다 줄 것이라고 믿는 이유가 그 회사가 원칙을 지키기 때문인지 생각해 보자. 아니, 엄격하게 시행되는 프로세스를 따르고, 신중하게 테스트하고 정기적으로 검사한 장비를 사용하는 숙련된 조종사, 기술자, 항공 교통 관제사가 있어서 신뢰하는 것이다.

비행기 여행과 마찬가지로 RAI의 핵심은 원칙을 실현하고 시행하는 사람, 프로세스, 기술이다. 독자 모두 이미 올바른 원칙을 정해두었을 것이다. 문제는 실행이다.
 

잘못된 통념 2 : RAI는 윤리와 관계가 있다

RAI는 AI를 윤리적으로 사용하고, 모델이 공정하게 작동하도록, 새로운 차별이 생겨나지 않도록 하는 지침일 뿐이라고 생각한다면 틀렸다.

신용 평가에 사용되거나 이력서를 심사하거나 실직 여부를 결정하는 모델 등 실제로 윤리나 공정성을 고려하는 AI 사용례는 극히 일부일 뿐이다. 당연히 이러한 사용례를 책임감 있게 처리하는 것도 RAI의 목적이지만, 다른 모든 AI 솔루션의 안전과 안정적 개발 및 사용, 또 기업의 성능 및 재무 요구 사항 충족도 RAI가 필요한 부문이다.

설명 가능성을 제공하고, 편향성을 확인하고, 개인정보 보호를 보장하는 데 사용하는 도구는 정확성, 신뢰성 및 데이터 보호를 보장하는 데 사용하는 도구와 정확히 같다. RAI는 공정성을 고려해야 할 때 AI를 윤리적으로 사용하는 데 도움이 되지만, 다른 모든 AI 사용례에서도 마찬가지로 중요하다.
 

잘못된 통념 3 : RAI를 알려면 설명 가능성부터 알아야 한다

AI를 신뢰하고 책임감 있게 사용하기 위해서는 설명 가능성, 즉 해석 가능성이 필요하다는 것이 일반적인 생각이지만, 사실은 아니다. 비행기 여행에 안심하기 위해서 비행기의 작동 원리를 알아야 할 필요가 없듯, AI를 신뢰하기 위해 설명 가능성을 꼭 이해할 필요는 없다.

인간의 결정이 좋은 예다. 인간은 거의 대부분 자신의 결정 이유를 설명할 수 있지만, 사실 설명은 의사 결정 행동의 실제 동인과는 거의 관련이 없는, 사후에 지어낸 이야기라는 증거가 많다.

그러나 쉽게 이해할 수 있는 '화이트박스' 모델과 LIME 및 ShAP과 같은 방법을 사용하는 AI 설명 가능성은 모델이 올바르게 작동하는지 테스트하는 데 중요하다. 잘못된 상관관계와 잠재적인 불공정한 차별을 식별하는 데 도움이 되기 때문이다. 패턴을 쉽게 감지하고 설명할 수 있는 간단한 사용례에서는 신뢰도를 높이는 지름길이 될 수 있다. 그러나 패턴이 충분히 복잡한 경우에는 어떤 설명도 기껏해야 어떤 결정이 어떻게 내려졌는지에 대한 암시만 제공할 뿐이며, 최악의 경우 완전한 중언부언이나 횡설수설에 지나지 않을 것이다.

즉, 요컨대, 설명 가능성은 있으면 좋지만, 이해관계자와의 신뢰를 의미 있게 이끌어내는 방식으로 제공하는 것은 불가능할 때가 많다. RAI는 모든 AI 사용례에 대한 신뢰를 보장하는 것으로, 이는 개발 및 운영에 사용되는 사람, 프로세스, 기술(특히 플랫폼)을 통해 신뢰를 제공하는 것을 의미한다.
 

책임감 있는 AI는 곧 위험 관리

결국 RAI는 AI 및 머신러닝 모델을 개발하고 사용할 때 위험을 관리하는 관행이다. 여기에는 비즈니스 위험(성능 저하 또는 신뢰할 수 없는 모델 등), 법적 위험(규제 벌금, 고객사나 또는 직원 소송 등), 심지어 사회적 위험(차별이나 환경 피해 등)까지 관리해야 한다.

이러한 위험은 사람, 프로세스, 기술의 형태로 RAI 역량을 구축하는 다층적 전략을 통해 관리된다. 사람 측면에서는 RAI를 책임지는 리더(예: 최고 데이터 분석 책임자, 최고 AI 책임자, 데이터 과학 책임자, ML 부사장)의 역량을 강화하고 실무자와 사용자가 책임감 있게 AI를 개발, 관리, 사용할 수 있도록 교육해야 한다.

프로세스 측면에서는 데이터 액세스 및 모델 학습에서 모델 배포, 모니터링 및 재학습에 이르는 엔드투엔드 수명 주기를 관리하고 통제해야 한다. 기술 측면에서 특히 중요한 것은 측면에서 플랫폼은 대규모로 사람과 프로세스를 지원하고 활성화하는 플랫폼이다. 플랫폼은 설명 가능성, 편향성 감지, 편향성 완화, 공정성 평가, 드리프트 모니터링 등 RAI 방법에 대한 액세스를 민주화하고, AI 아티팩트 거버넌스, 계보 추적, 문서 자동화, 승인 워크플로 조율, 데이터 보안은 물론 RAI 프로세스를 간소화하는 수많은 기능을 제공한다.

제약, 금융 서비스, 보험 등 규제가 심한 산업의 고급 AI 팀이 이미 이러한 기능을 구축하여 가치 창출에 나서고 있다. 이들 선도 업체는 빠른 구현, 더 큰 채택, 더 나은 성능, 향상된 안정성 등의 이점을 통해 모든 AI, 특히 생성형 AI에 대한 신뢰를 대규모로 구축하고 있다. AI 규제 마련에 대비하여 AI 이니셔티브를 미래에 대비할 뿐 아니라 무엇보다도 모든 사용자를 더 안전하게 만드는 데에도 유용하다. 책임감 있는 AI는 대규모의 AI 가치를 실현하는 열쇠지만, 그러려면 먼저 잘못된 통념을 깨야 한다.

 

https://www.itworld.co.kr/news/335878

 

'책임감 있는 AI'에 대한 잘못된 통념 3가지

책임감 있는 AI, 일명 RAI(Responsible AI)에 대한 관심이 더욱 높아지고 있다. RAI는 신뢰와 도입부터 LLM 환각 관리, 유

www.itworld.co.kr

 

 

반응형
반응형

윈도우에서 리눅스로 전환할 때 가장 큰 어려움이 바로 소프트웨어다. 윈도우에서 쓰던 것을 리눅스에서 쓰지 못할지 우려한다. 하지만 리눅스에서도 특수 목적 애플리케이션 외에 다양한 소프트웨어를 사용할 수 있다.
 

ⓒ IDG
기본적으로 리눅스는 윈도우와 다르다. 윈도우와 다르게 작동, 구성되며 윈도우 프로그램을 바로 실행할 수 없다. 다만, 가상화 또는 와인(Wine)을 사용해 리눅스에서도 윈도우 프로그램을 사용할 수 있다. 반면 윈도우에서 이미 오픈소스 소프트웨어를 많이 써왔다면 리눅스 환경에서도 쉽게 적응할 수 있다. 리브레 오피스, 파이어폭스, 썬더버드, VLC와 같은 프로그램은 리눅스와 윈도우에서 모두 똑같이 사용할 수 있으며, 작동 방식도 거의 같다. 이들 외에 다른 애플리케이션도 다양하지만, 일부 기능과 작동 방식은 윈도우용 프로그램과 다소 차이가 있다. 어느 정도 익숙해지는 시간이 필요하다.
 

리눅스에서 소프트웨어 설치하기

윈도우 10 또는 11에서는 마이크로소프트 스토어를 통해 새 소프트웨어를 설치하고 업데이트할 수 있다. 여기서 제공되지 않는 것은 제조업체의 웹사이트나 기타 다른 곳에서 내려받은 설치 파일을 사용한다. 이런 소프트웨어를 표준화된 관리 기능이 없으므로 업데이트가 자동으로 이뤄지지 않을 수 있다.

리눅스 시스템은 윈도우와 달리 각 배포판의 리포지토리에서 소프트웨어를 가져온다. 중앙 패키지 데이터베이스가 있으며, 업데이트는 시스템과 설치된 모든 프로그램에 적용됩니다. 따라서 프로그램 패키지를 확인하고 서명하기 때문에 높은 수준의 보안이 보장된다.

문제는 우분투 22.04 또는 리눅스 민트 21.2와 같은 LTS(long-term support) 리눅스 배포판은 5년의 지원 기간 동안 새로운 소프트웨어가 거의 제공되지 않는다는 점이다. 파이어폭스나 썬더버드 같은 보안에 중요한 소프트웨어는 정기적으로 업데이트되지만, 리브레 오피스와 같은 프로그램은 기본 버전이 그대로 유지된다. 시스템의 안정성을 더 중요하게 생각하기 때문이다.
 

사용할 수 있는 업데이트에 대해 자동으로 알려준다. 시스템과 설치된 모든 애플리케이션에 대한 설치가 중앙 집중식으로 이루어진다. ⓒ IDG
리눅스용 최신 프로그램 : LTS 배포판 사용자가 최신 소프트웨어를 계속 받을 수 있도록 우분투와 리눅스 민트는 표준 패키지 관리 외에도 스냅(Snap)과 플랫팩(Flatpak) 등 2가지 컨테이너 형식을 사용한다. 프로그램과 필요한 모든 시스템 파일을 별도의 영역에 설정해 시스템의 나머지 부분이 최신 파일의 영향을 받지 않는 장점이 있다. 반면 기존 패키지 설치보다 하드 드라이브에 더 많은 공간이 있어야 하는 것은 단점이다.

사용자 관점에서 처음에는 패키지의 정확한 형식이 중요하지 않다. 우분투에서는 "우분투 소프트웨어(Ubuntu Software)" 도구를 통해, 리눅스 민트에서는 "애플리케이션 관리(Application Management)"를 통해 패키지 관리가 여전히 중앙 집중화되어 있기 때문이다. 여기서 프로그램을 검색하거나 프로그램을 추천받을 수 있다.

하지만 새 프로그램을 설치할 때는 사용 가능한 버전이 여러 가지인 경우가 많으므로 버전에 주의해야 한다. 우분투 소프트웨어에서 "스냅 스토어(Snap Store)"와 "ubunt-jammyuniverse(deb) 또는 이와 유사한 소스(Source) 중 선택할 수 있다. 스냅 스토어의 프로그램은 일반적으로 더 최신 버전이며, "프리뷰/엣지(preview/edge)"가 추가된 프리 릴리즈 버전도 있다. 리눅스 민트도 비슷합니다. 일반적으로 오래된 "시스템 패키지(system package)"와 "플랫팩, 플랫텁(Flatpak, Flathub)" 및 "플랫팩, 플랫텁 베타(Flatpak, Flathub Beta)" 중 선택할 수 있다. 클래식 시스템 패키지와 스냅, 플랫팩 컨테이너를 병렬로 설치할 수도 있다.
 

리브레 오피스는 광범위한 기능을 제공한다. 메뉴 대신 리본을 활성화할 수 있어 마이크로소프트 오피스와 비슷하다. ⓒ IDG 

리눅스용 오피스 패키지

마이크로소프트 오피스는 가장 널리 쓰이는 생산성 앱이다. 워드 프로세싱, 스프레드시트의 전 세계 표준이다. 하지만 가끔 메일을 사용하거나 문서를 작성하는 정도라면 우분투와 리눅스 민트에 사전 설치된 리브레 오피스로도 충분하다.

다른 사람과 문서를 자주 주고받거나 마이크로소프트 인프라에 의존하는 환경에서 작업한다면 리눅스를 자신 있게 추천하기 힘들다. 리브레 오피스는 마이크로소프트 서식 문서를 읽을 수 있지만, 복잡한 문서의 경우 서식이 항상 올바르게 표시되는 것은 아니다. 특히 표나 매크로 등이 깨질 가능성이 있다. 가장 큰 어려움은 아마도 스프레드시트다. 리브레 오피스의 스프레드시트 앱인 칼크(Calc)는 엑셀과 비슷한 기능을 제공하지만, 전체적으로 같은 기능이라고 할 수는 없다. 

또한, 리브레 오피스는 기본적으로 클래식 메뉴 모음을 표시한다. 마이크로소프트 오피스에 익숙하다면 보기 > 사용자 인터페이스에서 "탭에서(In tab)"를 선택해 활성화하는 리본을 선호할 수도 있다. "모두(For all)"를 클릭하면 이 설정이 오피스 제품군의 모든 하위 프로그램에 적용된다.

리브레 오피스 대신 리눅스에서 사용할 만한 생산성 앱을 더 찾는다면, 마이크로소프트의 무료 온라인 오피스 제품도 살펴볼 만하다. 오피스닷컴(www.office.com)에 마이크로소프트 계정으로 로그인하기만 하면, 웹 브라우저에서 워드, 엑셀, 파워포인트 프로그램을 무료로 쓸 수 있다. 단, 웹용 오피스는 설치용 마이크로소프트 365와 같은 기능이 아니며, 자세한 차이점은 여길 참고하면 된다. 제약이 있기는 하지만 외부에서 마이크로소프트 오피스 파일을 편집하거나 다른 사람과 파일을 교환할 때 유용하다. 이렇게 만든 오피스 파일은 클라우드 원드라이브에 저장된다. 오프라인에서 문서 작업을 하려면 파일을 내려받아야 한다. 웹용 오피스에서는 리브레 오피스 형식의 파일을 가져오고 내보낼 수도 있다.

또 다른 대안으로는 워드 프로세서인 텍스트메이커, 프레젠테이션 프로그램인 프레젠테이션, 스프레드시트 프로그램인 플랜메이커가 포함된 소프트메이커 오피스(Softmaker Office)의 무료 버전 소프트메이커 프리오피스(Softmaker FreeOffice)가 있다. 
 

김프는 사진 편집은 물론, 여러 레이어와 마스크가 있는 복잡한 그래픽 작업까지 지원한다. ⓒ IDG 

이미지 편집과 그래픽

리눅스에는 이미 간단한 이미지 뷰어가 설치돼 있다. 그놈(Gnome) 데스크톱이 설치된 우분투에는 그놈 이미지 뷰어, 리눅스 민트에는 X 뷰어가 있다. 두 프로그램을 이용하면 슬라이드쇼를 표시하고 이미지를 회전할 수 있다. 간단한 편집 기능이 필요하다면 우분투에서는 샷웰(Shotwell)을, 리눅스 민트에서는 픽스(Pix)를 쓰면 된다. 기능은 비슷하다. 적목 제거, 색상 및 밝기 조정, 이미지 자르기 등을 지원한다.

이 중에서 디지캠(Digikam)은 사진 관리 앱이다. 앨범, 태그 또는 키워드에 따라 이미지를 정렬하고 분류할 수 있다. 톤 보정, 적목 제거, 프레임 추가 등 다양한 사진 편집 툴을 쓸 수 있다. RAW 파일을 보거나, 화이트 밸런스 같은 보정을 할 수 있다. 물론 로 테라피(Raw Therapee)나 다크테이블(Darktable)과 맞먹을 정도는 아니다. 더 많은 기능이 필요하면 전문가용 앱을 찾아야 한다.

김프(Gimp)는 주로 윈도우에서 포토샵을 쓰던 사용자를 대상으로 한 이미지 툴이다. 이 프로그램은 페인팅 도구, 레이어, 마스크, 자동 향상, 다양한 필터를 제공한다. 사진을 수정하고, 이미지를 자르거나 변형할 수 있다. 포토샵과 마찬가지로 대부분 기능은 즉시 액세스할 수 없고, 특정 효과는 마스크와 레이어를 적절히 조합해야만 쓸 수 있다. 하지만 널리 보급돼 사용되고 있으므로 궁금한 점은 구글링으로 대부분 해결할 수 있다. 김프 홈페이지의 문서(www.gimp.org/docs)가 좋은 출발점이다.

크리타(Krita)는 김프와 비슷하지만 더 쓰기 쉽다. 이 소프트웨어는 원래 페인팅 프로그램으로 만들어졌고, 드로잉 태블릿에서도 쓸 수 있다. 기본적으로 이미지를 선명하게 하거나 노이즈를 줄이는 등 사진에 대한 다양한 자동 보정과 최적화 도구를 지원한다. 크리타는 이미지를 예술적으로 바꾸는 다양한 효과도 제공한다. '이미지(Image)' 메뉴의 명령으로 크기 조정, 회전, 자르기를 빠르고 쉽게 할 수 있다.

잉크스케이프(Inkscape)는 어도비 일러스트레이터와 비슷하다. 벡터 그래픽 프로그램으로, 만화, 클립아트, 로고, 전단지, 브로셔, 다이어그램과 같은 예술적, 기술적 일러스트레이션 작업에 적합하다. 해상도와 관계없이 화면 및 인쇄용 요소를 선명하고 손실 없이 만들 수 있다. 잉크스케이프에서 사용하는 파일 형식은 SVG(Scalable Vector Graphics)다.
 

VLC 미디어 플레이어는 윈도우 버전과 거의 비슷하며 똑같이 작동한다. 일반적인 미디어 형식을 안정적으로 재생한다. ⓒ IDG 

오디오와 비디오용 소프트웨어

리눅스는 멀티미디어 기능에 있어서 아쉬운 것이 없다. 운영체제에 기본적으로 포함된 프로그램이 대부분 동영상 형식을 문제없이 재생하고, 필요한 경우 추가 코덱을 설치하면 된다.

대표적인 동영상 플레이어인 VLC는 윈도우에서 자주 사용되는데, 리눅스에서도 사용할 수 있다. 모든 일반적인 코덱이 포함돼 있어 거의 모든 동영상 형식을 재생한다. 복사 방지된 DVD를 재생하려면 추가 라이브러리가 필요하지만, 이는 다른 모든 미디어 플레이어와 마찬가지다. 복사 방지 DVD 재생 라이브러리를 설치하려면 일단 터미널을 연다.
 

sudo apt install libdvd-pkg


다음 명령으로 설치를 완료한다.
 

sudo dpkg-reconfigure libdvd-pkg


VLC 대신 쓸 수 있는 동영상 플레이어를 찾는다면 Sm플레이어(Smplayer)가 있다. 때에 따라 실행 속도가 조금 더 빠르고 별도 테마를 통해 인터페이스를 사용자 지정할 수 있는 것이 특징이다. Sm플레이어에는 중요한 오디오, 비디오 코덱도 모두 포함돼 있다. 오픈샷(Openshot)은 동영상을 편집하고, 효과를 추가하고, 여러 형식으로 저장하는 소프트웨어다. 인터페이스가 직관적이어서 초보자도 쉽게 이해하고 사용할 수 있다.
 

리브레 오피스 구성 폴더의 구조는 윈도우와 리눅스에서 같다. 따라서 파일을 리눅스 시스템으로 간단히 복사할 수 있다. ⓒ IDG 

윈도우의 앱 설정을 그대로 리눅스로 옮기는 방법

윈도우에서 리눅스로 전환해 필요한 소프트웨어를 사용할 때 리브레 오피스와 같이 복잡한 구성의 프로그램은 윈도우에서 데이터를 복사하는 방식으로 설정하는 시간을 줄일 수 있다. 인터페이스를 일관되게 사용할 수 있을 뿐만 아니라 자동 텍스트 모듈, 자체 사전 또는 매크로도 그대로 쓸 수 있다. 단, 이렇게 설정을 그대로 옮기려면 두 프로그램의 버전이 일치해야 한다.

윈도우용 리브레 오피스의 구성을 리눅스로 옮기기
구체적인 방법은 다음과 같다.
 

  • 1단계 : 윈도우에서 리브레 오피스를 종료하고 윈도우 탐색기의 주소 표시줄에 %appdata%를 입력한 후 엔터 키를 눌러 확인한다. 그런 다음 하위 폴더 "LibreOffice\4"로 변경하고 "user" 폴더를 ZIP 파일로 압축한다. 이 파일을 리눅스 홈 디렉터리에 복사한다.
  • 2단계 : 아직 리눅스에서 오피스 패키지를 실행한 적이 없다면, 리브레 오피스를 시작하고 프로그램을 다시 닫는다. 파일 관리자에서 홈 디렉터리로 이동해 ZIP 파일의 압축을 풀고 Ctrl-C를 사용하여 "사용자" 폴더를 복사한다.
  • 3단계: 파일 관리자에서 Ctrl-H를 사용해 숨겨진 파일을 표시하고 ".config/libreoffice/4" 폴더로 변경한다. 디렉터리 이름을 "user"로 바꾸고 윈도우에서 복사한 폴더를 Ctrl-V로 붙여 넣는다. 이제 리브레 오피스를 시작하면 윈도우에서 설정한 그대로 사용할 수 있다.


파이어폭스의 구성 옮기기
파이어폭스에서 데이터를 옮기려면 리눅스용 파이어폭스 버전이 윈도우 버전과 같거나 최신 버전이어야 한다. 버전 번호는 도움말 > 파이어폭스 정보에서 햄버거 메뉴를 클릭해 확인할 수 있다. 구체적인 방법은 다음과 같다.
 

  • 파이어폭스의 윈도우에서 햄버거 메뉴(대시 3개)를 클릭하고 도움말 > 추가 문제 해결 정보를 선택한다. "일반 정보" 아래에서 "프로필 폴더" 다음에 있는 "폴더 열기"를 클릭합니다. 파이어폭스를 종료한다. 표시된 폴더를 ZIP 파일로 압축한 다음 리눅스 홈 디렉터리에 복사해 압축을 푼다.
  • 리눅스에서 파이어폭스를 시작하고 윈도우와 마찬가지로 프로필 폴더를 만든다. 파이어폭스를 종료한다. 윈도우 백업의 프로필 폴더에 있는 모든 파일을 리눅스 프로필 폴더로 복사하고 기존 파일을 모두 덮어쓴다.


썬더버드의 구성 옮기기
여기서도 두 프로그램 버전이 같거나 리눅스 버전이 최신 버전이어야 한다. 파이어폭스와 마찬가지로 프로필 폴더를 정하고 썬더버드를 닫은 다음 폴더를 ZIP 파일로 압축한다. 리눅스에서 이 파일의 압축을 풀고 썬더버드 프로필 폴더를 확인한다. 이제 썬더버드를 닫고 윈도우 프로필의 파일을 이 폴더에 복사한다.

 

https://www.itworld.co.kr/news/335815

반응형
반응형

소프트웨어 개발이 쉽다고 생각하는 사람은 아무도 없지만, 이렇게나 다양한 방식으로 어려울 수 있다고 누가 생각했을까? 에반스 데이터에 따르면, 전 세계의 소프트웨어 개발자는 약 2,690만 명으로 추산된다. 

최근 AWS의 알리 스피텔이 트위터를 통해 던진 "개발자로서 일하면서 가장 어려운 부분이 무엇인가?"라는 질문에 100명 이상의 개발자가 답했다. 답변은 대부분 몇 가지 핵심 주제에 수렴할 것으로 예상했지만, 실제로는 매우 다양했다. 개발자의 삶을 개선할 방법을 찾고자 하는 기업이라면, 이들 응답을 자세히 살펴볼 가치가 있다. 개발자들의 이야기를 들어보자.

점점 늘어나는 프로젝트 범위
때때로 우리는 개발자를 너무 사랑한다. 우리는 개발자(새로운 킹메이커와 퀸메이커)에게 의존해 혁신을 이루고 혁신을 지속한다. 카일 쉐블린은 "제품과 디자인에 대한 끊임없는 범위 확대의 위협"이 개발자의 삶을 어렵게 만든다고 지적했다. 이는 개발자의 재능에 대한 건강한 믿음에서 비롯된 것이지만, 무분별한 범위 확대는 잔뜩 부풀어 오른 소프트웨어를 낳고 이런 소프트웨어는 유지 관리도 어렵다. 브라이언 심쿠스가 강조한 것처럼 여기에 "비개발자가 설정한 비현실적인 마감일"이 더해지면 이중고를 겪게 된다.

또한 다니엘레 헤벌링이 지적하듯이 개발자는 "실제로 구축해야 하는 것과 기대되는 결과물에 대한 팀 내 의견 불일치"를 싫어한다. 개발자는 항상 "더 나은 솔루션이 있는지 끊임없이 의심"하게 된다. 물론 더 나은 솔루션은 존재하기 마련이다. 다만 뒤늦게 그 해결책에 도달할 뿐이다. 핵심은 자비어 곤잘레스가 주장하듯이 "완벽주의의 무한 루프를 멈출 때"를 파악하는 것이다. 코드는 결코 완벽할 수 없다. 이를 받아들이고 계속 나아가야 한다.
 
학습의 속도
수십 년 동안 코볼에 대한 이해에 안주해 온 모든 개발자는 오늘날 프레임워크의 유동성이라는 현실에 직면해 있다. 브랜던 트래본은 개발자에게 "언어와 프레임워크의 끊임없는 변화를 따라잡는 것"은 심각한 도전이 될 수 있다고 지적했다. "가장 주목받을 수 있다고 생각되는 것을 골라 거기서부터 시작”할 수 있지만, 그것만으로는 충분하지 않다. "물론 새로운 것으로 '전환'할 준비가 되어 있어야 한다." 프레임워크는 개발자가 데이터베이스나 기타 시스템을 제대로 활용하지 못하게 만드는 경우가 많지만, 때로는 개발자가 혁신의 속도를 따라잡을 수 있는 유일한 방법이기도 하다. 그럼에도 불구하고 쉬운 일은 아니다. 프레임워크가 도움이 되긴 하지만 프레임워크도 변화하고, 그 변화가 문제를 일으킨다.

이와 관련된 것은 애플리케이션 자체의 아키텍처이다. 마이클 자크제스키에 따르면, "애플리케이션이 어떻게 발전할지 예상해 최상의 아키텍처를 준비하되, 처음부터 무리하지 말아야 한다." 어려운 일이다. 예를 들어, 개발자는 확장에 대비해야 할 수도 있지만, 미리 비용을 초과할 정도로 과도하게 프로비저닝해서는 안 된다.
 
'더 많이 코딩할 수만 있다면'
루크 프로서는 "코딩하지 않는 모든 것"은 소프트웨어 개발을 어렵게 만든다고 말한다. 일부 조사에 따르면, 개발자는 전체 시간 중 5%만 코드를 작성하고 나머지 70%는 코드를 이해하려고 노력하거나 코드와 관련이 없어 보이는 일을 하는 데 소비한다. 이를 "코딩 프로세스를 시작하기 위해 모든 세부 사항을 파악하려고 노력하는 것"이라고 표현하기도 한다. 또 다른 까다로운 문제가 있는데, 바로 "팀 간 협업, 특히 대기업의 경우"이다. 0과 1에 초점을 맞추고 싶지만 소프트웨어 개발은 궁극적으로 사람이 하는 것이며, 사람은 어렵다.

AI가 소프트웨어 개발에서 인간을 배제할 것이라는 일반적인 두려움은 어떤가? AI가 사람을 대체할 수는 없다. 지금은 물론 앞으로도 없을 것이다. 그러니 "매일 아침 일어나서 내가 여전히 이 일과 이 산업에 관심이 있다고 스스로를 설득해야 하는", "LLM이 우리를 비롯한 모든 실제 가치를 창출하는 사람들을 쓸모없게 만들 것이라고 예측하는 관리자들이 걱정하는" 제시카 리와 나머지 모든 숙련된 소프트웨어 개발자들에게 이 말로 끝을 맺고 싶다. “기계가 소프트웨어 개발의 지루한 작업을 더 많이 맡게 되면서 진정으로 사려 깊고 혁신적인 작업은 여러분과 같은 창의적이고 훌륭한 개발자가 영원히 수행할 것이다.”

 

https://www.itworld.co.kr/news/335871

 

블로그 | 개발자가 싫어하는 것

소프트웨어 개발이 쉽다고 생각하는 사람은 아무도 없지만, 이렇게나 다양한 방식으로 어려울 수 있다고 누가 생각했을까? 에반스 데이터에 따르면,

www.itworld.co.kr

 

반응형

+ Recent posts